1 #!/usr/bin/env python 2 3 """ 4 Interpretation and preparation of iMIP content, together with a content handling 5 mechanism employed by specific recipients. 6 """ 7 8 from datetime import date, datetime 9 from email.mime.text import MIMEText 10 from pytz import timezone, UnknownTimeZoneError 11 from vCalendar import parse, ParseError, to_dict 12 import imip_store 13 import re 14 15 try: 16 from cStringIO import StringIO 17 except ImportError: 18 from StringIO import StringIO 19 20 # iCalendar date and datetime parsing (from DateSupport in MoinSupport). 21 22 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 23 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 24 ur'(?:' \ 25 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 26 ur'(?P<utc>Z)?' \ 27 ur')?' 28 29 match_date_icalendar = re.compile(date_icalendar_regexp_str, re.UNICODE).match 30 match_datetime_icalendar = re.compile(datetime_icalendar_regexp_str, re.UNICODE).match 31 32 # Content interpretation. 33 34 def get_items(d, name, all=True): 35 36 """ 37 Get all items from 'd' with the given 'name', returning single items if 38 'all' is specified and set to a false value and if only one value is 39 present for the name. Return None if no items are found for the name. 40 """ 41 42 if d.has_key(name): 43 values = d[name] 44 if not all and len(values) == 1: 45 return values[0] 46 else: 47 return values 48 else: 49 return None 50 51 def get_item(d, name): 52 return get_items(d, name, False) 53 54 def get_value_map(d, name): 55 56 """ 57 Return a dictionary for all items in 'd' having the given 'name'. The 58 dictionary will map values for the name to any attributes or qualifiers 59 that may have been present. 60 """ 61 62 items = get_items(d, name) 63 if items: 64 return dict(items) 65 else: 66 return {} 67 68 def get_values(d, name, all=True): 69 if d.has_key(name): 70 values = d[name] 71 if not all and len(values) == 1: 72 return values[0][0] 73 else: 74 return map(lambda x: x[0], values) 75 else: 76 return None 77 78 def get_value(d, name): 79 return get_values(d, name, False) 80 81 def get_utc_datetime(d, name): 82 value, attr = get_item(d, name) 83 dt = get_datetime(value, attr) 84 return to_utc_datetime(dt) 85 86 def to_utc_datetime(dt): 87 if not dt: 88 return None 89 elif isinstance(dt, datetime): 90 return dt.astimezone(timezone("UTC")) 91 else: 92 return dt 93 94 def format_datetime(dt): 95 if not dt: 96 return None 97 elif isinstance(dt, datetime): 98 return dt.strftime("%Y%m%dT%H%M%SZ") 99 else: 100 return dt.strftime("%Y%m%d") 101 102 def get_address(value): 103 return value.startswith("mailto:") and value[7:] or value 104 105 def get_uri(value): 106 return value.startswith("mailto:") and value or "mailto:%s" % value 107 108 def get_datetime(value, attr): 109 try: 110 tz = attr.has_key("TZID") and timezone(attr["TZID"]) or None 111 except UnknownTimeZoneError: 112 tz = None 113 114 if attr.get("VALUE") in (None, "DATE-TIME"): 115 m = match_datetime_icalendar(value) 116 if m: 117 dt = datetime( 118 int(m.group("year")), int(m.group("month")), int(m.group("day")), 119 int(m.group("hour")), int(m.group("minute")), int(m.group("second")) 120 ) 121 122 # Impose the indicated timezone. 123 # NOTE: This needs an ambiguity policy for DST changes. 124 125 tz = m.group("utc") and timezone("UTC") or tz or None 126 if tz is not None: 127 return tz.localize(dt) 128 else: 129 return dt 130 131 if attr.get("VALUE") == "DATE": 132 m = match_date_icalendar(value) 133 if m: 134 return date( 135 int(m.group("year")), int(m.group("month")), int(m.group("day")) 136 ) 137 return None 138 139 # Handler mechanism objects. 140 141 def handle_itip_part(part, recipients, handlers): 142 143 """ 144 Handle the given iTIP 'part' for the given 'recipients' using the given 145 'handlers'. 146 """ 147 148 method = part.get_param("method") 149 150 # Decode the data and parse it. 151 152 f = StringIO(part.get_payload(decode=True)) 153 154 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 155 156 # Ignore the part if not a calendar object. 157 158 if not itip: 159 return [] 160 161 # Only handle calendar information. 162 163 all_parts = [] 164 165 # Require consistency between declared and employed methods. 166 167 if get_value(itip, "METHOD") == method: 168 169 # Look for different kinds of sections. 170 171 all_objects = [] 172 173 for name, cls in handlers: 174 for details in get_values(itip, name) or []: 175 176 # Dispatch to a handler and obtain any response. 177 178 handler = cls(details, recipients) 179 object = methods[method](handler)() 180 181 # Concatenate responses for a single calendar object. 182 183 if object: 184 all_objects += object 185 186 # Obtain a message part for the objects. 187 188 if all_objects: 189 all_parts.append(to_part(response_methods[method], all_objects)) 190 191 return all_parts 192 193 def parse_object(f, encoding, objtype): 194 195 """ 196 Parse the iTIP content from 'f' having the given 'encoding'. Return None if 197 the content was not readable or suitable. 198 """ 199 200 try: 201 try: 202 doctype, attrs, elements = obj = parse(f, encoding=encoding) 203 if doctype == objtype: 204 return to_dict(obj)[objtype][0] 205 finally: 206 f.close() 207 except (ParseError, ValueError): 208 pass 209 210 return None 211 212 def to_part(method, calendar): 213 214 """ 215 Write using the given 'method', the 'calendar' details to a MIME 216 text/calendar part. 217 """ 218 219 encoding = "utf-8" 220 out = StringIO() 221 try: 222 imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) 223 part = MIMEText(out.getvalue(), "calendar", encoding) 224 part.set_param("method", method) 225 return part 226 227 finally: 228 out.close() 229 230 class Handler: 231 232 "General handler support." 233 234 def __init__(self, details, recipients): 235 236 """ 237 Initialise the handler with the 'details' of a calendar object and the 238 'recipients' of the object. 239 """ 240 241 self.details = details 242 self.recipients = set(recipients) 243 244 self.uid = get_value(details, "UID") 245 self.sequence = get_value(details, "SEQUENCE") 246 self.dtstamp = get_value(details, "DTSTAMP") 247 248 self.store = imip_store.FileStore() 249 250 try: 251 self.publisher = imip_store.FilePublisher() 252 except OSError: 253 self.publisher = None 254 255 def get_items(self, name, all=True): 256 return get_items(self.details, name, all) 257 258 def get_item(self, name): 259 return get_item(self.details, name) 260 261 def get_value_map(self, name): 262 return get_value_map(self.details, name) 263 264 def get_values(self, name, all=True): 265 return get_values(self.details, name, all) 266 267 def get_value(self, name): 268 return get_value(self.details, name) 269 270 def get_utc_datetime(self, name): 271 return get_utc_datetime(self.details, name) 272 273 def filter_by_recipients(self, values): 274 return self.recipients.intersection(map(get_address, values)) 275 276 def require_organiser_and_attendees(self): 277 attendee_map = self.get_value_map("ATTENDEE") 278 organiser = self.get_item("ORGANIZER") 279 280 # Only provide details for recipients who are also attendees. 281 282 attendees = {} 283 for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): 284 attendees[attendee] = attendee_map[attendee] 285 286 if not attendees and not organiser: 287 return None 288 289 return organiser, attendees 290 291 # Handler registry. 292 293 methods = { 294 "ADD" : lambda handler: handler.add, 295 "CANCEL" : lambda handler: handler.cancel, 296 "COUNTER" : lambda handler: handler.counter, 297 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 298 "PUBLISH" : lambda handler: handler.publish, 299 "REFRESH" : lambda handler: handler.refresh, 300 "REPLY" : lambda handler: handler.reply, 301 "REQUEST" : lambda handler: handler.request, 302 } 303 304 response_methods = { 305 "REQUEST" : "REPLY", 306 } 307 308 # vim: tabstop=4 expandtab shiftwidth=4