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'. Return a list of responses, each response being a tuple of the 146 form (is-outgoing, message-part). 147 """ 148 149 method = part.get_param("method") 150 151 # Decode the data and parse it. 152 153 f = StringIO(part.get_payload(decode=True)) 154 155 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 156 157 # Ignore the part if not a calendar object. 158 159 if not itip: 160 return [] 161 162 # Require consistency between declared and employed methods. 163 164 if get_value(itip, "METHOD") == method: 165 166 # Look for different kinds of sections. 167 168 all_results = [] 169 170 for name, cls in handlers: 171 for details in get_values(itip, name) or []: 172 173 # Dispatch to a handler and obtain any response. 174 175 handler = cls(details, recipients) 176 result = methods[method](handler)() 177 178 # Concatenate responses for a single calendar object. 179 180 if result: 181 response_method, part = result 182 outgoing = method != response_method 183 all_results.append((outgoing, part)) 184 185 return all_results 186 187 return [] 188 189 def parse_object(f, encoding, objtype=None): 190 191 """ 192 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 193 given, only objects of that type will be returned. 194 195 Return None if the content was not readable or suitable. 196 """ 197 198 try: 199 try: 200 doctype, attrs, elements = obj = parse(f, encoding=encoding) 201 if objtype and doctype == objtype: 202 return to_dict(obj)[objtype][0] 203 elif not objtype: 204 return to_dict(obj)[doctype][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 # Access to calendar structures and other data. 256 257 def get_items(self, name, all=True): 258 return get_items(self.details, name, all) 259 260 def get_item(self, name): 261 return get_item(self.details, name) 262 263 def get_value_map(self, name): 264 return get_value_map(self.details, name) 265 266 def get_values(self, name, all=True): 267 return get_values(self.details, name, all) 268 269 def get_value(self, name): 270 return get_value(self.details, name) 271 272 def get_utc_datetime(self, name): 273 return get_utc_datetime(self.details, name) 274 275 def filter_by_recipients(self, values): 276 return self.recipients.intersection(map(get_address, values)) 277 278 def require_organiser_and_attendees(self): 279 280 """ 281 Return the organiser and attendees for the current object, filtered by 282 the recipients of interest. Return None if no identities are eligible. 283 """ 284 285 attendee_map = self.get_value_map("ATTENDEE") 286 organiser = self.get_item("ORGANIZER") 287 288 # Only provide details for recipients who are also attendees. 289 290 attendees = {} 291 for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): 292 attendees[attendee] = attendee_map[attendee] 293 294 if not attendees and not organiser: 295 return None 296 297 return organiser, attendees 298 299 def have_new_object(self, attendee, objtype): 300 301 """ 302 Return whether the current object is new to the 'attendee' for the 303 given 'objtype'. 304 """ 305 306 f = self.store.get_event(attendee, self.uid) 307 event = f and parse_object(f, "utf-8", objtype) 308 309 # If found, compare SEQUENCE and potentially DTSTAMP. 310 311 if event: 312 sequence = get_value(event, "SEQUENCE") 313 dtstamp = get_value(event, "DTSTAMP") 314 315 # If the request refers to an older version of the event, ignore 316 # it. 317 318 old_dtstamp = self.dtstamp < dtstamp 319 320 if sequence is not None and ( 321 int(self.sequence) < int(sequence) or 322 int(self.sequence) == int(sequence) and old_dtstamp 323 ) or old_dtstamp: 324 325 return False 326 327 return True 328 329 # Handler registry. 330 331 methods = { 332 "ADD" : lambda handler: handler.add, 333 "CANCEL" : lambda handler: handler.cancel, 334 "COUNTER" : lambda handler: handler.counter, 335 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 336 "PUBLISH" : lambda handler: handler.publish, 337 "REFRESH" : lambda handler: handler.refresh, 338 "REPLY" : lambda handler: handler.reply, 339 "REQUEST" : lambda handler: handler.request, 340 } 341 342 # vim: tabstop=4 expandtab shiftwidth=4