Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/imap/client.rs

82.6 KiB, 270 runs

created by r1870400018:13340, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Client-side IMAP4rev1 (RFC 3501) for reading a remote mailbox.
2//!
3//! The counterpart to [`crate::imap::server`]: where that serves a local
4//! Maildir to a mail reader, this connects *out* to somebody else's IMAP
5//! server and pulls messages down. It is what a program needs in order to
6//! treat a hosted mailbox (Gmail, Fastmail, a corporate Dovecot) as a
7//! source of raw RFC 5322 messages.
8//!
9//! What it does:
10//!
11//! - Connects with implicit TLS (the 993 case), STARTTLS (143), or plain.
12//! - `LOGIN` with a password, or `AUTHENTICATE XOAUTH2` with a bearer
13//! token, whichever the account requires.
14//! - `CAPABILITY`, `LIST`, `SELECT`/`EXAMINE`, `UID SEARCH`, `UID FETCH`,
15//! `UID STORE`, `APPEND`, `LOGOUT`.
16//!
17//! What it does not do: `IDLE`, `CONDSTORE`, `QRESYNC`, compression. A
18//! caller wanting to know what changed polls, which is what a caller
19//! without a long-lived socket has to do anyway.
20//!
21//! The awkward part of IMAP is the wire format, and it is handled once,
22//! here. A response is a line, except when it is a line with a literal
23//! (`{1234}` followed by exactly that many raw bytes, which may contain
24//! anything at all including CRLF) spliced into the middle of it. So the
25//! reader assembles a *logical* line: the text with each literal lifted
26//! out into a side queue, which the tokeniser then puts back in order.
27//! Parsing the text as a line and hoping no message body contains a CRLF
28//! is the classic way to write an IMAP client that works until somebody
29//! sends you an attachment.
30//!
31//! # Example
32//!
33//! ```no_run
34//! # use oxedyne_fe2o3_core::prelude::*;
35//! # use oxedyne_fe2o3_net::imap::client::{FetchWhat, ImapClient, ImapConfig, Security};
36//! # async fn f() -> Outcome<()> {
37//! let cfg = ImapConfig::new("imap.example.com", 993, Security::ImplicitTls);
38//! let mut c = res!(ImapClient::connect(&cfg).await);
39//! res!(c.login("alice@example.com", "app-password").await);
40//! let inbox = res!(c.select("INBOX").await);
41//! let uids = res!(c.uid_search(&fmt!("UID {}:*", inbox.uid_next.saturating_sub(10))).await);
42//! for msg in res!(c.uid_fetch(&uids, FetchWhat::Full).await) {
43//! println!("{} bytes, flags {:?}", msg.body.len(), msg.flags);
44//! }
45//! res!(c.logout().await);
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
51//! Anthropic Claude
52
53use crate::tls::{
54 self,
55 ClientStream,
56};
57
58use oxedyne_fe2o3_core::prelude::*;
59
60use std::{
61 collections::VecDeque,
62 net::SocketAddr,
63 sync::Arc,
64 time::Duration,
65};
66
67use tokio::{
68 io::{
69 AsyncBufReadExt,
70 AsyncReadExt,
71 AsyncWriteExt,
72 BufReader,
73 },
74 net::TcpStream,
75 time::timeout,
76};
77use tokio_rustls::rustls::ClientConfig;
78
79
80// Generous, because a large `UID FETCH` against a slow mailbox is a legitimate
81// multi-second read.
82pub const IMAP_CLIENT_TIMEOUT: Duration = Duration::from_secs(60);
83
84// A server announcing a 4 GB literal is either broken or hostile, and either way
85// the client should not try to allocate for it.
86pub const MAX_LITERAL_BYTES: usize = 64 * 1024 * 1024;
87
88
89// ┌───────────────────────────────────────────────────────────────────────────┐
90// │ CONFIGURATION │
91// └───────────────────────────────────────────────────────────────────────────┘
92
93/// How the connection is protected.
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum Security {
96 ImplicitTls, // TLS from the first byte, the usual case on port 993
97 // In the clear, then `STARTTLS` before authenticating, on port 143. Where the
98 // upgrade fails the client refuses to send the credential.
99 StartTls,
100 Plain, // a test server on loopback, and nothing else
101}
102
103/// Where to connect and how.
104#[derive(Clone, Debug)]
105pub struct ImapConfig {
106 pub host: String, // also the name the certificate is validated against
107 pub port: u16, // conventionally 993 (implicit TLS) or 143 (STARTTLS)
108 pub security: Security,
109 pub timeout: Duration, // per IO
110 // Dialled instead of resolving `host`; the certificate is still validated against `host`, so
111 // pinning the address weakens nothing. A server connecting to a host its *user* named must
112 // resolve the name, satisfy itself the answer is somewhere it is willing to go (see
113 // crate::addr::resolve_public), and then connect to that address -- not re-resolve the name
114 // and hope for the same answer twice. This field is how it does the last part.
115 pub addr: Option<SocketAddr>,
116}
117
118impl ImapConfig {
119
120 /// The default timeout, and no pinned address.
121 pub fn new<S: Into<String>>(host: S, port: u16, security: Security) -> Self {
122 Self {
123 host: host.into(),
124 port,
125 security,
126 timeout: IMAP_CLIENT_TIMEOUT,
127 addr: None,
128 }
129 }
130
131 pub fn with_timeout(mut self, timeout: Duration) -> Self {
132 self.timeout = timeout;
133 self
134 }
135
136 pub fn with_addr(mut self, addr: SocketAddr) -> Self {
137 self.addr = Some(addr);
138 self
139 }
140}
141
142
143// ┌───────────────────────────────────────────────────────────────────────────┐
144// │ RESULT TYPES │
145// └───────────────────────────────────────────────────────────────────────────┘
146
147/// One mailbox as reported by `LIST`.
148#[derive(Clone, Debug)]
149pub struct MailboxInfo {
150 pub name: String, // as the server spells it
151 pub delimiter: Option<char>, // `None` for a flat namespace
152 pub attrs: Vec<String>, // e.g. `\HasChildren`, `\Noselect`, `\Sent`
153}
154
155impl MailboxInfo {
156 /// A mailbox that exists only to hold children cannot itself be selected.
157 pub fn selectable(&self) -> bool {
158 !self.attrs.iter().any(|a| a.eq_ignore_ascii_case("\\Noselect"))
159 }
160
161 /// The RFC 6154 special-use role, where the server declared one. This is how a client
162 /// recognises the Sent folder on a mailbox whose
163 /// names are localised -- Gmail spells it "[Gmail]/Gesendet" for a German
164 /// account, but the attribute is `\Sent` everywhere.
165 pub fn special_use(&self) -> Option<SpecialUse> {
166 for a in &self.attrs {
167 let role = match a.to_uppercase().as_str() {
168 "\\SENT" => Some(SpecialUse::Sent),
169 "\\DRAFTS" => Some(SpecialUse::Drafts),
170 "\\TRASH" => Some(SpecialUse::Trash),
171 "\\JUNK" => Some(SpecialUse::Junk),
172 "\\ARCHIVE" => Some(SpecialUse::Archive),
173 "\\ALL" => Some(SpecialUse::All),
174 "\\FLAGGED" => Some(SpecialUse::Flagged),
175 _ => None,
176 };
177 if role.is_some() {
178 return role;
179 }
180 }
181 None
182 }
183}
184
185/// The special-use roles of RFC 6154, one per mailbox at most.
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub enum SpecialUse {
188 Sent,
189 Drafts,
190 Trash, // deleted messages await expunge
191 Junk, // spam
192 Archive, // filed out of the inbox
193 All, // every message however filed, Gmail's "All Mail"
194 Flagged, // starred or flagged
195}
196
197/// The state of a mailbox after `SELECT` or `EXAMINE`.
198///
199/// `uid_validity` is the one field a synchronising caller must persist:
200/// if it changes, every UID it has cached is meaningless and the mailbox
201/// must be re-read from scratch.
202#[derive(Clone, Debug, Default)]
203pub struct MailboxStatus {
204 pub name: String,
205 pub exists: u32, // messages present
206 pub recent: u32, // messages flagged `\Recent`
207 pub uid_validity: u32, // UID namespace generation
208 pub uid_next: u32, // the UID the next arrival will be given
209 pub flags: Vec<String>, // defined in this mailbox
210 pub read_only: bool, // `EXAMINE`, or a server that downgraded the `SELECT`
211}
212
213/// How much of each message to pull down.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum FetchWhat {
216 Meta, // UID, flags, internal date, size; no body at all
217 Headers, // and the RFC 5322 header block
218 Full, // and the whole message
219}
220
221impl FetchWhat {
222 /// `BODY.PEEK` rather than `BODY`, so reading a message does not silently mark it `\Seen` --
223 /// a sync should be invisible to whoever is reading the mailbox elsewhere.
224 fn items(&self) -> &'static str {
225 match self {
226 Self::Meta => "(UID FLAGS INTERNALDATE RFC822.SIZE)",
227 Self::Headers => "(UID FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER])",
228 Self::Full => "(UID FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[])",
229 }
230 }
231}
232
233/// One message returned by `UID FETCH`.
234#[derive(Clone, Debug, Default)]
235pub struct FetchedMessage {
236 pub seq: u32, // sequence number in the current mailbox view
237 pub uid: u32, // stable within the mailbox's current `uid_validity`
238 pub flags: Vec<String>, // e.g. `\Seen`, `\Answered`
239 // `INTERNALDATE` exactly as the server gave it, e.g. `01-Jan-2026 09:15:00 +0000`. Left as
240 // the server's string, because only the caller knows what calendar it wants it in.
241 pub internal_date: String,
242 pub size: u32, // `RFC822.SIZE`, which may exceed `body.len()`
243 pub body: Vec<u8>, // whole message, header block, or empty, per FetchWhat
244}
245
246/// What a `UID STORE` does to the named flags.
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248pub enum FlagOp {
249 Add, // leaving the others alone
250 Remove, // leaving the others alone
251 Set, // replacing the flag set entirely
252}
253
254impl FlagOp {
255 /// `.SILENT` throughout: the client already knows what it asked for, and the untagged FETCH
256 /// the server would otherwise send back is a round trip spent on nothing.
257 fn item(&self) -> &'static str {
258 match self {
259 Self::Add => "+FLAGS.SILENT",
260 Self::Remove => "-FLAGS.SILENT",
261 Self::Set => "FLAGS.SILENT",
262 }
263 }
264}
265
266/// The completion status of a tagged command.
267#[derive(Clone, Copy, Debug, Eq, PartialEq)]
268enum Status {
269 Ok,
270 No, // refused
271 Bad, // not understood
272}
273
274/// One logical response line: the text, with every literal lifted out
275/// into `literals` in the order it appeared.
276#[derive(Clone, Debug)]
277struct RawLine {
278 text: String, // each literal appears only as its `{n}` marker
279 literals: Vec<Vec<u8>>, // the payloads, in order
280}
281
282/// Everything one tagged command produced. Only a successful command
283/// yields one: a `NO` or a `BAD` becomes an error carrying the server's
284/// own words, so there is no status to carry here.
285#[derive(Clone, Debug)]
286struct Response {
287 untagged: Vec<RawLine>, // the `*` lines that arrived before completion
288 text: String, // the completion line, which may carry a response code
289}
290
291
292// ┌───────────────────────────────────────────────────────────────────────────┐
293// │ CLIENT │
294// └───────────────────────────────────────────────────────────────────────────┘
295
296/// A connected IMAP client. One connection, one mailbox selected at a
297/// time, exactly as the protocol is.
298pub struct ImapClient {
299 // Buffered, because literal reads need byte precision. An `Option` only so that a STARTTLS
300 // upgrade can move the socket out, wrap it and put it back; it is `None` for no other reason,
301 // and any use of a `None` stream is a failed upgrade and a dead connection.
302 stream: Option<BufReader<ClientStream>>,
303 tag: u32, // monotonic, never reused
304 caps: Vec<String>, // as last advertised, upper-cased
305 timeout: Duration, // per IO, from the config
306 host: String, // kept for error messages and the TLS upgrade
307}
308
309impl ImapClient {
310
311 /// Protects the connection as configured and reads the greeting. Does not authenticate.
312 pub async fn connect(cfg: &ImapConfig) -> Outcome<Self> {
313 let tls_cfg = Arc::new(res!(tls::default_client_config()));
314 Self::connect_with(cfg, tls_cfg).await
315 }
316
317 /// Connect using a caller-supplied rustls config, for a private CA or
318 /// a pinned root.
319 pub async fn connect_with(
320 cfg: &ImapConfig,
321 tls_cfg: Arc<ClientConfig>,
322 )
323 -> Outcome<Self>
324 {
325 let addr = match cfg.addr {
326 Some(a) => a.to_string(),
327 None => fmt!("{}:{}", cfg.host, cfg.port),
328 };
329 let plain = match timeout(cfg.timeout, TcpStream::connect(&addr)).await {
330 Ok(Ok(s)) => s,
331 Ok(Err(e)) => return Err(err!(e,
332 "Connecting to IMAP server {}.", addr;
333 IO, Network)),
334 Err(_) => return Err(err!(
335 "Timeout connecting to IMAP server {}.", addr;
336 IO, Network, Timeout)),
337 };
338
339 let stream = match cfg.security {
340 Security::ImplicitTls =>
341 res!(tls::upgrade(plain, &cfg.host, tls_cfg.clone(), cfg.timeout).await),
342 Security::StartTls | Security::Plain =>
343 ClientStream::Plain(plain),
344 };
345
346 let mut client = Self {
347 stream: Some(BufReader::new(stream)),
348 tag: 0,
349 caps: Vec::new(),
350 timeout: cfg.timeout,
351 host: cfg.host.clone(),
352 };
353
354 // The greeting: an untagged OK, PREAUTH or BYE. It may carry a
355 // CAPABILITY list, saving a round trip.
356 let greeting = res!(client.read_line().await);
357 let up = greeting.text.to_uppercase();
358 if up.starts_with("* BYE") {
359 return Err(err!(
360 "IMAP server {} refused the connection: {}", cfg.host, greeting.text;
361 IO, Network, Wire));
362 }
363 if !up.starts_with("* OK") && !up.starts_with("* PREAUTH") {
364 return Err(err!(
365 "Expected an IMAP greeting from {}, got: {}", cfg.host, greeting.text;
366 IO, Network, Wire));
367 }
368 client.absorb_capabilities(&greeting.text);
369
370 if cfg.security == Security::StartTls {
371 res!(client.starttls(tls_cfg).await);
372 }
373 if client.caps.is_empty() {
374 res!(client.capability().await);
375 }
376 Ok(client)
377 }
378
379 /// Upgrade a plain connection in place. The credentials have not been
380 /// sent yet, and if the upgrade fails they never will be.
381 async fn starttls(&mut self, tls_cfg: Arc<ClientConfig>) -> Outcome<()> {
382 if self.caps.is_empty() {
383 res!(self.capability().await);
384 }
385 if !self.has_cap("STARTTLS") {
386 return Err(err!(
387 "IMAP server {} does not offer STARTTLS, and the connection \
388 is not otherwise protected.", self.host;
389 Security, Unimplemented));
390 }
391 res!(self.command("STARTTLS").await);
392
393 // Anything the server pipelined behind its STARTTLS response is
394 // sitting in the read buffer, unprotected, and would be indis-
395 // tinguishable from what the TLS peer says next. That is a
396 // downgrade attack (RFC 2595 §3.1), so refuse rather than discard.
397 let buffered = res!(self.take_stream());
398 if !buffered.buffer().is_empty() {
399 return Err(err!(
400 "IMAP server {} sent data after its STARTTLS response, before \
401 the handshake. Refusing to continue.", self.host;
402 IO, Network, Security));
403 }
404 let plain = match buffered.into_inner().into_plain() {
405 Some(s) => s,
406 None => return Err(err!(
407 "STARTTLS issued on an already-protected connection.";
408 Invalid, Bug)),
409 };
410 let upgraded = res!(tls::upgrade(plain, &self.host, tls_cfg, self.timeout).await);
411 self.stream = Some(BufReader::new(upgraded));
412
413 // Capabilities before and after TLS are allowed to differ, and the
414 // pre-TLS set must not be trusted: re-ask.
415 self.caps.clear();
416 res!(self.capability().await);
417 Ok(())
418 }
419
420 /// The live stream, or an error if a failed upgrade has left none.
421 fn stream_mut(&mut self) -> Outcome<&mut BufReader<ClientStream>> {
422 match self.stream.as_mut() {
423 Some(s) => Ok(s),
424 None => Err(err!(
425 "The IMAP connection to {} was dropped by a failed TLS \
426 upgrade.", self.host;
427 IO, Network, Missing)),
428 }
429 }
430
431 /// Move the stream out, for the one operation that needs to own it.
432 fn take_stream(&mut self) -> Outcome<BufReader<ClientStream>> {
433 match self.stream.take() {
434 Some(s) => Ok(s),
435 None => Err(err!(
436 "The IMAP connection to {} was dropped by a failed TLS \
437 upgrade.", self.host;
438 IO, Network, Missing)),
439 }
440 }
441
442 /// A refresh replaces rather than accumulates: the set a server offers
443 /// legitimately changes across a STARTTLS or a login, and a stale
444 /// entry left in the list is a capability the client believes in and
445 /// the server has withdrawn.
446 pub async fn capability(&mut self) -> Outcome<&[String]> {
447 let resp = res!(self.command("CAPABILITY").await);
448 let mut caps: Vec<String> = Vec::new();
449 for line in &resp.untagged {
450 caps.extend(parse_capabilities(&line.text));
451 }
452 caps.sort();
453 caps.dedup();
454 self.caps = caps;
455 Ok(&self.caps)
456 }
457
458 /// Does the server advertise this capability? Compared case-insensitively.
459 pub fn has_cap(&self, cap: &str) -> bool {
460 let want = cap.to_uppercase();
461 self.caps.iter().any(|c| *c == want)
462 }
463
464 /// For a consumer mailbox the password is an app password, not the account password.
465 pub async fn login(&mut self, user: &str, pass: &str) -> Outcome<()> {
466 if self.has_cap("LOGINDISABLED") {
467 return Err(err!(
468 "IMAP server {} has disabled password login on this \
469 connection.", self.host;
470 Unauthorised, Security));
471 }
472 let cmd = fmt!("LOGIN {} {}", quoted(user), quoted(pass));
473 // The password is in the command, so keep it out of any error.
474 let resp = res!(self.command_hushed(&cmd, "LOGIN").await);
475 self.absorb_capabilities_from(&resp);
476 if self.caps.is_empty() {
477 res!(self.capability().await);
478 }
479 Ok(())
480 }
481
482 /// SASL `XOAUTH2`, the mechanism the large providers require of a registered application.
483 pub async fn authenticate_xoauth2(&mut self, user: &str, token: &str) -> Outcome<()> {
484 if !self.has_cap("AUTH=XOAUTH2") {
485 return Err(err!(
486 "IMAP server {} does not offer XOAUTH2.", self.host;
487 Unimplemented, Mismatch));
488 }
489 let raw = fmt!("user={}\u{1}auth=Bearer {}\u{1}\u{1}", user, token);
490 let cmd = fmt!("AUTHENTICATE XOAUTH2 {}", base64::encode(raw.as_bytes()));
491 let resp = res!(self.command_hushed(&cmd, "AUTHENTICATE XOAUTH2").await);
492 self.absorb_capabilities_from(&resp);
493 if self.caps.is_empty() {
494 res!(self.capability().await);
495 }
496 Ok(())
497 }
498
499 /// List mailboxes under `reference` matching `pattern` (`"*"` for all,
500 /// `"%"` for one level).
501 pub async fn list(&mut self, reference: &str, pattern: &str) -> Outcome<Vec<MailboxInfo>> {
502 // Ask for the RFC 6154 roles when the server offers them, so a
503 // localised "[Gmail]/Gesendet" still carries `\Sent`. A server
504 // without the capability gets the plain command it expects.
505 let cmd = if self.has_cap("SPECIAL-USE") {
506 fmt!("LIST {} {} RETURN (SPECIAL-USE)", quoted(reference), quoted(pattern))
507 } else {
508 fmt!("LIST {} {}", quoted(reference), quoted(pattern))
509 };
510 let resp = res!(self.command(&cmd).await);
511 let mut out = Vec::new();
512 for line in &resp.untagged {
513 if let Some(mb) = res!(parse_list_line(line)) {
514 out.push(mb);
515 }
516 }
517 Ok(out)
518 }
519
520 pub async fn select(&mut self, mailbox: &str) -> Outcome<MailboxStatus> {
521 self.select_impl(mailbox, false).await
522 }
523
524 /// Read-only, so nothing the client does can change a flag. The safe choice for a sync that
525 /// must not disturb the mailbox.
526 pub async fn examine(&mut self, mailbox: &str) -> Outcome<MailboxStatus> {
527 self.select_impl(mailbox, true).await
528 }
529
530 async fn select_impl(&mut self, mailbox: &str, read_only: bool) -> Outcome<MailboxStatus> {
531 let verb = if read_only { "EXAMINE" } else { "SELECT" };
532 let cmd = fmt!("{} {}", verb, quoted(mailbox));
533 let resp = res!(self.command(&cmd).await);
534
535 let mut st = MailboxStatus {
536 name: mailbox.to_string(),
537 read_only,
538 ..Default::default()
539 };
540 for line in &resp.untagged {
541 res!(absorb_select_line(&mut st, &line.text));
542 }
543 // A server may downgrade a SELECT to read-only, and says so in the
544 // completion line's response code.
545 if resp.text.to_uppercase().contains("[READ-ONLY]") {
546 st.read_only = true;
547 }
548 Ok(st)
549 }
550
551 /// `criteria` is the raw IMAP search key, e.g. `ALL`, `UID 1234:*`, `UNSEEN`, or
552 /// `SINCE 01-Jan-2026`.
553 pub async fn uid_search(&mut self, criteria: &str) -> Outcome<Vec<u32>> {
554 let cmd = fmt!("UID SEARCH {}", criteria);
555 let resp = res!(self.command(&cmd).await);
556 let mut uids: Vec<u32> = Vec::new();
557 for line in &resp.untagged {
558 let up = line.text.to_uppercase();
559 if !up.starts_with("* SEARCH") { continue; }
560 for tok in line.text.split_whitespace().skip(2) {
561 if let Ok(n) = tok.parse::<u32>() {
562 uids.push(n);
563 }
564 }
565 }
566 uids.sort_unstable();
567 uids.dedup();
568 Ok(uids)
569 }
570
571 /// An empty `uids` is a no-op rather than a command, because `UID FETCH ` with no set is a
572 /// syntax error and a caller passing an empty search result is not doing anything wrong.
573 pub async fn uid_fetch(
574 &mut self,
575 uids: &[u32],
576 what: FetchWhat,
577 )
578 -> Outcome<Vec<FetchedMessage>>
579 {
580 if uids.is_empty() {
581 return Ok(Vec::new());
582 }
583 let cmd = fmt!("UID FETCH {} {}", uid_set(uids), what.items());
584 let resp = res!(self.command(&cmd).await);
585 let mut out: Vec<FetchedMessage> = Vec::new();
586 for line in &resp.untagged {
587 if let Some(msg) = res!(parse_fetch_line(line)) {
588 out.push(msg);
589 }
590 }
591 Ok(out)
592 }
593
594 /// `None` where the server does not return it: expunged between the search and the fetch,
595 /// which is a race the caller cannot prevent and should not treat as an error.
596 pub async fn uid_fetch_one(&mut self, uid: u32) -> Outcome<Option<FetchedMessage>> {
597 let mut msgs = res!(self.uid_fetch(&[uid], FetchWhat::Full).await);
598 Ok(if msgs.is_empty() { None } else { Some(msgs.remove(0)) })
599 }
600
601 pub async fn uid_store_flags(
602 &mut self,
603 uids: &[u32],
604 op: FlagOp,
605 flags: &[&str],
606 )
607 -> Outcome<()>
608 {
609 if uids.is_empty() {
610 return Ok(());
611 }
612 let cmd = fmt!("UID STORE {} {} ({})",
613 uid_set(uids), op.item(), flags.join(" "));
614 res!(self.command(&cmd).await);
615 Ok(())
616 }
617
618 /// `body` is a raw RFC 5322 message: this is how a sent message is filed in `Sent` after
619 /// SMTP has delivered it.
620 pub async fn append(
621 &mut self,
622 mailbox: &str,
623 flags: &[&str],
624 body: &[u8],
625 )
626 -> Outcome<()>
627 {
628 let tag = self.next_tag();
629 let flag_part = if flags.is_empty() {
630 String::new()
631 } else {
632 fmt!(" ({})", flags.join(" "))
633 };
634 let cmd = fmt!("{} APPEND {}{} {{{}}}\r\n",
635 tag, quoted(mailbox), flag_part, body.len());
636 res!(self.write_all(cmd.as_bytes()).await);
637
638 // The server must answer a synchronising literal with a `+`
639 // continuation before the bytes may be sent.
640 let cont = res!(self.read_line().await);
641 if !cont.text.starts_with('+') {
642 return Err(err!(
643 "APPEND to '{}' was refused: {}", mailbox, cont.text;
644 IO, Network, Wire));
645 }
646 res!(self.write_all(body).await);
647 res!(self.write_all(b"\r\n").await);
648
649 let resp = res!(self.read_until_tag(&tag, "APPEND").await);
650 let _ = resp;
651 Ok(())
652 }
653
654 pub async fn logout(&mut self) -> Outcome<()> {
655 res!(self.command("LOGOUT").await);
656 if let Some(s) = self.stream.as_mut() {
657 let _ = s.get_mut().shutdown().await;
658 }
659 Ok(())
660 }
661
662 // ── Command plumbing ─────────────────────────────────────────
663
664 fn next_tag(&mut self) -> String {
665 self.tag += 1;
666 fmt!("a{:04}", self.tag)
667 }
668
669 /// A `NO` or a `BAD` is an error carrying the server's own words, which are usually the most
670 /// useful thing anyone will say about the failure.
671 async fn command(&mut self, cmd: &str) -> Outcome<Response> {
672 let verb = cmd.split_whitespace().next().unwrap_or(cmd).to_string();
673 self.command_hushed(cmd, &verb).await
674 }
675
676 /// As [`Self::command`], but naming the command in errors rather than
677 /// echoing it -- for commands whose text contains a credential.
678 async fn command_hushed(&mut self, cmd: &str, label: &str) -> Outcome<Response> {
679 let tag = self.next_tag();
680 let line = fmt!("{} {}\r\n", tag, cmd);
681 res!(self.write_all(line.as_bytes()).await);
682 self.read_until_tag(&tag, label).await
683 }
684
685 /// Read untagged lines until the one carrying `tag`, then judge it.
686 async fn read_until_tag(&mut self, tag: &str, label: &str) -> Outcome<Response> {
687 let mut untagged: Vec<RawLine> = Vec::new();
688 loop {
689 let line = res!(self.read_line().await);
690 if line.text.starts_with("* ") || line.text == "*" {
691 untagged.push(line);
692 continue;
693 }
694 if line.text.starts_with('+') {
695 return Err(err!(
696 "IMAP server asked for a literal in reply to {}, which \
697 sends none.", label;
698 IO, Network, Wire));
699 }
700 let rest = match line.text.strip_prefix(tag) {
701 Some(r) => r.trim_start(),
702 None => {
703 // A tag we did not send: the connection is out of step
704 // and nothing read after this can be trusted.
705 return Err(err!(
706 "IMAP response carried tag other than '{}': {}",
707 tag, line.text;
708 IO, Network, Wire));
709 }
710 };
711 let (status, text) = res!(parse_completion(rest));
712 return match status {
713 Status::Ok => Ok(Response { untagged, text }),
714 Status::No => Err(err!(
715 "IMAP server refused {}: {}", label, text;
716 IO, Network, Invalid)),
717 Status::Bad => Err(err!(
718 "IMAP server rejected {} as malformed: {}", label, text;
719 IO, Network, Wire)),
720 };
721 }
722 }
723
724 /// Merge any `[CAPABILITY ...]` response code carried on a command's
725 /// completion line into the cached set.
726 fn absorb_capabilities_from(&mut self, resp: &Response) {
727 let text = resp.text.clone();
728 self.absorb_capabilities(&text);
729 }
730
731 /// Pull a `CAPABILITY` list out of a line, whether it arrived as an
732 /// untagged `* CAPABILITY ...` or as a `[CAPABILITY ...]` response
733 /// code inside a greeting or completion.
734 fn absorb_capabilities(&mut self, text: &str) {
735 self.caps.extend(parse_capabilities(text));
736 self.caps.sort();
737 self.caps.dedup();
738 }
739
740 // ── Wire ─────────────────────────────────────────────────────
741
742 async fn write_all(&mut self, bytes: &[u8]) -> Outcome<()> {
743 let host = self.host.clone();
744 let deadline = self.timeout;
745 let w = res!(self.stream_mut()).get_mut();
746 match timeout(deadline, w.write_all(bytes)).await {
747 Ok(Ok(())) => (),
748 Ok(Err(e)) => return Err(err!(e,
749 "Writing to IMAP server {}.", host;
750 IO, Network, Write)),
751 Err(_) => return Err(err!(
752 "Timeout writing to IMAP server {}.", host;
753 IO, Network, Timeout)),
754 }
755 match timeout(deadline, w.flush()).await {
756 Ok(Ok(())) => Ok(()),
757 Ok(Err(e)) => Err(err!(e,
758 "Flushing to IMAP server {}.", host;
759 IO, Network, Write)),
760 Err(_) => Err(err!(
761 "Timeout flushing to IMAP server {}.", host;
762 IO, Network, Timeout)),
763 }
764 }
765
766 /// Read one *logical* response line: CRLF-terminated text, except that
767 /// a trailing `{n}` is a literal whose `n` raw bytes follow, after
768 /// which the line continues. The literals are lifted out; what returns
769 /// is the text with its `{n}` markers still in place, and the payloads
770 /// alongside in order.
771 async fn read_line(&mut self) -> Outcome<RawLine> {
772 let mut text = String::new();
773 let mut literals = Vec::new();
774 loop {
775 let chunk = res!(self.read_crlf_line().await);
776 text.push_str(&chunk);
777 let n = match trailing_literal_len(&chunk) {
778 Some(n) => n,
779 None => break,
780 };
781 if n > MAX_LITERAL_BYTES {
782 return Err(err!(
783 "IMAP server {} announced a {}-byte literal, over the \
784 {}-byte limit.", self.host, n, MAX_LITERAL_BYTES;
785 IO, Network, Excessive));
786 }
787 let mut buf = vec![0u8; n];
788 let host = self.host.clone();
789 let deadline = self.timeout;
790 let rd = res!(self.stream_mut());
791 match timeout(deadline, rd.read_exact(&mut buf)).await {
792 Ok(Ok(_)) => (),
793 Ok(Err(e)) => return Err(err!(e,
794 "Reading a {}-byte literal from IMAP server {}.", n, host;
795 IO, Network, Read)),
796 Err(_) => return Err(err!(
797 "Timeout reading a {}-byte literal from IMAP server {}.",
798 n, host;
799 IO, Network, Timeout)),
800 }
801 literals.push(buf);
802 }
803 Ok(RawLine { text, literals })
804 }
805
806 /// Without its terminator.
807 async fn read_crlf_line(&mut self) -> Outcome<String> {
808 let mut buf: Vec<u8> = Vec::with_capacity(256);
809 let host = self.host.clone();
810 let deadline = self.timeout;
811 let rd = res!(self.stream_mut());
812 let n = match timeout(deadline, rd.read_until(b'\n', &mut buf)).await {
813 Ok(Ok(n)) => n,
814 Ok(Err(e)) => return Err(err!(e,
815 "Reading from IMAP server {}.", host;
816 IO, Network, Read)),
817 Err(_) => return Err(err!(
818 "Timeout reading from IMAP server {}.", host;
819 IO, Network, Timeout)),
820 };
821 if n == 0 {
822 return Err(err!(
823 "IMAP server {} closed the connection.", host;
824 IO, Network, Read));
825 }
826 while buf.last() == Some(&b'\n') || buf.last() == Some(&b'\r') {
827 buf.pop();
828 }
829 // Response text is 7-bit ASCII plus, in practice, whatever a
830 // server puts in a mailbox name. Lossy is right: a malformed byte
831 // in a mailbox name must not fail the sync.
832 Ok(String::from_utf8_lossy(&buf).into_owned())
833 }
834}
835
836
837// ┌───────────────────────────────────────────────────────────────────────────┐
838// │ PARSING │
839// └───────────────────────────────────────────────────────────────────────────┘
840
841/// One element of an IMAP response.
842#[derive(Clone, Debug, Eq, PartialEq)]
843enum Tok {
844 Atom(String), // a bare word: a number, a flag, `NIL`, a `BODY[...]` item name
845 Quoted(String), // unescaped
846 Literal(Vec<u8>), // the payload, spliced back in from the reader's side queue
847 List(Vec<Tok>), // parenthesised
848}
849
850impl Tok {
851 /// An atom or a quoted string only.
852 fn as_str(&self) -> Option<&str> {
853 match self {
854 Self::Atom(s) | Self::Quoted(s) => Some(s),
855 _ => None,
856 }
857 }
858
859 /// Whether it arrived as a literal or a string. `NIL` comes back empty, which is what a
860 /// server means by it here.
861 fn as_bytes(&self) -> Vec<u8> {
862 match self {
863 Self::Literal(b) => b.clone(),
864 Self::Quoted(s) => s.as_bytes().to_vec(),
865 Self::Atom(s) => if s.eq_ignore_ascii_case("NIL") {
866 Vec::new()
867 } else {
868 s.as_bytes().to_vec()
869 },
870 Self::List(_) => Vec::new(),
871 }
872 }
873}
874
875/// Tokenise a response line, splicing each `{n}` marker back into the
876/// literal that followed it on the wire.
877fn tokenise(text: &str, literals: &[Vec<u8>]) -> Outcome<Vec<Tok>> {
878 let chars: Vec<char> = text.chars().collect();
879 let mut queue: VecDeque<Vec<u8>> = literals.iter().cloned().collect();
880 let mut pos = 0usize;
881 let toks = res!(tokenise_until(&chars, &mut pos, &mut queue, None));
882 Ok(toks)
883}
884
885/// Tokenise until `close` (or the end of the input when `close` is
886/// `None`). Recursive, because IMAP lists nest.
887fn tokenise_until(
888 chars: &[char],
889 pos: &mut usize,
890 queue: &mut VecDeque<Vec<u8>>,
891 close: Option<char>,
892)
893 -> Outcome<Vec<Tok>>
894{
895 let mut out: Vec<Tok> = Vec::new();
896 while *pos < chars.len() {
897 let c = chars[*pos];
898 if c.is_whitespace() {
899 *pos += 1;
900 continue;
901 }
902 if Some(c) == close {
903 *pos += 1;
904 return Ok(out);
905 }
906 match c {
907 '(' => {
908 *pos += 1;
909 let inner = res!(tokenise_until(chars, pos, queue, Some(')')));
910 out.push(Tok::List(inner));
911 }
912 ')' => {
913 // An unbalanced close: the caller wanted end-of-input.
914 return Err(err!(
915 "Unbalanced ')' in IMAP response at character {}.", pos;
916 Invalid, Input, Decode));
917 }
918 '"' => {
919 *pos += 1;
920 let mut s = String::new();
921 let mut closed = false;
922 while *pos < chars.len() {
923 let d = chars[*pos];
924 *pos += 1;
925 if d == '\\' && *pos < chars.len() {
926 s.push(chars[*pos]);
927 *pos += 1;
928 continue;
929 }
930 if d == '"' { closed = true; break; }
931 s.push(d);
932 }
933 if !closed {
934 return Err(err!(
935 "Unterminated quoted string in IMAP response.";
936 Invalid, Input, Decode));
937 }
938 out.push(Tok::Quoted(s));
939 }
940 '{' => {
941 // A literal marker. Its payload was read off the wire and
942 // is waiting in the queue, in order.
943 while *pos < chars.len() && chars[*pos] != '}' {
944 *pos += 1;
945 }
946 if *pos >= chars.len() {
947 return Err(err!(
948 "Unterminated literal marker in IMAP response.";
949 Invalid, Input, Decode));
950 }
951 *pos += 1; // past the '}'
952 match queue.pop_front() {
953 Some(bytes) => out.push(Tok::Literal(bytes)),
954 None => return Err(err!(
955 "IMAP response has more literal markers than \
956 literals were read.";
957 Invalid, Input, Decode)),
958 }
959 }
960 _ => {
961 // An atom, which may embed a bracketed section --
962 // `BODY[HEADER.FIELDS (FROM TO)]` is one token, spaces and
963 // parentheses and all.
964 let mut s = String::new();
965 let mut depth = 0usize;
966 while *pos < chars.len() {
967 let d = chars[*pos];
968 if depth == 0 {
969 if d.is_whitespace() || d == '(' || d == ')' { break; }
970 if Some(d) == close { break; }
971 }
972 if d == '[' { depth += 1; }
973 if d == ']' { depth = depth.saturating_sub(1); }
974 s.push(d);
975 *pos += 1;
976 }
977 out.push(Tok::Atom(s));
978 }
979 }
980 }
981 if close.is_some() {
982 return Err(err!(
983 "IMAP response ended inside a parenthesised list.";
984 Invalid, Input, Decode));
985 }
986 Ok(out)
987}
988
989/// If a line ends with a synchronising or non-synchronising literal
990/// marker (`{123}` or `{123+}`), the length it announces.
991fn trailing_literal_len(line: &str) -> Option<usize> {
992 let trimmed = line.trim_end();
993 if !trimmed.ends_with('}') { return None; }
994 let open = ok!(trimmed.rfind('{'));
995 let inner = &trimmed[open + 1..trimmed.len() - 1];
996 let digits = inner.strip_suffix('+').unwrap_or(inner);
997 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
998 return None;
999 }
1000 digits.parse::<usize>().ok()
1001}
1002
1003/// A completion line is `OK ...`, `NO ...` or `BAD ...`.
1004fn parse_completion(rest: &str) -> Outcome<(Status, String)> {
1005 let mut it = rest.splitn(2, char::is_whitespace);
1006 let word = it.next().unwrap_or("");
1007 let text = it.next().unwrap_or("").trim().to_string();
1008 let status = match word.to_uppercase().as_str() {
1009 "OK" => Status::Ok,
1010 "NO" => Status::No,
1011 "BAD" => Status::Bad,
1012 other => return Err(err!(
1013 "IMAP completion line has unknown status '{}'.", other;
1014 Invalid, Input, Decode)),
1015 };
1016 Ok((status, text))
1017}
1018
1019/// Pull capability names out of `* CAPABILITY ...` or a `[CAPABILITY ...]`
1020/// response code, upper-cased.
1021fn parse_capabilities(text: &str) -> Vec<String> {
1022 let up = text.to_uppercase();
1023 let body = if let Some(i) = up.find("[CAPABILITY ") {
1024 let start = i + "[CAPABILITY ".len();
1025 match up[start..].find(']') {
1026 Some(e) => &up[start..start + e],
1027 None => return Vec::new(),
1028 }
1029 } else if let Some(i) = up.find("* CAPABILITY ") {
1030 &up[i + "* CAPABILITY ".len()..]
1031 } else {
1032 return Vec::new();
1033 };
1034 body.split_whitespace().map(|s| s.to_string()).collect()
1035}
1036
1037/// Reads `* LIST (\HasNoChildren) "/" "INBOX"`; `None` for an untagged line that is not a
1038/// `LIST` reply.
1039fn parse_list_line(line: &RawLine) -> Outcome<Option<MailboxInfo>> {
1040 let up = line.text.to_uppercase();
1041 if !up.starts_with("* LIST") && !up.starts_with("* LSUB") {
1042 return Ok(None);
1043 }
1044 let toks = res!(tokenise(&line.text, &line.literals));
1045 // `*`, `LIST`, (attrs), delimiter, name
1046 if toks.len() < 5 {
1047 return Ok(None);
1048 }
1049 let attrs = match &toks[2] {
1050 Tok::List(items) => items.iter()
1051 .filter_map(|t| t.as_str().map(|s| s.to_string()))
1052 .collect(),
1053 _ => Vec::new(),
1054 };
1055 let delimiter = toks[3].as_str()
1056 .filter(|s| !s.eq_ignore_ascii_case("NIL"))
1057 .and_then(|s| s.chars().next());
1058 let name = String::from_utf8_lossy(&toks[4].as_bytes()).into_owned();
1059 if name.is_empty() {
1060 return Ok(None);
1061 }
1062 Ok(Some(MailboxInfo { name, delimiter, attrs }))
1063}
1064
1065/// Fold one untagged line of a `SELECT`/`EXAMINE` reply into the status.
1066/// Unrecognised lines are ignored -- a server is free to volunteer more
1067/// than the client asked for.
1068fn absorb_select_line(st: &mut MailboxStatus, text: &str) -> Outcome<()> {
1069 let up = text.to_uppercase();
1070 let parts: Vec<&str> = up.split_whitespace().collect();
1071
1072 // `* 42 EXISTS` / `* 3 RECENT`
1073 if parts.len() >= 3 && parts[0] == "*" {
1074 if let Ok(n) = parts[1].parse::<u32>() {
1075 match parts[2] {
1076 "EXISTS" => { st.exists = n; return Ok(()); }
1077 "RECENT" => { st.recent = n; return Ok(()); }
1078 _ => (),
1079 }
1080 }
1081 }
1082 // `* OK [UIDVALIDITY 1234]` / `* OK [UIDNEXT 5678]`
1083 if let Some(v) = bracket_value(&up, "UIDVALIDITY") {
1084 st.uid_validity = v;
1085 }
1086 if let Some(v) = bracket_value(&up, "UIDNEXT") {
1087 st.uid_next = v;
1088 }
1089 // `* FLAGS (\Answered \Flagged ...)`
1090 if up.starts_with("* FLAGS") {
1091 if let (Some(a), Some(b)) = (text.find('('), text.rfind(')')) {
1092 if b > a {
1093 st.flags = text[a + 1..b]
1094 .split_whitespace()
1095 .map(|s| s.to_string())
1096 .collect();
1097 }
1098 }
1099 }
1100 if up.contains("[READ-ONLY]") {
1101 st.read_only = true;
1102 }
1103 Ok(())
1104}
1105
1106/// The number inside a `[NAME 123]` response code, if present.
1107fn bracket_value(up: &str, name: &str) -> Option<u32> {
1108 let pat = fmt!("[{} ", name);
1109 let i = ok!(up.find(&pat));
1110 let start = i + pat.len();
1111 let end = ok!(up[start..].find(']'));
1112 up[start..start + end].trim().parse::<u32>().ok()
1113}
1114
1115/// Reads `* 12 FETCH (UID 345 FLAGS (\Seen) ... BODY[] {4523}...)`; `None` for an untagged line
1116/// that is not a `FETCH` reply.
1117fn parse_fetch_line(line: &RawLine) -> Outcome<Option<FetchedMessage>> {
1118 let toks = res!(tokenise(&line.text, &line.literals));
1119 if toks.len() < 3 {
1120 return Ok(None);
1121 }
1122 if toks[0].as_str() != Some("*") {
1123 return Ok(None);
1124 }
1125 match toks[1].as_str().map(|s| s.parse::<u32>()) {
1126 Some(Ok(_)) => (),
1127 _ => return Ok(None),
1128 }
1129 if !toks[2].as_str().map(|s| s.eq_ignore_ascii_case("FETCH")).unwrap_or(false) {
1130 return Ok(None);
1131 }
1132 let seq = match toks[1].as_str().and_then(|s| s.parse::<u32>().ok()) {
1133 Some(n) => n,
1134 None => return Ok(None),
1135 };
1136 let items = match toks.get(3) {
1137 Some(Tok::List(items)) => items,
1138 _ => return Err(err!(
1139 "IMAP FETCH reply has no data list: {}", line.text;
1140 Invalid, Input, Decode)),
1141 };
1142
1143 let mut msg = FetchedMessage { seq, ..Default::default() };
1144 let mut i = 0usize;
1145 while i < items.len() {
1146 let key = match items[i].as_str() {
1147 Some(s) => s.to_uppercase(),
1148 None => { i += 1; continue; }
1149 };
1150 let val = match items.get(i + 1) {
1151 Some(v) => v,
1152 None => break,
1153 };
1154 i += 2;
1155 match key.as_str() {
1156 "UID" => {
1157 if let Some(n) = val.as_str().and_then(|s| s.parse::<u32>().ok()) {
1158 msg.uid = n;
1159 }
1160 }
1161 "RFC822.SIZE" => {
1162 if let Some(n) = val.as_str().and_then(|s| s.parse::<u32>().ok()) {
1163 msg.size = n;
1164 }
1165 }
1166 "FLAGS" => {
1167 if let Tok::List(fs) = val {
1168 msg.flags = fs.iter()
1169 .filter_map(|t| t.as_str().map(|s| s.to_string()))
1170 .collect();
1171 }
1172 }
1173 "INTERNALDATE" => {
1174 if let Some(s) = val.as_str() {
1175 msg.internal_date = s.to_string();
1176 }
1177 }
1178 _ => {
1179 // Every body-ish item -- `BODY[]`, `BODY[HEADER]`,
1180 // `RFC822`, `RFC822.HEADER` -- carries the bytes we want,
1181 // and the first one that does wins. Anything else is an
1182 // item the caller did not ask for, and is skipped.
1183 if key.starts_with("BODY[") || key == "RFC822" || key == "RFC822.HEADER" {
1184 if msg.body.is_empty() {
1185 msg.body = val.as_bytes();
1186 }
1187 }
1188 }
1189 }
1190 }
1191 if msg.uid == 0 {
1192 // Without a UID the message cannot be addressed again, so it is
1193 // useless to a synchronising caller. A server that omits it after
1194 // being asked for it is broken.
1195 return Err(err!(
1196 "IMAP FETCH reply for sequence {} carried no UID.", seq;
1197 Invalid, Input, Missing));
1198 }
1199 Ok(Some(msg))
1200}
1201
1202/// Render a UID list as a compact IMAP sequence set, collapsing runs into
1203/// ranges: `[1,2,3,7,9,10]` becomes `1:3,7,9:10`. A mailbox synced after a
1204/// week away yields a set of thousands of consecutive UIDs, and a server
1205/// is entitled to reject a command line that long.
1206fn uid_set(uids: &[u32]) -> String {
1207 let mut sorted: Vec<u32> = uids.to_vec();
1208 sorted.sort_unstable();
1209 sorted.dedup();
1210
1211 let mut out = String::new();
1212 let mut i = 0usize;
1213 while i < sorted.len() {
1214 let start = sorted[i];
1215 let mut end = start;
1216 while i + 1 < sorted.len() && sorted[i + 1] == end + 1 {
1217 i += 1;
1218 end = sorted[i];
1219 }
1220 if !out.is_empty() { out.push(','); }
1221 if start == end {
1222 out.push_str(&fmt!("{}", start));
1223 } else {
1224 out.push_str(&fmt!("{}:{}", start, end));
1225 }
1226 i += 1;
1227 }
1228 out
1229}
1230
1231/// An IMAP quoted-string, with the two characters that need escaping escaped.
1232fn quoted(s: &str) -> String {
1233 let mut out = String::with_capacity(s.len() + 2);
1234 out.push('"');
1235 for c in s.chars() {
1236 if c == '"' || c == '\\' {
1237 out.push('\\');
1238 }
1239 out.push(c);
1240 }
1241 out.push('"');
1242 out
1243}
1244
1245// ┌───────────────────────────────────────────────────────────────────────────┐
1246// │ TESTS │
1247// └───────────────────────────────────────────────────────────────────────────┘
1248
1249#[cfg(test)]
1250mod tests {
1251 use super::*;
1252
1253 use std::sync::Mutex;
1254
1255 use tokio::net::TcpListener;
1256
1257 fn line(text: &str, lits: Vec<Vec<u8>>) -> RawLine {
1258 RawLine { text: text.to_string(), literals: lits }
1259 }
1260
1261
1262 // ┌───────────────────────────────────────────────────────────────────────┐
1263 // │ A SCRIPTED SERVER │
1264 // └───────────────────────────────────────────────────────────────────────┘
1265
1266 // The round-trip against fe2o3's own ImapServer lives in
1267 // fe2o3_mail/tests/imap_roundtrip.rs and proves the happy path. What it
1268 // cannot pose is a server behaving badly -- a BYE greeting, a literal larger
1269 // than the client will allocate for, a tag the client never sent, bytes
1270 // pipelined behind a STARTTLS response. Those are the cases where this client
1271 // either refuses or is quietly compromised, so they are scripted here.
1272
1273 const USER: &str = "alice@example.com";
1274 const PASS: &str = "app-password-not-a-real-one";
1275
1276 /// Every line the server was sent, in order, plus a `<literal N>` marker and
1277 /// its payload wherever the client sent one.
1278 type Transcript = Arc<Mutex<Vec<String>>>;
1279
1280 /// Serve `greeting`, then answer each command the client sends with the next
1281 /// entry of `script`, verbatim. `%T%` in an entry becomes the tag the client
1282 /// used on the command being answered, so a script need not track them.
1283 ///
1284 /// A command ending in a `{n}` literal marker is answered as scripted; where
1285 /// that answer begins with `+`, the `n` bytes and their CRLF are read off and
1286 /// logged before the next command. Running off the end of the script closes
1287 /// the connection, which is a legitimate thing for a server to do and one the
1288 /// client must not hang on.
1289 async fn scripted(
1290 greeting: &'static str,
1291 script: Vec<&'static str>,
1292 )
1293 -> Outcome<(SocketAddr, Transcript)>
1294 {
1295 let listener = res!(TcpListener::bind("127.0.0.1:0").await
1296 .map_err(|e| err!(e, "Binding the scripted IMAP server."; IO, Network)));
1297 let addr = res!(listener.local_addr()
1298 .map_err(|e| err!(e, "Reading its address."; IO, Network)));
1299
1300 let seen: Transcript = Arc::new(Mutex::new(Vec::new()));
1301 let log = seen.clone();
1302
1303 tokio::spawn(async move {
1304 let (sock, _) = match listener.accept().await {
1305 Ok(x) => x,
1306 Err(_) => return,
1307 };
1308 let (r, mut w) = sock.into_split();
1309 let mut rd = BufReader::new(r);
1310
1311 let _ = w.write_all(greeting.as_bytes()).await;
1312
1313 let mut script = script.into_iter();
1314 while let Some(reply) = script.next() {
1315 let mut buf: Vec<u8> = Vec::with_capacity(256);
1316 match rd.read_until(b'\n', &mut buf).await {
1317 Ok(0) | Err(_) => return,
1318 Ok(_) => (),
1319 }
1320 while buf.last() == Some(&b'\n') || buf.last() == Some(&b'\r') {
1321 buf.pop();
1322 }
1323 let cmd = String::from_utf8_lossy(&buf).into_owned();
1324 if let Ok(mut g) = log.lock() {
1325 g.push(cmd.clone());
1326 }
1327 let tag = cmd.split_whitespace().next().unwrap_or("").to_string();
1328 let out = reply.replace("%T%", &tag);
1329 let _ = w.write_all(out.as_bytes()).await;
1330
1331 // A synchronising literal the client is now entitled to send. It
1332 // is not a command, so the entry that follows goes out without
1333 // waiting for another line -- the client is waiting on the
1334 // completion and will send nothing until it has it.
1335 if out.starts_with('+') {
1336 if let Some(n) = trailing_literal_len(&cmd) {
1337 let mut lit = vec![0u8; n];
1338 if rd.read_exact(&mut lit).await.is_err() {
1339 return;
1340 }
1341 let mut tail = Vec::new();
1342 let _ = rd.read_until(b'\n', &mut tail).await;
1343 if let Ok(mut g) = log.lock() {
1344 g.push(fmt!("<literal {}>", n));
1345 g.push(String::from_utf8_lossy(&lit).into_owned());
1346 }
1347 match script.next() {
1348 Some(done) => {
1349 let _ = w.write_all(
1350 done.replace("%T%", &tag).as_bytes()).await;
1351 },
1352 None => return,
1353 }
1354 }
1355 }
1356 }
1357 });
1358
1359 Ok((addr, seen))
1360 }
1361
1362 fn cfg(addr: SocketAddr) -> ImapConfig {
1363 ImapConfig::new(fmt!("127.0.0.1"), addr.port(), Security::Plain)
1364 .with_addr(addr)
1365 .with_timeout(Duration::from_secs(10))
1366 }
1367
1368 fn lines_of(t: &Transcript) -> Outcome<Vec<String>> {
1369 match t.lock() {
1370 Ok(g) => Ok(g.clone()),
1371 Err(_) => Err(err!("The server's transcript was poisoned."; Lock, Poisoned)),
1372 }
1373 }
1374
1375 /// A greeting that already carries the capability list, which is the common
1376 /// case and saves a round trip.
1377 const GREET: &str = "* OK [CAPABILITY IMAP4rev1 SPECIAL-USE AUTH=PLAIN] ready\r\n";
1378
1379 // ── The greeting ──────────────────────────────────────────────
1380
1381 /// A `BYE` greeting is a refusal, and the server's own words are the most
1382 /// useful thing anybody will say about it.
1383 #[tokio::test]
1384 async fn test_a_bye_greeting_is_a_refusal_00() -> Outcome<()> {
1385 let (addr, _) = res!(scripted(
1386 "* BYE Too many connections from your IP\r\n", vec![]).await);
1387 let out = ImapClient::connect(&cfg(addr)).await;
1388 let msg = match out {
1389 Err(e) => fmt!("{}", e),
1390 Ok(_) => return Err(err!(
1391 "A BYE greeting was treated as a usable connection."; Test, Invalid)),
1392 };
1393 req!(true, msg.contains("Too many connections"),
1394 "the server's own words were dropped: {}", msg);
1395 Ok(())
1396 }
1397
1398 /// Anything that is not `OK` or `PREAUTH` is not an IMAP server, and talking
1399 /// on regardless is how a client sends a password to whatever answered.
1400 #[tokio::test]
1401 async fn test_a_greeting_that_is_not_a_greeting_is_refused_00() -> Outcome<()> {
1402 let (addr, seen) = res!(scripted("+OK POP3 server ready\r\n", vec![]).await);
1403 req!(true, ImapClient::connect(&cfg(addr)).await.is_err(),
1404 "a POP3 greeting was accepted as IMAP");
1405 req!(true, res!(lines_of(&seen)).is_empty(),
1406 "the client sent a command to something that never greeted it");
1407 Ok(())
1408 }
1409
1410 /// The greeting's `[CAPABILITY ...]` is taken at its word: a client that asks
1411 /// again anyway has spent a round trip on nothing, on every connection.
1412 #[tokio::test]
1413 async fn test_a_capability_in_the_greeting_saves_the_round_trip_00() -> Outcome<()> {
1414 let (addr, seen) = res!(scripted(GREET, vec![]).await);
1415 let c = res!(ImapClient::connect(&cfg(addr)).await);
1416 req!(true, c.has_cap("IMAP4REV1"));
1417 req!(true, c.has_cap("SPECIAL-USE"));
1418 req!(true, res!(lines_of(&seen)).is_empty(),
1419 "the client asked for CAPABILITY it had already been given");
1420 Ok(())
1421 }
1422
1423 /// A greeting without one leaves the client not knowing what the server can
1424 /// do, so it must ask before doing anything that depends on the answer.
1425 #[tokio::test]
1426 async fn test_a_bare_greeting_is_followed_by_capability_00() -> Outcome<()> {
1427 let (addr, seen) = res!(scripted(
1428 "* OK ready\r\n",
1429 vec!["* CAPABILITY IMAP4rev1 STARTTLS\r\n%T% OK done\r\n"],
1430 ).await);
1431 let c = res!(ImapClient::connect(&cfg(addr)).await);
1432 req!(true, c.has_cap("STARTTLS"));
1433 let lines = res!(lines_of(&seen));
1434 req!(1, lines.len());
1435 req!(true, lines[0].to_uppercase().ends_with("CAPABILITY"),
1436 "the client did not ask for CAPABILITY: {:?}", lines);
1437 Ok(())
1438 }
1439
1440 /// A refresh replaces the set rather than adding to it. A capability left
1441 /// behind is one the client believes in and the server has withdrawn --
1442 /// after a login, that is a command sent to a server that will reject it.
1443 #[tokio::test]
1444 async fn test_a_capability_refresh_replaces_rather_than_accumulates_00() -> Outcome<()> {
1445 let (addr, _) = res!(scripted(
1446 "* OK [CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED] ready\r\n",
1447 vec!["* CAPABILITY IMAP4rev1 IDLE\r\n%T% OK done\r\n"],
1448 ).await);
1449 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1450 req!(true, c.has_cap("LOGINDISABLED"));
1451 res!(c.capability().await);
1452 req!(false, c.has_cap("LOGINDISABLED"),
1453 "a withdrawn capability survived the refresh");
1454 req!(false, c.has_cap("STARTTLS"), "a withdrawn capability survived the refresh");
1455 req!(true, c.has_cap("IDLE"));
1456 Ok(())
1457 }
1458
1459 // ── Authentication ────────────────────────────────────────────
1460
1461 /// A refusal carries the server's words -- `[AUTHENTICATIONFAILED]` is what
1462 /// tells a person their app password is the problem -- and never the password.
1463 #[tokio::test]
1464 async fn test_a_refused_login_keeps_the_words_and_not_the_password_00() -> Outcome<()> {
1465 let (addr, seen) = res!(scripted(
1466 GREET,
1467 vec!["%T% NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)\r\n"],
1468 ).await);
1469 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1470 let msg = match c.login(USER, PASS).await {
1471 Err(e) => fmt!("{}", e),
1472 Ok(()) => return Err(err!("A refused LOGIN was reported as a success.";
1473 Test, Invalid)),
1474 };
1475 req!(true, msg.contains("AUTHENTICATIONFAILED"),
1476 "the server's response code was dropped: {}", msg);
1477 req!(false, msg.contains(PASS), "the password leaked into the error: {}", msg);
1478 // It did go over the wire, which is what LOGIN is; the point is that the
1479 // error does not repeat it into a log file.
1480 req!(true, res!(lines_of(&seen)).iter().any(|l| l.contains(PASS)));
1481 Ok(())
1482 }
1483
1484 /// `LOGINDISABLED` means the password will be refused, so it is not sent. A
1485 /// client that tries anyway has put the credential on the wire to learn
1486 /// something the server already told it.
1487 #[tokio::test]
1488 async fn test_logindisabled_withholds_the_password_00() -> Outcome<()> {
1489 let (addr, seen) = res!(scripted(
1490 "* OK [CAPABILITY IMAP4rev1 LOGINDISABLED STARTTLS] ready\r\n",
1491 vec!["%T% OK never reached\r\n"],
1492 ).await);
1493 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1494 req!(true, c.login(USER, PASS).await.is_err(),
1495 "the client sent a password to a server that had disabled it");
1496 req!(false, res!(lines_of(&seen)).iter().any(|l| l.contains(PASS)),
1497 "the password crossed the wire anyway");
1498 Ok(())
1499 }
1500
1501 /// XOAUTH2 is refused outright where the server does not offer it, rather
1502 /// than sent and rejected -- a bearer token spent on a server that cannot
1503 /// use it is a token in somebody's log.
1504 #[tokio::test]
1505 async fn test_xoauth2_is_not_offered_to_a_server_without_it_00() -> Outcome<()> {
1506 let (addr, seen) = res!(scripted(GREET, vec!["%T% OK never reached\r\n"]).await);
1507 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1508 req!(true, c.authenticate_xoauth2(USER, "ya29.a-bearer-token").await.is_err());
1509 req!(false, res!(lines_of(&seen)).iter().any(|l| l.contains("ya29")),
1510 "the token crossed the wire to a server that does not speak XOAUTH2");
1511 Ok(())
1512 }
1513
1514 /// STARTTLS was asked for and the server does not offer it. The credential is
1515 /// not sent in the clear as a fallback.
1516 #[tokio::test]
1517 async fn test_starttls_absent_refuses_the_connection_00() -> Outcome<()> {
1518 let (addr, seen) = res!(scripted(
1519 "* OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] ready\r\n", vec![]).await);
1520 let c = ImapConfig::new(fmt!("127.0.0.1"), addr.port(), Security::StartTls)
1521 .with_addr(addr)
1522 .with_timeout(Duration::from_secs(10));
1523 let msg = match ImapClient::connect(&c).await {
1524 Err(e) => fmt!("{}", e),
1525 Ok(_) => return Err(err!(
1526 "A connection that could not be secured was returned as usable.";
1527 Test, Invalid)),
1528 };
1529 req!(true, msg.contains("STARTTLS"), "the error did not name STARTTLS: {}", msg);
1530 req!(false, res!(lines_of(&seen)).iter().any(|l| l.to_uppercase().contains("LOGIN")),
1531 "the client began to log in over an unprotected connection");
1532 Ok(())
1533 }
1534
1535 /// RFC 2595 §3.1: anything the server pipelines behind its `STARTTLS` reply
1536 /// arrived unprotected and is indistinguishable from what the TLS peer says
1537 /// next. Discarding it silently is the downgrade attack; the connection must
1538 /// be refused.
1539 #[tokio::test]
1540 async fn test_data_pipelined_behind_starttls_is_refused_00() -> Outcome<()> {
1541 let (addr, _) = res!(scripted(
1542 "* OK [CAPABILITY IMAP4rev1 STARTTLS] ready\r\n",
1543 // The OK, and then an injected untagged line the real peer never sent.
1544 vec!["%T% OK Begin TLS negotiation now\r\n* OK [CAPABILITY IMAP4rev1 \
1545 AUTH=PLAIN LOGINDISABLED] injected\r\n"],
1546 ).await);
1547 let c = ImapConfig::new(fmt!("127.0.0.1"), addr.port(), Security::StartTls)
1548 .with_addr(addr)
1549 .with_timeout(Duration::from_secs(10));
1550 let msg = match ImapClient::connect(&c).await {
1551 Err(e) => fmt!("{}", e),
1552 Ok(_) => return Err(err!(
1553 "Bytes injected before the handshake were accepted as the peer's.";
1554 Test, Invalid)),
1555 };
1556 req!(true, msg.contains("after its STARTTLS response"),
1557 "the refusal did not name the reason: {}", msg);
1558 Ok(())
1559 }
1560
1561 // ── The wire, where a line is not a line ──────────────────────
1562
1563 /// The reader's whole job: a literal announced mid-line, whose payload
1564 /// contains CRLF, a close paren and a `{n}` of its own. A client that reads
1565 /// FETCH replies line by line loses the message here, and the bug does not
1566 /// show until somebody sends an attachment.
1567 #[tokio::test]
1568 async fn test_a_literal_containing_crlf_survives_the_reader_00() -> Outcome<()> {
1569 // 62 bytes, counted: a body with two blank lines, a close paren and a
1570 // fake literal marker in it.
1571 let body = "Subject: awkward\r\n\r\n) not the end {17}\r\nlast line\r\n";
1572 req!(51, body.len(), "the scripted literal length must match the payload");
1573 let script = vec![
1574 "%T% OK [READ-WRITE] SELECT completed\r\n",
1575 "* 1 FETCH (UID 9 FLAGS (\\Seen) RFC822.SIZE 51 BODY[] {51}\r\n\
1576 Subject: awkward\r\n\r\n) not the end {17}\r\nlast line\r\n)\r\n\
1577 %T% OK FETCH completed\r\n",
1578 ];
1579 let (addr, _) = res!(scripted(GREET, script).await);
1580 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1581 res!(c.select("INBOX").await);
1582 let msgs = res!(c.uid_fetch(&[9], FetchWhat::Full).await);
1583 req!(1, msgs.len(), "the FETCH reply was lost");
1584 req!(body.as_bytes().to_vec(), msgs[0].body,
1585 "the literal came back changed: {:?}", String::from_utf8_lossy(&msgs[0].body));
1586 req!(9, msgs[0].uid);
1587 req!(vec![fmt!("\\Seen")], msgs[0].flags);
1588 Ok(())
1589 }
1590
1591 /// A server announcing more than the client will allocate for is refused
1592 /// before the allocation, not after it.
1593 #[tokio::test]
1594 async fn test_an_oversized_literal_is_refused_before_allocating_00() -> Outcome<()> {
1595 let (addr, _) = res!(scripted(
1596 GREET,
1597 vec![
1598 "%T% OK SELECT completed\r\n",
1599 // 64 MiB + 1, announced and never sent.
1600 "* 1 FETCH (UID 9 BODY[] {67108865}\r\n",
1601 ],
1602 ).await);
1603 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1604 res!(c.select("INBOX").await);
1605 let msg = match c.uid_fetch(&[9], FetchWhat::Full).await {
1606 Err(e) => fmt!("{}", e),
1607 Ok(_) => return Err(err!(
1608 "A 64 MiB literal was accepted from a server that never sent it.";
1609 Test, Invalid)),
1610 };
1611 req!(true, msg.contains("67108865"), "the error did not name the size: {}", msg);
1612 Ok(())
1613 }
1614
1615 /// A tag the client never sent means the connection is out of step, and
1616 /// nothing read after it belongs to the command that is waiting. Matching it
1617 /// loosely is how one command's reply is read as another's.
1618 #[tokio::test]
1619 async fn test_a_foreign_tag_takes_the_connection_out_of_step_00() -> Outcome<()> {
1620 let (addr, _) = res!(scripted(
1621 GREET,
1622 vec!["a9999 OK SELECT completed\r\n"],
1623 ).await);
1624 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1625 let msg = match c.select("INBOX").await {
1626 Err(e) => fmt!("{}", e),
1627 Ok(_) => return Err(err!(
1628 "A reply carrying somebody else's tag was accepted."; Test, Invalid)),
1629 };
1630 req!(true, msg.contains("a9999"), "the error did not name the tag seen: {}", msg);
1631 Ok(())
1632 }
1633
1634 /// A server that hangs up mid-command is an error naming the close, not a
1635 /// hang and not an empty success.
1636 #[tokio::test]
1637 async fn test_a_closed_connection_is_named_00() -> Outcome<()> {
1638 let (addr, _) = res!(scripted(GREET, vec![]).await);
1639 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1640 let msg = match c.select("INBOX").await {
1641 Err(e) => fmt!("{}", e),
1642 Ok(_) => return Err(err!(
1643 "A closed connection was reported as a SELECT."; Test, Invalid)),
1644 };
1645 req!(true, msg.contains("closed the connection"),
1646 "the error did not say the server hung up: {}", msg);
1647 Ok(())
1648 }
1649
1650 /// `NO` and `BAD` are different failures and are reported as such: one is a
1651 /// server refusing, the other a client having sent nonsense.
1652 #[tokio::test]
1653 async fn test_no_and_bad_are_reported_apart_00() -> Outcome<()> {
1654 let (addr, _) = res!(scripted(GREET,
1655 vec!["%T% NO Mailbox does not exist\r\n"]).await);
1656 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1657 let msg = match c.select("nosuch").await {
1658 Err(e) => fmt!("{}", e),
1659 Ok(_) => return Err(err!("A NO was a success."; Test, Invalid)),
1660 };
1661 req!(true, msg.contains("refused"), "a NO did not read as a refusal: {}", msg);
1662
1663 let (addr, _) = res!(scripted(GREET, vec!["%T% BAD Command unrecognised\r\n"]).await);
1664 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1665 let msg = match c.select("INBOX").await {
1666 Err(e) => fmt!("{}", e),
1667 Ok(_) => return Err(err!("A BAD was a success."; Test, Invalid)),
1668 };
1669 req!(true, msg.contains("malformed"), "a BAD did not read as malformed: {}", msg);
1670 Ok(())
1671 }
1672
1673 // ── What the client puts on the wire ──────────────────────────
1674
1675 /// A week's worth of consecutive UIDs is a command line a server is entitled
1676 /// to reject, so they go over collapsed into ranges. Asserted from what the
1677 /// server received, not from the function that builds it.
1678 #[tokio::test]
1679 async fn test_a_uid_set_reaches_the_server_collapsed_00() -> Outcome<()> {
1680 let (addr, seen) = res!(scripted(
1681 GREET,
1682 vec![
1683 "%T% OK SELECT completed\r\n",
1684 "%T% OK FETCH completed\r\n",
1685 ],
1686 ).await);
1687 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1688 res!(c.select("INBOX").await);
1689 res!(c.uid_fetch(&[1, 2, 3, 7, 9, 10], FetchWhat::Meta).await);
1690 let lines = res!(lines_of(&seen));
1691 req!(true, lines.iter().any(|l| l.contains("UID FETCH 1:3,7,9:10")),
1692 "the UID set was not collapsed on the wire: {:?}", lines);
1693 Ok(())
1694 }
1695
1696 /// An empty set is no command at all: `UID FETCH ` with nothing after it is a
1697 /// syntax error, and a caller passing an empty search result has done nothing
1698 /// wrong.
1699 #[tokio::test]
1700 async fn test_an_empty_fetch_sends_nothing_00() -> Outcome<()> {
1701 let (addr, seen) = res!(scripted(GREET, vec!["%T% OK SELECT completed\r\n"]).await);
1702 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1703 res!(c.select("INBOX").await);
1704 let before = res!(lines_of(&seen)).len();
1705 req!(true, res!(c.uid_fetch(&[], FetchWhat::Full).await).is_empty());
1706 res!(c.uid_store_flags(&[], FlagOp::Add, &["\\Seen"]).await);
1707 req!(before, res!(lines_of(&seen)).len(),
1708 "a command was sent for an empty UID set");
1709 Ok(())
1710 }
1711
1712 /// `RETURN (SPECIAL-USE)` is asked for only where the server said it
1713 /// understands it. Sent to a server without the capability it is a `LIST` the
1714 /// server may reject outright, and then no folders are listed at all.
1715 #[tokio::test]
1716 async fn test_special_use_is_asked_for_only_when_offered_00() -> Outcome<()> {
1717 let (addr, seen) = res!(scripted(GREET,
1718 vec!["* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n%T% OK LIST completed\r\n"]).await);
1719 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1720 res!(c.list("", "*").await);
1721 req!(true, res!(lines_of(&seen)).iter().any(|l| l.contains("RETURN (SPECIAL-USE)")),
1722 "SPECIAL-USE was offered and not asked for");
1723
1724 let (addr, seen) = res!(scripted(
1725 "* OK [CAPABILITY IMAP4rev1] ready\r\n",
1726 vec!["* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n%T% OK LIST completed\r\n"],
1727 ).await);
1728 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1729 res!(c.list("", "*").await);
1730 req!(false, res!(lines_of(&seen)).iter().any(|l| l.contains("SPECIAL-USE")),
1731 "SPECIAL-USE was asked of a server that never offered it");
1732 Ok(())
1733 }
1734
1735 /// `EXAMINE` rather than `SELECT`, because a sync that marks mail read is a
1736 /// sync somebody notices. The gateway's fetch path uses this one.
1737 #[tokio::test]
1738 async fn test_examine_is_read_only_on_the_wire_00() -> Outcome<()> {
1739 let (addr, seen) = res!(scripted(
1740 GREET,
1741 vec!["* 42 EXISTS\r\n* OK [UIDVALIDITY 1234567890]\r\n\
1742 * OK [UIDNEXT 4321]\r\n%T% OK [READ-ONLY] EXAMINE completed\r\n"],
1743 ).await);
1744 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1745 let st = res!(c.examine("INBOX").await);
1746 req!(true, res!(lines_of(&seen)).iter().any(|l| l.contains("EXAMINE \"INBOX\"")),
1747 "EXAMINE was not the verb sent");
1748 req!(42, st.exists);
1749 req!(1_234_567_890, st.uid_validity);
1750 req!(4_321, st.uid_next);
1751 req!(true, st.read_only);
1752 Ok(())
1753 }
1754
1755 /// A server may downgrade a `SELECT` and say so only in the completion line's
1756 /// response code. A client that misses it believes it may set flags.
1757 #[tokio::test]
1758 async fn test_a_downgraded_select_is_read_only_00() -> Outcome<()> {
1759 let (addr, _) = res!(scripted(
1760 GREET,
1761 vec!["* 3 EXISTS\r\n%T% OK [READ-ONLY] SELECT completed\r\n"],
1762 ).await);
1763 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1764 let st = res!(c.select("INBOX").await);
1765 req!(true, st.read_only, "the downgrade in the completion line was missed");
1766 Ok(())
1767 }
1768
1769 /// `.SILENT` on every `STORE`: the client already knows what it asked for,
1770 /// and the untagged FETCH the server would send back is a round trip spent on
1771 /// nothing.
1772 #[tokio::test]
1773 async fn test_a_store_is_silent_on_the_wire_00() -> Outcome<()> {
1774 let (addr, seen) = res!(scripted(
1775 GREET,
1776 vec!["%T% OK SELECT completed\r\n", "%T% OK STORE completed\r\n"],
1777 ).await);
1778 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1779 res!(c.select("INBOX").await);
1780 res!(c.uid_store_flags(&[4, 5], FlagOp::Add, &["\\Seen"]).await);
1781 let lines = res!(lines_of(&seen));
1782 req!(true, lines.iter().any(|l| l.contains("UID STORE 4:5 +FLAGS.SILENT (\\Seen)")),
1783 "the STORE was not what was expected: {:?}", lines);
1784 Ok(())
1785 }
1786
1787 // ── APPEND, the one command with a literal going the other way ──
1788
1789 /// The body waits for the `+` continuation. Sending it before the server has
1790 /// asked is how a client and a server disagree about where a message ends.
1791 #[tokio::test]
1792 async fn test_append_waits_for_the_continuation_00() -> Outcome<()> {
1793 let body = b"Subject: filed\r\n\r\nA sent message.\r\n";
1794 let (addr, seen) = res!(scripted(
1795 GREET,
1796 vec!["+ Ready for literal data\r\n", "%T% OK [APPENDUID 1 12] APPEND completed\r\n"],
1797 ).await);
1798 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1799 res!(c.append("Sent", &["\\Seen"], body).await);
1800
1801 let lines = res!(lines_of(&seen));
1802 // The command, then the marker the fixture logs when it reads the
1803 // literal, then the payload -- in that order, which is the point.
1804 let cmd = res!(lines.iter().position(|l| l.contains("APPEND \"Sent\""))
1805 .ok_or_else(|| err!("No APPEND was sent: {:?}", lines; Test, Missing)));
1806 let lit = res!(lines.iter().position(|l| l.starts_with("<literal "))
1807 .ok_or_else(|| err!("The body never arrived: {:?}", lines; Test, Missing)));
1808 req!(true, cmd < lit, "the body was sent before the command");
1809 req!(true, lines[cmd].contains(&fmt!("{{{}}}", body.len())),
1810 "the announced length was wrong: {}", lines[cmd]);
1811 req!(true, lines[cmd].contains("(\\Seen)"), "the flags were dropped");
1812 req!(fmt!("<literal {}>", body.len()), lines[lit]);
1813 req!(String::from_utf8_lossy(body).into_owned(), lines[lit + 1]);
1814 Ok(())
1815 }
1816
1817 /// A refusal instead of a continuation stops the body: a server that will not
1818 /// take the message must not be sent it anyway.
1819 #[tokio::test]
1820 async fn test_a_refused_append_sends_no_body_00() -> Outcome<()> {
1821 let body = b"Subject: filed\r\n\r\nA sent message.\r\n";
1822 let (addr, seen) = res!(scripted(
1823 GREET,
1824 vec!["%T% NO [OVERQUOTA] Mailbox is full\r\n"],
1825 ).await);
1826 let mut c = res!(ImapClient::connect(&cfg(addr)).await);
1827 let msg = match c.append("Sent", &[], body).await {
1828 Err(e) => fmt!("{}", e),
1829 Ok(()) => return Err(err!("A refused APPEND was a success."; Test, Invalid)),
1830 };
1831 req!(true, msg.contains("OVERQUOTA"), "the server's words were dropped: {}", msg);
1832 req!(false, res!(lines_of(&seen)).iter().any(|l| l.starts_with("<literal ")),
1833 "the body was sent to a mailbox that had refused it");
1834 Ok(())
1835 }
1836
1837 #[test]
1838 fn test_uid_set_collapses_runs() {
1839 assert_eq!(uid_set(&[1, 2, 3, 7, 9, 10]), "1:3,7,9:10");
1840 assert_eq!(uid_set(&[5]), "5");
1841 assert_eq!(uid_set(&[3, 1, 2]), "1:3");
1842 assert_eq!(uid_set(&[]), "");
1843 }
1844
1845 #[test]
1846 fn test_quoted_escapes() {
1847 assert_eq!(quoted("plain"), "\"plain\"");
1848 assert_eq!(quoted("a\"b"), "\"a\\\"b\"");
1849 assert_eq!(quoted("a\\b"), "\"a\\\\b\"");
1850 }
1851
1852 #[test]
1853 fn test_trailing_literal_len() {
1854 assert_eq!(trailing_literal_len("* 1 FETCH (BODY[] {42}"), Some(42));
1855 assert_eq!(trailing_literal_len("* 1 FETCH (BODY[] {42+}"), Some(42));
1856 assert_eq!(trailing_literal_len("a001 OK done"), None);
1857 assert_eq!(trailing_literal_len("* OK [UIDNEXT 5]"), None);
1858 }
1859
1860 #[test]
1861 fn test_tokenise_nested_and_bracketed() {
1862 let toks = tokenise(
1863 "* 1 FETCH (UID 9 FLAGS (\\Seen \\Answered) BODY[HEADER.FIELDS (FROM TO)] \"x\")",
1864 &[],
1865 ).unwrap();
1866 assert_eq!(toks[0], Tok::Atom(fmt!("*")));
1867 assert_eq!(toks[1], Tok::Atom(fmt!("1")));
1868 assert_eq!(toks[2], Tok::Atom(fmt!("FETCH")));
1869 let items = match &toks[3] {
1870 Tok::List(v) => v.clone(),
1871 other => panic!("expected a list, got {:?}", other),
1872 };
1873 assert_eq!(items[0], Tok::Atom(fmt!("UID")));
1874 assert_eq!(items[1], Tok::Atom(fmt!("9")));
1875 assert_eq!(items[3], Tok::List(vec![
1876 Tok::Atom(fmt!("\\Seen")),
1877 Tok::Atom(fmt!("\\Answered")),
1878 ]));
1879 // The bracketed section stays one atom, spaces and all.
1880 assert_eq!(items[4], Tok::Atom(fmt!("BODY[HEADER.FIELDS (FROM TO)]")));
1881 assert_eq!(items[5], Tok::Quoted(fmt!("x")));
1882 }
1883
1884 #[test]
1885 fn test_parse_fetch_with_literal_body() {
1886 let body = b"From: a@b.co\r\nSubject: hi\r\n\r\nbody\r\n".to_vec();
1887 let l = line(
1888 &fmt!("* 12 FETCH (UID 345 FLAGS (\\Seen) INTERNALDATE \
1889 \"01-Jan-2026 09:15:00 +0000\" RFC822.SIZE {} BODY[] {{{}}})",
1890 body.len(), body.len()),
1891 vec![body.clone()],
1892 );
1893 let msg = parse_fetch_line(&l).unwrap().unwrap();
1894 assert_eq!(msg.seq, 12);
1895 assert_eq!(msg.uid, 345);
1896 assert_eq!(msg.flags, vec![fmt!("\\Seen")]);
1897 assert_eq!(msg.internal_date, "01-Jan-2026 09:15:00 +0000");
1898 assert_eq!(msg.size as usize, body.len());
1899 assert_eq!(msg.body, body);
1900 }
1901
1902 /// A body containing a CRLF and even a `)` must survive: this is
1903 /// precisely what a line-oriented parser gets wrong.
1904 #[test]
1905 fn test_literal_body_containing_crlf_and_paren() {
1906 let body = b"Subject: x\r\n\r\nline one\r\n) not the end\r\n".to_vec();
1907 let l = line(
1908 &fmt!("* 1 FETCH (UID 2 BODY[] {{{}}})", body.len()),
1909 vec![body.clone()],
1910 );
1911 let msg = parse_fetch_line(&l).unwrap().unwrap();
1912 assert_eq!(msg.body, body);
1913 assert_eq!(msg.uid, 2);
1914 }
1915
1916 #[test]
1917 fn test_parse_fetch_headers_only() {
1918 let hdr = b"From: a@b.co\r\n\r\n".to_vec();
1919 let l = line(
1920 &fmt!("* 3 FETCH (UID 7 RFC822.SIZE 999 BODY[HEADER] {{{}}})", hdr.len()),
1921 vec![hdr.clone()],
1922 );
1923 let msg = parse_fetch_line(&l).unwrap().unwrap();
1924 assert_eq!(msg.body, hdr);
1925 assert_eq!(msg.size, 999); // the whole message, not the fetch
1926 }
1927
1928 #[test]
1929 fn test_parse_fetch_without_uid_is_an_error() {
1930 let l = line("* 1 FETCH (FLAGS (\\Seen))", vec![]);
1931 assert!(parse_fetch_line(&l).is_err());
1932 }
1933
1934 #[test]
1935 fn test_non_fetch_untagged_line_is_skipped() {
1936 let l = line("* 42 EXISTS", vec![]);
1937 assert!(parse_fetch_line(&l).unwrap().is_none());
1938 }
1939
1940 #[test]
1941 fn test_parse_list_line() {
1942 let l = line("* LIST (\\HasNoChildren) \"/\" \"INBOX\"", vec![]);
1943 let mb = parse_list_line(&l).unwrap().unwrap();
1944 assert_eq!(mb.name, "INBOX");
1945 assert_eq!(mb.delimiter, Some('/'));
1946 assert_eq!(mb.attrs, vec![fmt!("\\HasNoChildren")]);
1947 assert!(mb.selectable());
1948
1949 let l = line("* LIST (\\Noselect \\HasChildren) \".\" \"[Gmail]\"", vec![]);
1950 let mb = parse_list_line(&l).unwrap().unwrap();
1951 assert_eq!(mb.name, "[Gmail]");
1952 assert_eq!(mb.delimiter, Some('.'));
1953 assert!(!mb.selectable());
1954 }
1955
1956 #[test]
1957 fn test_special_use_survives_localised_names() {
1958 // A German Gmail account: the NAME is localised, the ROLE is not.
1959 let l = line("* LIST (\\HasNoChildren \\Sent) \"/\" \"[Gmail]/Gesendet\"", vec![]);
1960 let mb = parse_list_line(&l).unwrap().unwrap();
1961 assert_eq!(mb.name, "[Gmail]/Gesendet");
1962 assert_eq!(mb.special_use(), Some(SpecialUse::Sent));
1963
1964 let l = line("* LIST (\\HasNoChildren \\All) \"/\" \"[Gmail]/Alle Nachrichten\"", vec![]);
1965 let mb = parse_list_line(&l).unwrap().unwrap();
1966 assert_eq!(mb.special_use(), Some(SpecialUse::All));
1967
1968 // No role declared: an ordinary user folder.
1969 let l = line("* LIST (\\HasNoChildren) \"/\" \"receipts\"", vec![]);
1970 let mb = parse_list_line(&l).unwrap().unwrap();
1971 assert_eq!(mb.special_use(), None);
1972 }
1973
1974 #[test]
1975 fn test_select_lines_fold_into_status() {
1976 let mut st = MailboxStatus::default();
1977 absorb_select_line(&mut st, "* 42 EXISTS").unwrap();
1978 absorb_select_line(&mut st, "* 3 RECENT").unwrap();
1979 absorb_select_line(&mut st, "* OK [UIDVALIDITY 1234567890]").unwrap();
1980 absorb_select_line(&mut st, "* OK [UIDNEXT 4321]").unwrap();
1981 absorb_select_line(&mut st, "* FLAGS (\\Answered \\Seen)").unwrap();
1982 assert_eq!(st.exists, 42);
1983 assert_eq!(st.recent, 3);
1984 assert_eq!(st.uid_validity, 1_234_567_890);
1985 assert_eq!(st.uid_next, 4_321);
1986 assert_eq!(st.flags, vec![fmt!("\\Answered"), fmt!("\\Seen")]);
1987 assert!(!st.read_only);
1988 }
1989
1990 #[test]
1991 fn test_capabilities_from_greeting_and_untagged() {
1992 let a = parse_capabilities("* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN] ready");
1993 assert!(a.contains(&fmt!("STARTTLS")));
1994 assert!(a.contains(&fmt!("IMAP4REV1")));
1995 let b = parse_capabilities("* CAPABILITY IMAP4rev1 IDLE AUTH=XOAUTH2");
1996 assert!(b.contains(&fmt!("AUTH=XOAUTH2")));
1997 assert!(parse_capabilities("* 12 EXISTS").is_empty());
1998 }
1999
2000 #[test]
2001 fn test_parse_completion() {
2002 assert_eq!(parse_completion("OK LOGIN completed").unwrap().0, Status::Ok);
2003 assert_eq!(parse_completion("NO [AUTHENTICATIONFAILED] bad").unwrap().0, Status::No);
2004 assert_eq!(parse_completion("BAD nonsense").unwrap().0, Status::Bad);
2005 assert!(parse_completion("WAT something").is_err());
2006 }
2007
2008 #[test]
2009 fn test_fetch_items_peek_not_seen() {
2010 // Fetching must not silently mark mail as read.
2011 assert!(FetchWhat::Full.items().contains("BODY.PEEK[]"));
2012 assert!(!FetchWhat::Full.items().contains("BODY[]"));
2013 }
2014}