1use alloc::{
8 collections::{BTreeMap, BTreeSet},
9 string::String,
10 vec::Vec,
11};
12use std::{
13 collections::hash_map::RandomState,
14 fs,
15 hash::{BuildHasher, Hasher},
16 io,
17 path::{Path, PathBuf},
18 process,
19};
20
21use io_m2dir::{client::M2dirClient as InnerM2dirClient, coroutine::*, path::M2dirPath};
22use log::trace;
23use thiserror::Error;
24
25#[cfg(feature = "search")]
26use crate::{
27 envelope::m2dir::search::{M2dirEnvelopeSearch, M2dirEnvelopeSearchError},
28 search::query::SearchEmailsQuery,
29};
30use crate::{
31 envelope::{
32 m2dir::list::{M2dirEnvelopeList, M2dirEnvelopeListError},
33 types::Envelope,
34 },
35 flag::{
36 m2dir::store::{M2dirFlagStore, M2dirFlagStoreError},
37 types::{Flag, FlagOp},
38 },
39 mailbox::{
40 m2dir::{
41 create::{M2dirMailboxCreate, M2dirMailboxCreateError},
42 delete::{M2dirMailboxDelete, M2dirMailboxDeleteError},
43 list::{M2dirMailboxList, M2dirMailboxListError},
44 },
45 types::Mailbox,
46 },
47 message::m2dir::{
48 add::{M2dirMessageAdd, M2dirMessageAddError},
49 copy::{M2dirMessageCopy, M2dirMessageCopyError},
50 delete::{M2dirMessageDelete, M2dirMessageDeleteError},
51 get::{M2dirMessageGet, M2dirMessageGetError},
52 r#move::{M2dirMessageMove, M2dirMessageMoveError},
53 },
54};
55
56#[derive(Debug, Error)]
60pub enum M2dirClientError {
61 #[error(transparent)]
62 Io(#[from] io::Error),
63 #[error(transparent)]
64 MailboxList(#[from] M2dirMailboxListError),
65 #[error(transparent)]
66 EnvelopeList(#[from] M2dirEnvelopeListError),
67 #[cfg(feature = "search")]
68 #[error(transparent)]
69 EnvelopeSearch(#[from] M2dirEnvelopeSearchError),
70 #[error(transparent)]
71 FlagStore(#[from] M2dirFlagStoreError),
72 #[error(transparent)]
73 MailboxCreate(#[from] M2dirMailboxCreateError),
74 #[error(transparent)]
75 MailboxDelete(#[from] M2dirMailboxDeleteError),
76 #[error(transparent)]
77 MessageAdd(#[from] M2dirMessageAddError),
78 #[error(transparent)]
79 MessageCopy(#[from] M2dirMessageCopyError),
80 #[error(transparent)]
81 MessageDelete(#[from] M2dirMessageDeleteError),
82 #[error(transparent)]
83 MessageGet(#[from] M2dirMessageGetError),
84 #[error(transparent)]
85 MessageMove(#[from] M2dirMessageMoveError),
86 #[error(transparent)]
87 Inner(#[from] io_m2dir::client::M2dirClientError),
88}
89
90pub struct M2dirClient {
95 pub inner: InnerM2dirClient,
96}
97
98impl M2dirClient {
99 pub fn new(root: impl Into<M2dirPath>) -> Self {
102 Self {
103 inner: InnerM2dirClient::new(root),
104 }
105 }
106
107 pub fn run<C, T, E>(&self, mut coroutine: C) -> Result<T, M2dirClientError>
114 where
115 C: M2dirCoroutine<Yield = M2dirYield, Return = Result<T, E>>,
116 M2dirClientError: From<E>,
117 {
118 let mut arg: Option<M2dirArg> = None;
119
120 loop {
121 match coroutine.resume(arg.take()) {
122 M2dirCoroutineState::Complete(Ok(out)) => return Ok(out),
123 M2dirCoroutineState::Complete(Err(err)) => return Err(err.into()),
124 M2dirCoroutineState::Yielded(M2dirYield::WantsPid) => {
125 arg = Some(M2dirArg::Pid(process::id()));
126 }
127 M2dirCoroutineState::Yielded(M2dirYield::WantsRandom { len }) => {
128 arg = Some(M2dirArg::Random(random_bytes(len)));
129 }
130 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(paths)) => {
131 arg = Some(M2dirArg::FileExists(file_exists(paths)));
132 }
133 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => {
134 arg = Some(M2dirArg::DirRead(read_dirs(paths)?));
135 }
136 M2dirCoroutineState::Yielded(M2dirYield::WantsDirCreate(paths)) => {
137 create_dirs(paths)?;
138 arg = Some(M2dirArg::DirCreate);
139 }
140 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRemove(paths)) => {
141 remove_dirs(paths)?;
142 arg = Some(M2dirArg::DirRemove);
143 }
144 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => {
145 arg = Some(M2dirArg::FileRead(read_files_tolerant(paths)?));
146 }
147 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => {
148 write_files(files)?;
149 arg = Some(M2dirArg::FileCreate);
150 }
151 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => {
152 remove_files_tolerant(paths)?;
153 arg = Some(M2dirArg::FileRemove);
154 }
155 M2dirCoroutineState::Yielded(M2dirYield::WantsRename(pairs)) => {
156 rename_paths(pairs)?;
157 arg = Some(M2dirArg::Rename);
158 }
159 }
160 }
161 }
162
163 pub fn list_mailboxes(&self, with_counts: bool) -> Result<Vec<Mailbox>, M2dirClientError> {
167 self.run(M2dirMailboxList::new(
168 PathBuf::from(self.inner.root().as_str()),
169 with_counts,
170 ))
171 }
172
173 pub fn list_envelopes(
177 &self,
178 mailbox: &str,
179 page: Option<u32>,
180 page_size: Option<u32>,
181 with_attachment: bool,
182 ) -> Result<Vec<Envelope>, M2dirClientError> {
183 self.run(M2dirEnvelopeList::new(
184 PathBuf::from(self.inner.root().as_str()),
185 mailbox,
186 page,
187 page_size,
188 with_attachment,
189 )?)
190 }
191
192 #[cfg(feature = "search")]
195 pub fn search_envelopes(
196 &self,
197 mailbox: &str,
198 query: Option<&SearchEmailsQuery>,
199 page: Option<u32>,
200 page_size: Option<u32>,
201 with_attachment: bool,
202 ) -> Result<Vec<Envelope>, M2dirClientError> {
203 self.run(M2dirEnvelopeSearch::new(
204 PathBuf::from(self.inner.root().as_str()),
205 mailbox,
206 query,
207 page,
208 page_size,
209 with_attachment,
210 )?)
211 }
212
213 pub fn store_flags(
216 &self,
217 mailbox: &str,
218 ids: &[&str],
219 flags: &[Flag],
220 op: FlagOp,
221 ) -> Result<(), M2dirClientError> {
222 self.run(M2dirFlagStore::new(
223 PathBuf::from(self.inner.root().as_str()),
224 mailbox,
225 ids,
226 flags,
227 op,
228 )?)
229 }
230
231 pub fn get_message(&self, mailbox: &str, id: &str) -> Result<Vec<u8>, M2dirClientError> {
234 self.run(M2dirMessageGet::new(
235 PathBuf::from(self.inner.root().as_str()),
236 mailbox,
237 id,
238 )?)
239 }
240
241 pub fn add_message(
245 &self,
246 mailbox: &str,
247 flags: &[Flag],
248 raw: Vec<u8>,
249 ) -> Result<String, M2dirClientError> {
250 self.run(M2dirMessageAdd::new(
251 PathBuf::from(self.inner.root().as_str()),
252 mailbox,
253 flags,
254 raw,
255 )?)
256 }
257
258 pub fn create_mailbox(&self, name: &str) -> Result<(), M2dirClientError> {
261 self.run(M2dirMailboxCreate::new(
262 PathBuf::from(self.inner.root().as_str()),
263 name,
264 )?)
265 }
266
267 pub fn delete_mailbox(&self, name: &str) -> Result<(), M2dirClientError> {
269 self.run(M2dirMailboxDelete::new(
270 PathBuf::from(self.inner.root().as_str()),
271 name,
272 )?)
273 }
274
275 pub fn delete_message(&self, mailbox: &str, id: &str) -> Result<(), M2dirClientError> {
277 self.run(M2dirMessageDelete::new(
278 PathBuf::from(self.inner.root().as_str()),
279 mailbox,
280 id,
281 )?)
282 }
283
284 pub fn copy_messages(
288 &self,
289 from: &str,
290 to: &str,
291 ids: &[&str],
292 ) -> Result<(), M2dirClientError> {
293 self.run(M2dirMessageCopy::new(
294 PathBuf::from(self.inner.root().as_str()),
295 from,
296 to,
297 ids,
298 )?)
299 }
300
301 pub fn move_messages(
305 &self,
306 from: &str,
307 to: &str,
308 ids: &[&str],
309 ) -> Result<(), M2dirClientError> {
310 self.run(M2dirMessageMove::new(
311 PathBuf::from(self.inner.root().as_str()),
312 from,
313 to,
314 ids,
315 )?)
316 }
317}
318
319fn create_dirs(paths: BTreeSet<M2dirPath>) -> Result<(), io::Error> {
322 for path in paths {
323 trace!("create_dir_all {path}");
324 fs::create_dir_all(path.as_str())?;
325 }
326 Ok(())
327}
328
329fn remove_dirs(paths: BTreeSet<M2dirPath>) -> Result<(), io::Error> {
330 for path in paths {
331 trace!("remove_dir_all {path}");
332 fs::remove_dir_all(path.as_str())?;
333 }
334 Ok(())
335}
336
337fn write_files(files: BTreeMap<M2dirPath, Vec<u8>>) -> Result<(), io::Error> {
338 for (path, contents) in files {
339 trace!("write {path} ({} bytes)", contents.len());
340
341 if let Some(parent) = Path::new(path.as_str()).parent() {
342 fs::create_dir_all(parent)?;
343 }
344 fs::write(path.as_str(), &contents)?;
345 }
346 Ok(())
347}
348
349fn remove_files_tolerant(paths: BTreeSet<M2dirPath>) -> Result<(), io::Error> {
350 for path in paths {
351 trace!("remove_file (tolerant) {path}");
352 match fs::remove_file(path.as_str()) {
353 Ok(()) => {}
354 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
355 Err(err) => return Err(err),
356 }
357 }
358 Ok(())
359}
360
361fn read_dirs(
362 paths: BTreeSet<M2dirPath>,
363) -> Result<BTreeMap<M2dirPath, BTreeSet<M2dirPath>>, io::Error> {
364 let mut entries = BTreeMap::new();
365
366 for path in paths {
367 trace!("read_dir {path}");
368
369 let mut names = BTreeSet::new();
370 match fs::read_dir(path.as_str()) {
371 Ok(iter) => {
372 for entry in iter {
373 let entry = entry?;
374 names.insert(normalize_path(entry.path()));
375 }
376 }
377 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
378 Err(err) if err.kind() == io::ErrorKind::NotADirectory => {}
379 Err(err) => return Err(err),
380 }
381
382 entries.insert(path, names);
383 }
384
385 Ok(entries)
386}
387
388fn read_files_tolerant(
389 paths: BTreeSet<M2dirPath>,
390) -> Result<BTreeMap<M2dirPath, Vec<u8>>, io::Error> {
391 let mut contents = BTreeMap::new();
392
393 for path in paths {
394 trace!("read_file (tolerant) {path}");
395 match fs::read(path.as_str()) {
396 Ok(bytes) => {
397 contents.insert(path, bytes);
398 }
399 Err(err) if err.kind() == io::ErrorKind::NotFound => {
400 contents.insert(path, Vec::new());
401 }
402 Err(err) => return Err(err),
403 }
404 }
405
406 Ok(contents)
407}
408
409fn rename_paths(pairs: Vec<(M2dirPath, M2dirPath)>) -> Result<(), io::Error> {
410 for (from, to) in pairs {
411 trace!("rename {from} -> {to}");
412 fs::rename(from.as_str(), to.as_str())?;
413 }
414 Ok(())
415}
416
417fn file_exists(paths: BTreeSet<M2dirPath>) -> BTreeMap<M2dirPath, bool> {
418 let mut out = BTreeMap::new();
419 for path in paths {
420 let exists = fs::metadata(path.as_str())
421 .map(|m| m.is_file())
422 .unwrap_or(false);
423 trace!("file_exists {path}: {exists}");
424 out.insert(path, exists);
425 }
426 out
427}
428
429fn normalize_path(path: PathBuf) -> M2dirPath {
430 let s = path.to_string_lossy().into_owned();
431 #[cfg(windows)]
432 let s = s.replace('\\', "/");
433 M2dirPath::new(s)
434}
435
436fn random_bytes(len: usize) -> Vec<u8> {
439 let mut state = RandomState::new().build_hasher().finish();
440 if state == 0 {
441 state = 0xdeadbeef;
442 }
443
444 let mut out = Vec::with_capacity(len);
445 let mut buf = 0u64;
446 let mut i = 8;
447
448 while out.len() < len {
449 if i == 8 {
450 state ^= state << 13;
451 state ^= state >> 7;
452 state ^= state << 17;
453 buf = state;
454 i = 0;
455 }
456 out.push(buf as u8);
457 buf >>= 8;
458 i += 1;
459 }
460
461 out
462}