1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
use std::{
21
    io::{Read, Write},
22
    os::unix::fs::PermissionsExt,
23
    path::{Path, PathBuf},
24
};
25

            
26
use chrono::prelude::*;
27

            
28
use super::errors::*;
29

            
30
/// How to send e-mail.
31
27
#[derive(Debug, Serialize, Deserialize, Clone)]
32
#[serde(tag = "type", content = "value")]
33
pub enum SendMail {
34
    /// A `melib` configuration for talking to an SMTP server.
35
10
    Smtp(melib::smtp::SmtpServerConf),
36
    /// A plain shell command passed to `sh -c` with the e-mail passed in the
37
    /// stdin.
38
12
    ShellCommand(String),
39
}
40

            
41
/// The configuration for the mailpot database and the mail server.
42
40
#[derive(Debug, Serialize, Deserialize, Clone)]
43
pub struct Configuration {
44
    /// How to send e-mail.
45
7
    pub send_mail: SendMail,
46
    /// The location of the sqlite3 file.
47
17
    pub db_path: PathBuf,
48
    /// The directory where data are stored.
49
17
    pub data_path: PathBuf,
50
    /// Instance administrators (List of e-mail addresses). Optional.
51
    #[serde(default)]
52
17
    pub administrators: Vec<String>,
53
}
54

            
55
impl Configuration {
56
    /// Create a new configuration value from a given database path value.
57
    ///
58
    /// If you wish to create a new database with this configuration, use
59
    /// [`Connection::open_or_create_db`](crate::Connection::open_or_create_db).
60
    /// To open an existing database, use
61
    /// [`Database::open_db`](crate::Connection::open_db).
62
    pub fn new(db_path: impl Into<PathBuf>) -> Self {
63
        let db_path = db_path.into();
64
        Self {
65
            send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
66
            data_path: db_path
67
                .parent()
68
                .map(Path::to_path_buf)
69
                .unwrap_or_else(|| db_path.clone()),
70
            administrators: vec![],
71
            db_path,
72
        }
73
    }
74

            
75
    /// Deserialize configuration from TOML file.
76
1
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
77
1
        let path = path.as_ref();
78
1
        let mut s = String::new();
79
2
        let mut file = std::fs::File::open(path)
80
1
            .with_context(|| format!("Configuration file {} not found.", path.display()))?;
81
2
        file.read_to_string(&mut s)
82
1
            .with_context(|| format!("Could not read from file {}.", path.display()))?;
83
2
        let config: Self = toml::from_str(&s)
84
            .map_err(anyhow::Error::from)
85
2
            .with_context(|| {
86
1
                format!(
87
                    "Could not parse configuration file `{}` successfully: ",
88
1
                    path.display()
89
                )
90
2
            })?;
91

            
92
        Ok(config)
93
1
    }
94

            
95
    /// The saved data path.
96
    pub fn data_directory(&self) -> &Path {
97
        self.data_path.as_path()
98
    }
99

            
100
    /// The sqlite3 database path.
101
    pub fn db_path(&self) -> &Path {
102
        self.db_path.as_path()
103
    }
104

            
105
    /// Save message to a custom path.
106
    pub fn save_message_to_path(&self, msg: &str, mut path: PathBuf) -> Result<PathBuf> {
107
        if path.is_dir() {
108
            let now = Local::now().timestamp();
109
            path.push(format!("{}-failed.eml", now));
110
        }
111

            
112
        debug_assert!(path != self.db_path());
113
        let mut file = std::fs::File::create(&path)
114
            .with_context(|| format!("Could not create file {}.", path.display()))?;
115
        let metadata = file
116
            .metadata()
117
            .with_context(|| format!("Could not fstat file {}.", path.display()))?;
118
        let mut permissions = metadata.permissions();
119

            
120
        permissions.set_mode(0o600); // Read/write for owner only.
121
        file.set_permissions(permissions)
122
            .with_context(|| format!("Could not chmod 600 file {}.", path.display()))?;
123
        file.write_all(msg.as_bytes())
124
            .with_context(|| format!("Could not write message to file {}.", path.display()))?;
125
        file.flush()
126
            .with_context(|| format!("Could not flush message I/O to file {}.", path.display()))?;
127
        Ok(path)
128
    }
129

            
130
    /// Save message to the data directory.
131
    pub fn save_message(&self, msg: String) -> Result<PathBuf> {
132
        self.save_message_to_path(&msg, self.data_directory().to_path_buf())
133
    }
134

            
135
    /// Serialize configuration to a TOML string.
136
5
    pub fn to_toml(&self) -> String {
137
5
        toml::Value::try_from(self)
138
            .expect("Could not serialize config to TOML")
139
            .to_string()
140
5
    }
141
}
142

            
143
#[cfg(test)]
144
mod tests {
145
    use tempfile::TempDir;
146

            
147
    use super::*;
148

            
149
    #[test]
150
2
    fn test_config_parse_error() {
151
1
        let tmp_dir = TempDir::new().unwrap();
152
1
        let conf_path = tmp_dir.path().join("conf.toml");
153
1
        std::fs::write(&conf_path, b"afjsad skas as a as\n\n\n\n\t\x11\n").unwrap();
154

            
155
1
        assert_eq!(
156
1
            Configuration::from_file(&conf_path)
157
                .unwrap_err()
158
                .display_chain()
159
                .to_string(),
160
1
            format!(
161
                "[1] Could not parse configuration file `{}` successfully:  Caused by:\n[2] \
162
                 Error: expected an equals, found an identifier at line 1 column 8\n",
163
1
                conf_path.display()
164
            ),
165
        );
166
2
    }
167
}