Skip to main content

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        AdvisoryFileLock::try_lock(&left_lock_file, FileLockMode::Exclusive)
419            .map_err(|err| Error::LockFileError(err, left_lock_file_path.clone()))?;
420
421        let right_lock_file_path = RUNTIME_DIR.join(format!("{}.lock", self.right_hash));
422        debug!("locking right sync file {right_lock_file_path:?}");
423        let right_lock_file = OpenOptions::new()
424            .create(true)
425            .write(true)
426            .truncate(true)
427            .open(&right_lock_file_path)
428            .map_err(|err| Error::OpenLockFileError(err, right_lock_file_path.clone()))?;
429        AdvisoryFileLock::try_lock(&right_lock_file, FileLockMode::Exclusive)
430            .map_err(|err| Error::LockFileError(err, right_lock_file_path.clone()))?;
431
432        let mut left_cache_builder = self.get_left_cache_builder()?;
433        let left_cache_check = left_cache_builder.ctx_builder.check_configuration();
434
435        let mut left_builder = self.left_builder.clone();
436        let left_check = left_builder.ctx_builder.check_configuration();
437
438        match (left_cache_check, left_check) {
439            (Ok(()), Ok(())) => Ok(()),
440            (Ok(()), Err(err)) => Err(Error::LeftContextNotConfiguredError(err)),
441            (Err(_), Ok(())) => {
442                left_cache_builder
443                    .ctx_builder
444                    .configure()
445                    .await
446                    .map_err(Error::ConfigureLeftContextError)?;
447                Ok(())
448            }
449            (Err(_), Err(_)) => {
450                left_cache_builder
451                    .ctx_builder
452                    .configure()
453                    .await
454                    .map_err(Error::ConfigureLeftContextError)?;
455                left_builder
456                    .ctx_builder
457                    .configure()
458                    .await
459                    .map_err(Error::ConfigureLeftContextError)?;
460                Ok(())
461            }
462        }?;
463
464        let mut right_cache_builder = self.get_right_cache_builder()?;
465        let right_cache_check = right_cache_builder.ctx_builder.check_configuration();
466
467        let mut right_builder = self.right_builder.clone();
468        let right_check = right_builder.ctx_builder.check_configuration();
469
470        match (right_cache_check, right_check) {
471            (Ok(()), Ok(())) => Ok(()),
472            (Ok(()), Err(err)) => Err(Error::RightContextNotConfiguredError(err)),
473            (Err(_), Ok(())) => {
474                right_cache_builder
475                    .ctx_builder
476                    .configure()
477                    .await
478                    .map_err(Error::ConfigureRightContextError)?;
479                Ok(())
480            }
481            (Err(_), Err(_)) => {
482                right_cache_builder
483                    .ctx_builder
484                    .configure()
485                    .await
486                    .map_err(Error::ConfigureRightContextError)?;
487                right_builder
488                    .ctx_builder
489                    .configure()
490                    .await
491                    .map_err(Error::ConfigureRightContextError)?;
492                Ok(())
493            }
494        }?;
495
496        let ctx = Arc::new(
497            SyncPoolContextBuilder::new(
498                self.config,
499                left_cache_builder,
500                left_builder,
501                right_cache_builder,
502                right_builder,
503            )
504            .build()
505            .await
506            .map_err(Error::BuildSyncPoolContextError)?,
507        );
508
509        let mut report = SyncReport::default();
510
511        report.folder = folder::sync::<L, R>(ctx.clone())
512            .await
513            .map_err(Error::SyncFoldersError)?;
514        report.email = email::sync::<L, R>(ctx.clone(), &report.folder.names)
515            .await
516            .map_err(Error::SyncEmailsError)?;
517
518        folder::sync::expunge::<L, R>(ctx.clone(), &report.folder.names).await;
519
520        debug!("unlocking sync files");
521        AdvisoryFileLock::unlock(&left_lock_file)
522            .map_err(|err| Error::UnlockFileError(err, left_lock_file_path))?;
523        AdvisoryFileLock::unlock(&right_lock_file)
524            .map_err(|err| Error::UnlockFileError(err, right_lock_file_path))?;
525
526        Ok(report)
527    }
528}
529
530/// The synchronization async event handler.
531pub type SyncEventHandler =
532    dyn Fn(SyncEvent) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
533
534/// The synchronization event.
535///
536/// Represents all the events that can be triggered during the
537/// backends synchronization process.
538#[derive(Clone, Debug, Eq, PartialEq, Hash)]
539pub enum SyncEvent {
540    ListedLeftCachedFolders(usize),
541    ListedLeftFolders(usize),
542    ListedRightCachedFolders(usize),
543    ListedRightFolders(usize),
544    ListedAllFolders,
545    GeneratedFolderPatch(BTreeMap<FolderName, FolderSyncPatch>),
546    ProcessedFolderHunk(FolderSyncHunk),
547    ProcessedAllFolderHunks,
548    ListedLeftCachedEnvelopes(FolderName, usize),
549    ListedLeftEnvelopes(FolderName, usize),
550    ListedRightCachedEnvelopes(FolderName, usize),
551    ListedRightEnvelopes(FolderName, usize),
552    GeneratedEmailPatch(BTreeMap<FolderName, BTreeSet<EmailSyncHunk>>),
553    ProcessedEmailHunk(EmailSyncHunk),
554    ProcessedAllEmailHunks,
555    ExpungedAllFolders,
556}
557
558impl SyncEvent {
559    pub async fn emit(&self, handler: &Option<Arc<SyncEventHandler>>) {
560        if let Some(handler) = handler.as_ref() {
561            if let Err(err) = handler(self.clone()).await {
562                debug!(?err, "error while emitting sync event");
563            } else {
564                debug!("emitted sync event {self:?}");
565            }
566        }
567    }
568}
569
570impl fmt::Display for SyncEvent {
571    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572        match self {
573            SyncEvent::ListedLeftCachedFolders(n) => {
574                write!(f, "Listed {n} left cached folders")
575            }
576            SyncEvent::ListedLeftFolders(n) => {
577                write!(f, "Listed {n} left folders")
578            }
579            SyncEvent::ListedRightCachedFolders(n) => {
580                write!(f, "Listed {n} right cached folders")
581            }
582            SyncEvent::ListedRightFolders(n) => {
583                write!(f, "Listed {n} right folders")
584            }
585            SyncEvent::ListedAllFolders => {
586                write!(f, "Listed all folders")
587            }
588            SyncEvent::GeneratedFolderPatch(patch) => {
589                let n = patch.keys().count();
590                let p = patch.values().flatten().count();
591                write!(f, "Generated {p} patch for {n} folders")
592            }
593            SyncEvent::ProcessedFolderHunk(hunk) => {
594                write!(f, "{hunk}")
595            }
596            SyncEvent::ProcessedAllFolderHunks => {
597                write!(f, "Processed all folder hunks")
598            }
599            SyncEvent::ListedLeftCachedEnvelopes(folder, n) => {
600                write!(f, "Listed {n} left cached envelopes from {folder}")
601            }
602            SyncEvent::ListedLeftEnvelopes(folder, n) => {
603                write!(f, "Listed {n} left envelopes from {folder}")
604            }
605            SyncEvent::ListedRightCachedEnvelopes(folder, n) => {
606                write!(f, "Listed {n} right cached envelopes from {folder}")
607            }
608            SyncEvent::ListedRightEnvelopes(folder, n) => {
609                write!(f, "Listed {n} right envelopes from {folder}")
610            }
611            SyncEvent::GeneratedEmailPatch(patch) => {
612                let nf = patch.keys().count();
613                let np = patch.values().flatten().count();
614                write!(f, "Generated {np} patch for {nf} folders")
615            }
616            SyncEvent::ProcessedEmailHunk(hunk) => {
617                write!(f, "{hunk}")
618            }
619            SyncEvent::ProcessedAllEmailHunks => {
620                write!(f, "Processed all email hunks")
621            }
622            SyncEvent::ExpungedAllFolders => {
623                write!(f, "Expunged all folders")
624            }
625        }
626    }
627}
628
629/// The synchronization destination.
630#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
631pub enum SyncDestination {
632    Left,
633    Right,
634}
635
636impl fmt::Display for SyncDestination {
637    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638        match self {
639            Self::Left => write!(f, "left"),
640            Self::Right => write!(f, "right"),
641        }
642    }
643}