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)
|
||||
}
|
||||
|
||||
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:") {
|
||||
|
|
|
@ -183,19 +183,12 @@ 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())
|
||||
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()
|
||||
.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(
|
||||
|
@ -213,6 +206,7 @@ impl Composer {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut op = account.operation(parent_message.hash());
|
||||
let parent_bytes = op.as_bytes();
|
||||
|
|
|
@ -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());
|
||||
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())
|
||||
|
|
|
@ -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<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())
|
||||
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 */
|
||||
|
@ -100,17 +60,95 @@ pub fn detect<'a>(envelope: &'a Envelope) -> Option<ListActions<'a>> {
|
|||
});
|
||||
|
||||
vec.into_iter()
|
||||
.map(|elem| UnsubscribeOption::from(elem))
|
||||
.collect::<StackVec<UnsubscribeOption<'a>>>()
|
||||
.map(|elem| ListAction::from(elem))
|
||||
.collect::<StackVec<ListAction<'a>>>()
|
||||
})
|
||||
.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 */
|
||||
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<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