diff -r 2dde35b02026 MoinMoin/parser/text_vcard.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/parser/text_vcard.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,46 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Parser for vCard
+
+    @copyright: 2007 Rafael Weber
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.support import vobject
+from MoinMoin.parser.text_x_hcard import main
+
+Dependencies = []
+
+class Parser:
+    extensions = ['.vcf', '.vcard']
+    Dependencies = []
+
+    def __init__(self, raw, request, **kw):
+        self.raw = raw
+        self.request = request
+        self.form = request.form
+        self._ = request.getText
+    
+    def format(self, formatter):
+        card = vobject.readOne(self.raw)
+
+        # we must generate a list like `[(key, type, value)]` to use the 
+        # vcard data with the `main` function
+        data = []
+        for line in card.lines():
+            if line.name == 'ADR':
+                # the address must splitted into street, postal code etc.
+                parts = line.value.split(';')
+                sorted = {'post-office-box': parts[-7],
+                        'extended-address': parts[-6],
+                        'street-address': parts[-5],
+                        'locality': parts[-4], 
+                        'region': parts[-3],
+                        'postal-code': parts[-2],
+                        'country-name': parts[-1]
+                        }
+                for k,v in sorted.items():
+                    if v:
+                        data.append((k, line.singletonparams, v))
+            else:
+                data.append((line.name, line.singletonparams, line.value))
+        main(data, self.request, formatter)
diff -r 2dde35b02026 MoinMoin/parser/text_x_hcard.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/parser/text_x_hcard.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,229 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - Parser for hCard
+
+    Note: Own extension (.hcf odr .hcard) is used.
+
+    @copyright: 2007 Rafael Weber
+    @license: GNU GPL, see COPYING for details.
+"""
+from MoinMoin.support.microformatparser import parse
+from StringIO import StringIO
+from time import strftime
+
+Dependencies = []
+
+order = (
+    'n',
+    'nickname',
+    'photo',
+    'logo',
+    'org',
+    'title',
+    'role',
+    'sound',
+    'bday',
+    'url',
+    'email',
+    'tel',
+    'post-office-box',
+    'extended-address',
+    'street-address',
+    'locality',
+    'region',
+    'postal-code',
+    'country-name',
+    'geo',
+    'tz',
+    'class',
+    'key',
+    'mailer',
+    'rev',
+    'uid',
+    'note'
+)
+
+names = {
+    'n': 'Name',
+    'bday': 'Birthday',
+    'tz': 'Timezone',
+    'org': 'Organisation',
+    'url': 'Homepage',
+    'street-address': 'Street Address',
+    'postal-code': 'Postal Code',
+    'tel': 'Telephone',
+    'country-name': 'Country',
+    'post-office-box': 'Post Office Box',
+    'extended-address': 'Extended Address',
+    'geo': 'Global Position',
+    'prodid': 'Product ID',
+    'rev': 'Revision',
+    'uid': 'UID'
+}
+
+
+def main(card, req, fmt):
+    """
+    The main function. Outputs the table in form of
+    || key || type || value || .
+    """
+    def _get_types(x):
+        if len(x) == 3:
+            if isinstance(x[1], list):
+                return ', '.join([type.capitalize() for type in x[1]])
+            return x[1]
+        return ''
+
+    def nice_name(name):
+        """
+        Returns a nicer name for the keys.
+        Some are defined in the dict and others just get capitalized.
+        """
+        try:
+            return names[name.lower()]
+        except KeyError:
+            return name.capitalize()
+
+    def nice_value(line):
+        """
+        Returns linked homepages, linked email addresses and stuff like 
+        that.
+        """
+        key = line[0].lower()
+        value = line[-1]
+        if key == 'sort-string' or key == 'sound' or key == 'fn':
+            # hide it
+            pass
+        else:
+            req.write(fmt.table_cell(1))
+            if key == 'n':
+                parts = value.split(';')
+                output = []
+                for part in parts:
+                    if ',' in part:
+                        output.append(' '.join(p for p in part.split(',')))
+                    else:
+                        output.append(part)
+                req.write(' '.join(output))
+            elif key == 'url':
+                req.write(fmt.url(1, value, css='http'))
+                req.write(value)
+                req.write(fmt.url(0))
+            elif key == 'email':
+                req.write(fmt.url(1, 'mailto:%s' % value.\
+                        lstrip('mailto:'), css='mailto'))
+                # avoid two "mailto:"
+                req.write(value.lstrip('mailto:'))
+                req.write(fmt.url(0))
+            elif key == 'photo' or key == 'logo':
+                req.write(fmt.url(1, value))
+                req.write(fmt.image(value, width='100px', height='133px'))
+                req.write(fmt.url(0))
+            elif key == 'country-name':
+                req.write(value.capitalize())
+            elif key == 'uid':
+                req.write(value) # without capitalize()
+            elif key == 'rev' or key == 'bday':
+                if len(value) >= 19 and value[4] == '-' and value[7] == \
+                        '-':
+                    year, month, day, = int(value[0:4]), \
+                    int(value[5:7]), int(value[8:10])
+                    hour, minute, second = int(value[11:13]), \
+                    int(value[14:16]), int(value[17:19])
+                    tz = value[20:]
+                    if tz:
+                        result = '%d-%d-%d %d:%d:%d %s' % (year, month, day,
+                            hour, minute, second, tz)
+                    else:
+                        result = '%d-%d-%d %d:%d:%d' % (year, month, day,
+                                hour, minute, second)
+                else:
+                    # only day-precise
+                    year, month, day = int(value[0:4]), int(value[5:7]), \
+                    int(value[8:10])
+                    result = '%d-%d-%d' % (year, month, day)
+                req.write(result)
+
+            else:
+                req.write(value.rstrip(';'))
+            req.write(fmt.table_cell(0))
+        
+    req.write(fmt.table(1))
+
+    # create head line
+    req.write(fmt.table_row(1))
+
+    req.write(fmt.table_cell(1))
+    req.write('Key')
+    req.write(fmt.table_cell(0))
+
+    req.write(fmt.table_cell(1))
+    req.write('Type')
+    req.write(fmt.table_cell(0))
+
+    req.write(fmt.table_cell(1))
+    req.write('Value')
+    req.write(fmt.table_cell(0))
+
+    req.write(fmt.table_row(0))
+    
+    for o in order:
+        for field in card:
+            if field[0].lower() == o:
+                req.write(fmt.table_row(1))
+                # key cell
+                req.write(fmt.table_cell(1))
+                req.write(fmt.strong(1))
+                req.write(nice_name(field[0]))
+                req.write(fmt.strong(0))
+                req.write(fmt.table_cell(0))
+
+                # type cell
+                req.write(fmt.table_cell(1))
+                req.write(fmt.emphasis(1))
+                req.write(_get_types(field))
+                req.write(fmt.emphasis(0))
+
+                # value cell
+                nice_value(field)
+                    
+                req.write(fmt.table_row(0))
+    req.write(fmt.table(0))
+
+class Parser:
+
+    extensions = ['.hcf', '.hcard']
+    Dependencies = []
+
+    def __init__(self, raw, request, **kw):
+        self.raw = raw
+        self.request = request
+        self.form = request.form
+        self._ = request.getText
+    
+    def format(self, formatter):
+        tree = parse(StringIO(self.raw))
+       
+        def get_dict():
+            card = []
+            for t in tree[0][0][1]:
+                if isinstance(t, tuple):
+                    # flat, but maybe list or something in value
+                    if isinstance(t[1], basestring):
+                        # already flat
+                        card.append((t[0],t[1]))
+                    elif isinstance(t[1], list):
+                        if len(t[1]) == 1:
+                            card.append((t[0], t[1][0][1]))
+                        elif t[1][0][0] == 'type':
+                            card.append((t[0], t[1][0][1], t[1][1][1]))
+                        elif t[1][1][0] == 'type':
+                            card.append((t[0], t[1][1][1], t[1][0][1])) 
+                        else:
+                            for t_ in t[1]:
+                                card.append((t_[0], t_[1]))
+            return card
+
+        card = get_dict()
+
+        main(card, self.request, formatter)
diff -r 2dde35b02026 MoinMoin/support/microformatparser.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/microformatparser.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+#
+# Microformat parser hack
+# - My lame attempt to build a generic microformat parser engine
+# (C) Phil Dawes 2005
+# Distributed under a New BSD style license:
+# See: http://www.opensource.org/licenses/bsd-license.php
+#
+# Usage: python ./
+
+import sys
+import urlparse
+from HTMLParser import HTMLParser
+import re
+import urllib2
+
+class MicroformatSchema:
+
+    def __init__(self,props,parentprops):
+        self.props = props
+        self.parentprops = parentprops
+
+    def isValidProperty(self,prop):
+        if prop in self.props + self.parentprops:
+            return True
+        return False
+
+    def isParentProperty(self,prop):
+        return prop in self.parentprops
+
+vcardprops = MicroformatSchema(['fn','family-name', 'given-name', 'additional-name', 'honorific-prefix', 'honorific-suffix', 'nickname', 'sort-string','url','email','type','tel','post-office-box', 'extended-address', 'street-address', 'locality', 'region', 'postal-code', 'country-name', 'label', 'latitude', 'longitude', 'tz', 'photo', 'logo', 'sound', 'bday','title', 'role','organization-name', 'organization-unit','category', 'note','class', 'key', 'mailer', 'uid', 'rev'],['n','email','adr','geo','org','tel'])
+
+veventprops = MicroformatSchema(["summary","url","dtstart","dtend","location"],[])
+
+SCHEMAS= {'vcard':vcardprops,'vevent':veventprops}
+
+class nodeitem:
+    def __init__(self,id,tag,predicates,attrs,nested):
+        self.tag = tag
+        self.id = id
+        self.predicates = predicates
+        self.attrs = attrs
+        self.nested = nested
+
+    def __repr__(self):
+        return "<nodeitem %s, %s, %s, %s, %s>"%(self.tag, self.id, self.predicates,
+                                            self.attrs,self.nested)
+
+class MicroformatToStmts(HTMLParser):
+    def __init__(self,url):
+        self.url = url
+        HTMLParser.__init__(self)
+        self.nodestack = []
+        self.nodemap = {}
+        self.chars = ""
+        self.tree = []
+        self.treestack = []
+
+    def _getattr(self,name,attrs):
+        for attr in attrs:
+            if name == attr[0]: return attr[1]    
+
+    def predicateIsAParent(self,pred):
+        if SCHEMAS[self.currentCompoundFormat].isParentProperty(pred):
+            return True
+        return False
+        
+    def handle_starttag(self, elementtag, attrs):
+        self.chars=""
+        if self.currentlyInAMicroformat():
+            try:
+                preds = self._getattr("class",attrs).split()
+            except AttributeError:
+                self.nodestack.append(nodeitem(1,elementtag,None,attrs,False))
+                return
+
+            prevpreds = []
+            #while 1:
+            nested = False
+            while 1:
+                if prevpreds == preds:
+                    break
+                prevpreds = preds
+                if self.predicateIsAParent(preds[0]):
+                    self.openParentProperty(preds[0])
+                    nested = True
+
+                if elementtag == "img":
+                    self.emitAttributeAsPropertyIfExists('src',attrs, preds)
+                elif elementtag == "a":
+                    self.emitAttributeAsPropertyIfExists('href',attrs, preds)
+                    self.emitAttributeAsPropertyIfExists('title',attrs, preds)
+                elif elementtag == "abbr":
+                    self.emitAttributeAsPropertyIfExists('title',attrs, preds)
+                
+            self.nodestack.append(nodeitem(1,elementtag,preds,attrs,nested))
+
+        elif self.nodeStartsAMicroformat(attrs):
+
+            classattrs = self._getattr('class',attrs).split()
+            for classattr in classattrs:
+                if classattr in SCHEMAS.keys():
+                    self.currentCompoundFormat = classattr
+                    break
+            self.nodestack.append(nodeitem(1,elementtag,[self._getattr('class',attrs)],attrs,True))
+            self.tree.append([])
+            self.treestack = [self.tree[-1]] # opening tree stack frame
+            self.openParentProperty(self.currentCompoundFormat)
+
+    def openParentProperty(self,prop):
+        self.treestack[-1].append((prop,[]))
+        self.treestack.append(self.treestack[-1][-1][1])
+                    
+    def currentlyInAMicroformat(self):
+        return self.nodestack != []
+
+    def nodeStartsAMicroformat(self, attrs):
+        return self._getattr('class',attrs) in SCHEMAS.keys()
+
+    def emitAttributeAsPropertyIfExists(self, attrname, attrs, preds):
+        obj = self._getattr(attrname,attrs)
+        if obj is not None:
+            try:
+                pred = preds[0]
+                if SCHEMAS[self.currentCompoundFormat].isValidProperty(pred):
+                    if attrname in ("href","src"):
+                        obj = urlparse.urljoin(self.url,obj)
+                    obj = self.makeDatesParsable(pred,obj)
+                    self.addPropertyValueToOutput(pred,obj)
+                del preds[0]
+            except IndexError:
+                pass
+
+    def addPropertyValueToOutput(self,prop,val):
+        self.treestack[-1].append((prop,val))
+        
+    def handle_endtag(self,tag):
+        if self.currentlyInAMicroformat():
+            while 1:
+                try:
+                    item = self.nodestack.pop()
+                except IndexError:
+                    return # no more elements
+                if item.tag == tag:
+                    break  # found it!
+
+            # if there's still predicates, then output the text as object
+            if item.predicates and item.predicates != [] and self.chars.strip() != "":
+                #if item.tag == 'a':
+                #    print "ITEM:a",self.treestack
+                preds = item.predicates
+                self.treestack[-1].append((preds[0],self.chars))
+                del preds[0]
+            if item.nested == 1:
+                self.treestack.pop()
+            self.chars = ""
+
+    # HTMLPARSER interface
+    def handle_data(self,content):
+        if self.hasPredicatesPending():
+            content = content.strip()
+            if content == "":
+                return        
+            self.chars += content
+
+    def hasPredicatesPending(self):
+        for n in self.nodestack:
+            if n.predicates != []:
+                return 1
+        return 0
+
+    # hack to stop dates like '20051005' being interpreted as floats downstream
+    def makeDatesParsable(self,p,o):
+        if p in ["dtstart","dtend"]:
+            try:
+                float(o) # can it be interpreted as a float?
+                o = "%s-%s-%s"%(o[:4],o[4:6],o[6:])
+            except ValueError:
+                pass
+        return o
+
+
+def printTree(tree,tab=""):
+    for p,v in tree:
+        if isinstance(v,list):
+            print tab + p
+            printTree(v,tab+"    ")
+        else:
+            print tab + str(p),":",v
+
+def printTreeStack(treestack,tab=""):
+    for t in treestack:
+        if isinstance(t,list):
+            printTreeStack(t,tab+"    ")
+        else:
+            print t
+
+def parse(f,url="http://dummyurl.com/"):
+    m = MicroformatToStmts(url)
+    s = f.read()
+    m.feed(s)
+    m.close()
+
+    return m.tree
+    
+
+if __name__ == "__main__":
+    import urllib
+    if len(sys.argv) == 1:
+        print "Usage:",sys.argv[0],"<url>"
+        sys.exit(0)
+    else:
+        for url in sys.argv[1:]:
+            trees = parse(urllib.urlopen(url),url)
+        for tree in trees:
+            printTree(tree)
diff -r 2dde35b02026 MoinMoin/support/vobject/__init__.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/__init__.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,86 @@
+"""
+VObject Overview
+================
+    vobject parses vCard or vCalendar files, returning a tree of Python objects.
+    It also provids an API to create vCard or vCalendar data structures which
+    can then be serialized.
+
+    Parsing existing streams
+    ------------------------
+    Streams containing one or many L{Component<base.Component>}s can be
+    parsed using L{readComponents<base.readComponents>}.  As each Component
+    is parsed, vobject will attempt to give it a L{Behavior<behavior.Behavior>}.
+    If an appropriate Behavior is found, any base64, quoted-printable, or
+    backslash escaped data will automatically be decoded.  Dates and datetimes
+    will be transformed to datetime.date or datetime.datetime instances.
+    Components containing recurrence information will have a special rruleset
+    attribute (a dateutil.rrule.rruleset instance).
+
+    Validation
+    ----------
+    L{Behavior<behavior.Behavior>} classes implement validation for
+    L{Component<base.Component>}s.  To validate, an object must have all
+    required children.  There (TODO: will be) a toggle to raise an exception or
+    just log unrecognized, non-experimental children and parameters.
+    
+    Creating objects programatically
+    --------------------------------
+    A L{Component<base.Component>} can be created from scratch.  No encoding
+    is necessary, serialization will encode data automatically.  Factory
+    functions (TODO: will be) available to create standard objects.
+
+    Serializing objects
+    -------------------
+    Serialization: 
+      - Looks for missing required children that can be automatically generated,
+        like a UID or a PRODID, and adds them
+      - Encodes all values that can be automatically encoded
+      - Checks to make sure the object is valid (unless this behavior is
+        explicitly disabled)
+      - Appends the serialized object to a buffer, or fills a new
+        buffer and returns it
+    
+    Examples
+    --------
+    
+    >>> import datetime
+    >>> import dateutil.rrule as rrule
+    >>> x = iCalendar()
+    >>> x.add('vevent')
+    <VEVENT| []>
+    >>> x
+    <VCALENDAR| [<VEVENT| []>]>
+    >>> v = x.vevent
+    >>> utc = icalendar.utc
+    >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc)
+    >>> v
+    <VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>
+    >>> x
+    <VCALENDAR| [<VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>]>
+    >>> newrule = rrule.rruleset()
+    >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value))
+    >>> v.rruleset = newrule
+    >>> list(v.rruleset)
+    [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())]
+    >>> v.add('uid').value = "randomuid@MYHOSTNAME"
+    >>> print x.serialize()
+    BEGIN:VCALENDAR
+    VERSION:2.0
+    PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+    BEGIN:VEVENT
+    UID:randomuid@MYHOSTNAME
+    DTSTART:20041215T140000Z
+    RRULE:FREQ=WEEKLY;COUNT=2
+    END:VEVENT
+    END:VCALENDAR
+    
+"""
+
+import base, icalendar, vcard
+from base import readComponents, readOne, newFromBehavior
+
+def iCalendar():
+    return newFromBehavior('vcalendar', '2.0')
+
+def vCard():
+    return newFromBehavior('vcard', '3.0')
\ No newline at end of file
diff -r 2dde35b02026 MoinMoin/support/vobject/base.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/base.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,1072 @@
+"""vobject module for reading vCard and vCalendar files."""
+
+import copy
+import re
+import sys
+import logging
+import StringIO
+import string
+import exceptions
+import codecs
+
+#------------------------------------ Logging ----------------------------------
+logger = logging.getLogger(__name__)
+if not logging.getLogger().handlers:
+    handler = logging.StreamHandler()
+    formatter = logging.Formatter('%(name)s %(levelname)s %(message)s')
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+logger.setLevel(logging.ERROR) # Log errors
+DEBUG = False # Don't waste time on debug calls
+#----------------------------------- Constants ---------------------------------
+CR     = unichr(13)
+LF     = unichr(10)
+CRLF   = CR + LF
+SPACE  = unichr(32)
+TAB    = unichr(9)
+SPACEORTAB = SPACE + TAB
+#-------------------------------- Useful modules -------------------------------
+#   use doctest, it kills two birds with one stone and docstrings often become
+#                more readable to boot (see parseLine's docstring).
+#   use logging, then when debugging we can just set our verbosity.
+#   use epydoc syntax for documenting code, please document every class and non-
+#                trivial method (see http://epydoc.sourceforge.net/epytext.html
+#                and http://epydoc.sourceforge.net/fields.html).  Also, please
+#                follow http://www.python.org/peps/pep-0257.html for docstrings.
+#-------------------------------------------------------------------------------
+
+#--------------------------------- Main classes --------------------------------
+class VBase(object):
+    """Base class for ContentLine and Component.
+    
+    @ivar behavior:
+        The Behavior class associated with this object, which controls
+        validation, transformations, and encoding.
+    @ivar parentBehavior:
+        The object's parent's behavior, or None if no behaviored parent exists.
+    @ivar isNative:
+        Boolean describing whether this component is a Native instance.
+    @ivar group:
+        An optional group prefix, should be used only to indicate sort order in 
+        vCards, according to RFC2426
+    """
+    def __init__(self, group=None, *args, **kwds):
+        super(VBase, self).__init__(*args, **kwds)
+        self.group      = group
+        self.behavior   = None
+        self.parentBehavior = None
+        self.isNative = False
+    
+    def copy(self, copyit):
+        self.group = copyit.group
+        self.behavior = copyit.behavior
+        self.parentBehavior = copyit.parentBehavior
+        self.isNative = copyit.isNative
+        
+    def validate(self, *args, **kwds):
+        """Call the behavior's validate method, or return True."""
+        if self.behavior:
+            return self.behavior.validate(self, *args, **kwds)
+        else: return True
+
+    def getChildren(self):
+        """Return an iterable containing the contents of the object."""
+        return []
+
+    def clearBehavior(self, cascade=True):
+        """Set behavior to None. Do for all descendants if cascading."""
+        self.behavior=None
+        if cascade: self.transformChildrenFromNative()
+
+    def autoBehavior(self, cascade=False):
+        """Set behavior if name is in self.parentBehavior.knownChildren.
+        
+        If cascade is True, unset behavior and parentBehavior for all
+        descendants, then recalculate behavior and parentBehavior.
+        
+        """
+        parentBehavior = self.parentBehavior
+        if parentBehavior is not None:
+            knownChildTup = parentBehavior.knownChildren.get(self.name, None)
+            if knownChildTup is not None:
+                behavior = getBehavior(self.name, knownChildTup[2])
+                if behavior is not None:
+                    self.setBehavior(behavior, cascade)
+                    if isinstance(self, ContentLine) and self.encoded:
+                        self.behavior.decode(self)
+            elif isinstance(self, ContentLine):
+                self.behavior = parentBehavior.defaultBehavior   
+                if self.encoded and self.behavior:
+                    self.behavior.decode(self)
+
+    def setBehavior(self, behavior, cascade=True):
+        """Set behavior. If cascade is True, autoBehavior all descendants."""
+        self.behavior=behavior
+        if cascade:
+            for obj in self.getChildren():
+                obj.parentBehavior=behavior
+                obj.autoBehavior(True)
+
+    def transformToNative(self):
+        """Transform this object into a custom VBase subclass.
+        
+        transformToNative should always return a representation of this object.
+        It may do so by modifying self in place then returning self, or by
+        creating a new object.
+        
+        """
+        if self.isNative or not self.behavior or not self.behavior.hasNative:
+            return self
+        else:
+            try:
+                return self.behavior.transformToNative(self)
+            except Exception, e:      
+                # wrap errors in transformation in a ParseError
+                lineNumber = getattr(self, 'lineNumber', None)
+                if isinstance(e, ParseError):
+                    if lineNumber is not None:
+                        e.lineNumber = lineNumber
+                    raise
+                else:
+                    msg = "In transformToNative, unhandled exception: %s: %s"
+                    msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
+                    new_error = ParseError(msg, lineNumber)
+                    raise ParseError, new_error, sys.exc_info()[2]
+                
+
+    def transformFromNative(self):
+        """Return self transformed into a ContentLine or Component if needed.
+        
+        May have side effects.  If it does, transformFromNative and
+        transformToNative MUST have perfectly inverse side effects. Allowing
+        such side effects is convenient for objects whose transformations only
+        change a few attributes.
+        
+        Note that it isn't always possible for transformFromNative to be a
+        perfect inverse of transformToNative, in such cases transformFromNative
+        should return a new object, not self after modifications.
+        
+        """
+        if self.isNative and self.behavior and self.behavior.hasNative:
+            try:
+                return self.behavior.transformFromNative(self)
+            except Exception, e:
+                # wrap errors in transformation in a NativeError
+                lineNumber = getattr(self, 'lineNumber', None)
+                if isinstance(e, NativeError):
+                    if lineNumber is not None:
+                        e.lineNumber = lineNumber
+                    raise
+                else:
+                    msg = "In transformFromNative, unhandled exception: %s: %s"
+                    msg = msg % (sys.exc_info()[0], sys.exc_info()[1])
+                    new_error = NativeError(msg, lineNumber)
+                    raise NativeError, new_error, sys.exc_info()[2]
+        else: return self
+
+    def transformChildrenToNative(self):
+        """Recursively replace children with their native representation."""
+        pass
+
+    def transformChildrenFromNative(self, clearBehavior=True):
+        """Recursively transform native children to vanilla representations."""
+        pass
+
+    def serialize(self, buf=None, lineLength=75, validate=True, behavior=None):
+        """Serialize to buf if it exists, otherwise return a string.
+        
+        Use self.behavior.serialize if behavior exists.
+        
+        """
+        if not behavior:
+            behavior = self.behavior
+        
+        if behavior:
+            if DEBUG: logger.debug("serializing %s with behavior" % self.name)
+            return behavior.serialize(self, buf, lineLength, validate)
+        else:
+            if DEBUG: logger.debug("serializing %s without behavior" % self.name)
+            return defaultSerialize(self, buf, lineLength)
+
+def ascii(s):
+    """Turn s into a printable string.  Won't work for 8-bit ASCII."""
+    return unicode(s).encode('ascii', 'replace')
+
+def toVName(name, stripNum = 0, upper = False):
+    """
+    Turn a Python name into an iCalendar style name, optionally uppercase and 
+    with characters stripped off.
+    """
+    if upper:
+        name = name.upper()
+    if stripNum != 0:
+        name = name[:-stripNum]
+    return name.replace('_', '-')
+
+class ContentLine(VBase):
+    """Holds one content line for formats like vCard and vCalendar.
+
+    For example::
+      <SUMMARY{u'param1' : [u'val1'], u'param2' : [u'val2']}Bastille Day Party>
+
+    @ivar name:
+        The uppercased name of the contentline.
+    @ivar params:
+        A dictionary of parameters and associated lists of values (the list may
+        be empty for empty parameters).
+    @ivar value:
+        The value of the contentline.
+    @ivar singletonparams:
+        A list of parameters for which it's unclear if the string represents the
+        parameter name or the parameter value. In vCard 2.1, "The value string
+        can be specified alone in those cases where the value is unambiguous".
+        This is crazy, but we have to deal with it.
+    @ivar encoded:
+        A boolean describing whether the data in the content line is encoded.
+        Generally, text read from a serialized vCard or vCalendar should be
+        considered encoded.  Data added programmatically should not be encoded.
+    @ivar lineNumber:
+        An optional line number associated with the contentline.
+    """
+    def __init__(self, name, params, value, group=None, 
+                 encoded=False, isNative=False,
+                 lineNumber = None, *args, **kwds):
+        """Take output from parseLine, convert params list to dictionary."""
+        # group is used as a positional argument to match parseLine's return
+        super(ContentLine, self).__init__(group, *args, **kwds)
+        self.name        = name.upper()
+        self.value       = value
+        self.encoded     = encoded
+        self.params      = {}
+        self.singletonparams = []
+        self.isNative = isNative
+        self.lineNumber = lineNumber
+        def updateTable(x):
+            if len(x) == 1:
+                self.singletonparams += x
+            else:
+                paramlist = self.params.setdefault(x[0].upper(), [])
+                paramlist.extend(x[1:])
+        map(updateTable, params)
+        qp = False
+        if 'ENCODING' in self.params:
+            if 'QUOTED-PRINTABLE' in self.params['ENCODING']:
+                qp = True
+                self.params['ENCODING'].remove('QUOTED-PRINTABLE')
+                if 0==len(self.params['ENCODING']):
+                    del self.params['ENCODING']
+        if 'QUOTED-PRINTABLE' in self.singletonparams:
+            qp = True
+            self.singletonparams.remove('QUOTED-PRINTABLE')
+        if qp:
+            self.value = str(self.value).decode('quoted-printable')
+
+    @classmethod
+    def duplicate(clz, copyit):
+        newcopy = clz('', {}, '')
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(ContentLine, self).copy(copyit)
+        self.name = copyit.name
+        self.value = copy.copy(copyit.value)
+        self.encoded = self.encoded
+        self.params = copy.copy(copyit.params)
+        self.singletonparams = copy.copy(copyit.singletonparams)
+        self.lineNumber = copyit.lineNumber
+        
+    def __eq__(self, other):
+        try:
+            return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)
+        except:
+            return False
+
+    def __getattr__(self, name):
+        """Make params accessible via self.foo_param or self.foo_paramlist.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+
+        """
+        try:
+            if name.endswith('_param'):
+                return self.params[toVName(name, 6, True)][0]
+            elif name.endswith('_paramlist'):
+                return self.params[toVName(name, 10, True)]
+            else:
+                raise exceptions.AttributeError, name
+        except KeyError:
+            raise exceptions.AttributeError, name
+
+    def __setattr__(self, name, value):
+        """Make params accessible via self.foo_param or self.foo_paramlist.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        
+        """
+        if name.endswith('_param'):
+            if type(value) == list:
+                self.params[toVName(name, 6, True)] = value
+            else:
+                self.params[toVName(name, 6, True)] = [value]
+        elif name.endswith('_paramlist'):
+            if type(value) == list:
+                self.params[toVName(name, 10, True)] = value
+            else:
+                raise VObjectError("Parameter list set to a non-list")
+        else:
+            prop = getattr(self.__class__, name, None)
+            if isinstance(prop, property):
+                prop.fset(self, value)
+            else:
+                object.__setattr__(self, name, value)
+
+    def __delattr__(self, name):
+        try:
+            if name.endswith('_param'):
+                del self.params[toVName(name, 6, True)]
+            elif name.endswith('_paramlist'):
+                del self.params[toVName(name, 10, True)]
+            else:
+                object.__delattr__(self, name)
+        except KeyError:
+            raise exceptions.AttributeError, name
+
+    def valueRepr( self ):
+        """transform the representation of the value according to the behavior,
+        if any"""
+        v = self.value
+        if self.behavior:
+            v = self.behavior.valueRepr( self )
+        return ascii( v )
+        
+    def __str__(self):
+        return "<"+ascii(self.name)+ascii(self.params)+self.valueRepr()+">"
+
+    def __repr__(self):
+        return self.__str__().replace('\n', '\\n')
+
+    def prettyPrint(self, level = 0, tabwidth=3):
+        pre = ' ' * level * tabwidth
+        print pre, self.name + ":", self.valueRepr()
+        if self.params:
+            lineKeys= self.params.keys()
+            print pre, "params for ", self.name +':'
+            for aKey in lineKeys:
+                print pre + ' ' * tabwidth, aKey, ascii(self.params[aKey])
+
+class Component(VBase):
+    """A complex property that can contain multiple ContentLines.
+    
+    For our purposes, a component must start with a BEGIN:xxxx line and end with
+    END:xxxx, or have a PROFILE:xxx line if a top-level component.
+
+    @ivar contents:
+        A dictionary of lists of Component or ContentLine instances. The keys
+        are the lowercased names of child ContentLines or Components.
+        Note that BEGIN and END ContentLines are not included in contents.
+    @ivar name:
+        Uppercase string used to represent this Component, i.e VCARD if the
+        serialized object starts with BEGIN:VCARD.
+    @ivar useBegin:
+        A boolean flag determining whether BEGIN: and END: lines should
+        be serialized.
+
+    """
+    def __init__(self, name=None, *args, **kwds):
+        super(Component, self).__init__(*args, **kwds)
+        self.contents  = {}
+        if name:
+            self.name=name.upper()
+            self.useBegin = True
+        else:
+            self.name = ''
+            self.useBegin = False
+        
+        self.autoBehavior()
+
+    @classmethod
+    def duplicate(clz, copyit):
+        newcopy = clz()
+        newcopy.copy(copyit)
+        return newcopy
+
+    def copy(self, copyit):
+        super(Component, self).copy(copyit)
+        
+        # deep copy of contents
+        self.contents = {}
+        for key, lvalue in copyit.contents.items():
+            newvalue = []
+            for value in lvalue:
+                newitem = value.duplicate(value)
+                newvalue.append(newitem)
+            self.contents[key] = newvalue
+
+        self.name = copyit.name
+        self.useBegin = copyit.useBegin
+         
+    def setProfile(self, name):
+        """Assign a PROFILE to this unnamed component.
+        
+        Used by vCard, not by vCalendar.
+        
+        """
+        if self.name or self.useBegin:
+            if self.name == name: return
+            raise VObjectError("This component already has a PROFILE or uses BEGIN.")
+        self.name = name.upper()
+
+    def __getattr__(self, name):
+        """For convenience, make self.contents directly accessible.
+        
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        
+        """
+        try:
+            if name.endswith('_list'):
+                return self.contents[toVName(name, 5)]
+            else:
+                return self.contents[toVName(name)][0]
+        except KeyError:
+            raise exceptions.AttributeError, name
+
+    normal_attributes = ['contents','name','behavior','parentBehavior','group']
+    def __setattr__(self, name, value):
+        """For convenience, make self.contents directly accessible.
+
+        Underscores, legal in python variable names, are converted to dashes,
+        which are legal in IANA tokens.
+        
+        """
+        if name not in self.normal_attributes and name.lower()==name:
+            if type(value) == list:
+                if name.endswith('_list'):
+                    name = name[:-5]
+                self.contents[toVName(name)] = value
+            elif name.endswith('_list'):
+                raise VObjectError("Component list set to a non-list")
+            else:
+                self.contents[toVName(name)] = [value]
+        else:
+            prop = getattr(self.__class__, name, None)
+            if isinstance(prop, property):
+                prop.fset(self, value)
+            else:
+                object.__setattr__(self, name, value)
+
+    def __delattr__(self, name):
+        try:
+            if name not in self.normal_attributes and name.lower()==name:
+                if name.endswith('_list'):
+                    del self.contents[toVName(name, 5)]
+                else:
+                    del self.contents[toVName(name)]
+            else:
+                object.__delattr__(self, name)
+        except KeyError:
+            raise exceptions.AttributeError, name
+
+    def getChildValue(self, childName, default = None, childNumber = 0):
+        """Return a child's value (the first, by default), or None."""
+        child = self.contents.get(toVName(childName))
+        if child is None:
+            return default
+        else:
+            return child[childNumber].value
+
+    def add(self, objOrName, group = None):
+        """Add objOrName to contents, set behavior if it can be inferred.
+        
+        If objOrName is a string, create an empty component or line based on
+        behavior. If no behavior is found for the object, add a ContentLine.
+
+        group is an optional prefix to the name of the object (see
+        RFC 2425).
+        """
+        if isinstance(objOrName, VBase):
+            obj = objOrName
+            if self.behavior:
+                obj.parentBehavior = self.behavior
+                obj.autoBehavior(True)
+        else:
+            name = objOrName.upper()
+            try:
+                id=self.behavior.knownChildren[name][2]
+                behavior = getBehavior(name, id)
+                if behavior.isComponent:
+                    obj = Component(name)
+                else:
+                    obj = ContentLine(name, [], '', group)
+                obj.parentBehavior = self.behavior
+                obj.behavior = behavior
+                obj = obj.transformToNative()     
+            except (KeyError, AttributeError):
+                obj = ContentLine(objOrName, [], '', group)
+            if obj.behavior is None and self.behavior is not None:
+                if isinstance(obj, ContentLine):
+                    obj.behavior = self.behavior.defaultBehavior
+        self.contents.setdefault(obj.name.lower(), []).append(obj)
+        return obj
+
+    def remove(self, obj):
+        """Remove obj from contents."""
+        named = self.contents.get(obj.name.lower())
+        if named:
+            try:
+                named.remove(obj)
+                if len(named) == 0:
+                    del self.contents[obj.name.lower()]
+            except ValueError:
+                pass;
+
+    def getChildren(self):
+        """Return an iterable of all children."""
+        for objList in self.contents.values():
+            for obj in objList: yield obj
+
+    def components(self):
+        """Return an iterable of all Component children."""
+        return (i for i in self.getChildren() if isinstance(i, Component))
+
+    def lines(self):
+        """Return an iterable of all ContentLine children."""
+        return (i for i in self.getChildren() if isinstance(i, ContentLine))
+
+    def sortChildKeys(self):
+        try:
+            first = [s for s in self.behavior.sortFirst if s in self.contents]
+        except:
+            first = []
+        return first + sorted(k for k in self.contents.keys() if k not in first)
+
+    def getSortedChildren(self):
+        return [obj for k in self.sortChildKeys() for obj in self.contents[k]]
+
+    def setBehaviorFromVersionLine(self, versionLine):
+        """Set behavior if one matches name, versionLine.value."""
+        v=getBehavior(self.name, versionLine.value)
+        if v: self.setBehavior(v)
+        
+    def transformChildrenToNative(self):
+        """Recursively replace children with their native representation."""
+        #sort to get dependency order right, like vtimezone before vevent
+        for childArray in (self.contents[k] for k in self.sortChildKeys()):
+            for i in xrange(len(childArray)):
+                childArray[i]=childArray[i].transformToNative()
+                childArray[i].transformChildrenToNative()
+
+    def transformChildrenFromNative(self, clearBehavior=True):
+        """Recursively transform native children to vanilla representations."""
+        for childArray in self.contents.values():
+            for i in xrange(len(childArray)):
+                childArray[i]=childArray[i].transformFromNative()
+                childArray[i].transformChildrenFromNative(clearBehavior)
+                if clearBehavior:
+                    childArray[i].behavior = None
+                    childArray[i].parentBehavior = None
+    
+    def __str__(self):
+        if self.name:
+            return "<" + self.name + "| " + str(self.getSortedChildren()) + ">"
+        else:
+            return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>'
+
+    def __repr__(self):
+        return self.__str__()
+
+    def prettyPrint(self, level = 0, tabwidth=3):
+        pre = ' ' * level * tabwidth
+        print pre, self.name
+        if isinstance(self, Component):
+            for line in self.getChildren():
+                line.prettyPrint(level + 1, tabwidth)
+        print
+
+class VObjectError(Exception):
+    def __init__(self, message, lineNumber=None):
+        self.message = message
+        if lineNumber is not None:
+            self.lineNumber = lineNumber
+    def __str__(self):
+        if hasattr(self, 'lineNumber'):
+            return "At line %s: %s" % \
+                   (self.lineNumber, self.message)
+        else:
+            return repr(self.message)
+
+class ParseError(VObjectError):
+    pass
+
+class ValidateError(VObjectError):
+    pass
+
+class NativeError(VObjectError):
+    pass
+
+#-------------------------- Parsing functions ----------------------------------
+
+# parseLine regular expressions
+
+patterns = {}
+
+patterns['name'] = '[a-zA-Z0-9\-]+'
+patterns['safe_char'] = '[^";:,]'
+patterns['qsafe_char'] = '[^"]'
+
+# the combined Python string replacement and regex syntax is a little confusing;
+# remember that %(foobar)s is replaced with patterns['foobar'], so for instance
+# param_value is any number of safe_chars or any number of qsaf_chars surrounded
+# by double quotes.
+
+patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns
+
+
+# get a tuple of two elements, one will be empty, the other will have the value
+patterns['param_value_grouped'] = """
+" ( %(qsafe_char)s * )" | ( %(safe_char)s + )
+""" % patterns
+
+# get a parameter and its values, without any saved groups
+patterns['param'] = r"""
+; (?: %(name)s )                     # parameter name
+(?:
+    (?: = (?: %(param_value)s ) )?   # 0 or more parameter values, multiple 
+    (?: , (?: %(param_value)s ) )*   # parameters are comma separated
+)*                         
+""" % patterns
+
+# get a parameter, saving groups for name and value (value still needs parsing)
+patterns['params_grouped'] = r"""
+; ( %(name)s )
+
+(?: =
+    (
+        (?:   (?: %(param_value)s ) )?   # 0 or more parameter values, multiple 
+        (?: , (?: %(param_value)s ) )*   # parameters are comma separated
+    )
+)?
+""" % patterns
+
+# get a full content line, break it up into group, name, parameters, and value
+patterns['line'] = r"""
+^ ((?P<group> %(name)s)\.)?(?P<name> %(name)s) # name group
+  (?P<params> (?: %(param)s )* )               # params group (may be empty)
+: (?P<value> .* )$                             # value group
+""" % patterns
+
+' "%(qsafe_char)s*" | %(safe_char)s* '
+
+param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE)
+params_re       = re.compile(patterns['params_grouped'],      re.VERBOSE)
+line_re         = re.compile(patterns['line'],                re.VERBOSE)
+begin_re        = re.compile('BEGIN', re.IGNORECASE)
+
+
+def parseParams(string):
+    """
+    >>> parseParams(';ALTREP="http://www.wiz.org"')
+    [['ALTREP', 'http://www.wiz.org']]
+    >>> parseParams('')
+    []
+    >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR')
+    [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']]
+    """
+    all = params_re.findall(string)
+    allParameters = []
+    for tup in all:
+        paramList = [tup[0]] # tup looks like (name, valuesString)
+        for pair in param_values_re.findall(tup[1]):
+            # pair looks like ('', value) or (value, '')
+            if pair[0] != '':
+                paramList.append(pair[0])
+            else:
+                paramList.append(pair[1])
+        allParameters.append(paramList)
+    return allParameters
+
+
+def parseLine(line, lineNumber = None):
+    """
+    >>> parseLine("BLAH:")
+    ('BLAH', [], '', None)
+    >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904")
+    ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
+    >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA')
+    ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None)
+    >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com")
+    ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
+    >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com')
+    ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
+    >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;')
+    ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
+    >>> parseLine(":")
+    Traceback (most recent call last):
+    ...
+    ParseError: 'Failed to parse line: :'
+    """
+    
+    match = line_re.match(line)
+    if match is None:
+        raise ParseError("Failed to parse line: %s" % line, lineNumber)
+    return (match.group('name'), 
+            parseParams(match.group('params')),
+            match.group('value'), match.group('group'))
+
+# logical line regular expressions
+
+patterns['lineend'] = r'(?:\r\n|\r|\n|$)'
+patterns['wrap'] = r'%(lineend)s [\t ]' % patterns
+patterns['logicallines'] = r"""
+(
+   (?: [^\r\n] | %(wrap)s )*
+   %(lineend)s
+)
+""" % patterns
+
+patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns
+
+wrap_re          = re.compile(patterns['wraporend'],    re.VERBOSE)
+logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE)
+
+testLines="""
+Line 0 text
+ , Line 0 continued.
+Line 1;encoding=quoted-printable:this is an evil=
+ evil=
+ format.
+Line 2 is a new line, it does not start with whitespace.
+"""
+
+def getLogicalLines(fp, allowQP=True, findBegin=False):
+    """Iterate through a stream, yielding one logical line at a time.
+
+    Because many applications still use vCard 2.1, we have to deal with the
+    quoted-printable encoding for long lines, as well as the vCard 3.0 and
+    vCalendar line folding technique, a whitespace character at the start
+    of the line.
+       
+    Quoted-printable data will be decoded in the Behavior decoding phase.
+       
+    >>> import StringIO
+    >>> f=StringIO.StringIO(testLines)
+    >>> for n, l in enumerate(getLogicalLines(f)):
+    ...     print "Line %s: %s" % (n, l[0])
+    ...
+    Line 0: Line 0 text, Line 0 continued.
+    Line 1: Line 1;encoding=quoted-printable:this is an evil=
+     evil=
+     format.
+    Line 2: Line 2 is a new line, it does not start with whitespace.
+
+    """
+    if not allowQP:
+        bytes = fp.read(-1)
+        if len(bytes) > 0:
+            if type(bytes[0]) == unicode:
+                val = bytes
+            elif not findBegin:
+                val = bytes.decode('utf-8')
+            else:
+                for encoding in 'utf-8', 'utf-16-LE', 'utf-16-BE', 'iso-8859-1':
+                    try:
+                        val = bytes.decode(encoding)
+                        if begin_re.search(val) is not None:
+                            break
+                    except UnicodeDecodeError:
+                        pass
+                else:
+                    raise ParseError, 'Could not find BEGIN when trying to determine encoding'
+        else:
+            val = bytes
+        
+        # strip off any UTF8 BOMs which Python's UTF8 decoder leaves
+
+        val = val.lstrip( unicode( codecs.BOM_UTF8, "utf8" ) )
+
+        lineNumber = 1
+        for match in logical_lines_re.finditer(val):
+            line, n = wrap_re.subn('', match.group())
+            if line != '':
+                yield line, lineNumber
+            lineNumber += n
+        
+    else:
+        quotedPrintable=False
+        newbuffer = StringIO.StringIO
+        logicalLine = newbuffer()
+        lineNumber = 0
+        lineStartNumber = 0
+        while True:
+            line = fp.readline()
+            if line == '':
+                break
+            else:
+                line = line.rstrip(CRLF)
+                lineNumber += 1
+            if line.rstrip() == '':
+                if logicalLine.pos > 0:
+                    yield logicalLine.getvalue(), lineStartNumber
+                lineStartNumber = lineNumber
+                logicalLine = newbuffer()
+                quotedPrintable=False
+                continue
+    
+            if quotedPrintable and allowQP:
+                logicalLine.write('\n')
+                logicalLine.write(line)
+                quotedPrintable=False
+            elif line[0] in SPACEORTAB:
+                logicalLine.write(line[1:])
+            elif logicalLine.pos > 0:
+                yield logicalLine.getvalue(), lineStartNumber
+                lineStartNumber = lineNumber
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+            else:
+                logicalLine = newbuffer()
+                logicalLine.write(line)
+            
+            # hack to deal with the fact that vCard 2.1 allows parameters to be
+            # encoded without a parameter name.  False positives are unlikely, but
+            # possible.
+            val = logicalLine.getvalue()
+            if val[-1]=='=' and val.lower().find('quoted-printable') >= 0:
+                quotedPrintable=True
+    
+        if logicalLine.pos > 0:
+            yield logicalLine.getvalue(), lineStartNumber
+
+
+def textLineToContentLine(text, n=None):
+    return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n})
+            
+
+def dquoteEscape(param):
+    """Return param, or "param" if ',' or ';' or ':' is in param."""
+    if param.find('"') >= 0:
+        raise VObjectError("Double quotes aren't allowed in parameter values.")
+    for char in ',;:':
+        if param.find(char) >= 0:
+            return '"'+ param + '"'
+    return param
+
+def foldOneLine(outbuf, input, lineLength = 75):
+    if isinstance(input, basestring): input = StringIO.StringIO(input)
+    input.seek(0)
+    outbuf.write(input.read(lineLength) + CRLF)
+    brokenline = input.read(lineLength - 1)
+    while brokenline:
+        outbuf.write(' ' + brokenline + CRLF)
+        brokenline = input.read(lineLength - 1)
+
+def defaultSerialize(obj, buf, lineLength):
+    """Encode and fold obj and its children, write to buf or return a string."""
+
+    outbuf = buf or StringIO.StringIO()
+
+    if isinstance(obj, Component):
+        if obj.group is None:
+            groupString = ''
+        else:
+            groupString = obj.group + '.'
+        if obj.useBegin:
+            foldOneLine(outbuf, groupString + u"BEGIN:" + obj.name, lineLength)
+        for child in obj.getSortedChildren():
+            #validate is recursive, we only need to validate once
+            child.serialize(outbuf, lineLength, validate=False)
+        if obj.useBegin:
+            foldOneLine(outbuf, groupString + u"END:" + obj.name, lineLength)
+        if DEBUG: logger.debug("Finished %s" % obj.name.upper())
+        
+    elif isinstance(obj, ContentLine):
+        startedEncoded = obj.encoded
+        if obj.behavior and not startedEncoded: obj.behavior.encode(obj)
+        s=StringIO.StringIO() #unfolded buffer
+        if obj.group is not None:
+            s.write(obj.group + '.')
+        if DEBUG: logger.debug("Serializing line" + str(obj))
+        s.write(obj.name.upper())
+        for key, paramvals in obj.params.iteritems():
+            s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals)))
+        s.write(':' + obj.value)
+        if obj.behavior and not startedEncoded: obj.behavior.decode(obj)
+        foldOneLine(outbuf, s, lineLength)
+        if DEBUG: logger.debug("Finished %s line" % obj.name.upper())
+    
+    return buf or outbuf.getvalue()
+
+
+testVCalendar="""
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+SUMMARY;blah=hi!:Bastille Day Party
+END:VEVENT
+END:VCALENDAR"""
+
+class Stack:
+    def __init__(self):
+        self.stack = []
+    def __len__(self):
+        return len(self.stack)
+    def top(self):
+        if len(self) == 0: return None
+        else: return self.stack[-1]
+    def topName(self):
+        if len(self) == 0: return None
+        else: return self.stack[-1].name
+    def modifyTop(self, item):
+        top = self.top()
+        if top:
+            top.add(item)
+        else:
+            new = Component()
+            self.push(new)
+            new.add(item) #add sets behavior for item and children
+    def push(self, obj): self.stack.append(obj)
+    def pop(self): return self.stack.pop()
+
+
+def readComponents(streamOrString, validate=False, transform=True,
+                   findBegin=True, ignoreUnreadable=False):
+    """Generate one Component at a time from a stream.
+
+    >>> import StringIO
+    >>> f = StringIO.StringIO(testVCalendar)
+    >>> cal=readComponents(f).next()
+    >>> cal
+    <VCALENDAR| [<VEVENT| [<SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>]>]>
+    >>> cal.vevent.summary
+    <SUMMARY{u'BLAH': [u'hi!']}Bastille Day Party>
+    
+    """
+    if isinstance(streamOrString, basestring):
+        stream = StringIO.StringIO(streamOrString)
+    else:
+        stream = streamOrString
+
+    try:
+        stack = Stack()
+        versionLine = None
+        n = 0
+        for line, n in getLogicalLines(stream, False, findBegin):
+            if ignoreUnreadable:
+                try:
+                    vline = textLineToContentLine(line, n)
+                except VObjectError, e:
+                    if e.lineNumber is not None:
+                        msg = "Skipped line %(lineNumber)s, message: %(msg)s"
+                    else:
+                        msg = "Skipped a line, message: %(msg)s"
+                    logger.error(msg % {'lineNumber' : e.lineNumber, 
+                                        'msg' : e.message})
+                    continue
+            else:
+                vline = textLineToContentLine(line, n)
+            if   vline.name == "VERSION":
+                versionLine = vline
+                stack.modifyTop(vline)
+            elif vline.name == "BEGIN":
+                stack.push(Component(vline.value, group=vline.group))
+            elif vline.name == "PROFILE":
+                if not stack.top(): stack.push(Component())
+                stack.top().setProfile(vline.value)
+            elif vline.name == "END":
+                if len(stack) == 0:
+                    err = "Attempted to end the %s component, \
+                           but it was never opened" % vline.value
+                    raise ParseError(err, n)
+                if vline.value.upper() == stack.topName(): #START matches END
+                    if len(stack) == 1:
+                        component=stack.pop()
+                        if versionLine is not None:
+                            component.setBehaviorFromVersionLine(versionLine)
+                        if validate: component.validate(raiseException=True)
+                        if transform: component.transformChildrenToNative()
+                        yield component #EXIT POINT
+                    else: stack.modifyTop(stack.pop())
+                else:
+                    err = "%s component wasn't closed" 
+                    raise ParseError(err % stack.topName(), n)
+            else: stack.modifyTop(vline) #not a START or END line
+        if stack.top():
+            if stack.topName() is None:
+                logger.warning("Top level component was never named")
+            elif stack.top().useBegin:
+                raise ParseError("Component %s was never closed" % (stack.topName()), n)
+            yield stack.pop()
+
+    except ParseError, e:
+        e.input = streamOrString
+        raise
+
+
+def readOne(stream, validate=False, transform=True, findBegin=True,
+            ignoreUnreadable=False):
+    """Return the first component from stream."""
+    return readComponents(stream, validate, transform, findBegin,
+                          ignoreUnreadable).next()
+
+#--------------------------- version registry ----------------------------------
+__behaviorRegistry={}
+
+def registerBehavior(behavior, name=None, default=False, id=None):
+    """Register the given behavior.
+    
+    If default is True (or if this is the first version registered with this 
+    name), the version will be the default if no id is given.
+    
+    """
+    if not name: name=behavior.name.upper()
+    if id is None: id=behavior.versionString
+    if name in __behaviorRegistry:
+        if default:
+            __behaviorRegistry[name].insert(0, (id, behavior))
+        else:
+            __behaviorRegistry[name].append((id, behavior))
+    else:
+        __behaviorRegistry[name]=[(id, behavior)]
+
+def getBehavior(name, id=None):
+    """Return a matching behavior if it exists, or None.
+    
+    If id is None, return the default for name.
+    
+    """
+    name=name.upper()
+    if name in __behaviorRegistry:
+        if id:
+            for n, behavior in __behaviorRegistry[name]:
+                if n==id:
+                    return behavior
+        else:
+            return __behaviorRegistry[name][0][1]
+    return None
+
+def newFromBehavior(name, id=None):
+    """Given a name, return a behaviored ContentLine or Component."""
+    name = name.upper()
+    behavior = getBehavior(name, id)
+    if behavior is None:
+        raise VObjectError("No behavior found named %s" % name)
+    if behavior.isComponent:
+        obj = Component(name)
+    else:
+        obj = ContentLine(name, [], '')
+    obj.behavior = behavior
+    obj.isNative = True
+    return obj
+
+
+#--------------------------- Helper function -----------------------------------
+def backslashEscape(s):
+    s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,")
+    return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n")
+
+#------------------- Testing and running functions -----------------------------
+if __name__ == '__main__':
+    import tests
+    tests._test()   
diff -r 2dde35b02026 MoinMoin/support/vobject/behavior.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/behavior.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,164 @@
+"""Behavior (validation, encoding, and transformations) for vobjects."""
+
+import base
+
+#------------------------ Abstract class for behavior --------------------------
+class Behavior(object):
+    """Abstract class to describe vobject options, requirements and encodings.
+    
+    Behaviors are used for root components like VCALENDAR, for subcomponents
+    like VEVENT, and for individual lines in components.
+    
+    Behavior subclasses are not meant to be instantiated, all methods should
+    be classmethods.
+    
+    @cvar name:
+        The uppercase name of the object described by the class, or a generic
+        name if the class defines behavior for many objects.
+    @cvar description:
+        A brief excerpt from the RFC explaining the function of the component or
+        line.
+    @cvar versionString:
+        The string associated with the component, for instance, 2.0 if there's a
+        line like VERSION:2.0, an empty string otherwise.
+    @cvar knownChildren:
+        A dictionary with uppercased component/property names as keys and a
+        tuple (min, max, id) as value, where id is the id used by
+        L{registerBehavior}, min and max are the limits on how many of this child
+        must occur.  None is used to denote no max or no id.
+    @cvar quotedPrintable:
+        A boolean describing whether the object should be encoded and decoded
+        using quoted printable line folding and character escaping.
+    @cvar defaultBehavior:
+        Behavior to apply to ContentLine children when no behavior is found.
+    @cvar hasNative:
+        A boolean describing whether the object can be transformed into a more
+        Pythonic object.
+    @cvar isComponent:
+        A boolean, True if the object should be a Component.
+    @cvar sortFirst:
+        The lower-case list of children which should come first when sorting.
+    @cvar allowGroup:
+        Whether or not vCard style group prefixes are allowed.
+    """
+    name=''
+    description=''
+    versionString=''
+    knownChildren = {}
+    quotedPrintable = False
+    defaultBehavior = None
+    hasNative= False
+    isComponent = False
+    allowGroup = False
+    forceUTC = False
+    sortFirst = []
+
+    def __init__(self):
+        err="Behavior subclasses are not meant to be instantiated"
+        raise base.VObjectError(err)
+   
+    @classmethod
+    def validate(cls, obj, raiseException=False, complainUnrecognized=False):
+        """Check if the object satisfies this behavior's requirements.
+        
+        @param obj:
+            The L{ContentLine<base.ContentLine>} or
+            L{Component<base.Component>} to be validated.
+        @param raiseException:
+            If True, raise a L{base.ValidateError} on validation failure.
+            Otherwise return a boolean.
+        @param complainUnrecognized:
+            If True, fail to validate if an uncrecognized parameter or child is
+            found.  Otherwise log the lack of recognition.
+
+        """
+        if not cls.allowGroup and obj.group is not None:
+            err = str(obj) + " has a group, but this object doesn't support groups"
+            raise base.VObjectError(err)
+        if isinstance(obj, base.ContentLine):
+            return cls.lineValidate(obj, raiseException, complainUnrecognized)
+        elif isinstance(obj, base.Component):
+            count = {}
+            for child in obj.getChildren():
+                if not child.validate(raiseException, complainUnrecognized):
+                    return False
+                name=child.name.upper()
+                count[name] = count.get(name, 0) + 1
+            for key, val in cls.knownChildren.iteritems():
+                if count.get(key,0) < val[0]: 
+                    if raiseException:
+                        m = "%s components must contain at least %i %s"
+                        raise base.ValidateError(m % (cls.name, val[0], key))
+                    return False
+                if val[1] and count.get(key,0) > val[1]:
+                    if raiseException:
+                        m = "%s components cannot contain more than %i %s"
+                        raise base.ValidateError(m % (cls.name, val[1], key))
+                    return False
+            return True
+        else:
+            err = str(obj) + " is not a Component or Contentline"
+            raise base.VObjectError(err)
+    
+    @classmethod
+    def lineValidate(cls, line, raiseException, complainUnrecognized):
+        """Examine a line's parameters and values, return True if valid."""
+        return True
+
+    @classmethod
+    def decode(cls, line):
+        if line.encoded: line.encoded=0
+    
+    @classmethod
+    def encode(cls, line):
+        if not line.encoded: line.encoded=1
+
+    @classmethod
+    def transformToNative(cls, obj):
+        """Turn a ContentLine or Component into a Python-native representation.
+        
+        If appropriate, turn dates or datetime strings into Python objects.
+        Components containing VTIMEZONEs turn into VtimezoneComponents.
+        
+        """
+        return obj
+    
+    @classmethod
+    def transformFromNative(cls, obj):
+        """Inverse of transformToNative."""
+        raise base.NativeError("No transformFromNative defined")
+    
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """Generate any required information that don't yet exist."""
+        pass
+    
+    @classmethod
+    def serialize(cls, obj, buf, lineLength, validate=True):
+        """Set implicit parameters, do encoding, return unicode string.
+        
+        If validate is True, raise VObjectError if the line doesn't validate
+        after implicit parameters are generated.
+        
+        Default is to call base.defaultSerialize.
+        
+        """
+      
+        cls.generateImplicitParameters(obj)
+        if validate: cls.validate(obj, raiseException=True)
+        
+        if obj.isNative:
+            transformed = obj.transformFromNative()
+            undoTransform = True
+        else:
+            transformed = obj
+            undoTransform = False
+        
+        out = base.defaultSerialize(transformed, buf, lineLength)
+        if undoTransform: obj.transformToNative()
+        return out
+    
+    @classmethod
+    def valueRepr( cls, line ):
+        """return the representation of the given content line value"""
+        return line.value
\ No newline at end of file
diff -r 2dde35b02026 MoinMoin/support/vobject/hcalendar.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/hcalendar.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,125 @@
+"""
+hCalendar: A microformat for serializing iCalendar data
+          (http://microformats.org/wiki/hcalendar)
+
+Here is a sample event in an iCalendar:
+
+BEGIN:VCALENDAR
+PRODID:-//XYZproduct//EN
+VERSION:2.0
+BEGIN:VEVENT
+URL:http://www.web2con.com/
+DTSTART:20051005
+DTEND:20051008
+SUMMARY:Web 2.0 Conference
+LOCATION:Argent Hotel\, San Francisco\, CA
+END:VEVENT
+END:VCALENDAR
+
+and an equivalent event in hCalendar format with various elements optimized appropriately.
+
+<span class="vevent">
+ <a class="url" href="http://www.web2con.com/">
+  <span class="summary">Web 2.0 Conference</span>: 
+  <abbr class="dtstart" title="2005-10-05">October 5</abbr>-
+  <abbr class="dtend" title="2005-10-08">7</abbr>,
+ at the <span class="location">Argent Hotel, San Francisco, CA</span>
+ </a>
+</span>
+"""
+
+from base import foldOneLine, CRLF, registerBehavior
+from icalendar import VCalendar2_0
+from datetime import date, datetime, timedelta
+import StringIO
+
+class HCalendar(VCalendar2_0):
+    name = 'HCALENDAR'
+    
+    @classmethod
+    def serialize(cls, obj, buf=None, lineLength=None, validate=True):
+        """
+        Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
+        """
+        
+        outbuf = buf or StringIO.StringIO()
+        level = 0 # holds current indentation level        
+        tabwidth = 3
+        
+        def indent():
+            return ' ' * level * tabwidth
+        
+        def out(s):
+            outbuf.write(indent())
+            outbuf.write(s)
+        
+        # not serializing optional vcalendar wrapper
+        
+        vevents = obj.vevent_list
+        
+        for event in vevents:
+            out('<span class="vevent">' + CRLF)
+            level += 1
+            
+            # URL
+            url = event.getChildValue("url")
+            if url:
+                out('<a class="url" href="' + url + '">' + CRLF)
+                level += 1
+            # SUMMARY
+            summary = event.getChildValue("summary")
+            if summary:
+                out('<span class="summary">' + summary + '</span>:' + CRLF)
+            
+            # DTSTART
+            dtstart = event.getChildValue("dtstart")
+            if dtstart:
+                if type(dtstart) == date:
+                    timeformat = "%A, %B %e"
+                    machine    = "%Y%m%d"
+                elif type(dtstart) == datetime:
+                    timeformat = "%A, %B %e, %H:%M"
+                    machine    = "%Y%m%dT%H%M%S%z"
+
+                #TODO: Handle non-datetime formats?
+                #TODO: Spec says we should handle when dtstart isn't included
+                
+                out('<abbr class="dtstart", title="%s">%s</abbr>\r\n' % 
+                     (dtstart.strftime(machine), dtstart.strftime(timeformat)))
+                
+                # DTEND
+                dtend = event.getChildValue("dtend")
+                if not dtend:
+                    duration = event.getChildValue("duration")
+                    if duration:
+                        dtend = duration + dtstart
+                   # TODO: If lacking dtend & duration?
+               
+                if dtend:
+                    human = dtend
+                    # TODO: Human readable part could be smarter, excluding repeated data
+                    if type(dtend) == date:
+                        human = dtend - timedelta(days=1)
+                        
+                    out('- <abbr class="dtend", title="%s">%s</abbr>\r\n' % 
+                     (dtend.strftime(machine), human.strftime(timeformat)))    
+
+            # LOCATION    
+            location = event.getChildValue("location")
+            if location:
+                out('at <span class="location">' + location + '</span>' + CRLF)
+        
+            description = event.getChildValue("description")
+            if description:
+                out('<div class="description">' + description + '</div>' + CRLF)
+            
+            if url:
+                level -= 1
+                out('</a>' + CRLF)
+            
+            level -= 1                
+            out('</span>' + CRLF) # close vevent
+
+        return buf or outbuf.getvalue()
+    
+registerBehavior(HCalendar)
\ No newline at end of file
diff -r 2dde35b02026 MoinMoin/support/vobject/icalendar.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/icalendar.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,1769 @@
+"""Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
+
+import string
+import behavior
+import dateutil.rrule
+import dateutil.tz
+import StringIO
+import datetime
+import socket, random #for generating a UID
+import itertools
+
+from base import VObjectError, NativeError, ValidateError, ParseError, \
+                    VBase, Component, ContentLine, logger, defaultSerialize, \
+                    registerBehavior, backslashEscape, foldOneLine, \
+                    newFromBehavior, CRLF, LF
+
+#------------------------------- Constants -------------------------------------
+DATENAMES = ("rdate", "exdate")
+RULENAMES = ("exrule", "rrule")
+DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
+PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
+
+WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
+FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
+               'SECONDLY')
+
+zeroDelta = datetime.timedelta(0)
+twoHours  = datetime.timedelta(hours=2)
+
+#---------------------------- TZID registry ------------------------------------
+__tzidMap={}
+
+def registerTzid(tzid, tzinfo):
+    """Register a tzid -> tzinfo mapping."""
+    __tzidMap[tzid]=tzinfo
+
+def getTzid(tzid):
+    """Return the tzid if it exists, or None."""
+    return __tzidMap.get(tzid, None)
+
+utc = dateutil.tz.tzutc()
+registerTzid("UTC", utc)
+
+#-------------------- Helper subclasses ----------------------------------------
+
+class TimezoneComponent(Component):
+    """A VTIMEZONE object.
+    
+    VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo
+    subclass is stored in self.tzinfo, self.tzid stores the TZID associated
+    with this timezone.
+    
+    @ivar name:
+        The uppercased name of the object, in this case always 'VTIMEZONE'.
+    @ivar tzinfo:
+        A datetime.tzinfo subclass representing this timezone.
+    @ivar tzid:
+        The string used to refer to this timezone.
+    
+    """    
+    def __init__(self, tzinfo=None, *args, **kwds):
+        """Accept an existing Component or a tzinfo class."""
+        super(TimezoneComponent, self).__init__(*args, **kwds)
+        self.isNative=True
+        # hack to make sure a behavior is assigned
+        if self.behavior is None:
+            self.behavior = VTimezone
+        if tzinfo is not None:
+            self.tzinfo = tzinfo
+        if not hasattr(self, 'name') or self.name == '':
+            self.name = 'VTIMEZONE'
+            self.useBegin = True
+
+    @classmethod
+    def registerTzinfo(obj, tzinfo):
+        """Register tzinfo if it's not already registered, return its tzid."""
+        tzid = obj.pickTzid(tzinfo)
+        if tzid and not getTzid(tzid):
+            registerTzid(tzid, tzinfo)
+        return tzid
+
+    def gettzinfo(self):
+        # workaround for dateutil failing to parse some experimental properties
+        good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
+                      'tzoffsetto', 'tzid')
+        buffer = StringIO.StringIO()
+        # allow empty VTIMEZONEs
+        if len(self.contents) == 0:
+            return None
+        def customSerialize(obj):
+            if isinstance(obj, Component):
+                foldOneLine(buffer, u"BEGIN:" + obj.name)
+                for child in obj.lines():
+                    if child.name.lower() in good_lines:
+                        child.serialize(buffer, 75, validate=False)
+                for comp in obj.components():
+                    customSerialize(comp)
+                foldOneLine(buffer, u"END:" + obj.name)
+        customSerialize(self)
+        return dateutil.tz.tzical(StringIO.StringIO(str(buffer.getvalue()))).get()
+
+    def settzinfo(self, tzinfo, start=2000, end=2030):
+        """Create appropriate objects in self to represent tzinfo.
+        
+        Collapse DST transitions to rrules as much as possible.
+        
+        Assumptions:
+        - DST <-> Standard transitions occur on the hour
+        - never within a month of one another
+        - twice or fewer times a year
+        - never in the month of December
+        - DST always moves offset exactly one hour later
+        - tzinfo classes dst method always treats times that could be in either
+          offset as being in the later regime
+        
+        """  
+        def fromLastWeek(dt):
+            """How many weeks from the end of the month dt is, starting from 1."""
+            weekDelta = datetime.timedelta(weeks=1)
+            n = 1
+            current = dt + weekDelta
+            while current.month == dt.month:
+                n += 1
+                current += weekDelta
+            return n
+        
+        # lists of dictionaries defining rules which are no longer in effect
+        completed = {'daylight' : [], 'standard' : []}
+    
+        # dictionary defining rules which are currently in effect
+        working   = {'daylight' : None, 'standard' : None}
+        
+        # rule may be based on the nth week of the month or the nth from the last
+        for year in xrange(start, end + 1):
+            newyear = datetime.datetime(year, 1, 1)
+            for transitionTo in 'daylight', 'standard':
+                transition = getTransition(transitionTo, year, tzinfo)
+                oldrule = working[transitionTo]
+    
+                if transition == newyear:
+                    # transitionTo is in effect for the whole year
+                    rule = {'end'        : None,
+                            'start'      : newyear,
+                            'month'      : 1,
+                            'weekday'    : None,
+                            'hour'       : None,
+                            'plus'       : None,
+                            'minus'      : None,
+                            'name'       : tzinfo.tzname(newyear),
+                            'offset'     : tzinfo.utcoffset(newyear),
+                            'offsetfrom' : tzinfo.utcoffset(newyear)}
+                    if oldrule is None:
+                        # transitionTo was not yet in effect
+                        working[transitionTo] = rule
+                    else:
+                        # transitionTo was already in effect
+                        if (oldrule['offset'] != 
+                            tzinfo.utcoffset(newyear)):
+                            # old rule was different, it shouldn't continue
+                            oldrule['end'] = year - 1
+                            completed[transitionTo].append(oldrule)
+                            working[transitionTo] = rule
+                elif transition is None:
+                    # transitionTo is not in effect
+                    if oldrule is not None:
+                        # transitionTo used to be in effect
+                        oldrule['end'] = year - 1
+                        completed[transitionTo].append(oldrule)
+                        working[transitionTo] = None
+                else:
+                    # an offset transition was found
+                    old_offset = tzinfo.utcoffset(transition - twoHours)
+                    rule = {'end'     : None, # None, or an integer year
+                            'start'   : transition, # the datetime of transition
+                            'month'   : transition.month,
+                            'weekday' : transition.weekday(),
+                            'hour'    : transition.hour,
+                            'name'    : tzinfo.tzname(transition),
+                            'plus'    : (transition.day - 1)/ 7 + 1,#nth week of the month
+                            'minus'   : fromLastWeek(transition), #nth from last week
+                            'offset'  : tzinfo.utcoffset(transition), 
+                            'offsetfrom' : old_offset}
+        
+                    if oldrule is None: 
+                        working[transitionTo] = rule
+                    else:
+                        plusMatch  = rule['plus']  == oldrule['plus'] 
+                        minusMatch = rule['minus'] == oldrule['minus'] 
+                        truth = plusMatch or minusMatch
+                        for key in 'month', 'weekday', 'hour', 'offset':
+                            truth = truth and rule[key] == oldrule[key]
+                        if truth:
+                            # the old rule is still true, limit to plus or minus
+                            if not plusMatch:
+                                oldrule['plus'] = None
+                            if not minusMatch:
+                                oldrule['minus'] = None
+                        else:
+                            # the new rule did not match the old
+                            oldrule['end'] = year - 1
+                            completed[transitionTo].append(oldrule)
+                            working[transitionTo] = rule
+    
+        for transitionTo in 'daylight', 'standard':
+            if working[transitionTo] is not None:
+                completed[transitionTo].append(working[transitionTo])
+    
+        self.tzid = []
+        self.daylight = []
+        self.standard = []
+        
+        self.add('tzid').value = self.pickTzid(tzinfo, True)
+        
+        old = None
+        for transitionTo in 'daylight', 'standard':
+            for rule in completed[transitionTo]:
+                comp = self.add(transitionTo)
+                dtstart = comp.add('dtstart')
+                dtstart.value = rule['start']
+                if rule['name'] is not None:
+                    comp.add('tzname').value  = rule['name']
+                line = comp.add('tzoffsetto')
+                line.value = deltaToOffset(rule['offset'])
+                line = comp.add('tzoffsetfrom')
+                line.value = deltaToOffset(rule['offsetfrom'])
+    
+                if rule['plus'] is not None:
+                    num = rule['plus']
+                elif rule['minus'] is not None:
+                    num = -1 * rule['minus']
+                else:
+                    num = None
+                if num is not None:
+                    dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']]
+                else:
+                    dayString = ""
+                if rule['end'] is not None:
+                    if rule['hour'] is None:
+                        # all year offset, with no rule
+                        endDate = datetime.datetime(rule['end'], 1, 1)
+                    else:
+                        weekday = dateutil.rrule.weekday(rule['weekday'], num)
+                        du_rule = dateutil.rrule.rrule(dateutil.rrule.YEARLY,
+                                   bymonth = rule['month'],byweekday = weekday,
+                                   dtstart = datetime.datetime(
+                                       rule['end'], 1, 1, rule['hour'])
+                                  )
+                        endDate = du_rule[0]
+                    endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom']
+                    endString = ";UNTIL="+ dateTimeToString(endDate)
+                else:
+                    endString = ''
+                rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \
+                              (dayString, str(rule['month']), endString)
+                
+                comp.add('rrule').value = rulestring
+
+    tzinfo = property(gettzinfo, settzinfo)
+    # prevent Component's __setattr__ from overriding the tzinfo property
+    normal_attributes = Component.normal_attributes + ['tzinfo']
+
+    @staticmethod
+    def pickTzid(tzinfo, allowUTC=False):
+        """
+        Given a tzinfo class, use known APIs to determine TZID, or use tzname.
+        """
+        if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
+            #If tzinfo is UTC, we don't need a TZID
+            return None
+        # try PyICU's tzid key
+        if hasattr(tzinfo, 'tzid'):
+            return tzinfo.tzid
+            
+        # try pytz zone key
+        if hasattr(tzinfo, 'zone'):
+            return tzinfo.zone
+
+        # try tzical's tzid key
+        elif hasattr(tzinfo, '_tzid'):
+            return tzinfo._tzid
+        else:
+            # return tzname for standard (non-DST) time
+            notDST = datetime.timedelta(0)
+            for month in xrange(1,13):
+                dt = datetime.datetime(2000, month, 1)
+                if tzinfo.dst(dt) == notDST:
+                    return tzinfo.tzname(dt)
+        # there was no standard time in 2000!
+        raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo))
+
+    def __str__(self):
+        return "<VTIMEZONE | " + str(getattr(self, 'tzid', 'No TZID')) +">"
+    
+    def __repr__(self):
+        return self.__str__()
+    
+    def prettyPrint(self, level, tabwidth):
+        pre = ' ' * level * tabwidth
+        print pre, self.name
+        print pre, "TZID:", self.tzid
+        print
+
+class RecurringComponent(Component):
+    """A vCalendar component like VEVENT or VTODO which may recur.
+        
+    Any recurring component can have one or multiple RRULE, RDATE,
+    EXRULE, or EXDATE lines, and one or zero DTSTART lines.  It can also have a
+    variety of children that don't have any recurrence information.  
+    
+    In the example below, note that dtstart is included in the rruleset.
+    This is not the default behavior for dateutil's rrule implementation unless
+    dtstart would already have been a member of the recurrence rule, and as a
+    result, COUNT is wrong. This can be worked around when getting rruleset by
+    adjusting count down by one if an rrule has a count and dtstart isn't in its
+    result set, but by default, the rruleset property doesn't do this work
+    around, to access it getrruleset must be called with addRDate set True.
+    
+    >>> import dateutil.rrule, datetime
+    >>> vevent = RecurringComponent(name='VEVENT')
+    >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
+    >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
+    
+    When creating rrule's programmatically it should be kept in
+    mind that count doesn't necessarily mean what rfc2445 says.
+    
+    >>> list(vevent.rruleset)
+    [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
+    >>> list(vevent.getrruleset(addRDate=True))
+    [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)]
+    
+    Also note that dateutil will expand all-day events (datetime.date values) to
+    datetime.datetime value with time 0 and no timezone.
+    
+    >>> vevent.dtstart.value = datetime.date(2005,3,18)
+    >>> list(vevent.rruleset)
+    [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
+    >>> list(vevent.getrruleset(True))
+    [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
+    
+    @ivar rruleset:
+        A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
+    """
+    def __init__(self, *args, **kwds):
+        super(RecurringComponent, self).__init__(*args, **kwds)
+        self.isNative=True
+        #self.clobberedRDates=[]
+
+
+    def getrruleset(self, addRDate = False):
+        """Get an rruleset created from self.
+        
+        If addRDate is True, add an RDATE for dtstart if it's not included in
+        an RRULE, and count is decremented if it exists.
+        
+        Note that for rules which don't match DTSTART, DTSTART may not appear
+        in list(rruleset), although it should.  By default, an RDATE is not
+        created in these cases, and count isn't updated, so dateutil may list
+        a spurious occurrence.
+        
+        """
+        rruleset = None
+        for name in DATESANDRULES:
+            addfunc = None
+            for line in self.contents.get(name, ()):
+                # don't bother creating a rruleset unless there's a rule
+                if rruleset is None:
+                    rruleset = dateutil.rrule.rruleset()
+                if addfunc is None:
+                    addfunc=getattr(rruleset, name)
+
+                if name in DATENAMES:
+                    if type(line.value[0]) == datetime.datetime:
+                        map(addfunc, line.value)
+                    elif type(line.value[0]) == datetime.date:
+                        for dt in line.value:
+                            addfunc(datetime.datetime(dt.year, dt.month, dt.day))
+                    else:
+                        # ignore RDATEs with PERIOD values for now
+                        pass
+                elif name in RULENAMES:
+                    try:
+                        dtstart = self.dtstart.value
+                    except AttributeError, KeyError:
+                        # Special for VTODO - try DUE property instead
+                        try:
+                            if self.name == "VTODO":
+                                dtstart = self.due.value
+                            else:
+                                # if there's no dtstart, just return None
+                                return None
+                        except AttributeError, KeyError:
+                            # if there's no due, just return None
+                            return None
+
+                    # rrulestr complains about unicode, so cast to str
+                    rule = dateutil.rrule.rrulestr(str(line.value),
+                                                   dtstart=dtstart)
+                    until = rule._until 
+                    if until is not None and \
+                       isinstance(dtstart, datetime.datetime) and \
+                       (until.tzinfo != dtstart.tzinfo): 
+                        # dateutil converts the UNTIL date to a datetime,
+                        # check to see if the UNTIL parameter value was a date
+                        vals = dict(pair.split('=') for pair in
+                                    line.value.upper().split(';'))
+                        if len(vals.get('UNTIL', '')) == 8:
+                            until = datetime.datetime.combine(until.date(),
+                                                              dtstart.time())
+                        # While RFC2445 says UNTIL MUST be UTC, Chandler allows
+                        # floating recurring events, and uses floating UNTIL values.
+                        # Also, some odd floating UNTIL but timezoned DTSTART values
+                        # have shown up in the wild, so put floating UNTIL values
+                        # DTSTART's timezone
+                        if until.tzinfo is None:
+                            until = until.replace(tzinfo=dtstart.tzinfo)
+
+                        if dtstart.tzinfo is not None:
+                            until = until.astimezone(dtstart.tzinfo)
+
+                        rule._until = until
+                    
+                    # add the rrule or exrule to the rruleset
+                    addfunc(rule)
+                    
+                    if name == 'rrule' and addRDate:
+                        try:
+                            # dateutils does not work with all-day (datetime.date) items
+                            # so we need to convert to a datetime.datetime
+                            # (which is what dateutils does internally)
+                            if not isinstance(dtstart, datetime.datetime):
+                                adddtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+                            else:
+                                adddtstart = dtstart
+                            if rruleset._rrule[-1][0] != adddtstart:
+                                rruleset.rdate(adddtstart)
+                                added = True
+                            else:
+                                added = False
+                        except IndexError:
+                            # it's conceivable that an rrule might have 0 datetimes
+                            added = False
+                        if added and rruleset._rrule[-1]._count != None:
+                            rruleset._rrule[-1]._count -= 1
+        return rruleset
+
+    def setrruleset(self, rruleset):
+        
+        # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
+        try:
+            dtstart = self.dtstart.value
+        except AttributeError, KeyError:
+            if self.name == "VTODO":
+                dtstart = self.due.value
+            else:
+                raise
+            
+        isDate = datetime.date == type(dtstart)
+        if isDate:
+            dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day)
+            untilSerialize = dateToString
+        else:
+            # make sure to convert time zones to UTC
+            untilSerialize = lambda x: dateTimeToString(x, True)
+
+        for name in DATESANDRULES:
+            if hasattr(self.contents, name):
+                del self.contents[name]
+            setlist = getattr(rruleset, '_' + name)
+            if name in DATENAMES:
+                setlist = list(setlist) # make a copy of the list
+                if name == 'rdate' and dtstart in setlist:
+                    setlist.remove(dtstart)
+                if isDate:
+                    setlist = [dt.date() for dt in setlist]
+                if len(setlist) > 0:
+                    self.add(name).value = setlist
+            elif name in RULENAMES:
+                for rule in setlist:
+                    buf = StringIO.StringIO()
+                    buf.write('FREQ=')
+                    buf.write(FREQUENCIES[rule._freq])
+                    
+                    values = {}
+                    
+                    if rule._interval != 1:
+                        values['INTERVAL'] = [str(rule._interval)]
+                    if rule._wkst != 0: # wkst defaults to Monday
+                        values['WKST'] = [WEEKDAYS[rule._wkst]]
+                    if rule._bysetpos is not None:
+                        values['BYSETPOS'] = [str(i) for i in rule._bysetpos]
+                    
+                    if rule._count is not None:
+                        values['COUNT'] = [str(rule._count)]
+                    elif rule._until is not None:
+                        values['UNTIL'] = [untilSerialize(rule._until)]
+
+                    days = []
+                    if (rule._byweekday is not None and (
+                                  dateutil.rrule.WEEKLY != rule._freq or 
+                                   len(rule._byweekday) != 1 or 
+                                rule._dtstart.weekday() != rule._byweekday[0])):
+                        # ignore byweekday if freq is WEEKLY and day correlates
+                        # with dtstart because it was automatically set by
+                        # dateutil
+                        days.extend(WEEKDAYS[n] for n in rule._byweekday)    
+                        
+                    if rule._bynweekday is not None:
+                        days.extend(str(n) + WEEKDAYS[day] for day, n in rule._bynweekday)
+                        
+                    if len(days) > 0:
+                        values['BYDAY'] = days 
+                                                            
+                    if rule._bymonthday is not None and len(rule._bymonthday) > 0:
+                        if not (rule._freq <= dateutil.rrule.MONTHLY and
+                                len(rule._bymonthday) == 1 and
+                                rule._bymonthday[0] == rule._dtstart.day):
+                            # ignore bymonthday if it's generated by dateutil
+                            values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday]
+
+                    if rule._bymonth is not None and len(rule._bymonth) > 0:
+                        if (rule._byweekday is not None or
+                            len(rule._bynweekday or ()) > 0 or
+                            not (rule._freq == dateutil.rrule.YEARLY and
+                                 len(rule._bymonth) == 1 and
+                                 rule._bymonth[0] == rule._dtstart.month)):
+                            # ignore bymonth if it's generated by dateutil
+                            values['BYMONTH'] = [str(n) for n in rule._bymonth]
+
+                    if rule._byyearday is not None:
+                        values['BYYEARDAY'] = [str(n) for n in rule._byyearday]
+                    if rule._byweekno is not None:
+                        values['BYWEEKNO'] = [str(n) for n in rule._byweekno]
+
+                    # byhour, byminute, bysecond are always ignored for now
+
+                    
+                    for key, paramvals in values.iteritems():
+                        buf.write(';')
+                        buf.write(key)
+                        buf.write('=')
+                        buf.write(','.join(paramvals))
+
+                    self.add(name).value = buf.getvalue()
+
+
+            
+    rruleset = property(getrruleset, setrruleset)
+
+    def __setattr__(self, name, value):
+        """For convenience, make self.contents directly accessible."""
+        if name == 'rruleset':
+            self.setrruleset(value)
+        else:
+            super(RecurringComponent, self).__setattr__(name, value)
+
+class TextBehavior(behavior.Behavior):
+    """Provide backslash escape encoding/decoding for single valued properties.
+    
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    
+    """
+    base64string = 'BASE64' # vCard uses B
+    
+    @classmethod
+    def decode(cls, line):
+        """Remove backslash escaping from line.value."""
+        if line.encoded:
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding and encoding.upper() == cls.base64string:
+                line.value = line.value.decode('base64')
+            else:
+                line.value = stringToTextValues(line.value)[0]
+            line.encoded=False
+    
+    @classmethod
+    def encode(cls, line):
+        """Backslash escape line.value."""
+        if not line.encoded:
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding and encoding.upper() == cls.base64string:
+                line.value = line.value.encode('base64').replace('\n', '')
+            else:
+                line.value = backslashEscape(line.value)
+            line.encoded=True
+
+class VCalendarComponentBehavior(behavior.Behavior):
+    defaultBehavior = TextBehavior
+    isComponent = True
+
+class RecurringBehavior(VCalendarComponentBehavior):
+    """Parent Behavior for components which should be RecurringComponents."""
+    hasNative = True
+    
+    @staticmethod
+    def transformToNative(obj):
+        """Turn a recurring Component into a RecurringComponent."""
+        if not obj.isNative:
+            object.__setattr__(obj, '__class__', RecurringComponent)
+            obj.isNative = True
+        return obj
+    
+    @staticmethod
+    def transformFromNative(obj):
+        if obj.isNative:
+            object.__setattr__(obj, '__class__', Component)
+            obj.isNative = False
+        return obj
+    
+    @staticmethod        
+    def generateImplicitParameters(obj):
+        """Generate a UID if one does not exist.
+        
+        This is just a dummy implementation, for now.
+        
+        """
+        if not hasattr(obj, 'uid'):
+            rand = str(int(random.random() * 100000))
+            now = datetime.datetime.now(utc)
+            now = dateTimeToString(now)
+            host = socket.gethostname()
+            obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host))        
+            
+    
+class DateTimeBehavior(behavior.Behavior):
+    """Parent Behavior for ContentLines containing one DATE-TIME."""
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a datetime.
+
+        RFC2445 allows times without time zone information, "floating times"
+        in some properties.  Mostly, this isn't what you want, but when parsing
+        a file, real floating times are noted by setting to 'TRUE' the
+        X-VOBJ-FLOATINGTIME-ALLOWED parameter.
+
+        """
+        if obj.isNative: return obj
+        obj.isNative = True
+        if obj.value == '': return obj
+        obj.value=str(obj.value)
+        #we're cheating a little here, parseDtstart allows DATE
+        obj.value=parseDtstart(obj)
+        if obj.value.tzinfo is None:
+            obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
+        if obj.params.get('TZID'):
+            # Keep a copy of the original TZID around
+            obj.params['X-VOBJ-ORIGINAL-TZID'] = obj.params['TZID']
+            del obj.params['TZID']
+        return obj
+
+    @classmethod
+    def transformFromNative(cls, obj):
+        """Replace the datetime in obj.value with an ISO 8601 string."""
+        if obj.isNative:
+            obj.isNative = False
+            tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
+            obj.value = dateTimeToString(obj.value, cls.forceUTC)
+            if not cls.forceUTC and tzid is not None:
+                obj.tzid_param = tzid
+            if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
+                if not hasattr(obj, 'tzid_param'):
+                    obj.tzid_param = obj.params['X-VOBJ-ORIGINAL-TZID']
+                del obj.params['X-VOBJ-ORIGINAL-TZID']
+
+        return obj
+
+class UTCDateTimeBehavior(DateTimeBehavior):
+    """A value which must be specified in UTC."""
+    forceUTC = True
+
+class DateOrDateTimeBehavior(behavior.Behavior):
+    """Parent Behavior for ContentLines containing one DATE or DATE-TIME."""
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a date or datetime."""
+        if obj.isNative: return obj
+        obj.isNative = True
+        if obj.value == '': return obj
+        obj.value=str(obj.value)
+        obj.value=parseDtstart(obj, allowSignatureMismatch=True)
+        if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
+            if hasattr(obj, 'tzid_param'):
+                # Keep a copy of the original TZID around
+                obj.params['X-VOBJ-ORIGINAL-TZID'] = obj.tzid_param
+                del obj.tzid_param
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """Replace the date or datetime in obj.value with an ISO 8601 string."""
+        if type(obj.value) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = dateToString(obj.value)
+            return obj
+        else: return DateTimeBehavior.transformFromNative(obj)
+
+class MultiDateBehavior(behavior.Behavior):
+    """
+    Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
+    PERIOD.
+    
+    """
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """
+        Turn obj.value into a list of dates, datetimes, or
+        (datetime, timedelta) tuples.
+        
+        """
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            obj.value = []
+            return obj
+        tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+        valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
+        valTexts = obj.value.split(",")
+        if valueParam == "DATE":
+            obj.value = [stringToDate(x) for x in valTexts]
+        elif valueParam == "DATE-TIME":
+            obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
+        elif valueParam == "PERIOD":
+            obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """
+        Replace the date, datetime or period tuples in obj.value with
+        appropriate strings.
+        
+        """
+        if obj.value and type(obj.value[0]) == datetime.date:
+            obj.isNative = False
+            obj.value_param = 'DATE'
+            obj.value = ','.join([dateToString(val) for val in obj.value])
+            return obj
+        # Fixme: handle PERIOD case
+        else:
+            if obj.isNative:
+                obj.isNative = False
+                transformed = []
+                tzid = None
+                for val in obj.value:
+                    if tzid is None and type(val) == datetime.datetime:
+                        tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
+                        if tzid is not None:
+                            obj.tzid_param = tzid
+                    transformed.append(dateTimeToString(val))
+                obj.value = ','.join(transformed)
+            return obj
+
+class MultiTextBehavior(behavior.Behavior):
+    """Provide backslash escape encoding/decoding of each of several values.
+    
+    After transformation, value is a list of strings.
+    
+    """
+
+    @staticmethod
+    def decode(line):
+        """Remove backslash escaping from line.value, then split on commas."""
+        if line.encoded:
+            line.value = stringToTextValues(line.value)
+            line.encoded=False
+    
+    @staticmethod
+    def encode(line):
+        """Backslash escape line.value."""
+        if not line.encoded:
+            line.value = ','.join(backslashEscape(val) for val in line.value)
+            line.encoded=True
+    
+
+#------------------------ Registered Behavior subclasses -----------------------
+class VCalendar2_0(VCalendarComponentBehavior):
+    """vCalendar 2.0 behavior."""
+    name = 'VCALENDAR'
+    description = 'vCalendar 2.0, also known as iCalendar.'
+    versionString = '2.0'
+    sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
+    knownChildren = {'CALSCALE':  (0, 1, None),#min, max, behaviorRegistry id
+                     'METHOD':    (0, 1, None),
+                     'VERSION':   (0, 1, None),#required, but auto-generated
+                     'PRODID':    (1, 1, None),
+                     'VTIMEZONE': (0, None, None),
+                     'VEVENT':    (0, None, None),
+                     'VTODO':     (0, None, None),
+                     'VJOURNAL':  (0, None, None),
+                     'VFREEBUSY': (0, None, None)
+                    }
+                    
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """Create PRODID, VERSION, and VTIMEZONEs if needed.
+        
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        
+        """
+        for comp in obj.components():
+            if comp.behavior is not None:
+                comp.behavior.generateImplicitParameters(comp)
+        if not hasattr(obj, 'prodid'):
+            obj.add(ContentLine('PRODID', [], PRODID))
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+        tzidsUsed = {}
+
+        def findTzids(obj, table):
+            if isinstance(obj, ContentLine) and (obj.behavior is None or
+                                                 not obj.behavior.forceUTC):
+                if getattr(obj, 'tzid_param', None):
+                    table[obj.tzid_param] = 1
+                else:
+                    if type(obj.value) == list:
+                        for item in obj.value:
+                            tzinfo = getattr(obj.value, 'tzinfo', None)
+                            tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                            if tzid:
+                                table[tzid] = 1
+                    else:
+                        tzinfo = getattr(obj.value, 'tzinfo', None)
+                        tzid = TimezoneComponent.registerTzinfo(tzinfo)
+                        if tzid:
+                            table[tzid] = 1
+            for child in obj.getChildren():
+                if obj.name != 'VTIMEZONE':
+                    findTzids(child, table)
+        
+        findTzids(obj, tzidsUsed)
+        oldtzids = [x.tzid.value for x in getattr(obj, 'vtimezone_list', [])]
+        for tzid in tzidsUsed.keys():
+            if tzid != 'UTC' and tzid not in oldtzids:
+                obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
+registerBehavior(VCalendar2_0)
+
+class VTimezone(VCalendarComponentBehavior):
+    """Timezone behavior."""
+    name = 'VTIMEZONE'
+    hasNative = True
+    description = 'A grouping of component properties that defines a time zone.'
+    sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
+    knownChildren = {'TZID':         (1, 1, None),#min, max, behaviorRegistry id
+                     'LAST-MODIFIED':(0, 1, None),
+                     'TZURL':        (0, 1, None),
+                     'STANDARD':     (0, None, None),#NOTE: One of Standard or
+                     'DAYLIGHT':     (0, None, None) #      Daylight must appear
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if not hasattr(obj, 'tzid') or obj.tzid.value is None:
+            if raiseException:
+                m = "VTIMEZONE components must contain a valid TZID"
+                raise ValidateError(m)
+            return False            
+        if obj.contents.has_key('standard') or obj.contents.has_key('daylight'):
+            return super(VTimezone, cls).validate(obj, raiseException, *args)
+        else:
+            if raiseException:
+                m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\
+                     component"
+                raise ValidateError(m)
+            return False
+
+
+    @staticmethod
+    def transformToNative(obj):
+        if not obj.isNative:
+            object.__setattr__(obj, '__class__', TimezoneComponent)
+            obj.isNative = True
+            obj.registerTzinfo(obj.tzinfo)
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        return obj
+
+        
+registerBehavior(VTimezone)
+
+class DaylightOrStandard(VCalendarComponentBehavior):
+    hasNative = False
+    knownChildren = {'DTSTART':      (1, 1, None),#min, max, behaviorRegistry id
+                     'RRULE':        (0, 1, None)}
+
+registerBehavior(DaylightOrStandard, 'STANDARD')
+registerBehavior(DaylightOrStandard, 'DAYLIGHT')
+
+
+class VEvent(RecurringBehavior):
+    """Event behavior."""
+    name='VEVENT'
+    sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
+
+    description='A grouping of component properties, and possibly including \
+                 "VALARM" calendar components, that represents a scheduled \
+                 amount of time on a calendar.'
+    knownChildren = {'DTSTART':      (0, 1, None),#min, max, behaviorRegistry id
+                     'CLASS':        (0, 1, None),  
+                     'CREATED':      (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None),  
+                     'GEO':          (0, 1, None),  
+                     'LAST-MODIFIED':(0, 1, None),
+                     'LOCATION':     (0, 1, None),  
+                     'ORGANIZER':    (0, 1, None),  
+                     'PRIORITY':     (0, 1, None),  
+                     'DTSTAMP':      (0, 1, None),
+                     'SEQUENCE':     (0, 1, None),  
+                     'STATUS':       (0, 1, None),  
+                     'SUMMARY':      (0, 1, None),                     
+                     'TRANSP':       (0, 1, None),  
+                     'UID':          (1, 1, None),  
+                     'URL':          (0, 1, None),  
+                     'RECURRENCE-ID':(0, 1, None),  
+                     'DTEND':        (0, 1, None), #NOTE: Only one of DtEnd or
+                     'DURATION':     (0, 1, None), #      Duration can appear
+                     'ATTACH':       (0, None, None),
+                     'ATTENDEE':     (0, None, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'EXRULE':       (0, None, None),
+                     'REQUEST-STATUS': (0, None, None),
+                     'RELATED-TO':   (0, None, None),
+                     'RESOURCES':    (0, None, None),
+                     'RDATE':        (0, None, None),
+                     'RRULE':        (0, None, None),
+                     'VALARM':       (0, None, None)
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'):
+            if raiseException:
+                m = "VEVENT components cannot contain both DTEND and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VEvent, cls).validate(obj, raiseException, *args)
+      
+registerBehavior(VEvent)
+
+
+class VTodo(RecurringBehavior):
+    """To-do behavior."""
+    name='VTODO'
+    description='A grouping of component properties and possibly "VALARM" \
+                 calendar components that represent an action-item or \
+                 assignment.'
+    knownChildren = {'DTSTART':      (0, 1, None),#min, max, behaviorRegistry id
+                     'CLASS':        (0, 1, None),
+                     'COMPLETED':    (0, 1, None),
+                     'CREATED':      (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None),  
+                     'GEO':          (0, 1, None),  
+                     'LAST-MODIFIED':(0, 1, None),
+                     'LOCATION':     (0, 1, None),  
+                     'ORGANIZER':    (0, 1, None),  
+                     'PERCENT':      (0, 1, None),  
+                     'PRIORITY':     (0, 1, None),  
+                     'DTSTAMP':      (0, 1, None),
+                     'SEQUENCE':     (0, 1, None),  
+                     'STATUS':       (0, 1, None),  
+                     'SUMMARY':      (0, 1, None),
+                     'UID':          (0, 1, None),  
+                     'URL':          (0, 1, None),  
+                     'RECURRENCE-ID':(0, 1, None),  
+                     'DUE':          (0, 1, None), #NOTE: Only one of Due or
+                     'DURATION':     (0, 1, None), #      Duration can appear
+                     'ATTACH':       (0, None, None),
+                     'ATTENDEE':     (0, None, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'EXRULE':       (0, None, None),
+                     'REQUEST-STATUS': (0, None, None),
+                     'RELATED-TO':   (0, None, None),
+                     'RESOURCES':    (0, None, None),
+                     'RDATE':        (0, None, None),
+                     'RRULE':        (0, None, None),
+                     'VALARM':       (0, None, None)
+                    }
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        if obj.contents.has_key('DUE') and obj.contents.has_key('DURATION'):
+            if raiseException:
+                m = "VTODO components cannot contain both DUE and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VTodo, cls).validate(obj, raiseException, *args)
+      
+registerBehavior(VTodo)
+
+
+class VJournal(RecurringBehavior):
+    """Journal entry behavior."""
+    name='VJOURNAL'
+    knownChildren = {'DTSTART':      (0, 1, None),#min, max, behaviorRegistry id
+                     'CLASS':        (0, 1, None),  
+                     'CREATED':      (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None),  
+                     'LAST-MODIFIED':(0, 1, None),
+                     'ORGANIZER':    (0, 1, None),  
+                     'DTSTAMP':      (0, 1, None),
+                     'SEQUENCE':     (0, 1, None),  
+                     'STATUS':       (0, 1, None),  
+                     'SUMMARY':      (0, 1, None),                     
+                     'UID':          (0, 1, None),  
+                     'URL':          (0, 1, None),  
+                     'RECURRENCE-ID':(0, 1, None),  
+                     'ATTACH':       (0, None, None),
+                     'ATTENDEE':     (0, None, None),
+                     'CATEGORIES':   (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'CONTACT':      (0, None, None),
+                     'EXDATE':       (0, None, None),
+                     'EXRULE':       (0, None, None),
+                     'REQUEST-STATUS': (0, None, None),
+                     'RELATED-TO':   (0, None, None),
+                     'RDATE':        (0, None, None),
+                     'RRULE':        (0, None, None)
+                    }
+registerBehavior(VJournal)
+
+
+class VFreeBusy(VCalendarComponentBehavior):
+    """Free/busy state behavior.
+
+    >>> vfb = newFromBehavior('VFREEBUSY')
+    >>> vfb.add('uid').value = 'test'
+    >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
+    >>> vfb.add('dtend').value   = vfb.dtstart.value + twoHours
+    >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)]
+    >>> print vfb.serialize()
+    BEGIN:VFREEBUSY
+    UID:test
+    DTSTART:20060216T010000Z
+    DTEND:20060216T030000Z
+    FREEBUSY:20060216T010000Z/PT1H
+    END:VFREEBUSY
+
+    """
+    name='VFREEBUSY'
+    description='A grouping of component properties that describe either a \
+                 request for free/busy time, describe a response to a request \
+                 for free/busy time or describe a published set of busy time.'
+    sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
+    knownChildren = {'DTSTART':      (0, 1, None),#min, max, behaviorRegistry id
+                     'CONTACT':      (0, 1, None),
+                     'DTEND':        (0, 1, None),
+                     'DURATION':     (0, 1, None),
+                     'ORGANIZER':    (0, 1, None),  
+                     'DTSTAMP':      (0, 1, None), 
+                     'UID':          (0, 1, None),  
+                     'URL':          (0, 1, None),   
+                     'ATTENDEE':     (0, None, None),
+                     'COMMENT':      (0, None, None),
+                     'FREEBUSY':     (0, None, None),
+                     'REQUEST-STATUS': (0, None, None)
+                    }
+registerBehavior(VFreeBusy)
+
+
+class VAlarm(VCalendarComponentBehavior):
+    """Alarm behavior."""
+    name='VALARM'
+    description='Alarms describe when and how to provide alerts about events \
+                 and to-dos.'
+    knownChildren = {'ACTION':       (1, 1, None),#min, max, behaviorRegistry id
+                     'TRIGGER':      (1, 1, None),  
+                     'DURATION':     (0, 1, None),
+                     'REPEAT':       (0, 1, None),
+                     'DESCRIPTION':  (0, 1, None)
+                    }
+
+    @staticmethod
+    def generateImplicitParameters(obj):
+        """Create default ACTION and TRIGGER if they're not set."""
+        try:
+            obj.action
+        except AttributeError:
+            obj.add('action').value = 'AUDIO'
+        try:
+            obj.trigger
+        except AttributeError:
+            obj.add('trigger').value = datetime.timedelta(0)
+
+
+    @classmethod
+    def validate(cls, obj, raiseException, *args):
+        """
+        #TODO
+     audioprop  = 2*(
+
+                ; 'action' and 'trigger' are both REQUIRED,
+                ; but MUST NOT occur more than once
+
+                action / trigger /
+
+                ; 'duration' and 'repeat' are both optional,
+                ; and MUST NOT occur more than once each,
+                ; but if one occurs, so MUST the other
+
+                duration / repeat /
+
+                ; the following is optional,
+                ; but MUST NOT occur more than once
+
+                attach /
+
+     dispprop   = 3*(
+
+                ; the following are all REQUIRED,
+                ; but MUST NOT occur more than once
+
+                action / description / trigger /
+
+                ; 'duration' and 'repeat' are both optional,
+                ; and MUST NOT occur more than once each,
+                ; but if one occurs, so MUST the other
+
+                duration / repeat /
+
+     emailprop  = 5*(
+
+                ; the following are all REQUIRED,
+                ; but MUST NOT occur more than once
+
+                action / description / trigger / summary
+
+                ; the following is REQUIRED,
+                ; and MAY occur more than once
+
+                attendee /
+
+                ; 'duration' and 'repeat' are both optional,
+                ; and MUST NOT occur more than once each,
+                ; but if one occurs, so MUST the other
+
+                duration / repeat /
+
+     procprop   = 3*(
+
+                ; the following are all REQUIRED,
+                ; but MUST NOT occur more than once
+
+                action / attach / trigger /
+
+                ; 'duration' and 'repeat' are both optional,
+                ; and MUST NOT occur more than once each,
+                ; but if one occurs, so MUST the other
+
+                duration / repeat /
+
+                ; 'description' is optional,
+                ; and MUST NOT occur more than once
+
+                description /
+        if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'):
+            if raiseException:
+                m = "VEVENT components cannot contain both DTEND and DURATION\
+                     components"
+                raise ValidateError(m)
+            return False
+        else:
+            return super(VEvent, cls).validate(obj, raiseException, *args)
+        """
+        return True
+    
+registerBehavior(VAlarm)
+
+class Duration(behavior.Behavior):
+    """Behavior for Duration ContentLines.  Transform to datetime.timedelta."""
+    name = 'DURATION'
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a datetime.timedelta."""
+        if obj.isNative: return obj
+        obj.isNative = True
+        obj.value=str(obj.value)
+        if obj.value == '':
+            return obj
+        else:
+            deltalist=stringToDurations(obj.value)
+            #When can DURATION have multiple durations?  For now:
+            if len(deltalist) == 1:
+                obj.value = deltalist[0]
+                return obj
+            else:
+                raise ParseError("DURATION must have a single duration string.")
+
+    @staticmethod
+    def transformFromNative(obj):
+        """Replace the datetime.timedelta in obj.value with an RFC2445 string.
+        """
+        if not obj.isNative: return obj
+        obj.isNative = False
+        obj.value = timedeltaToString(obj.value)
+        return obj
+    
+registerBehavior(Duration)
+
+class Trigger(behavior.Behavior):
+    """DATE-TIME or DURATION"""
+    name='TRIGGER'
+    description='This property specifies when an alarm will trigger.'
+    hasNative = True
+    forceUTC = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a timedelta or datetime."""
+        if obj.isNative: return obj
+        value = getattr(obj, 'value_param', 'DURATION').upper()
+        if hasattr(obj, 'value_param'):
+            del obj.value_param
+        if obj.value == '':
+            obj.isNative = True
+            return obj
+        elif value  == 'DURATION':
+            try:
+                return Duration.transformToNative(obj)
+            except ParseError:
+                logger.warn("TRIGGER not recognized as DURATION, trying "
+                             "DATE-TIME, because iCal sometimes exports "
+                             "DATE-TIMEs without setting VALUE=DATE-TIME")
+                try:
+                    obj.isNative = False
+                    dt = DateTimeBehavior.transformToNative(obj)
+                    return dt
+                except:
+                    msg = "TRIGGER with no VALUE not recognized as DURATION " \
+                          "or as DATE-TIME"
+                    raise ParseError(msg)
+        elif value == 'DATE-TIME':
+            #TRIGGERs with DATE-TIME values must be in UTC, we could validate
+            #that fact, for now we take it on faith.
+            return DateTimeBehavior.transformToNative(obj)
+        else:
+            raise ParseError("VALUE must be DURATION or DATE-TIME")        
+
+    @staticmethod
+    def transformFromNative(obj):
+        if type(obj.value) == datetime.datetime:
+            obj.value_param = 'DATE-TIME'
+            return UTCDateTimeBehavior.transformFromNative(obj)
+        elif type(obj.value) == datetime.timedelta:
+            return Duration.transformFromNative(obj)
+        else:
+            raise NativeError("Native TRIGGER values must be timedelta or datetime")
+
+registerBehavior(Trigger)
+
+class PeriodBehavior(behavior.Behavior):
+    """A list of (date-time, timedelta) tuples.
+
+    >>> line = ContentLine('test', [], '', isNative=True)
+    >>> line.behavior = PeriodBehavior
+    >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)]
+    >>> line.transformFromNative().value
+    '20060216T100000/PT2H'
+    >>> line.transformToNative().value
+    [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))]
+    >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours))
+    >>> print line.serialize().strip()
+    TEST:20060216T100000/PT2H,20060516T100000/PT2H
+    """
+    hasNative = True
+    
+    @staticmethod
+    def transformToNative(obj):
+        """Convert comma separated periods into tuples."""
+        if obj.isNative:
+            return obj
+        obj.isNative = True
+        if obj.value == '':
+            obj.value = []
+            return obj
+        tzinfo = getTzid(getattr(obj, 'tzid_param', None))
+        obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")]
+        return obj
+        
+    @classmethod
+    def transformFromNative(cls, obj):
+        """Convert the list of tuples in obj.value to strings."""
+        if obj.isNative:
+            obj.isNative = False
+            transformed = []
+            for tup in obj.value:
+                transformed.append(periodToString(tup, cls.forceUTC))
+            if len(transformed) > 0:
+                tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo)
+                if not cls.forceUTC and tzid is not None:
+                    obj.tzid_param = tzid
+                            
+            obj.value = ','.join(transformed)
+
+        return obj
+
+class FreeBusy(PeriodBehavior):
+    """Free or busy period of time, must be specified in UTC."""
+    name = 'FREEBUSY'
+    forceUTC = True
+registerBehavior(FreeBusy)
+
+class RRule(behavior.Behavior):
+    """
+    Dummy behavior to avoid having RRULEs being treated as text lines (and thus
+    having semi-colons inaccurately escaped).
+    """
+registerBehavior(RRule, 'RRULE')
+registerBehavior(RRule, 'EXRULE')
+
+#------------------------ Registration of common classes -----------------------
+
+utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
+map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList)
+
+dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
+map(lambda x: registerBehavior(DateOrDateTimeBehavior, x),
+    dateTimeOrDateList)
+    
+registerBehavior(MultiDateBehavior, 'RDATE')
+registerBehavior(MultiDateBehavior, 'EXDATE')
+
+
+textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION',
+            'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO',
+            'UID', 'ACTION', 'REQUEST-STATUS', 'TZID']
+map(lambda x: registerBehavior(TextBehavior, x), textList)
+
+multiTextList = ['CATEGORIES', 'RESOURCES']
+map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList)
+
+#------------------------ Serializing helper functions -------------------------
+
+def numToDigits(num, places):
+    """Helper, for converting numbers to textual digits."""
+    s = str(num)
+    if len(s) < places:
+        return ("0" * (places - len(s))) + s
+    elif len(s) > places:
+        return s[len(s)-places: ]
+    else:
+        return s
+
+def timedeltaToString(delta):
+    """Convert timedelta to an rfc2445 DURATION."""
+    if delta.days == 0: sign = 1
+    else: sign = delta.days / abs(delta.days)
+    delta = abs(delta)
+    days = delta.days
+    hours = delta.seconds / 3600
+    minutes = (delta.seconds % 3600) / 60
+    seconds = delta.seconds % 60
+    out = ''
+    if sign == -1: out = '-'
+    out += 'P'
+    if days: out += str(days) + 'D'
+    if hours or minutes or seconds: out += 'T'
+    elif not days: #Deal with zero duration
+        out += 'T0S'
+    if hours: out += str(hours) + 'H'
+    if minutes: out += str(minutes) + 'M'
+    if seconds: out += str(seconds) + 'S'
+    return out
+
+def timeToString(dateOrDateTime):
+    """
+    Wraps dateToString and dateTimeToString, returning the results
+    of either based on the type of the argument
+    """
+    # Didn't use isinstance here as date and datetime sometimes evalutes as both
+    if (type(dateOrDateTime) == datetime.date):
+        return dateToString(dateOrDateTime)
+    elif(type(dateOrDateTime) == datetime.datetime):
+        return dateTimeToString(dateOrDateTime)
+    
+
+def dateToString(date):
+    year  = numToDigits( date.year,  4 )
+    month = numToDigits( date.month, 2 )
+    day   = numToDigits( date.day,   2 )
+    return year + month + day
+
+def dateTimeToString(dateTime, convertToUTC=False):
+    """Ignore tzinfo unless convertToUTC.  Output string."""
+    if dateTime.tzinfo and convertToUTC:
+        dateTime = dateTime.astimezone(utc)
+    if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z"
+    else: utcString = ""
+
+    year  = numToDigits( dateTime.year,  4 )
+    month = numToDigits( dateTime.month, 2 )
+    day   = numToDigits( dateTime.day,   2 )
+    hour  = numToDigits( dateTime.hour,  2 )
+    mins  = numToDigits( dateTime.minute,  2 )
+    secs  = numToDigits( dateTime.second,  2 )
+
+    return year + month + day + "T" + hour + mins + secs + utcString
+
+def deltaToOffset(delta):
+    absDelta = abs(delta)
+    hours = absDelta.seconds / 3600
+    hoursString      = numToDigits(hours, 2)
+    minutesString    = '00'
+    if absDelta == delta:
+        signString = "+"
+    else:
+        signString = "-"
+    return signString + hoursString + minutesString
+
+def periodToString(period, convertToUTC=False):
+    txtstart = dateTimeToString(period[0], convertToUTC)
+    if isinstance(period[1], datetime.timedelta):
+        txtend = timedeltaToString(period[1])
+    else:
+        txtend = dateTimeToString(period[1], convertToUTC)
+    return txtstart + "/" + txtend
+
+#----------------------- Parsing functions -------------------------------------
+
+def isDuration(s):
+    s = string.upper(s)
+    return (string.find(s, "P") != -1) and (string.find(s, "P") < 2)
+
+def stringToDate(s):
+    year  = int( s[0:4] )
+    month = int( s[4:6] )
+    day   = int( s[6:8] )
+    return datetime.date(year,month,day)
+
+def stringToDateTime(s, tzinfo=None):
+    """Returns datetime.datetime object."""
+    try:
+        year   = int( s[0:4] )
+        month  = int( s[4:6] )
+        day    = int( s[6:8] )
+        hour   = int( s[9:11] )
+        minute = int( s[11:13] )
+        second = int( s[13:15] )
+        if len(s) > 15:
+            if s[15] == 'Z':
+                tzinfo = utc
+    except:
+        raise ParseError("'%s' is not a valid DATE-TIME" % s)
+    return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
+
+
+escapableCharList = "\\;,Nn"
+
+def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
+    """Returns list of strings."""
+    
+    if charList is None:
+        charList = escapableCharList
+
+    def escapableChar (c):
+        return c in charList
+
+    def error(msg):
+        if strict:
+            raise ParseError(msg)
+        else:
+            #logger.error(msg)
+            print msg
+
+    #vars which control state machine
+    charIterator = enumerate(s)
+    state        = "read normal"
+
+    current = ""
+    results = []
+
+    while True:
+        try:
+            charIndex, char = charIterator.next()
+        except:
+            char = "eof"
+
+        if state == "read normal":
+            if char == '\\':
+                state = "read escaped char"
+            elif char == listSeparator:
+                state = "read normal"
+                results.append(current)
+                current = ""
+            elif char == "eof":
+                state = "end"
+            else:
+                state = "read normal"
+                current = current + char
+
+        elif state == "read escaped char":
+            if escapableChar(char):
+                state = "read normal"
+                if char in 'nN': 
+                    current = current + '\n'
+                else:
+                    current = current + char
+            else:
+                state = "read normal"
+                # leave unrecognized escaped characters for later passes
+                current = current + '\\' + char 
+
+        elif state == "end":    #an end state
+            if current != "" or len(results) == 0:
+                results.append(current)
+            return results
+
+        elif state == "error":  #an end state
+            return results
+
+        else:
+            state = "error"
+            error("error: unknown state: '%s' reached in %s" % (state, s))
+
+def stringToDurations(s, strict=False):
+    """Returns list of timedelta objects."""
+    def makeTimedelta(sign, week, day, hour, minute, sec):
+        if sign == "-": sign = -1
+        else: sign = 1
+        week      = int(week)
+        day       = int(day)
+        hour      = int(hour)
+        minute    = int(minute)
+        sec       = int(sec)
+        return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec)
+
+    def error(msg):
+        if strict:
+            raise ParseError(msg)
+        else:
+            raise ParseError(msg)
+            #logger.error(msg)
+    
+    #vars which control state machine
+    charIterator = enumerate(s)
+    state        = "start"
+
+    durations = []
+    current   = ""
+    sign      = None
+    week      = 0
+    day       = 0
+    hour      = 0
+    minute    = 0
+    sec       = 0
+
+    while True:
+        try:
+            charIndex, char = charIterator.next()
+        except:
+            charIndex += 1
+            char = "eof"
+
+        if state == "start":
+            if char == '+':
+                state = "start"
+                sign = char
+            elif char == '-':
+                state = "start"
+                sign = char
+            elif char.upper() == 'P':
+                state = "read field"
+            elif char == "eof":
+                state = "error"
+                error("got end-of-line while reading in duration: " + s)
+            elif char in string.digits:
+                state = "read field"
+                current = current + char   #update this part when updating "read field"
+            else:
+                state = "error"
+                print "got unexpected character %s reading in duration: %s" % (char, s)
+                error("got unexpected character %s reading in duration: %s" % (char, s))
+
+        elif state == "read field":
+            if (char in string.digits):
+                state = "read field"
+                current = current + char   #update part above when updating "read field"   
+            elif char.upper() == 'T':
+                state = "read field"
+            elif char.upper() == 'W':
+                state = "read field"
+                week    = current
+                current = ""
+            elif char.upper() == 'D':
+                state = "read field"
+                day     = current
+                current = ""
+            elif char.upper() == 'H':
+                state = "read field"
+                hour    = current
+                current = ""
+            elif char.upper() == 'M':
+                state = "read field"
+                minute  = current
+                current = ""
+            elif char.upper() == 'S':
+                state = "read field"
+                sec     = current
+                current = ""
+            elif char == ",":
+                state = "start"
+                durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
+                current   = ""
+                sign      = None
+                week      = None
+                day       = None
+                hour      = None
+                minute    = None
+                sec       = None  
+            elif char == "eof":
+                state = "end"
+            else:
+                state = "error"
+                error("got unexpected character reading in duration: " + s)
+            
+        elif state == "end":    #an end state
+            #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations)
+
+            if (sign or week or day or hour or minute or sec):
+                durations.append( makeTimedelta(sign, week, day, hour, minute, sec) )
+            return durations
+
+        elif state == "error":  #an end state
+            error("in error state")
+            return durations
+
+        else:
+            state = "error"
+            error("error: unknown state: '%s' reached in %s" % (state, s))
+
+def parseDtstart(contentline, allowSignatureMismatch=False):
+    """Convert a contentline's value into a date or date-time.
+    
+    A variety of clients don't serialize dates with the appropriate VALUE
+    parameter, so rather than failing on these (technically invalid) lines,
+    if allowSignatureMismatch is True, try to parse both varieties.
+    
+    """
+    tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
+    valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
+    if valueParam == "DATE":
+        return stringToDate(contentline.value)
+    elif valueParam == "DATE-TIME":
+        try:
+            return stringToDateTime(contentline.value, tzinfo)
+        except:
+            if allowSignatureMismatch:
+                return stringToDate(contentline.value)
+            else:
+                raise
+
+def stringToPeriod(s, tzinfo=None):
+    values   = string.split(s, "/")
+    start = stringToDateTime(values[0], tzinfo)
+    valEnd   = values[1]
+    if isDuration(valEnd): #period-start = date-time "/" dur-value
+        delta = stringToDurations(valEnd)[0]
+        return (start, delta)
+    else:
+        return (start, stringToDateTime(valEnd, tzinfo) - start)
+
+
+def getTransition(transitionTo, year, tzinfo):
+    """Return the datetime of the transition to/from DST, or None."""
+
+    def firstTransition(iterDates, test):
+        """
+        Return the last date not matching test, or None if all tests matched.
+        """
+        success = None
+        for dt in iterDates:
+            if not test(dt):
+                success = dt
+            else:
+                if success is not None:
+                    return success
+        return success # may be None
+
+    def generateDates(year, month=None, day=None):
+        """Iterate over possible dates with unspecified values."""
+        months = range(1, 13)
+        days   = range(1, 32)
+        hours  = range(0, 24)
+        if month is None:
+            for month in months:
+                yield datetime.datetime(year, month, 1)
+        elif day is None:
+            for day in days:
+                try:
+                    yield datetime.datetime(year, month, day)
+                except ValueError:
+                    pass
+        else:
+            for hour in hours:
+                yield datetime.datetime(year, month, day, hour)
+
+    assert transitionTo in ('daylight', 'standard')
+    if transitionTo == 'daylight':
+        def test(dt): return tzinfo.dst(dt) != zeroDelta
+    elif transitionTo == 'standard':
+        def test(dt): return tzinfo.dst(dt) == zeroDelta
+    newyear = datetime.datetime(year, 1, 1)
+    monthDt = firstTransition(generateDates(year), test)
+    if monthDt is None:
+        return newyear
+    elif monthDt.month == 12:
+        return None
+    else:
+        # there was a good transition somewhere in a non-December month
+        month = monthDt.month
+        day         = firstTransition(generateDates(year, month), test).day
+        uncorrected = firstTransition(generateDates(year, month, day), test)
+        if transitionTo == 'standard':
+            # assuming tzinfo.dst returns a new offset for the first
+            # possible hour, we need to add one hour for the offset change
+            # and another hour because firstTransition returns the hour
+            # before the transition
+            return uncorrected + datetime.timedelta(hours=2)
+        else:
+            return uncorrected + datetime.timedelta(hours=1)
+
+def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020):
+    """Compare offsets and DST transitions from startYear to endYear."""
+    if tzinfo1 == tzinfo2:
+        return True
+    elif tzinfo1 is None or tzinfo2 is None:
+        return False
+    
+    def dt_test(dt):
+        if dt is None:
+            return True
+        return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt)
+
+    if not dt_test(datetime.datetime(startYear, 1, 1)):
+        return False
+    for year in xrange(startYear, endYear):
+        for transitionTo in 'daylight', 'standard':
+            t1=getTransition(transitionTo, year, tzinfo1)
+            t2=getTransition(transitionTo, year, tzinfo2)
+            if t1 != t2 or not dt_test(t1):
+                return False
+    return True
+
+
+#------------------- Testing and running functions -----------------------------
+if __name__ == '__main__':
+    import tests
+    tests._test()
diff -r 2dde35b02026 MoinMoin/support/vobject/ics_diff.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/ics_diff.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,219 @@
+"""Compare VTODOs and VEVENTs in two iCalendar sources."""
+from base import Component, getBehavior, newFromBehavior
+
+def getSortKey(component):
+    def getUID(component):
+        return component.getChildValue('uid', '')
+    
+    # it's not quite as simple as getUID, need to account for recurrenceID and 
+    # sequence
+
+    def getSequence(component):
+        sequence = component.getChildValue('sequence', 0)
+        return "%05d" % int(sequence)
+    
+    def getRecurrenceID(component):
+        recurrence_id = component.getChildValue('recurrence_id', None)
+        if recurrence_id is None:
+            return '0000-00-00'
+        else:
+            return recurrence_id.isoformat()
+    
+    return getUID(component) + getSequence(component) + getRecurrenceID(component)
+
+def sortByUID(components):
+    return sorted(components, key=getSortKey)    
+
+def deleteExtraneous(component, ignore_dtstamp=False):
+    """
+    Recursively walk the component's children, deleting extraneous details like
+    X-VOBJ-ORIGINAL-TZID.
+    """
+    for comp in component.components():
+        deleteExtraneous(comp, ignore_dtstamp)
+    for line in component.lines():
+        if line.params.has_key('X-VOBJ-ORIGINAL-TZID'):
+            del line.params['X-VOBJ-ORIGINAL-TZID']
+    if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
+        del component.dtstamp_list
+
+def diff(left, right):
+    """
+    Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
+    return a list of object pairs containing just UID and the bits
+    that didn't match, using None for objects that weren't present in one 
+    version or the other.
+    
+    When there are multiple ContentLines in one VEVENT, for instance many
+    DESCRIPTION lines, such lines original order is assumed to be 
+    meaningful.  Order is also preserved when comparing (the unlikely case
+    of) multiple parameters of the same type in a ContentLine
+    
+    """                
+    
+    def processComponentLists(leftList, rightList):
+        output = []
+        rightIndex = 0
+        rightListSize = len(rightList)
+        
+        for comp in leftList:
+            if rightIndex >= rightListSize:
+                output.append((comp, None))
+            else:
+                leftKey  = getSortKey(comp)
+                rightComp = rightList[rightIndex]
+                rightKey = getSortKey(rightComp)
+                while leftKey > rightKey:
+                    output.append((None, rightComp))
+                    rightIndex += 1
+                    if rightIndex >= rightListSize:
+                        output.append((comp, None))                    
+                        break
+                    else:
+                        rightComp = rightList[rightIndex]
+                        rightKey = getSortKey(rightComp)
+                
+                if leftKey < rightKey:
+                    output.append((comp, None))
+                elif leftKey == rightKey:
+                    rightIndex += 1
+                    matchResult = processComponentPair(comp, rightComp)
+                    if matchResult is not None:
+                        output.append(matchResult)
+        
+        return output
+
+    def newComponent(name, body):
+        if body is None:
+            return None
+        else:
+            c = Component(name)
+            c.behavior = getBehavior(name)
+            c.isNative = True
+            return c
+
+    def processComponentPair(leftComp, rightComp):
+        """
+        Return None if a match, or a pair of components including UIDs and
+        any differing children.
+        
+        """        
+        leftChildKeys = leftComp.contents.keys()
+        rightChildKeys = rightComp.contents.keys()
+        
+        differentContentLines = []
+        differentComponents = {}
+        
+        for key in leftChildKeys:
+            rightList = rightComp.contents.get(key, [])
+            if isinstance(leftComp.contents[key][0], Component):
+                compDifference = processComponentLists(leftComp.contents[key],
+                                                       rightList)
+                if len(compDifference) > 0:
+                    differentComponents[key] = compDifference
+                    
+            elif leftComp.contents[key] != rightList:
+                differentContentLines.append((leftComp.contents[key],
+                                              rightList))
+                
+        for key in rightChildKeys:
+            if key not in leftChildKeys:
+                if isinstance(rightComp.contents[key][0], Component):
+                    differentComponents[key] = ([], rightComp.contents[key])
+                else:
+                    differentContentLines.append(([], rightComp.contents[key]))
+        
+        if len(differentContentLines) == 0 and len(differentComponents) == 0:
+            return None
+        else:
+            left  = newFromBehavior(leftComp.name)
+            right = newFromBehavior(leftComp.name)
+            # add a UID, if one existed, despite the fact that they'll always be
+            # the same
+            uid = leftComp.getChildValue('uid')
+            if uid is not None:
+                left.add( 'uid').value = uid
+                right.add('uid').value = uid
+                
+            for name, childPairList in differentComponents.iteritems():
+                leftComponents, rightComponents = zip(*childPairList)
+                if len(leftComponents) > 0:
+                    # filter out None
+                    left.contents[name] = filter(None, leftComponents)
+                if len(rightComponents) > 0:
+                    # filter out None
+                    right.contents[name] = filter(None, rightComponents)
+            
+            for leftChildLine, rightChildLine in differentContentLines:
+                nonEmpty = leftChildLine or rightChildLine
+                name = nonEmpty[0].name
+                if leftChildLine is not None:
+                    left.contents[name] = leftChildLine
+                if rightChildLine is not None:
+                    right.contents[name] = rightChildLine
+            
+            return left, right
+
+
+    vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
+                                    sortByUID(getattr(right, 'vevent_list', [])))
+    
+    vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
+                                   sortByUID(getattr(right, 'vtodo_list', [])))
+    
+    return vevents + vtodos
+
+def prettyDiff(leftObj, rightObj):
+    for left, right in diff(leftObj, rightObj):
+        print "<<<<<<<<<<<<<<<"
+        if left is not None:
+            left.prettyPrint()
+        print "==============="
+        if right is not None:
+            right.prettyPrint()
+        print ">>>>>>>>>>>>>>>"
+        print
+        
+        
+from optparse import OptionParser
+import icalendar, base
+import os
+import codecs
+
+def main():
+    options, args = getOptions()
+    if args:
+        ignore_dtstamp = options.ignore
+        ics_file1, ics_file2 = args
+        cal1 = base.readOne(file(ics_file1))
+        cal2 = base.readOne(file(ics_file2))
+        deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
+        deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
+        prettyDiff(cal1, cal2)
+
+version = "0.1"
+
+def getOptions():
+    ##### Configuration options #####
+
+    usage = "usage: %prog [options] ics_file1 ics_file2"
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("ics_diff will print a comparison of two iCalendar files ")
+
+    parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
+                      default=False, help="ignore DTSTAMP lines [default: False]")
+
+    (cmdline_options, args) = parser.parse_args()
+    if len(args) < 2:
+        print "error: too few arguments given"
+        print
+        print parser.format_help()
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print "Aborted"
diff -r 2dde35b02026 MoinMoin/support/vobject/midnight.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/midnight.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight"""
+
+from optparse import OptionParser
+import icalendar, base, datetime
+import os
+import codecs
+
+midnight = datetime.time(0)
+one_day = datetime.timedelta(1)
+
+def changeVeventTimes(vevent):
+    dtstart = vevent.getChildValue('dtstart')
+    dtend   = vevent.getChildValue('dtend')
+    if isinstance(dtstart, datetime.datetime):
+        if dtend is None:
+            if dtstart.tzinfo is None and dtstart.time() == midnight:
+                vevent.dtstart.value = dtstart.date()
+        elif (isinstance(dtend, datetime.datetime) and 
+              dtend.tzinfo is None and dtend.time() == midnight):
+            vevent.dtstart.value = dtstart.date()
+            vevent.dtend.value   = dtend.date()
+
+
+def main():
+    options, args = getOptions()
+    if args:
+        in_filename, out_filename = args
+        cal = base.readOne(file(in_filename))
+        for vevent in cal.vevent_list:
+            changeVeventTimes(vevent)
+        out_file = codecs.open(out_filename, "w", "utf-8")
+        cal.serialize(out_file)
+        out_file.close()
+
+
+version = "0.1"
+
+def getOptions():
+    ##### Configuration options #####
+
+    usage = "usage: %prog [options] in_file out_file"
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("convert midnight events to all")
+    
+    (cmdline_options, args) = parser.parse_args()
+    if len(args) < 2:
+        print "error: too few arguments given"
+        print
+        print parser.format_help()
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print "Aborted"
diff -r 2dde35b02026 MoinMoin/support/vobject/to_pst.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/to_pst.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight"""
+
+from optparse import OptionParser
+import icalendar, base, datetime
+import os
+import codecs
+import PyICU
+
+pst = PyICU.ICUtzinfo.getInstance('US/Pacific')
+
+def changeVeventTimes(vevent):
+    dtstart = vevent.getChildValue('dtstart')
+    dtend   = vevent.getChildValue('dtend')
+    if isinstance(dtstart, datetime.datetime):
+        if dtstart.tzinfo is None:
+            vevent.dtstart.value = dtstart.replace(tzinfo=pst)
+        if dtend is not None and dtend.tzinfo is None:
+            vevent.dtend.value   = dtend.replace(tzinfo=pst)
+
+def main():
+    options, args = getOptions()
+    if args:
+        in_filename, out_filename = args
+        cal = base.readOne(file(in_filename))
+        for vevent in cal.vevent_list:
+            changeVeventTimes(vevent)
+        out_file = codecs.open(out_filename, "w", "utf-8")
+        cal.serialize(out_file)
+        out_file.close()
+
+
+version = "0.1"
+
+def getOptions():
+    ##### Configuration options #####
+
+    usage = "usage: %prog [options] in_file out_file"
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("convert midnight events to all")
+    
+    (cmdline_options, args) = parser.parse_args()
+    if len(args) < 2:
+        print "error: too few arguments given"
+        print
+        print parser.format_help()
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print "Aborted"
diff -r 2dde35b02026 MoinMoin/support/vobject/vcard.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/vcard.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,289 @@
+"""Definitions and behavior for vCard 3.0"""
+
+import behavior
+import itertools
+
+from base import VObjectError, NativeError, ValidateError, ParseError, \
+                    VBase, Component, ContentLine, logger, defaultSerialize, \
+                    registerBehavior, backslashEscape, ascii
+from icalendar import stringToTextValues
+
+#------------------------ vCard structs ----------------------------------------
+
+class Name(object):
+    def __init__(self, family = '', given = '', additional = '', prefix = '',
+                 suffix = ''):
+        """Each name attribute can be a string or a list of strings."""
+        self.family     = family
+        self.given      = given
+        self.additional = additional
+        self.prefix     = prefix
+        self.suffix     = suffix
+        
+    @staticmethod
+    def toString(val):
+        """Turn a string or array value into a string."""
+        if type(val) in (list, tuple):
+            return ' '.join(val)
+        return val
+
+    def __str__(self):
+        eng_order = ('prefix', 'given', 'additional', 'family', 'suffix')
+        out = ' '.join(self.toString(getattr(self, val)) for val in eng_order)
+        return ascii(out)
+
+    def __repr__(self):
+        return "<Name: %s>" % self.__str__()
+
+    def __eq__(self, other):
+        try:
+            return (self.family == other.family and
+                    self.given == other.given and
+                    self.additional == other.additional and
+                    self.prefix == other.prefix and
+                    self.suffix == other.suffix)
+        except:
+            return False
+
+class Address(object):
+    def __init__(self, street = '', city = '', region = '', code = '',
+                 country = '', box = '', extended = ''):
+        """Each name attribute can be a string or a list of strings."""
+        self.box      = box
+        self.extended = extended
+        self.street   = street
+        self.city     = city
+        self.region   = region
+        self.code     = code
+        self.country  = country
+        
+    @staticmethod
+    def toString(val, join_char='\n'):
+        """Turn a string or array value into a string."""
+        if type(val) in (list, tuple):
+            return join_char.join(val)
+        return val
+
+    lines = ('box', 'extended', 'street')
+    one_line = ('city', 'region', 'code')
+
+    def __str__(self):
+        lines = '\n'.join(self.toString(getattr(self, val)) for val in self.lines if getattr(self, val))
+        one_line = tuple(self.toString(getattr(self, val), ' ') for val in self.one_line)
+        lines += "\n%s, %s %s" % one_line
+        if self.country:
+            lines += '\n' + self.toString(self.country)
+        return ascii(lines)
+
+    def __repr__(self):
+        return "<Address: %s>" % repr(str(self))[1:-1]
+
+    def __eq__(self, other):
+        try:
+            return (self.box == other.box and
+                    self.extended == other.extended and
+                    self.street == other.street and
+                    self.city == other.city and
+                    self.region == other.region and
+                    self.code == other.code and
+                    self.country == other.country)
+        except:
+            False
+        
+
+#------------------------ Registered Behavior subclasses -----------------------
+
+class VCardTextBehavior(behavior.Behavior):
+    """Provide backslash escape encoding/decoding for single valued properties.
+    
+    TextBehavior also deals with base64 encoding if the ENCODING parameter is
+    explicitly set to BASE64.
+    
+    """
+    allowGroup = True
+    base64string = 'B'
+    
+    @classmethod
+    def decode(cls, line):
+        """Remove backslash escaping from line.valueDecode line, either to remove
+        backslash espacing, or to decode base64 encoding. The content line should
+        contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
+        export a singleton parameter of 'BASE64', which does not match the 3.0
+        vCard spec. If we encouter that, then we transform the parameter to
+        ENCODING=b"""
+        if line.encoded:
+            if 'BASE64' in line.singletonparams:
+                line.singletonparams.remove('BASE64')
+                line.encoding_param = cls.base64string
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding:
+                line.value = line.value.decode('base64')
+            else:
+                line.value = stringToTextValues(line.value)[0]
+            line.encoded=False
+    
+    @classmethod
+    def encode(cls, line):
+        """Backslash escape line.value."""
+        if not line.encoded:
+            encoding = getattr(line, 'encoding_param', None)
+            if encoding and encoding.upper() == cls.base64string:
+                line.value = line.value.encode('base64').replace('\n', '')
+            else:
+                line.value = backslashEscape(line.value)
+            line.encoded=True
+
+
+class VCardBehavior(behavior.Behavior):
+    allowGroup = True
+    defaultBehavior = VCardTextBehavior
+
+class VCard3_0(VCardBehavior):
+    """vCard 3.0 behavior."""
+    name = 'VCARD'
+    description = 'vCard 3.0, defined in rfc2426'
+    versionString = '3.0'
+    isComponent = True
+    sortFirst = ('version', 'prodid', 'uid')
+    knownChildren = {'N':         (1, 1, None),#min, max, behaviorRegistry id
+                     'FN':        (1, 1, None),
+                     'VERSION':   (1, 1, None),#required, auto-generated
+                     'PRODID':    (0, 1, None),
+                     'LABEL':     (0, None, None),
+                     'UID':       (0, None, None),
+                     'ADR':       (0, None, None),
+                     'ORG':       (0, None, None),
+                     'PHOTO':     (0, None, None),
+                     'CATEGORIES':(0, None, None)
+                    }
+                    
+    @classmethod
+    def generateImplicitParameters(cls, obj):
+        """Create PRODID, VERSION, and VTIMEZONEs if needed.
+        
+        VTIMEZONEs will need to exist whenever TZID parameters exist or when
+        datetimes with tzinfo exist.
+        
+        """
+        if not hasattr(obj, 'version'):
+            obj.add(ContentLine('VERSION', [], cls.versionString))
+registerBehavior(VCard3_0, default=True)
+
+class FN(VCardTextBehavior):
+    name = "FN"
+    description = 'Formatted name'
+registerBehavior(FN)
+
+class Label(VCardTextBehavior):
+    name = "Label"
+    description = 'Formatted address'
+registerBehavior(FN)
+
+class Photo(VCardTextBehavior):
+    name = "Photo"
+    description = 'Photograph'
+    @classmethod
+    def valueRepr( cls, line ):
+        return " (BINARY PHOTO DATA at 0x%s) " % id( line.value )
+    
+registerBehavior(Photo)
+
+def toListOrString(string):
+    stringList = stringToTextValues(string)
+    if len(stringList) == 1:
+        return stringList[0]
+    else:
+        return stringList
+
+def splitFields(string):
+    """Return a list of strings or lists from a Name or Address."""
+    return [toListOrString(i) for i in
+            stringToTextValues(string, listSeparator=';', charList=';')]
+
+def toList(stringOrList):
+    if isinstance(stringOrList, basestring):
+        return [stringOrList]
+    return stringOrList
+
+def serializeFields(obj, order=None):
+    """Turn an object's fields into a ';' and ',' seperated string.
+    
+    If order is None, obj should be a list, backslash escape each field and
+    return a ';' separated string.
+    """
+    fields = []
+    if order is None:
+        fields = [backslashEscape(val) for val in obj]
+    else:
+        for field in order:
+            escapedValueList = [backslashEscape(val) for val in
+                                toList(getattr(obj, field))]
+            fields.append(','.join(escapedValueList))            
+    return ';'.join(fields)
+
+NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix')
+
+class NameBehavior(VCardBehavior):
+    """A structured name."""
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a Name."""
+        if obj.isNative: return obj
+        obj.isNative = True
+        obj.value = Name(**dict(zip(NAME_ORDER, splitFields(obj.value))))
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """Replace the Name in obj.value with a string."""
+        obj.isNative = False
+        obj.value = serializeFields(obj.value, NAME_ORDER)
+        return obj
+registerBehavior(NameBehavior, 'N')
+
+ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code', 
+                 'country')
+
+class AddressBehavior(VCardBehavior):
+    """A structured address."""
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into an Address."""
+        if obj.isNative: return obj
+        obj.isNative = True
+        obj.value = Address(**dict(zip(ADDRESS_ORDER, splitFields(obj.value))))
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """Replace the Address in obj.value with a string."""
+        obj.isNative = False
+        obj.value = serializeFields(obj.value, ADDRESS_ORDER)
+        return obj
+registerBehavior(AddressBehavior, 'ADR')
+    
+class OrgBehavior(VCardBehavior):
+    """A list of organization values and sub-organization values."""
+    hasNative = True
+
+    @staticmethod
+    def transformToNative(obj):
+        """Turn obj.value into a list."""
+        if obj.isNative: return obj
+        obj.isNative = True
+        obj.value = splitFields(obj.value)
+        return obj
+
+    @staticmethod
+    def transformFromNative(obj):
+        """Replace the list in obj.value with a string."""
+        if not obj.isNative: return obj
+        obj.isNative = False
+        obj.value = serializeFields(obj.value)
+        return obj
+registerBehavior(OrgBehavior, 'ORG')
+    
diff -r 2dde35b02026 MoinMoin/support/vobject/win32tz.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/vobject/win32tz.py	Thu Dec 20 20:04:49 2007 +0100
@@ -0,0 +1,156 @@
+import _winreg
+import struct
+import datetime
+
+handle=_winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
+tzparent=_winreg.OpenKey(handle,
+            "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones")
+parentsize=_winreg.QueryInfoKey(tzparent)[0]
+
+localkey=_winreg.OpenKey(handle,
+            "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation")
+WEEKS=datetime.timedelta(7)
+
+def list_timezones():
+    """Return a list of all time zones known to the system."""
+    l=[]
+    for i in xrange(parentsize):
+        l.append(_winreg.EnumKey(tzparent, i))
+    return l
+
+class win32tz(datetime.tzinfo):
+    """tzinfo class based on win32's timezones available in the registry.
+    
+    >>> local = win32tz('Central Standard Time')
+    >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local)
+    >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local)
+    >>> oct1.dst()
+    datetime.timedelta(0, 3600)
+    >>> dec1.dst()
+    datetime.timedelta(0)
+    >>> braz = win32tz('E. South America Standard Time')
+    >>> braz.dst(oct1)
+    datetime.timedelta(0)
+    >>> braz.dst(dec1)
+    datetime.timedelta(0, 3600)
+    
+    """
+    def __init__(self, name):
+        self.data=win32tz_data(name)
+        
+    def utcoffset(self, dt):
+        if self._isdst(dt):
+            return datetime.timedelta(minutes=self.data.dstoffset)
+        else:
+            return datetime.timedelta(minutes=self.data.stdoffset)
+
+    def dst(self, dt):
+        if self._isdst(dt):
+            minutes = self.data.dstoffset - self.data.stdoffset
+            return datetime.timedelta(minutes=minutes)
+        else:
+            return datetime.timedelta(0)
+        
+    def tzname(self, dt):
+        if self._isdst(dt): return self.data.dstname
+        else: return self.data.stdname
+    
+    def _isdst(self, dt):
+        dat=self.data
+        dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek,
+                               dat.dsthour, dat.dstminute, dat.dstweeknumber)
+        dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek,
+                                dat.stdhour, dat.stdminute, dat.stdweeknumber)
+        if dston < dstoff:
+            if dston <= dt.replace(tzinfo=None) < dstoff: return True
+            else: return False
+        else:
+            if dstoff <= dt.replace(tzinfo=None) < dston: return False
+            else: return True
+
+    def __repr__(self):
+        return "<win32tz - %s>" % self.data.display
+
+def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek):
+    """dayofweek == 0 means Sunday, whichweek > 4 means last instance"""
+    first = datetime.datetime(year=year, month=month, hour=hour, minute=minute,
+                              day=1)
+    weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1))
+    for n in xrange(whichweek - 1, -1, -1):
+        dt=weekdayone + n * WEEKS
+        if dt.month == month: return dt
+
+
+class win32tz_data(object):
+    """Read a registry key for a timezone, expose its contents."""
+    
+    def __init__(self, path):
+        """Load path, or if path is empty, load local time."""
+        if path:
+            keydict=valuesToDict(_winreg.OpenKey(tzparent, path))
+            self.display = keydict['Display']
+            self.dstname = keydict['Dlt']
+            self.stdname = keydict['Std']
+            
+            #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+            tup = struct.unpack('=3l16h', keydict['TZI'])
+            self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1
+            self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1
+            
+            offset=3
+            self.stdmonth = tup[1 + offset]
+            self.stddayofweek = tup[2 + offset] #Sunday=0
+            self.stdweeknumber = tup[3 + offset] #Last = 5
+            self.stdhour = tup[4 + offset]
+            self.stdminute = tup[5 + offset]
+            
+            offset=11
+            self.dstmonth = tup[1 + offset]
+            self.dstdayofweek = tup[2 + offset] #Sunday=0
+            self.dstweeknumber = tup[3 + offset] #Last = 5
+            self.dsthour = tup[4 + offset]
+            self.dstminute = tup[5 + offset]
+            
+        else:
+            keydict=valuesToDict(localkey)
+            
+            self.stdname = keydict['StandardName']
+            self.dstname = keydict['DaylightName']
+            
+            sourcekey=_winreg.OpenKey(tzparent, self.stdname)
+            self.display = valuesToDict(sourcekey)['Display']
+            
+            self.stdoffset = -keydict['Bias']-keydict['StandardBias']
+            self.dstoffset = self.stdoffset - keydict['DaylightBias']
+
+            #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
+            tup = struct.unpack('=8h', keydict['StandardStart'])
+
+            offset=0
+            self.stdmonth = tup[1 + offset]
+            self.stddayofweek = tup[2 + offset] #Sunday=0
+            self.stdweeknumber = tup[3 + offset] #Last = 5
+            self.stdhour = tup[4 + offset]
+            self.stdminute = tup[5 + offset]
+            
+            tup = struct.unpack('=8h', keydict['DaylightStart'])
+            self.dstmonth = tup[1 + offset]
+            self.dstdayofweek = tup[2 + offset] #Sunday=0
+            self.dstweeknumber = tup[3 + offset] #Last = 5
+            self.dsthour = tup[4 + offset]
+            self.dstminute = tup[5 + offset]
+
+def valuesToDict(key):
+    """Convert a registry key's values to a dictionary."""
+    dict={}
+    size=_winreg.QueryInfoKey(key)[1]
+    for i in xrange(size):
+        dict[_winreg.EnumValue(key, i)[0]]=_winreg.EnumValue(key, i)[1]
+    return dict
+
+def _test():
+    import win32tz, doctest
+    doctest.testmod(win32tz, verbose=0)
+
+if __name__ == '__main__':
+    _test()
\ No newline at end of file
diff -r 2dde35b02026 MoinMoin/wikiutil.py
--- a/MoinMoin/wikiutil.py	Wed Nov 28 10:43:48 2007 +0100
+++ b/MoinMoin/wikiutil.py	Thu Dec 20 20:04:49 2007 +0100
@@ -921,6 +921,8 @@ MIMETYPES_MORE = {
  '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
  '.otp': 'application/vnd.oasis.opendocument.presentation-template',
  '.otg': 'application/vnd.oasis.opendocument.graphics-template',
+ '.hcf': 'text/x-hcard',
+ '.hcard': 'text/x-hcard'
 }
 [mimetypes.add_type(mimetype, ext, True) for ext, mimetype in MIMETYPES_MORE.items()]
