From 449a24d075ebeb647beb6424542697046f5ee3e2 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 18 Nov 2019 14:55:48 +0200 Subject: [PATCH] ui: ListActions changes - Parse List-Post value like List-Unsubscribe: comma separated angle bracket limited list of or values - Check if List-Archive value is angle bracket delimited --- melib/src/email/parser.rs | 2 +- ui/src/components/mail/compose.rs | 46 +++--- ui/src/components/mail/view.rs | 34 ++-- .../components/mail/view/list_management.rs | 152 +++++++++++------- 4 files changed, 140 insertions(+), 94 deletions(-) diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index ef03353e..67c7308f 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -921,7 +921,7 @@ 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)))); +named!(pub angle_bracket_delimeted_list>, separated_nonempty_list!(complete!(is_a!(",")), ws!(complete!(complete!(delimited!(tag!("<"), take_until1!(">"), tag!(">"))))))); pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> { if !input.starts_with(b"mailto:") { diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 4619b07e..367d449d 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -183,33 +183,27 @@ impl Composer { let parent_message = account.collection.get_env(p.message().unwrap()); /* If message is from a mailing list and we detect a List-Post header, ask user if they * want to reply to the mailing list or the submitter of the message */ - if let Some(actions) = list_management::detect(&parent_message) { + if let Some(actions) = list_management::ListActions::detect(&parent_message) { if let Some(post) = actions.post { - /* Try to parse header value in this order - * - - * - mailto:******@***** - * - <***@****> - */ - if let Ok(list_address) = melib::email::parser::message_id(post.as_bytes()) - .to_full_result() - .and_then(|b| melib::email::parser::mailto(b).to_full_result()) - .or(melib::email::parser::mailto(post.as_bytes()).to_full_result()) - .map(|m| m.address) - .or(melib::email::parser::address(post.as_bytes()).to_full_result()) - { - let list_address_string = list_address.to_string(); - ret.mode = ViewMode::SelectRecipients(Selector::new( - "select recipients", - vec![ - ( - parent_message.from()[0].clone(), - parent_message.field_from_to_string(), - ), - (list_address, list_address_string), - ], - false, - context, - )); + if let list_management::ListAction::Email(list_post_addr) = post[0] { + if let Ok(list_address) = melib::email::parser::mailto(list_post_addr) + .to_full_result() + .map(|m| m.address) + { + let list_address_string = list_address.to_string(); + ret.mode = ViewMode::SelectRecipients(Selector::new( + "select recipients", + vec![ + ( + parent_message.from()[0].clone(), + parent_message.field_from_to_string(), + ), + (list_address, list_address_string), + ], + false, + context, + )); + } } } } diff --git a/ui/src/components/mail/view.rs b/ui/src/components/mail/view.rs index 5485e255..8ea7bca9 100644 --- a/ui/src/components/mail/view.rs +++ b/ui/src/components/mail/view.rs @@ -459,7 +459,7 @@ impl Component for MailView { ref archive, ref post, ref unsubscribe, - }) = list_management::detect(&envelope) + }) = list_management::ListActions::detect(&envelope) { let mut x = get_x(upper_left); y += 1; @@ -1170,16 +1170,30 @@ impl Component for MailView { return true; } let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); - if let Some(actions) = list_management::detect(&envelope) { + if let Some(actions) = list_management::ListActions::detect(&envelope) { match e { MailingListAction::ListPost if actions.post.is_some() => { /* open composer */ - let mut draft = Draft::default(); - draft.set_header("To", actions.post.unwrap().to_string()); - context.replies.push_back(UIEvent::Action(Tab(NewDraft( - self.coordinates.0, - Some(draft), - )))); + let mut failure = true; + if let list_management::ListAction::Email(list_post_addr) = + actions.post.unwrap()[0] + { + if let Ok(mailto) = Mailto::try_from(list_post_addr) { + let draft: Draft = mailto.into(); + context.replies.push_back(UIEvent::Action(Tab(NewDraft( + self.coordinates.0, + Some(draft), + )))); + failure = false; + } + } + if failure { + context.replies.push_back(UIEvent::StatusEvent( + StatusEvent::DisplayMessage(String::from( + "Couldn't parse List-Post header value", + )), + )); + } return true; } MailingListAction::ListUnsubscribe if actions.unsubscribe.is_some() => { @@ -1188,7 +1202,7 @@ impl Component for MailView { for option in unsubscribe { /* TODO: Ask for confirmation before proceding with an action */ match option { - list_management::UnsubscribeOption::Email(email) => { + list_management::ListAction::Email(email) => { if let Ok(mailto) = Mailto::try_from(email) { let mut draft: Draft = mailto.into(); draft.headers_mut().insert( @@ -1219,7 +1233,7 @@ impl Component for MailView { } } } - list_management::UnsubscribeOption::Url(url) => { + list_management::ListAction::Url(url) => { if let Err(e) = Command::new("xdg-open") .arg(String::from_utf8_lossy(url).into_owned()) .stdin(Stdio::piped()) diff --git a/ui/src/components/mail/view/list_management.rs b/ui/src/components/mail/view/list_management.rs index 74826347..9d525cf1 100644 --- a/ui/src/components/mail/view/list_management.rs +++ b/ui/src/components/mail/view/list_management.rs @@ -24,70 +24,30 @@ use melib::StackVec; use std::convert::From; #[derive(Debug, Copy)] -pub enum UnsubscribeOption<'a> { +pub enum ListAction<'a> { Url(&'a [u8]), Email(&'a [u8]), } -impl<'a> From<&'a [u8]> for UnsubscribeOption<'a> { +impl<'a> From<&'a [u8]> for ListAction<'a> { fn from(value: &'a [u8]) -> Self { if value.starts_with(b"mailto:") { /* if branch looks if value looks like a mailto url but doesn't validate it. * parser::mailto() will handle this if user tries to unsubscribe. */ - UnsubscribeOption::Email(value) + ListAction::Email(value) } else { /* Otherwise treat it as url. There's no foolproof way to check if this is valid, so * postpone it until we try an HTTP request. */ - UnsubscribeOption::Url(value) + ListAction::Url(value) } } } -/* Required for StackVec's place holder elements, never actually used */ -impl<'a> Default for UnsubscribeOption<'a> { - fn default() -> Self { - UnsubscribeOption::Email(b"") - } -} - -impl<'a> Clone for UnsubscribeOption<'a> { - fn clone(&self) -> Self { - match self { - UnsubscribeOption::Url(a) => UnsubscribeOption::Url(<&[u8]>::clone(a)), - UnsubscribeOption::Email(a) => UnsubscribeOption::Email(<&[u8]>::clone(a)), - } - } -} - -#[derive(Default, Debug)] -pub struct ListActions<'a> { - pub id: Option<&'a str>, - pub archive: Option<&'a str>, - pub post: Option<&'a str>, - pub unsubscribe: Option>>, -} - -pub fn detect<'a>(envelope: &'a Envelope) -> Option> { - let mut ret = ListActions::default(); - - if let Some(id) = envelope.other_headers().get("List-ID") { - ret.id = Some(id); - } else if let Some(id) = envelope.other_headers().get("List-Id") { - ret.id = Some(id); - } - - if let Some(archive) = envelope.other_headers().get("List-Archive") { - ret.archive = Some(archive); - } - - if let Some(post) = envelope.other_headers().get("List-Post") { - ret.post = Some(post); - } - - if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") { - ret.unsubscribe = parser::angle_bracket_delimeted_list(unsubscribe.as_bytes()) +impl<'a> ListAction<'a> { + pub fn parse_options_list(input: &'a [u8]) -> Option>> { + parser::angle_bracket_delimeted_list(input) .map(|mut vec| { /* Prefer email options first, since this _is_ a mail client after all and it's * more automated */ @@ -100,17 +60,95 @@ pub fn detect<'a>(envelope: &'a Envelope) -> Option> { }); vec.into_iter() - .map(|elem| UnsubscribeOption::from(elem)) - .collect::>>() + .map(|elem| ListAction::from(elem)) + .collect::>>() }) .to_full_result() - .ok(); - } - - if ret.id.is_none() && ret.archive.is_none() && ret.post.is_none() && ret.unsubscribe.is_none() - { - None - } else { - Some(ret) + .ok() + } +} + +/* Required for StackVec's place holder elements, never actually used */ +impl<'a> Default for ListAction<'a> { + fn default() -> Self { + ListAction::Email(b"") + } +} + +impl<'a> Clone for ListAction<'a> { + fn clone(&self) -> Self { + match self { + ListAction::Url(a) => ListAction::Url(<&[u8]>::clone(a)), + ListAction::Email(a) => ListAction::Email(<&[u8]>::clone(a)), + } + } +} + +#[derive(Default, Debug)] +pub struct ListActions<'a> { + pub id: Option<&'a str>, + pub archive: Option<&'a str>, + pub post: Option>>, + pub unsubscribe: Option>>, +} + +pub fn list_id_header<'a>(envelope: &'a Envelope) -> Option<&'a str> { + envelope + .other_headers() + .get("List-ID") + .or(envelope.other_headers().get("List-Id")) + .map(String::as_str) +} + +pub fn list_id<'a>(header: Option<&'a str>) -> Option<&'a str> { + /* rfc2919 https://tools.ietf.org/html/rfc2919 */ + /* list-id-header = "List-ID:" [phrase] "<" list-id ">" CRLF */ + header.and_then(|v| { + if let Some(l) = v.rfind("<") { + if let Some(r) = v.rfind(">") { + if l < r { + return Some(&v[l + 1..r]); + } + } + } + None + }) +} + +impl<'a> ListActions<'a> { + pub fn detect(envelope: &'a Envelope) -> Option> { + let mut ret = ListActions::default(); + + ret.id = list_id_header(envelope); + + if let Some(archive) = envelope.other_headers().get("List-Archive") { + if archive.starts_with("<") { + if let Some(pos) = archive.find(">") { + ret.archive = Some(&archive[1..pos]); + } else { + ret.archive = Some(archive); + } + } else { + ret.archive = Some(archive); + } + } + + if let Some(post) = envelope.other_headers().get("List-Post") { + ret.post = ListAction::parse_options_list(post.as_bytes()); + } + + if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") { + ret.unsubscribe = ListAction::parse_options_list(unsubscribe.as_bytes()); + } + + if ret.id.is_none() + && ret.archive.is_none() + && ret.post.is_none() + && ret.unsubscribe.is_none() + { + None + } else { + Some(ret) + } } }