Skip to content

Commit d2803c4

Browse files
authored
feat: Parse proton vCards again (#6771)
Proton vCards now contain this extra `PREF=1` parameter, which threw off our parsing. This PR fixes both and adds a test.
1 parent ab47d6f commit d2803c4

File tree

1 file changed

+77
-20
lines changed
  • deltachat-contact-tools/src

1 file changed

+77
-20
lines changed

deltachat-contact-tools/src/lib.rs

+77-20
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,16 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
108108
None
109109
}
110110
}
111-
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
112-
let remainder = remove_prefix(s, property)?;
111+
/// Returns (parameters, value) tuple.
112+
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
113+
let remainder = remove_prefix(line, property)?;
113114
// If `s` is `EMAIL;TYPE=work:[email protected]` and `property` is `EMAIL`,
114115
// then `remainder` is now `;TYPE=work:[email protected]`
115116

116117
// Note: This doesn't handle the case where there are quotes around a colon,
117118
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
118119
// This could be improved in the future, but for now, the parsing is good enough.
119-
let (params, value) = remainder.split_once(':')?;
120+
let (mut params, value) = remainder.split_once(':')?;
120121
// In the example from above, `params` is now `;TYPE=work`
121122
// and `value` is now `[email protected]`
122123

@@ -130,7 +131,47 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
130131
// so this line's property is actually something else
131132
return None;
132133
}
133-
Some(value)
134+
if let Some(p) = remove_prefix(params, ";") {
135+
params = p;
136+
}
137+
if let Some(p) = remove_prefix(params, "PREF=1") {
138+
params = p;
139+
}
140+
Some((params, value))
141+
}
142+
fn base64_key(line: &str) -> Option<&str> {
143+
let (params, value) = vcard_property(line, "key")?;
144+
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
145+
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
146+
{
147+
return Some(value);
148+
}
149+
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
150+
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
151+
{
152+
return Some(value);
153+
}
154+
155+
None
156+
}
157+
fn base64_photo(line: &str) -> Option<&str> {
158+
let (params, value) = vcard_property(line, "photo")?;
159+
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
160+
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
161+
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
162+
|| params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG")
163+
|| params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG")
164+
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64")
165+
{
166+
return Some(value);
167+
}
168+
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
169+
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
170+
{
171+
return Some(value);
172+
}
173+
174+
None
134175
}
135176
fn parse_datetime(datetime: &str) -> Result<i64> {
136177
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
@@ -185,26 +226,15 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
185226
line = remainder;
186227
}
187228

188-
if let Some(email) = vcard_property(line, "email") {
229+
if let Some((_params, email)) = vcard_property(line, "email") {
189230
addr.get_or_insert(email);
190-
} else if let Some(name) = vcard_property(line, "fn") {
231+
} else if let Some((_params, name)) = vcard_property(line, "fn") {
191232
display_name.get_or_insert(name);
192-
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
193-
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
194-
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
195-
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
196-
{
233+
} else if let Some(k) = base64_key(line) {
197234
key.get_or_insert(k);
198-
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
199-
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
200-
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
201-
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
202-
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
203-
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
204-
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
205-
{
235+
} else if let Some(p) = base64_photo(line) {
206236
photo.get_or_insert(p);
207-
} else if let Some(rev) = vcard_property(line, "rev") {
237+
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
208238
datetime.get_or_insert(rev);
209239
} else if line.eq_ignore_ascii_case("END:VCARD") {
210240
let (authname, addr) =
@@ -774,6 +804,33 @@ END:VCARD",
774804
assert_eq!(contacts[0].profile_image, None);
775805
}
776806

807+
/// Proton at some point slightly changed the format of their vcards
808+
#[test]
809+
fn test_protonmail_vcard2() {
810+
let contacts = parse_vcard(
811+
r"BEGIN:VCARD
812+
VERSION:4.0
813+
FN;PREF=1:Alice
814+
PHOTO;PREF=1:data:image/jpeg;base64\,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
815+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
816+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z
817+
REV:Invalid Date
818+
ITEM1.EMAIL;PREF=1:[email protected]
819+
KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa
820+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
821+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
822+
UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
823+
END:VCARD",
824+
);
825+
826+
assert_eq!(contacts.len(), 1);
827+
assert_eq!(&contacts[0].addr, "[email protected]");
828+
assert_eq!(&contacts[0].authname, "Alice");
829+
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
830+
assert!(contacts[0].timestamp.is_err());
831+
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
832+
}
833+
777834
#[test]
778835
fn test_sanitize_name() {
779836
assert_eq!(&sanitize_name(" hello world "), "hello world");

0 commit comments

Comments
 (0)