1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
//! IMAP connection and small protocol helpers.
use std::net::TcpStream;
use anyhow::{anyhow, Result};
use native_tls::{TlsConnector, TlsStream};
use crate::config::Account;
pub type ImapSession = imap::Session<TlsStream<TcpStream>>;
/// Opens a logged-in IMAP session, using STARTTLS or implicit TLS as configured.
pub fn connect(acc: &Account) -> Result<ImapSession> {
let (ep, pass) = acc.imap()?;
let tls = TlsConnector::new()?;
let client = if ep.implicit_tls {
imap::connect((ep.host.as_str(), ep.port), ep.host.as_str(), &tls)?
} else {
imap::connect_starttls((ep.host.as_str(), ep.port), ep.host.as_str(), &tls)?
};
client.login(&ep.user, &pass).map_err(|e| {
anyhow!(
"IMAP login failed for {} (account '{}'): {:?}",
ep.user,
acc.name,
e.0
)
})
}
/// Quotes a mailbox name when it needs it (spaces, quotes, parentheses).
pub fn quote_mailbox(name: &str) -> String {
if name.starts_with('"') || !name.contains([' ', '"', '(', ')']) {
name.to_string()
} else {
format!("\"{}\"", name.replace('"', "\\\""))
}
}
/// Renders a UID list as an IMAP sequence set.
pub fn uid_set(uids: &[u32]) -> String {
uids.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",")
}
/// Moves messages, preferring `UID MOVE` with a COPY+EXPUNGE fallback for
/// servers without the MOVE capability.
pub fn move_uids(session: &mut ImapSession, uids: &[u32], to: &str) -> Result<()> {
let set = uid_set(uids);
let dest = quote_mailbox(to);
if session.uid_mv(&set, &dest).is_ok() {
return Ok(());
}
session.uid_copy(&set, &dest)?;
session.uid_store(&set, "+FLAGS (\\Deleted)")?;
session.expunge()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_names_stay_unquoted() {
assert_eq!(quote_mailbox("INBOX"), "INBOX");
assert_eq!(quote_mailbox("Archives/2025"), "Archives/2025");
}
#[test]
fn names_with_spaces_get_quoted() {
assert_eq!(quote_mailbox("Sent Mail"), "\"Sent Mail\"");
}
#[test]
fn embedded_quotes_are_escaped() {
assert_eq!(quote_mailbox("a\"b"), "\"a\\\"b\"");
}
#[test]
fn already_quoted_names_pass_through() {
assert_eq!(quote_mailbox("\"Sent Mail\""), "\"Sent Mail\"");
}
#[test]
fn uid_sets_join_with_commas() {
assert_eq!(uid_set(&[7, 9, 11]), "7,9,11");
assert_eq!(uid_set(&[42]), "42");
}
}
|