#!/usr/bin/python """ vCard 3.0 and iCalendar 2.0 Core Types and Functions as described in RFC 2425 (A MIME Content-Type for Directory Information) """ # 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: vcore.py 82 2004-07-11 13:01:44Z henning $ import re from types import * import debug VALUE_DELIM_RE = re.compile(r"(?", ">") def quoteParamValue(str): # Has to be quoted? # Check for non-safe chars: if str.find('=') != -1 \ or str.find(';') != -1\ or str.find(':') != -1\ or str.find(',') != -1: return "\""+str+"\"" else: return str def chop_line(line): "Truncate line breaks" while len(line) and line[-1] in ["\r","\n"]: line=line[:-1] return line def isNonEmptyLine(line): """are there characters other than space? used especially with filter() """ line = chop_line(line) if line: ret = not line.isspace() else: ret = False return ret def makeloglines(physical_lines): "Line De-Folding (RFC 2425): turn physical lines into logical ones" i = 0 while i < len(physical_lines): if i > 0 and len(physical_lines[i]) >= 2: # Line is continued if next phy. line starts with # single space or tab: if physical_lines[i][0] in " \t": physical_lines[i-1] =\ chop_line(physical_lines[i-1]) + physical_lines[i][1:] del physical_lines[i] else: i += 1 else: i += 1 def log2phylines(logical_line): "Fold(break) Lines longer than 75-Chars" # Max-Length of physical Line: i = 75 result = logical_line while True: if i < len(logical_line): # Break with CR and start next line with single space character: result = result[:i] + "\n " + result[i:] else: break i += 76 return result def getitem(seq, idx, default=None): "Return List Item or default if idx is out of range" if len(seq) > idx: return seq[idx] else: return default def gettuple(seq, length, default=None): "Fills seq up to length with default and return tuple" if len(seq) < length: if callable(default): ret = seq for i in range(length - len(seq)): ret.append(default()) return tuple(ret) else: return tuple(seq) + tuple(((length - len(seq))*[default])) else: return tuple(seq) class vC_params: "Parameters used by all vCard/vCalendar records" from Set import Set def __init__(self, text=""): self.dict = {} self.parse(text) def parse(self, text): params = VALUE_DELIM_RE.split(text) for param in params: matchobj = PARAM_RE.match(param) if matchobj: param_name = matchobj.group(1).lower() param_value = matchobj.group(2) if param_value != "": # Compat: Version 2 of vCard had no TYPE param: if param_name == "": param_name = "type" # Is the Value a quoted String? (DQUOTE str DQUOTE) if param_value[0] == "\"" and param_value[-1] == "\"" \ and len(param_value) > 1: param_value = param_value[1:-1] # DeQuote self.dict.setdefault(param_name, self.Set()).extend(param_value.split(",")) def get(self, param_name): return self.dict.get(param_name, None) def set(self, param_name, param_value): self.dict[param_name] = param_value def VCF_repr(self): ret = "" for key, value in zip(self.dict.keys(), self.dict.values()): ret = ret + ";" + key.upper() + "=" + ",".join(map(quoteParamValue,value.items())) return ret def XML_repr(self): if not self.dict: return '' ret = "" # XXX: This is only for vCard-RDF! for item in self.dict.get('type', []): ret = ret + '' return ret def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.dict)) class vC_contentline: "Logical vCard/vCalendar Content Line" def __init__(self, text): self.name = "" self.params = "" self.value = "" self.parse(text) def parse(self, text): matchobj = CONTENT_LINE_RE.match(chop_line(text)) if matchobj: self.group = matchobj.group(1)[:-1].upper() self.name = matchobj.group(2).upper() self.params = vC_params(matchobj.group(3)) self.value = matchobj.group(4) else: debug.echo("WARNING: Illegal vCard/vCalendar contentline: '%s'" % text) def VCF_repr(self): ret = "" if self.group: ret = ret + self.group + "." ret = ret + self.name + str(self.params) + ":" + self.value return ret class vC_AbstractBaseValue: "Abstract Base Class for all vCard/vCalendar leaf nodes" def VCF_repr(self): "Return VCF representation of value" raise NotImplementedError def XML_repr(self): raise NotImplementedError def get(self): "Return value as String" raise NotImplementedError def set(self, value): "Set value (value should be a string)" raise NotImplementedError def is_empty(self): "Test value for emptieness" raise NotImplementedError class vC_AbstractCompoundValue: "Abstract Base Class for all vCard compound values (n, adr, org)" def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.__dict__)) def is_empty(self): "return True if value is empty" raise NotImplementedError def VCF_repr(self): "Return VCF representation of value" raise NotImplementedError def XML_repr(self): raise NotImplementedError class vC_value(vC_AbstractBaseValue): "Raw Value used by vC_text" def __init__(self, value=""): self.__data = deescape(value) def VCF_repr(self): return escape(self.__data) def XML_repr(self): return escapexml(self.__data) def set(self, value): self.__data = value def get(self): return self.__data def is_empty(self): return self.__data == "" def __repr__(self): return "<%s: %s>" % (self.__class__, repr(self.__data)) class vC_text(vC_AbstractBaseValue): "vCard/vCalendar Text Value (encapsulates vC_value and vC_params)" def __init__(self, value="", params=None): if params is None: params = vC_params() self.params = params self.value = vC_value(value) def get(self): return self.value.get() def set(self, val): self.value.set(val) def is_empty(self): return self.value.is_empty() def VCF_repr(self): return self.params.VCF_repr() + ":" + self.value.VCF_repr() def XML_repr(self): paramxml = self.params.XML_repr() if paramxml: return ""+self.value.XML_repr()+"" + paramxml else: return self.value.XML_repr() def __repr__(self): return "<%s: %s %s>" % (self.__class__, repr(self.value), repr(self.params)) class vC_categories(vC_text): def VCF_repr(self): # value includes ',' therefore do not escape in a whole: ret = self.params.VCF_repr() + ":" + ','.join(\ map(escape, self.value.get().split(','))) return ret class vC_geo(vC_text): def VCF_repr(self): # value includes ';' therefore do not escape: ret = self.params.VCF_repr() + ":" + self.value.get() return ret class vC_datetime(vC_AbstractBaseValue): "vCard/vCalendar DateTime Value Type" # TODO: Datetime is UTC, support TimeZones! def __init__(self, value=None, params=vC_params()): self.params = params self.dateonly = False self.value = None self.set(value) def __repr__(self): return "<%s: %s>" % (self.__class__, self.get()) def is_empty(self): return self.value is None def getDate(self): "get date as string (YYYY-MM-DD)" if self.value is not None: return "%04d-%02d-%02d" % self.value[0:3] else: return "" def getTime(self): "get time as string (HH:MM:SS)" if self.value is not None and not self.dateonly: return "%02d:%02d:%02d" % self.value[3:6] else: return "" def setDate(self, d): "set date from given string (YYYY-MM-DD)" if self.value is None: self.value = (0,0,0,0,0,0,0,0,0) try: if d.find("-") != -1: d = gettuple(map(int, d.split("-")), 3, 0) else: d = tuple(map(int, (d[0:4], d[4:6], d[6:8]))) self.value = d + self.value[3:] except ValueError: # Date-String was probably incorrect: We clear ourself: self.value = None def setTime(self, t): "set time from given string (HH:MM:SS)" if self.value is None: self.value = (0,0,0,0,0,0,0,0,0) debug.echo('vC_datetime.setTime(): Setting Time, but Date not set!') try: if t.find(":") != -1: if t[-1:] == "Z": t = t[:-1] t = gettuple(map(int, t.split(":")), 3, 0) else: t = tuple(map(int, (t[0:2], t[2:4], t[4:6]))) self.value = self.value[0:3] + t + self.value[6:] self.dateonly = False except ValueError: # Something has gone wrong: Set Time to Zero: self.value = self.value[0:3] + (0,0,0) + self.value[6:] self.dateonly = True def get(self): if self.value is not None: if self.dateonly: ret = self.getDate() else: ret = self.getDate() + ' ' + self.getTime() else: ret = "" return ret def set(self, value): if (type(value) is StringType or\ type(value) is UnicodeType): # Unpack datetime string (YYYY-MM-DDTHH:MM:SS[Z]): d, t = gettuple(value.split('T'), 2) if not t: # try wspace-char as date-time separator: d, t = gettuple(value.split(' '), 2) self.setDate(d) if t: self.setTime(t) else: self.dateonly = True elif value: # value is (hopefully) a time.struct_time tuple: self.value = value else: self.value = None def VCF_repr(self): if self.value is not None: # To comply with RFC 2445 (iCalendar) # DateTime does not include "-" nor ":": if self.dateonly: datetimestr = "%04d%02d%02d" % self.value[0:3] else: datetimestr = "%04d%02d%02dT%02d%02d%02dZ" % self.value[0:6] else: datetimestr = "" return self.params.VCF_repr() + ":" + datetimestr def XML_repr(self): if self.value is not None: if self.dateonly: datetimestr = "%04d%02d%02d" % self.value[0:3] else: datetimestr = "%04d%02d%02dT%02d%02d%02dZ" % self.value[0:6] else: datetimestr = "" return datetimestr