aboutsummaryrefslogtreecommitdiffstats
path: root/src/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client.rs')
-rw-r--r--src/client.rs92
1 files changed, 92 insertions, 0 deletions
diff --git a/src/client.rs b/src/client.rs
new file mode 100644
index 0000000..3d9b719
--- /dev/null
+++ b/src/client.rs
@@ -0,0 +1,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");
+ }
+}