From 3b808556fcd425175c0d2e53239f0a5c293520d8 Mon Sep 17 00:00:00 2001 From: Connor Black Date: Mon, 6 Apr 2026 15:31:45 -0400 Subject: [PATCH] add sync_max_age_days to email adapter When connecting an email account for the first time, every unread email in the inbox gets imported and treated as new. This floods the agent with stale messages it shouldn't respond to. Add a `sync_max_age_days` config option (default: 0 / no limit) that combines IMAP's UNSEEN flag with a SINCE date filter so only recent unread emails are imported. The SINCE query is evaluated server-side by the IMAP server, so it's efficient even on large mailboxes. Supported on both the default email config and per-instance configs. --- src/config.rs | 1 + src/config/load.rs | 2 ++ src/config/toml_schema.rs | 4 ++++ src/config/types.rs | 2 ++ src/messaging/email.rs | 19 ++++++++++++++++++- 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index eb0891a9a..04677cef0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1706,6 +1706,7 @@ maintenance_merge_similarity_threshold = 1.1 allowed_senders: vec![], max_body_bytes: 1_000_000, max_attachment_bytes: 10_000_000, + sync_max_age_days: 0, instances: vec![], }), webhook: None, diff --git a/src/config/load.rs b/src/config/load.rs index 1e6997515..c0fd56d01 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -2161,6 +2161,7 @@ impl Config { allowed_senders: instance.allowed_senders, max_body_bytes: instance.max_body_bytes, max_attachment_bytes: instance.max_attachment_bytes, + sync_max_age_days: instance.sync_max_age_days, } }) .collect::>(); @@ -2231,6 +2232,7 @@ impl Config { allowed_senders: email.allowed_senders, max_body_bytes: email.max_body_bytes, max_attachment_bytes: email.max_attachment_bytes, + sync_max_age_days: email.sync_max_age_days, instances, }) }), diff --git a/src/config/toml_schema.rs b/src/config/toml_schema.rs index 5ab153d86..9f8f7b447 100644 --- a/src/config/toml_schema.rs +++ b/src/config/toml_schema.rs @@ -627,6 +627,8 @@ pub(super) struct TomlEmailConfig { #[serde(default = "default_email_max_attachment_bytes")] pub(super) max_attachment_bytes: usize, #[serde(default)] + pub(super) sync_max_age_days: u64, + #[serde(default)] pub(super) instances: Vec, } @@ -661,6 +663,8 @@ pub(super) struct TomlEmailInstanceConfig { pub(super) max_body_bytes: usize, #[serde(default = "default_email_max_attachment_bytes")] pub(super) max_attachment_bytes: usize, + #[serde(default)] + pub(super) sync_max_age_days: u64, } #[derive(Deserialize)] diff --git a/src/config/types.rs b/src/config/types.rs index 1cbd07501..5e508fd06 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -2600,6 +2600,7 @@ pub struct EmailConfig { pub allowed_senders: Vec, pub max_body_bytes: usize, pub max_attachment_bytes: usize, + pub sync_max_age_days: u64, pub instances: Vec, } @@ -2625,6 +2626,7 @@ pub struct EmailInstanceConfig { pub allowed_senders: Vec, pub max_body_bytes: usize, pub max_attachment_bytes: usize, + pub sync_max_age_days: u64, } impl std::fmt::Debug for EmailInstanceConfig { diff --git a/src/messaging/email.rs b/src/messaging/email.rs index 884dab226..1c12237d8 100644 --- a/src/messaging/email.rs +++ b/src/messaging/email.rs @@ -90,6 +90,7 @@ struct EmailPollConfig { poll_interval: Duration, allowed_senders: Vec, max_body_bytes: usize, + sync_max_age_days: u64, runtime_key: String, } @@ -142,6 +143,7 @@ pub struct EmailAdapter { allowed_senders: Vec, max_body_bytes: usize, max_attachment_bytes: usize, + sync_max_age_days: u64, smtp_transport: AsyncSmtpTransport, shutdown_tx: Arc>>>, poll_task: Arc>>>, @@ -199,6 +201,7 @@ impl EmailAdapter { allowed_senders: config.allowed_senders.clone(), max_body_bytes: config.max_body_bytes, max_attachment_bytes: config.max_attachment_bytes, + sync_max_age_days: config.sync_max_age_days, instances: Vec::new(), }; Self::build(runtime_key.into(), &email_config) @@ -238,6 +241,7 @@ impl EmailAdapter { allowed_senders: config.allowed_senders.clone(), max_body_bytes: config.max_body_bytes.max(1024), max_attachment_bytes: config.max_attachment_bytes.max(1024), + sync_max_age_days: config.sync_max_age_days, smtp_transport, shutdown_tx: Arc::new(RwLock::new(None)), poll_task: Arc::new(RwLock::new(None)), @@ -257,6 +261,7 @@ impl EmailAdapter { poll_interval: self.poll_interval, allowed_senders: self.allowed_senders.clone(), max_body_bytes: self.max_body_bytes, + sync_max_age_days: self.sync_max_age_days, runtime_key: self.runtime_key.clone(), } } @@ -713,8 +718,19 @@ fn poll_inbox_once(config: &EmailPollConfig) -> anyhow::Result 0 { + let since_date = (Utc::now() - ChronoDuration::days(config.sync_max_age_days as i64)) + .format("%d-%b-%Y") + .to_string(); + format!("UNSEEN SINCE {since_date}") + } else { + "UNSEEN".to_string() + }; + let message_uids = session - .uid_search("UNSEEN") + .uid_search(&search_query) .with_context(|| format!("failed to search unseen messages in folder '{folder}'"))?; for uid in message_uids { @@ -1210,6 +1226,7 @@ pub fn search_mailbox( poll_interval: Duration::from_secs(config.poll_interval_secs.max(5)), allowed_senders: config.allowed_senders.clone(), max_body_bytes: config.max_body_bytes.max(1024), + sync_max_age_days: config.sync_max_age_days, runtime_key: "email".to_string(), })?;