# HG changeset patch # User Paul Boddie # Date 1411557409 -7200 # Node ID 63937ac3d03c8f541571b4aef34378204ddf1801 # Parent db37d281b8ff1e480d71f45aadc5372b448fc366 Added timezone awareness; fixed event replies, producing all event records. diff -r db37d281b8ff -r 63937ac3d03c imip_agent.py --- a/imip_agent.py Tue Sep 23 23:32:11 2014 +0200 +++ b/imip_agent.py Wed Sep 24 13:16:49 2014 +0200 @@ -1,12 +1,15 @@ #!/usr/bin/env python from bisect import bisect_left, insort_left +from datetime import date, datetime from email import message_from_file from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from pytz import timezone, UnknownTimeZoneError from smtplib import SMTP from vCalendar import parse, ParseError, SECTION_TYPES import imip_store +import re import sys try: @@ -47,6 +50,18 @@ "text/x-vcalendar", "application/ics", # other possibilities ] +# iCalendar date and datetime parsing (from DateSupport in MoinSupport). + +date_icalendar_regexp_str = ur'(?P[0-9]{4})(?P[0-9]{2})(?P[0-9]{2})' +datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ + ur'(?:' \ + ur'T(?P[0-2][0-9])(?P[0-5][0-9])(?P[0-6][0-9])' \ + ur'(?PZ)?' \ + ur')?' + +match_date_icalendar = re.compile(date_icalendar_regexp_str, re.UNICODE).match +match_datetime_icalendar = re.compile(datetime_icalendar_regexp_str, re.UNICODE).match + # Content interpretation. def get_itip_structure(elements): @@ -104,12 +119,49 @@ def get_value(d, name): return get_values(d, name, False) +def get_utc_datetime(d, name): + value, attr = get_item(d, name) + dt = get_datetime(value, attr) + if isinstance(dt, datetime): + return dt.astimezone(timezone("UTC")).strftime("%Y%m%dT%H%M%SZ") + else: + return dt.strftime("%Y%m%d") + def get_address(value): return value.startswith("mailto:") and value[7:] or value def get_uri(value): return value.startswith("mailto:") and value or "mailto:%s" % value +def get_datetime(value, attr): + try: + tz = attr.has_key("TZID") and timezone(attr["TZID"]) or None + except UnknownTimeZoneError: + tz = None + + m = match_datetime_icalendar(value) + if m: + dt = datetime( + int(m.group("year")), int(m.group("month")), int(m.group("day")), + int(m.group("hour")), int(m.group("minute")), int(m.group("second")) + ) + + # Impose the indicated timezone. + # NOTE: This needs an ambiguity policy for DST changes. + + tz = m.group("utc") and timezone("UTC") or tz or None + if tz is not None: + return tz.localize(dt) + else: + return dt + + m = match_date_icalendar(value) + if m: + return date( + int(m.group("year")), int(m.group("month")), int(m.group("day")) + ) + return None + # Time management. def insert_period(freebusy, period): @@ -302,6 +354,9 @@ def get_value(self, name): return get_value(self.details, name) + def get_utc_datetime(self, name): + return get_utc_datetime(self.details, name) + def filter_by_recipients(self, values): return self.recipients.intersection(map(get_address, values)) @@ -374,6 +429,8 @@ # Process each attendee separately. + calendar = [] + for attendee, attendee_attr in attendees.items(): # Check for event using UID. @@ -402,8 +459,8 @@ # If newer than any old version, discard old details from the # free/busy record and check for suitability. - dtstart = self.get_value("DTSTART") - dtend = self.get_value("DTEND") + dtstart = self.get_utc_datetime("DTSTART") + dtend = self.get_utc_datetime("DTEND") conflict = False freebusy = self.store.get_freebusy(attendee) @@ -432,7 +489,9 @@ attendee_attr["PARTSTAT"] = "DECLINED" self.details["ATTENDEE"] = [(attendee, attendee_attr)] - return [("VEVENT", {}, get_structure_items(self.details))] + calendar.append(("VEVENT", {}, get_structure_items(self.details))) + + return calendar class Freebusy(Handler):