email/folder/
mod.rs

1//! # Folder module
2//!
3//! Module dedicated to folder (as known as mailbox) management.
4//!
5//! The main entities are [`FolderKind`], [`Folder`] and [`Folders`].
6//!
7//! The [`config`] module exposes all the folder configuration used by
8//! the account configuration.
9//!
10//! Backend features reside in their own module as well: [`add`],
11//! [`list`], [`expunge`], [`purge`], [`delete`].
12//!
13//! Finally, the [`sync`] module contains everything needed to
14//! synchronize a remote folder with a local one.
15pub mod add;
16pub mod config;
17pub mod delete;
18mod error;
19pub mod expunge;
20#[cfg(feature = "imap")]
21pub mod imap;
22pub mod list;
23#[cfg(feature = "maildir")]
24pub mod maildir;
25pub mod purge;
26#[cfg(feature = "sync")]
27pub mod sync;
28
29use std::{
30    fmt,
31    hash::Hash,
32    ops::{Deref, DerefMut},
33    str::FromStr,
34};
35
36#[cfg(feature = "sync")]
37pub(crate) use sync::sync;
38
39#[doc(inline)]
40pub use self::error::{Error, Result};
41
42pub const INBOX: &str = "INBOX";
43pub const SENT: &str = "Sent";
44pub const DRAFT: &str = "Drafts";
45pub const DRAFTS: &str = "Drafts";
46pub const TRASH: &str = "Trash";
47
48/// The folder kind enumeration.
49///
50/// The folder kind is a category that gives a specific purpose to a
51/// folder. It is used internally by the library to operate on the
52/// right folder.
53///
54/// [`FolderConfig::aliases`](crate::folder::config::FolderConfig)
55/// allows users to map custom folder names but also to map the
56/// following folder kinds.
57#[derive(Clone, Debug, Eq, PartialEq, Hash)]
58pub enum FolderKind {
59    /// The kind of folder that contains received emails.
60    ///
61    /// This folder kind is mostly used for listing new or recent
62    /// emails.
63    Inbox,
64
65    /// The kind of folder that contains sent emails.
66    ///
67    /// This folder kind is used to store a copy of sent emails.
68    Sent,
69
70    /// The kind of folder than contains not finished emails.
71    ///
72    /// This kind of folder is used to store drafts. Emails in this
73    /// folder are supposed to be edited. Once completed they should
74    /// be removed from the folder.
75    Drafts,
76
77    /// The kind of folder that contains trashed emails.
78    ///
79    /// This kind of folder is used as a trash bin. Emails contained
80    /// in this folder are supposed to be deleted.
81    Trash,
82
83    /// The user-defined kind of folder.
84    ///
85    /// This kind of folder represents the alias as defined by the
86    /// user in [`config::FolderConfig`]::aliases.
87    UserDefined(String),
88}
89
90impl FolderKind {
91    /// Return `true` if the current folder kind matches the Inbox
92    /// variant.
93    pub fn is_inbox(&self) -> bool {
94        matches!(self, FolderKind::Inbox)
95    }
96
97    /// Return `true` if the current folder kind matches the Sent
98    /// variant.
99    pub fn is_sent(&self) -> bool {
100        matches!(self, FolderKind::Sent)
101    }
102
103    /// Return `true` if the current folder kind matches the Drafts
104    /// variant.
105    pub fn is_drafts(&self) -> bool {
106        matches!(self, FolderKind::Drafts)
107    }
108
109    /// Return `true` if the current folder kind matches the Trash
110    /// variant.
111    pub fn is_trash(&self) -> bool {
112        matches!(self, FolderKind::Trash)
113    }
114
115    /// Return `true` if the current folder kind matches the
116    /// UserDefined variant.
117    pub fn is_user_defined(&self) -> bool {
118        matches!(self, FolderKind::UserDefined(_))
119    }
120
121    /// Return `true` if the give string matches the Inbox variant.
122    pub fn matches_inbox(folder: impl AsRef<str>) -> bool {
123        folder
124            .as_ref()
125            .parse::<FolderKind>()
126            .map(|kind| kind.is_inbox())
127            .unwrap_or_default()
128    }
129
130    /// Return `true` if the given string matches the Sent variant.
131    pub fn matches_sent(folder: impl AsRef<str>) -> bool {
132        folder
133            .as_ref()
134            .parse::<FolderKind>()
135            .map(|kind| kind.is_sent())
136            .unwrap_or_default()
137    }
138
139    /// Return `true` if the given string matches the Drafts variant.
140    pub fn matches_drafts(folder: impl AsRef<str>) -> bool {
141        folder
142            .as_ref()
143            .parse::<FolderKind>()
144            .map(|kind| kind.is_drafts())
145            .unwrap_or_default()
146    }
147
148    /// Return `true` if the given string matches the Trash variant.
149    pub fn matches_trash(folder: impl AsRef<str>) -> bool {
150        folder
151            .as_ref()
152            .parse::<FolderKind>()
153            .map(|kind| kind.is_trash())
154            .unwrap_or_default()
155    }
156
157    /// Return the folder kind as string slice.
158    pub fn as_str(&self) -> &str {
159        match self {
160            Self::Inbox => INBOX,
161            Self::Sent => SENT,
162            Self::Drafts => DRAFTS,
163            Self::Trash => TRASH,
164            Self::UserDefined(alias) => alias.as_str(),
165        }
166    }
167}
168
169impl FromStr for FolderKind {
170    type Err = Error;
171
172    fn from_str(kind: &str) -> Result<Self> {
173        match kind {
174            kind if kind.eq_ignore_ascii_case(INBOX) => Ok(Self::Inbox),
175            kind if kind.eq_ignore_ascii_case(SENT) => Ok(Self::Sent),
176            kind if kind.eq_ignore_ascii_case(DRAFT) => Ok(Self::Drafts),
177            kind if kind.eq_ignore_ascii_case(DRAFTS) => Ok(Self::Drafts),
178            kind if kind.eq_ignore_ascii_case(TRASH) => Ok(Self::Trash),
179            kind => Err(Error::ParseFolderKindError(kind.to_owned())),
180        }
181    }
182}
183
184impl<T: AsRef<str>> From<T> for FolderKind {
185    fn from(kind: T) -> Self {
186        kind.as_ref()
187            .parse()
188            .ok()
189            .unwrap_or_else(|| Self::UserDefined(kind.as_ref().to_owned()))
190    }
191}
192
193impl fmt::Display for FolderKind {
194    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
195        write!(f, "{}", self.as_str())
196    }
197}
198
199/// The folder structure.
200///
201/// The folder is just a container for emails. Depending on the
202/// backend used, the folder can be seen as a mailbox (IMAP/JMAP) or
203/// as a system directory (Maildir).
204#[derive(Clone, Debug, Default, Eq)]
205pub struct Folder {
206    /// The optional folder kind.
207    pub kind: Option<FolderKind>,
208
209    /// The folder name.
210    pub name: String,
211
212    /// The folder description.
213    ///
214    /// The description depends on the backend used: it can be IMAP
215    /// attributes or Maildir path.
216    pub desc: String,
217}
218
219impl Folder {
220    /// Return `true` if the folder kind matches the Inbox variant.
221    pub fn is_inbox(&self) -> bool {
222        self.kind
223            .as_ref()
224            .map(|kind| kind.is_inbox())
225            .unwrap_or_default()
226    }
227
228    /// Return `true` if the folder kind matches the Sent variant.
229    pub fn is_sent(&self) -> bool {
230        self.kind
231            .as_ref()
232            .map(|kind| kind.is_sent())
233            .unwrap_or_default()
234    }
235
236    /// Return `true` if the folder kind matches the Drafts variant.
237    pub fn is_drafts(&self) -> bool {
238        self.kind
239            .as_ref()
240            .map(|kind| kind.is_drafts())
241            .unwrap_or_default()
242    }
243
244    /// Return `true` if the folder kind matches the Trash variant.
245    pub fn is_trash(&self) -> bool {
246        self.kind
247            .as_ref()
248            .map(|kind| kind.is_trash())
249            .unwrap_or_default()
250    }
251
252    /// Return the folder kind as string slice if existing, otherwise
253    /// return the folder name as string slice.
254    pub fn get_kind_or_name(&self) -> &str {
255        self.kind
256            .as_ref()
257            .map(FolderKind::as_str)
258            .unwrap_or(self.name.as_str())
259    }
260}
261
262impl PartialEq for Folder {
263    fn eq(&self, other: &Self) -> bool {
264        match (&self.kind, &other.kind) {
265            (Some(self_kind), Some(other_kind)) => self_kind == other_kind,
266            (None, None) => self.name == other.name,
267            _ => false,
268        }
269        // self.kind == other.kind || self.name == other.name
270    }
271}
272impl Hash for Folder {
273    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
274        match &self.kind {
275            Some(kind) => kind.hash(state),
276            None => self.name.hash(state),
277        }
278    }
279}
280
281impl fmt::Display for Folder {
282    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
283        match &self.kind {
284            Some(kind) => write!(f, "{kind}"),
285            None => write!(f, "{}", self.name),
286        }
287    }
288}
289
290/// The list of folders.
291///
292/// This structure is just a convenient wrapper used to implement
293/// custom mappers for backends.
294#[derive(Clone, Debug, Default, Eq, PartialEq)]
295pub struct Folders(Vec<Folder>);
296
297impl Deref for Folders {
298    type Target = Vec<Folder>;
299
300    fn deref(&self) -> &Self::Target {
301        &self.0
302    }
303}
304
305impl DerefMut for Folders {
306    fn deref_mut(&mut self) -> &mut Self::Target {
307        &mut self.0
308    }
309}
310
311impl IntoIterator for Folders {
312    type Item = Folder;
313    type IntoIter = std::vec::IntoIter<Self::Item>;
314
315    fn into_iter(self) -> Self::IntoIter {
316        self.0.into_iter()
317    }
318}
319
320impl FromIterator<Folder> for Folders {
321    fn from_iter<T: IntoIterator<Item = Folder>>(iter: T) -> Self {
322        let mut folders = Folders::default();
323        folders.extend(iter);
324        folders
325    }
326}
327
328impl From<Folders> for Vec<Folder> {
329    fn from(val: Folders) -> Self {
330        val.0
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use std::{collections::hash_map::DefaultHasher, hash::Hasher};
337
338    use super::*;
339    fn folder_inbox_foo() -> Folder {
340        Folder {
341            kind: Some(FolderKind::Inbox),
342            name: "foo".to_owned(),
343            desc: "1".to_owned(),
344        }
345    }
346    fn folder_none_foo() -> Folder {
347        Folder {
348            kind: None,
349            name: "foo".to_owned(),
350            desc: "2".to_owned(),
351        }
352    }
353    fn folder_none_bar() -> Folder {
354        Folder {
355            kind: None,
356            name: "bar".to_owned(),
357            desc: "3".to_owned(),
358        }
359    }
360    fn folder_inbox_bar() -> Folder {
361        Folder {
362            kind: Some(FolderKind::Inbox),
363            name: "bar".to_owned(),
364            desc: "4".to_owned(),
365        }
366    }
367
368    fn hash<H: Hash>(item: H) -> u64 {
369        let mut hasher = DefaultHasher::new();
370        item.hash(&mut hasher);
371        hasher.finish()
372    }
373
374    #[test]
375    fn folder_inbox_bar_equals_inbox_foo_test() {
376        assert_eq!(folder_inbox_bar(), folder_inbox_foo());
377    }
378
379    #[test]
380    fn folder_inbox_bar_equals_inbox_foo_test_hash() {
381        assert_eq!(hash(folder_inbox_bar()), hash(folder_inbox_foo()));
382    }
383
384    #[test]
385    fn folder_none_foo_not_equals_inbox_foo_test() {
386        assert_ne!(folder_none_foo(), folder_inbox_foo());
387    }
388
389    #[test]
390    fn folder_none_foo_not_equals_inbox_foo_test_hash() {
391        assert_ne!(hash(folder_none_foo()), hash(folder_inbox_foo()));
392    }
393
394    #[test]
395    fn folder_none_foo_not_equals_none_bar_test() {
396        assert_ne!(folder_none_foo(), folder_none_bar());
397    }
398
399    #[test]
400    fn folder_none_foo_not_equals_none_bar_test_hash() {
401        assert_ne!(hash(folder_none_foo()), hash(folder_none_bar()));
402    }
403}