ecr_store/
notmuch_store.rs1use crate::error::{Error, Result};
2use crate::notmuch::Notmuch;
3use crate::paths::MailPaths;
4use crate::store::{BodyOptions, MailStore, ProgressSink};
5use crate::{discovery, mbsync, msmtp, oauth};
6use ecr_core::account::{Account, AccountId};
7use ecr_core::doctor::Doctor;
8use ecr_core::message::{
9 Body, Message, MessageId, Part, PartId, Query, SyncReport, TagOp, Thread, ThreadId,
10 ThreadSummary,
11};
12use ecr_core::revision::Revision;
13use std::sync::Arc;
14use std::time::Instant;
15
16pub struct NotmuchStore {
17 paths: Arc<MailPaths>,
18 notmuch: Notmuch,
19}
20
21impl NotmuchStore {
22 pub fn open() -> Result<Self> {
23 Ok(Self::new(Arc::new(MailPaths::discover()?)))
24 }
25
26 pub fn new(paths: Arc<MailPaths>) -> Self {
27 Self {
28 notmuch: Notmuch::new(Arc::clone(&paths)),
29 paths,
30 }
31 }
32
33 pub fn paths(&self) -> &MailPaths {
34 &self.paths
35 }
36
37 pub fn notmuch(&self) -> &Notmuch {
38 &self.notmuch
39 }
40
41 fn channels_for(&self, accounts: &[AccountId]) -> Vec<String> {
42 let discovered = discovery::accounts(&self.paths);
43 discovered
44 .into_iter()
45 .filter(|a| accounts.is_empty() || accounts.contains(&a.id))
46 .filter_map(|a| a.mbsync_channel)
47 .collect()
48 }
49}
50
51impl MailStore for NotmuchStore {
52 async fn revision(&self) -> Result<Revision> {
53 self.notmuch.revision().await
54 }
55
56 async fn accounts(&self) -> Result<Vec<Account>> {
57 Ok(discovery::accounts(&self.paths))
58 }
59
60 async fn search_threads(&self, query: &Query) -> Result<Vec<ThreadSummary>> {
61 self.notmuch.search_threads(query).await
62 }
63
64 async fn count(&self, query: &Query) -> Result<usize> {
65 self.notmuch.count(query).await
66 }
67
68 async fn count_batch(&self, queries: &[String]) -> Result<Vec<u64>> {
69 self.notmuch.count_batch(queries).await
70 }
71
72 async fn thread(&self, id: &ThreadId) -> Result<Thread> {
73 self.notmuch.thread(id).await
74 }
75
76 async fn message(&self, id: &MessageId) -> Result<Message> {
77 self.notmuch.message_with_parts(id).await
78 }
79
80 async fn body(&self, id: &MessageId, options: BodyOptions) -> Result<Body> {
81 self.notmuch
82 .body(id, options.format, options.allow_remote_resources)
83 .await
84 }
85
86 async fn part(&self, id: &MessageId, part: &PartId) -> Result<Part> {
87 self.notmuch.part(id, part).await
88 }
89
90 async fn tag(&self, ops: &[TagOp]) -> Result<Revision> {
91 self.notmuch.tag(ops).await
92 }
93
94 async fn sync(
95 &self,
96 accounts: &[AccountId],
97 progress: &dyn ProgressSink,
98 ) -> Result<SyncReport> {
99 let started = Instant::now();
100 let channels = self.channels_for(accounts);
101 let before = self.notmuch.count(&Query::new("*")).await.unwrap_or(0);
102
103 let warnings = mbsync::run(&self.paths, &channels, progress).await?;
104
105 progress.line("indexing new mail");
106 self.notmuch.index_new().await?;
107
108 let after = self.notmuch.count(&Query::new("*")).await.unwrap_or(before);
109
110 Ok(SyncReport {
111 channels,
112 new_messages: after.saturating_sub(before),
113 duration_ms: started.elapsed().as_millis() as u64,
114 warnings,
115 })
116 }
117
118 async fn send(&self, account: &AccountId, raw: &[u8]) -> Result<()> {
119 let accounts = discovery::accounts(&self.paths);
120 let found = accounts.iter().find(|a| &a.id == account);
121 let msmtp_account = found
122 .and_then(|a| a.msmtp_account.clone())
123 .unwrap_or_else(|| account.to_string());
124
125 msmtp::send(&self.paths, &msmtp_account, raw)
129 .await
130 .map_err(|e| match e {
131 Error::ToolFailed { tool, stderr } => {
132 let profile = found.and_then(|a| discovery::oauth_profile(&self.paths, a));
133 let stderr = match profile.as_deref() {
134 Some(profile) => format!("{stderr}\n\n{}", oauth::authorize_hint(profile)),
135 None => stderr,
136 };
137 Error::ToolFailed { tool, stderr }
138 }
139 other => other,
140 })
141 }
142
143 async fn doctor(&self) -> Doctor {
144 crate::doctor::run_with_paths(&self.paths).await
145 }
146}