1use alloc::{
21 collections::{BTreeMap, BTreeSet, VecDeque},
22 string::String,
23 sync::Arc,
24 vec::Vec,
25};
26use core::{
27 mem,
28 sync::atomic::{AtomicBool, Ordering},
29};
30
31use io_jmap::{
32 coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
33 rfc8620::{
34 JmapSession,
35 changes::JmapChangesOutput,
36 event_source::{
37 JmapCloseAfter, JmapStateChange,
38 subscribe::{JmapEventSource, JmapEventSourceError, JmapEventSourceYield},
39 },
40 },
41 rfc8621::email::{
42 JmapEmail, JmapEmailProperty,
43 changes::{JmapEmailChanges, JmapEmailChangesError, JmapEmailChangesOptions},
44 get::{JmapEmailGet, JmapEmailGetError, JmapEmailGetOptions},
45 },
46};
47use log::trace;
48use secrecy::SecretString;
49use thiserror::Error;
50
51use crate::{
52 envelope::event::WatchEvent,
53 flag::types::Flag,
54 jmap::convert::{account_id_of, envelope_from, envelope_properties},
55};
56
57const EMAIL_TYPE: &str = "Email";
59const PING_SECONDS: u64 = 30;
61
62#[derive(Debug, Error)]
64pub enum JmapWatchMailboxError {
65 #[error(transparent)]
66 EventSource(#[from] JmapEventSourceError),
67 #[error(transparent)]
68 EmailChanges(#[from] JmapEmailChangesError),
69 #[error(transparent)]
70 EmailGet(#[from] JmapEmailGetError),
71}
72
73#[derive(Debug)]
75pub enum JmapWatchMailboxYield {
76 WantsRead,
78 WantsWrite(Vec<u8>),
80 Event(WatchEvent),
82}
83
84pub struct JmapWatchMailbox {
87 state: State,
88 mailbox_id: String,
89 session: JmapSession,
90 http_auth: SecretString,
91 account_id: String,
92 shutdown: Arc<AtomicBool>,
93 shadow: BTreeMap<String, BTreeSet<String>>,
95 email_state: Option<String>,
97 pending: VecDeque<WatchEvent>,
99 suppress_events: bool,
101}
102
103impl JmapWatchMailbox {
104 pub fn new(
105 session: &JmapSession,
106 http_auth: &SecretString,
107 mailbox: &str,
108 shutdown: Arc<AtomicBool>,
109 ) -> Result<Self, JmapWatchMailboxError> {
110 trace!("prepare JMAP mailbox watch");
111 let es = JmapEventSource::new(
112 session,
113 http_auth,
114 &[EMAIL_TYPE],
115 PING_SECONDS,
116 JmapCloseAfter::State,
117 shutdown.clone(),
118 )?;
119 Ok(Self {
120 state: State::Subscribing {
121 es,
122 latest_change: None,
123 },
124 mailbox_id: mailbox.into(),
125 session: session.clone(),
126 http_auth: http_auth.clone(),
127 account_id: account_id_of(session),
128 shutdown,
129 shadow: BTreeMap::new(),
130 email_state: None,
131 pending: VecDeque::new(),
132 suppress_events: true,
133 })
134 }
135
136 fn fresh_subscription_state(&self) -> Result<State, JmapWatchMailboxError> {
138 let es = JmapEventSource::new(
139 &self.session,
140 &self.http_auth,
141 &[EMAIL_TYPE],
142 PING_SECONDS,
143 JmapCloseAfter::State,
144 self.shutdown.clone(),
145 )?;
146 Ok(State::Subscribing {
147 es,
148 latest_change: None,
149 })
150 }
151
152 fn handle_cycle_end(
155 &mut self,
156 change: Option<JmapStateChange>,
157 ) -> Result<State, JmapWatchMailboxError> {
158 let observed_state = change
159 .as_ref()
160 .and_then(|c| c.changed.get(&self.account_id))
161 .and_then(|ts| ts.get(EMAIL_TYPE))
162 .cloned();
163
164 let needs_diff = match (&observed_state, &self.email_state) {
165 (Some(observed), Some(known)) => observed != known,
166 (_, None) => true,
168 (None, _) => false,
170 };
171
172 if needs_diff {
173 let since = self.email_state.clone().unwrap_or_default();
174 let changes = JmapEmailChanges::new(
175 &self.session,
176 &self.http_auth,
177 since,
178 JmapEmailChangesOptions::default(),
179 )?;
180 return Ok(State::FetchingChanges(changes));
181 }
182
183 if !self.suppress_events && change.is_some() {
184 self.pending.push_back(WatchEvent::KeepAlive);
185 }
186 Ok(State::Emitting)
187 }
188
189 fn dispatch_get(&self, ok: JmapChangesOutput) -> Result<State, JmapWatchMailboxError> {
192 let JmapChangesOutput {
193 new_state,
194 has_more_changes,
195 mut created,
196 updated,
197 destroyed,
198 ..
199 } = ok;
200
201 if has_more_changes {
202 trace!("JMAP Email/changes truncated; next subscription cycle will catch up");
203 }
204
205 created.extend(updated);
206 let mut properties = envelope_properties();
207 properties.push(JmapEmailProperty::MailboxIds);
208
209 let opts = JmapEmailGetOptions {
210 properties: Some(properties),
211 ..Default::default()
212 };
213 let get = JmapEmailGet::new(&self.session, &self.http_auth, created, opts)?;
214
215 Ok(State::FetchingEmails {
216 get,
217 destroyed,
218 new_state,
219 })
220 }
221
222 fn apply_diff(&mut self, emails: Vec<JmapEmail>, destroyed: Vec<String>) {
225 for email in emails {
226 let Some(id) = email.id.clone() else {
227 continue;
228 };
229 let in_mailbox = email
230 .mailbox_ids
231 .as_ref()
232 .is_some_and(|map| map.get(&self.mailbox_id).copied().unwrap_or(false));
233 let new_keywords: BTreeSet<String> = email
234 .keywords
235 .clone()
236 .unwrap_or_default()
237 .into_iter()
238 .filter_map(|(k, v)| if v { Some(k) } else { None })
239 .collect();
240 let was_in_shadow = self.shadow.contains_key(&id);
241
242 if in_mailbox {
243 if was_in_shadow {
244 let old = self.shadow.get(&id).cloned().unwrap_or_default();
245 let added: BTreeSet<String> = new_keywords.difference(&old).cloned().collect();
246 let removed: BTreeSet<String> =
247 old.difference(&new_keywords).cloned().collect();
248 if !added.is_empty() && !self.suppress_events {
249 self.pending.push_back(WatchEvent::FlagsAdded {
250 mailbox: self.mailbox_id.clone(),
251 id: id.clone(),
252 flags: added.into_iter().map(Flag::from_raw).collect(),
253 });
254 }
255 if !removed.is_empty() && !self.suppress_events {
256 self.pending.push_back(WatchEvent::FlagsRemoved {
257 mailbox: self.mailbox_id.clone(),
258 id: id.clone(),
259 flags: removed.into_iter().map(Flag::from_raw).collect(),
260 });
261 }
262 self.shadow.insert(id, new_keywords);
263 } else {
264 let envelope = envelope_from(email);
265 if !self.suppress_events {
266 self.pending.push_back(WatchEvent::EnvelopeAdded {
267 mailbox: self.mailbox_id.clone(),
268 envelope: envelope.clone(),
269 });
270 }
271 self.shadow.insert(envelope.id, new_keywords);
272 }
273 } else if was_in_shadow {
274 if !self.suppress_events {
275 self.pending.push_back(WatchEvent::EnvelopeRemoved {
276 mailbox: self.mailbox_id.clone(),
277 id: id.clone(),
278 });
279 }
280 self.shadow.remove(&id);
281 }
282 }
283
284 for id in destroyed {
285 if self.shadow.remove(&id).is_some() && !self.suppress_events {
286 self.pending.push_back(WatchEvent::EnvelopeRemoved {
287 mailbox: self.mailbox_id.clone(),
288 id,
289 });
290 }
291 }
292 }
293}
294
295impl JmapCoroutine for JmapWatchMailbox {
296 type Yield = JmapWatchMailboxYield;
297 type Return = Result<(), JmapWatchMailboxError>;
298
299 fn resume(&mut self, bytes: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
300 if self.shutdown.load(Ordering::SeqCst) {
301 self.state = State::Done;
302 return JmapCoroutineState::Complete(Ok(()));
303 }
304
305 let mut bytes = bytes;
306 loop {
307 match mem::replace(&mut self.state, State::Done) {
308 State::Subscribing {
309 mut es,
310 mut latest_change,
311 } => match es.resume(bytes.take()) {
312 JmapCoroutineState::Yielded(JmapEventSourceYield::WantsRead) => {
313 self.state = State::Subscribing { es, latest_change };
314 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsRead);
315 }
316 JmapCoroutineState::Yielded(JmapEventSourceYield::WantsWrite(out)) => {
317 self.state = State::Subscribing { es, latest_change };
318 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsWrite(out));
319 }
320 JmapCoroutineState::Yielded(JmapEventSourceYield::Frame(change)) => {
321 latest_change = Some(change);
322 self.state = State::Subscribing { es, latest_change };
323 }
324 JmapCoroutineState::Complete(Ok(())) => {
325 self.state = match self.handle_cycle_end(latest_change) {
326 Ok(s) => s,
327 Err(err) => return JmapCoroutineState::Complete(Err(err)),
328 };
329 }
330 JmapCoroutineState::Complete(Err(err)) => {
331 return JmapCoroutineState::Complete(Err(err.into()));
332 }
333 },
334 State::FetchingChanges(mut changes) => match changes.resume(bytes.take()) {
335 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
336 self.state = State::FetchingChanges(changes);
337 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsRead);
338 }
339 JmapCoroutineState::Yielded(JmapYield::WantsWrite(out)) => {
340 self.state = State::FetchingChanges(changes);
341 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsWrite(out));
342 }
343 JmapCoroutineState::Complete(Err(err)) => {
344 return JmapCoroutineState::Complete(Err(err.into()));
345 }
346 JmapCoroutineState::Complete(Ok(ok)) => {
347 self.state = match self.dispatch_get(ok) {
348 Ok(s) => s,
349 Err(err) => return JmapCoroutineState::Complete(Err(err)),
350 };
351 }
352 },
353 State::FetchingEmails {
354 mut get,
355 destroyed,
356 new_state,
357 } => match get.resume(bytes.take()) {
358 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
359 self.state = State::FetchingEmails {
360 get,
361 destroyed,
362 new_state,
363 };
364 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsRead);
365 }
366 JmapCoroutineState::Yielded(JmapYield::WantsWrite(out)) => {
367 self.state = State::FetchingEmails {
368 get,
369 destroyed,
370 new_state,
371 };
372 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::WantsWrite(out));
373 }
374 JmapCoroutineState::Complete(Err(err)) => {
375 return JmapCoroutineState::Complete(Err(err.into()));
376 }
377 JmapCoroutineState::Complete(Ok(ok)) => {
378 self.apply_diff(ok.emails, destroyed);
379 self.email_state = Some(new_state);
380 self.suppress_events = false;
381 self.state = State::Emitting;
382 }
383 },
384 State::Emitting => {
385 if let Some(evt) = self.pending.pop_front() {
386 self.state = State::Emitting;
387 return JmapCoroutineState::Yielded(JmapWatchMailboxYield::Event(evt));
388 }
389 self.state = match self.fresh_subscription_state() {
390 Ok(s) => s,
391 Err(err) => return JmapCoroutineState::Complete(Err(err)),
392 };
393 }
394 State::Done => return JmapCoroutineState::Complete(Ok(())),
395 }
396 }
397 }
398}
399
400enum State {
402 Subscribing {
403 es: JmapEventSource,
404 latest_change: Option<JmapStateChange>,
405 },
406 FetchingChanges(JmapEmailChanges),
407 FetchingEmails {
408 get: JmapEmailGet,
409 destroyed: Vec<String>,
410 new_state: String,
411 },
412 Emitting,
413 Done,
414}