You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

372 lines
11 KiB

  1. """Definitions and behavior for vCard 3.0"""
  2. import codecs
  3. from . import behavior
  4. from .base import ContentLine, registerBehavior, backslashEscape, basestring, str_
  5. from .icalendar import stringToTextValues
  6. # ------------------------ vCard structs ---------------------------------------
  7. class Name(object):
  8. def __init__(self, family='', given='', additional='', prefix='',
  9. suffix=''):
  10. """
  11. Each name attribute can be a string or a list of strings.
  12. """
  13. self.family = family
  14. self.given = given
  15. self.additional = additional
  16. self.prefix = prefix
  17. self.suffix = suffix
  18. @staticmethod
  19. def toString(val):
  20. """
  21. Turn a string or array value into a string.
  22. """
  23. if type(val) in (list, tuple):
  24. return ' '.join(val)
  25. return val
  26. def __str__(self):
  27. eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
  28. out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
  29. return str_(out)
  30. def __repr__(self):
  31. return "<Name: {0!s}>".format(self.__str__())
  32. def __eq__(self, other):
  33. try:
  34. return (self.family == other.family and
  35. self.given == other.given and
  36. self.additional == other.additional and
  37. self.prefix == other.prefix and
  38. self.suffix == other.suffix)
  39. except:
  40. return False
  41. class Address(object):
  42. def __init__(self, street='', city='', region='', code='',
  43. country='', box='', extended=''):
  44. """
  45. Each name attribute can be a string or a list of strings.
  46. """
  47. self.box = box
  48. self.extended = extended
  49. self.street = street
  50. self.city = city
  51. self.region = region
  52. self.code = code
  53. self.country = country
  54. @staticmethod
  55. def toString(val, join_char='\n'):
  56. """
  57. Turn a string or array value into a string.
  58. """
  59. if type(val) in (list, tuple):
  60. return join_char.join(val)
  61. return val
  62. lines = ('box', 'extended', 'street')
  63. one_line = ('city', 'region', 'code')
  64. def __str__(self):
  65. lines = '\n'.join(self.toString(getattr(self, val))
  66. for val in self.lines if getattr(self, val))
  67. one_line = tuple(self.toString(getattr(self, val), ' ')
  68. for val in self.one_line)
  69. lines += "\n{0!s}, {1!s} {2!s}".format(*one_line)
  70. if self.country:
  71. lines += '\n' + self.toString(self.country)
  72. return lines
  73. def __repr__(self):
  74. return "<Address: {0!s}>".format(self)
  75. def __eq__(self, other):
  76. try:
  77. return (self.box == other.box and
  78. self.extended == other.extended and
  79. self.street == other.street and
  80. self.city == other.city and
  81. self.region == other.region and
  82. self.code == other.code and
  83. self.country == other.country)
  84. except:
  85. return False
  86. # ------------------------ Registered Behavior subclasses ----------------------
  87. class VCardTextBehavior(behavior.Behavior):
  88. """
  89. Provide backslash escape encoding/decoding for single valued properties.
  90. TextBehavior also deals with base64 encoding if the ENCODING parameter is
  91. explicitly set to BASE64.
  92. """
  93. allowGroup = True
  94. base64string = 'B'
  95. @classmethod
  96. def decode(cls, line):
  97. """
  98. Remove backslash escaping from line.valueDecode line, either to remove
  99. backslash espacing, or to decode base64 encoding. The content line should
  100. contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
  101. export a singleton parameter of 'BASE64', which does not match the 3.0
  102. vCard spec. If we encouter that, then we transform the parameter to
  103. ENCODING=b
  104. """
  105. if line.encoded:
  106. if 'BASE64' in line.singletonparams:
  107. line.singletonparams.remove('BASE64')
  108. line.encoding_param = cls.base64string
  109. encoding = getattr(line, 'encoding_param', None)
  110. if encoding:
  111. if isinstance(line.value, bytes):
  112. line.value = codecs.decode(line.value, "base64")
  113. else:
  114. line.value = codecs.decode(line.value.encode("utf-8"), "base64")
  115. else:
  116. line.value = stringToTextValues(line.value)[0]
  117. line.encoded = False
  118. @classmethod
  119. def encode(cls, line):
  120. """
  121. Backslash escape line.value.
  122. """
  123. if not line.encoded:
  124. encoding = getattr(line, 'encoding_param', None)
  125. if encoding and encoding.upper() == cls.base64string:
  126. if isinstance(line.value, bytes):
  127. line.value = codecs.encode(line.value, "base64").decode("utf-8").replace('\n', '')
  128. else:
  129. line.value = codecs.encode(line.value.encode(encoding), "base64").decode("utf-8")
  130. else:
  131. line.value = backslashEscape(line.value)
  132. line.encoded = True
  133. class VCardBehavior(behavior.Behavior):
  134. allowGroup = True
  135. defaultBehavior = VCardTextBehavior
  136. class VCard3_0(VCardBehavior):
  137. """
  138. vCard 3.0 behavior.
  139. """
  140. name = 'VCARD'
  141. description = 'vCard 3.0, defined in rfc2426'
  142. versionString = '3.0'
  143. isComponent = True
  144. sortFirst = ('version', 'prodid', 'uid')
  145. knownChildren = {
  146. 'N': (0, 1, None), # min, max, behaviorRegistry id
  147. 'FN': (1, None, None),
  148. 'VERSION': (1, 1, None), # required, auto-generated
  149. 'PRODID': (0, 1, None),
  150. 'LABEL': (0, None, None),
  151. 'UID': (0, None, None),
  152. 'ADR': (0, None, None),
  153. 'ORG': (0, None, None),
  154. 'PHOTO': (0, None, None),
  155. 'CATEGORIES': (0, None, None),
  156. 'GEO': (0, None, None)
  157. }
  158. @classmethod
  159. def generateImplicitParameters(cls, obj):
  160. """
  161. Create PRODID, VERSION, and VTIMEZONEs if needed.
  162. VTIMEZONEs will need to exist whenever TZID parameters exist or when
  163. datetimes with tzinfo exist.
  164. """
  165. if not hasattr(obj, 'version'):
  166. obj.add(ContentLine('VERSION', [], cls.versionString))
  167. registerBehavior(VCard3_0, default=True)
  168. class FN(VCardTextBehavior):
  169. name = "FN"
  170. description = 'Formatted name'
  171. registerBehavior(FN)
  172. class Label(VCardTextBehavior):
  173. name = "Label"
  174. description = 'Formatted address'
  175. registerBehavior(Label)
  176. class GEO(VCardBehavior):
  177. name = "GEO"
  178. description = "Geographical location"
  179. registerBehavior(GEO)
  180. wacky_apple_photo_serialize = True
  181. REALLY_LARGE = 1E50
  182. class Photo(VCardTextBehavior):
  183. name = "Photo"
  184. description = 'Photograph'
  185. @classmethod
  186. def valueRepr(cls, line):
  187. return " (BINARY PHOTO DATA at 0x{0!s}) ".format(id(line.value))
  188. @classmethod
  189. def serialize(cls, obj, buf, lineLength, validate, *args, **kwargs):
  190. """
  191. Apple's Address Book is *really* weird with images, it expects
  192. base64 data to have very specific whitespace. It seems Address Book
  193. can handle PHOTO if it's not wrapped, so don't wrap it.
  194. """
  195. if wacky_apple_photo_serialize:
  196. lineLength = REALLY_LARGE
  197. VCardTextBehavior.serialize(obj, buf, lineLength, validate, *args, **kwargs)
  198. registerBehavior(Photo)
  199. def toListOrString(string):
  200. stringList = stringToTextValues(string)
  201. if len(stringList) == 1:
  202. return stringList[0]
  203. else:
  204. return stringList
  205. def splitFields(string):
  206. """
  207. Return a list of strings or lists from a Name or Address.
  208. """
  209. return [toListOrString(i) for i in
  210. stringToTextValues(string, listSeparator=';', charList=';')]
  211. def toList(stringOrList):
  212. if isinstance(stringOrList, basestring):
  213. return [stringOrList]
  214. return stringOrList
  215. def serializeFields(obj, order=None):
  216. """
  217. Turn an object's fields into a ';' and ',' seperated string.
  218. If order is None, obj should be a list, backslash escape each field and
  219. return a ';' separated string.
  220. """
  221. fields = []
  222. if order is None:
  223. fields = [backslashEscape(val) for val in obj]
  224. else:
  225. for field in order:
  226. escapedValueList = [backslashEscape(val) for val in
  227. toList(getattr(obj, field))]
  228. fields.append(','.join(escapedValueList))
  229. return ';'.join(fields)
  230. NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
  231. ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code',
  232. 'country')
  233. class NameBehavior(VCardBehavior):
  234. """
  235. A structured name.
  236. """
  237. hasNative = True
  238. @staticmethod
  239. def transformToNative(obj):
  240. """
  241. Turn obj.value into a Name.
  242. """
  243. if obj.isNative:
  244. return obj
  245. obj.isNative = True
  246. obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value))))
  247. return obj
  248. @staticmethod
  249. def transformFromNative(obj):
  250. """
  251. Replace the Name in obj.value with a string.
  252. """
  253. obj.isNative = False
  254. obj.value = serializeFields(obj.value, NAME_ORDER)
  255. return obj
  256. registerBehavior(NameBehavior, 'N')
  257. class AddressBehavior(VCardBehavior):
  258. """
  259. A structured address.
  260. """
  261. hasNative = True
  262. @staticmethod
  263. def transformToNative(obj):
  264. """
  265. Turn obj.value into an Address.
  266. """
  267. if obj.isNative:
  268. return obj
  269. obj.isNative = True
  270. obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
  271. return obj
  272. @staticmethod
  273. def transformFromNative(obj):
  274. """
  275. Replace the Address in obj.value with a string.
  276. """
  277. obj.isNative = False
  278. obj.value = serializeFields(obj.value, ADDRESS_ORDER)
  279. return obj
  280. registerBehavior(AddressBehavior, 'ADR')
  281. class OrgBehavior(VCardBehavior):
  282. """
  283. A list of organization values and sub-organization values.
  284. """
  285. hasNative = True
  286. @staticmethod
  287. def transformToNative(obj):
  288. """
  289. Turn obj.value into a list.
  290. """
  291. if obj.isNative:
  292. return obj
  293. obj.isNative = True
  294. obj.value = splitFields(obj.value)
  295. return obj
  296. @staticmethod
  297. def transformFromNative(obj):
  298. """
  299. Replace the list in obj.value with a string.
  300. """
  301. if not obj.isNative:
  302. return obj
  303. obj.isNative = False
  304. obj.value = serializeFields(obj.value)
  305. return obj
  306. registerBehavior(OrgBehavior, 'ORG')