#!/usr/bin/python """ vCard 3.0 Implementation as described in RFC 2426 """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: vcard.py 94 2004-12-06 21:10:31Z henning $ import re, time from types import * import string import debug from vcore import * # For py2exe ModuleFinder: import codecs from encodings import utf_8 FIELDNAMES = [ "FormattedName", "DisplayName", # virtual (does not map to a real vCard field) "Family", "Given", "Additional", "Prefixes", "Suffixes", "NickName", "Birthday", "Organization", "Units", "Title", "Role", "Phone", "Email", "Mailer", "POBox", "Extended", "Street", "PostalCode", "City", "Region", "Country", "Label", "Note", "Categories", "SortName", # virtual "SortString", "URL", "Key", "TimeZone", "GlobalPosition", "Photo", "Logo", "Rev", "UID"] ADRFIELDS = ["POBox", "Extended", "Street", "PostalCode", "City", "Region", "Country"] FIELDNAME2ATTR = {"FormattedName":"fn", "DisplayName":"getDisplayName", "Family":"n.family", "Given":"n.given", "Additional":"n.additional", "Prefixes":"n.prefixes", "Suffixes":"n.suffixes", "NickName":"nickname", "Birthday":"bday", "Organization":"org.org", "Units":"org.units", "Title":"title", "Role":"role", "Phone":"tel", "Email":"email", "Mailer":"mailer", "POBox":"adr.pobox", "Extended":"adr.extended", "Street":"adr.street", "City":"adr.city", "Region":"adr.region", "PostalCode":"adr.postcode", "Country":"adr.country", "Label":"label", "Note":"note", "Categories":"categories", "SortName":"getSortName", "SortString":"sort_string", "URL":"url", "Key":"key", "TimeZone":"tz", "GlobalPosition":"geo", "Photo":"photo", "Logo":"logo", "Rev":"rev", "UID":"uid" } vC_adr_types = ["pref", "home", "work", "intl", "postal", "parcel", "dom"] # DOMestic is currently not supported in PyCoCuMa vC_adr_types_default = ["intl", "postal", "parcel", "work"] vC_tel_types = ["pref", "home", "work", "voice", "cell", "pager", "car", "fax", "modem", "msg", "isdn", "video", "bbs", "pcs"] # BBS and PCS are currently not supported in PyCoCuMa vC_tel_types_default = ["voice"] vC_email_types = ["pref", "internet", "x400"] # These will not display in PyCoCuMa vC_email_types_default = ["internet"] SUPPORTED_VERSIONS = ["2.1", "3.0"] class vC_n(vC_AbstractCompoundValue): "vCard Name record" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params parts = VALUE_DELIM_RE.split(value) self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes = gettuple(map(vC_value, parts), 5, vC_value) def is_empty(self): return False def getFamilyGiven(self): fam = self.family.get() giv = self.given.get() if fam and giv: return fam + ", " + giv else: return fam or giv or "" def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes])) return ret def XML_repr(self): ret = "%s%s%s%s" ret = ret + "%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.family,\ self.given,\ self.additional,\ self.prefixes,\ self.suffixes])) return ret class vC_tel(vC_text): "Single Telephone Entry" def __init__(self, value="", params=None): vC_text.__init__(self, value, params) import Set # Set default TEL-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_tel_types_default) self.params.set("type", defaultset) def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") class vC_email(vC_text): "Single Email Address" def __init__(self, value="", params=None): vC_text.__init__(self, value, params) import Set # Set default EMAIL-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_email_types_default) self.params.set("type", defaultset) def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") class vC_adr(vC_AbstractCompoundValue): "Single Adress Record" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params import Set # Set default ADR-Type if not defined: if self.params.get("type") is None: defaultset = Set.Set() defaultset.extend(vC_adr_types_default) self.params.set("type", defaultset) parts = VALUE_DELIM_RE.split(value) self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country = gettuple(map(vC_value, parts), 7, vC_value) def is_empty(self): return False def is_pref(self): "Is this Entry preferred?" return "pref" in self.params.get("type") def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country])) return ret def XML_repr(self): ret = "%s%s%s" ret = ret + "%s%s%s" ret = ret + "%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.pobox,\ self.extended,\ self.street,\ self.city,\ self.region,\ self.postcode,\ self.country])) return ret+self.params.XML_repr() class vC_orgunits(vC_AbstractBaseValue): "Contains List of Organizational Units" def __init__(self): self.list = [] def get(self): return ";".join(self.list) def set(self, value): if type(value) is ListType: self.list = value else: self.list = map(string.strip, value.split(";")) def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.list)) def is_empty(self): return self.list == [] def VCF_repr(self): return ";".join(map(escape, self.list)) def XML_repr(self): ret = "" for unit in self.list: ret = ret + "%s" % escapexml(unit) return ret+'' class vC_org(vC_AbstractCompoundValue): "Organizational Information" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params parts = VALUE_DELIM_RE.split(value) self.org = vC_value(getitem(parts, 0, "")) self.units = vC_orgunits() if len(parts) > 1: self.units.list = map(deescape, filter(isNonEmptyLine, parts[1:])) def is_empty(self): return self.org.get() == "" and self.units.is_empty() def VCF_repr(self): ret = self.params.VCF_repr() + ":" ret = ret + ";".join(map(getvcfrepr, [self.org, self.units])) return ret def XML_repr(self): ret = "%s%s" ret = ret % tuple(map(lambda x: x.XML_repr(), [self.org, self.units])) return ret # Stores information about last call to getFieldValue(): # _lastreturnedfield = "%d_%s" % (vcard.handle(), fieldname) _lastreturnedfield = "" _lastreturnedvalueidx = 0 class vCard: """vCard 3.0 Implementation as described in RFC 2426""" def _name2attr(self, name): return name.lower().replace("-", "_") def _attr2name(self, attr): return attr.upper().replace("_", "-") def __init__(self, block_of_lines=[]): self.__dict__["_handle"] = None # default values: self.__dict__["data"] = { "version": vC_text("3.0"), "n" : vC_n(), "fn" : vC_text(), "nickname": vC_text(), "bday" : vC_datetime(), "tel" : [], "adr" : [], "label" : vC_text(), "email" : [], "mailer": vC_text(), "org" : vC_org(), "title" : vC_text(), "role" : vC_text(), "note" : vC_text(), "categories" : vC_categories(), "sort_string" : vC_text(), "url" : vC_text(), "key" : vC_text(), "rev" : vC_datetime(time.gmtime()), "uid" : vC_text(), # Geographical Types: "tz" : vC_text(), "geo" : vC_geo(), # Photographic Picture: "photo" : vC_text(), # company logo image: "logo" : vC_text()} # Name to function mapping: self.__dict__["name_func"] = { "N" : vC_n, "FN" : vC_text, "NICKNAME": vC_text, "BDAY" : vC_datetime, "TEL" : vC_tel, "ADR" : vC_adr, "LABEL" : vC_text, "EMAIL" : vC_email, "MAILER": vC_text, "ORG" : vC_org, "TITLE" : vC_text, "ROLE" : vC_text, "NOTE" : vC_text, "CATEGORIES" : vC_categories, "SORT-STRING" : vC_text, "URL" : vC_text, "KEY" : vC_text, "REV" : vC_datetime, "UID" : vC_text, "TZ" : vC_text, "GEO" : vC_geo, "PHOTO" : vC_text, "LOGO" : vC_text} if type(block_of_lines) != ListType: # Input was a string? # Delete empty lines: block_of_lines = filter(isNonEmptyLine, block_of_lines.split('\n')) # de-fold: makeloglines(block_of_lines) block_of_lines = map(vC_contentline, block_of_lines) for line in block_of_lines: if line.name == "VERSION" \ and not line.value in SUPPORTED_VERSIONS : debug.echo("WARNING: Unsupported vCard version (should be \"3.0\" or \"2.1\").") self._insertline(line) global _lastreturnedvalueidx _lastreturnedvalueidx = 0 def _insertline(self, line): if self.name_func.has_key(line.name): attr = self._name2attr(line.name) if hasattr(self, attr) and type(self.__getattr__(attr)) == ListType: # Multi-Value (adr, tel and email): self.__getattr__(attr).append(\ self.name_func[line.name](line.value, line.params)) else: # Single-Value: self.__setattr__(attr,\ self.name_func[line.name](line.value, line.params)) def __getattr__(self, name): try: return self.data[name] except: raise AttributeError, "No Attribute '%s'" % (name) def __setattr__(self, name, value): try: self.data[name] = value except: raise AttributeError, "No Attribute '%s'" % (name) def __repr__(self): ret = "<%s: " % self.__class__ for key, value in zip(self.data.keys(), self.data.values()): ret = ret + "%s: %s\n" % (repr(key), repr(value)) return ret+">" def handle(self): return self.__dict__["_handle"] def sethandle(self, handle): "Handle can be set only once in a lifetime!" if self.__dict__["_handle"] == None: self.__dict__["_handle"] = handle def getSortName(self): "usually the same as getDisplayName, differs when sort-string is set" return self.sort_string.get() or\ self.n.getFamilyGiven() or\ self.fn.get() def getDisplayName(self): "Get DisplayName (this is: 'Family, Given' or the FormattedName)" return self.n.getFamilyGiven() or\ self.fn.get() def getFieldValue(self, field_and_idx): """Returns content of field as vC_value field_and_idx => e.g. 'Street 1' return => vC_value or None On multiple calls it returns further items from value array (Call this functions until it returns None)""" global _lastreturnedfield, _lastreturnedvalueidx def lastretfieldstr(field, self=self): handle = self.handle() if handle is None: return "None_%s" % (field) else: return "%d_%s" % (handle, field) parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: if lastretfieldstr(field) == _lastreturnedfield: idx = _lastreturnedvalueidx + 1 else: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: value = None elif idx >0: value = None else: value = attrobj _lastreturnedvalueidx = idx _lastreturnedfield = lastretfieldstr(field) return value def getFieldValueStr(self, field_and_idx, default=""): """Returns content of field as string field_and_idx => e.g. 'Street 1' return => e.g. 'Fifth-Avenue 89'""" val = self.getFieldValue(field_and_idx) if val: return flattenattr(val) else: # the field does not exist # (NOTE: this does not mean empty fields!): return default def setFieldValueStr(self, field_and_idx, val): "Set Field Value" parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: # XXX: This is ugly hard-coded! if field in ADRFIELDS: self.__getattr__('adr').append(self.name_func['ADR']()) elif field == 'Phone': self.__getattr__('tel').append(self.name_func['TEL']()) elif field == 'Email': self.__getattr__('email').append(self.name_func['EMAIL']()) else: raise ValueError attrobj = getsubattr(self, attr) value = attrobj[-1] elif idx >0: raise ValueError else: value = attrobj value.set(val) def VCF_repr(self): "Native vCard Representation" ret = "BEGIN:VCARD\n" for name, val in self.data.items(): if type(val) == ListType: for itm in val: ret = ret + log2phylines(self._attr2name(name) + itm.VCF_repr() + "\n") elif val != None: if not val.is_empty(): ret = ret + log2phylines(self._attr2name(name) + val.VCF_repr() + "\n") return ret + "END:VCARD\n" def XML_repr(self): "XML Representation" ret = '\n' % escapexml(self.uid.get()) for name, val in self.data.items(): if name != 'version': # We don't need to specify the Version with XML! if type(val) == ListType: if len(val) > 1: ret = ret + "" % self._attr2name(name) ret = ret + '' for itm in val: ret = ret + '' + itm.XML_repr() + '' ret = ret + '' ret = ret + "\n" % self._attr2name(name) elif len(val) > 0: ret = ret + "" % self._attr2name(name) ret = ret + val[0].XML_repr() ret = ret + "\n" % self._attr2name(name) elif val != None: if not val.is_empty(): ret = ret + "" % self._attr2name(name) ret = ret + val.XML_repr() ret = ret + "\n" % self._attr2name(name) return ret + "\n" class vCardList: """List of vCards""" def __init__(self): self.handlecounter = 0 self.data = {} def sortedlist(self, sortby=""): "Return list of card handles" if not sortby: sortby = "SortName" cards = self.data.values() handles = self.data.keys() def getfieldvaluestr(card, field=sortby): return card.getFieldValueStr(field) decorated = zip(map(getfieldvaluestr, cards), handles) decorated.sort() self.forgetLastReturnedField() return [ handle for sortstr, handle in decorated ] def LoadFromFile(self, fname): "Load from *.vcf file" try: fd = open(fname, "rb") self.LoadFromStream(fd) fd.close() except: # Loading failed return False return True def LoadFromStream(self, stream, encoding='utf8'): "Load from any text stream with file-like methods" vCardBlock = None lines = stream.readlines() makeloglines(lines) for line in filter(isNonEmptyLine, lines): if type(line) != UnicodeType: line = unicode(line, encoding, 'replace') line = vC_contentline(line) if line.name == "BEGIN" \ and line.value.upper() == "VCARD": vCardName = "default" vCardBlock = [] if vCardBlock == None: debug.echo("ERROR: Missing 'BEGIN:VCARD' ?") else: vCardBlock.append(line) if line.name == "END" \ and line.value.upper() == "VCARD": self.add(vCard(vCardBlock)) def SaveToFile(self, fname): "Save to *.vcf file" fd = open(fname, "wb") self.SaveToStream(fd) fd.close() def SaveToStream(self, stream, encoding='utf8'): "Save to Stream (FileDescriptor)" stream.write(self.VCF_repr().encode(encoding, 'replace')) def __str__(self): return str(zip(self.data.keys(), map(str,self.data.values()))) def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def clear(self): "Removes all vCards" self.data.clear() def add(self, card=None): "Add new vCard to list" if card == None: card = vCard() self.handlecounter += 1 newhandle = self.handlecounter # Handle must not have been set before: card.sethandle(newhandle) self.data[newhandle] = card return newhandle def delete(self, handle): "Removes vCard with handle from list" if self.data.has_key(handle): del self.data[handle] return True else: return False def forgetLastReturnedField(self): "Reset the lastreturnedfield variable" global _lastreturnedfield, _lastreturnedvalueidx _lastreturnedfield = "" _lastreturnedvalueidx = 0 def VCF_repr(self): "Representation for file output" ret = "" for card in self.data.values(): ret = ret + card.VCF_repr() + "\n" # add blank line return ret def XML_repr(self): ret = '\n' ret = ret + '' for card in self.data.values(): ret = ret + card.XML_repr() + "\n" # add blank line return ret + '\n'