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
8pub 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}