diff --git a/docs/meli.1 b/docs/meli.1 index 97e5298ce..e248538af 100644 --- a/docs/meli.1 +++ b/docs/meli.1 @@ -351,7 +351,7 @@ To open a draft for further editing, select your draft in the mail listing and p \&. .Sh CONTACTS .Nm -supports two kinds of contact backends: +supports three kinds of contact backends: .sp .Bl -enum -compact -offset indent .It @@ -366,6 +366,11 @@ The path defined as .Ic vcard_folder can hold multiple vCards per file. They are loaded read only. +.It +a +.Xr mutt 1 +compatible alias file in the option +.Ic mutt_alias_file .El .sp See diff --git a/docs/meli.conf.5 b/docs/meli.conf.5 index 40d671927..090461289 100644 --- a/docs/meli.conf.5 +++ b/docs/meli.conf.5 @@ -176,6 +176,12 @@ Available options are 'none' and 'sqlite3' .Pq Em optional Folder that contains .vcf files. They are parsed and imported read-only. +.It Ic mutt_alias_file Ar String +.Pq Em optional +Path of +.Xr mutt 1 +compatible alias file in the option +They are parsed and imported read-only. .It Ic mailboxes Ar mailbox .Pq Em optional Configuration for each mailbox. diff --git a/melib/src/addressbook.rs b/melib/src/addressbook.rs index 276c0ae5e..42867d08a 100644 --- a/melib/src/addressbook.rs +++ b/melib/src/addressbook.rs @@ -22,7 +22,10 @@ #[cfg(feature = "vcard")] pub mod vcard; +pub mod mutt; + use crate::datetime::{self, UnixTimestamp}; +use crate::parsec::Parser; use std::collections::HashMap; use uuid::Uuid; @@ -97,30 +100,50 @@ impl AddressBook { } pub fn with_account(s: &crate::conf::AccountSettings) -> AddressBook { - #[cfg(not(feature = "vcard"))] - { - AddressBook::new(s.name.clone()) - } - #[cfg(feature = "vcard")] - { - let mut ret = AddressBook::new(s.name.clone()); - if let Some(vcard_path) = s.vcard_folder() { - match vcard::load_cards(std::path::Path::new(vcard_path)) { - Ok(cards) => { - for c in cards { - ret.add_card(c); - } - } - Err(err) => { - crate::log( - format!("Could not load vcards from {:?}: {}", vcard_path, err), - crate::WARN, - ); + let mut ret = AddressBook::new(s.name.clone()); + if let Some(mutt_alias_file) = s.extra.get("mutt_alias_file").map(String::as_str) { + match std::fs::read_to_string(std::path::Path::new(mutt_alias_file)) + .map_err(|err| err.to_string()) + .and_then(|contents| { + contents + .lines() + .map(|line| mutt::parse_mutt_contact().parse(line).map(|(_, c)| c)) + .collect::, &str>>() + .map_err(|err| err.to_string()) + }) { + Ok(cards) => { + for c in cards { + ret.add_card(c); } } + Err(err) => { + crate::log( + format!( + "Could not load mutt alias file {:?}: {}", + mutt_alias_file, err + ), + crate::WARN, + ); + } } - ret } + #[cfg(feature = "vcard")] + if let Some(vcard_path) = s.vcard_folder() { + match vcard::load_cards(std::path::Path::new(vcard_path)) { + Ok(cards) => { + for c in cards { + ret.add_card(c); + } + } + Err(err) => { + crate::log( + format!("Could not load vcards from {:?}: {}", vcard_path, err), + crate::WARN, + ); + } + } + } + ret } pub fn add_card(&mut self, card: Card) { @@ -203,36 +226,54 @@ impl Card { datetime::timestamp_to_string(self.last_edited, None, false) } - pub fn set_id(&mut self, new_val: CardId) { + pub fn set_id(&mut self, new_val: CardId) -> &mut Self { self.id = new_val; - } - pub fn set_title(&mut self, new: String) { - self.title = new; - } - pub fn set_name(&mut self, new: String) { - self.name = new; - } - pub fn set_additionalname(&mut self, new: String) { - self.additionalname = new; - } - pub fn set_name_prefix(&mut self, new: String) { - self.name_prefix = new; - } - pub fn set_name_suffix(&mut self, new: String) { - self.name_suffix = new; - } - pub fn set_email(&mut self, new: String) { - self.email = new; - } - pub fn set_url(&mut self, new: String) { - self.url = new; - } - pub fn set_key(&mut self, new: String) { - self.key = new; + self } - pub fn set_extra_property(&mut self, key: &str, value: String) { + pub fn set_title(&mut self, new: String) -> &mut Self { + self.title = new; + self + } + + pub fn set_name(&mut self, new: String) -> &mut Self { + self.name = new; + self + } + + pub fn set_additionalname(&mut self, new: String) -> &mut Self { + self.additionalname = new; + self + } + + pub fn set_name_prefix(&mut self, new: String) -> &mut Self { + self.name_prefix = new; + self + } + + pub fn set_name_suffix(&mut self, new: String) -> &mut Self { + self.name_suffix = new; + self + } + + pub fn set_email(&mut self, new: String) -> &mut Self { + self.email = new; + self + } + + pub fn set_url(&mut self, new: String) -> &mut Self { + self.url = new; + self + } + + pub fn set_key(&mut self, new: String) -> &mut Self { + self.key = new; + self + } + + pub fn set_extra_property(&mut self, key: &str, value: String) -> &mut Self { self.extra_properties.insert(key.to_string(), value); + self } pub fn extra_property(&self, key: &str) -> Option<&str> { @@ -243,8 +284,9 @@ impl Card { &self.extra_properties } - pub fn set_external_resource(&mut self, new_val: bool) { + pub fn set_external_resource(&mut self, new_val: bool) -> &mut Self { self.external_resource = new_val; + self } pub fn external_resource(&self) -> bool { diff --git a/melib/src/addressbook/mutt.rs b/melib/src/addressbook/mutt.rs new file mode 100644 index 000000000..805c35463 --- /dev/null +++ b/melib/src/addressbook/mutt.rs @@ -0,0 +1,106 @@ +/* + * meli - addressbook 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 . + */ + +//! # Mutt contact formats +//! + +use super::*; +use crate::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser}; +use std::collections::VecDeque; + +//alias [ ]
+// From mutt doc: +// +// ```text +// Since the name can consist of several whitespace-separated words, the +// last word is considered the address, and it can be optionally enclosed +// between angle brackets. +// For example: alias mumon My dear pupil Mumon foobar@example.com +// will be parsed in this way: +// +// alias mumon My dear pupil Mumon foobar@example.com +// ^ ^ ^ +// nickname long name email address +// The nickname (or alias) will be used to select a corresponding long name +// and email address when specifying the To field of an outgoing message, +// e.g. when using the function in the browser or index context. +// The long name is optional, so you can specify an alias command in this +// way: +// +// alias mumon foobar@example.com +// ^ ^ +// nickname email address +// ``` +pub fn parse_mutt_contact<'a>() -> impl Parser<'a, Card> { + move |input| { + map_res( + prefix(match_literal_anycase("alias "), is_not(b"\r\n")), + |l| { + let mut tokens = l.split_whitespace().collect::>(); + + let mut ret = Card::new(); + let title = tokens.pop_front().ok_or(l)?.to_string(); + let mut email = tokens.pop_back().ok_or(l)?.to_string(); + if email.starts_with('<') && email.ends_with('>') { + email.pop(); + email.remove(0); + } + let mut name = tokens.into_iter().fold(String::new(), |mut acc, el| { + acc.push_str(el); + acc.push(' '); + acc + }); + name.pop(); + if name.trim().is_empty() { + name = title.clone(); + } + ret.set_title(title).set_email(email).set_name(name); + Ok::(ret) + }, + ) + .parse(input) + } +} + +#[test] +fn test_mutt_contacts() { + let a = "alias mumon My dear pupil Mumon foobar@example.com"; + let b = "alias mumon foobar@example.com"; + let c = "alias
"; + + let (other, a_card) = parse_mutt_contact().parse(a).unwrap(); + assert!(other.is_empty()); + assert_eq!(a_card.name(), "My dear pupil Mumon"); + assert_eq!(a_card.title(), "mumon"); + assert_eq!(a_card.email(), "foobar@example.com"); + + let (other, b_card) = parse_mutt_contact().parse(b).unwrap(); + assert!(other.is_empty()); + assert_eq!(b_card.name(), "mumon"); + assert_eq!(b_card.title(), "mumon"); + assert_eq!(b_card.email(), "foobar@example.com"); + + let (other, c_card) = parse_mutt_contact().parse(c).unwrap(); + assert!(other.is_empty()); + assert_eq!(c_card.name(), ""); + assert_eq!(c_card.title(), ""); + assert_eq!(c_card.email(), "address"); +} diff --git a/melib/src/parsec.rs b/melib/src/parsec.rs index a502ca74b..8eebf608a 100644 --- a/melib/src/parsec.rs +++ b/melib/src/parsec.rs @@ -343,6 +343,23 @@ pub fn is_a<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> { } } +pub fn is_not<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> { + move |input: &'a str| { + let mut i = 0; + for byte in input.as_bytes().iter() { + if slice.contains(byte) { + break; + } + i += 1; + } + if i == 0 { + return Err(""); + } + let (b, a) = input.split_at(i); + Ok((a, b)) + } +} + /// Try alternative parsers in order until one succeeds. /// /// ```rust