@@ -20,6 +20,8 @@ pub struct VcardContact {
20
20
pub key : Option < String > ,
21
21
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
22
22
pub profile_image : Option < String > ,
23
+ /// The biography, stored in the vcard property `note`
24
+ pub biography : Option < String > ,
23
25
/// The timestamp when the vcard was created / last updated, vcard property `rev`
24
26
pub timestamp : Result < i64 > ,
25
27
}
@@ -44,21 +46,29 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
44
46
Some ( datetime. format ( "%Y%m%dT%H%M%SZ" ) . to_string ( ) )
45
47
}
46
48
49
+ fn escape ( s : & str ) -> String {
50
+ s. replace ( ',' , "\\ ," )
51
+ }
52
+
47
53
let mut res = "" . to_string ( ) ;
48
54
for c in contacts {
49
- let addr = & c. addr ;
50
- 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 ( ) ) ;
51
58
res += & format ! (
52
59
"BEGIN:VCARD\r \n \
53
60
VERSION:4.0\r \n \
54
61
EMAIL:{addr}\r \n \
55
62
FN:{display_name}\r \n "
56
63
) ;
57
64
if let Some ( key) = & c. key {
58
- res += & format ! ( "KEY:data:application/pgp-keys;base64,{key}\r \n " ) ;
65
+ res += & format ! ( "KEY:data:application/pgp-keys;base64\\ ,{key}\r \n " ) ;
59
66
}
60
67
if let Some ( profile_image) = & c. profile_image {
61
- res += & format ! ( "PHOTO:data:image/jpeg;base64,{profile_image}\r \n " ) ;
68
+ res += & format ! ( "PHOTO:data:image/jpeg;base64\\ ,{profile_image}\r \n " ) ;
69
+ }
70
+ if let Some ( biography) = & c. biography {
71
+ res += & format ! ( "NOTE:{}\r \n " , escape( biography) ) ;
62
72
}
63
73
if let Some ( timestamp) = format_timestamp ( c) {
64
74
res += & format ! ( "REV:{timestamp}\r \n " ) ;
@@ -79,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
79
89
None
80
90
}
81
91
}
82
- /// Returns (parameters, value) tuple.
83
- 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 ) > {
84
94
let remainder = remove_prefix ( line, property) ?;
85
95
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
86
96
// then `remainder` is now `;TYPE=work:alice@example.com`
@@ -110,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
110
120
}
111
121
Some ( ( params, value) )
112
122
}
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
+ }
113
129
fn base64_key ( line : & str ) -> Option < & str > {
114
- let ( params, value) = vcard_property ( line, "key" ) ?;
130
+ let ( params, value) = vcard_property_raw ( line, "key" ) ?;
115
131
if params. eq_ignore_ascii_case ( "PGP;ENCODING=BASE64" )
116
132
|| params. eq_ignore_ascii_case ( "TYPE=PGP;ENCODING=b" )
117
133
{
118
134
return Some ( value) ;
119
135
}
120
- if let Some ( value) = remove_prefix ( value, "data:application/pgp-keys;base64," )
121
- . or_else ( || remove_prefix ( value, r"data:application/pgp-keys;base64\," ) )
122
- {
123
- return Some ( value) ;
124
- }
125
-
126
- 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," ) )
127
139
}
128
140
fn base64_photo ( line : & str ) -> Option < & str > {
129
- let ( params, value) = vcard_property ( line, "photo" ) ?;
141
+ let ( params, value) = vcard_property_raw ( line, "photo" ) ?;
130
142
if params. eq_ignore_ascii_case ( "JPEG;ENCODING=BASE64" )
131
143
|| params. eq_ignore_ascii_case ( "ENCODING=BASE64;JPEG" )
132
144
|| params. eq_ignore_ascii_case ( "TYPE=JPEG;ENCODING=b" )
@@ -136,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
136
148
{
137
149
return Some ( value) ;
138
150
}
139
- if let Some ( value) = remove_prefix ( value, "data:image/jpeg;base64," )
140
- . or_else ( || remove_prefix ( value, r"data:image/jpeg;base64\," ) )
141
- {
142
- return Some ( value) ;
143
- }
144
-
145
- None
151
+ remove_prefix ( value, "data:image/jpeg;base64\\ ," )
152
+ // Old Delta Chat format.
153
+ . or_else ( || remove_prefix ( value, "data:image/jpeg;base64," ) )
146
154
}
147
155
fn parse_datetime ( datetime : & str ) -> Result < i64 > {
148
156
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
@@ -186,6 +194,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
186
194
let mut addr = None ;
187
195
let mut key = None ;
188
196
let mut photo = None ;
197
+ let mut biography = None ;
189
198
let mut datetime = None ;
190
199
191
200
for mut line in lines. by_ref ( ) {
@@ -205,18 +214,24 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
205
214
key. get_or_insert ( k) ;
206
215
} else if let Some ( p) = base64_photo ( line) {
207
216
photo. get_or_insert ( p) ;
217
+ } else if let Some ( ( _params, bio) ) = vcard_property ( line, "note" ) {
218
+ biography. get_or_insert ( bio) ;
208
219
} else if let Some ( ( _params, rev) ) = vcard_property ( line, "rev" ) {
209
220
datetime. get_or_insert ( rev) ;
210
221
} else if line. eq_ignore_ascii_case ( "END:VCARD" ) {
211
- let ( authname, addr) =
212
- 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
+ ) ;
213
226
214
227
contacts. push ( VcardContact {
215
228
authname,
216
229
addr,
217
230
key : key. map ( |s| s. to_string ( ) ) ,
218
231
profile_image : photo. map ( |s| s. to_string ( ) ) ,
232
+ biography,
219
233
timestamp : datetime
234
+ . as_deref ( )
220
235
. context ( "No timestamp in vcard" )
221
236
. and_then ( parse_datetime) ,
222
237
} ) ;
0 commit comments