1use std::fs;
42use std::io;
43use std::path::{Path, PathBuf};
44use std::process::Command;
45
46use dirs::{config_dir, home_dir};
47use shellexpand::tilde;
48use thiserror::Error;
49
50const DEFAULT_MAILDIR: &str = "~/.maildir";
51
52#[derive(Error, Debug)]
53pub enum ConfigError {
54 #[error("error getting default config file location")]
55 DirError,
56 #[error("error reading config file: {0}")]
57 IOError(#[from] io::Error),
58 #[error("error executing pass-cmd: {0}")]
59 PassExecError(String),
60 #[error("UTF-8 error: {0}")]
61 UTF8Error(#[from] std::string::FromUtf8Error),
62 #[error("error parsing config file: {0}")]
63 TOMLError(#[from] toml::de::Error),
64 #[error("config error: {0}")]
65 Error(&'static str),
66}
67
68pub struct Account<'a> {
70 name: String,
71 settings: &'a toml::value::Table,
72 local: String,
74}
75
76pub struct Config {
78 cfg: toml::value::Table,
79}
80
81impl Config {
82 pub fn for_account(&self, name: Option<&str>) -> Option<Account<'_>> {
88 let name = match name {
89 Some(n) => n,
90 None => self.cfg.keys().next().expect("no account found"),
92 };
93
94 if let Some(a) = self.cfg.get(name) {
95 if let Some(t) = a.as_table() {
96 let mut path = DEFAULT_MAILDIR;
98 if let Some(l) = t.get("local") {
99 if let Some(p) = l.as_str() {
100 path = p
101 }
102 };
103
104 return Some(Account {
105 name: String::from(name),
106 settings: t,
107 local: tilde(path).into_owned(),
108 });
109 }
110 }
111 None
112 }
113}
114
115impl Account<'_> {
116 pub fn name(&self) -> &String {
118 &self.name
119 }
120
121 pub fn local(&self) -> &str {
123 &self.local
124 }
125
126 pub fn remote(&self) -> Option<&str> {
128 self.settings.get("remote").and_then(|v| v.as_str())
129 }
130
131 pub fn user(&self) -> Option<&str> {
133 self.settings.get("user").and_then(|v| v.as_str())
134 }
135
136 pub fn send_from(&self) -> Option<&str> {
140 let v = self
141 .settings
142 .get("send")
143 .and_then(|v| v.as_table())
144 .and_then(|v| v.get("from"))
145 .and_then(|v| v.as_str());
146 v.or_else(|| self.user())
147 }
148
149 pub fn send_user(&self) -> Option<&str> {
153 let v = self
154 .settings
155 .get("send")
156 .and_then(|v| v.as_table())
157 .and_then(|v| v.get("user"))
158 .and_then(|v| v.as_str());
159 v.or_else(|| self.user())
160 }
161
162 pub fn send_remote(&self) -> Option<&str> {
166 let v = self
167 .settings
168 .get("send")
169 .and_then(|v| v.as_table())
170 .and_then(|v| v.get("remote"))
171 .and_then(|v| v.as_str());
172 v.or_else(|| self.remote())
173 }
174
175 fn pass_cmd(cmd: &str) -> Result<Option<String>, ConfigError> {
176 let out = Command::new("sh").arg("-c").arg(cmd).output();
177 match out {
178 Ok(mut output) => {
179 let newline: u8 = 10;
180 if Some(&newline) == output.stdout.last() {
181 _ = output.stdout.pop(); }
183 Ok(Some(String::from_utf8(output.stdout)?))
184 }
185 Err(e) => Err(ConfigError::PassExecError(e.to_string())),
186 }
187 }
188
189 pub fn password(&self) -> Result<Option<String>, ConfigError> {
195 if let Some(p) = self.settings.get("password").and_then(|v| v.as_str()) {
196 return Ok(Some(String::from(p)));
197 } else if let Some(cmd) = self.settings.get("pass-cmd").and_then(|v| v.as_str()) {
198 return Account::pass_cmd(cmd);
199 }
200 Ok(None)
201 }
202
203 pub fn send_password(&self) -> Result<Option<String>, ConfigError> {
211 let pass = self
212 .settings
213 .get("send")
214 .and_then(|v| v.as_table())
215 .and_then(|v| v.get("password"))
216 .and_then(|v| v.as_str());
217 if let Some(p) = pass {
218 return Ok(Some(String::from(p)));
219 }
220
221 let pcmd = self
222 .settings
223 .get("send")
224 .and_then(|v| v.as_table())
225 .and_then(|v| v.get("pass-cmd"))
226 .and_then(|v| v.as_str());
227 if let Some(cmd) = pcmd {
228 return Account::pass_cmd(cmd);
229 }
230 self.password()
231 }
232}
233
234pub fn default_path() -> Result<PathBuf, ConfigError> {
238 if let Some(mut path) = config_dir() {
239 path.push("vomit");
241 path.push("config.toml");
242 return Ok(path);
243 };
244 if let Some(mut path) = home_dir() {
245 path.push(".vomitrc");
246 return Ok(path);
247 }
248 Err(ConfigError::DirError)
249}
250
251pub fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Config, ConfigError> {
259 let contents = match path {
260 Some(p) => fs::read_to_string(p)?,
261 None => fs::read_to_string(default_path()?)?,
262 };
263
264 let table = contents.parse::<toml::Table>()?;
265 if table.is_empty() {
266 Err(ConfigError::Error("no accounts found"))
267 } else {
268 Ok(Config { cfg: table })
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_simple() {
278 let table: toml::value::Table = toml::from_str(
279 r#"
280 [example]
281 local = '/home/test/.maildir'
282 remote = 'mx.example.com'
283 user = 'johndoe'
284 password = 'hunter1'
285 "#,
286 )
287 .unwrap();
288
289 let config = Config { cfg: table };
290
291 let acc = config.for_account(None).expect("no account found");
292
293 assert_eq!(acc.user(), Some("johndoe"));
294 assert_eq!(acc.remote(), Some("mx.example.com"));
295 assert_eq!(
296 acc.password().expect("failed to get password"),
297 Some(String::from("hunter1"))
298 );
299 assert_eq!(acc.send_user(), Some("johndoe"));
300 assert_eq!(acc.send_remote(), Some("mx.example.com"));
301 assert_eq!(
302 acc.send_password().expect("failed to get password"),
303 Some(String::from("hunter1"))
304 );
305 }
306
307 #[test]
308 fn test_minimal() {
309 let table: toml::value::Table = toml::from_str(
310 r#"
311 [example]
312 "#,
313 )
314 .unwrap();
315
316 let config = Config { cfg: table };
317
318 let _ = config.for_account(None).expect("no account found");
319 }
320
321 #[test]
322 fn test_send() {
323 let table: toml::value::Table = toml::from_str(
324 r#"
325 [example]
326 local = '/home/test/.maildir'
327 remote = 'imap.example.com'
328 user = 'johndoe'
329 password = 'hunter1'
330 send.remote = 'smtp.example.com'
331 send.user = 'johndoe@example.com'
332 send.password = 's3cr3t'
333 "#,
334 )
335 .unwrap();
336
337 let config = Config { cfg: table };
338
339 let acc = config.for_account(None).expect("no account found");
340
341 assert_eq!(acc.user(), Some("johndoe"));
342 assert_eq!(acc.remote(), Some("imap.example.com"));
343 assert_eq!(
344 acc.password().expect("failed to get password"),
345 Some(String::from("hunter1"))
346 );
347 assert_eq!(acc.send_user(), Some("johndoe@example.com"));
348 assert_eq!(acc.send_remote(), Some("smtp.example.com"));
349 assert_eq!(
350 acc.send_password().expect("failed to get password"),
351 Some(String::from("s3cr3t"))
352 );
353 }
354
355 #[test]
356 fn test_simple_cmd() {
357 let table: toml::value::Table = toml::from_str(
358 r#"
359 [example]
360 local = '/home/test/.maildir'
361 remote = 'mx.example.com'
362 user = 'johndoe'
363 pass-cmd = 'echo hunter1'
364 "#,
365 )
366 .unwrap();
367
368 let config = Config { cfg: table };
369
370 let acc = config.for_account(None).expect("no account found");
371
372 assert_eq!(acc.user(), Some("johndoe"));
373 assert_eq!(acc.remote(), Some("mx.example.com"));
374 assert_eq!(
375 acc.password().expect("failed to get password"),
376 Some(String::from("hunter1"))
377 );
378 assert_eq!(acc.send_user(), Some("johndoe"));
379 assert_eq!(acc.send_remote(), Some("mx.example.com"));
380 assert_eq!(
381 acc.send_password().expect("failed to get password"),
382 Some(String::from("hunter1"))
383 );
384 }
385
386 #[test]
387 fn test_send_cmd() {
388 let table: toml::value::Table = toml::from_str(
389 r#"
390 [example]
391 local = '/home/test/.maildir'
392 remote = 'imap.example.com'
393 user = 'johndoe'
394 pass-cmd = 'echo hunter1'
395 send.remote = 'smtp.example.com'
396 send.user = 'johndoe@example.com'
397 send.pass-cmd = 'echo s3cr3t'
398 "#,
399 )
400 .unwrap();
401
402 let config = Config { cfg: table };
403
404 let acc = config.for_account(None).expect("no account found");
405
406 assert_eq!(acc.user(), Some("johndoe"));
407 assert_eq!(acc.remote(), Some("imap.example.com"));
408 assert_eq!(
409 acc.password().expect("failed to get password"),
410 Some(String::from("hunter1"))
411 );
412 assert_eq!(acc.send_user(), Some("johndoe@example.com"));
413 assert_eq!(acc.send_remote(), Some("smtp.example.com"));
414 assert_eq!(
415 acc.send_password().expect("failed to get password"),
416 Some(String::from("s3cr3t"))
417 );
418 }
419}