1mod 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
534pub type SyncEventHandler =
536 dyn Fn(SyncEvent) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
537
538#[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#[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}