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 delimitedjmap
parent
590619de0e
commit
449a24d075
|
@ -921,7 +921,7 @@ pub fn phrase(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
||||||
IResult::Done(&input[ptr..], acc)
|
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> {
|
pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> {
|
||||||
if !input.starts_with(b"mailto:") {
|
if !input.starts_with(b"mailto:") {
|
||||||
|
|
|
@ -183,33 +183,27 @@ impl Composer {
|
||||||
let parent_message = account.collection.get_env(p.message().unwrap());
|
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
|
/* 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 */
|
* 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 {
|
if let Some(post) = actions.post {
|
||||||
/* Try to parse header value in this order
|
if let list_management::ListAction::Email(list_post_addr) = post[0] {
|
||||||
* - <mailto:*****@*****>
|
if let Ok(list_address) = melib::email::parser::mailto(list_post_addr)
|
||||||
* - mailto:******@*****
|
.to_full_result()
|
||||||
* - <***@****>
|
.map(|m| m.address)
|
||||||
*/
|
{
|
||||||
if let Ok(list_address) = melib::email::parser::message_id(post.as_bytes())
|
let list_address_string = list_address.to_string();
|
||||||
.to_full_result()
|
ret.mode = ViewMode::SelectRecipients(Selector::new(
|
||||||
.and_then(|b| melib::email::parser::mailto(b).to_full_result())
|
"select recipients",
|
||||||
.or(melib::email::parser::mailto(post.as_bytes()).to_full_result())
|
vec![
|
||||||
.map(|m| m.address)
|
(
|
||||||
.or(melib::email::parser::address(post.as_bytes()).to_full_result())
|
parent_message.from()[0].clone(),
|
||||||
{
|
parent_message.field_from_to_string(),
|
||||||
let list_address_string = list_address.to_string();
|
),
|
||||||
ret.mode = ViewMode::SelectRecipients(Selector::new(
|
(list_address, list_address_string),
|
||||||
"select recipients",
|
],
|
||||||
vec![
|
false,
|
||||||
(
|
context,
|
||||||
parent_message.from()[0].clone(),
|
));
|
||||||
parent_message.field_from_to_string(),
|
}
|
||||||
),
|
|
||||||
(list_address, list_address_string),
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
context,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -459,7 +459,7 @@ impl Component for MailView {
|
||||||
ref archive,
|
ref archive,
|
||||||
ref post,
|
ref post,
|
||||||
ref unsubscribe,
|
ref unsubscribe,
|
||||||
}) = list_management::detect(&envelope)
|
}) = list_management::ListActions::detect(&envelope)
|
||||||
{
|
{
|
||||||
let mut x = get_x(upper_left);
|
let mut x = get_x(upper_left);
|
||||||
y += 1;
|
y += 1;
|
||||||
|
@ -1170,16 +1170,30 @@ impl Component for MailView {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
|
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 {
|
match e {
|
||||||
MailingListAction::ListPost if actions.post.is_some() => {
|
MailingListAction::ListPost if actions.post.is_some() => {
|
||||||
/* open composer */
|
/* open composer */
|
||||||
let mut draft = Draft::default();
|
let mut failure = true;
|
||||||
draft.set_header("To", actions.post.unwrap().to_string());
|
if let list_management::ListAction::Email(list_post_addr) =
|
||||||
context.replies.push_back(UIEvent::Action(Tab(NewDraft(
|
actions.post.unwrap()[0]
|
||||||
self.coordinates.0,
|
{
|
||||||
Some(draft),
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
MailingListAction::ListUnsubscribe if actions.unsubscribe.is_some() => {
|
MailingListAction::ListUnsubscribe if actions.unsubscribe.is_some() => {
|
||||||
|
@ -1188,7 +1202,7 @@ impl Component for MailView {
|
||||||
for option in unsubscribe {
|
for option in unsubscribe {
|
||||||
/* TODO: Ask for confirmation before proceding with an action */
|
/* TODO: Ask for confirmation before proceding with an action */
|
||||||
match option {
|
match option {
|
||||||
list_management::UnsubscribeOption::Email(email) => {
|
list_management::ListAction::Email(email) => {
|
||||||
if let Ok(mailto) = Mailto::try_from(email) {
|
if let Ok(mailto) = Mailto::try_from(email) {
|
||||||
let mut draft: Draft = mailto.into();
|
let mut draft: Draft = mailto.into();
|
||||||
draft.headers_mut().insert(
|
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")
|
if let Err(e) = Command::new("xdg-open")
|
||||||
.arg(String::from_utf8_lossy(url).into_owned())
|
.arg(String::from_utf8_lossy(url).into_owned())
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
|
|
|
@ -24,70 +24,30 @@ use melib::StackVec;
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
|
|
||||||
#[derive(Debug, Copy)]
|
#[derive(Debug, Copy)]
|
||||||
pub enum UnsubscribeOption<'a> {
|
pub enum ListAction<'a> {
|
||||||
Url(&'a [u8]),
|
Url(&'a [u8]),
|
||||||
Email(&'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 {
|
fn from(value: &'a [u8]) -> Self {
|
||||||
if value.starts_with(b"mailto:") {
|
if value.starts_with(b"mailto:") {
|
||||||
/* if branch looks if value looks like a mailto url but doesn't validate it.
|
/* 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.
|
* parser::mailto() will handle this if user tries to unsubscribe.
|
||||||
*/
|
*/
|
||||||
UnsubscribeOption::Email(value)
|
ListAction::Email(value)
|
||||||
} else {
|
} else {
|
||||||
/* Otherwise treat it as url. There's no foolproof way to check if this is valid, so
|
/* 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.
|
* 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> ListAction<'a> {
|
||||||
impl<'a> Default for UnsubscribeOption<'a> {
|
pub fn parse_options_list(input: &'a [u8]) -> Option<StackVec<ListAction<'a>>> {
|
||||||
fn default() -> Self {
|
parser::angle_bracket_delimeted_list(input)
|
||||||
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<StackVec<UnsubscribeOption<'a>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn detect<'a>(envelope: &'a Envelope) -> Option<ListActions<'a>> {
|
|
||||||
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())
|
|
||||||
.map(|mut vec| {
|
.map(|mut vec| {
|
||||||
/* Prefer email options first, since this _is_ a mail client after all and it's
|
/* Prefer email options first, since this _is_ a mail client after all and it's
|
||||||
* more automated */
|
* more automated */
|
||||||
|
@ -100,17 +60,95 @@ pub fn detect<'a>(envelope: &'a Envelope) -> Option<ListActions<'a>> {
|
||||||
});
|
});
|
||||||
|
|
||||||
vec.into_iter()
|
vec.into_iter()
|
||||||
.map(|elem| UnsubscribeOption::from(elem))
|
.map(|elem| ListAction::from(elem))
|
||||||
.collect::<StackVec<UnsubscribeOption<'a>>>()
|
.collect::<StackVec<ListAction<'a>>>()
|
||||||
})
|
})
|
||||||
.to_full_result()
|
.to_full_result()
|
||||||
.ok();
|
.ok()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ret.id.is_none() && ret.archive.is_none() && ret.post.is_none() && ret.unsubscribe.is_none()
|
|
||||||
{
|
/* Required for StackVec's place holder elements, never actually used */
|
||||||
None
|
impl<'a> Default for ListAction<'a> {
|
||||||
} else {
|
fn default() -> Self {
|
||||||
Some(ret)
|
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<StackVec<ListAction<'a>>>,
|
||||||
|
pub unsubscribe: Option<StackVec<ListAction<'a>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ListActions<'a>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue