use std::path::PathBuf;
mod flags;
mod id;
mod m2dir;
mod m2store;
mod message;
mod util;
pub use flags::{Error as FlagsError, Flag, Flags};
pub use id::ID;
pub use m2dir::M2dir;
pub use m2store::{Folder, Folders, M2store};
pub use message::{Message, Messages};
pub use util::{decode_folder_name, encode_folder_name};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("error handling flags in {0}: {1}")]
Flags(PathBuf, #[source] FlagsError),
#[error("invalid folder name: {0}")]
InvalidFolderName(String),
#[error("folder is not a valid m2dir or m2store")]
FolderNotFound,
#[error("invalid file name: {0}")]
InvalidFileName(PathBuf),
#[error("invalid message ID: {0}")]
InvalidMsgID(String),
#[error("IO error: {0}")]
IO(#[from] std::io::Error),
#[error("error writing flags to {0}: {1}")]
WriteFlags(PathBuf, #[source] std::io::Error),
#[error("message not found")]
MessageNotFound,
#[error("failed to parse message")]
Parse(),
#[error("IO error walking directory: {0}")]
WalkDir(#[from] walkdir::Error),
}
#[cfg(test)]
mod tests {
use std::{
fs::OpenOptions,
io::Write,
path::Path,
time::{Duration, SystemTime},
};
use lettre::message::header::Date;
use tempfile::{tempdir, TempDir};
use flags::Flag;
use super::*;
const TEST_MAIL_BODY: &[u8] = b"Date: Sun, 29 Nov 2020 03:49:23 +0000
From: \"Conrad Hoffmann\" <ch@bitfehler.net>
To: \"Testy McTestface\" <test@bitfehler.net>
Subject: Test email that is about to be delivered
Content-Transfer-Encoding: 7bit
With a test body";
fn gen_message(from: &str, to: &str, subject: &str, body: &str) -> lettre::Message {
let date = Date::from(SystemTime::UNIX_EPOCH + Duration::from_secs(1606621763));
lettre::message::Message::builder()
.header(date)
.from(from.parse().unwrap())
.to(to.parse().unwrap())
.subject(subject)
.body(body.to_string())
.unwrap()
.into()
}
pub(crate) fn generate_test_data(target: impl AsRef<Path>) {
let store = M2store::create(&target).unwrap();
let inbox = store.create_folder("INBOX").unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email that is marked as read",
"With a test body",
);
inbox
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen]))
.unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email that is unread",
"And will hence not have any flags",
);
inbox.as_ref().deliver(&mgs.formatted()).unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email with a custom flag",
"With a test body",
);
inbox
.as_ref()
.store(
&mgs.formatted(),
&Flags::from([Flag::Seen, Flag::Custom("todo".to_string())]),
)
.unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Duplicate test email",
"Which should work just fine, we'll use different flags so this can be easily verified",
);
inbox
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen]))
.unwrap();
inbox
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen, Flag::Answered]))
.unwrap();
let folder = store.create_folder("lists/m2dir-dev").unwrap();
let mgs = gen_message(
"Conrad Hoffmann <with/slash@bitfehler.net>",
"Fake M2dir ML <m2dir@example.com>",
"Test email in a nested subfolder",
"Whose parent folder is not an m2dir (contains no emails)",
);
folder
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen]))
.unwrap();
let folder = store.create_folder("folder").unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email in a simple folder",
"Nothing special here",
);
folder
.as_ref()
.store(
&mgs.formatted(),
&Flags::from([
Flag::Answered,
Flag::Seen,
Flag::Custom("patch".to_string()),
]),
)
.unwrap();
let folder = folder.create_subfolder("subfölder").unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email in a nested folder",
"Whose parent is also a folder with messages",
);
folder
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen]))
.unwrap();
let folder = store.create_folder("brokenflags").unwrap();
let mgs = gen_message(
"Conrad Hoffmann <ch@bitfehler.net>",
"Testy McTestface <test@bitfehler.net>",
"Test email with broken flags file",
"To test error handling",
);
let m = folder
.as_ref()
.store(&mgs.formatted(), &Flags::from([Flag::Seen]))
.unwrap();
let mut f = OpenOptions::new()
.append(true)
.open(m.flags_path())
.unwrap();
f.write("\n\\InvalidFlag\n".as_ref()).unwrap();
}
fn setup(dir: &TempDir, folder: &str) -> M2dir {
generate_test_data(dir.as_ref());
let m2store = M2store::open(dir.path()).unwrap();
m2store.folder(folder).unwrap().unwrap().as_ref().clone()
}
#[test]
fn test_load_single() {
let tmpdir = tempdir().unwrap();
let m2dir = setup(&tmpdir, "folder");
assert_eq!(m2dir.count(), 1);
let msgs = m2dir.list();
let msg = msgs.into_iter().next().unwrap().unwrap();
assert!(msg
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
let loaded_flags = msg.flags().unwrap();
let flags = Flags::from([
Flag::Answered,
Flag::Seen,
Flag::Custom(String::from("patch")),
]);
assert_eq!(flags, loaded_flags);
}
#[test]
fn test_deliver() {
let tmpdir = tempdir().unwrap();
let m2dir = M2dir::create(tmpdir.path()).unwrap();
let msg = m2dir.deliver(TEST_MAIL_BODY).unwrap();
eprintln!("{}", msg.path.file_name().unwrap().to_string_lossy());
assert!(msg
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_ch@bitfehler.net,4AAAADUevRZ_R3P7"));
assert_eq!(msg.flags().unwrap().len(), 0);
}
#[test]
fn test_store() {
let tmpdir = tempdir().unwrap();
let m2dir = M2dir::create(tmpdir.path()).unwrap();
let flags = Flags::from([Flag::Answered, Flag::Seen]);
let msg = m2dir.store(TEST_MAIL_BODY, &flags).unwrap();
assert!(msg
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_ch@bitfehler.net,4AAAADUevRZ_R3P7"));
let loaded_flags = msg.flags().unwrap();
assert_eq!(flags, loaded_flags);
}
#[test]
fn test_copy() {
let srcdir = tempdir().unwrap();
let m2dir = setup(&srcdir, "folder");
let srcfolder = m2dir.path();
let msgs = m2dir.list();
let msg = msgs.into_iter().next().unwrap().unwrap();
let dstdir = tempdir().unwrap();
let m2dir = M2dir::create(dstdir.path()).unwrap();
assert_eq!(m2dir.count(), 0);
let cpy = msg.copy_to(&m2dir).unwrap();
assert_eq!(m2dir.count(), 1);
assert_eq!(msg.path.parent().unwrap(), srcfolder);
assert_eq!(cpy.path.parent().unwrap(), dstdir.path());
assert!(cpy
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
let loaded_flags = cpy.flags().unwrap();
let flags = Flags::from([
Flag::Answered,
Flag::Seen,
Flag::Custom(String::from("patch")),
]);
assert_eq!(flags, loaded_flags);
assert!(cpy.copy_to(&m2dir).is_err());
}
#[test]
fn test_move() {
let srcdir = tempdir().unwrap();
let m2dir = setup(&srcdir, "folder");
let msgs = m2dir.list();
let mut msg = msgs.into_iter().next().unwrap().unwrap();
let dstdir = tempdir().unwrap();
let m2dir = M2dir::create(dstdir.path()).unwrap();
assert_eq!(m2dir.count(), 0);
msg.move_to(&m2dir).unwrap();
assert_eq!(m2dir.count(), 1);
assert_eq!(msg.path.parent().unwrap(), dstdir.path());
assert!(msg
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
let loaded_flags = msg.flags().unwrap();
let flags = Flags::from([
Flag::Answered,
Flag::Seen,
Flag::Custom(String::from("patch")),
]);
assert_eq!(flags, loaded_flags);
}
#[test]
fn test_invalid_flags() {
let srcdir = tempdir().unwrap();
let m2dir = setup(&srcdir, "brokenflags");
let msgs = m2dir.list();
let msg = msgs.into_iter().next().unwrap().unwrap();
assert!(msg.flags().is_err());
}
#[test]
fn test_sanitize_filename() {
let srcdir = tempdir().unwrap();
let m2dir = setup(&srcdir, "lists/m2dir-dev");
assert_eq!(m2dir.count(), 1);
let msgs = m2dir.list();
let msg = msgs.into_iter().next().unwrap().unwrap();
assert!(msg
.path
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("2020-11-29_04:49_with_slash@bitfehler.net,CgEAAHrIEagdPryC."));
}
#[test]
#[ignore]
fn generate() {
let target = std::env::var("M2DIR_GENERATE_TEST_DATA")
.expect("Env var M2DIR_GENERATE_TEST_DATA must be set to target path");
generate_test_data(target)
}
}