mirror of
https://github.com/chatmail/core.git
synced 2026-04-29 11:26:29 +03:00
fix(contact-tools): Escape commas in vCards' FN, KEY, PHOTO, NOTE (#6912)
Citing @link2xt: > RFC examples sometimes don't escape commas, but there is errata that fixes some of them. Also this unescapes commas in all fields. This can lead to, say, an email address with commas, but anyway the caller should check parsed `VcardContact`'s fields for correctness.
This commit is contained in:
@@ -46,10 +46,15 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace(',', "\\,")
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||
let addr = escape(&c.addr);
|
||||
let display_name = escape(c.display_name());
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
@@ -57,13 +62,13 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
FN:{display_name}\r\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(biography) = &c.biography {
|
||||
res += &format!("NOTE:{biography}\r\n");
|
||||
res += &format!("NOTE:{}\r\n", escape(biography));
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
@@ -84,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Returns (parameters, value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
/// Returns (parameters, raw value) tuple.
|
||||
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
let remainder = remove_prefix(line, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
@@ -115,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
Some((params, value))
|
||||
}
|
||||
/// Returns (parameters, unescaped value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||
let (params, value) = vcard_property_raw(line, property)?;
|
||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||
Some((params, value.replace("\\,", ",")))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "key")?;
|
||||
let (params, value) = vcard_property_raw(line, "key")?;
|
||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
||||
}
|
||||
fn base64_photo(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "photo")?;
|
||||
let (params, value) = vcard_property_raw(line, "photo")?;
|
||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||
@@ -141,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:image/jpeg;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
@@ -216,16 +219,19 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
let (authname, addr) = sanitize_name_and_addr(
|
||||
&display_name.unwrap_or_default(),
|
||||
&addr.unwrap_or_default(),
|
||||
);
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
biography: biography.map(|b| b.to_owned()),
|
||||
biography,
|
||||
timestamp: datetime
|
||||
.as_deref()
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
|
||||
@@ -108,9 +108,9 @@ fn test_make_and_parse_vcard() {
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
NOTE:Hi, I'm Alice\r\n\
|
||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||
NOTE:Hi\\, I'm Alice\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
@@ -249,7 +249,8 @@ END:VCARD",
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
/// Proton at some point slightly changed the format of their vcards
|
||||
/// Proton at some point slightly changed the format of their vcards.
|
||||
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
||||
#[test]
|
||||
fn test_protonmail_vcard2() {
|
||||
let contacts = parse_vcard(
|
||||
|
||||
Reference in New Issue
Block a user