Browse Source

ui: ListActions changes

- Parse List-Post value like List-Unsubscribe: comma separated angle bracket limited list of <mailto:> or <url> values
- Check if List-Archive value is angle bracket delimited
jmap
Manos Pitsidianakis 2 years ago
parent
commit
449a24d075
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 2
      melib/src/email/parser.rs
  2. 46
      ui/src/components/mail/compose.rs
  3. 34
      ui/src/components/mail/view.rs
  4. 134
      ui/src/components/mail/view/list_management.rs

2
melib/src/email/parser.rs

@ -921,7 +921,7 @@ pub fn phrase(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
IResult::Done(&input[ptr..], acc)
}
named!(pub angle_bracket_delimeted_list<Vec<&[u8]>>, separated_nonempty_list!(complete!(is_a!(",")), ws!(complete!(message_id))));
named!(pub angle_bracket_delimeted_list<Vec<&[u8]>>, 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:") {

46
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:*****@*****>
* - 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,
));
}
}
}
}

34
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())

134
ui/src/components/mail/view/list_management.rs

@ -24,39 +24,62 @@ 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)
}
}
}
impl<'a> ListAction<'a> {
pub fn parse_options_list(input: &'a [u8]) -> Option<StackVec<ListAction<'a>>> {
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 */
vec.sort_unstable_by(|a, b| {
match (a.starts_with(b"mailto:"), b.starts_with(b"mailto:")) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
});
vec.into_iter()
.map(|elem| ListAction::from(elem))
.collect::<StackVec<ListAction<'a>>>()
})
.to_full_result()
.ok()
}
}
/* Required for StackVec's place holder elements, never actually used */
impl<'a> Default for UnsubscribeOption<'a> {
impl<'a> Default for ListAction<'a> {
fn default() -> Self {
UnsubscribeOption::Email(b"")
ListAction::Email(b"")
}
}
impl<'a> Clone for UnsubscribeOption<'a> {
impl<'a> Clone for ListAction<'a> {
fn clone(&self) -> Self {
match self {
UnsubscribeOption::Url(a) => UnsubscribeOption::Url(<&[u8]>::clone(a)),
UnsubscribeOption::Email(a) => UnsubscribeOption::Email(<&[u8]>::clone(a)),
ListAction::Url(a) => ListAction::Url(<&[u8]>::clone(a)),
ListAction::Email(a) => ListAction::Email(<&[u8]>::clone(a)),
}
}
}
@ -65,52 +88,67 @@ impl<'a> Clone for UnsubscribeOption<'a> {
pub struct ListActions<'a> {
pub id: Option<&'a str>,
pub archive: Option<&'a str>,
pub post: Option<&'a str>,
pub unsubscribe: Option<StackVec<UnsubscribeOption<'a>>>,
pub post: Option<StackVec<ListAction<'a>>>,
pub unsubscribe: Option<StackVec<ListAction<'a>>>,
}
pub fn detect<'a>(envelope: &'a Envelope) -> Option<ListActions<'a>> {
let mut ret = ListActions::default();
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)
}
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);
}
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
})
}
if let Some(archive) = envelope.other_headers().get("List-Archive") {
ret.archive = Some(archive);
}
impl<'a> ListActions<'a> {
pub fn detect(envelope: &'a Envelope) -> Option<ListActions<'a>> {
let mut ret = ListActions::default();
if let Some(post) = envelope.other_headers().get("List-Post") {
ret.post = Some(post);
}
ret.id = list_id_header(envelope);
if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") {
ret.unsubscribe = parser::angle_bracket_delimeted_list(unsubscribe.as_bytes())
.map(|mut vec| {
/* Prefer email options first, since this _is_ a mail client after all and it's
* more automated */
vec.sort_unstable_by(|a, b| {
match (a.starts_with(b"mailto:"), b.starts_with(b"mailto:")) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
});
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);
}
}
vec.into_iter()
.map(|elem| UnsubscribeOption::from(elem))
.collect::<StackVec<UnsubscribeOption<'a>>>()
})
.to_full_result()
.ok();
}
if let Some(post) = envelope.other_headers().get("List-Post") {
ret.post = ListAction::parse_options_list(post.as_bytes());
}
if ret.id.is_none() && ret.archive.is_none() && ret.post.is_none() && ret.unsubscribe.is_none()
{
None
} else {
Some(ret)
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)
}
}
}
Loading…
Cancel
Save