5
5
import re
6
6
import quopri
7
7
8
+
8
9
class VCard :
9
10
BEGIN = 'BEGIN:VCARD'
10
11
END = 'END:VCARD'
11
12
N = re .compile ('^N[:;]' )
12
13
FN = re .compile ('^FN[:;]' )
13
14
NICKNAME = re .compile ('NICKNAME[:;]' )
15
+
14
16
def __init__ (self , prune_empty = False ):
15
17
self .reset ()
16
18
self ._prune_empty = prune_empty
19
+
17
20
def reset (self ):
18
21
self .lines = []
19
22
self ._omit = False
20
23
self ._n = None
21
24
self ._fn = None
25
+
22
26
def add (self , line , idx = None ):
23
27
if idx is not None :
24
28
self .lines .insert (idx , line )
@@ -28,10 +32,13 @@ def add(self, line, idx=None):
28
32
self ._n = line
29
33
if VCard .FN .match (line ):
30
34
self ._fn = line
35
+
31
36
def omit (self ):
32
37
self ._omit = True
38
+
33
39
def valid (self ):
34
40
return self ._n != None and self ._fn != None
41
+
35
42
def repair (self ):
36
43
if self .valid ():
37
44
return
@@ -50,16 +57,21 @@ def repair(self):
50
57
# Arbitrary "convertion": whitespace in FN <-> ';' in N
51
58
# N and FN have precedence over NICKNAME
52
59
if n_idx >= 0 and self ._fn is None :
53
- self .add (self .lines [n_idx ].replace ('N' , 'FN' , 1 ).replace (';' , ' ' , 1 ), n_idx + 1 )
60
+ self .add (self .lines [n_idx ].replace (
61
+ 'N' , 'FN' , 1 ).replace (';' , ' ' , 1 ), n_idx + 1 )
54
62
elif fn_idx >= 0 and self ._n is None :
55
- self .add (';' .join (self .lines [fn_idx ].replace ('FN' , 'N' , 1 ).split ())+ '\n ' , fn_idx + 1 )
63
+ self .add (';' .join (self .lines [fn_idx ].replace (
64
+ 'FN' , 'N' , 1 ).split ())+ '\n ' , fn_idx + 1 )
56
65
elif nick_idx >= 0 and (not self ._prune_empty or len (self .lines ) > 4 ):
57
66
# no N or FN but NICKNAME found, use it
58
67
# note that this loosens the vCard2.1 spec. (NICKNAME unsupported)
59
68
if self ._n is None :
60
- self .add (self .lines [nick_idx ].replace ('NICKNAME' , 'N' , 1 ), nick_idx )
69
+ self .add (self .lines [nick_idx ].replace (
70
+ 'NICKNAME' , 'N' , 1 ), nick_idx )
61
71
if self ._fn is None :
62
- self .add (self .lines [nick_idx ].replace ('NICKNAME' , 'FN' , 1 ), nick_idx )
72
+ self .add (self .lines [nick_idx ].replace (
73
+ 'NICKNAME' , 'FN' , 1 ), nick_idx )
74
+
63
75
def write (self , to ):
64
76
if self ._omit :
65
77
return
@@ -70,11 +82,14 @@ def write(self, to):
70
82
for line in self .lines :
71
83
to .write (line )
72
84
85
+
73
86
class QuotedPrintableDecoder :
74
87
# Match 'QUOTED-PRINTABLE' with optional preceding or following 'CHARSET'.
75
88
# Note: the value of CHARSET is ignored, decoding is always done using the 'encoding' constructor parameter.
76
- quoted = re .compile ('.*((;CHARSET=.+)?;ENCODING=QUOTED-PRINTABLE(;CHARSET=.+?)?):' )
77
- def __init__ (self , encoding = 'UTF-8' ):
89
+ quoted = re .compile (
90
+ '.*((;CHARSET=.+)?;ENCODING=QUOTED-PRINTABLE(;CHARSET=.+?)?):' )
91
+
92
+ def __init__ (self , encoding = 'UTF-8' ):
78
93
self .encoding = encoding
79
94
self ._consumed_lines = ''
80
95
pass
@@ -83,11 +98,12 @@ def __call__(self, line):
83
98
return self .decode (line )
84
99
85
100
def decode (self , line ):
86
- line = self ._consumed_lines + line # add potentially stored previous lines
101
+ line = self ._consumed_lines + line # add potentially stored previous lines
87
102
self ._consumed_lines = ''
88
103
m = QuotedPrintableDecoder .quoted .match (line )
89
104
if m :
90
- line = line [:m .start (1 )] + line [m .end (1 ):] # remove the matched group 1 from line
105
+ # remove the matched group 1 from line
106
+ line = line [:m .start (1 )] + line [m .end (1 ):]
91
107
decoded_line = quopri .decodestring (line ).decode (self .encoding )
92
108
# Escape newlines, but preserve the last one (which must be '\n', since we read the file in universal newliens mode)
93
109
decoded_line = decoded_line [:- 1 ].replace ('\r \n ' , '\\ n' )
@@ -106,22 +122,35 @@ def consume_incomplete(self, line):
106
122
107
123
108
124
class Replacer :
109
- type_param_re = re .compile ('^(TEL|EMAIL|ADR|URL|LABEL|IMPP);([^:=]+:)' ) # Regex to create 'TYPE=' paramter, see also Replacer.type_lc. In the second group match everything up to ':', but don't match if '=' or another ':' is found.
125
+ # Regex to create 'TYPE=' paramter, see also Replacer.type_lc.
126
+ # In the second group match everything up to ':', but don't match if '=' or another ':' is found.
127
+ type_param_re = re .compile ('^(TEL|EMAIL|ADR|URL|LABEL|IMPP);([^:=]+:)' )
110
128
111
129
def __init__ (self ):
112
- self .replace_filters = [] # array of 2-tuples. Each tuple consists of regular expression object and replacement. Replacement may be a string or a function, see https://docs.python.org/3/library/re.html#re.sub
113
- self .replace_filters .append ( (re .compile ('^VERSION:.*' ), 'VERSION:3.0' ) )
114
- #self.replace_filters.append( (re.compile('^PHOTO;ENCODING=BASE64;JPEG:'), 'PHOTO:data:image/jpeg;base64,') ) # Version 4.0
115
- self .replace_filters .append ( (re .compile ('^PHOTO;ENCODING=BASE64;JPEG:' ), 'PHOTO;ENCODING=b;TYPE=JPEG:' )) # Version 3.0
116
- self .replace_filters .append ( (re .compile (';X-INTERNET([;:])' ), '\\ 1' ) ) # remove non standard X-INTERNET (not needed for EMAIL anyway)
117
- self .replace_filters .append ( (re .compile ('^X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;([^;]+);.*' ), 'NICKNAME:\\ 1' ) )
118
- self .replace_filters .append ( (re .compile ('^X-JABBER(;?.*):(.+)' ), 'IMPP\\ 1:xmpp:\\ 2' ) ) # Version 4.0
119
- self .replace_filters .append ( (re .compile ('^X-ICQ(;?.*):(.+)' ), 'IMPP\\ 1:icq:\\ 2' ) ) # Version 4.0
120
- self .replace_filters .append ( (Replacer .type_param_re , Replacer .type_lc ) )
121
- self .replace_filters .append ( (re .compile (';PREF([;:])' ), ';TYPE=PREF\\ 1' ) ) # Version 3.0
122
- #self.replace_filters.append( (re.compile(';PREF([;:])'), ';PREF=1\\1') ) # Version 4.0
123
- self .replace_filters .append ( (re .compile ('^EMAIL:([^@]+@jabber.*)' ), 'IMPP;xmpp:\\ 1' ) )
124
- self .replace_filters .append ( (re .compile ('^TEL;TYPE=x-mobil:(.*)' ), 'TEL;TYPE=cell:\\ 1' ) ) # see #9
130
+ # array of 2-tuples.
131
+ # Each tuple consists of regular expression object and replacement.
132
+ # Replacement may be a string or a function, see https://docs.python.org/3/library/re.html#re.sub
133
+ self .replace_filters = []
134
+ self .replace_filters .append ((re .compile ('^VERSION:.*' ), 'VERSION:3.0' ))
135
+ # self.replace_filters.append( (re.compile('^PHOTO;ENCODING=BASE64;JPEG:'), 'PHOTO:data:image/jpeg;base64,') ) # Version 4.0
136
+ self .replace_filters .append ((re .compile (
137
+ '^PHOTO;ENCODING=BASE64;JPEG:' ), 'PHOTO;ENCODING=b;TYPE=JPEG:' )) # Version 3.0
138
+ # remove non standard X-INTERNET (not needed for EMAIL anyway)
139
+ self .replace_filters .append ((re .compile (';X-INTERNET([;:])' ), '\\ 1' ))
140
+ self .replace_filters .append ((re .compile (
141
+ '^X-ANDROID-CUSTOM:vnd.android.cursor.item/nickname;([^;]+);.*' ), 'NICKNAME:\\ 1' ))
142
+ self .replace_filters .append (
143
+ (re .compile ('^X-JABBER(;?.*):(.+)' ), 'IMPP\\ 1:xmpp:\\ 2' )) # Version 4.0
144
+ self .replace_filters .append (
145
+ (re .compile ('^X-ICQ(;?.*):(.+)' ), 'IMPP\\ 1:icq:\\ 2' )) # Version 4.0
146
+ self .replace_filters .append ((Replacer .type_param_re , Replacer .type_lc ))
147
+ self .replace_filters .append (
148
+ (re .compile (';PREF([;:])' ), ';TYPE=PREF\\ 1' )) # Version 3.0
149
+ # self.replace_filters.append( (re.compile(';PREF([;:])'), ';PREF=1\\1') ) # Version 4.0
150
+ self .replace_filters .append (
151
+ (re .compile ('^EMAIL:([^@]+@jabber.*)' ), 'IMPP;xmpp:\\ 1' ))
152
+ self .replace_filters .append (
153
+ (re .compile ('^TEL;TYPE=x-mobil:(.*)' ), 'TEL;TYPE=cell:\\ 1' )) # see #9
125
154
126
155
def type_lc (matchobj ):
127
156
# Example:
@@ -130,7 +159,6 @@ def type_lc(matchobj):
130
159
# TEL;TYPE=cell,voice:+49123456789
131
160
return matchobj .group (1 )+ ';TYPE=' + matchobj .group (2 ).lower ().replace (";" , "," )
132
161
133
-
134
162
def __call__ (self , line ):
135
163
return self .replace (line )
136
164
@@ -157,17 +185,23 @@ def remove(self, line):
157
185
return False
158
186
159
187
160
-
161
188
def main (argv ):
162
- parser = argparse .ArgumentParser (description = 'Convert VCard 2.1 to VCard 3.0.' )
189
+ parser = argparse .ArgumentParser (
190
+ description = 'Convert VCard 2.1 to VCard 3.0.' )
163
191
parser .add_argument ('infile' )
164
192
parser .add_argument ('outfile' , nargs = '?' )
165
- parser .add_argument ('--in_encoding' , default = sys .getdefaultencoding (), help = 'the encoding of the input file (default: platform dependent)' )
166
- parser .add_argument ('--out_encoding' , default = sys .getdefaultencoding (), help = 'the encoding for the output file (default: platform dependent)' )
167
- parser .add_argument ('-r' , '--remove' , action = 'append' , help = 'remove lines matching regex REMOVE, can be given multiple times' )
168
- parser .add_argument ('--remove_card' , action = 'append' , help = 'remove vcards for which any line matches regex REMOVE, can be given multiple times' )
169
- parser .add_argument ('--remove_dollar' , action = 'store_true' , help = 'remove "$" in N and FN values' )
170
- parser .add_argument ('-p' , '--prune_empty' , action = 'store_true' , help = 'remove vcards which have only FN but no additional fields' )
193
+ parser .add_argument ('--in_encoding' , default = sys .getdefaultencoding (),
194
+ help = 'the encoding of the input file (default: platform dependent)' )
195
+ parser .add_argument ('--out_encoding' , default = sys .getdefaultencoding (),
196
+ help = 'the encoding for the output file (default: platform dependent)' )
197
+ parser .add_argument ('-r' , '--remove' , action = 'append' ,
198
+ help = 'remove lines matching regex REMOVE, can be given multiple times' )
199
+ parser .add_argument ('--remove_card' , action = 'append' ,
200
+ help = 'remove vcards for which any line matches regex REMOVE, can be given multiple times' )
201
+ parser .add_argument ('--remove_dollar' , action = 'store_true' ,
202
+ help = 'remove "$" in N and FN values' )
203
+ parser .add_argument ('-p' , '--prune_empty' , action = 'store_true' ,
204
+ help = 'remove vcards which have only FN but no additional fields' )
171
205
args = parser .parse_args (argv )
172
206
173
207
if args .outfile :
@@ -179,7 +213,8 @@ def main(argv):
179
213
decoder = QuotedPrintableDecoder (args .in_encoding )
180
214
replace = Replacer ()
181
215
if args .remove_dollar :
182
- replace .replace_filters .append ( (re .compile ('^(N|FN):([^$]+)\$' ), '\\ 1:\\ 2' ) )
216
+ replace .replace_filters .append (
217
+ (re .compile ('^(N|FN):([^$]+)\$' ), '\\ 1:\\ 2' ))
183
218
remove_line = Remover (args .remove if args .remove else None )
184
219
remove_card = Remover (args .remove_card if args .remove_card else None )
185
220
@@ -204,5 +239,6 @@ def main(argv):
204
239
if line .startswith (VCard .END ):
205
240
vcard .write (outfile )
206
241
242
+
207
243
if __name__ == "__main__" :
208
244
main (sys .argv [1 :])
0 commit comments