aboutsummaryrefslogtreecommitdiffstats
path: root/src/client.rs
blob: 3d9b719769a40c061506663d6e1a9d48e8e8cff4 (plain)
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");
    }
}