summaryrefslogtreecommitdiff
path: root/pykolab/itip/__init__.py
blob: 42e08dd34d71e782e175287485ef0ada566decca (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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import icalendar
import pykolab

from pykolab.xml import to_dt
from pykolab.xml import event_from_ical
from pykolab.translate import _

log = pykolab.getLogger('pykolab.wallace')


def events_from_message(message, methods=None):
    return objects_from_message(message, "VEVENT", methods)

def todos_from_message(message, methods=None):
    return objects_from_message(message, "VTODO", methods)


def objects_from_message(message, objname, methods=None):
    """
        Obtain the iTip payload from email.message <message>
    """
    # Placeholder for any itip_objects found in the message.
    itip_objects = []
    seen_uids = []

    # iTip methods we are actually interested in. Other methods will be ignored.
    if methods is None:
        methods = [ "REQUEST", "CANCEL" ]

    # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A
    # MIME body part containing content information that conforms to this
    # document MUST have (...)" but does not state whether an iTip message must
    # therefore also be multipart.

    # Check each part
    for part in message.walk():

        # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4)
        # But in real word, other mime-types are used as well
        if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]:
            if not str(part.get_param('method')).upper() in methods:
                log.info(_("Method %r not really interesting for us.") % (part.get_param('method')))
                continue

            # Get the itip_payload
            itip_payload = part.get_payload(decode=True)

            log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9)

            # Python iCalendar prior to 3.0 uses "from_string".
            if hasattr(icalendar.Calendar, 'from_ical'):
                cal = icalendar.Calendar.from_ical(itip_payload)
            elif hasattr(icalendar.Calendar, 'from_string'):
                cal = icalendar.Calendar.from_string(itip_payload)

            # If we can't read it, we're out
            else:
                log.error(_("Could not read iTip from message."))
                return []

            for c in cal.walk():
                if c.name == objname:
                    itip = {}

                    if c['uid'] in seen_uids:
                        log.debug(_("Duplicate iTip object: %s") % (c['uid']), level=9)
                        continue

                    # From the event, take the following properties:
                    #
                    # - method
                    # - uid
                    # - sequence
                    # - start
                    # - end (if any)
                    # - duration (if any)
                    # - organizer
                    # - attendees (if any)
                    # - resources (if any)
                    #

                    itip['uid'] = str(c['uid'])
                    itip['method'] = str(cal['method']).upper()
                    itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0

                    if c.has_key('dtstart'):
                        itip['start'] = c['dtstart'].dt
                    else:
                        log.error(_("iTip event without a start"))
                        continue

                    if c.has_key('dtend'):
                        itip['end'] = c['dtend'].dt

                    if c.has_key('duration'):
                        itip['duration'] = c['duration'].dt
                        itip['end'] = itip['start'] + c['duration'].dt

                    itip['organizer'] = c['organizer']

                    itip['attendees'] = c['attendee']

                    if itip.has_key('attendee') and not isinstance(itip['attendees'], list):
                        itip['attendees'] = [c['attendee']]

                    if c.has_key('resources'):
                        itip['resources'] = c['resources']

                    itip['raw'] = itip_payload

                    try:
                        # TODO: distinguish event and todo here
                        itip['xml'] = event_from_ical(c.to_ical())
                    except Exception, e:
                        log.error("event_from_ical() exception: %r" % (e))
                        continue

                    itip_objects.append(itip)

                    seen_uids.append(c['uid'])

                # end if c.name == "VEVENT"

            # end for c in cal.walk()

        # end if part.get_content_type() == "text/calendar"

    # end for part in message.walk()

    if not len(itip_objects) and not message.is_multipart():
        log.debug(_("Message is not an iTip message (non-multipart message)"), level=5)

    return itip_objects


def check_event_conflict(kolab_event, itip_event):
    """
        Determine whether the given kolab event conflicts with the given itip event
    """
    conflict = False

    # don't consider conflict with myself
    if kolab_event.uid == itip_event['uid']:
        return conflict

    _es = to_dt(kolab_event.get_start())
    _ee = to_dt(kolab_event.get_end())

    # naive loops to check for collisions in (recurring) events
    # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
    while not conflict and _es is not None:
        _is = to_dt(itip_event['start'])
        _ie = to_dt(itip_event['end'])

        while not conflict and _is is not None:
            # log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9)
            conflict = check_date_conflict(_es, _ee, _is, _ie)
            _is = to_dt(itip_event['xml'].get_next_occurence(_is)) if kolab_event.is_recurring() else None
            _ie = to_dt(itip_event['xml'].get_occurence_end_date(_is))

        _es = to_dt(kolab_event.get_next_occurence(_es)) if kolab_event.is_recurring() else None
        _ee = to_dt(kolab_event.get_occurence_end_date(_es))

    return conflict


def check_date_conflict(_es, _ee, _is, _ie):
    """
        Check the given event start/end dates for conflicts
    """
    conflict = False

    # TODO: add margin for all-day dates (+13h; -12h)

    if _es < _is:
        if _es <= _ie:
            if _ee <= _is:
                conflict = False
            else:
                conflict = True
        else:
            conflict = True
    elif _es == _is:
        conflict = True
    else: # _es > _is
        if _es <= _ie:
            conflict = True
        else:
            conflict = False
    
    return conflict


def send_reply(from_address, itip_events, response_text, subject=None):
    """
        Send the given iCal events as a valid iTip REPLY to the organizer.
    """

    import smtplib
    smtp = smtplib.SMTP("localhost", 10027)

    conf = pykolab.getConf()

    if conf.debuglevel > 8:
        smtp.set_debuglevel(True)

    if isinstance(itip_events, dict):
        itip_events = [ itip_events ]

    for itip_event in itip_events:
        attendee = itip_event['xml'].get_attendee_by_email(from_address)
        participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)

        event_summary = itip_event['xml'].get_summary()
        message_text = response_text % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }

        if subject is not None:
            subject = subject % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }

        message = itip_event['xml'].to_message_itip(from_address,
            method="REPLY",
            participant_status=participant_status,
            message_text=message_text,
            subject=subject
        )
        smtp.sendmail(message['From'], message['To'], message.as_string())

    smtp.quit()