summaryrefslogtreecommitdiff
path: root/caldav/lib/vcal.py
blob: 2b132ae0df76bc6042de7f4d1e0f8642d52988c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python
# -*- encoding: utf-8 -*-


import re
from caldav.lib.python_utilities import to_local, to_wire
import datetime
import uuid

## Fixups to the icalendar data to work around compatbility issues.

## TODO:

## 1) this should only be done if needed.  Use try-except around the
## fragments where icalendar/vobject is parsing ical data, and do the
## fixups there.

## 2) arguably, this is outside the scope of the caldav library.
## check if this can be done in vobject or icalendar libraries instead
## of here

## TODO: would be nice with proper documentation on what systems are
## generating broken data.  Compatibility issues should also be collected
## in the documentation. somewhere.
def fix(event):
    """This function receives some ical as it's given from the server, checks for 
    breakages with the standard, and attempts to fix up known issues:

    1) COMPLETED MUST be a datetime in UTC according to the RFC, but sometimes 
    a date is given. (Google Calendar?)

    2) The RFC does not specify any range restrictions on the dates,
    but clearly it doesn't make sense with a CREATED-timestamp that is
    centuries or decades before RFC2445 was published in 1998.
    Apparently some calendar servers generate nonsensical CREATED
    timestamps while other calendar servers can't handle CREATED
    timestamps prior to 1970.  Probably it would make more sense to
    drop the CREATED line completely rather than moving it from the
    end of year 0AD to the beginning of year 1970. (Google Calendar)

    3) iCloud apparently duplicates the DTSTAMP property sometimes -
    keep the first DTSTAMP encountered (arguably the DTSTAMP with earliest value
    should be kept).

    4) ref https://github.com/python-caldav/caldav/issues/37,
    X-APPLE-STRUCTURED-EVENT attribute sometimes comes with trailing
    white space.  I've decided to remove all trailing spaces, since
    they seem to cause a traceback with vobject and those lines are
    simply ignored by icalendar.
    """
    ## TODO: add ^ before COMPLETED and CREATED?
    ## 1) Add a random time if completed is given as date
    fixed = re.sub('COMPLETED:(\d+)\s', 'COMPLETED:\g<1>T120000Z',
                   to_local(event))

    ## 2) CREATED timestamps prior to epoch does not make sense,
    ## change from year 0001 to epoch.
    fixed = re.sub('CREATED:00001231T000000Z',
                   'CREATED:19700101T000000Z', fixed)
    fixed = re.sub(r"\\+('\")", r"\1", fixed)

    ## 4) trailing whitespace probably never makes sense
    fixed = re.sub(' *$', '', fixed)

    ## 3 fix duplicated DTSTAMP
    ## OPTIMIZATION TODO: use list and join rather than concatination
    ## remove duplication of DTSTAMP
    fixed2 = ""
    for line in fixed.strip().split('\n'):
        if line.startswith('BEGIN:V'):
            cnt = 0
        if line.startswith('DTSTAMP:'):
            if not cnt:
                fixed2 += line + "\n"
            cnt += 1
        else:
            fixed2 += line + "\n"

    return fixed2

## sorry for being english-language-euro-centric ... fits rather perfectly as default language for me :-)
def create_ical(ical_fragment=None, objtype=None, language='en_DK', **attributes):
    """
    I somehow feel this fits more into the icalendar library than here
    """
    ## late import, icalendar is not an explicit requirement for v0.x of the caldav library.
    ## (perhaps I should change my position on that soon)
    import icalendar
    if ical_fragment:
        ical_fragment = to_wire(ical_fragment)
    if not ical_fragment or not re.search(b'^BEGIN:V', ical_fragment, re.MULTILINE):
        my_instance = icalendar.Calendar()
        my_instance.add('prodid', f'-//python-caldav//caldav//{language}')
        my_instance.add('version', '2.0')
        if objtype is None:
            objtype='VEVENT'
        component = icalendar.cal.component_factory[objtype]()
        component.add('dtstamp', datetime.datetime.now())
        component.add('uid', uuid.uuid1())
        my_instance.add_component(component)
    else:
        if not ical_fragment.startswith(b'BEGIN:VCALENDAR'):
            ical_fragment = b"BEGIN:VCALENDAR\n"+to_wire(ical_fragment.strip())+b"\nEND:VCALENDAR\n"
        my_instance = icalendar.Calendar.from_ical(ical_fragment)
        component = my_instance.subcomponents[0]
        ical_fragment = None
    for attribute in attributes:
        if attributes[attribute] is not None:
            component.add(attribute, attributes[attribute])
    ret = my_instance.to_ical()
    if ical_fragment is not None:
        ret = re.sub(b"^END:V", ical_fragment.strip() + b"\nEND:V", ret, flags=re.MULTILINE)
    return ret