vomit_m2dir/
message.rs

1use std::fs::{self, read_dir, File, ReadDir};
2use std::ops::Deref;
3use std::path::{Path, PathBuf};
4
5use crate::flags::{self, Flags};
6use crate::{util, Error, M2dir};
7
8/// An email message stored in an m2dir.
9///
10/// Note that this implementation does not actually verify that the backing file
11/// contains a valid email message, as it has no need to parse the contents.
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub struct Message {
14    pub(crate) id: String,
15    pub(crate) path: PathBuf,
16}
17
18impl TryFrom<&Path> for Message {
19    type Error = Error;
20
21    fn try_from(path: &Path) -> Result<Self, Error> {
22        if !path.is_file() {
23            return Err(Error::MessageNotFound);
24        }
25        let fname = path.file_name().unwrap().to_string_lossy();
26        let (_, id) = fname
27            .rsplit_once(',')
28            .ok_or(Error::InvalidFileName(path.to_path_buf()))?;
29        Ok(Message {
30            id: id.to_string(),
31            path: PathBuf::from(path),
32        })
33    }
34}
35
36impl Message {
37    pub fn path(&self) -> &Path {
38        &self.path
39    }
40
41    pub fn id(&self) -> &str {
42        &self.id
43    }
44
45    pub fn flags_path(&self) -> PathBuf {
46        flags::flags_path_for(self.path.parent().unwrap(), self.id())
47    }
48
49    pub fn flags(&self) -> Result<Flags, Error> {
50        let flags_path = self.flags_path();
51
52        if !flags_path.exists() {
53            return Ok(Flags::default());
54        }
55
56        let file = File::open(&flags_path)?;
57        Flags::parse_file(file).map_err(|e| Error::Flags(flags_path, e))
58    }
59
60    pub fn set_flags(&self, flags: &Flags) -> Result<(), Error> {
61        let flags_path = self.flags_path();
62        fs::create_dir_all(flags_path.parent().unwrap())
63            .map_err(|e| Error::WriteFlags(flags_path.clone(), e))?;
64        let mut file =
65            File::create(&flags_path).map_err(|e| Error::WriteFlags(flags_path.clone(), e))?;
66        flags
67            .write_file(&mut file)
68            .map_err(|e| Error::WriteFlags(flags_path, e))?;
69        Ok(())
70    }
71
72    pub fn copy_to(&self, target: &M2dir) -> Result<Message, Error> {
73        let src_flags = self.flags_path();
74        let dst_flags = flags::flags_path_for(&target.path, self.id());
75        let dst = PathBuf::from_iter([target.path.as_os_str(), self.path.file_name().unwrap()]);
76
77        fs::create_dir_all(dst_flags.parent().unwrap())?;
78        if src_flags.exists() {
79            util::copy_atomic(src_flags, &dst_flags)?;
80        }
81        util::copy_atomic(&self.path, &dst).inspect_err(|_| {
82            _ = fs::remove_file(&dst_flags);
83        })?;
84        Ok(Message {
85            id: self.id.clone(),
86            path: dst,
87        })
88    }
89
90    pub fn move_to(&mut self, target: &M2dir) -> Result<(), Error> {
91        let src_flags = self.flags_path();
92        let dst_flags = flags::flags_path_for(&target.path, self.id());
93        let dst = PathBuf::from_iter([target.path.as_os_str(), self.path.file_name().unwrap()]);
94
95        fs::create_dir_all(dst_flags.parent().unwrap())?;
96        if src_flags.exists() {
97            fs::rename(&src_flags, &dst_flags)?;
98        }
99        fs::rename(&self.path, &dst).inspect_err(|_| {
100            _ = fs::rename(&dst_flags, &src_flags);
101        })?;
102        self.path = dst;
103        Ok(())
104    }
105
106    pub fn delete(self) -> Result<(), Error> {
107        let flags = self.flags_path();
108        fs::remove_file(self.path())?;
109        if flags.exists() {
110            fs::remove_file(self.flags_path())?;
111        }
112        Ok(())
113    }
114}
115
116/// An iterator over the email messages in a particular m2dir.
117///
118/// Usually constructed via [`M2dir::list`].
119///
120/// The order of messages in the iterator is not specified, and is not
121/// guaranteed to be stable over multiple invocations of this method.
122///
123/// Note that each file with a valid name according to the m2dir spec is
124/// considered a message. The contents of files are not parsed or validated.
125pub struct Messages {
126    path: PathBuf,
127    readdir: Option<ReadDir>,
128}
129
130impl Messages {
131    pub(crate) fn new(path: PathBuf) -> Messages {
132        Messages {
133            path,
134            readdir: None,
135        }
136    }
137
138    pub fn path(&self) -> &Path {
139        &self.path
140    }
141}
142
143impl Iterator for Messages {
144    type Item = Result<Message, Error>;
145
146    fn next(&mut self) -> Option<Result<Message, Error>> {
147        if self.readdir.is_none() {
148            self.readdir = match read_dir(&self.path) {
149                Err(_) => return None,
150                Ok(v) => Some(v),
151            };
152        }
153
154        loop {
155            // skip over directories and files starting with a '.'
156            let dir_entry = self.readdir.iter_mut().next().unwrap().next();
157            let result = dir_entry.map(|e| {
158                let entry = e?;
159                let ftype = entry.file_type()?;
160                if ftype.is_dir() {
161                    return Ok(None);
162                }
163                let filename = String::from(entry.file_name().to_string_lossy().deref());
164                if filename.starts_with('.') {
165                    return Ok(None);
166                }
167                Ok(Some(Message::try_from(entry.path().as_path())?))
168            });
169            return match result {
170                None => None,
171                Some(Err(e)) => Some(Err(e)),
172                Some(Ok(None)) => continue,
173                Some(Ok(Some(v))) => Some(Ok(v)),
174            };
175        }
176    }
177}