use std::{
fmt,
io::{Write, stdout},
};
use anyhow::{Context, Result, bail};
use mail_parser::{Addr, Address, MessageParser};
use pimalaya_cli::printer::Printer;
use schemars::JsonSchema;
use serde::Serialize;
use crate::{
account::context::Account,
email::flag::{Flag, IanaFlag},
shared::client::EmailClient,
};
pub enum Outcome {
Stdout,
Saved {
id: Option<String>,
sent: bool,
queued: Option<i64>,
},
Sent {
queued: Option<i64>,
},
}
pub fn apply(
account: &Account,
client: &mut EmailClient,
raw: Vec<u8>,
flags: &[Flag],
save: Option<&str>,
send: bool,
) -> Result<Outcome> {
if !send && save.is_none() {
let mut out = stdout().lock();
out.write_all(&raw)?;
return Ok(Outcome::Stdout);
}
let mailbox = save.map(|name| account.resolve_mailbox(name));
let queued = match send {
true => {
let sent = mailbox.or_else(|| account.mailbox_alias.get("sent").map(String::as_str));
client.send_message(sent, raw.clone())?
}
false => None,
};
let saved_id = match mailbox {
Some(mailbox) if send => Some(
client
.add_message(mailbox, flags, raw)
.with_context(|| format!("Message sent, but saving a copy to {mailbox} failed"))?,
),
Some(mailbox) => Some(client.add_message(mailbox, flags, raw)?),
None => None,
};
Ok(match saved_id {
Some(id) => Outcome::Saved {
id,
sent: send,
queued,
},
None => Outcome::Sent { queued },
})
}
pub fn route(
printer: &mut impl Printer,
account: &Account,
client: &mut EmailClient,
raw: Vec<u8>,
save: Option<&str>,
send: bool,
) -> Result<()> {
if printer.is_json() && !send && save.is_none() {
return printer.out(MessageTemplate::parse(&raw)?);
}
let outcome = apply(
account,
client,
raw,
&[Flag::from_iana(IanaFlag::Seen)],
save,
send,
)?;
let (message, queue_id) = match outcome {
Outcome::Stdout => return Ok(()),
Outcome::Saved {
sent: true,
queued: Some(row),
..
} => (
"Message successfully saved and queued for sending",
Some(row),
),
Outcome::Saved { sent: true, .. } => ("Message successfully saved and sent", None),
Outcome::Saved { sent: false, .. } => ("Message successfully saved", None),
Outcome::Sent { queued: Some(row) } => {
("Message successfully queued for sending", Some(row))
}
Outcome::Sent { queued: None } => ("Message successfully sent", None),
};
printer.out(MessageRouteOutput {
message: message.to_owned(),
queue_id,
})
}
#[derive(Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageRouteOutput {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub queue_id: Option<i64>,
}
impl fmt::Display for MessageRouteOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Serialize, JsonSchema)]
pub struct MessageTemplate {
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
pub to: Vec<String>,
pub cc: Vec<String>,
pub bcc: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
pub body: String,
}
impl MessageTemplate {
fn parse(raw: &[u8]) -> Result<Self> {
let Some(message) = MessageParser::new().parse(raw) else {
bail!("Failed to parse composed message");
};
let mailboxes = |address: Option<&Address>| -> Vec<String> {
address
.map(|address| address.iter().map(mailbox).collect())
.unwrap_or_default()
};
Ok(Self {
from: message.from().and_then(Address::first).map(mailbox),
to: mailboxes(message.to()),
cc: mailboxes(message.cc()),
bcc: mailboxes(message.bcc()),
subject: message.subject().map(str::to_owned),
body: message.body_text(0).unwrap_or_default().into_owned(),
})
}
}
fn mailbox(addr: &Addr) -> String {
let email = addr.address.as_deref().unwrap_or_default();
let Some(name) = addr.name.as_deref().filter(|name| !name.is_empty()) else {
return email.to_owned();
};
if name.contains(|c| "()<>[]:;@\\,.\"".contains(c)) {
let escaped = name.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\" <{email}>")
} else {
format!("{name} <{email}>")
}
}
impl fmt::Display for MessageTemplate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(from) = &self.from {
writeln!(f, "From: {from}")?;
}
for (name, list) in [("To", &self.to), ("Cc", &self.cc), ("Bcc", &self.bcc)] {
if !list.is_empty() {
writeln!(f, "{name}: {}", list.join(", "))?;
}
}
if let Some(subject) = &self.subject {
writeln!(f, "Subject: {subject}")?;
}
writeln!(f)?;
write!(f, "{}", self.body)
}
}
#[cfg(test)]
mod tests {
use super::MessageTemplate;
#[test]
fn template_decodes_headers_and_body_and_quotes_names() {
let raw = concat!(
"From: =?utf-8?q?Cl=C3=A9ment?= <c@example.org>\r\n",
"To: a@example.org, Bob <b@example.org>, \"Doe, J.\" <j@example.org>\r\n",
"Subject: =?utf-8?q?R=C3=A9union?=\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=C3=A0 demain\r\n",
);
let template = MessageTemplate::parse(raw.as_bytes()).unwrap();
assert_eq!(template.from.as_deref(), Some("Clément <c@example.org>"));
assert_eq!(
template.to,
[
"a@example.org",
"Bob <b@example.org>",
"\"Doe, J.\" <j@example.org>",
]
);
assert!(template.cc.is_empty());
assert_eq!(template.subject.as_deref(), Some("Réunion"));
assert_eq!(template.body, "à demain\r\n");
}
}