1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
pub mod config;

use async_trait::async_trait;
use log::info;
use maildirpp::Maildir;
use shellexpand_utils::{shellexpand_path, try_shellexpand_path};
use std::{
    io,
    ops::Deref,
    path::{self, PathBuf},
    sync::Arc,
};
use thiserror::Error;
use tokio::sync::Mutex;

use crate::{
    account::config::AccountConfig, backend::BackendContextBuilder, folder::FolderKind, maildir,
    Result,
};

use self::config::MaildirConfig;

#[derive(Debug, Error)]
pub enum Error {
    #[error("maildir: cannot init folders structure at {1}")]
    InitFoldersStructureError(#[source] maildirpp::Error, PathBuf),
    #[error("maildir: cannot read folder: invalid path {0}")]
    ReadFolderInvalidError(path::PathBuf),
    #[error("maildir: cannot get current folder")]
    GetCurrentFolderError(#[source] io::Error),
}

/// The Maildir session builder.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MaildirSessionBuilder {
    /// The account configuration.
    pub account_config: AccountConfig,

    /// The Maildir configuration.
    pub maildir_config: MaildirConfig,
}

impl MaildirSessionBuilder {
    pub fn new(account_config: AccountConfig, maildir_config: MaildirConfig) -> Self {
        Self {
            account_config,
            maildir_config,
        }
    }
}

#[async_trait]
impl BackendContextBuilder for MaildirSessionBuilder {
    type Context = MaildirSessionSync;

    /// Build an IMAP sync session.
    ///
    /// The IMAP session is created at this moment. If the session
    /// cannot be created using the OAuth 2.0 authentication, the
    /// access token is refreshed first then a new session is created.
    async fn build(self) -> Result<Self::Context> {
        info!("building new maildir session");

        let path = shellexpand_path(&self.maildir_config.root_dir);
        let session = MaildirSession {
            account_config: self.account_config.clone(),
            maildir_config: self.maildir_config.clone(),
            session: Maildir::from(path),
        };

        session.create_dirs()?;

        Ok(MaildirSessionSync {
            account_config: self.account_config,
            maildir_config: self.maildir_config,
            session: Arc::new(Mutex::new(session)),
        })
    }
}

/// The Maildir session.
///
/// This session is unsync, which means it cannot be shared between
/// threads. For the sync version, see [`MaildirSessionSync`].
pub struct MaildirSession {
    /// The account configuration.
    pub account_config: AccountConfig,

    /// The Maildir configuration.
    pub maildir_config: MaildirConfig,

    /// The current Maildir session.
    session: Maildir,
}

impl Deref for MaildirSession {
    type Target = Maildir;

    fn deref(&self) -> &Self::Target {
        &self.session
    }
}

impl MaildirSession {
    pub fn create_dirs(&self) -> Result<()> {
        self.session
            .create_dirs()
            .map_err(|err| Error::InitFoldersStructureError(err, self.session.path().to_owned()))?;
        Ok(())
    }

    /// Creates a maildir instance from a folder name.
    pub fn get_maildir_from_folder_name(&self, folder: &str) -> Result<Maildir> {
        // If the folder matches to the inbox folder kind, create a
        // maildir instance from the root folder.
        if FolderKind::matches_inbox(folder) {
            return try_shellexpand_path(self.session.path().to_owned())
                .map(Maildir::from)
                .map_err(Into::into);
        }

        let folder = self.account_config.get_folder_alias(folder);

        // If the folder is a valid maildir path, creates a maildir
        // instance from it. First check for absolute path…
        try_shellexpand_path(&folder)
            // then check for relative path to `maildir-dir`…
            .or_else(|_| try_shellexpand_path(self.session.path().join(&folder)))
            // TODO: should move to CLI
            // // and finally check for relative path to the current
            // // directory
            // .or_else(|_| {
            //     try_shellexpand_path(
            //         env::current_dir()
            //             .map_err(Error::GetCurrentFolderError)?
            //             .join(&folder),
            //     )
            // })
            .or_else(|_| {
                // Otherwise creates a maildir instance from a maildir
                // subdirectory by adding a "." in front of the name
                // as described in the [spec].
                //
                // [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
                let folder = maildir::encode_folder(&folder);
                try_shellexpand_path(self.session.path().join(format!(".{}", folder)))
            })
            .map(Maildir::from)
            .map_err(Into::into)
    }
}

/// The sync version of the Maildir session.
///
/// This is just a Maildir session wrapped into a mutex, so the same
/// Maildir session can be shared and updated across multiple threads.
#[derive(Clone)]
pub struct MaildirSessionSync {
    /// The account configuration.
    pub account_config: AccountConfig,

    /// The MAILDIR configuration.
    pub maildir_config: MaildirConfig,

    /// The MAILDIR session wrapped into a mutex.
    session: Arc<Mutex<MaildirSession>>,
}

impl MaildirSessionSync {
    /// Create a new MAILDIR sync session from an MAILDIR session.
    pub fn new(
        account_config: AccountConfig,
        maildir_config: MaildirConfig,
        session: MaildirSession,
    ) -> Self {
        Self {
            account_config,
            maildir_config,
            session: Arc::new(Mutex::new(session)),
        }
    }
}

impl Deref for MaildirSessionSync {
    type Target = Mutex<MaildirSession>;

    fn deref(&self) -> &Self::Target {
        &self.session
    }
}

/// URL-encodes the given folder. The aim is to avoid naming
/// issues due to special characters.
pub fn encode_folder(folder: impl AsRef<str> + ToString) -> String {
    urlencoding::encode(folder.as_ref()).to_string()
}

/// URL-decodes the given folder.
pub fn decode_folder(folder: impl AsRef<str> + ToString) -> String {
    urlencoding::decode(folder.as_ref())
        .map(|folder| folder.to_string())
        .unwrap_or_else(|_| folder.to_string())
}