Skip to content

Commit 7ec7329

Browse files
committed
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.
1 parent a8a7cec commit 7ec7329

File tree

2 files changed

+37
-30
lines changed

2 files changed

+37
-30
lines changed

deltachat-contact-tools/src/vcard.rs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,29 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
4646
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
4747
}
4848

49+
fn escape(s: &str) -> String {
50+
s.replace(',', "\\,")
51+
}
52+
4953
let mut res = "".to_string();
5054
for c in contacts {
51-
let addr = &c.addr;
52-
let display_name = c.display_name();
55+
// Mustn't contain ',', but it's easier to escape than to error out.
56+
let addr = escape(&c.addr);
57+
let display_name = escape(c.display_name());
5358
res += &format!(
5459
"BEGIN:VCARD\r\n\
5560
VERSION:4.0\r\n\
5661
EMAIL:{addr}\r\n\
5762
FN:{display_name}\r\n"
5863
);
5964
if let Some(key) = &c.key {
60-
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
65+
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
6166
}
6267
if let Some(profile_image) = &c.profile_image {
63-
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
68+
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
6469
}
6570
if let Some(biography) = &c.biography {
66-
res += &format!("NOTE:{biography}\r\n");
71+
res += &format!("NOTE:{}\r\n", escape(biography));
6772
}
6873
if let Some(timestamp) = format_timestamp(c) {
6974
res += &format!("REV:{timestamp}\r\n");
@@ -84,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
8489
None
8590
}
8691
}
87-
/// Returns (parameters, value) tuple.
88-
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
92+
/// Returns (parameters, raw value) tuple.
93+
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
8994
let remainder = remove_prefix(line, property)?;
9095
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
9196
// then `remainder` is now `;TYPE=work:alice@example.com`
@@ -115,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
115120
}
116121
Some((params, value))
117122
}
123+
/// Returns (parameters, unescaped value) tuple.
124+
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
125+
let (params, value) = vcard_property_raw(line, property)?;
126+
// Some fields can't contain commas, but unescape them everywhere for safety.
127+
Some((params, value.replace("\\,", ",")))
128+
}
118129
fn base64_key(line: &str) -> Option<&str> {
119-
let (params, value) = vcard_property(line, "key")?;
130+
let (params, value) = vcard_property_raw(line, "key")?;
120131
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
121132
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
122133
{
123134
return Some(value);
124135
}
125-
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
126-
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
127-
{
128-
return Some(value);
129-
}
130-
131-
None
136+
remove_prefix(value, "data:application/pgp-keys;base64\\,")
137+
// Old Delta Chat format.
138+
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
132139
}
133140
fn base64_photo(line: &str) -> Option<&str> {
134-
let (params, value) = vcard_property(line, "photo")?;
141+
let (params, value) = vcard_property_raw(line, "photo")?;
135142
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
136143
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
137144
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
@@ -141,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
141148
{
142149
return Some(value);
143150
}
144-
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
145-
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
146-
{
147-
return Some(value);
148-
}
149-
150-
None
151+
remove_prefix(value, "data:image/jpeg;base64\\,")
152+
// Old Delta Chat format.
153+
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
151154
}
152155
fn parse_datetime(datetime: &str) -> Result<i64> {
153156
// 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> {
216219
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
217220
datetime.get_or_insert(rev);
218221
} else if line.eq_ignore_ascii_case("END:VCARD") {
219-
let (authname, addr) =
220-
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
222+
let (authname, addr) = sanitize_name_and_addr(
223+
&display_name.unwrap_or_default(),
224+
&addr.unwrap_or_default(),
225+
);
221226

222227
contacts.push(VcardContact {
223228
authname,
224229
addr,
225230
key: key.map(|s| s.to_string()),
226231
profile_image: photo.map(|s| s.to_string()),
227-
biography: biography.map(|b| b.to_owned()),
232+
biography,
228233
timestamp: datetime
234+
.as_deref()
229235
.context("No timestamp in vcard")
230236
.and_then(parse_datetime),
231237
});

deltachat-contact-tools/src/vcard/vcard_tests.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ fn test_make_and_parse_vcard() {
108108
VERSION:4.0\r\n\
109109
EMAIL:alice@example.org\r\n\
110110
FN:Alice Wonderland\r\n\
111-
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
112-
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
113-
NOTE:Hi, I'm Alice\r\n\
111+
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
112+
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
113+
NOTE:Hi\\, I'm Alice\r\n\
114114
REV:20240418T184242Z\r\n\
115115
END:VCARD\r\n",
116116
"BEGIN:VCARD\r\n\
@@ -249,7 +249,8 @@ END:VCARD",
249249
assert_eq!(contacts[0].profile_image, None);
250250
}
251251

252-
/// Proton at some point slightly changed the format of their vcards
252+
/// Proton at some point slightly changed the format of their vcards.
253+
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
253254
#[test]
254255
fn test_protonmail_vcard2() {
255256
let contacts = parse_vcard(

0 commit comments

Comments
 (0)