1#![allow(clippy::wildcard_imports)]
2use super::*;
3
4impl ImapConnection {
5 pub async fn login(&self, user: &str, pass: &str, timeout: Duration) -> Result<(), Error> {
16 use super::dispatch::LoginConsumer;
17
18 {
20 let snap = self.state_rx.borrow();
21 if snap.session_state != SessionState::NotAuthenticated {
24 return Err(Error::Protocol(format!(
25 "command not valid in {:?} state (expected one of \
26 [{:?}])",
27 snap.session_state,
28 SessionState::NotAuthenticated,
29 )));
30 }
31 if snap
35 .capabilities
36 .iter()
37 .any(|c| matches!(c, Capability::LoginDisabled))
38 {
39 return Err(Error::Protocol(
40 "LOGIN disabled by server (LOGINDISABLED capability \
41 advertised, RFC 3501 Section 6.2.3)"
42 .into(),
43 ));
44 }
45
46 let has_rev2 = is_rev2_from_snapshot(&snap)
52 || snap
53 .capabilities
54 .iter()
55 .any(|c| matches!(c, Capability::Imap4Rev2));
56 let has_auth_plain = snap
57 .capabilities
58 .iter()
59 .any(|c| matches!(c, Capability::Auth(ref m) if m.eq_ignore_ascii_case("PLAIN")));
60 drop(snap);
61 if has_rev2 && has_auth_plain {
62 warn!(
63 "LOGIN is deprecated on IMAP4rev2 servers \
64 (RFC 9051 Section 2.2); prefer \
65 authenticate_plain() — the server advertises \
66 AUTH=PLAIN"
67 );
68 }
69 }
70
71 if !user.is_ascii() || !pass.is_ascii() {
74 return Err(Error::Protocol(
75 "LOGIN does not support non-ASCII credentials; \
76 use AUTHENTICATE (RFC 6855 Section 5)"
77 .into(),
78 ));
79 }
80
81 let deadline = tokio::time::Instant::now() + timeout;
82 let cmd = Command::Login {
83 user: user.to_owned(),
84 pass: pass.to_owned(),
85 };
86
87 let caps_provided =
88 tokio::time::timeout(timeout, self.submit_regular(cmd, LoginConsumer::default()))
89 .await
90 .map_err(|_| Error::Timeout)??;
91
92 self.complete_auth(caps_provided, deadline).await
93 }
94
95 pub async fn authenticate_plain(
100 &self,
101 user: &str,
102 pass: &str,
103 timeout: Duration,
104 ) -> Result<(), Error> {
105 use super::dispatch::AuthenticatePlainConsumer;
106 use base64::Engine;
107
108 let (encoded, has_sasl_ir) = {
110 let snap = self.state_rx.borrow();
111 if snap.session_state != SessionState::NotAuthenticated {
112 return Err(Error::Protocol(format!(
113 "command not valid in {:?} state (expected one of \
114 [{:?}])",
115 snap.session_state,
116 SessionState::NotAuthenticated,
117 )));
118 }
119 if !snap
122 .capabilities
123 .contains(&Capability::Auth("PLAIN".into()))
124 {
125 return Err(Error::MissingCapability("AUTH=PLAIN".into()));
126 }
127
128 let mut payload = Vec::with_capacity(1 + user.len() + 1 + pass.len());
131 payload.push(b'\0');
132 payload.extend_from_slice(user.as_bytes());
133 payload.push(b'\0');
134 payload.extend_from_slice(pass.as_bytes());
135 let encoded = base64::engine::general_purpose::STANDARD.encode(&payload);
136
137 let has_sasl_ir =
138 snap.capabilities.contains(&Capability::SaslIr) || is_rev2_from_snapshot(&snap);
139 drop(snap);
140 (encoded, has_sasl_ir)
141 };
142
143 let cmd = Command::Authenticate {
144 mechanism: "PLAIN".to_owned(),
145 initial_response: if has_sasl_ir {
146 Some(encoded.clone())
147 } else {
148 None
149 },
150 };
151
152 let consumer = AuthenticatePlainConsumer::new(encoded, has_sasl_ir);
153 let deadline = tokio::time::Instant::now() + timeout;
154 let caps_provided =
155 tokio::time::timeout(timeout, self.submit_with_continuations(cmd, consumer))
156 .await
157 .map_err(|_| Error::Timeout)??;
158
159 self.complete_auth(caps_provided, deadline).await
160 }
161
162 pub async fn authenticate_xoauth2(
167 &self,
168 user: &str,
169 token: &str,
170 timeout: Duration,
171 ) -> Result<(), Error> {
172 use super::dispatch::AuthenticateXoauth2Consumer;
173 use base64::Engine;
174
175 let (encoded, has_sasl_ir) = {
177 let snap = self.state_rx.borrow();
178 if snap.session_state != SessionState::NotAuthenticated {
179 return Err(Error::Protocol(format!(
180 "command not valid in {:?} state (expected one of \
181 [{:?}])",
182 snap.session_state,
183 SessionState::NotAuthenticated,
184 )));
185 }
186 if !snap
189 .capabilities
190 .contains(&Capability::Auth("XOAUTH2".into()))
191 {
192 return Err(Error::MissingCapability("AUTH=XOAUTH2".into()));
193 }
194
195 let payload = format!("user={user}\x01auth=Bearer {token}\x01\x01");
198 let encoded = base64::engine::general_purpose::STANDARD.encode(payload.as_bytes());
199
200 let has_sasl_ir =
201 snap.capabilities.contains(&Capability::SaslIr) || is_rev2_from_snapshot(&snap);
202 drop(snap);
203 (encoded, has_sasl_ir)
204 };
205
206 let cmd = Command::Authenticate {
207 mechanism: "XOAUTH2".to_owned(),
208 initial_response: if has_sasl_ir {
209 Some(encoded.clone())
210 } else {
211 None
212 },
213 };
214
215 let consumer = AuthenticateXoauth2Consumer::new(encoded, has_sasl_ir);
216 let deadline = tokio::time::Instant::now() + timeout;
217 let caps_provided =
218 tokio::time::timeout(timeout, self.submit_with_continuations(cmd, consumer))
219 .await
220 .map_err(|_| Error::Timeout)??;
221
222 self.complete_auth(caps_provided, deadline).await
223 }
224
225 async fn complete_auth(
237 &self,
238 caps_provided: bool,
239 deadline: tokio::time::Instant,
240 ) -> Result<(), Error> {
241 if !caps_provided {
242 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
243 if remaining.is_zero() {
244 return Err(Error::Timeout);
245 }
246 tokio::time::timeout(remaining, self.fetch_capabilities_via_driver())
251 .await
252 .map_err(|_| Error::Timeout)??;
253 }
254 Ok(())
255 }
256
257 async fn fetch_capabilities_via_driver(&self) -> Result<(), Error> {
263 use super::dispatch::CapabilityConsumer;
264
265 let _caps: Vec<Capability> = self
266 .submit_regular(Command::Capability, CapabilityConsumer::default())
267 .await?;
268 Ok(())
269 }
270
271 pub async fn unauthenticate(&self, timeout: Duration) -> Result<(), Error> {
297 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
299
300 {
302 let snap = self.state_rx.borrow();
303 if !snap.capabilities.contains(&Capability::Unauthenticate) {
304 return Err(Error::MissingCapability("UNAUTHENTICATE".into()));
305 }
306 }
307
308 let deadline = tokio::time::Instant::now() + timeout;
309
310 tokio::time::timeout(
311 timeout,
312 self.submit_regular(
313 Command::Unauthenticate,
314 super::dispatch::TaggedOkConsumer::default(),
315 ),
316 )
317 .await
318 .map_err(|_| Error::Timeout)??;
319
320 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
324 if remaining.is_zero() {
325 return Err(Error::Timeout);
326 }
327 tokio::time::timeout(remaining, self.fetch_capabilities_via_driver())
328 .await
329 .map_err(|_| Error::Timeout)??;
330
331 Ok(())
332 }
333
334 pub async fn logout(&self) -> Result<(), Error> {
348 use super::dispatch::LogoutConsumer;
349
350 if self.state_rx.borrow().session_state == SessionState::Logout {
352 return Ok(());
353 }
354
355 let _: () = self
356 .submit_regular(Command::Logout, LogoutConsumer::default())
357 .await?;
358 Ok(())
359 }
360
361 pub(super) async fn submit_regular<C: super::dispatch::Consumer + 'static>(
368 &self,
369 cmd: Command,
370 consumer: C,
371 ) -> Result<C::Output, Error>
372 where
373 C::Output: 'static,
374 {
375 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
376 let dcmd = driver::DriverCommand::Run {
377 payload: driver::DriverCommandPayload::Standard(cmd),
378 consumer: driver::DriverConsumer::Regular(
379 Box::new(consumer) as Box<dyn driver::ConsumerErased>
380 ),
381 result_tx,
382 };
383 if self.cmd_tx.send(dcmd).await.is_err() {
384 return Err(self.observe_driver_panic().await);
385 }
386 let result = match result_rx.await {
387 Ok(inner) => inner?,
388 Err(_) => return Err(self.observe_driver_panic().await),
389 };
390 let output = *result
391 .downcast::<C::Output>()
392 .map_err(|_| Error::Internal("type mismatch in driver result".into()))?;
393 Ok(output)
394 }
395
396 pub(super) async fn submit_with_continuations<
402 C: super::dispatch::ContinuationConsumer + 'static,
403 >(
404 &self,
405 cmd: Command,
406 consumer: C,
407 ) -> Result<C::Output, Error>
408 where
409 C::Output: 'static,
410 {
411 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
412 let dcmd = driver::DriverCommand::Run {
413 payload: driver::DriverCommandPayload::Standard(cmd),
414 consumer: driver::DriverConsumer::WithContinuations(
415 Box::new(consumer) as Box<dyn driver::ContinuationConsumerErased>
416 ),
417 result_tx,
418 };
419 if self.cmd_tx.send(dcmd).await.is_err() {
420 return Err(self.observe_driver_panic().await);
421 }
422 let result = match result_rx.await {
423 Ok(inner) => inner?,
424 Err(_) => return Err(self.observe_driver_panic().await),
425 };
426 let output = *result
427 .downcast::<C::Output>()
428 .map_err(|_| Error::Internal("type mismatch in driver result".into()))?;
429 Ok(output)
430 }
431
432 pub(super) async fn submit_prebuilt<C: super::dispatch::Consumer + 'static>(
439 &self,
440 wire_bytes: bytes::BytesMut,
441 tag: String,
442 cmd_kind: crate::types::CommandKind,
443 cmd_target: Option<crate::types::validated::MailboxName>,
444 consumer: C,
445 ) -> Result<C::Output, Error>
446 where
447 C::Output: 'static,
448 {
449 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
450 let dcmd = driver::DriverCommand::Run {
451 payload: driver::DriverCommandPayload::PreBuilt {
452 wire_bytes,
453 tag,
454 cmd_kind,
455 cmd_target,
456 },
457 consumer: driver::DriverConsumer::Regular(
458 Box::new(consumer) as Box<dyn driver::ConsumerErased>
459 ),
460 result_tx,
461 };
462 if self.cmd_tx.send(dcmd).await.is_err() {
463 return Err(self.observe_driver_panic().await);
464 }
465 let result = match result_rx.await {
466 Ok(inner) => inner?,
467 Err(_) => return Err(self.observe_driver_panic().await),
468 };
469 let output = *result
470 .downcast::<C::Output>()
471 .map_err(|_| Error::Internal("type mismatch in driver result".into()))?;
472 Ok(output)
473 }
474
475 pub(super) async fn submit_upgrade(
481 &self,
482 payload: driver::UpgradePayload,
483 ) -> Result<(), Error> {
484 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
485 let dcmd = driver::DriverCommand::Upgrade { payload, result_tx };
486 if self.cmd_tx.send(dcmd).await.is_err() {
487 return Err(self.observe_driver_panic().await);
488 }
489 match result_rx.await {
490 Ok(inner) => {
491 inner?;
492 Ok(())
493 }
494 Err(_) => Err(self.observe_driver_panic().await),
495 }
496 }
497}
498
499pub(super) fn is_rev2_from_snapshot(snap: &driver::ConnectionStateSnapshot) -> bool {
505 let has_rev2 = snap.capabilities.contains(&Capability::Imap4Rev2);
506 let has_rev1 = snap.capabilities.contains(&Capability::Imap4Rev1);
507 if has_rev2 && has_rev1 {
508 snap.enabled
511 .iter()
512 .any(|e| e.eq_ignore_ascii_case("IMAP4rev2"))
513 } else {
514 has_rev2
515 }
516}