summaryrefslogtreecommitdiff
path: root/pykolab/itip/__init__.py
blob: 83c9fe1d1fc53525264dd130f070a2a95308f376 (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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import icalendar
import pykolab
import traceback
import kolabformat

from pykolab.xml import to_dt
from pykolab.xml import event_from_ical
from pykolab.xml import todo_from_ical
from pykolab.xml import participant_status_label
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, objnames, 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 (%r): %r") % (part.get_param('charset'), 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 in objnames:
                    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['type'] = 'task' if c.name == 'VTODO' else 'event'
                    itip['uid'] = str(c['uid'])
                    itip['method'] = str(cal['method']).upper()
                    itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
                    itip['recurrence-id'] = c['recurrence-id'].dt if c.has_key('recurrence-id') and hasattr(c['recurrence-id'], 'dt') else None

                    if c.has_key('dtstart'):
                        itip['start'] = c['dtstart'].dt
                    elif itip['type'] == 'event':
                        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('attendees') and not isinstance(itip['attendees'], list):
                        itip['attendees'] = [c['attendee']]

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

                    itip['raw'] = itip_payload

                    try:
                        # distinguish event and todo here
                        if itip['type'] == 'task':
                            itip['xml'] = todo_from_ical(c, itip_payload)
                        else:
                            itip['xml'] = event_from_ical(c, itip_payload)
                    except Exception, e:
                        log.error("event|todo_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
                        continue

                    itip_objects.append(itip)

                    seen_uids.append(c['uid'])

                # end if c.name in objnames

            # 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

    # don't consider conflict if event has TRANSP:TRANSPARENT
    if _is_transparent(kolab_event):
        return conflict

    _es = to_dt(kolab_event.get_start())
    _ee = to_dt(kolab_event.get_ical_dtend())  # use iCal style end date: next day for all-day events
    _ev = kolab_event
    _ei = 0

    # 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'])
        _iv = itip_event['xml']
        _ii = 0

        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 = not _is_transparent(_ev) and not _is_transparent(_iv) and check_date_conflict(_es, _ee, _is, _ie)
            _is = to_dt(itip_event['xml'].get_next_occurence(_is)) if itip_event['xml'].is_recurring() else None
            _ie = to_dt(itip_event['xml'].get_occurence_end_date(_is))

            # get full occurrence to compare the dates from a possible exception
            if _is is not None and itip_event['xml'].has_exceptions():
                _ix = itip_event['xml'].get_instance(_is)
                if _ix is not None:
                    _is = to_dt(_ix.get_start())
                    _ie = to_dt(_ix.get_end())
                    _iv = _ix

            # iterate through all exceptions (non-recurring)
            elif _is is None and not itip_event['xml'].is_recurring() and len(itip_event['xml'].get_exceptions()) > _ii:
                _iv = itip_event['xml'].get_exceptions()[_ii]
                _is = to_dt(_iv.get_start())
                _ie = to_dt(_iv.get_end())
                _ii += 1

        _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))

        # get full instance to compare the dates from a possible exception
        if _es is not None and kolab_event.has_exceptions():
            _ex = kolab_event.get_instance(_es)
            if _ex is not None:
                _es = to_dt(_ex.get_start())
                _ee = to_dt(_ex.get_end())
                _ev = _ex

        # iterate through all exceptions (non-recurring)
        elif _es is None and not kolab_event.is_recurring() and len(kolab_event.get_exceptions()) > _ei:
            _ev = kolab_event.get_exceptions()[_ei]
            _es = to_dt(_ev.get_start())
            _ee = to_dt(_ev.get_end())
            _ei += 1

    return conflict


def _is_transparent(event):
    return event.get_transparency() or event.get_status() == kolabformat.StatusCancelled


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

    conf = pykolab.getConf()
    smtp = None

    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)

        log.debug(_("Send iTip reply %s for %s %r") % (participant_status, itip_event['xml'].type, itip_event['xml'].uid), level=8)

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

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

        try:
            message = itip_event['xml'].to_message_itip(from_address,
                method="REPLY",
                participant_status=participant_status,
                message_text=message_text,
                subject=subject
            )
        except Exception, e:
            log.error(_("Failed to compose iTip reply message: %r: %s") % (e, traceback.format_exc()))
            return

        smtp = smtplib.SMTP("localhost", 10026)  # replies go through wallace again

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

        try:
            smtp.sendmail(message['From'], message['To'], message.as_string())
        except Exception, e:
            log.error(_("SMTP sendmail error: %r") % (e))

    if smtp:
        smtp.quit()


def send_request(to_address, itip_events, request_text, subject=None, direct=False):
    """
        Send an iTip REQUEST message from the given iCal events
    """
    import smtplib

    conf = pykolab.getConf()
    smtp = None

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

    for itip_event in itip_events:
        event_summary = itip_event['xml'].get_summary()
        message_text = request_text % { 'summary':event_summary }

        if subject is not None:
            subject = subject % { 'summary':event_summary }

        try:
            message = itip_event['xml'].to_message_itip(None,
                method="REQUEST",
                message_text=message_text,
                subject=subject
            )
        except Exception, e:
            log.error(_("Failed to compose iTip request message: %r") % (e))
            return

        port = 10027 if direct else 10026
        smtp = smtplib.SMTP("localhost", port)

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

        try:
            smtp.sendmail(message['From'], to_address, message.as_string())
        except Exception, e:
            log.error(_("SMTP sendmail error: %r") % (e))

    if smtp:
        smtp.quit()