1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imiptools/content.py Tue Oct 21 19:58:20 2014 +0200
1.3 @@ -0,0 +1,308 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Interpretation and preparation of iMIP content, together with a content handling
1.8 +mechanism employed by specific recipients.
1.9 +"""
1.10 +
1.11 +from datetime import date, datetime
1.12 +from email.mime.text import MIMEText
1.13 +from pytz import timezone, UnknownTimeZoneError
1.14 +from vCalendar import parse, ParseError, to_dict
1.15 +import imip_store
1.16 +import re
1.17 +
1.18 +try:
1.19 + from cStringIO import StringIO
1.20 +except ImportError:
1.21 + from StringIO import StringIO
1.22 +
1.23 +# iCalendar date and datetime parsing (from DateSupport in MoinSupport).
1.24 +
1.25 +date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'
1.26 +datetime_icalendar_regexp_str = date_icalendar_regexp_str + \
1.27 + ur'(?:' \
1.28 + ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \
1.29 + ur'(?P<utc>Z)?' \
1.30 + ur')?'
1.31 +
1.32 +match_date_icalendar = re.compile(date_icalendar_regexp_str, re.UNICODE).match
1.33 +match_datetime_icalendar = re.compile(datetime_icalendar_regexp_str, re.UNICODE).match
1.34 +
1.35 +# Content interpretation.
1.36 +
1.37 +def get_items(d, name, all=True):
1.38 +
1.39 + """
1.40 + Get all items from 'd' with the given 'name', returning single items if
1.41 + 'all' is specified and set to a false value and if only one value is
1.42 + present for the name. Return None if no items are found for the name.
1.43 + """
1.44 +
1.45 + if d.has_key(name):
1.46 + values = d[name]
1.47 + if not all and len(values) == 1:
1.48 + return values[0]
1.49 + else:
1.50 + return values
1.51 + else:
1.52 + return None
1.53 +
1.54 +def get_item(d, name):
1.55 + return get_items(d, name, False)
1.56 +
1.57 +def get_value_map(d, name):
1.58 +
1.59 + """
1.60 + Return a dictionary for all items in 'd' having the given 'name'. The
1.61 + dictionary will map values for the name to any attributes or qualifiers
1.62 + that may have been present.
1.63 + """
1.64 +
1.65 + items = get_items(d, name)
1.66 + if items:
1.67 + return dict(items)
1.68 + else:
1.69 + return {}
1.70 +
1.71 +def get_values(d, name, all=True):
1.72 + if d.has_key(name):
1.73 + values = d[name]
1.74 + if not all and len(values) == 1:
1.75 + return values[0][0]
1.76 + else:
1.77 + return map(lambda x: x[0], values)
1.78 + else:
1.79 + return None
1.80 +
1.81 +def get_value(d, name):
1.82 + return get_values(d, name, False)
1.83 +
1.84 +def get_utc_datetime(d, name):
1.85 + value, attr = get_item(d, name)
1.86 + dt = get_datetime(value, attr)
1.87 + return to_utc_datetime(dt)
1.88 +
1.89 +def to_utc_datetime(dt):
1.90 + if not dt:
1.91 + return None
1.92 + elif isinstance(dt, datetime):
1.93 + return dt.astimezone(timezone("UTC"))
1.94 + else:
1.95 + return dt
1.96 +
1.97 +def format_datetime(dt):
1.98 + if not dt:
1.99 + return None
1.100 + elif isinstance(dt, datetime):
1.101 + return dt.strftime("%Y%m%dT%H%M%SZ")
1.102 + else:
1.103 + return dt.strftime("%Y%m%d")
1.104 +
1.105 +def get_address(value):
1.106 + return value.startswith("mailto:") and value[7:] or value
1.107 +
1.108 +def get_uri(value):
1.109 + return value.startswith("mailto:") and value or "mailto:%s" % value
1.110 +
1.111 +def get_datetime(value, attr):
1.112 + try:
1.113 + tz = attr.has_key("TZID") and timezone(attr["TZID"]) or None
1.114 + except UnknownTimeZoneError:
1.115 + tz = None
1.116 +
1.117 + if attr.get("VALUE") in (None, "DATE-TIME"):
1.118 + m = match_datetime_icalendar(value)
1.119 + if m:
1.120 + dt = datetime(
1.121 + int(m.group("year")), int(m.group("month")), int(m.group("day")),
1.122 + int(m.group("hour")), int(m.group("minute")), int(m.group("second"))
1.123 + )
1.124 +
1.125 + # Impose the indicated timezone.
1.126 + # NOTE: This needs an ambiguity policy for DST changes.
1.127 +
1.128 + tz = m.group("utc") and timezone("UTC") or tz or None
1.129 + if tz is not None:
1.130 + return tz.localize(dt)
1.131 + else:
1.132 + return dt
1.133 +
1.134 + if attr.get("VALUE") == "DATE":
1.135 + m = match_date_icalendar(value)
1.136 + if m:
1.137 + return date(
1.138 + int(m.group("year")), int(m.group("month")), int(m.group("day"))
1.139 + )
1.140 + return None
1.141 +
1.142 +# Handler mechanism objects.
1.143 +
1.144 +def handle_itip_part(part, recipients, handlers):
1.145 +
1.146 + """
1.147 + Handle the given iTIP 'part' for the given 'recipients' using the given
1.148 + 'handlers'.
1.149 + """
1.150 +
1.151 + method = part.get_param("method")
1.152 +
1.153 + # Decode the data and parse it.
1.154 +
1.155 + f = StringIO(part.get_payload(decode=True))
1.156 +
1.157 + itip = parse_object(f, part.get_content_charset(), "VCALENDAR")
1.158 +
1.159 + # Ignore the part if not a calendar object.
1.160 +
1.161 + if not itip:
1.162 + return []
1.163 +
1.164 + # Only handle calendar information.
1.165 +
1.166 + all_parts = []
1.167 +
1.168 + # Require consistency between declared and employed methods.
1.169 +
1.170 + if get_value(itip, "METHOD") == method:
1.171 +
1.172 + # Look for different kinds of sections.
1.173 +
1.174 + all_objects = []
1.175 +
1.176 + for name, cls in handlers:
1.177 + for details in get_values(itip, name) or []:
1.178 +
1.179 + # Dispatch to a handler and obtain any response.
1.180 +
1.181 + handler = cls(details, recipients)
1.182 + object = methods[method](handler)()
1.183 +
1.184 + # Concatenate responses for a single calendar object.
1.185 +
1.186 + if object:
1.187 + all_objects += object
1.188 +
1.189 + # Obtain a message part for the objects.
1.190 +
1.191 + if all_objects:
1.192 + all_parts.append(to_part(response_methods[method], all_objects))
1.193 +
1.194 + return all_parts
1.195 +
1.196 +def parse_object(f, encoding, objtype):
1.197 +
1.198 + """
1.199 + Parse the iTIP content from 'f' having the given 'encoding'. Return None if
1.200 + the content was not readable or suitable.
1.201 + """
1.202 +
1.203 + try:
1.204 + try:
1.205 + doctype, attrs, elements = obj = parse(f, encoding=encoding)
1.206 + if doctype == objtype:
1.207 + return to_dict(obj)[objtype][0]
1.208 + finally:
1.209 + f.close()
1.210 + except (ParseError, ValueError):
1.211 + pass
1.212 +
1.213 + return None
1.214 +
1.215 +def to_part(method, calendar):
1.216 +
1.217 + """
1.218 + Write using the given 'method', the 'calendar' details to a MIME
1.219 + text/calendar part.
1.220 + """
1.221 +
1.222 + encoding = "utf-8"
1.223 + out = StringIO()
1.224 + try:
1.225 + imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding)
1.226 + part = MIMEText(out.getvalue(), "calendar", encoding)
1.227 + part.set_param("method", method)
1.228 + return part
1.229 +
1.230 + finally:
1.231 + out.close()
1.232 +
1.233 +class Handler:
1.234 +
1.235 + "General handler support."
1.236 +
1.237 + def __init__(self, details, recipients):
1.238 +
1.239 + """
1.240 + Initialise the handler with the 'details' of a calendar object and the
1.241 + 'recipients' of the object.
1.242 + """
1.243 +
1.244 + self.details = details
1.245 + self.recipients = set(recipients)
1.246 +
1.247 + self.uid = get_value(details, "UID")
1.248 + self.sequence = get_value(details, "SEQUENCE")
1.249 + self.dtstamp = get_value(details, "DTSTAMP")
1.250 +
1.251 + self.store = imip_store.FileStore()
1.252 +
1.253 + try:
1.254 + self.publisher = imip_store.FilePublisher()
1.255 + except OSError:
1.256 + self.publisher = None
1.257 +
1.258 + def get_items(self, name, all=True):
1.259 + return get_items(self.details, name, all)
1.260 +
1.261 + def get_item(self, name):
1.262 + return get_item(self.details, name)
1.263 +
1.264 + def get_value_map(self, name):
1.265 + return get_value_map(self.details, name)
1.266 +
1.267 + def get_values(self, name, all=True):
1.268 + return get_values(self.details, name, all)
1.269 +
1.270 + def get_value(self, name):
1.271 + return get_value(self.details, name)
1.272 +
1.273 + def get_utc_datetime(self, name):
1.274 + return get_utc_datetime(self.details, name)
1.275 +
1.276 + def filter_by_recipients(self, values):
1.277 + return self.recipients.intersection(map(get_address, values))
1.278 +
1.279 + def require_organiser_and_attendees(self):
1.280 + attendee_map = self.get_value_map("ATTENDEE")
1.281 + organiser = self.get_item("ORGANIZER")
1.282 +
1.283 + # Only provide details for recipients who are also attendees.
1.284 +
1.285 + attendees = {}
1.286 + for attendee in map(get_uri, self.filter_by_recipients(attendee_map)):
1.287 + attendees[attendee] = attendee_map[attendee]
1.288 +
1.289 + if not attendees and not organiser:
1.290 + return None
1.291 +
1.292 + return organiser, attendees
1.293 +
1.294 +# Handler registry.
1.295 +
1.296 +methods = {
1.297 + "ADD" : lambda handler: handler.add,
1.298 + "CANCEL" : lambda handler: handler.cancel,
1.299 + "COUNTER" : lambda handler: handler.counter,
1.300 + "DECLINECOUNTER" : lambda handler: handler.declinecounter,
1.301 + "PUBLISH" : lambda handler: handler.publish,
1.302 + "REFRESH" : lambda handler: handler.refresh,
1.303 + "REPLY" : lambda handler: handler.reply,
1.304 + "REQUEST" : lambda handler: handler.request,
1.305 + }
1.306 +
1.307 +response_methods = {
1.308 + "REQUEST" : "REPLY",
1.309 + }
1.310 +
1.311 +# vim: tabstop=4 expandtab shiftwidth=4