use super::Transport;
use anyhow::Context;
use vsmtp_common::{
libc_abstraction::{chown, getpwuid},
rcpt::Rcpt,
transfer::{EmailTransferStatus, TransferErrorsVariant},
Address, ContextFinished,
};
use vsmtp_config::Config;
#[derive(Default)]
#[non_exhaustive]
pub struct Maildir;
#[async_trait::async_trait]
impl Transport for Maildir {
#[tracing::instrument(name = "maildir", skip_all)]
async fn deliver(
self,
config: &Config,
ctx: &ContextFinished,
_: &Option<Address>,
mut to: Vec<Rcpt>,
content: &str,
) -> Vec<Rcpt> {
let msg_uuid = &ctx.mail_from.message_uuid;
for rcpt in &mut to {
match users::get_user_by_name(rcpt.address.local_part()).map(|user| {
Self::write_to_maildir(
rcpt,
&user,
config.server.system.group_local.as_ref(),
msg_uuid,
content,
)
}) {
Some(Ok(())) => {
tracing::info!("Email delivered.");
rcpt.email_status = EmailTransferStatus::sent();
}
Some(Err(error)) => {
tracing::error!(%error, "Email delivery failure.");
rcpt.email_status
.held_back(TransferErrorsVariant::LocalDeliveryError {
error: error.to_string(),
});
}
None => {
tracing::error!(
error = format!("user not found: {}", rcpt.address.local_part()),
"Email delivery failure."
);
rcpt.email_status
.held_back(TransferErrorsVariant::NoSuchMailbox {
name: rcpt.address.local_part().to_owned(),
});
}
}
}
to
}
}
impl Maildir {
#[allow(clippy::unreachable, clippy::panic_in_result_fn)] #[tracing::instrument(name = "create-maildir", fields(folder = ?path.display()))]
fn create_and_chown(
path: &std::path::PathBuf,
user: &users::User,
group_local: Option<&users::Group>,
) -> anyhow::Result<()> {
if path.exists() {
tracing::info!("Folder already exists.");
} else {
tracing::debug!("Creating folder.");
std::fs::create_dir_all(path)
.with_context(|| format!("failed to create {}", path.display()))?;
tracing::trace!(
user = user.uid(),
group = group_local.map_or(u32::MAX, users::Group::gid),
"Setting permissions.",
);
chown(path, Some(user.uid()), group_local.map(users::Group::gid))
.with_context(|| format!("failed to set user rights to {}", path.display()))?;
}
Ok(())
}
fn write_to_maildir(
rcpt: &Rcpt,
user: &users::User,
group_local: Option<&users::Group>,
msg_uuid: &uuid::Uuid,
content: &str,
) -> anyhow::Result<()> {
let maildir = std::path::PathBuf::from_iter([getpwuid(user.uid())?, "Maildir".into()]);
Self::create_and_chown(&maildir, user, group_local)?;
for dir in ["new", "tmp", "cur"] {
Self::create_and_chown(&maildir.join(dir), user, group_local)?;
}
let file_in_maildir_inbox = maildir.join(format!("new/{msg_uuid}.eml"));
let mut email = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&file_in_maildir_inbox)?;
std::io::Write::write_all(&mut email, format!("Delivered-To: {rcpt}\n").as_bytes())?;
std::io::Write::write_all(&mut email, content.as_bytes())?;
chown(
&file_in_maildir_inbox,
Some(user.uid()),
group_local.map(users::Group::gid),
)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use users::os::unix::UserExt;
use vsmtp_common::{addr, transfer::Transfer};
use vsmtp_test::config::{local_ctx, local_test};
#[rstest::rstest]
#[case::not_existing("foobar", Err(TransferErrorsVariant::NoSuchMailbox {
name: "foobar".to_owned()
}))]
#[case::no_privilege("root", Err(TransferErrorsVariant::LocalDeliveryError {
error: "failed to create /root/Maildir".to_owned()
}))]
#[case::valid(users::get_current_username().unwrap().to_str().unwrap().to_owned(), Ok(()))]
fn maildir(#[case] mailbox: String, #[case] expected: Result<(), TransferErrorsVariant>) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let config = local_test();
let context = local_ctx();
let fake_message = "Hello World!\r\n";
let result = Maildir::default()
.deliver(
&config,
&context,
&Some(addr!("foo@domain.com")),
vec![Rcpt {
address: addr!(&format!("{mailbox}@domain.com")),
transfer_method: Transfer::Maildir,
email_status: EmailTransferStatus::default(),
}],
fake_message,
)
.await;
#[allow(
clippy::indexing_slicing,
clippy::unreachable,
clippy::wildcard_enum_match_arm
)]
match expected {
Ok(()) => {
assert!(matches!(
result[0].email_status,
EmailTransferStatus::Sent { .. }
));
let filepath = std::path::PathBuf::from_iter([
users::get_user_by_uid(users::get_current_uid())
.unwrap()
.home_dir()
.as_os_str()
.to_str()
.unwrap(),
"Maildir",
"new",
&format!("{}.eml", context.mail_from.message_uuid),
]);
assert_eq!(
std::fs::read_to_string(filepath).unwrap(),
format!("Delivered-To: {mailbox}@domain.com\nHello World!\r\n")
);
}
Err(error) => match result[0].email_status {
EmailTransferStatus::HeldBack { ref errors } => {
assert_eq!(errors[0].variant, error);
}
_ => unreachable!(),
},
}
});
}
}