1use chrono::{DateTime, Local, Utc};
2use mail_parser::MessageParser;
3use std::{
4 env,
5 ffi::OsStr,
6 fs,
7 io::Write,
8 path::{Path, PathBuf},
9};
10
11use crate::{
12 flags::{self, Flags},
13 util::sanitize_filename,
14 ID,
15};
16use crate::{util, Error, Message, Messages};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct M2dir {
24 pub(crate) path: PathBuf,
25}
26
27impl TryFrom<&Path> for M2dir {
28 type Error = Error;
29
30 fn try_from(path: &Path) -> Result<Self, Error> {
31 let path = path.canonicalize()?;
32 let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2dir")]);
33 if !marker.is_file() {
34 return Err(Error::FolderNotFound);
35 }
36 Ok(M2dir { path })
37 }
38}
39
40impl M2dir {
41 pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
46 M2dir::try_from(path.as_ref())
47 }
48
49 pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
54 fs::create_dir_all(&path)?;
55 let marker = PathBuf::from_iter([path.as_ref().as_os_str(), OsStr::new(".m2dir")]);
56 let _ = fs::File::create(marker)?;
57 M2dir::open(path)
58 }
59
60 pub fn path(&self) -> &Path {
66 &self.path
67 }
68
69 pub fn count(&self) -> usize {
71 self.list().count()
72 }
73
74 pub fn list(&self) -> Messages {
79 Messages::new(self.path.clone())
80 }
81
82 pub fn deliver(&self, data: &[u8]) -> Result<Message, Error> {
84 self.store(data, &Flags::new())
85 }
86
87 pub fn find(&self, id: &ID) -> Result<Option<Message>, Error> {
89 for msg in self.list() {
90 let msg = msg?;
91 if msg.id() == id {
92 return Ok(Some(msg));
93 }
94 }
95 Ok(None)
96 }
97
98 pub fn store(&self, data: &[u8], flags: &Flags) -> Result<Message, Error> {
101 let message = MessageParser::default()
102 .parse_headers(data)
103 .ok_or(Error::Parse())?;
104
105 let date = match message.date() {
108 None => Local::now(),
109 Some(dt) => DateTime::from_timestamp(dt.to_timestamp(), 0)
110 .unwrap_or_else(Utc::now)
111 .with_timezone(&Local),
112 };
113 let from = message
114 .from()
115 .and_then(|f| f.first())
116 .and_then(|a| a.address())
117 .unwrap_or("<parse_error>");
118 let from = sanitize_filename(from);
119
120 let id = ID::new(data);
121
122 let fmt = env::var("VOMIT_M2DIR_DATE_FORMAT").unwrap_or("%Y-%m-%d_%H:%M".to_string());
123
124 let final_name = format!("{}_{},{}", date.format(&fmt), from, id);
125 let mut final_path = self.path.clone();
126 final_path.push(&final_name);
127
128 if !flags.is_empty() {
129 let flags_path = flags::flags_path_for(&self.path, id.as_ref());
130
131 util::write_atomic(&flags_path, |f| {
132 for flag in flags.iter() {
133 writeln!(f, "{}", flag)?;
134 }
135 Ok(())
136 })
137 .map_err(|e| Error::WriteFlags(flags_path, e))?;
138 }
139
140 util::write_atomic(&final_path, |f| f.write_all(data)).inspect_err(|_| {
141 if !flags.is_empty() {
142 let flags_path = flags::flags_path_for(&self.path, id.as_ref());
143 _ = fs::remove_file(flags_path);
144 }
145 })?;
146
147 Ok(Message {
148 id,
149 path: final_path,
150 })
151 }
152}