email/sync/
mod.rs

1//! # Synchronization
2//!
3//! Module dedicated to synchronization of folders and emails between
4//! two backends. The main structure of this module is
5//! [`SyncBuilder`].
6
7mod error;
8pub mod hash;
9pub mod pool;
10pub mod report;
11
12use std::{
13    collections::{BTreeMap, BTreeSet},
14    env, fmt,
15    fs::{self, OpenOptions},
16    future::Future,
17    hash::{DefaultHasher, Hash, Hasher},
18    path::PathBuf,
19    pin::Pin,
20    sync::Arc,
21};
22
23use advisory_lock::{AdvisoryFileLock, FileLockMode};
24use dirs::{cache_dir, runtime_dir};
25use once_cell::sync::Lazy;
26use tracing::debug;
27
28#[doc(inline)]
29pub use self::error::{Error, Result};
30use self::{hash::SyncHash, report::SyncReport};
31use crate::{
32    backend::{context::BackendContextBuilder, BackendBuilder},
33    email::{self, sync::hunk::EmailSyncHunk},
34    envelope::sync::config::EnvelopeSyncFilters,
35    flag::sync::config::FlagSyncPermissions,
36    folder::{
37        self,
38        sync::{
39            config::{FolderSyncPermissions, FolderSyncStrategy},
40            hunk::{FolderName, FolderSyncHunk},
41            patch::FolderSyncPatch,
42        },
43    },
44    maildir::{config::MaildirConfig, MaildirContextBuilder},
45    message::sync::config::MessageSyncPermissions,
46    sync::pool::{SyncPoolConfig, SyncPoolContextBuilder},
47};
48
49static RUNTIME_DIR: Lazy<PathBuf> = Lazy::new(|| {
50    let dir = runtime_dir()
51        .unwrap_or_else(env::temp_dir)
52        .join("pimalaya")
53        .join("email")
54        .join("sync");
55    fs::create_dir_all(&dir).expect(&format!("should create runtime directory {dir:?}"));
56    dir
57});
58
59/// The synchronization builder.
60#[derive(Clone)]
61pub struct SyncBuilder<L: BackendContextBuilder + SyncHash, R: BackendContextBuilder + SyncHash> {
62    config: SyncPoolConfig,
63    left_builder: BackendBuilder<L>,
64    left_hash: String,
65    right_builder: BackendBuilder<R>,
66    right_hash: String,
67    cache_dir: Option<PathBuf>,
68}
69
70impl<L, R> SyncBuilder<L, R>
71where
72    L: BackendContextBuilder + SyncHash + 'static,
73    R: BackendContextBuilder + SyncHash + 'static,
74{
75    /// Create a new synchronization builder using the two given
76    /// backend builders.
77    pub fn new(left_builder: BackendBuilder<L>, right_builder: BackendBuilder<R>) -> Self {
78        let mut left_hasher = DefaultHasher::new();
79        left_builder.sync_hash(&mut left_hasher);
80        let left_hash = format!("{:x}", left_hasher.finish());
81
82        let mut right_hasher = DefaultHasher::new();
83        right_builder.sync_hash(&mut right_hasher);
84        let right_hash = format!("{:x}", right_hasher.finish());
85
86        Self {
87            config: Default::default(),
88            left_builder,
89            left_hash,
90            right_builder,
91            right_hash,
92            cache_dir: None,
93        }
94    }
95
96    // cache dir setters
97
98    pub fn set_some_cache_dir(&mut self, dir: Option<impl Into<PathBuf>>) {
99        self.cache_dir = dir.map(Into::into);
100    }
101
102    pub fn set_cache_dir(&mut self, dir: impl Into<PathBuf>) {
103        self.set_some_cache_dir(Some(dir));
104    }
105
106    pub fn with_some_cache_dir(mut self, dir: Option<impl Into<PathBuf>>) -> Self {
107        self.set_some_cache_dir(dir);
108        self
109    }
110
111    pub fn with_cache_dir(mut self, dir: impl Into<PathBuf>) -> Self {
112        self.set_cache_dir(dir);
113        self
114    }
115
116    // handler setters
117
118    pub fn set_some_handler<F: Future<Output = Result<()>> + Send + 'static>(
119        &mut self,
120        handler: Option<impl Fn(SyncEvent) -> F + Send + Sync + 'static>,
121    ) {
122        self.config.handler = match handler {
123            Some(handler) => Some(Arc::new(move |evt| Box::pin(handler(evt)))),
124            None => None,
125        };
126    }
127
128    pub fn set_handler<F: Future<Output = Result<()>> + Send + 'static>(
129        &mut self,
130        handler: impl Fn(SyncEvent) -> F + Send + Sync + 'static,
131    ) {
132        self.set_some_handler(Some(handler));
133    }
134
135    pub fn with_some_handler<F: Future<Output = Result<()>> + Send + 'static>(
136        mut self,
137        handler: Option<impl Fn(SyncEvent) -> F + Send + Sync + 'static>,
138    ) -> Self {
139        self.set_some_handler(handler);
140        self
141    }
142
143    pub fn with_handler<F: Future<Output = Result<()>> + Send + 'static>(
144        mut self,
145        handler: impl Fn(SyncEvent) -> F + Send + Sync + 'static,
146    ) -> Self {
147        self.set_handler(handler);
148        self
149    }
150
151    // dry run setters and getter
152
153    pub fn set_some_dry_run(&mut self, dry_run: Option<bool>) {
154        self.config.dry_run = dry_run;
155    }
156
157    pub fn set_dry_run(&mut self, dry_run: bool) {
158        self.set_some_dry_run(Some(dry_run));
159    }
160
161    pub fn with_some_dry_run(mut self, dry_run: Option<bool>) -> Self {
162        self.set_some_dry_run(dry_run);
163        self
164    }
165
166    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
167        self.set_dry_run(dry_run);
168        self
169    }
170
171    pub fn get_dry_run(&self) -> bool {
172        self.config.dry_run.unwrap_or_default()
173    }
174
175    // folder filters setters
176
177    pub fn set_some_folder_filters(&mut self, f: Option<impl Into<FolderSyncStrategy>>) {
178        self.config.folder_filters = f.map(Into::into);
179    }
180
181    pub fn set_folder_filters(&mut self, f: impl Into<FolderSyncStrategy>) {
182        self.set_some_folder_filters(Some(f));
183    }
184
185    pub fn with_some_folder_filters(mut self, f: Option<impl Into<FolderSyncStrategy>>) -> Self {
186        self.set_some_folder_filters(f);
187        self
188    }
189
190    pub fn with_folder_filters(mut self, f: impl Into<FolderSyncStrategy>) -> Self {
191        self.set_folder_filters(f);
192        self
193    }
194
195    // left folder permissions setters
196
197    pub fn set_some_left_folder_permissions(
198        &mut self,
199        p: Option<impl Into<FolderSyncPermissions>>,
200    ) {
201        self.config.left_folder_permissions = p.map(Into::into);
202    }
203
204    pub fn set_left_folder_permissions(&mut self, p: impl Into<FolderSyncPermissions>) {
205        self.set_some_left_folder_permissions(Some(p));
206    }
207
208    pub fn with_some_left_folder_permissions(
209        mut self,
210        p: Option<impl Into<FolderSyncPermissions>>,
211    ) -> Self {
212        self.set_some_left_folder_permissions(p);
213        self
214    }
215
216    pub fn with_left_folder_permissions(mut self, p: impl Into<FolderSyncPermissions>) -> Self {
217        self.set_left_folder_permissions(p);
218        self
219    }
220
221    // right folder permissions setters
222
223    pub fn set_some_right_folder_permissions(
224        &mut self,
225        p: Option<impl Into<FolderSyncPermissions>>,
226    ) {
227        self.config.right_folder_permissions = p.map(Into::into);
228    }
229
230    pub fn set_right_folder_permissions(&mut self, p: impl Into<FolderSyncPermissions>) {
231        self.set_some_right_folder_permissions(Some(p));
232    }
233
234    pub fn with_some_right_folder_permissions(
235        mut self,
236        p: Option<impl Into<FolderSyncPermissions>>,
237    ) -> Self {
238        self.set_some_right_folder_permissions(p);
239        self
240    }
241
242    pub fn with_right_folder_permissions(mut self, p: impl Into<FolderSyncPermissions>) -> Self {
243        self.set_right_folder_permissions(p);
244        self
245    }
246
247    // envelope filters setters
248
249    pub fn set_some_envelope_filters(&mut self, f: Option<impl Into<EnvelopeSyncFilters>>) {
250        self.config.envelope_filters = f.map(Into::into);
251    }
252
253    pub fn set_envelope_filters(&mut self, f: impl Into<EnvelopeSyncFilters>) {
254        self.set_some_envelope_filters(Some(f));
255    }
256
257    pub fn with_some_envelope_filters(mut self, f: Option<impl Into<EnvelopeSyncFilters>>) -> Self {
258        self.set_some_envelope_filters(f);
259        self
260    }
261
262    pub fn with_envelope_filters(mut self, f: impl Into<EnvelopeSyncFilters>) -> Self {
263        self.set_envelope_filters(f);
264        self
265    }
266
267    // left flag permissions setters
268
269    pub fn set_some_left_flag_permissions(&mut self, p: Option<impl Into<FlagSyncPermissions>>) {
270        self.config.left_flag_permissions = p.map(Into::into);
271    }
272
273    pub fn set_left_flag_permissions(&mut self, p: impl Into<FlagSyncPermissions>) {
274        self.set_some_left_flag_permissions(Some(p));
275    }
276
277    pub fn with_some_left_flag_permissions(
278        mut self,
279        p: Option<impl Into<FlagSyncPermissions>>,
280    ) -> Self {
281        self.set_some_left_flag_permissions(p);
282        self
283    }
284
285    pub fn with_left_flag_permissions(mut self, p: impl Into<FlagSyncPermissions>) -> Self {
286        self.set_left_flag_permissions(p);
287        self
288    }
289
290    // right flag permissions setters
291
292    pub fn set_some_right_flag_permissions(&mut self, p: Option<impl Into<FlagSyncPermissions>>) {
293        self.config.right_flag_permissions = p.map(Into::into);
294    }
295
296    pub fn set_right_flag_permissions(&mut self, p: impl Into<FlagSyncPermissions>) {
297        self.set_some_right_flag_permissions(Some(p));
298    }
299
300    pub fn with_some_right_flag_permissions(
301        mut self,
302        p: Option<impl Into<FlagSyncPermissions>>,
303    ) -> Self {
304        self.set_some_right_flag_permissions(p);
305        self
306    }
307
308    pub fn with_right_flag_permissions(mut self, p: impl Into<FlagSyncPermissions>) -> Self {
309        self.set_right_flag_permissions(p);
310        self
311    }
312
313    // left message permissions setters
314
315    pub fn set_some_left_message_permissions(
316        &mut self,
317        p: Option<impl Into<MessageSyncPermissions>>,
318    ) {
319        self.config.left_message_permissions = p.map(Into::into);
320    }
321
322    pub fn set_left_message_permissions(&mut self, p: impl Into<MessageSyncPermissions>) {
323        self.set_some_left_message_permissions(Some(p));
324    }
325
326    pub fn with_some_left_message_permissions(
327        mut self,
328        p: Option<impl Into<MessageSyncPermissions>>,
329    ) -> Self {
330        self.set_some_left_message_permissions(p);
331        self
332    }
333
334    pub fn with_left_message_permissions(mut self, p: impl Into<MessageSyncPermissions>) -> Self {
335        self.set_left_message_permissions(p);
336        self
337    }
338
339    // right message permissions setters
340
341    pub fn set_some_right_message_permissions(
342        &mut self,
343        p: Option<impl Into<MessageSyncPermissions>>,
344    ) {
345        self.config.right_message_permissions = p.map(Into::into);
346    }
347
348    pub fn set_right_message_permissions(&mut self, p: impl Into<MessageSyncPermissions>) {
349        self.set_some_right_message_permissions(Some(p));
350    }
351
352    pub fn with_some_right_message_permissions(
353        mut self,
354        p: Option<impl Into<MessageSyncPermissions>>,
355    ) -> Self {
356        self.set_some_right_message_permissions(p);
357        self
358    }
359
360    pub fn with_right_message_permissions(mut self, p: impl Into<MessageSyncPermissions>) -> Self {
361        self.set_right_message_permissions(p);
362        self
363    }
364
365    // getters
366
367    pub fn find_default_cache_dir(&self) -> Option<PathBuf> {
368        cache_dir().map(|dir| dir.join("pimalaya").join("email").join("sync"))
369    }
370
371    pub fn get_cache_dir(&self) -> Result<PathBuf> {
372        self.cache_dir
373            .as_ref()
374            .cloned()
375            .or_else(|| self.find_default_cache_dir())
376            .ok_or(Error::GetCacheDirectorySyncError.into())
377    }
378
379    pub fn get_left_cache_builder(&self) -> Result<BackendBuilder<MaildirContextBuilder>> {
380        let left_config = self.left_builder.account_config.clone();
381        let root_dir = self.get_cache_dir()?.join(&self.left_hash);
382        let ctx = MaildirContextBuilder::new(
383            left_config.clone(),
384            Arc::new(MaildirConfig {
385                root_dir,
386                maildirpp: false,
387            }),
388        );
389        let left_cache_builder = BackendBuilder::new(left_config, ctx);
390        Ok(left_cache_builder)
391    }
392
393    pub fn get_right_cache_builder(&self) -> Result<BackendBuilder<MaildirContextBuilder>> {
394        let right_config = self.right_builder.account_config.clone();
395        let root_dir = self.get_cache_dir()?.join(&self.right_hash);
396        let ctx = MaildirContextBuilder::new(
397            right_config.clone(),
398            Arc::new(MaildirConfig {
399                root_dir,
400                maildirpp: false,
401            }),
402        );
403        let right_cache_builder = BackendBuilder::new(right_config, ctx);
404        Ok(right_cache_builder)
405    }
406
407    // build
408
409    pub async fn sync(self) -> Result<SyncReport> {
410        let left_lock_file_path = RUNTIME_DIR.join(format!("{}.lock", self.left_hash));
411        debug!("locking left sync file {left_lock_file_path:?}");
412        let left_lock_file = OpenOptions::new()
413            .create(true)
414            .write(true)
415            .truncate(true)
416            .open(&left_lock_file_path)
417            .map_err(|err| Error::OpenLockFileError(err, left_lock_file_path.clone()))?;
418        left_lock_file
419            .try_lock(FileLockMode::Exclusive)
420            .map_err(|err| Error::LockFileError(err, left_lock_file_path.clone()))?;
421
422        let right_lock_file_path = RUNTIME_DIR.join(format!("{}.lock", self.right_hash));
423        debug!("locking right sync file {right_lock_file_path:?}");
424        let right_lock_file = OpenOptions::new()
425            .create(true)
426            .write(true)
427            .truncate(true)
428            .open(&right_lock_file_path)
429            .map_err(|err| Error::OpenLockFileError(err, right_lock_file_path.clone()))?;
430        right_lock_file
431            .try_lock(FileLockMode::Exclusive)
432            .map_err(|err| Error::LockFileError(err, right_lock_file_path.clone()))?;
433
434        let mut left_cache_builder = self.get_left_cache_builder()?;
435        let left_cache_check = left_cache_builder.ctx_builder.check_configuration();
436
437        let mut left_builder = self.left_builder.clone();
438        let left_check = left_builder.ctx_builder.check_configuration();
439
440        match (left_cache_check, left_check) {
441            (Ok(()), Ok(())) => Ok(()),
442            (Ok(()), Err(err)) => Err(Error::LeftContextNotConfiguredError(err)),
443            (Err(_), Ok(())) => {
444                left_cache_builder
445                    .ctx_builder
446                    .configure()
447                    .await
448                    .map_err(Error::ConfigureLeftContextError)?;
449                Ok(())
450            }
451            (Err(_), Err(_)) => {
452                left_cache_builder
453                    .ctx_builder
454                    .configure()
455                    .await
456                    .map_err(Error::ConfigureLeftContextError)?;
457                left_builder
458                    .ctx_builder
459                    .configure()
460                    .await
461                    .map_err(Error::ConfigureLeftContextError)?;
462                Ok(())
463            }
464        }?;
465
466        let mut right_cache_builder = self.get_right_cache_builder()?;
467        let right_cache_check = right_cache_builder.ctx_builder.check_configuration();
468
469        let mut right_builder = self.right_builder.clone();
470        let right_check = right_builder.ctx_builder.check_configuration();
471
472        match (right_cache_check, right_check) {
473            (Ok(()), Ok(())) => Ok(()),
474            (Ok(()), Err(err)) => Err(Error::RightContextNotConfiguredError(err)),
475            (Err(_), Ok(())) => {
476                right_cache_builder
477                    .ctx_builder
478                    .configure()
479                    .await
480                    .map_err(Error::ConfigureRightContextError)?;
481                Ok(())
482            }
483            (Err(_), Err(_)) => {
484                right_cache_builder
485                    .ctx_builder
486                    .configure()
487                    .await
488                    .map_err(Error::ConfigureRightContextError)?;
489                right_builder
490                    .ctx_builder
491                    .configure()
492                    .await
493                    .map_err(Error::ConfigureRightContextError)?;
494                Ok(())
495            }
496        }?;
497
498        let ctx = Arc::new(
499            SyncPoolContextBuilder::new(
500                self.config,
501                left_cache_builder,
502                left_builder,
503                right_cache_builder,
504                right_builder,
505            )
506            .build()
507            .await
508            .map_err(Error::BuildSyncPoolContextError)?,
509        );
510
511        let mut report = SyncReport::default();
512
513        report.folder = folder::sync::<L, R>(ctx.clone())
514            .await
515            .map_err(Error::SyncFoldersError)?;
516        report.email = email::sync::<L, R>(ctx.clone(), &report.folder.names)
517            .await
518            .map_err(Error::SyncEmailsError)?;
519
520        folder::sync::expunge::<L, R>(ctx.clone(), &report.folder.names).await;
521
522        debug!("unlocking sync files");
523        left_lock_file
524            .unlock()
525            .map_err(|err| Error::UnlockFileError(err, left_lock_file_path))?;
526        right_lock_file
527            .unlock()
528            .map_err(|err| Error::UnlockFileError(err, right_lock_file_path))?;
529
530        Ok(report)
531    }
532}
533
534/// The synchronization async event handler.
535pub type SyncEventHandler =
536    dyn Fn(SyncEvent) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
537
538/// The synchronization event.
539///
540/// Represents all the events that can be triggered during the
541/// backends synchronization process.
542#[derive(Clone, Debug, Eq, PartialEq, Hash)]
543pub enum SyncEvent {
544    ListedLeftCachedFolders(usize),
545    ListedLeftFolders(usize),
546    ListedRightCachedFolders(usize),
547    ListedRightFolders(usize),
548    ListedAllFolders,
549    GeneratedFolderPatch(BTreeMap<FolderName, FolderSyncPatch>),
550    ProcessedFolderHunk(FolderSyncHunk),
551    ProcessedAllFolderHunks,
552    ListedLeftCachedEnvelopes(FolderName, usize),
553    ListedLeftEnvelopes(FolderName, usize),
554    ListedRightCachedEnvelopes(FolderName, usize),
555    ListedRightEnvelopes(FolderName, usize),
556    GeneratedEmailPatch(BTreeMap<FolderName, BTreeSet<EmailSyncHunk>>),
557    ProcessedEmailHunk(EmailSyncHunk),
558    ProcessedAllEmailHunks,
559    ExpungedAllFolders,
560}
561
562impl SyncEvent {
563    pub async fn emit(&self, handler: &Option<Arc<SyncEventHandler>>) {
564        if let Some(handler) = handler.as_ref() {
565            if let Err(err) = handler(self.clone()).await {
566                debug!(?err, "error while emitting sync event");
567            } else {
568                debug!("emitted sync event {self:?}");
569            }
570        }
571    }
572}
573
574impl fmt::Display for SyncEvent {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        match self {
577            SyncEvent::ListedLeftCachedFolders(n) => {
578                write!(f, "Listed {n} left cached folders")
579            }
580            SyncEvent::ListedLeftFolders(n) => {
581                write!(f, "Listed {n} left folders")
582            }
583            SyncEvent::ListedRightCachedFolders(n) => {
584                write!(f, "Listed {n} right cached folders")
585            }
586            SyncEvent::ListedRightFolders(n) => {
587                write!(f, "Listed {n} right folders")
588            }
589            SyncEvent::ListedAllFolders => {
590                write!(f, "Listed all folders")
591            }
592            SyncEvent::GeneratedFolderPatch(patch) => {
593                let n = patch.keys().count();
594                let p = patch.values().flatten().count();
595                write!(f, "Generated {p} patch for {n} folders")
596            }
597            SyncEvent::ProcessedFolderHunk(hunk) => {
598                write!(f, "{hunk}")
599            }
600            SyncEvent::ProcessedAllFolderHunks => {
601                write!(f, "Processed all folder hunks")
602            }
603            SyncEvent::ListedLeftCachedEnvelopes(folder, n) => {
604                write!(f, "Listed {n} left cached envelopes from {folder}")
605            }
606            SyncEvent::ListedLeftEnvelopes(folder, n) => {
607                write!(f, "Listed {n} left envelopes from {folder}")
608            }
609            SyncEvent::ListedRightCachedEnvelopes(folder, n) => {
610                write!(f, "Listed {n} right cached envelopes from {folder}")
611            }
612            SyncEvent::ListedRightEnvelopes(folder, n) => {
613                write!(f, "Listed {n} right envelopes from {folder}")
614            }
615            SyncEvent::GeneratedEmailPatch(patch) => {
616                let nf = patch.keys().count();
617                let np = patch.values().flatten().count();
618                write!(f, "Generated {np} patch for {nf} folders")
619            }
620            SyncEvent::ProcessedEmailHunk(hunk) => {
621                write!(f, "{hunk}")
622            }
623            SyncEvent::ProcessedAllEmailHunks => {
624                write!(f, "Processed all email hunks")
625            }
626            SyncEvent::ExpungedAllFolders => {
627                write!(f, "Expunged all folders")
628            }
629        }
630    }
631}
632
633/// The synchronization destination.
634#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
635pub enum SyncDestination {
636    Left,
637    Right,
638}
639
640impl fmt::Display for SyncDestination {
641    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642        match self {
643            Self::Left => write!(f, "left"),
644            Self::Right => write!(f, "right"),
645        }
646    }
647}