diff --git a/melib/src/email.rs b/melib/src/email.rs index b5eb5800a..9c3c68b35 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -26,6 +26,8 @@ use fnv::FnvHashMap; mod compose; pub use self::compose::*; +mod mailto; +pub use mailto::*; mod attachment_types; pub mod attachments; pub use crate::attachments::*; diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index fdfb60994..af785cc26 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -127,6 +127,11 @@ impl Draft { 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 { let mut ret = Draft::default(); ret.headers_mut().insert( diff --git a/melib/src/email/mailto.rs b/melib/src/email/mailto.rs new file mode 100644 index 000000000..890fbcc87 --- /dev/null +++ b/melib/src/email/mailto.rs @@ -0,0 +1,148 @@ +/* + * meli - parser module + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::*; +use std::convert::TryFrom; + +#[derive(Debug)] +pub struct Mailto { + pub address: Address, + pub subject: Option, + pub cc: Option, + pub bcc: Option, + pub body: Option, +} + +impl From for Draft { + fn from(val: Mailto) -> Self { + let mut ret = Draft::default(); + let Mailto { + address, + subject, + cc, + bcc, + body, + } = val; + ret.set_header("Subject", subject.unwrap_or(String::new())); + ret.set_header("Cc", cc.unwrap_or(String::new())); + ret.set_header("Bcc", bcc.unwrap_or(String::new())); + ret.set_body(body.unwrap_or(String::new())); + ret.set_header("To", address.to_string()); + debug!(ret) + } +} + +impl TryFrom<&[u8]> for Mailto { + type Error = String; + + fn try_from(value: &[u8]) -> std::result::Result { + let parse_res = super::parser::mailto(value).to_full_result(); + if parse_res.is_ok() { + Ok(parse_res.unwrap()) + } else { + debug!( + "parser::mailto returned error while parsing {}:\n{:?}", + String::from_utf8_lossy(value), + parse_res.as_ref().err().unwrap() + ); + Err(format!("{:?}", parse_res.err().unwrap())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mailto() { + let test_address = super::parser::address(b"info@example.com") + .to_full_result() + .unwrap(); + let mailto = Mailto::try_from(&b"mailto:info@example.com?subject=email%20subject"[0..]) + .expect("Could not parse mailto link."); + let Mailto { + ref address, + ref subject, + ref cc, + ref bcc, + ref body, + } = mailto; + + assert_eq!( + ( + address, + subject.as_ref().map(String::as_str), + cc.as_ref().map(String::as_str), + bcc.as_ref().map(String::as_str), + body.as_ref().map(String::as_str), + ), + (&test_address, Some("email%20subject"), None, None, None) + ); + let mailto = Mailto::try_from(&b"mailto:info@example.com?cc=8cc9@example.com"[0..]) + .expect("Could not parse mailto link."); + let Mailto { + ref address, + ref subject, + ref cc, + ref bcc, + ref body, + } = mailto; + assert_eq!( + ( + address, + subject.as_ref().map(String::as_str), + cc.as_ref().map(String::as_str), + bcc.as_ref().map(String::as_str), + body.as_ref().map(String::as_str), + ), + (&test_address, None, Some("8cc9@example.com"), None, None) + ); + let mailto = Mailto::try_from( + &b"mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast" + [0..], + ) + .expect("Could not parse mailto link."); + let Mailto { + ref address, + ref subject, + ref cc, + ref bcc, + ref body, + } = mailto; + assert_eq!( + ( + address, + subject.as_ref().map(String::as_str), + cc.as_ref().map(String::as_str), + bcc.as_ref().map(String::as_str), + body.as_ref().map(String::as_str), + ), + ( + &test_address, + None, + None, + Some("7bcc8@example.com"), + Some("line first\nbut not\nlast") + ) + ); + } +} diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index cba496449..4d2e376b2 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -530,7 +530,7 @@ fn group(input: &[u8]) -> IResult<&[u8], Address> { } } -named!(address
, ws!(alt_complete!(mailbox | group))); +named!(pub address
, ws!(alt_complete!(mailbox | group))); named!(pub rfc2822address_list>, ws!( separated_list!(is_a!(","), address))); @@ -788,6 +788,82 @@ pub fn phrase(input: &[u8]) -> IResult<&[u8], Vec> { IResult::Done(&input[ptr..], acc) } +named!(pub angle_bracket_delimeted_list>, separated_nonempty_list!(complete!(is_a!(",")), ws!(complete!(message_id)))); + +pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> { + if !input.starts_with(b"mailto:") { + return IResult::Error(error_code!(ErrorKind::Custom(43))); + } + + input = &input[b"mailto:".len()..]; + + let end = input.iter().position(|e| *e == b'?').unwrap_or(input.len()); + let address: Address; + + if let IResult::Done(rest, addr) = crate::email::parser::address(&input[..end]) { + address = addr; + input = if input[end..].is_empty() { + &input[end..] + } else { + &input[end + 1..] + }; + } else { + return IResult::Error(error_code!(ErrorKind::Custom(43))); + } + + let mut subject = None; + let mut cc = None; + let mut bcc = None; + let mut body = None; + while !input.is_empty() { + let tag = if let Some(tag_pos) = input.iter().position(|e| *e == b'=') { + let ret = &input[0..tag_pos]; + input = &input[tag_pos + 1..]; + ret + } else { + return IResult::Error(error_code!(ErrorKind::Custom(43))); + }; + + let value_end = input.iter().position(|e| *e == b'&').unwrap_or(input.len()); + + let value = String::from_utf8_lossy(&input[..value_end]).to_string(); + match tag { + b"subject" if subject.is_none() => { + subject = Some(value); + } + b"cc" if cc.is_none() => { + cc = Some(value); + } + b"bcc" if bcc.is_none() => { + bcc = Some(value); + } + b"body" if body.is_none() => { + /* FIXME: + * Parse escaped characters properly. + */ + body = Some(value.replace("%20", " ").replace("%0A", "\n")); + } + _ => { + return IResult::Error(error_code!(ErrorKind::Custom(43))); + } + } + if input[value_end..].is_empty() { + break; + } + input = &input[value_end + 1..]; + } + IResult::Done( + input, + Mailto { + address, + subject, + cc, + bcc, + body, + }, + ) +} + #[cfg(test)] mod tests {