# HG changeset patch # User Paul Boddie # Date 1413914300 -7200 # Node ID e1e6ca8ec42b701966d73be67408d78ae99b69b2 # Parent 65898c7c7d9f2cda244081f9ad065bb244129b38 Moved content processing and handling functionality into a separate package. Fixed permissions on various modules. Added a special module for resource handling. diff -r 65898c7c7d9f -r e1e6ca8ec42b imip_agent.py --- a/imip_agent.py Thu Oct 09 22:50:41 2014 +0200 +++ b/imip_agent.py Tue Oct 21 19:58:20 2014 +0200 @@ -1,23 +1,13 @@ #!/usr/bin/env python -from bisect import bisect_left, insort_left -from datetime import date, datetime, timedelta 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, to_dict, to_node -from vRecurrence import get_parameters, get_rule, to_tuple -import imip_store -import re +from imiptools.content import handle_itip_part +import imip_resource import sys -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - MESSAGE_SENDER = "resources+agent@example.com" MESSAGE_SUBJECT = "Calendar system message" @@ -37,134 +27,6 @@ "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_items(d, name, all=True): - if d.has_key(name): - values = d[name] - if not all and len(values) == 1: - return values[0] - else: - return values - else: - return None - -def get_item(d, name): - return get_items(d, name, False) - -def get_value_map(d, name): - items = get_items(d, name) - if items: - return dict(items) - else: - return {} - -def get_values(d, name, all=True): - if d.has_key(name): - values = d[name] - if not all and len(values) == 1: - return values[0][0] - else: - return map(lambda x: x[0], values) - else: - return None - -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) - return to_utc_datetime(dt) - -def to_utc_datetime(dt): - if not dt: - return None - elif isinstance(dt, datetime): - return dt.astimezone(timezone("UTC")) - else: - return dt - -def format_datetime(dt): - if not dt: - return None - elif isinstance(dt, datetime): - return dt.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 - - if attr.get("VALUE") in (None, "DATE-TIME"): - 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 - - if attr.get("VALUE") == "DATE": - 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): - insort_left(freebusy, period) - -def remove_period(freebusy, uid): - i = 0 - while i < len(freebusy): - t = freebusy[i] - if len(t) >= 3 and t[2] == uid: - del freebusy[i] - else: - i += 1 - -def period_overlaps(freebusy, period): - dtstart, dtend = period[:2] - i = bisect_left(freebusy, (dtstart, dtend, None)) - return ( - i < len(freebusy) and (dtend is None or freebusy[i][0] < dtend) - or - i > 0 and freebusy[i - 1][1] > dtstart - ) - # Sending of outgoing messages. def sendmail(sender, recipients, data): @@ -194,7 +56,9 @@ if part.get_content_type() in itip_content_types and \ part.get_param("method"): - all_parts += handle_itip_part(part, original_recipients) + # NOTE: Act on behalf of resources for now. + + all_parts += handle_itip_part(part, original_recipients, imip_resource.handlers) # Pack the parts into a single message. @@ -216,447 +80,10 @@ def get_all_values(msg, key): l = [] - for v in msg.get_all(key): + for v in msg.get_all(key) or []: l += [s.strip() for s in v.split(",")] return l -def to_part(method, calendar): - - """ - Write using the given 'method', the 'calendar' details to a MIME - text/calendar part. - """ - - encoding = "utf-8" - out = StringIO() - try: - imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) - part = MIMEText(out.getvalue(), "calendar", encoding) - part.set_param("method", method) - return part - - finally: - out.close() - -def parse_object(f, encoding, objtype): - - """ - Parse the iTIP content from 'f' having the given 'encoding'. Return None if - the content was not readable or suitable. - """ - - try: - try: - doctype, attrs, elements = obj = parse(f, encoding=encoding) - if doctype == objtype: - return to_dict(obj)[objtype][0] - finally: - f.close() - except (ParseError, ValueError): - pass - - return None - -def handle_itip_part(part, recipients): - - "Handle the given iTIP 'part' for the given 'recipients'." - - method = part.get_param("method") - - # Decode the data and parse it. - - f = StringIO(part.get_payload(decode=True)) - - itip = parse_object(f, part.get_content_charset(), "VCALENDAR") - - # Ignore the part if not a calendar object. - - if not itip: - return [] - - # Only handle calendar information. - - all_parts = [] - - # Require consistency between declared and employed methods. - - if get_value(itip, "METHOD") == method: - - # Look for different kinds of sections. - - all_objects = [] - - for name, cls in handlers: - for details in get_values(itip, name) or []: - - # Dispatch to a handler and obtain any response. - - handler = cls(details, recipients) - object = methods[method](handler)() - - # Concatenate responses for a single calendar object. - - if object: - all_objects += object - - # Obtain a message part for the objects. - - if all_objects: - all_parts.append(to_part(response_methods[method], all_objects)) - - return all_parts - -class Handler: - - "General handler support." - - def __init__(self, details, recipients): - - """ - Initialise the handler with the 'details' of a calendar object and the - 'recipients' of the object. - """ - - self.details = details - self.recipients = set(recipients) - - self.uid = get_value(details, "UID") - self.sequence = get_value(details, "SEQUENCE") - self.dtstamp = get_value(details, "DTSTAMP") - - self.store = imip_store.FileStore() - - try: - self.publisher = imip_store.FilePublisher() - except OSError: - self.publisher = None - - def get_items(self, name, all=True): - return get_items(self.details, name, all) - - def get_item(self, name): - return get_item(self.details, name) - - def get_value_map(self, name): - return get_value_map(self.details, name) - - def get_values(self, name, all=True): - return get_values(self.details, name, all) - - 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)) - - def require_organiser_and_attendees(self): - attendee_map = self.get_value_map("ATTENDEE") - organiser = self.get_item("ORGANIZER") - - # Only provide details for recipients who are also attendees. - - attendees = {} - for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): - attendees[attendee] = attendee_map[attendee] - - if not attendees and not organiser: - return None - - return organiser, attendees - -class Event(Handler): - - "An event handler." - - def add(self): - pass - - def cancel(self): - pass - - def counter(self): - - "Since this handler does not send requests, it will not handle replies." - - pass - - def declinecounter(self): - - """ - Since this handler does not send counter proposals, it will not handle - replies to such proposals. - """ - - pass - - def publish(self): - pass - - def refresh(self): - pass - - def reply(self): - - "Since this handler does not send requests, it will not handle replies." - - pass - - def request(self): - - """ - Respond to a request by preparing a reply containing accept/decline - information for each indicated attendee. - - No support for countering requests is implemented. - """ - - oa = self.require_organiser_and_attendees() - if not oa: - return None - - (organiser, organiser_attr), attendees = oa - - # Process each attendee separately. - - calendar = [] - - for attendee, attendee_attr in attendees.items(): - - # Check for event using UID. - - f = self.store.get_event(attendee, self.uid) - event = f and parse_object(f, "utf-8", "VEVENT") - - # If found, compare SEQUENCE and potentially DTSTAMP. - - if event: - sequence = get_value(event, "SEQUENCE") - dtstamp = get_value(event, "DTSTAMP") - - # If the request refers to an older version of the event, ignore - # it. - - old_dtstamp = self.dtstamp < dtstamp - - if sequence is not None and ( - int(self.sequence) < int(sequence) or - int(self.sequence) == int(sequence) and old_dtstamp - ) or old_dtstamp: - - continue - - # If newer than any old version, discard old details from the - # free/busy record and check for suitability. - - dtstart = self.get_utc_datetime("DTSTART") - dtend = self.get_utc_datetime("DTEND") - - # NOTE: Need also DURATION support. - - duration = dtend - dtstart - - # Recurrence rules create multiple instances to be checked. - # Conflicts may only be assessed within a period defined by policy - # for the agent, with instances outside that period being considered - # unchecked. - - window_end = datetime.now() + timedelta(100) - - # NOTE: Need also RDATE and EXDATE support. - - rrule = self.get_value("RRULE") - - if rrule: - selector = get_rule(dtstart, rrule) - parameters = get_parameters(rrule) - periods = [] - for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): - start = datetime(*start, tzinfo=timezone("UTC")) - end = start + duration - periods.append((format_datetime(start), format_datetime(end))) - else: - periods = [(format_datetime(dtstart), format_datetime(dtend))] - - conflict = False - freebusy = self.store.get_freebusy(attendee) - - if freebusy: - remove_period(freebusy, self.uid) - conflict = True - for start, end in periods: - if period_overlaps(freebusy, (start, end)): - break - else: - conflict = False - else: - freebusy = [] - - # If the event can be scheduled, it is registered and a reply sent - # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an - # attribute.) - - if not conflict: - for start, end in periods: - insert_period(freebusy, (start, end, self.uid)) - - if self.get_value("TRANSP") in (None, "OPAQUE"): - self.store.set_freebusy(attendee, freebusy) - - if self.publisher: - self.publisher.set_freebusy(attendee, freebusy) - - self.store.set_event(attendee, self.uid, to_node( - {"VEVENT" : [(self.details, {})]} - )) - attendee_attr["PARTSTAT"] = "ACCEPTED" - - # If the event cannot be scheduled, it is not registered and a reply - # sent declining the event. (The attendee has PARTSTAT=DECLINED as an - # attribute.) - - else: - attendee_attr["PARTSTAT"] = "DECLINED" - - self.details["ATTENDEE"] = [(attendee, attendee_attr)] - calendar.append(to_node( - {"VEVENT" : [(self.details, {})]} - )) - - return calendar - -class Freebusy(Handler): - - "A free/busy handler." - - def publish(self): - pass - - def reply(self): - - "Since this handler does not send requests, it will not handle replies." - - pass - - def request(self): - - """ - Respond to a request by preparing a reply containing free/busy - information for each indicated attendee. - """ - - oa = self.require_organiser_and_attendees() - if not oa: - return None - - (organiser, organiser_attr), attendees = oa - - # Construct an appropriate fragment. - - calendar = [] - cwrite = calendar.append - - # Get the details for each attendee. - - for attendee, attendee_attr in attendees.items(): - freebusy = self.store.get_freebusy(attendee) - - if freebusy: - record = [] - rwrite = record.append - - rwrite(("ORGANIZER", organiser_attr, organiser)) - rwrite(("ATTENDEE", attendee_attr, attendee)) - rwrite(("UID", {}, self.uid)) - - for start, end, uid in freebusy: - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end])) - - cwrite(("VFREEBUSY", {}, record)) - - # Return the reply. - - return calendar - -class Journal(Handler): - - "A journal entry handler." - - def add(self): - pass - - def cancel(self): - pass - - def publish(self): - pass - -class Todo(Handler): - - "A to-do item handler." - - def add(self): - pass - - def cancel(self): - pass - - def counter(self): - - "Since this handler does not send requests, it will not handle replies." - - pass - - def declinecounter(self): - - """ - Since this handler does not send counter proposals, it will not handle - replies to such proposals. - """ - - pass - - def publish(self): - pass - - def refresh(self): - pass - - def reply(self): - - "Since this handler does not send requests, it will not handle replies." - - pass - - def request(self): - pass - -# Handler registry. - -handlers = [ - ("VFREEBUSY", Freebusy), - ("VEVENT", Event), - ("VTODO", Todo), - ("VJOURNAL", Journal), - ] - -methods = { - "ADD" : lambda handler: handler.add, - "CANCEL" : lambda handler: handler.cancel, - "COUNTER" : lambda handler: handler.counter, - "DECLINECOUNTER" : lambda handler: handler.declinecounter, - "PUBLISH" : lambda handler: handler.publish, - "REFRESH" : lambda handler: handler.refresh, - "REPLY" : lambda handler: handler.reply, - "REQUEST" : lambda handler: handler.request, - } - -response_methods = { - "REQUEST" : "REPLY", - } - def main(): "Interpret program arguments and process input." diff -r 65898c7c7d9f -r e1e6ca8ec42b imip_resource.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imip_resource.py Tue Oct 21 19:58:20 2014 +0200 @@ -0,0 +1,290 @@ +#!/usr/bin/env python + +""" +Handlers for a resource. +""" + +from datetime import date, datetime, timedelta +from imiptools.content import Handler, format_datetime, get_value, parse_object +from imiptools.period import insert_period, period_overlaps, remove_period +from vCalendar import to_node +from vRecurrence import get_parameters, get_rule + +class Event(Handler): + + "An event handler." + + def add(self): + pass + + def cancel(self): + pass + + def counter(self): + + "Since this handler does not send requests, it will not handle replies." + + pass + + def declinecounter(self): + + """ + Since this handler does not send counter proposals, it will not handle + replies to such proposals. + """ + + pass + + def publish(self): + pass + + def refresh(self): + pass + + def reply(self): + + "Since this handler does not send requests, it will not handle replies." + + pass + + def request(self): + + """ + Respond to a request by preparing a reply containing accept/decline + information for each indicated attendee. + + No support for countering requests is implemented. + """ + + oa = self.require_organiser_and_attendees() + if not oa: + return None + + (organiser, organiser_attr), attendees = oa + + # Process each attendee separately. + + calendar = [] + + for attendee, attendee_attr in attendees.items(): + + # Check for event using UID. + + f = self.store.get_event(attendee, self.uid) + event = f and parse_object(f, "utf-8", "VEVENT") + + # If found, compare SEQUENCE and potentially DTSTAMP. + + if event: + sequence = get_value(event, "SEQUENCE") + dtstamp = get_value(event, "DTSTAMP") + + # If the request refers to an older version of the event, ignore + # it. + + old_dtstamp = self.dtstamp < dtstamp + + if sequence is not None and ( + int(self.sequence) < int(sequence) or + int(self.sequence) == int(sequence) and old_dtstamp + ) or old_dtstamp: + + continue + + # If newer than any old version, discard old details from the + # free/busy record and check for suitability. + + dtstart = self.get_utc_datetime("DTSTART") + dtend = self.get_utc_datetime("DTEND") + + # NOTE: Need also DURATION support. + + duration = dtend - dtstart + + # Recurrence rules create multiple instances to be checked. + # Conflicts may only be assessed within a period defined by policy + # for the agent, with instances outside that period being considered + # unchecked. + + # NOTE: Need to expose the 100 day window in the configuration. + + window_end = datetime.now() + timedelta(100) + + # NOTE: Need also RDATE and EXDATE support. + + rrule = self.get_value("RRULE") + + if rrule: + selector = get_rule(dtstart, rrule) + parameters = get_parameters(rrule) + periods = [] + for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): + start = datetime(*start, tzinfo=timezone("UTC")) + end = start + duration + periods.append((format_datetime(start), format_datetime(end))) + else: + periods = [(format_datetime(dtstart), format_datetime(dtend))] + + conflict = False + freebusy = self.store.get_freebusy(attendee) + + if freebusy: + remove_period(freebusy, self.uid) + conflict = True + for start, end in periods: + if period_overlaps(freebusy, (start, end)): + break + else: + conflict = False + else: + freebusy = [] + + # If the event can be scheduled, it is registered and a reply sent + # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an + # attribute.) + + if not conflict: + for start, end in periods: + insert_period(freebusy, (start, end, self.uid)) + + if self.get_value("TRANSP") in (None, "OPAQUE"): + self.store.set_freebusy(attendee, freebusy) + + if self.publisher: + self.publisher.set_freebusy(attendee, freebusy) + + self.store.set_event(attendee, self.uid, to_node( + {"VEVENT" : [(self.details, {})]} + )) + attendee_attr["PARTSTAT"] = "ACCEPTED" + + # If the event cannot be scheduled, it is not registered and a reply + # sent declining the event. (The attendee has PARTSTAT=DECLINED as an + # attribute.) + + else: + attendee_attr["PARTSTAT"] = "DECLINED" + + self.details["ATTENDEE"] = [(attendee, attendee_attr)] + calendar.append(to_node( + {"VEVENT" : [(self.details, {})]} + )) + + return calendar + +class Freebusy(Handler): + + "A free/busy handler." + + def publish(self): + pass + + def reply(self): + + "Since this handler does not send requests, it will not handle replies." + + pass + + def request(self): + + """ + Respond to a request by preparing a reply containing free/busy + information for each indicated attendee. + """ + + oa = self.require_organiser_and_attendees() + if not oa: + return None + + (organiser, organiser_attr), attendees = oa + + # Construct an appropriate fragment. + + calendar = [] + cwrite = calendar.append + + # Get the details for each attendee. + + for attendee, attendee_attr in attendees.items(): + freebusy = self.store.get_freebusy(attendee) + + if freebusy: + record = [] + rwrite = record.append + + rwrite(("ORGANIZER", organiser_attr, organiser)) + rwrite(("ATTENDEE", attendee_attr, attendee)) + rwrite(("UID", {}, self.uid)) + + for start, end, uid in freebusy: + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end])) + + cwrite(("VFREEBUSY", {}, record)) + + # Return the reply. + + return calendar + +class Journal(Handler): + + "A journal entry handler." + + def add(self): + pass + + def cancel(self): + pass + + def publish(self): + pass + +class Todo(Handler): + + "A to-do item handler." + + def add(self): + pass + + def cancel(self): + pass + + def counter(self): + + "Since this handler does not send requests, it will not handle replies." + + pass + + def declinecounter(self): + + """ + Since this handler does not send counter proposals, it will not handle + replies to such proposals. + """ + + pass + + def publish(self): + pass + + def refresh(self): + pass + + def reply(self): + + "Since this handler does not send requests, it will not handle replies." + + pass + + def request(self): + pass + +# Handler registry. + +handlers = [ + ("VFREEBUSY", Freebusy), + ("VEVENT", Event), + ("VTODO", Todo), + ("VJOURNAL", Journal), + ] + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 65898c7c7d9f -r e1e6ca8ec42b imip_store.py diff -r 65898c7c7d9f -r e1e6ca8ec42b imiptools/__init__.py diff -r 65898c7c7d9f -r e1e6ca8ec42b imiptools/content.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/content.py Tue Oct 21 19:58:20 2014 +0200 @@ -0,0 +1,308 @@ +#!/usr/bin/env python + +""" +Interpretation and preparation of iMIP content, together with a content handling +mechanism employed by specific recipients. +""" + +from datetime import date, datetime +from email.mime.text import MIMEText +from pytz import timezone, UnknownTimeZoneError +from vCalendar import parse, ParseError, to_dict +import imip_store +import re + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +# 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_items(d, name, all=True): + + """ + Get all items from 'd' with the given 'name', returning single items if + 'all' is specified and set to a false value and if only one value is + present for the name. Return None if no items are found for the name. + """ + + if d.has_key(name): + values = d[name] + if not all and len(values) == 1: + return values[0] + else: + return values + else: + return None + +def get_item(d, name): + return get_items(d, name, False) + +def get_value_map(d, name): + + """ + Return a dictionary for all items in 'd' having the given 'name'. The + dictionary will map values for the name to any attributes or qualifiers + that may have been present. + """ + + items = get_items(d, name) + if items: + return dict(items) + else: + return {} + +def get_values(d, name, all=True): + if d.has_key(name): + values = d[name] + if not all and len(values) == 1: + return values[0][0] + else: + return map(lambda x: x[0], values) + else: + return None + +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) + return to_utc_datetime(dt) + +def to_utc_datetime(dt): + if not dt: + return None + elif isinstance(dt, datetime): + return dt.astimezone(timezone("UTC")) + else: + return dt + +def format_datetime(dt): + if not dt: + return None + elif isinstance(dt, datetime): + return dt.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 + + if attr.get("VALUE") in (None, "DATE-TIME"): + 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 + + if attr.get("VALUE") == "DATE": + m = match_date_icalendar(value) + if m: + return date( + int(m.group("year")), int(m.group("month")), int(m.group("day")) + ) + return None + +# Handler mechanism objects. + +def handle_itip_part(part, recipients, handlers): + + """ + Handle the given iTIP 'part' for the given 'recipients' using the given + 'handlers'. + """ + + method = part.get_param("method") + + # Decode the data and parse it. + + f = StringIO(part.get_payload(decode=True)) + + itip = parse_object(f, part.get_content_charset(), "VCALENDAR") + + # Ignore the part if not a calendar object. + + if not itip: + return [] + + # Only handle calendar information. + + all_parts = [] + + # Require consistency between declared and employed methods. + + if get_value(itip, "METHOD") == method: + + # Look for different kinds of sections. + + all_objects = [] + + for name, cls in handlers: + for details in get_values(itip, name) or []: + + # Dispatch to a handler and obtain any response. + + handler = cls(details, recipients) + object = methods[method](handler)() + + # Concatenate responses for a single calendar object. + + if object: + all_objects += object + + # Obtain a message part for the objects. + + if all_objects: + all_parts.append(to_part(response_methods[method], all_objects)) + + return all_parts + +def parse_object(f, encoding, objtype): + + """ + Parse the iTIP content from 'f' having the given 'encoding'. Return None if + the content was not readable or suitable. + """ + + try: + try: + doctype, attrs, elements = obj = parse(f, encoding=encoding) + if doctype == objtype: + return to_dict(obj)[objtype][0] + finally: + f.close() + except (ParseError, ValueError): + pass + + return None + +def to_part(method, calendar): + + """ + Write using the given 'method', the 'calendar' details to a MIME + text/calendar part. + """ + + encoding = "utf-8" + out = StringIO() + try: + imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) + part = MIMEText(out.getvalue(), "calendar", encoding) + part.set_param("method", method) + return part + + finally: + out.close() + +class Handler: + + "General handler support." + + def __init__(self, details, recipients): + + """ + Initialise the handler with the 'details' of a calendar object and the + 'recipients' of the object. + """ + + self.details = details + self.recipients = set(recipients) + + self.uid = get_value(details, "UID") + self.sequence = get_value(details, "SEQUENCE") + self.dtstamp = get_value(details, "DTSTAMP") + + self.store = imip_store.FileStore() + + try: + self.publisher = imip_store.FilePublisher() + except OSError: + self.publisher = None + + def get_items(self, name, all=True): + return get_items(self.details, name, all) + + def get_item(self, name): + return get_item(self.details, name) + + def get_value_map(self, name): + return get_value_map(self.details, name) + + def get_values(self, name, all=True): + return get_values(self.details, name, all) + + 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)) + + def require_organiser_and_attendees(self): + attendee_map = self.get_value_map("ATTENDEE") + organiser = self.get_item("ORGANIZER") + + # Only provide details for recipients who are also attendees. + + attendees = {} + for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): + attendees[attendee] = attendee_map[attendee] + + if not attendees and not organiser: + return None + + return organiser, attendees + +# Handler registry. + +methods = { + "ADD" : lambda handler: handler.add, + "CANCEL" : lambda handler: handler.cancel, + "COUNTER" : lambda handler: handler.counter, + "DECLINECOUNTER" : lambda handler: handler.declinecounter, + "PUBLISH" : lambda handler: handler.publish, + "REFRESH" : lambda handler: handler.refresh, + "REPLY" : lambda handler: handler.reply, + "REQUEST" : lambda handler: handler.request, + } + +response_methods = { + "REQUEST" : "REPLY", + } + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 65898c7c7d9f -r e1e6ca8ec42b imiptools/period.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/period.py Tue Oct 21 19:58:20 2014 +0200 @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +from bisect import bisect_left, insort_left + +# Time management. + +def insert_period(freebusy, period): + insort_left(freebusy, period) + +def remove_period(freebusy, uid): + i = 0 + while i < len(freebusy): + t = freebusy[i] + if len(t) >= 3 and t[2] == uid: + del freebusy[i] + else: + i += 1 + +def period_overlaps(freebusy, period): + dtstart, dtend = period[:2] + i = bisect_left(freebusy, (dtstart, dtend, None)) + return ( + i < len(freebusy) and (dtend is None or freebusy[i][0] < dtend) + or + i > 0 and freebusy[i - 1][1] > dtstart + ) + +# vim: tabstop=4 expandtab shiftwidth=4