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 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
530pub type SyncEventHandler =
532 dyn Fn(SyncEvent) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
533
534#[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#[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}