melib/addressbook: add parser for mutt alias file

sieve
Manos Pitsidianakis 2022-12-23 02:32:22 +02:00
parent 40c6647db8
commit 2878bbb8c8
5 changed files with 224 additions and 48 deletions

View File

@ -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

View File

@ -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.

View File

@ -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::<Result<Vec<Card>, &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 {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
//! # Mutt contact formats
//!
use super::*;
use crate::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser};
use std::collections::VecDeque;
//alias <nickname> [ <long name> ] <address>
// 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::<VecDeque<&str>>();
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::<Card, &'a str>(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 <nickname> <long name> <address>";
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(), "<long name>");
assert_eq!(c_card.title(), "<nickname>");
assert_eq!(c_card.email(), "address");
}

View File

@ -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