meli/melib/src/email/compose.rs

466 lines
15 KiB
Rust
Raw Normal View History

2018-08-29 14:11:59 +03:00
use super::*;
2019-05-26 15:54:45 +03:00
use crate::backends::BackendOp;
use crate::email::attachments::AttachmentBuilder;
2018-08-29 14:11:59 +03:00
use chrono::{DateTime, Local};
use data_encoding::BASE64_MIME;
2019-08-01 12:14:45 +03:00
use std::ffi::OsStr;
use std::io::Read;
use std::path::Path;
use std::str;
2018-08-29 14:11:59 +03:00
pub mod mime;
pub mod random;
2019-02-15 19:21:58 +02:00
2019-03-03 22:11:15 +02:00
//use self::mime::*;
2018-08-29 14:11:59 +03:00
use super::parser;
2019-04-04 14:21:52 +03:00
use fnv::FnvHashMap;
2018-08-29 14:11:59 +03:00
2019-04-06 00:43:50 +03:00
#[derive(Debug, PartialEq, Clone)]
2018-08-29 14:11:59 +03:00
pub struct Draft {
2019-09-28 10:46:49 +03:00
pub headers: FnvHashMap<String, String>,
pub header_order: Vec<String>,
pub body: String,
2018-08-29 14:11:59 +03:00
2019-09-28 10:46:49 +03:00
pub attachments: Vec<AttachmentBuilder>,
2018-08-29 14:11:59 +03:00
}
impl Default for Draft {
fn default() -> Self {
let mut headers = FnvHashMap::with_capacity_and_hasher(8, Default::default());
2019-02-15 19:21:58 +02:00
let mut header_order = Vec::with_capacity(8);
headers.insert("From".into(), "".into());
headers.insert("To".into(), "".into());
headers.insert("Cc".into(), "".into());
headers.insert("Bcc".into(), "".into());
2018-08-29 14:11:59 +03:00
let now: DateTime<Local> = Local::now();
headers.insert("Date".into(), now.to_rfc2822());
headers.insert("Subject".into(), "".into());
headers.insert(
"User-Agent".into(),
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
);
2019-02-15 19:21:58 +02:00
header_order.push("Date".into());
2019-03-02 08:11:38 +02:00
header_order.push("From".into());
header_order.push("To".into());
header_order.push("Cc".into());
header_order.push("Bcc".into());
2019-02-15 19:21:58 +02:00
header_order.push("Subject".into());
header_order.push("User-Agent".into());
2018-08-29 14:11:59 +03:00
Draft {
headers,
2019-02-15 19:21:58 +02:00
header_order,
2018-08-29 14:11:59 +03:00
body: String::new(),
attachments: Vec::new(),
}
}
}
impl str::FromStr for Draft {
type Err = MeliError;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
2019-02-15 19:21:58 +02:00
return Err(MeliError::new("Empty input in Draft::from_str"));
}
let (headers, _) = parser::mail(s.as_bytes()).to_full_result()?;
let mut ret = Draft::default();
for (k, v) in headers {
if ignore_header(k) {
continue;
}
2019-03-14 12:19:25 +02:00
if ret
.headers
.insert(
String::from_utf8(k.to_vec())?,
String::from_utf8(v.to_vec())?,
)
.is_none()
{
2019-02-15 19:21:58 +02:00
ret.header_order.push(String::from_utf8(k.to_vec())?);
}
}
2019-04-14 17:26:33 +03:00
if ret.headers.contains_key("From") && !ret.headers.contains_key("Message-ID") {
if let super::parser::IResult::Done(_, addr) =
super::parser::mailbox(ret.headers["From"].as_bytes())
{
if let Some(fqdn) = addr.get_fqdn() {
if ret
.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn))
.is_none()
{
let pos = ret
.header_order
.iter()
.position(|h| h == "Subject")
.unwrap();
ret.header_order.insert(pos, "Message-ID".into());
}
}
}
}
let body = Envelope::new(0).body_bytes(s.as_bytes());
ret.body = String::from_utf8(decode(&body, None))?;
Ok(ret)
}
}
2018-08-29 14:11:59 +03:00
impl Draft {
pub fn edit(envelope: &Envelope, mut op: Box<dyn BackendOp>) -> Result<Self> {
let mut ret = Draft::default();
//TODO: Inform user if error
{
let bytes = op.as_bytes().unwrap_or(&[]);
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
if ignore_header(k.as_bytes()) {
continue;
}
if ret.headers.insert(k.into(), v.into()).is_none() {
ret.header_order.push(k.into());
}
}
}
ret.body = envelope.body(op)?.text();
Ok(ret)
}
pub fn set_header(&mut self, header: &str, value: String) {
if self.headers.insert(header.to_string(), value).is_none() {
self.header_order.push(header.to_string());
}
}
pub fn new_reply(envelope: &Envelope, bytes: &[u8]) -> Self {
2018-09-04 15:00:23 +03:00
let mut ret = Draft::default();
ret.headers_mut().insert(
"References".into(),
format!(
"{} {}",
envelope
.references()
.iter()
.fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(&x.to_string());
acc
}),
envelope.message_id_display()
2018-09-04 15:00:23 +03:00
),
);
2019-02-15 19:21:58 +02:00
ret.header_order.push("References".into());
2018-09-04 15:00:23 +03:00
ret.headers_mut()
.insert("In-Reply-To".into(), envelope.message_id_display().into());
2019-02-15 19:21:58 +02:00
ret.header_order.push("In-Reply-To".into());
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.headers_mut().insert("To".into(), reply_to.to_string());
} else {
ret.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
}
2018-09-04 15:00:23 +03:00
ret.headers_mut()
.insert("Cc".into(), envelope.field_cc_to_string());
let body = envelope.body_bytes(bytes);
ret.body = {
let reply_body_bytes = decode_rec(&body, None);
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
let lines: Vec<&str> = reply_body.lines().collect();
let mut ret = String::with_capacity(reply_body.len() + lines.len());
for l in lines {
ret.push('>');
ret.push(' ');
2018-09-04 15:00:23 +03:00
ret.push_str(l.trim());
ret.push('\n');
}
ret.pop();
ret
};
ret
}
pub fn headers_mut(&mut self) -> &mut FnvHashMap<String, String> {
&mut self.headers
}
2018-09-04 15:00:23 +03:00
pub fn headers(&self) -> &FnvHashMap<String, String> {
&self.headers
}
pub fn attachments(&self) -> &Vec<AttachmentBuilder> {
&self.attachments
}
pub fn attachments_mut(&mut self) -> &mut Vec<AttachmentBuilder> {
&mut self.attachments
}
pub fn body(&self) -> &str {
&self.body
}
2018-08-29 14:11:59 +03:00
pub fn set_body(&mut self, s: String) {
self.body = s;
}
pub fn to_string(&self) -> Result<String> {
let mut ret = String::new();
2019-02-15 19:21:58 +02:00
for k in &self.header_order {
let v = &self.headers[k];
ret.extend(format!("{}: {}\n", k, v).chars());
}
2019-02-15 19:21:58 +02:00
ret.push('\n');
ret.push_str(&self.body);
Ok(ret)
}
pub fn finalise(mut self) -> Result<String> {
2019-02-15 19:21:58 +02:00
let mut ret = String::new();
if self.headers.contains_key("From") && !self.headers.contains_key("Message-ID") {
if let super::parser::IResult::Done(_, addr) =
super::parser::mailbox(self.headers["From"].as_bytes())
{
if let Some(fqdn) = addr.get_fqdn() {
if self
.headers
.insert("Message-ID".into(), random::gen_message_id(&fqdn))
.is_none()
{
let pos = self
.header_order
.iter()
.position(|h| h == "Subject")
.unwrap();
self.header_order.insert(pos, "Message-ID".into());
}
}
}
}
2019-02-15 19:21:58 +02:00
for k in &self.header_order {
let v = &self.headers[k];
if v.is_ascii() {
ret.extend(format!("{}: {}\n", k, v).chars());
} else {
ret.extend(format!("{}: {}\n", k, mime::encode_header(v)).chars());
}
2018-08-29 14:11:59 +03:00
}
2019-04-14 17:26:33 +03:00
ret.push_str("MIME-Version: 1.0\n");
2018-08-29 14:11:59 +03:00
2019-09-28 10:46:49 +03:00
if self.attachments.is_empty() {
let content_type: ContentType = Default::default();
let content_transfer_encoding: ContentTransferEncoding = ContentTransferEncoding::_8Bit;
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", content_type).chars());
ret.extend(
format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(),
);
ret.push('\n');
ret.push_str(&self.body);
} else if self.attachments.len() == 1 && self.body.is_empty() {
let attachment: Attachment = self.attachments.remove(0).into();
ret.extend(attachment.into_raw().chars());
} else {
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
let mut body_attachment = AttachmentBuilder::default();
body_attachment.set_raw(self.body.as_bytes().to_vec());
parts.push(body_attachment);
parts.extend(attachments.into_iter());
build_multipart(&mut ret, MultipartType::Mixed, parts);
2018-08-29 14:11:59 +03:00
}
Ok(ret)
}
}
fn ignore_header(header: &[u8]) -> bool {
match header {
b"From" => false,
b"To" => false,
b"Date" => false,
b"Message-ID" => false,
b"User-Agent" => false,
b"Subject" => false,
b"Reply-to" => false,
b"Cc" => false,
b"Bcc" => false,
b"In-Reply-To" => false,
b"References" => false,
b"MIME-Version" => true,
2018-08-29 14:11:59 +03:00
h if h.starts_with(b"X-") => false,
_ => true,
}
}
fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec<AttachmentBuilder>) {
use ContentType::*;
let boundary = ContentType::make_boundary(&parts);
ret.extend(
format!(
"Content-Type: {}; charset=\"utf-8\"; boundary=\"{}\"\n",
kind, boundary
)
.chars(),
);
ret.push('\n');
/* rfc1341 */
ret.extend("This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.\n".chars());
for sub in parts {
ret.push_str("--");
ret.extend(boundary.chars());
ret.push('\n');
match sub.content_type {
ContentType::Text {
kind: crate::email::attachment_types::Text::Plain,
charset: Charset::UTF8,
} => {
ret.push('\n');
ret.push_str(&String::from_utf8_lossy(sub.raw()));
ret.push('\n');
}
Text {
ref kind,
charset: _,
} => {
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", kind).chars());
ret.push('\n');
ret.push_str(&String::from_utf8_lossy(sub.raw()));
ret.push('\n');
}
Multipart {
boundary: _boundary,
kind,
parts: subparts,
} => {
build_multipart(
ret,
kind,
subparts
.into_iter()
.map(|s| s.into())
.collect::<Vec<AttachmentBuilder>>(),
);
ret.push('\n');
}
MessageRfc822 | PGPSignature => {
ret.extend(format!("Content-Type: {}; charset=\"utf-8\"\n", kind).chars());
ret.push('\n');
ret.push_str(&String::from_utf8_lossy(sub.raw()));
ret.push('\n');
}
_ => {
let content_transfer_encoding: ContentTransferEncoding =
ContentTransferEncoding::Base64;
if let Some(name) = sub.content_type().name() {
ret.extend(
format!(
"Content-Type: {}; name=\"{}\"; charset=\"utf-8\"\n",
sub.content_type(),
name
)
.chars(),
);
} else {
ret.extend(
format!("Content-Type: {}; charset=\"utf-8\"\n", sub.content_type())
.chars(),
);
}
ret.extend(
format!("Content-Transfer-Encoding: {}\n", content_transfer_encoding).chars(),
);
ret.push('\n');
ret.push_str(&BASE64_MIME.encode(sub.raw()).trim());
ret.push('\n');
}
}
}
ret.push_str("--");
ret.extend(boundary.chars());
ret.push_str("--\n");
}
2018-08-29 14:11:59 +03:00
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::str::FromStr;
2018-08-29 14:11:59 +03:00
#[test]
fn test_new() {
let mut default = Draft::default();
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
default.set_body("αδφαφσαφασ".to_string());
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
default.set_body("ascii only".to_string());
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
}
#[test]
fn test_attachments() {
return;
let mut default = Draft::default();
default.set_body("αδφαφσαφασ".to_string());
let mut file = std::fs::File::open("file path").unwrap();
let mut contents = Vec::new();
file.read_to_end(&mut contents).unwrap();
let mut attachment = AttachmentBuilder::new(b"");
attachment
.set_raw(contents)
.set_content_type(ContentType::Other {
name: Some("images.jpeg".to_string()),
tag: b"image/jpeg".to_vec(),
})
.set_content_transfer_encoding(ContentTransferEncoding::Base64);
default.attachments_mut().push(attachment);
println!("{}", default.finalise().unwrap());
}
2018-08-29 14:11:59 +03:00
}
2019-08-01 12:14:45 +03:00
/// Reads file from given path, and returns an 'application/octet-stream' AttachmentBuilder object
pub fn attachment_from_file<I>(path: &I) -> Result<AttachmentBuilder>
where
I: AsRef<OsStr>,
{
let path: &Path = Path::new(path);
if !path.is_file() {
return Err(MeliError::new(format!("{} is not a file", path.display())));
}
let mut file = std::fs::File::open(path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let mut attachment = AttachmentBuilder::new(b"");
attachment
.set_raw(contents)
.set_content_type(ContentType::Other {
name: path.file_name().map(|s| s.to_string_lossy().into()),
tag: b"application/octet-stream".to_vec(),
});
Ok(attachment)
}