Tizen 2.1 base
[platform/upstream/hplip.git] / base / vcard.py
1 # -*- coding: utf-8 -*-
2 #
3 # (c) Copyright 2001-2008 Hewlett-Packard Development Company, L.P.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18 #
19 # ****************************************************************************
20 #
21 # Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
22 #
23 # This program is free software; you can redistribute it and/or modify
24 # it under the terms of the BitPim license as detailed in the LICENSE file.
25 #
26 # Code for reading and writing Vcard
27
28 # VCARD is defined in RFC 2425 and 2426
29
30 # Original author: Roger Binns <rogerb@rogerbinns.com>
31 # Modified for HPLIP by: Don Welch
32 #
33
34 # Local
35 from base.g import *
36
37 # Std Lib
38 import quopri
39 import base64
40 import codecs
41 import cStringIO
42 import re
43 import StringIO
44 import codecs
45
46
47
48 _boms = []
49 # 64 bit 
50 try:
51      import encodings.utf_64
52      _boms.append( (codecs.BOM64_BE, "utf_64") )
53      _boms.append( (codecs.BOM64_LE, "utf_64") )
54 except:  pass
55
56 # 32 bit
57 try:
58      import encodings.utf_32
59      _boms.append( (codecs.BOM_UTF32, "utf_32") )
60      _boms.append( (codecs.BOM_UTF32_BE, "utf_32") )
61      _boms.append( (codecs.BOM_UTF32_LE, "utf_32") )
62 except:  pass
63
64 # 16 bit
65 try:
66      import encodings.utf_16
67      _boms.append( (codecs.BOM_UTF16, "utf_16") )
68      _boms.append( (codecs.BOM_UTF16_BE, "utf_16_be") )
69      _boms.append( (codecs.BOM_UTF16_LE, "utf_16_le") )
70 except:  pass
71
72 # 8 bit
73 try:
74      import encodings.utf_8
75      _boms.append( (codecs.BOM_UTF8, "utf_8") )
76 except: pass
77
78 # Work arounds for Apple
79 _boms.append( ("\0B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D", "utf_16_be") )
80 _boms.append( ("B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D\0", "utf_16_le") )
81
82
83 # NB: the 32 bit and 64 bit versions have the BOM constants defined in Py 2.3
84 # but no corresponding encodings module.  They are here for completeness.
85 # The order of above also matters since the first ones have longer
86 # boms than the latter ones, and we need to be unambiguous
87
88 _maxbomlen = max([len(bom) for bom,codec in _boms])
89
90 def opentextfile(name):
91     """This function detects unicode byte order markers and if present
92     uses the codecs module instead to open the file instead with
93     appropriate unicode decoding, else returns the file using standard
94     open function"""
95     #with file(name, 'rb') as f:
96     f = file(name, 'rb')
97     start = f.read(_maxbomlen)
98     for bom,codec in _boms:
99         if start.startswith(bom):
100             # some codecs don't do readline, so we have to vector via stringio
101             # many postings also claim that the BOM is returned as the first
102             # character but that hasn't been the case in my testing
103             return StringIO.StringIO(codecs.open(name, "r", codec).read())
104     return file(name, "rtU")
105
106
107 _notdigits = re.compile("[^0-9]*")
108 _tendigits = re.compile("^[0-9]{10}$")
109 _sevendigits = re.compile("^[0-9]{7}$")
110
111
112 def phonenumber_normalise(n):
113     # this was meant to remove the long distance '1' prefix,
114     # temporary disable it, will be done on a phone-by-phone case.
115     return n
116     nums = "".join(re.split(_notdigits, n))
117     if len(nums) == 10:
118         return nums
119         
120     if len(nums) == 11 and nums[0] == "1":
121         return nums[1:]
122         
123     return n
124
125 def phonenumber_format(n):
126     if re.match(_tendigits, n) is not None:
127         return "(%s) %s-%s" % (n[0:3], n[3:6], n[6:])
128     elif re.match(_sevendigits, n) is not None:
129         return "%s-%s" %(n[:3], n[3:])
130     return n
131
132
133 def nameparser_formatsimplename(name):
134     "like L{formatname}, except we use the first matching component"
135     _fullname = nameparser_getfullname(name)
136     if _fullname:
137         return _fullname
138     return name.get('nickname', "")
139
140
141 def nameparser_getfullname(name):
142     """Gets the full name, joining the first/middle/last if necessary"""
143     if name.has_key("full"):
144         return name["full"]
145     return ' '.join([x for x in nameparser_getparts(name) if x])
146
147     
148 # See the following references for name parsing and how little fun it
149 # is.
150 #
151 # The simple way:
152 # http://cvs.gnome.org/lxr/source/evolution-data-server/addressbook/libebook/
153 # e-name-western*
154 #
155 # The "proper" way:
156 # http://cvs.xemacs.org/viewcvs.cgi/XEmacs/packages/xemacs-packages/mail-lib/mail-extr.el
157 #
158 # How we do it
159 #
160 #  [1] The name is split into white-space seperated parts
161 #  [2] If there is only one part, it becomes the firstname
162 #  [3] If there are only two parts, they become first name and surname
163 #  [4] For three or more parts, the first part is the first name and the last
164 #      part is the surname.  Then while the last part of the remainder starts with
165 #      a lower case letter or is in the list below, it is prepended to the surname.
166 #      Whatever is left becomes the middle name.
167
168 lastparts = [ "van", "von", "de", "di" ]
169
170 # I would also like to proudly point out that this code has no comment saying
171 # "Have I no shame".  It will be considered incomplete until that happens
172
173 def nameparser_getparts_FML(name):
174     n = name.get("full")
175     
176     # [1]
177     parts = n.split()
178
179     # [2]
180     if len(parts) <= 1:
181         return (n, "", "")
182     
183     # [3]
184     if len(parts) == 2:
185         return (parts[0], "", parts[1])
186
187     # [4]
188     f = [parts[0]]
189     m = []
190     l = [parts[-1]]
191     del parts[0]
192     del parts[-1]
193     
194     while len(parts) and (parts[-1][0].lower() == parts[-1][0] or parts[-1].lower() in lastparts):
195         l = [parts[-1]]+l
196         del parts[-1]
197     
198     m = parts
199
200     # return it all
201     return (" ".join(f), " ".join(m), " ".join(l))
202
203     
204 def nameparser_getparts_LFM(name):
205     n = name.get("full")
206     
207     parts = n.split(',')
208
209     if len(parts) <= 1:
210         return (n, '', '')
211     
212     _last = parts[0]
213     _first = ''
214     _middle = ''
215     parts = parts[1].split()
216     
217     if len(parts) >= 1:
218         _first = parts[0]
219         
220         if len(parts) > 1:
221             _middle = ' '.join(parts[1:])
222             
223     return (_first, _middle, _last)
224     
225     
226 def nameparser_getparts(name):
227     """Returns (first, middle, last) for name.  If the part doesn't exist
228     then a blank string is returned"""
229
230     # do we have any of the parts?
231     for i in ("first", "middle", "last"):
232         if name.has_key(i):
233             return (name.get("first", ""), name.get("middle", ""), name.get("last", ""))
234
235     # check we have full.  if not return nickname
236     if not name.has_key("full"):
237         return (name.get("nickname", ""), "", "")
238
239     n = name.nameparser_get("full")
240     
241     if ',' in n:
242         return nameparser_getparts_LFM(name)
243     
244     return nameparser_getparts_FML(name)
245
246
247
248
249 class VFileException(Exception):
250     pass
251
252     
253     
254 class VFile:
255     _charset_aliases = {
256         'MACINTOSH': 'MAC_ROMAN'
257         }
258         
259     def __init__(self, source):
260         self.source = source    
261         self.saved = None
262
263         
264     def __iter__(self):
265         return self
266
267         
268     def next(self):
269         # Get the next non-blank line
270         while True:  # python desperately needs do-while
271             line = self._getnextline()
272             
273             if line is None:
274                 raise StopIteration()
275             
276             if len(line) != 0:
277                 break
278
279         # Hack for evolution.  If ENCODING is QUOTED-PRINTABLE then it doesn't
280         # offset the next line, so we look to see what the first char is
281         normalcontinuations = True
282         colon = line.find(':')
283         if colon > 0:
284             s = line[:colon].lower().split(";")
285             
286             if "quoted-printable" in s or 'encoding=quoted-printable' in s:
287                 normalcontinuations = False
288                 while line[-1] == "=" or line[-2] == '=':
289                     if line[-1] == '=':
290                         i = -1
291                     else:
292                         i = -2
293                     
294                     nextl = self._getnextline()
295                     if nextl[0] in ("\t", " "): nextl = nextl[1:]
296                     line = line[:i]+nextl
297
298         while normalcontinuations:
299             nextline = self._lookahead()
300             
301             if nextline is None:
302                 break
303             
304             if len(nextline) == 0:
305                 break
306             
307             if  nextline[0] != ' ' and nextline[0] != '\t':
308                 break
309             
310             line += self._getnextline()[1:]
311
312         colon = line.find(':')
313         
314         if colon < 1:
315             # some evolution vcards don't even have colons
316             # raise VFileException("Invalid property: "+line)
317             log.debug("Fixing up bad line: %s" % line)
318             
319             colon = len(line)
320             line += ":"
321
322         b4 = line[:colon]
323         line = line[colon+1:].strip()
324
325         # upper case and split on semicolons
326         items = b4.upper().split(";")
327
328         newitems = []
329         if isinstance(line, unicode):
330             charset = None
331             
332         else:
333             charset = "LATIN-1"
334         
335         for i in items:
336             # ::TODO:: probably delete anything preceding a '.'
337             # (see 5.8.2 in rfc 2425)
338             # look for charset parameter
339             if i.startswith("CHARSET="):
340                 charset = i[8:] or "LATIN-1"
341                 continue
342            
343            # unencode anything that needs it
344             if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING="
345                 # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons
346                 newitems.append(i)
347                 continue
348             
349             try:
350                 if i == 'QUOTED-PRINTABLE' or i == "ENCODING=QUOTED-PRINTABLE":
351                     # technically quoted printable is ascii only but we decode anyway since not all vcards comply
352                     line = quopri.decodestring(line)
353                 
354                 elif i == 'ENCODING=B':
355                     line = base64.decodestring(line)
356                     charset = None
357                 
358                 else:
359                     raise VFileException("unknown encoding: "+i)
360                     
361             except Exception,e:
362                 if isinstance(e,VFileException):
363                     raise e
364                 raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line))
365         
366         # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and
367         # convert line as in 5.8.4 of rfc 2425
368         if len(newitems) == 0:
369             raise VFileException("Line contains no property: %s" % (line,))
370         
371         # charset frigging
372         if charset is not None:
373             try:
374                 decoder = codecs.getdecoder(self._charset_aliases.get(charset, charset))
375                 line,_ = decoder(line)
376             except LookupError:
377                 raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4))          
378         
379         if newitems == ["BEGIN"] or newitems == ["END"]:
380             line = line.upper()
381         
382         return newitems, line
383
384         
385     def _getnextline(self):
386         if self.saved is not None:
387             line = self.saved
388             self.saved = None
389             return line
390         else:
391             return self._readandstripline()
392
393             
394     def _readandstripline(self):
395         line = self.source.readline()
396         if line is not None:
397             if len(line) == 0:
398                 return None
399                 
400             elif line[-2:] == "\r\n":    
401                 return line[:-2]
402                 
403             elif line[-1] == '\r' or line[-1] == '\n':
404                 return line[:-1]
405                 
406         return line
407     
408     
409     def _lookahead(self):
410         assert self.saved is None
411         self.saved = self._readandstripline()
412         return self.saved
413         
414         
415         
416 class VCards:
417     "Understands vcards in a vfile"
418
419     
420     def __init__(self, vfile):
421         self.vfile = vfile
422
423         
424     def __iter__(self):
425         return self
426
427         
428     def next(self):
429         # find vcard start
430         field = value = None
431         for field,value in self.vfile:
432             if (field,value) != (["BEGIN"], "VCARD"):
433                 continue
434                 
435             found = True
436             break
437         
438         if (field,value) != (["BEGIN"], "VCARD"):
439             # hit eof without any BEGIN:vcard
440             raise StopIteration()
441         
442         # suck up lines
443         lines = []
444         for field,value in self.vfile:
445             if (field,value) != (["END"], "VCARD"):
446                 lines.append( (field,value) )
447                 continue
448                 
449             break
450         
451         if (field,value) != (["END"], "VCARD"):
452             raise VFileException("There is a BEGIN:VCARD but no END:VCARD")
453         
454         return VCard(lines)
455
456         
457         
458 class VCard:
459     "A single vcard"
460
461     def __init__(self, lines):
462         self._version = (2,0)  # which version of the vcard spec the card conforms to
463         self._origin = None    # which program exported the vcard
464         self._data = {}
465         self._groups = {}
466         self.lines = []
467         
468         # extract version field
469         for f,v in lines:
470             assert len(f)
471             
472             if f == ["X-EVOLUTION-FILE-AS"]: # all evolution cards have this
473                 self._origin = "evolution"
474             
475             if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")):
476                 self._origin = "apple"
477             
478             if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >= 0:
479                 self.origin = "apple"
480             
481             if f == ["VERSION"]:
482                 ver = v.split(".")
483                 try:
484                     ver = [int(xx) for xx in ver]    
485                 except ValueError:
486                     raise VFileException(v+" is not a valid vcard version")
487                 
488                 self._version = ver
489                 continue
490             
491             # convert {home,work}.{tel,label} to {tel,label};{home,work}
492             # this probably dates from *very* early vcards
493             if f[0] == "HOME.TEL": 
494                 f[0:1] = ["TEL", "HOME"]
495                 
496             elif f[0] == "HOME.LABEL": 
497                 f[0:1] = ["LABEL", "HOME"]
498                 
499             elif f[0] == "WORK.TEL": 
500                 f[0:1] = ["TEL", "WORK"]
501                 
502             elif f[0] == "WORK.LABEL": 
503                 f[0:1] = ["LABEL", "WORK"]
504                 
505             self.lines.append( (f,v) )
506         
507         self._parse(self.lines, self._data)
508         self._update_groups(self._data)
509
510         
511     def getdata(self):
512         "Returns a dict of the data parsed out of the vcard"
513         return self._data
514         
515     def get(self, key, default=''):
516         return self._data.get(key, default)
517
518         
519     def _getfieldname(self, name, dict):
520         """Returns the fieldname to use in the dict.
521
522         For example, if name is "email" and there is no "email" field
523         in dict, then "email" is returned.  If there is already an "email"
524         field then "email2" is returned, etc"""
525         if name not in dict:
526             return name
527         for i in xrange(2,99999):
528             if name+`i` not in dict:
529                 return name+`i`
530
531                 
532     def _parse(self, lines, result):
533         for field,value in lines:
534             if len(value.strip()) == 0: # ignore blank values
535                 continue
536                 
537             if '.' in field[0]:
538                 f = field[0][field[0].find('.')+1:]
539             else: 
540                 f = field[0]
541             
542             t = f.replace("-", "_")
543             func = getattr(self, "_field_"+t, self._default_field)
544             func(field, value, result)
545
546             
547     def _update_groups(self, result):
548         """Update the groups info """
549         for k,e in self._groups.items():
550             self._setvalue(result, *e)
551
552             
553     # fields we ignore
554
555     def _field_ignore(self, field, value, result):
556         pass
557
558         
559     _field_LABEL = _field_ignore        # we use the ADR field instead
560     _field_BDAY = _field_ignore         # not stored in bitpim
561     _field_ROLE = _field_ignore         # not stored in bitpim
562     _field_CALURI = _field_ignore       # not stored in bitpim
563     _field_CALADRURI = _field_ignore    # variant of above
564     _field_FBURL = _field_ignore        # not stored in bitpim
565     _field_REV = _field_ignore          # not stored in bitpim
566     _field_KEY = _field_ignore          # not stored in bitpim
567     _field_SOURCE = _field_ignore       # not stored in bitpim (although arguably part of serials)
568     _field_PHOTO = _field_ignore        # contained either binary image, or external URL, not used by BitPim
569
570     
571     # simple fields
572     
573     def _field_FN(self, field, value, result):
574         result[self._getfieldname("name", result)] = self.unquote(value)
575
576         
577     def _field_TITLE(self, field, value, result):
578         result[self._getfieldname("title", result)] = self.unquote(value)
579
580         
581     def _field_NICKNAME(self, field, value, result):
582         # ::TODO:: technically this is a comma seperated list ..
583         result[self._getfieldname("nickname", result)] = self.unquote(value)
584
585         
586     def _field_NOTE(self, field, value, result):
587         result[self._getfieldname("notes", result)] = self.unquote(value)
588
589         
590     def _field_UID(self, field, value, result):
591         result["uid"] = self.unquote(value) # note that we only store one UID (the "U" does stand for unique)
592
593         
594     #
595     #  Complex fields
596     # 
597
598     def _field_N(self, field, value, result):
599         value = self.splitandunquote(value)
600         familyname = givenname = additionalnames = honorificprefixes = honorificsuffixes = None
601         try:
602             familyname = value[0]
603             givenname = value[1]
604             additionalnames = value[2]
605             honorificprefixes = value[3]
606             honorificsuffixes = value[4]
607         except IndexError:
608             pass
609         
610         if familyname is not None and len(familyname):
611             result[self._getfieldname("last name", result)] = familyname
612         
613         if givenname is not None and len(givenname):
614             result[self._getfieldname("first name", result)] = givenname
615         
616         if additionalnames is not None and len(additionalnames):
617             result[self._getfieldname("middle name", result)] = additionalnames
618         
619         if honorificprefixes is not None and len(honorificprefixes):
620             result[self._getfieldname("prefix", result)] = honorificprefixes
621         
622         if honorificsuffixes is not None and len(honorificsuffixes):
623             result[self._getfieldname("suffix", result)] = honorificsuffixes
624
625             
626     _field_NAME = _field_N  # early versions of vcard did this
627
628     
629     def _field_ORG(self, field, value, result):
630         value = self.splitandunquote(value)
631         if len(value):
632             result[self._getfieldname("organisation", result)] = value[0]
633             
634         for f in value[1:]:
635             result[self._getfieldname("organisational unit", result)] = f
636
637             
638     _field_O = _field_ORG # early versions of vcard did this
639
640     
641     def _field_EMAIL(self, field, value, result):
642         value = self.unquote(value)
643         # work out the types
644         types = []
645         for f in field[1:]:
646             if f.startswith("TYPE="):
647                 ff = f[len("TYPE="):].split(",")
648             else: 
649                 ff = [f]
650                 
651             types.extend(ff)
652
653         # the standard doesn't specify types of "home" and "work" but
654         # does allow for random user defined types, so we look for them
655         type = None
656         for t in types:
657             if t == "HOME": 
658                 type="home"
659                 
660             if t == "WORK": 
661                 type="business"
662                 
663             if t == "X400": 
664                 return # we don't want no steenking X.400
665
666         preferred = "PREF" in types
667
668         if type is None:
669             self._setvalue(result, "email", value, preferred)
670         else:
671             addr = {'email': value, 'type': type}
672             self._setvalue(result, "email", addr, preferred)
673
674             
675     def _field_URL(self, field, value, result):
676         # the standard doesn't specify url types or a pref type,
677         # but we implement it anyway
678         value = self.unquote(value)
679         # work out the types
680         types = []
681         for f in field[1:]:
682             if f.startswith("TYPE="):
683                 ff = f[len("TYPE="):].split(",")
684             else: 
685                 ff=[f]
686                 
687             types.extend(ff)
688
689         type = None
690         for t in types:
691             if t == "HOME": 
692                 type="home"
693                 
694             if t == "WORK": 
695                 type="business"
696
697         preferred = "PREF" in types
698
699         if type is None:    
700             self._setvalue(result, "url", value, preferred)
701         else:
702             addr = {'url': value, 'type': type}
703             self._setvalue(result, "url", addr, preferred)        
704
705             
706     def _field_X_SPEEDDIAL(self, field, value, result):
707         if '.' in field[0]:
708             group = field[0][:field[0].find('.')]
709         else:
710             group = None
711         if group is None:
712             # this has to belong to a group!!
713             #print 'speedial has no group'
714             log.debug("speeddial has no group")
715         else:
716             self._setgroupvalue(result, 'phone', { 'speeddial': int(value) },
717                                 group, False)
718
719                                 
720     def _field_TEL(self, field, value, result):
721         value = self.unquote(value)
722         # see if this is part of a group
723         if '.' in field[0]:
724             group = field[0][:field[0].find('.')]
725         else:
726             group = None
727
728         # work out the types
729         types = []
730         
731         for f in field[1:]:
732             if f.startswith("TYPE="):
733                 ff = f[len("TYPE="):].split(",")
734             else: 
735                 ff = [f]
736                 
737             types.extend(ff)
738
739         # type munging - we map vcard types to simpler ones
740         munge = { "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL", 
741             "PCS": "CELL" }
742             
743         types = [munge.get(t, t) for t in types]
744
745         # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data
746         types = [t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE", 
747             "FAX", "CELL", "VIDEO", "PAGER", "DATA")]
748
749         # if type is in this list and voice not explicitly mentioned then it is not a voice type
750         antivoice = ["FAX", "PAGER", "DATA"]
751         
752         if "VOICE" in types:
753             voice = True
754         
755         else:
756             voice = True # default is voice
757             
758             for f in antivoice:
759                 if f in types:
760                     voice = False
761                     break
762                 
763         preferred = "PREF" in types
764
765         # vcard allows numbers to be multiple things at the same time, such as home voice, home fax
766         # and work fax so we have to test for all variations
767
768         # if neither work or home is specified, then no default (otherwise things get really complicated)
769         iswork = False
770         ishome = False
771         if "WORK" in types: 
772             iswork = True
773             
774         if "HOME" in types: 
775             ishome = True
776
777         if len(types) == 0 or types == ["PREF"]: 
778             iswork = True # special case when nothing else is specified
779     
780         
781         value = phonenumber_normalise(value)
782         if iswork and voice:
783             self._setgroupvalue(result,
784                            "phone", {"type": "business", "number": value},
785                            group, preferred)
786                            
787         if ishome and voice:
788             self._setgroupvalue(result,
789                            "phone", {"type": "home", "number": value},
790                            group, preferred)
791                            
792         if not iswork and not ishome and "FAX" in types:
793             # fax without explicit work or home
794             self._setgroupvalue(result,
795                            "phone", {"type": "fax", "number": value},
796                            group, preferred)
797                            
798         else:
799             if iswork and "FAX" in types:
800                 self._setgroupvalue(result, "phone",
801                                {"type": "business fax", "number": value},
802                                group, preferred)
803             
804             if ishome and "FAX" in types:
805                 self._setgroupvalue(result, "phone",
806                                {"type": "home fax", "number": value},
807                                group, preferred)
808         
809         if "CELL" in types:
810             self._setgroupvalue(result,
811                            "phone", {"type": "cell", "number": value},
812                            group, preferred)
813         
814         if "PAGER" in types:
815             self._setgroupvalue(result,
816                            "phone", {"type": "pager", "number": value},
817                            group, preferred)
818         
819         if "DATA" in types:
820             self._setgroupvalue(result,
821                            "phone", {"type": "data", "number": value},
822                            group, preferred)
823
824                            
825     def _setgroupvalue(self, result, type, value, group, preferred=False):
826         """ Set value of an item of a group
827         """
828         if group is None:
829             # no groups specified
830             return self._setvalue(result, type, value, preferred)
831             
832         group_type = self._groups.get(group, None)
833         
834         if group_type is None:
835             # 1st one of the group
836             self._groups[group] = [type, value, preferred]
837         
838         else:
839             if type != group_type[0]:
840                 log.debug('Group %s has different types: %s, %s' % (group, type,groups_type[0]))
841             
842             if preferred:
843                 group_type[2] = True
844             
845             group_type[1].update(value)
846
847             
848     def _setvalue(self, result, type, value, preferred=False):
849         if type not in result:
850             result[type] = value
851             return
852             
853         if not preferred:
854             result[self._getfieldname(type, result)] = value
855             return
856             
857         # we need to insert our value at the begining
858         values = [value]
859         
860         for suffix in [""]+range(2,99):
861             if type+str(suffix) in result:
862                 values.append(result[type+str(suffix)])
863             else:
864                 break
865                 
866         suffixes = [""]+range(2,len(values)+1)
867         
868         for l in range(len(suffixes)):
869             result[type+str(suffixes[l])] = values[l]
870
871             
872     def _field_CATEGORIES(self, field, value, result):
873         # comma seperated just for fun
874         values = self.splitandunquote(value, seperator=",")
875         values = [v.replace(";", "").strip() for v in values]  # semi colon is used as seperator in bitpim text field
876         values = [v for v in values if len(v)]
877         v = result.get('categories', None)
878         
879         if v:
880             result['categories'] = ';'.join([v, ";".join(values)])
881         
882         else:
883             result['categories'] = ';'.join(values)
884
885             
886     def _field_SOUND(self, field, value, result):
887         # comma seperated just for fun
888         values = self.splitandunquote(value, seperator=",")
889         values = [v.replace(";", "").strip() for v in values]  # semi colon is used as seperator in bitpim text field
890         values = [v for v in values if len(v)]
891         result[self._getfieldname("ringtones", result)] = ";".join(values)
892         
893         
894     _field_CATEGORY = _field_CATEGORIES  # apple use "category" which is not in the spec
895
896     
897     def _field_ADR(self, field, value, result):
898         # work out the type
899         preferred = False
900         type = "business"
901         
902         for f in field[1:]:
903             if f.startswith("TYPE="):
904                 ff = f[len("TYPE="):].split(",")
905                 
906             else: 
907                 ff = [f]
908                 
909             for x in ff:
910                 if x == "HOME":
911                     type = "home"
912                 if x == "PREF":
913                     preferred = True
914         
915         value = self.splitandunquote(value)
916         pobox = extendedaddress = streetaddress = locality = region = postalcode = country = None
917         try:
918             pobox = value[0]
919             extendedaddress = value[1]
920             streetaddress = value[2]
921             locality = value[3]
922             region = value[4]
923             postalcode = value[5]
924             country = value[6]
925         except IndexError:
926             pass
927         
928         addr = {}
929         
930         if pobox is not None and len(pobox):
931             addr["pobox"] = pobox
932         
933         if extendedaddress is not None and len(extendedaddress):
934             addr["street2"] = extendedaddress
935         
936         if streetaddress is not None and len(streetaddress):
937             addr["street"] = streetaddress
938         
939         if locality is not None and len(locality):
940             addr["city"] = locality
941         
942         if region is not None and len(region):
943             addr["state"] = region
944         
945         if postalcode is not None and len(postalcode):
946             addr["postalcode"] = postalcode
947         
948         if country is not None and len(country):
949             addr["country"] = country
950         
951         if len(addr):
952             addr["type"] = type
953             self._setvalue(result, "address", addr, preferred)
954
955             
956     def _field_X_PALM(self, field, value, result):
957         # handle a few PALM custom fields
958         ff = field[0].split(".")
959         f0 = ff[0]
960         
961         if len(ff) > 1:
962             f1 = ff[1]
963         else:
964             f1 = ''
965             
966         if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'):
967             self._field_CATEGORIES(['CATEGORIES'], value, result)
968         
969         elif f0 == 'X-PALM-NICKNAME' or f1 == 'X-PALM-NICKNAME':
970             self._field_NICKNAME(['NICKNAME'], value, result)
971         
972         else:
973             log.debug("Ignoring PALM custom field: %s" % field)
974         
975         
976     def _default_field(self, field, value, result):
977         ff = field[0].split(".")
978         f0 = ff[0]
979         
980         if len(ff) > 1:
981             f1 = ff[1]
982         else:
983             f1 = ''
984         
985         if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'):
986             self._field_X_PALM(field, value, result)
987             return
988         
989         elif f0.startswith("X-") or f1.startswith("X-"):
990             log.debug("Ignoring custom field: %s" % field)
991             return
992         
993         log.debug("No idea what to do with %s (%s)" % (field, value[:80]))
994         
995
996             
997     def unquote(self, value):
998         # ::TODO:: do this properly (deal with all backslashes)
999         return value.replace(r"\;", ";") \
1000                .replace(r"\,", ",") \
1001                .replace(r"\n", "\n") \
1002                .replace(r"\r\n", "\r\n") \
1003                .replace("\r\n", "\n") \
1004                .replace("\r", "\n")
1005
1006                
1007     def splitandunquote(self, value, seperator=";"):
1008         # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited
1009
1010         # short cut for normal case - no quoted seperators
1011         if value.find("\\"+seperator)<0:
1012             return [self.unquote(v) for v in value.split(seperator)]
1013
1014         # funky quoting, do it the slow hard way
1015         res = []
1016         build = ""
1017         v = 0
1018         while v < len(value):
1019             if value[v] == seperator:
1020                 res.append(build)
1021                 build = ""
1022                 v += 1
1023                 continue    
1024         
1025
1026             if value[v] == "\\":
1027                 build += value[v:v+2]
1028                 v += 2
1029                 continue
1030                 
1031             build += value[v]
1032             v += 1
1033         
1034         if len(build):
1035             res.append(build)
1036
1037         return [self.unquote(v) for v in res]
1038
1039         
1040     def version(self):
1041         "Best guess as to vcard version"
1042         return self._version
1043
1044         
1045     def origin(self):
1046         "Best guess as to what program wrote the vcard"
1047         return self._origin
1048         
1049         
1050     def __getitem__(self, item):
1051         return self._data[item]
1052         
1053     def __repr__(self):
1054         return repr(self._data)
1055
1056         
1057 # The formatters return a string
1058 def myqpencodestring(value):
1059     """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!"""
1060     return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A")
1061
1062     
1063 def format_stringv2(value):
1064     """Return a vCard v2 string.  Any embedded commas or semi-colons are removed."""
1065     return value.replace("\\", "").replace(",", "").replace(";", "")
1066
1067     
1068 def format_stringv3(value):
1069     """Return a vCard v3 string.  Embedded commas and semi-colons are backslash quoted"""
1070     return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;")
1071
1072     
1073 _string_formatters = (format_stringv2, format_stringv3)
1074
1075
1076 def format_binary(value):
1077     """Return base 64 encoded string"""
1078     # encodestring always adds a newline so we have to strip it off
1079     return base64.encodestring(value).rstrip()
1080
1081     
1082 def _is_sequence(v):
1083     """Determine if v is a sequence such as passed to value in out_line.
1084     Note that a sequence of chars is not a sequence for our purposes."""
1085     return isinstance(v, (type( () ), type([])))
1086
1087     
1088 def out_line(name, attributes, value, formatter, join_char=";"):
1089     """Returns a single field correctly formatted and encoded (including trailing newline)
1090
1091     @param name:  The field name
1092     @param attributes: A list of string attributes (eg "TYPE=intl,post" ).  Usually
1093                   empty except for TEL and ADR.  You can also pass in None.
1094     @param value: The field value.  You can also pass in a list of components which will be
1095                   joined with join_char such as the 6 components of N
1096     @param formatter:  The function that formats the value/components.  See the
1097                   various format_ functions.  They will automatically ensure that
1098                   ENCODING=foo attributes are added if appropriate"""
1099
1100     if attributes is None:     
1101         attributes = [] # ensure it is a list
1102     else: 
1103         attributes = list(attributes[:]) # ensure we work with a copy
1104
1105     if formatter in _string_formatters:
1106         if _is_sequence(value):
1107             qp = False
1108             for f in value:
1109                 f = formatter(f)
1110                 if myqpencodestring(f) != f:
1111                     qp = True
1112                     break
1113             
1114             if qp:
1115                 attributes.append("ENCODING=QUOTED-PRINTABLE")
1116                 value = [myqpencodestring(f) for f in value]
1117                 
1118             value = join_char.join(value)
1119         else:
1120             value = formatter(value)
1121             # do the qp test
1122             qp = myqpencodestring(value) != value
1123             if qp:
1124                 value = myqpencodestring(value)
1125                 attributes.append("ENCODING=QUOTED-PRINTABLE")
1126     else:
1127         assert not _is_sequence(value)
1128         if formatter is not None:
1129             value = formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types
1130
1131     res = ";".join([name]+attributes)+":"
1132     res += _line_reformat(value, 70, 70-len(res))
1133     assert res[-1] != "\n"
1134     
1135     return res+"\n"
1136
1137     
1138 def _line_reformat(line, width=70, firstlinewidth=0):
1139     """Takes line string and inserts newlines
1140     and spaces on following continuation lines
1141     so it all fits in width characters
1142
1143     @param width: how many characters to fit it in
1144     @param firstlinewidth: if >0 then first line is this width.
1145          if equal to zero then first line is same width as rest.
1146          if <0 then first line will go immediately to continuation.
1147          """
1148     if firstlinewidth == 0: 
1149         firstlinewidth = width
1150     
1151     if len(line) < firstlinewidth:
1152         return line
1153     
1154     res = ""
1155     
1156     if firstlinewidth > 0:
1157         res += line[:firstlinewidth]
1158         line = line[firstlinewidth:]
1159     
1160     while len(line):
1161         res += "\n "+line[:width]
1162         if len(line)<width: 
1163             break
1164             
1165         line = line[width:]
1166     
1167     return res
1168
1169 def out_names(vals, formatter, limit=1):
1170     res = ""
1171     for v in vals[:limit]:
1172         # full name
1173         res += out_line("FN", None, nameparser_formatsimplename(v), formatter)
1174         # name parts
1175         f,m,l = nameparser_getparts(v)
1176         res += out_line("N", None, (l,f,m,"",""), formatter)
1177         # nickname
1178         nn = v.get("nickname", "")
1179         
1180         if len(nn):
1181             res += out_line("NICKNAME", None, nn, formatter)
1182     
1183     return res
1184
1185 # Apple uses wrong field name so we do some futzing ...
1186 def out_categories(vals, formatter, field="CATEGORIES"):
1187     cats = [v.get("category") for v in vals]
1188     if len(cats):
1189         return out_line(field, None, cats, formatter, join_char=",")
1190     
1191     return ""
1192
1193     
1194 def out_categories_apple(vals, formatter):
1195     return out_categories(vals, formatter, field="CATEGORY")
1196
1197     
1198 # Used for both email and urls. we don't put any limits on how many are output
1199 def out_eu(vals, formatter, field, bpkey):
1200     res = ""
1201     first = True
1202     for v in vals:
1203         val = v.get(bpkey)
1204         type = v.get("type", "")
1205         
1206         if len(type):
1207             if type == "business": 
1208                 type = "work" # vcard uses different name
1209             
1210             type = type.upper()
1211             
1212             if first:
1213                 type = type+",PREF"
1214         
1215         elif first:
1216             type = "PREF"
1217         
1218         if len(type):
1219             type = ["TYPE="+type+["",",INTERNET"][field == "EMAIL"]] # email also has "INTERNET"
1220         else:
1221             type = None
1222             
1223         res += out_line(field, type, val, formatter)
1224         first = False
1225     
1226     return res
1227
1228     
1229 def out_emails(vals, formatter):
1230     return out_eu(vals, formatter, "EMAIL", "email")
1231
1232     
1233 def out_urls(vals, formatter):
1234     return out_eu(vals, formatter, "URL", "url")
1235
1236 _out_tel_mapping = { 
1237 'home': 'HOME',
1238 'office': 'WORK',
1239 'cell': 'CELL',
1240 'fax': 'FAX',
1241 'pager': 'PAGER',
1242 'data': 'MODEM',
1243 'none': 'VOICE'
1244 }
1245                    
1246                    
1247 def out_tel(vals, formatter):
1248     # ::TODO:: limit to one type of each number
1249     phones = ['phone'+str(x) for x in ['']+range(2,len(vals)+1)]
1250     res = ""
1251     first = True
1252     idx = 0
1253     
1254     for v in vals:
1255         sp = v.get('speeddial', None)
1256         
1257         if sp is None:
1258             # no speed dial
1259             res += out_line("TEL",
1260                           ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
1261                           phonenumber_format(v['number']), formatter)
1262         else:
1263             res += out_line(phones[idx]+".TEL",
1264                           ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
1265                           phonenumber_format(v['number']), formatter)
1266             res += out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter)
1267             idx += 1
1268         first = False
1269     
1270     return res
1271
1272     
1273 # and addresses
1274 def out_adr(vals, formatter):
1275     # ::TODO:: limit to one type of each address, and only one org
1276     res = ""
1277     first = True
1278     for v in vals:
1279         o = v.get("company", "")
1280         
1281         if len(o):
1282             res += out_line("ORG", None, o, formatter)
1283         
1284         if v.get("type") == "home": 
1285             type = "HOME"
1286         else: 
1287             type = "WORK"
1288             
1289         type = "TYPE="+type+("", ",PREF")[first]
1290         res += out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter)
1291         first = False
1292     
1293     return res
1294
1295     
1296 def out_note(vals, formatter, limit=1):
1297     return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
1298
1299     
1300 # Sany SCP-6600 (Katana) support
1301 def out_tel_scp6600(vals, formatter):
1302     res = ""
1303     _pref = len(vals) > 1
1304     
1305     if _pref:
1306         s = "PREF,"
1307     else:
1308         s = ''
1309     
1310     for v in vals:
1311         res += out_line("TEL", s,
1312                       ["TYPE=%s%s" % (s, _out_tel_mapping[v['type']])],
1313                       phonenumber_format(v['number']), formatter)
1314         
1315         _pref = False
1316         
1317     return res
1318     
1319     
1320 def out_email_scp6600(vals, formatter):
1321     res = ''
1322     for _idx in range(min(len(vals), 2)):
1323         v = vals[_idx]
1324         
1325         if v.get('email', None):
1326             res += out_line('EMAIL', ['TYPE=INTERNET'],
1327                           v['email'], formatter)
1328     
1329     return res
1330     
1331     
1332 def out_url_scp660(vals, formatter):
1333     if vals and vals[0].get('url', None):
1334         return out_line('URL', None, vals[0]['url'], formatter)
1335     return ''
1336     
1337     
1338 def out_adr_scp6600(vals, formatter):
1339     for v in vals:
1340         if v.get('type', None) == 'home':
1341             _type = 'HOME'
1342         else:
1343             _type = 'WORK'
1344         return out_line("ADR", ['TYPE=%s' % _type],
1345                         [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")],
1346                         formatter)
1347     return ''
1348
1349     
1350 # This is the order we write things out to the vcard.  Although
1351 # vCard doesn't require an ordering, it looks nicer if it
1352 # is (eg name first)
1353 _field_order = ("names", "wallpapers", "addresses", "numbers", "categories", 
1354                 "emails", "urls", "ringtones", "flags", "memos", "serials")
1355
1356
1357 def output_entry(entry, profile, limit_fields=None):
1358
1359     # debug build assertion that limit_fields only contains fields we know about
1360     if __debug__ and limit_fields is not None:
1361         assert len([f for f in limit_fields if f not in _field_order]) == 0
1362     
1363     fmt = profile["_formatter"]
1364     io = cStringIO.StringIO()
1365     io.write(out_line("BEGIN", None, "VCARD", None))
1366     io.write(out_line("VERSION", None, profile["_version"], None))
1367
1368     if limit_fields is None:
1369         fields = _field_order
1370     else:
1371         fields = [f for f in _field_order if f in limit_fields]
1372
1373     for f in fields:
1374         if f in entry and f in profile:
1375             func = profile[f]
1376             # does it have a limit?  (nice scary introspection :-)
1377             if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]:
1378                 lines = func(entry[f], fmt, limit = profile["_limit"])
1379             else:
1380                 lines = func(entry[f], fmt)
1381             if len(lines):
1382                 io.write(lines)
1383
1384     io.write(out_line("END", None, "VCARD", fmt))
1385     return io.getvalue()
1386
1387     
1388 profile_vcard2 = {
1389 '_formatter': format_stringv2,
1390 '_limit': 1,
1391 '_version': "2.1",
1392 'names': out_names,
1393 'categories': out_categories,
1394 'emails': out_emails,
1395 'urls': out_urls,
1396 'numbers': out_tel,
1397 'addresses': out_adr,
1398 'memos': out_note,
1399     }
1400
1401 profile_vcard3 = profile_vcard2.copy()
1402 profile_vcard3['_formatter'] = format_stringv3
1403 profile_vcard3['_version'] = "3.0"
1404
1405 profile_apple = profile_vcard3.copy()
1406 profile_apple['categories'] = out_categories_apple
1407
1408 profile_full = profile_vcard3.copy()
1409 profile_full['_limit'] = 99999
1410
1411 profile_scp6600 = profile_full.copy()
1412 del profile_scp6600['categories']
1413
1414 profile_scp6600.update(
1415 { 'numbers': out_tel_scp6600,
1416   'emails': out_email_scp6600,
1417   'urls': out_url_scp660,
1418   'addresses': out_adr_scp6600,
1419   })
1420
1421 profiles = {
1422 'vcard2':  { 'description': "vCard v2.1", 'profile': profile_vcard2 },
1423 'vcard3':  { 'description': "vCard v3.0", 'profile': profile_vcard3 },
1424 'apple':   { 'description': "Apple",      'profile': profile_apple  },
1425 'fullv3':  { 'description': "Full vCard v3.0", 'profile': profile_full},
1426 'scp6600': { 'description': "Sanyo SCP-6600 (Katana)",
1427              'profile': profile_scp6600 },
1428 }
1429
1430
1431