mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
3 Commits
v1.154.3
...
hoc/try-ou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2580ec2201 | ||
|
|
d2014f6038 | ||
|
|
ba6f3291ea |
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -1227,9 +1227,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"ical",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
@@ -2562,6 +2564,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idea"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -47,7 +47,7 @@ async_zip = { version = "0.0.12", default-features = false, features = ["deflate
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
brotli = { version = "5", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
|
||||
chrono = { workspace = true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
@@ -168,7 +168,8 @@ harness = false
|
||||
anyhow = "1"
|
||||
once_cell = "1.18.0"
|
||||
regex = "1.10"
|
||||
rusqlite = { version = "0.31" }
|
||||
rusqlite = "0.31"
|
||||
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-contact-tools"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0" # No semver-stable versioning
|
||||
edition = "2021"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address"
|
||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||
license = "MPL-2.0"
|
||||
# TODO maybe it should be called "deltachat-text-utils" or similar?
|
||||
|
||||
@@ -13,6 +13,8 @@ anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true }
|
||||
ical = { version = "0.11.0", default-features = false, features = ["vcard"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
|
||||
@@ -29,10 +29,81 @@ use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// TODOs to clean up:
|
||||
// - Check if sanitizing is done correctly everywhere
|
||||
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Contact {
|
||||
addr: String,
|
||||
display_name: String,
|
||||
key: String,
|
||||
profile_photo: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn parse_vcard(vcard: String) -> Result<Vec<Contact>> {
|
||||
let reader = ical::VcardParser::new(vcard.as_bytes());
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for vcard_contact in reader {
|
||||
let vcard_contact = vcard_contact?; // TODO should just continue with the next contact
|
||||
let mut new_contact = Contact::default();
|
||||
|
||||
let mut display_name = None;
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut timestamp = None;
|
||||
|
||||
for property in vcard_contact.properties {
|
||||
match &*property.name.to_lowercase() {
|
||||
"email" => addr = addr.or(property.value),
|
||||
"fn" => display_name = display_name.or(property.value),
|
||||
"key" => key = key.or(dbg!(property).value), // TODO hmmm, the ical crate can apparently only parse version 3.0
|
||||
"photo" => photo = photo.or(property.value),
|
||||
"rev" => {
|
||||
timestamp = timestamp.or(property.value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (display_name, addr) =
|
||||
sanitize_name_and_addr(&display_name.unwrap_or_default(), &addr.unwrap_or_default());
|
||||
new_contact.display_name = display_name;
|
||||
new_contact.addr = addr;
|
||||
|
||||
if let Some(key) = key {
|
||||
if let Some(base64_key) = key
|
||||
.strip_prefix("PGP;ENCODING=BASE64:")
|
||||
.or(key.strip_prefix("TYPE=PGP;ENCODING=b:"))
|
||||
.or(key.strip_prefix("data:application/pgp-keys;base64,"))
|
||||
{
|
||||
new_contact.key = base64_key.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(photo) = photo {
|
||||
if let Some(base64_photo) = photo
|
||||
.strip_prefix("PGP;ENCODING=BASE64:")
|
||||
.or(photo.strip_prefix("TYPE=PGP;ENCODING=b:"))
|
||||
.or(photo.strip_prefix("data:application/pgp-keys;base64,"))
|
||||
{}
|
||||
}
|
||||
|
||||
contacts.push(new_contact);
|
||||
}
|
||||
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactAddress(String);
|
||||
@@ -81,14 +152,10 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
/// Make the name and address
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
@@ -97,8 +164,21 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
(
|
||||
strip_rtlo_characters(&normalize_name(name)),
|
||||
addr.to_string(),
|
||||
)
|
||||
};
|
||||
let mut name = normalize_name(&name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
}
|
||||
|
||||
(name, addr)
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
@@ -232,6 +312,65 @@ impl rusqlite::types::ToSql for EmailAddress {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_thunderbird() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'Alice Mueller'
|
||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'bobzzz@freenet.de'
|
||||
EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, "".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].display_name, "".to_string());
|
||||
assert_eq!(contacts[1].key, "".to_string());
|
||||
assert_eq!(contacts[1].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_example() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Alice Wonderland
|
||||
N:Wonderland;Alice;;;Ms.
|
||||
GENDER:W
|
||||
EMAIL;TYPE=work:alice@example.com
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, "[base64-data]".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
assert_eq!(contacts[0].timestamp, 1713465762); // I did not check whether this timestamp is correct
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
@@ -277,4 +416,38 @@ mod tests {
|
||||
assert!(EmailAddress::new("u@tt").is_ok());
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_android_contact_export() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
TEL;CELL:+1-234-567-890
|
||||
EMAIL;HOME:bob@example.org
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Alice;;;
|
||||
FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
"
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, "".to_string());
|
||||
assert_eq!(contacts[0].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].display_name, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, "".to_string());
|
||||
assert_eq!(contacts[1].profile_photo, "".to_string());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user