Skip to main content

agent_first_mail/
remote.rs

1use crate::config::{ImapConfig, MailConfig, SpecialUseKind};
2use crate::error::{AppError, Result};
3use crate::imap_client::{ImapClientSession, MailboxInfo, MoveOutcome};
4use crate::types::RemoteLocation;
5use serde_json::Value;
6use std::cell::RefCell;
7
8/// Remote mail side effects used by push/pull code.
9///
10/// Workspace code depends on this trait so retry/state-machine tests can use a
11/// fake provider without reaching IMAP or SMTP.
12pub trait MailRemote {
13    fn list_mailboxes(&self) -> Result<Value>;
14    fn action_mailbox_folder(&self, mailbox_id: &str) -> Result<String>;
15    fn append_message(&self, folder: &str, raw_eml: &[u8], draft: bool) -> Result<()>;
16    fn move_message(
17        &self,
18        source_folder: &str,
19        uid: u64,
20        target_folder: &str,
21        rfc822_message_id: Option<&str>,
22    ) -> Result<MoveOutcome>;
23    fn add_flags(&self, source_folder: &str, uid: u64, flags: &[String]) -> Result<()>;
24    fn send_raw_message(
25        &self,
26        envelope_from: &str,
27        envelope_to: &[String],
28        raw: &[u8],
29    ) -> Result<()>;
30    fn find_by_message_id(
31        &self,
32        _folder: &str,
33        _rfc822_message_id: &str,
34    ) -> Result<Option<RemoteLocation>> {
35        Err(AppError::new(
36            "remote_operation_unsupported",
37            "find_by_message_id is not supported by this provider",
38        ))
39    }
40}
41
42pub struct ImapSmtpRemote<'a> {
43    config: &'a MailConfig,
44    imap: Option<ImapConfig>,
45    session: RefCell<Option<ImapClientSession>>,
46    mailboxes: RefCell<Option<Vec<MailboxInfo>>>,
47}
48
49impl<'a> ImapSmtpRemote<'a> {
50    pub fn new(config: &'a MailConfig) -> Self {
51        Self {
52            config,
53            imap: None,
54            session: RefCell::new(None),
55            mailboxes: RefCell::new(None),
56        }
57    }
58
59    fn imap(&self) -> Result<ImapConfig> {
60        if let Some(imap) = &self.imap {
61            return Ok(imap.clone());
62        }
63        self.config.require_imap()
64    }
65
66    fn with_session<T>(
67        &self,
68        operation: impl FnOnce(&mut ImapClientSession) -> Result<T>,
69    ) -> Result<T> {
70        if self.session.borrow().is_none() {
71            let imap = self.imap()?;
72            *self.session.borrow_mut() = Some(ImapClientSession::connect(&imap)?);
73        }
74        let mut session = self.session.borrow_mut();
75        let Some(session) = session.as_mut() else {
76            return Err(AppError::new(
77                "imap_session_missing",
78                "IMAP session was not initialized",
79            ));
80        };
81        operation(session)
82    }
83
84    fn cached_mailboxes(&self) -> Result<Vec<MailboxInfo>> {
85        if let Some(mailboxes) = self.mailboxes.borrow().clone() {
86            return Ok(mailboxes);
87        }
88        let mailboxes = self.with_session(|session| session.list_mailboxes())?;
89        *self.mailboxes.borrow_mut() = Some(mailboxes.clone());
90        Ok(mailboxes)
91    }
92}
93
94impl MailRemote for ImapSmtpRemote<'_> {
95    fn list_mailboxes(&self) -> Result<Value> {
96        let imap = self.imap()?;
97        crate::imap_pull::remote_folders(self.config, &imap)
98    }
99
100    fn action_mailbox_folder(&self, mailbox_id: &str) -> Result<String> {
101        let mailbox = self.config.mailbox(mailbox_id)?;
102        if let Some(folder) = &mailbox.mailbox_name {
103            return Ok(folder.clone());
104        }
105        if let Some(kind) = mailbox
106            .special_use
107            .as_deref()
108            .and_then(SpecialUseKind::from_attribute)
109        {
110            let mailboxes = self.cached_mailboxes()?;
111            return Ok(crate::imap_pull::resolve_special_use_from_mailboxes(
112                self.config,
113                kind,
114                &mailboxes,
115            )
116            .mailbox_name);
117        }
118        self.config.offline_mailbox_name(mailbox_id)
119    }
120
121    fn append_message(&self, folder: &str, raw_eml: &[u8], draft: bool) -> Result<()> {
122        self.with_session(|session| session.append_message(folder, raw_eml, draft))
123    }
124
125    fn move_message(
126        &self,
127        source_folder: &str,
128        uid: u64,
129        target_folder: &str,
130        rfc822_message_id: Option<&str>,
131    ) -> Result<MoveOutcome> {
132        self.with_session(|session| {
133            session.uid_mark_and_move(
134                source_folder,
135                uid,
136                target_folder,
137                rfc822_message_id,
138                false,
139                None,
140            )
141        })
142    }
143
144    fn add_flags(&self, source_folder: &str, uid: u64, flags: &[String]) -> Result<()> {
145        self.with_session(|session| session.uid_store_flags(source_folder, uid, flags, true))
146    }
147
148    fn send_raw_message(
149        &self,
150        envelope_from: &str,
151        envelope_to: &[String],
152        raw: &[u8],
153    ) -> Result<()> {
154        crate::smtp_send::send_raw_message(self.config, envelope_from, envelope_to, raw)
155    }
156
157    fn find_by_message_id(
158        &self,
159        folder: &str,
160        rfc822_message_id: &str,
161    ) -> Result<Option<RemoteLocation>> {
162        self.with_session(|session| session.find_uid_by_message_id(folder, rfc822_message_id))
163            .map(Some)
164    }
165}
166
167#[cfg(test)]
168#[derive(Default)]
169pub struct FakeMailRemote {
170    pub append_results: std::cell::RefCell<std::collections::VecDeque<Result<()>>>,
171    pub move_results: std::cell::RefCell<std::collections::VecDeque<Result<MoveOutcome>>>,
172    pub add_flags_results: std::cell::RefCell<std::collections::VecDeque<Result<()>>>,
173    pub send_results: std::cell::RefCell<std::collections::VecDeque<Result<()>>>,
174}
175
176#[cfg(test)]
177impl MailRemote for FakeMailRemote {
178    fn list_mailboxes(&self) -> Result<Value> {
179        Ok(serde_json::json!({"code": "remote_folders", "folders": []}))
180    }
181
182    fn action_mailbox_folder(&self, mailbox_id: &str) -> Result<String> {
183        Ok(mailbox_id.to_string())
184    }
185
186    fn append_message(&self, _folder: &str, _raw_eml: &[u8], _draft: bool) -> Result<()> {
187        self.append_results
188            .borrow_mut()
189            .pop_front()
190            .unwrap_or(Ok(()))
191    }
192
193    fn move_message(
194        &self,
195        _source_folder: &str,
196        _uid: u64,
197        target_folder: &str,
198        _rfc822_message_id: Option<&str>,
199    ) -> Result<MoveOutcome> {
200        self.move_results
201            .borrow_mut()
202            .pop_front()
203            .unwrap_or(Ok(MoveOutcome {
204                keyword_set: false,
205                keyword_error: None,
206                seen_set: false,
207                seen_error: None,
208                moved: true,
209                target_location: Some(RemoteLocation {
210                    mailbox_name: target_folder.to_string(),
211                    mailbox_id: None,
212                    uid_validity: Some(1),
213                    uid: Some(1),
214                    flags: Vec::new(),
215                    observed_rfc3339: crate::store::now_rfc3339(),
216                    missing_rfc3339: None,
217                }),
218            }))
219    }
220
221    fn add_flags(&self, _source_folder: &str, _uid: u64, _flags: &[String]) -> Result<()> {
222        self.add_flags_results
223            .borrow_mut()
224            .pop_front()
225            .unwrap_or(Ok(()))
226    }
227
228    fn send_raw_message(
229        &self,
230        _envelope_from: &str,
231        _envelope_to: &[String],
232        _raw: &[u8],
233    ) -> Result<()> {
234        self.send_results.borrow_mut().pop_front().unwrap_or(Ok(()))
235    }
236}