maildir: import my fork into the repo

I want to remove the mailparse dep from it. I could do that its
own little repo, but it's more convenient to add it to our monorepo,
and then we can make it depend on our new mailparsing crate.
This commit is contained in:
Wez Furlong
2023-08-25 08:55:29 -07:00
parent ff9937d8db
commit c3503f0949
18 changed files with 1296 additions and 10 deletions
Generated
+26 -8
View File
@@ -90,7 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "751bbd7d440576066233e740576f1b31fdc6ab86cfabfbd48c548de77eca73e4"
dependencies = [
"amq-protocol-types",
"percent-encoding",
"percent-encoding 2.3.0",
"url",
]
@@ -350,7 +350,7 @@ dependencies = [
"matchit",
"memchr",
"mime",
"percent-encoding",
"percent-encoding 2.3.0",
"pin-project-lite",
"rustversion",
"serde",
@@ -1561,7 +1561,7 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
"percent-encoding 2.3.0",
]
[[package]]
@@ -2745,10 +2745,13 @@ dependencies = [
[[package]]
name = "maildir"
version = "0.6.3"
source = "git+https://github.com/wez/maildir.git?rev=898d604fec05bfcb5af52f3960c4b07028faad39#898d604fec05bfcb5af52f3960c4b07028faad39"
dependencies = [
"gethostname 0.2.3",
"mailparse",
"memmap2",
"percent-encoding 1.0.1",
"tempfile",
"walkdir",
]
[[package]]
@@ -2819,6 +2822,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]]
name = "memmem"
version = "0.1.1"
@@ -3553,6 +3565,12 @@ dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@@ -4067,7 +4085,7 @@ dependencies = [
"combine",
"crc16",
"itoa",
"percent-encoding",
"percent-encoding 2.3.0",
"r2d2",
"rand",
"ryu",
@@ -4178,7 +4196,7 @@ dependencies = [
"log",
"mime",
"once_cell",
"percent-encoding",
"percent-encoding 2.3.0",
"pin-project-lite",
"rustls",
"rustls-pemfile",
@@ -5334,7 +5352,7 @@ dependencies = [
"http-body",
"hyper",
"hyper-timeout",
"percent-encoding",
"percent-encoding 2.3.0",
"pin-project",
"prost",
"tokio",
@@ -5670,7 +5688,7 @@ checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"percent-encoding 2.3.0",
"serde",
]
+1 -1
View File
@@ -14,7 +14,7 @@ kumo-api-types = {path="../kumo-api-types"}
kumo-log-types = {path="../kumo-log-types"}
lipsum = "0.8"
mail-builder = "0.2"
maildir = {git="https://github.com/wez/maildir.git", rev="898d604fec05bfcb5af52f3960c4b07028faad39"}
maildir = {path="../maildir"}
mailparse = "0.14"
nix = {version="0.26", features=["signal"]}
rfc5321 = {path="../rfc5321"}
+1 -1
View File
@@ -37,7 +37,7 @@ kumo-server-runtime = {path="../kumo-server-runtime"}
lazy_static = "1.4"
lruttl = {path="../lruttl"}
mail-builder = "0.2"
maildir = {git="https://github.com/wez/maildir.git", rev="898d604fec05bfcb5af52f3960c4b07028faad39"}
maildir = {path="../maildir"}
memchr = "2.5"
message = {path="../message"}
metrics = "0.20"
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "maildir"
version = "0.6.3"
authors = ["Kartikaya Gupta"]
edition = "2018"
license = "0BSD"
description = "A simple library for maildir manipulation"
homepage = "https://github.com/staktrace/maildir/blob/master/README.md"
repository = "https://github.com/staktrace/maildir"
readme = "README.md"
keywords = ["maildir", "email", "rfc822", "mime"]
categories = ["email", "filesystem"]
exclude = [".gitignore", ".github/**"]
[badges]
maintenance = { status = "passively-maintained" }
[dependencies]
mailparse = "0.14"
gethostname = "0.2.3"
memmap2 = { version = "0.5.8", optional = true }
[features]
mmap = ["memmap2"]
[dev-dependencies]
tempfile = "3.0.8"
walkdir = "2.2.7"
percent-encoding = "1.0.1"
+13
View File
@@ -0,0 +1,13 @@
Copyright (C) 2019 by Kartikaya Gupta
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
+25
View File
@@ -0,0 +1,25 @@
maildir
===
![Build Status](https://github.com/staktrace/maildir/actions/workflows/test.yml/badge.svg)
[![Crate](https://img.shields.io/crates/v/maildir.svg)](https://crates.io/crates/maildir)
A simple library to deal with maildir folders
API
---
The primary entry point for this library is the Maildir structure, which can be created from a path, like so:
```rust
let maildir = Maildir::from("path/to/maildir");
```
The Maildir structure then has functions that can be used to access and modify mail files.
Documentation
---
See the rustdoc at [docs.rs](https://docs.rs/maildir/).
Support maildir
---
If you want to support development of `maildir`, please do so by donating your money, time, and/or energy to fighting climate change.
A quick and easy way is to send a donation to [Replant.ca Environmental](http://www.replant-environmental.ca/donate.html), where every dollar gets a tree planted!
+35
View File
@@ -0,0 +1,35 @@
use maildir::MailEntry;
use maildir::Maildir;
use std::io;
fn list_mail(mail: MailEntry) {
println!("Path: {}", mail.path().display());
println!("ID: {}", mail.id());
println!("Flags: {}", mail.flags());
println!("is_draft: {}", mail.is_draft());
println!("is_flagged: {}", mail.is_flagged());
println!("is_passed: {}", mail.is_passed());
println!("is_replied: {}", mail.is_replied());
println!("is_seen: {}", mail.is_seen());
println!("is_trashed: {}", mail.is_trashed());
}
fn process_maildirs(maildirs: impl IntoIterator<Item = Maildir>) -> Result<(), io::Error> {
maildirs.into_iter().try_for_each(|mdir| {
mdir.list_new()
.chain(mdir.list_cur())
.map(|r| r.map(list_mail))
.collect::<Result<_, _>>()
})
}
fn main() {
let rc = match process_maildirs(std::env::args().skip(1).map(Into::into)) {
Err(e) => {
eprintln!("Error: {:?}", e);
1
}
Ok(_) => 0,
};
std::process::exit(rc);
}
+777
View File
@@ -0,0 +1,777 @@
#[cfg(feature = "mmap")]
extern crate memmap2;
use std::error;
use std::fmt;
use std::fs;
use std::io::prelude::*;
use std::io::ErrorKind;
use std::ops::Deref;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time;
static COUNTER: AtomicUsize = AtomicUsize::new(0);
use mailparse::*;
#[cfg(unix)]
const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ":";
#[cfg(windows)]
const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ";";
#[derive(Debug)]
pub enum MailEntryError {
IOError(std::io::Error),
ParseError(MailParseError),
DateError(&'static str),
}
impl fmt::Display for MailEntryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MailEntryError::IOError(ref err) => write!(f, "IO error: {}", err),
MailEntryError::ParseError(ref err) => write!(f, "Parse error: {}", err),
MailEntryError::DateError(ref msg) => write!(f, "Date error: {}", msg),
}
}
}
impl error::Error for MailEntryError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
MailEntryError::IOError(ref err) => Some(err),
MailEntryError::ParseError(ref err) => Some(err),
MailEntryError::DateError(_) => None,
}
}
}
impl From<std::io::Error> for MailEntryError {
fn from(err: std::io::Error) -> MailEntryError {
MailEntryError::IOError(err)
}
}
impl From<MailParseError> for MailEntryError {
fn from(err: MailParseError) -> MailEntryError {
MailEntryError::ParseError(err)
}
}
impl From<&'static str> for MailEntryError {
fn from(err: &'static str) -> MailEntryError {
MailEntryError::DateError(err)
}
}
enum MailData {
None,
#[cfg(not(feature = "mmap"))]
Bytes(Vec<u8>),
#[cfg(feature = "mmap")]
File(memmap2::Mmap),
}
impl MailData {
fn is_none(&self) -> bool {
match self {
MailData::None => true,
_ => false,
}
}
}
/// This struct represents a single email message inside
/// the maildir. Creation of the struct does not automatically
/// load the content of the email file into memory - however,
/// that may happen upon calling functions that require parsing
/// the email.
pub struct MailEntry {
id: String,
flags: String,
path: PathBuf,
data: MailData,
}
impl MailEntry {
pub fn id(&self) -> &str {
&self.id
}
fn read_data(&mut self) -> std::io::Result<()> {
if self.data.is_none() {
#[cfg(feature = "mmap")]
{
let f = fs::File::open(&self.path)?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&f)? };
self.data = MailData::File(mmap);
}
#[cfg(not(feature = "mmap"))]
{
let mut f = fs::File::open(&self.path)?;
let mut d = Vec::<u8>::new();
f.read_to_end(&mut d)?;
self.data = MailData::Bytes(d);
}
}
Ok(())
}
pub fn parsed(&mut self) -> Result<ParsedMail, MailEntryError> {
self.read_data()?;
match self.data {
MailData::None => panic!("read_data should have returned an Err!"),
#[cfg(not(feature = "mmap"))]
MailData::Bytes(ref b) => parse_mail(b).map_err(MailEntryError::ParseError),
#[cfg(feature = "mmap")]
MailData::File(ref m) => parse_mail(m).map_err(MailEntryError::ParseError),
}
}
pub fn headers(&mut self) -> Result<Vec<MailHeader>, MailEntryError> {
self.read_data()?;
let headers = match self.data {
MailData::None => panic!("read_data should have returned an Err!"),
#[cfg(not(feature = "mmap"))]
MailData::Bytes(ref b) => parse_headers(b),
#[cfg(feature = "mmap")]
MailData::File(ref m) => parse_headers(m),
};
headers.map(|(v, _)| v).map_err(MailEntryError::ParseError)
}
pub fn received(&mut self) -> Result<i64, MailEntryError> {
self.read_data()?;
let headers = self.headers()?;
let received = headers.get_first_value("Received");
match received {
Some(v) => v
.rsplit(';')
.nth(0)
.ok_or_else(|| MailEntryError::DateError("Unable to split Received header"))
.and_then(|ts| dateparse(ts).map_err(MailEntryError::from)),
None => Err("No Received header found")?,
}
}
pub fn date(&mut self) -> Result<i64, MailEntryError> {
self.read_data()?;
let headers = self.headers()?;
let date = headers.get_first_value("Date");
match date {
Some(ts) => dateparse(&ts).map_err(MailEntryError::from),
None => Err("No Date header found")?,
}
}
pub fn flags(&self) -> &str {
&self.flags
}
pub fn is_draft(&self) -> bool {
self.flags.contains('D')
}
pub fn is_flagged(&self) -> bool {
self.flags.contains('F')
}
pub fn is_passed(&self) -> bool {
self.flags.contains('P')
}
pub fn is_replied(&self) -> bool {
self.flags.contains('R')
}
pub fn is_seen(&self) -> bool {
self.flags.contains('S')
}
pub fn is_trashed(&self) -> bool {
self.flags.contains('T')
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
enum Subfolder {
New,
Cur,
}
/// An iterator over the email messages in a particular
/// maildir subfolder (either `cur` or `new`). This iterator
/// produces a `std::io::Result<MailEntry>`, which can be an
/// `Err` if an error was encountered while trying to read
/// file system properties on a particular entry, or if an
/// invalid file was found in the maildir. Files starting with
/// a dot (.) character in the maildir folder are ignored.
pub struct MailEntries {
path: PathBuf,
subfolder: Subfolder,
readdir: Option<fs::ReadDir>,
}
impl MailEntries {
fn new(path: PathBuf, subfolder: Subfolder) -> MailEntries {
MailEntries {
path,
subfolder,
readdir: None,
}
}
}
impl Iterator for MailEntries {
type Item = std::io::Result<MailEntry>;
fn next(&mut self) -> Option<std::io::Result<MailEntry>> {
if self.readdir.is_none() {
let mut dir_path = self.path.clone();
dir_path.push(match self.subfolder {
Subfolder::New => "new",
Subfolder::Cur => "cur",
});
self.readdir = match fs::read_dir(dir_path) {
Err(_) => return None,
Ok(v) => Some(v),
};
}
loop {
// we need to skip over files starting with a '.'
let dir_entry = self.readdir.iter_mut().next().unwrap().next();
let result = dir_entry.map(|e| {
let entry = e?;
let filename = String::from(entry.file_name().to_string_lossy().deref());
if filename.starts_with('.') {
return Ok(None);
}
let (id, flags) = match self.subfolder {
Subfolder::New => (Some(filename.as_str()), Some("")),
Subfolder::Cur => {
let delim = format!("{}2,", INFORMATIONAL_SUFFIX_SEPARATOR);
let mut iter = filename.split(&delim);
(iter.next(), iter.next())
}
};
if id.is_none() || flags.is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Non-maildir file found in maildir",
));
}
Ok(Some(MailEntry {
id: String::from(id.unwrap()),
flags: String::from(flags.unwrap()),
path: entry.path(),
data: MailData::None,
}))
});
return match result {
None => None,
Some(Err(e)) => Some(Err(e)),
Some(Ok(None)) => continue,
Some(Ok(Some(v))) => Some(Ok(v)),
};
}
}
}
#[derive(Debug)]
pub enum MaildirError {
Io(std::io::Error),
Utf8(std::str::Utf8Error),
Time(std::time::SystemTimeError),
}
impl fmt::Display for MaildirError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use MaildirError::*;
match *self {
Io(ref e) => write!(f, "IO Error: {}", e),
Utf8(ref e) => write!(f, "UTF8 Encoding Error: {}", e),
Time(ref e) => write!(f, "Time Error: {}", e),
}
}
}
impl error::Error for MaildirError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use MaildirError::*;
match *self {
Io(ref e) => Some(e),
Utf8(ref e) => Some(e),
Time(ref e) => Some(e),
}
}
}
impl From<std::io::Error> for MaildirError {
fn from(e: std::io::Error) -> MaildirError {
MaildirError::Io(e)
}
}
impl From<std::str::Utf8Error> for MaildirError {
fn from(e: std::str::Utf8Error) -> MaildirError {
MaildirError::Utf8(e)
}
}
impl From<std::time::SystemTimeError> for MaildirError {
fn from(e: std::time::SystemTimeError) -> MaildirError {
MaildirError::Time(e)
}
}
/// An iterator over the maildir subdirectories. This iterator
/// produces a `std::io::Result<Maildir>`, which can be an
/// `Err` if an error was encountered while trying to read
/// file system properties on a particular entry. Only
/// subdirectories starting with a single period are included.
pub struct MaildirEntries {
path: PathBuf,
readdir: Option<fs::ReadDir>,
}
impl MaildirEntries {
fn new(path: PathBuf) -> MaildirEntries {
MaildirEntries {
path,
readdir: None,
}
}
}
impl Iterator for MaildirEntries {
type Item = std::io::Result<Maildir>;
fn next(&mut self) -> Option<std::io::Result<Maildir>> {
if self.readdir.is_none() {
self.readdir = match fs::read_dir(&self.path) {
Err(_) => return None,
Ok(v) => Some(v),
};
}
loop {
let dir_entry = self.readdir.iter_mut().next().unwrap().next();
let result = dir_entry.map(|e| {
let entry = e?;
// a dir name should start by one single period
let filename = String::from(entry.file_name().to_string_lossy().deref());
if !filename.starts_with('.') || filename.starts_with("..") {
return Ok(None);
}
// the entry should be a directory
let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or_default();
if !is_dir {
return Ok(None);
}
Ok(Some(Maildir {
path: self.path.join(filename),
}))
});
return match result {
None => None,
Some(Err(e)) => Some(Err(e)),
Some(Ok(None)) => continue,
Some(Ok(Some(v))) => Some(Ok(v)),
};
}
}
}
/// The main entry point for this library. This struct can be
/// instantiated from a path using the `from` implementations.
/// The path passed in to the `from` should be the root of the
/// maildir (the folder containing `cur`, `new`, and `tmp`).
pub struct Maildir {
path: PathBuf,
}
impl Maildir {
/// Returns the path of the maildir base folder.
pub fn path(&self) -> &Path {
&self.path
}
/// Returns the number of messages found inside the `new`
/// maildir folder.
pub fn count_new(&self) -> usize {
self.list_new().count()
}
/// Returns the number of messages found inside the `cur`
/// maildir folder.
pub fn count_cur(&self) -> usize {
self.list_cur().count()
}
/// Returns an iterator over the messages inside the `new`
/// maildir folder. The order of messages in the iterator
/// is not specified, and is not guaranteed to be stable
/// over multiple invocations of this method.
pub fn list_new(&self) -> MailEntries {
MailEntries::new(self.path.clone(), Subfolder::New)
}
/// Returns an iterator over the messages inside the `cur`
/// maildir folder. The order of messages in the iterator
/// is not specified, and is not guaranteed to be stable
/// over multiple invocations of this method.
pub fn list_cur(&self) -> MailEntries {
MailEntries::new(self.path.clone(), Subfolder::Cur)
}
/// Returns an iterator over the maildir subdirectories.
/// The order of subdirectories in the iterator
/// is not specified, and is not guaranteed to be stable
/// over multiple invocations of this method.
pub fn list_subdirs(&self) -> MaildirEntries {
MaildirEntries::new(self.path.clone())
}
/// Moves a message from the `new` maildir folder to the
/// `cur` maildir folder. The id passed in should be
/// obtained from the iterator produced by `list_new`.
pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
self.move_new_to_cur_with_flags(id, "")
}
/// Moves a message from the `new` maildir folder to the `cur` maildir folder, and sets the
/// given flags. The id passed in should be obtained from the iterator produced by `list_new`.
///
/// The possible flags are described e.g. at <https://cr.yp.to/proto/maildir.html> or
/// <http://www.courier-mta.org/maildir.html>.
pub fn move_new_to_cur_with_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
let src = self.path.join("new").join(id);
let dst = self.path.join("cur").join(format!(
"{}{}2,{}",
id,
INFORMATIONAL_SUFFIX_SEPARATOR,
Self::normalize_flags(flags)
));
fs::rename(src, dst)
}
/// Copies a message from the current maildir to the targetted maildir.
pub fn copy_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
let entry = self.find(id).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
})?;
let filename = entry.path().file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid mail entry file name",
)
})?;
let src_path = entry.path();
let dst_path = target.path().join("cur").join(filename);
if src_path == &dst_path {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Target maildir needs to be different from the source",
));
}
fs::copy(src_path, dst_path)?;
Ok(())
}
/// Moves a message from the current maildir to the targetted maildir.
pub fn move_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
let entry = self.find(id).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
})?;
let filename = entry.path().file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid mail entry file name",
)
})?;
fs::rename(entry.path(), target.path().join("cur").join(filename))?;
Ok(())
}
/// Tries to find the message with the given id in the
/// maildir. This searches both the `new` and the `cur`
/// folders.
pub fn find(&self, id: &str) -> Option<MailEntry> {
let filter = |entry: &std::io::Result<MailEntry>| match *entry {
Err(_) => false,
Ok(ref e) => e.id() == id,
};
self.list_new()
.find(&filter)
.or_else(|| self.list_cur().find(&filter))
.map(|e| e.unwrap())
}
fn normalize_flags(flags: &str) -> String {
let mut flag_chars = flags.chars().collect::<Vec<char>>();
flag_chars.sort();
flag_chars.dedup();
flag_chars.into_iter().collect()
}
fn update_flags<F>(&self, id: &str, flag_op: F) -> std::io::Result<()>
where
F: Fn(&str) -> String,
{
let filter = |entry: &std::io::Result<MailEntry>| match *entry {
Err(_) => false,
Ok(ref e) => e.id() == id,
};
match self.list_cur().find(&filter).map(|e| e.unwrap()) {
Some(m) => {
let src = m.path();
let mut dst = m.path().clone();
dst.pop();
dst.push(format!(
"{}{}2,{}",
m.id(),
INFORMATIONAL_SUFFIX_SEPARATOR,
flag_op(m.flags())
));
fs::rename(src, dst)
}
None => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Mail entry not found",
)),
}
}
/// Updates the flags for the message with the given id in the
/// maildir. This only searches the `cur` folder, because that's
/// the folder where messages have flags. Returns an error if the
/// message was not found. All existing flags are overwritten with
/// the new flags provided.
pub fn set_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
self.update_flags(id, |_old_flags| Self::normalize_flags(flags))
}
/// Adds the given flags to the message with the given id in the maildir.
/// This only searches the `cur` folder, because that's the folder where
/// messages have flags. Returns an error if the message was not found.
/// Flags are deduplicated, so setting a already-set flag has no effect.
pub fn add_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
let flag_merge = |old_flags: &str| {
let merged = String::from(old_flags) + flags;
Self::normalize_flags(&merged)
};
self.update_flags(id, &flag_merge)
}
/// Removes the given flags to the message with the given id in the maildir.
/// This only searches the `cur` folder, because that's the folder where
/// messages have flags. Returns an error if the message was not found.
/// If the message doesn't have the flag(s) to be removed, those flags are
/// ignored.
pub fn remove_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
let flag_strip =
|old_flags: &str| old_flags.chars().filter(|c| !flags.contains(*c)).collect();
self.update_flags(id, &flag_strip)
}
/// Deletes the message with the given id in the maildir.
/// This searches both the `new` and the `cur` folders,
/// and deletes the file from the filesystem. Returns an
/// error if no message was found with the given id.
pub fn delete(&self, id: &str) -> std::io::Result<()> {
match self.find(id) {
Some(m) => fs::remove_file(m.path()),
None => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Mail entry not found",
)),
}
}
/// Creates all neccessary directories if they don't exist yet. It is the library user's
/// responsibility to call this before using `store_new`.
pub fn create_dirs(&self) -> std::io::Result<()> {
let mut path = self.path.clone();
for d in &["cur", "new", "tmp"] {
path.push(d);
fs::create_dir_all(path.as_path())?;
path.pop();
}
Ok(())
}
/// Stores the given message data as a new message file in the Maildir `new` folder. Does not
/// create the neccessary directories, so if in doubt call `create_dirs` before using
/// `store_new`.
/// Returns the Id of the inserted message on success.
pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
self.store(Subfolder::New, data, "")
}
/// Stores the given message data as a new message file in the Maildir `cur` folder, adding the
/// given `flags` to it. The possible flags are explained e.g. at
/// <https://cr.yp.to/proto/maildir.html> or <http://www.courier-mta.org/maildir.html>.
/// Returns the Id of the inserted message on success.
pub fn store_cur_with_flags(
&self,
data: &[u8],
flags: &str,
) -> std::result::Result<String, MaildirError> {
self.store(
Subfolder::Cur,
data,
&format!(
"{}2,{}",
INFORMATIONAL_SUFFIX_SEPARATOR,
Self::normalize_flags(flags)
),
)
}
fn store(
&self,
subfolder: Subfolder,
data: &[u8],
info: &str,
) -> std::result::Result<String, MaildirError> {
// try to get some uniquenes, as described at http://cr.yp.to/proto/maildir.html
// dovecot and courier IMAP use <timestamp>.M<usec>P<pid>.<hostname> for tmp-files and then
// move to <timestamp>.M<usec>P<pid>V<dev>I<ino>.<hostname>,S=<size_in_bytes> when moving
// to new dir. see for example http://www.courier-mta.org/maildir.html.
let pid = std::process::id();
let hostname = gethostname::gethostname()
.into_string()
// the hostname is always ASCII in order to be a valid DNS
// name, so into_string() will always succeed. The error case
// here is to satisfy the compiler which doesn't know this.
.unwrap_or_else(|_| "localhost".to_string());
// loop when conflicting filenames occur, as described at
// http://www.courier-mta.org/maildir.html
// this assumes that pid and hostname don't change.
let mut tmppath = self.path.clone();
tmppath.push("tmp");
let mut file;
let mut secs;
let mut nanos;
let mut counter;
loop {
let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
secs = ts.as_secs();
nanos = ts.subsec_nanos();
counter = COUNTER.fetch_add(1, Ordering::SeqCst);
tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmppath)
{
Ok(f) => {
file = f;
break;
}
Err(err) => {
if err.kind() != ErrorKind::AlreadyExists {
return Err(err.into());
}
tmppath.pop();
}
}
}
/// At this point, `file` is our new file at `tmppath`.
/// If we leave the scope of this function prior to
/// successfully writing the file to its final location,
/// we need to ensure that we remove the temporary file.
/// This struct takes care of that detail.
struct UnlinkOnError {
path_to_unlink: Option<PathBuf>,
}
impl Drop for UnlinkOnError {
fn drop(&mut self) {
if let Some(path) = self.path_to_unlink.take() {
// Best effort to remove it
std::fs::remove_file(path).ok();
}
}
}
// Ensure that we remove the temporary file on failure
let mut unlink_guard = UnlinkOnError {
path_to_unlink: Some(tmppath.clone()),
};
file.write_all(data)?;
file.sync_all()?;
let meta = file.metadata()?;
let mut newpath = self.path.clone();
newpath.push(match subfolder {
Subfolder::New => "new",
Subfolder::Cur => "cur",
});
#[cfg(unix)]
let dev = meta.dev();
#[cfg(windows)]
let dev: u64 = 0;
#[cfg(unix)]
let ino = meta.ino();
#[cfg(windows)]
let ino: u64 = 0;
#[cfg(unix)]
let size = meta.size();
#[cfg(windows)]
let size = meta.file_size();
let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
newpath.push(format!("{}{}", id, info));
std::fs::rename(&tmppath, &newpath)?;
unlink_guard.path_to_unlink.take();
Ok(id)
}
}
impl From<PathBuf> for Maildir {
fn from(p: PathBuf) -> Maildir {
Maildir { path: p }
}
}
impl From<String> for Maildir {
fn from(s: String) -> Maildir {
Maildir::from(PathBuf::from(s))
}
}
impl<'a> From<&'a str> for Maildir {
fn from(s: &str) -> Maildir {
Maildir::from(PathBuf::from(s))
}
}
@@ -0,0 +1,31 @@
Return-Path: <example@example.com>
X-Original-To: test.foobar@example.com
Delivered-To: x11700823@homiemail-mx7.g.dreamhost.com
Received: from homiemail-a22.g.dreamhost.com (agjbgdcfdaaf.dreamhost.com [69.163.253.5])
(using TLSv1 with cipher ADH-AES256-SHA (256/256 bits))
(No client certificate requested)
by homiemail-mx7.g.dreamhost.com (Postfix) with ESMTPS id 266801B5A25A
for <test.foobar@example.com>; Sat, 21 May 2016 15:08:27 -0700 (PDT)
Received: from homiemail-a22.g.dreamhost.com (localhost [127.0.0.1])
by homiemail-a22.g.dreamhost.com (Postfix) with ESMTP id D754B114066
for <test.foobar@example.com>; Sat, 21 May 2016 15:08:26 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=example.com; h=
mime-version:from:to:subject:date:content-type:message-id; s=
example.com; bh=GICvp8PrP/1WRMKD4SG0HV8hzq0=; b=hGorQMWIxA6tFt
+h5bIG81B9AHK3kTpAhHrRvyy01Xnbkai4vAaEMWn2pXzb1KkdxKquaZhnEagLrA
6OiVHlYHbM+y6USAw1+O6/AqB/AkctAZiaHXEQEtefBIWK1zzD9jHnPF1C5Ylb6K
hdMCOp6xlDDqNLXEBjIkfmFjeXU1U=
Received: from localhost (apache2-quack.mug.dreamhost.com [208.113.163.206])
(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
(No client certificate requested)
by homiemail-a22.g.dreamhost.com (Postfix) with ESMTPSA id 9AEBE114065
for <test.foobar@example.com>; Sat, 21 May 2016 15:08:26 -0700 (PDT)
MIME-Version: 1.0
From: Kartikaya Gupta <example@example.com>
To: test.foobar@example.com
Subject: test
Date: Sat, 21 May 2016 22:08:25 +0000
Content-Type: text/plain; charset=utf-8
Message-Id: <20160521220826.9AEBE114065@homiemail-a22.g.dreamhost.com>
Roundtrip
@@ -0,0 +1,31 @@
Return-Path: <example@example.com>
X-Original-To: test.foobar@example.com
Delivered-To: x11700823@homiemail-mx8.g.dreamhost.com
Received: from homiemail-a15.g.dreamhost.com (agjbgdcfdaaf.dreamhost.com [69.163.253.5])
(using TLSv1 with cipher ADH-AES256-SHA (256/256 bits))
(No client certificate requested)
by homiemail-mx8.g.dreamhost.com (Postfix) with ESMTPS id 7DEB6A010C
for <test.foobar@example.com>; Sun, 22 May 2016 11:16:51 -0700 (PDT)
Received: from homiemail-a15.g.dreamhost.com (localhost [127.0.0.1])
by homiemail-a15.g.dreamhost.com (Postfix) with ESMTP id 3016776C06B;
Sun, 22 May 2016 11:16:51 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=example.com; h=
mime-version:from:to:subject:date:content-type:message-id; s=
example.com; bh=OtypsA3TTYXz2zqEj+PynojfmXg=; b=SDmJv4I7Cq/qtN
eLA2FWO0PW59gmRw7JPjnQ8lRLrB6DaR7REHkfbKOurvMmri6WFlm59kS52zS0Xm
KDdhEO3tBFIGTtytiZAI9W033LHb8fZiwQ8W7O3ssSWUBR3kV6fDXjB4/uSXt+kU
H3aU9QSlHlPohrye0oGaSB5sl838k=
Received: from localhost (apache2-quack.mug.dreamhost.com [208.113.163.206])
(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
(No client certificate requested)
by homiemail-a15.g.dreamhost.com (Postfix) with ESMTPSA id D149A76C069;
Sun, 22 May 2016 11:16:50 -0700 (PDT)
MIME-Version: 1.0
From: Kartikaya Gupta <example@example.com>
To: test.foobar@example.com
Subject: test
Date: Sun, 22 May 2016 18:16:50 +0000
Content-Type: text/plain; charset=utf-8
Message-Id: <20160522181650.D149A76C069@homiemail-a15.g.dreamhost.com>
Ignore this :)
View File
View File
+326
View File
@@ -0,0 +1,326 @@
use maildir::*;
#[cfg(unix)]
use std::borrow::Cow;
#[cfg(unix)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::ffi::OsString;
use std::fs;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(windows)]
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use mailparse::MailHeaderMap;
use percent_encoding::percent_decode;
use tempfile::tempdir;
use walkdir::WalkDir;
static TESTDATA_DIR: &str = "testdata";
static MAILDIR_NAME: &str = "maildir";
static SUBMAILDIRS_NAME: &str = "submaildirs";
// `cargo package` doesn't package files with certain characters, such as
// colons, in the name, so we percent-decode the file names when copying the
// data for the tests.
// This code can likely be improved (for correctness, particularly on Windows)
// but there's no good docs on what `cargo package` does percent-encoding for,
// and how it deals with multibyte characters in filenames. In practice this
// code works fine for this crate, because we have a restricted set of ASCII
// characters in the filenames.
fn with_maildir<F>(name: &str, func: F)
where
F: FnOnce(Maildir),
{
let tmp_dir = tempdir().expect("could not create temporary directory");
let tmp_path = tmp_dir.path();
for entry in WalkDir::new(TESTDATA_DIR) {
let entry = entry.expect("directory walk error");
let relative = entry.path().strip_prefix(TESTDATA_DIR).unwrap();
if relative.parent().is_none() {
continue;
}
#[cfg(unix)]
let decoded_bytes: Cow<[u8]> = percent_decode(relative.as_os_str().as_bytes()).into();
#[cfg(unix)]
let decoded = OsStr::from_bytes(&decoded_bytes);
#[cfg(windows)]
let decoded_bytes = relative
.as_os_str()
.encode_wide()
.map(|b| b as u8)
.collect::<Vec<_>>();
#[cfg(windows)]
let decoded_bytes = percent_decode(decoded_bytes.as_slice())
.map(|b| (if b == b':' { b';' } else { b }) as u16)
.collect::<Vec<_>>();
#[cfg(windows)]
let decoded = OsString::from_wide(decoded_bytes.as_slice());
if entry.path().is_dir() {
fs::create_dir(tmp_path.join(&decoded)).expect("could not create directory");
} else {
fs::copy(entry.path(), tmp_path.join(decoded)).expect("could not copy test data");
}
}
func(Maildir::from(tmp_path.join(name)));
}
fn with_maildir_empty<F>(name: &str, func: F)
where
F: FnOnce(Maildir),
{
let tmp_dir = tempdir().expect("could not create temporary directory");
let tmp_path = tmp_dir.path();
func(Maildir::from(tmp_path.join(name)));
}
#[test]
fn maildir_count() {
with_maildir(MAILDIR_NAME, |maildir| {
assert_eq!(maildir.count_cur(), 1);
assert_eq!(maildir.count_new(), 1);
});
}
#[test]
fn maildir_list() {
with_maildir(MAILDIR_NAME, |maildir| {
let mut iter = maildir.list_new();
let mut first = iter.next().unwrap().unwrap();
assert_eq!(first.id(), "1463941010.5f7fa6dd4922c183dc457d033deee9d7");
assert_eq!(
first.headers().unwrap().get_first_value("Subject"),
Some(String::from("test"))
);
assert_eq!(first.is_seen(), false);
let second = iter.next();
assert!(second.is_none());
let mut iter = maildir.list_cur();
let mut first = iter.next().unwrap().unwrap();
assert_eq!(first.id(), "1463868505.38518452d49213cb409aa1db32f53184");
assert_eq!(
first.parsed().unwrap().headers.get_first_value("Subject"),
Some(String::from("test"))
);
assert_eq!(first.is_seen(), true);
let second = iter.next();
assert!(second.is_none());
})
}
#[test]
fn maildir_list_subdirs() {
with_maildir(SUBMAILDIRS_NAME, |maildir| {
let subdirs: Vec<_> = maildir
.list_subdirs()
.map(|dir| {
dir.unwrap()
.path()
.file_name()
.unwrap()
.to_string_lossy()
.to_string()
})
.collect();
assert_eq!(2, subdirs.len());
assert!(subdirs.contains(&".Subdir1".into()));
assert!(subdirs.contains(&".Subdir2".into()));
assert!(!subdirs.contains(&"..Subdir3".into()));
});
}
#[test]
fn maildir_find() {
with_maildir(MAILDIR_NAME, |maildir| {
assert_eq!(
maildir
.find("1463941010.5f7fa6dd4922c183dc457d033deee9d7")
.is_some(),
true
);
assert_eq!(
maildir
.find("1463868505.38518452d49213cb409aa1db32f53184")
.is_some(),
true
);
})
}
#[test]
fn check_delete() {
with_maildir(MAILDIR_NAME, |maildir| {
assert_eq!(
maildir
.find("1463941010.5f7fa6dd4922c183dc457d033deee9d7")
.is_some(),
true
);
assert_eq!(
maildir
.delete("1463941010.5f7fa6dd4922c183dc457d033deee9d7")
.is_ok(),
true
);
assert_eq!(
maildir
.find("1463941010.5f7fa6dd4922c183dc457d033deee9d7")
.is_some(),
false
);
})
}
#[test]
fn check_copy_and_move() {
with_maildir(MAILDIR_NAME, |maildir| {
with_maildir(SUBMAILDIRS_NAME, |submaildir| {
let id = "1463868505.38518452d49213cb409aa1db32f53184";
// check that we cannot copy a message from and to the same maildir
assert_eq!(
maildir.copy_to(id, &maildir).unwrap_err().kind(),
std::io::ErrorKind::InvalidInput,
);
// check that the message is present in "maildir" but not in "submaildir"
assert!(maildir.find(id).is_some());
assert!(submaildir.find(id).is_none());
// also check that the failed self-copy a few lines up didn't corrupt the
// message file.
assert!(maildir.find(id).unwrap().date().is_ok());
// copy the message from "maildir" to "submaildir"
maildir.copy_to(id, &submaildir).unwrap();
// check that the message is now present in both
assert!(maildir.find(id).is_some());
assert!(submaildir.find(id).is_some());
// move the message from "submaildir" to "maildir"
submaildir.move_to(id, &maildir).unwrap();
// check that the message is now only present in "maildir"
assert!(maildir.find(id).is_some());
assert!(submaildir.find(id).is_none());
})
})
}
#[test]
fn mark_read() {
with_maildir(MAILDIR_NAME, |maildir| {
assert_eq!(
maildir
.move_new_to_cur("1463941010.5f7fa6dd4922c183dc457d033deee9d7")
.unwrap(),
()
);
});
}
#[test]
fn check_received() {
with_maildir(MAILDIR_NAME, |maildir| {
let mut iter = maildir.list_cur();
let mut first = iter.next().unwrap().unwrap();
assert_eq!(first.received().unwrap(), 1_463_868_507);
});
}
#[test]
fn check_create_dirs() {
with_maildir_empty("maildir2", |maildir| {
assert!(!maildir.path().exists());
for name in &["cur", "new", "tmp"] {
assert!(!maildir.path().join(name).exists());
}
maildir.create_dirs().unwrap();
assert!(maildir.path().exists());
for name in &["cur", "new", "tmp"] {
assert!(maildir.path().join(name).exists());
}
});
}
const TEST_MAIL_BODY: &[u8] = b"Return-Path: <of82ecuq@cip.cs.fau.de>
X-Original-To: of82ecuq@cip.cs.fau.de
Delivered-To: of82ecuq@cip.cs.fau.de
Received: from faui0fl.informatik.uni-erlangen.de (unknown [IPv6:2001:638:a000:4160:131:188:60:117])
by faui03.informatik.uni-erlangen.de (Postfix) with ESMTP id 466C1240A3D
for <of82ecuq@cip.cs.fau.de>; Fri, 12 May 2017 10:09:45 +0000 (UTC)
Received: by faui0fl.informatik.uni-erlangen.de (Postfix, from userid 303135)
id 389CC10E1A32; Fri, 12 May 2017 12:09:45 +0200 (CEST)
To: of82ecuq@cip.cs.fau.de
MIME-Version: 1.0
Content-Type: text/plain; charset=\"UTF-8\"
Content-Transfer-Encoding: 8bit
Message-Id: <20170512100945.389CC10E1A32@faui0fl.informatik.uni-erlangen.de>
Date: Fri, 12 May 2017 12:09:45 +0200 (CEST)
From: of82ecuq@cip.cs.fau.de (Johannes Schilling)
Subject: maildir delivery test mail
Today is Boomtime, the 59th day of Discord in the YOLD 3183";
#[test]
fn check_store_new() {
with_maildir_empty("maildir2", |maildir| {
maildir.create_dirs().unwrap();
assert_eq!(maildir.count_new(), 0);
let id = maildir.store_new(TEST_MAIL_BODY);
assert!(id.is_ok());
assert_eq!(maildir.count_new(), 1);
let id = id.unwrap();
let msg = maildir.find(&id);
assert!(msg.is_some());
assert_eq!(
msg.unwrap().parsed().unwrap().get_body_raw().unwrap(),
b"Today is Boomtime, the 59th day of Discord in the YOLD 3183".as_ref()
);
});
}
#[test]
fn check_store_cur() {
with_maildir_empty("maildir2", |maildir| {
maildir.create_dirs().unwrap();
let testflags = "FRS";
assert_eq!(maildir.count_cur(), 0);
maildir
.store_cur_with_flags(TEST_MAIL_BODY, testflags)
.unwrap();
assert_eq!(maildir.count_cur(), 1);
let mut iter = maildir.list_cur();
let first = iter.next().unwrap().unwrap();
assert_eq!(first.flags(), testflags);
});
}
#[test]
fn check_flag_fiddling() {
with_maildir_empty("maildir2", |maildir| {
maildir.create_dirs().unwrap();
let id = maildir.store_cur_with_flags(TEST_MAIL_BODY, "SR").unwrap();
assert_eq!(maildir.count_cur(), 1);
assert_eq!(maildir.find(&id).unwrap().flags(), "RS");
maildir.remove_flags(&id, "FS").unwrap();
assert_eq!(maildir.find(&id).unwrap().flags(), "R");
maildir.add_flags(&id, "RF").unwrap();
assert_eq!(maildir.find(&id).unwrap().flags(), "FR");
maildir.set_flags(&id, "SF").unwrap();
assert_eq!(maildir.find(&id).unwrap().flags(), "FS");
});
}