1 # -*- coding: utf-8 -*-
3 # (c) Copyright 2001-2008 Hewlett-Packard Development Company, L.P.
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.
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.
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
19 # ****************************************************************************
21 # Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
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.
26 # Code for reading and writing Vcard
28 # VCARD is defined in RFC 2425 and 2426
30 # Original author: Roger Binns <rogerb@rogerbinns.com>
31 # Modified for HPLIP by: Don Welch
51 import encodings.utf_64
52 _boms.append( (codecs.BOM64_BE, "utf_64") )
53 _boms.append( (codecs.BOM64_LE, "utf_64") )
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") )
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") )
74 import encodings.utf_8
75 _boms.append( (codecs.BOM_UTF8, "utf_8") )
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") )
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
88 _maxbomlen = max([len(bom) for bom,codec in _boms])
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
95 #with file(name, 'rb') as f:
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")
107 _notdigits = re.compile("[^0-9]*")
108 _tendigits = re.compile("^[0-9]{10}$")
109 _sevendigits = re.compile("^[0-9]{7}$")
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.
116 nums = "".join(re.split(_notdigits, n))
120 if len(nums) == 11 and nums[0] == "1":
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:])
133 def nameparser_formatsimplename(name):
134 "like L{formatname}, except we use the first matching component"
135 _fullname = nameparser_getfullname(name)
138 return name.get('nickname', "")
141 def nameparser_getfullname(name):
142 """Gets the full name, joining the first/middle/last if necessary"""
143 if name.has_key("full"):
145 return ' '.join([x for x in nameparser_getparts(name) if x])
148 # See the following references for name parsing and how little fun it
152 # http://cvs.gnome.org/lxr/source/evolution-data-server/addressbook/libebook/
156 # http://cvs.xemacs.org/viewcvs.cgi/XEmacs/packages/xemacs-packages/mail-lib/mail-extr.el
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.
168 lastparts = [ "van", "von", "de", "di" ]
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
173 def nameparser_getparts_FML(name):
185 return (parts[0], "", parts[1])
194 while len(parts) and (parts[-1][0].lower() == parts[-1][0] or parts[-1].lower() in lastparts):
201 return (" ".join(f), " ".join(m), " ".join(l))
204 def nameparser_getparts_LFM(name):
215 parts = parts[1].split()
221 _middle = ' '.join(parts[1:])
223 return (_first, _middle, _last)
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"""
230 # do we have any of the parts?
231 for i in ("first", "middle", "last"):
233 return (name.get("first", ""), name.get("middle", ""), name.get("last", ""))
235 # check we have full. if not return nickname
236 if not name.has_key("full"):
237 return (name.get("nickname", ""), "", "")
239 n = name.nameparser_get("full")
242 return nameparser_getparts_LFM(name)
244 return nameparser_getparts_FML(name)
249 class VFileException(Exception):
256 'MACINTOSH': 'MAC_ROMAN'
259 def __init__(self, source):
269 # Get the next non-blank line
270 while True: # python desperately needs do-while
271 line = self._getnextline()
274 raise StopIteration()
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(':')
284 s = line[:colon].lower().split(";")
286 if "quoted-printable" in s or 'encoding=quoted-printable' in s:
287 normalcontinuations = False
288 while line[-1] == "=" or line[-2] == '=':
294 nextl = self._getnextline()
295 if nextl[0] in ("\t", " "): nextl = nextl[1:]
296 line = line[:i]+nextl
298 while normalcontinuations:
299 nextline = self._lookahead()
304 if len(nextline) == 0:
307 if nextline[0] != ' ' and nextline[0] != '\t':
310 line += self._getnextline()[1:]
312 colon = line.find(':')
315 # some evolution vcards don't even have colons
316 # raise VFileException("Invalid property: "+line)
317 log.debug("Fixing up bad line: %s" % line)
323 line = line[colon+1:].strip()
325 # upper case and split on semicolons
326 items = b4.upper().split(";")
329 if isinstance(line, unicode):
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"
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
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)
354 elif i == 'ENCODING=B':
355 line = base64.decodestring(line)
359 raise VFileException("unknown encoding: "+i)
362 if isinstance(e,VFileException):
364 raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line))
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,))
372 if charset is not None:
374 decoder = codecs.getdecoder(self._charset_aliases.get(charset, charset))
375 line,_ = decoder(line)
377 raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4))
379 if newitems == ["BEGIN"] or newitems == ["END"]:
382 return newitems, line
385 def _getnextline(self):
386 if self.saved is not None:
391 return self._readandstripline()
394 def _readandstripline(self):
395 line = self.source.readline()
400 elif line[-2:] == "\r\n":
403 elif line[-1] == '\r' or line[-1] == '\n':
409 def _lookahead(self):
410 assert self.saved is None
411 self.saved = self._readandstripline()
417 "Understands vcards in a vfile"
420 def __init__(self, vfile):
431 for field,value in self.vfile:
432 if (field,value) != (["BEGIN"], "VCARD"):
438 if (field,value) != (["BEGIN"], "VCARD"):
439 # hit eof without any BEGIN:vcard
440 raise StopIteration()
444 for field,value in self.vfile:
445 if (field,value) != (["END"], "VCARD"):
446 lines.append( (field,value) )
451 if (field,value) != (["END"], "VCARD"):
452 raise VFileException("There is a BEGIN:VCARD but no END:VCARD")
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
468 # extract version field
472 if f == ["X-EVOLUTION-FILE-AS"]: # all evolution cards have this
473 self._origin = "evolution"
475 if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")):
476 self._origin = "apple"
478 if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >= 0:
479 self.origin = "apple"
484 ver = [int(xx) for xx in ver]
486 raise VFileException(v+" is not a valid vcard version")
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"]
496 elif f[0] == "HOME.LABEL":
497 f[0:1] = ["LABEL", "HOME"]
499 elif f[0] == "WORK.TEL":
500 f[0:1] = ["TEL", "WORK"]
502 elif f[0] == "WORK.LABEL":
503 f[0:1] = ["LABEL", "WORK"]
505 self.lines.append( (f,v) )
507 self._parse(self.lines, self._data)
508 self._update_groups(self._data)
512 "Returns a dict of the data parsed out of the vcard"
515 def get(self, key, default=''):
516 return self._data.get(key, default)
519 def _getfieldname(self, name, dict):
520 """Returns the fieldname to use in the dict.
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"""
527 for i in xrange(2,99999):
528 if name+`i` not in dict:
532 def _parse(self, lines, result):
533 for field,value in lines:
534 if len(value.strip()) == 0: # ignore blank values
538 f = field[0][field[0].find('.')+1:]
542 t = f.replace("-", "_")
543 func = getattr(self, "_field_"+t, self._default_field)
544 func(field, value, result)
547 def _update_groups(self, result):
548 """Update the groups info """
549 for k,e in self._groups.items():
550 self._setvalue(result, *e)
555 def _field_ignore(self, field, value, result):
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
573 def _field_FN(self, field, value, result):
574 result[self._getfieldname("name", result)] = self.unquote(value)
577 def _field_TITLE(self, field, value, result):
578 result[self._getfieldname("title", result)] = self.unquote(value)
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)
586 def _field_NOTE(self, field, value, result):
587 result[self._getfieldname("notes", result)] = self.unquote(value)
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)
598 def _field_N(self, field, value, result):
599 value = self.splitandunquote(value)
600 familyname = givenname = additionalnames = honorificprefixes = honorificsuffixes = None
602 familyname = value[0]
604 additionalnames = value[2]
605 honorificprefixes = value[3]
606 honorificsuffixes = value[4]
610 if familyname is not None and len(familyname):
611 result[self._getfieldname("last name", result)] = familyname
613 if givenname is not None and len(givenname):
614 result[self._getfieldname("first name", result)] = givenname
616 if additionalnames is not None and len(additionalnames):
617 result[self._getfieldname("middle name", result)] = additionalnames
619 if honorificprefixes is not None and len(honorificprefixes):
620 result[self._getfieldname("prefix", result)] = honorificprefixes
622 if honorificsuffixes is not None and len(honorificsuffixes):
623 result[self._getfieldname("suffix", result)] = honorificsuffixes
626 _field_NAME = _field_N # early versions of vcard did this
629 def _field_ORG(self, field, value, result):
630 value = self.splitandunquote(value)
632 result[self._getfieldname("organisation", result)] = value[0]
635 result[self._getfieldname("organisational unit", result)] = f
638 _field_O = _field_ORG # early versions of vcard did this
641 def _field_EMAIL(self, field, value, result):
642 value = self.unquote(value)
646 if f.startswith("TYPE="):
647 ff = f[len("TYPE="):].split(",")
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
664 return # we don't want no steenking X.400
666 preferred = "PREF" in types
669 self._setvalue(result, "email", value, preferred)
671 addr = {'email': value, 'type': type}
672 self._setvalue(result, "email", addr, preferred)
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)
682 if f.startswith("TYPE="):
683 ff = f[len("TYPE="):].split(",")
697 preferred = "PREF" in types
700 self._setvalue(result, "url", value, preferred)
702 addr = {'url': value, 'type': type}
703 self._setvalue(result, "url", addr, preferred)
706 def _field_X_SPEEDDIAL(self, field, value, result):
708 group = field[0][:field[0].find('.')]
712 # this has to belong to a group!!
713 #print 'speedial has no group'
714 log.debug("speeddial has no group")
716 self._setgroupvalue(result, 'phone', { 'speeddial': int(value) },
720 def _field_TEL(self, field, value, result):
721 value = self.unquote(value)
722 # see if this is part of a group
724 group = field[0][:field[0].find('.')]
732 if f.startswith("TYPE="):
733 ff = f[len("TYPE="):].split(",")
739 # type munging - we map vcard types to simpler ones
740 munge = { "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL",
743 types = [munge.get(t, t) for t in types]
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")]
749 # if type is in this list and voice not explicitly mentioned then it is not a voice type
750 antivoice = ["FAX", "PAGER", "DATA"]
756 voice = True # default is voice
763 preferred = "PREF" in types
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
768 # if neither work or home is specified, then no default (otherwise things get really complicated)
777 if len(types) == 0 or types == ["PREF"]:
778 iswork = True # special case when nothing else is specified
781 value = phonenumber_normalise(value)
783 self._setgroupvalue(result,
784 "phone", {"type": "business", "number": value},
788 self._setgroupvalue(result,
789 "phone", {"type": "home", "number": value},
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},
799 if iswork and "FAX" in types:
800 self._setgroupvalue(result, "phone",
801 {"type": "business fax", "number": value},
804 if ishome and "FAX" in types:
805 self._setgroupvalue(result, "phone",
806 {"type": "home fax", "number": value},
810 self._setgroupvalue(result,
811 "phone", {"type": "cell", "number": value},
815 self._setgroupvalue(result,
816 "phone", {"type": "pager", "number": value},
820 self._setgroupvalue(result,
821 "phone", {"type": "data", "number": value},
825 def _setgroupvalue(self, result, type, value, group, preferred=False):
826 """ Set value of an item of a group
829 # no groups specified
830 return self._setvalue(result, type, value, preferred)
832 group_type = self._groups.get(group, None)
834 if group_type is None:
835 # 1st one of the group
836 self._groups[group] = [type, value, preferred]
839 if type != group_type[0]:
840 log.debug('Group %s has different types: %s, %s' % (group, type,groups_type[0]))
845 group_type[1].update(value)
848 def _setvalue(self, result, type, value, preferred=False):
849 if type not in result:
854 result[self._getfieldname(type, result)] = value
857 # we need to insert our value at the begining
860 for suffix in [""]+range(2,99):
861 if type+str(suffix) in result:
862 values.append(result[type+str(suffix)])
866 suffixes = [""]+range(2,len(values)+1)
868 for l in range(len(suffixes)):
869 result[type+str(suffixes[l])] = values[l]
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)
880 result['categories'] = ';'.join([v, ";".join(values)])
883 result['categories'] = ';'.join(values)
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)
894 _field_CATEGORY = _field_CATEGORIES # apple use "category" which is not in the spec
897 def _field_ADR(self, field, value, result):
903 if f.startswith("TYPE="):
904 ff = f[len("TYPE="):].split(",")
915 value = self.splitandunquote(value)
916 pobox = extendedaddress = streetaddress = locality = region = postalcode = country = None
919 extendedaddress = value[1]
920 streetaddress = value[2]
923 postalcode = value[5]
930 if pobox is not None and len(pobox):
931 addr["pobox"] = pobox
933 if extendedaddress is not None and len(extendedaddress):
934 addr["street2"] = extendedaddress
936 if streetaddress is not None and len(streetaddress):
937 addr["street"] = streetaddress
939 if locality is not None and len(locality):
940 addr["city"] = locality
942 if region is not None and len(region):
943 addr["state"] = region
945 if postalcode is not None and len(postalcode):
946 addr["postalcode"] = postalcode
948 if country is not None and len(country):
949 addr["country"] = country
953 self._setvalue(result, "address", addr, preferred)
956 def _field_X_PALM(self, field, value, result):
957 # handle a few PALM custom fields
958 ff = field[0].split(".")
966 if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'):
967 self._field_CATEGORIES(['CATEGORIES'], value, result)
969 elif f0 == 'X-PALM-NICKNAME' or f1 == 'X-PALM-NICKNAME':
970 self._field_NICKNAME(['NICKNAME'], value, result)
973 log.debug("Ignoring PALM custom field: %s" % field)
976 def _default_field(self, field, value, result):
977 ff = field[0].split(".")
985 if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'):
986 self._field_X_PALM(field, value, result)
989 elif f0.startswith("X-") or f1.startswith("X-"):
990 log.debug("Ignoring custom field: %s" % field)
993 log.debug("No idea what to do with %s (%s)" % (field, value[:80]))
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")
1007 def splitandunquote(self, value, seperator=";"):
1008 # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited
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)]
1014 # funky quoting, do it the slow hard way
1018 while v < len(value):
1019 if value[v] == seperator:
1026 if value[v] == "\\":
1027 build += value[v:v+2]
1037 return [self.unquote(v) for v in res]
1041 "Best guess as to vcard version"
1042 return self._version
1046 "Best guess as to what program wrote the vcard"
1050 def __getitem__(self, item):
1051 return self._data[item]
1054 return repr(self._data)
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")
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(";", "")
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"\;")
1073 _string_formatters = (format_stringv2, format_stringv3)
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()
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([])))
1088 def out_line(name, attributes, value, formatter, join_char=";"):
1089 """Returns a single field correctly formatted and encoded (including trailing newline)
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"""
1100 if attributes is None:
1101 attributes = [] # ensure it is a list
1103 attributes = list(attributes[:]) # ensure we work with a copy
1105 if formatter in _string_formatters:
1106 if _is_sequence(value):
1110 if myqpencodestring(f) != f:
1115 attributes.append("ENCODING=QUOTED-PRINTABLE")
1116 value = [myqpencodestring(f) for f in value]
1118 value = join_char.join(value)
1120 value = formatter(value)
1122 qp = myqpencodestring(value) != value
1124 value = myqpencodestring(value)
1125 attributes.append("ENCODING=QUOTED-PRINTABLE")
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
1131 res = ";".join([name]+attributes)+":"
1132 res += _line_reformat(value, 70, 70-len(res))
1133 assert res[-1] != "\n"
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
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.
1148 if firstlinewidth == 0:
1149 firstlinewidth = width
1151 if len(line) < firstlinewidth:
1156 if firstlinewidth > 0:
1157 res += line[:firstlinewidth]
1158 line = line[firstlinewidth:]
1161 res += "\n "+line[:width]
1169 def out_names(vals, formatter, limit=1):
1171 for v in vals[:limit]:
1173 res += out_line("FN", None, nameparser_formatsimplename(v), formatter)
1175 f,m,l = nameparser_getparts(v)
1176 res += out_line("N", None, (l,f,m,"",""), formatter)
1178 nn = v.get("nickname", "")
1181 res += out_line("NICKNAME", None, nn, formatter)
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]
1189 return out_line(field, None, cats, formatter, join_char=",")
1194 def out_categories_apple(vals, formatter):
1195 return out_categories(vals, formatter, field="CATEGORY")
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):
1204 type = v.get("type", "")
1207 if type == "business":
1208 type = "work" # vcard uses different name
1219 type = ["TYPE="+type+["",",INTERNET"][field == "EMAIL"]] # email also has "INTERNET"
1223 res += out_line(field, type, val, formatter)
1229 def out_emails(vals, formatter):
1230 return out_eu(vals, formatter, "EMAIL", "email")
1233 def out_urls(vals, formatter):
1234 return out_eu(vals, formatter, "URL", "url")
1236 _out_tel_mapping = {
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)]
1255 sp = v.get('speeddial', None)
1259 res += out_line("TEL",
1260 ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
1261 phonenumber_format(v['number']), formatter)
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)
1274 def out_adr(vals, formatter):
1275 # ::TODO:: limit to one type of each address, and only one org
1279 o = v.get("company", "")
1282 res += out_line("ORG", None, o, formatter)
1284 if v.get("type") == "home":
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)
1296 def out_note(vals, formatter, limit=1):
1297 return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
1300 # Sany SCP-6600 (Katana) support
1301 def out_tel_scp6600(vals, formatter):
1303 _pref = len(vals) > 1
1311 res += out_line("TEL", s,
1312 ["TYPE=%s%s" % (s, _out_tel_mapping[v['type']])],
1313 phonenumber_format(v['number']), formatter)
1320 def out_email_scp6600(vals, formatter):
1322 for _idx in range(min(len(vals), 2)):
1325 if v.get('email', None):
1326 res += out_line('EMAIL', ['TYPE=INTERNET'],
1327 v['email'], formatter)
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)
1338 def out_adr_scp6600(vals, formatter):
1340 if v.get('type', None) == 'home':
1344 return out_line("ADR", ['TYPE=%s' % _type],
1345 [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")],
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")
1357 def output_entry(entry, profile, limit_fields=None):
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
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))
1368 if limit_fields is None:
1369 fields = _field_order
1371 fields = [f for f in _field_order if f in limit_fields]
1374 if f in entry and f in profile:
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"])
1380 lines = func(entry[f], fmt)
1384 io.write(out_line("END", None, "VCARD", fmt))
1385 return io.getvalue()
1389 '_formatter': format_stringv2,
1393 'categories': out_categories,
1394 'emails': out_emails,
1397 'addresses': out_adr,
1401 profile_vcard3 = profile_vcard2.copy()
1402 profile_vcard3['_formatter'] = format_stringv3
1403 profile_vcard3['_version'] = "3.0"
1405 profile_apple = profile_vcard3.copy()
1406 profile_apple['categories'] = out_categories_apple
1408 profile_full = profile_vcard3.copy()
1409 profile_full['_limit'] = 99999
1411 profile_scp6600 = profile_full.copy()
1412 del profile_scp6600['categories']
1414 profile_scp6600.update(
1415 { 'numbers': out_tel_scp6600,
1416 'emails': out_email_scp6600,
1417 'urls': out_url_scp660,
1418 'addresses': out_adr_scp6600,
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 },