1 #!/usr/bin/env python 2 3 """ 4 Interpretation of vCalendar content. 5 6 Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from bisect import bisect_left 23 from datetime import date, datetime, timedelta 24 from email.mime.text import MIMEText 25 from imiptools.dates import format_datetime, get_datetime, \ 26 get_datetime_item as get_item_from_datetime, \ 27 get_datetime_tzid, \ 28 get_duration, get_period, get_period_item, \ 29 get_recurrence_start_point, \ 30 get_time, get_timestamp, get_tzid, to_datetime, \ 31 to_timezone, to_utc_datetime 32 from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod 33 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node 34 from vRecurrence import get_parameters, get_rule 35 import email.utils 36 37 try: 38 from cStringIO import StringIO 39 except ImportError: 40 from StringIO import StringIO 41 42 class Object: 43 44 "Access to calendar structures." 45 46 def __init__(self, fragment): 47 48 """ 49 Initialise the object with the given 'fragment'. This must be a 50 dictionary mapping an object type (such as "VEVENT") to a tuple 51 containing the object details and attributes, each being a dictionary 52 itself. 53 54 The result of parse_object can be processed to obtain a fragment by 55 obtaining a collection of records for an object type. For example: 56 57 l = parse_object(f, encoding, "VCALENDAR") 58 events = l["VEVENT"] 59 event = events[0] 60 61 Then, the specific object must be presented as follows: 62 63 object = Object({"VEVENT" : event}) 64 65 A convienience function is also provided to initialise objects: 66 67 object = new_object("VEVENT") 68 """ 69 70 self.objtype, (self.details, self.attr) = fragment.items()[0] 71 72 def get_uid(self): 73 return self.get_value("UID") 74 75 def get_recurrenceid(self): 76 77 """ 78 Return the recurrence identifier, normalised to a UTC datetime if 79 specified as a datetime or date with accompanying time zone information, 80 maintained as a date or floating datetime otherwise. If no recurrence 81 identifier is present, None is returned. 82 83 Note that this normalised form of the identifier may well not be the 84 same as the originally-specified identifier because that could have been 85 specified using an accompanying TZID attribute, whereas the normalised 86 form is effectively a converted datetime value. 87 """ 88 89 if not self.has_key("RECURRENCE-ID"): 90 return None 91 dt, attr = self.get_datetime_item("RECURRENCE-ID") 92 93 # Coerce any date to a UTC datetime if TZID was specified. 94 95 tzid = attr.get("TZID") 96 if tzid: 97 dt = to_timezone(to_datetime(dt, tzid), "UTC") 98 return format_datetime(dt) 99 100 def get_recurrence_start_point(self, recurrenceid, tzid): 101 102 """ 103 Return the start point corresponding to the given 'recurrenceid', using 104 the fallback 'tzid' to define the specific point in time referenced by 105 the recurrence identifier if the identifier has a date representation. 106 107 If 'recurrenceid' is given as None, this object's recurrence identifier 108 is used to obtain a start point, but if this object does not provide a 109 recurrence, None is returned. 110 111 A start point is typically used to match free/busy periods which are 112 themselves defined in terms of UTC datetimes. 113 """ 114 115 recurrenceid = recurrenceid or self.get_recurrenceid() 116 if recurrenceid: 117 return get_recurrence_start_point(recurrenceid, tzid) 118 else: 119 return None 120 121 def get_recurrence_start_points(self, recurrenceids, tzid): 122 return [self.get_recurrence_start_point(recurrenceid, tzid) for recurrenceid in recurrenceids] 123 124 # Structure access. 125 126 def add(self, obj): 127 128 "Add 'obj' to the structure." 129 130 name = obj.objtype 131 if not self.details.has_key(name): 132 l = self.details[name] = [] 133 else: 134 l = self.details[name] 135 l.append((obj.details, obj.attr)) 136 137 def copy(self): 138 return Object(self.to_dict()) 139 140 def get_items(self, name, all=True): 141 return get_items(self.details, name, all) 142 143 def get_item(self, name): 144 return get_item(self.details, name) 145 146 def get_value_map(self, name): 147 return get_value_map(self.details, name) 148 149 def get_values(self, name, all=True): 150 return get_values(self.details, name, all) 151 152 def get_value(self, name): 153 return get_value(self.details, name) 154 155 def get_utc_datetime(self, name, date_tzid=None): 156 return get_utc_datetime(self.details, name, date_tzid) 157 158 def get_date_value_items(self, name, tzid=None): 159 return get_date_value_items(self.details, name, tzid) 160 161 def get_date_value_item_periods(self, name, tzid=None): 162 return get_date_value_item_periods(self.details, name, self.get_main_period(tzid).get_duration(), tzid) 163 164 def get_period_values(self, name, tzid=None): 165 return get_period_values(self.details, name, tzid) 166 167 def get_datetime(self, name): 168 t = get_datetime_item(self.details, name) 169 if not t: return None 170 dt, attr = t 171 return dt 172 173 def get_datetime_item(self, name): 174 return get_datetime_item(self.details, name) 175 176 def get_duration(self, name): 177 return get_duration(self.get_value(name)) 178 179 # Serialisation. 180 181 def to_dict(self): 182 return to_dict(self.to_node()) 183 184 def to_node(self): 185 return to_node({self.objtype : [(self.details, self.attr)]}) 186 187 def to_part(self, method, encoding="utf-8", line_length=None): 188 return to_part(method, [self.to_node()], encoding, line_length) 189 190 def to_string(self, encoding="utf-8", line_length=None): 191 return to_string(self.to_node(), encoding, line_length) 192 193 # Direct access to the structure. 194 195 def has_key(self, name): 196 return self.details.has_key(name) 197 198 def get(self, name): 199 return self.details.get(name) 200 201 def keys(self): 202 return self.details.keys() 203 204 def __getitem__(self, name): 205 return self.details[name] 206 207 def __setitem__(self, name, value): 208 self.details[name] = value 209 210 def __delitem__(self, name): 211 del self.details[name] 212 213 def remove(self, name): 214 try: 215 del self[name] 216 except KeyError: 217 pass 218 219 def remove_all(self, names): 220 for name in names: 221 self.remove(name) 222 223 def preserve(self, names): 224 for name in self.keys(): 225 if not name in names: 226 self.remove(name) 227 228 # Computed results. 229 230 def get_main_period(self, tzid=None): 231 232 """ 233 Return a period object corresponding to the main start-end period for 234 the object. 235 """ 236 237 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items() 238 tzid = tzid or get_tzid(dtstart_attr, dtend_attr) 239 return RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr) 240 241 def get_main_period_items(self): 242 243 """ 244 Return two (value, attributes) items corresponding to the main start-end 245 period for the object. 246 """ 247 248 dtstart, dtstart_attr = self.get_datetime_item("DTSTART") 249 250 if self.has_key("DTEND"): 251 dtend, dtend_attr = self.get_datetime_item("DTEND") 252 elif self.has_key("DURATION"): 253 duration = self.get_duration("DURATION") 254 dtend = dtstart + duration 255 dtend_attr = dtstart_attr 256 else: 257 dtend, dtend_attr = dtstart, dtstart_attr 258 259 return (dtstart, dtstart_attr), (dtend, dtend_attr) 260 261 def get_periods(self, tzid, end=None, inclusive=False): 262 263 """ 264 Return periods defined by this object, employing the given 'tzid' where 265 no time zone information is defined, and limiting the collection to a 266 window of time with the given 'end'. 267 268 If 'end' is omitted, only explicit recurrences and recurrences from 269 explicitly-terminated rules will be returned. 270 271 If 'inclusive' is set to a true value, any period occurring at the 'end' 272 will be included. 273 """ 274 275 return get_periods(self, tzid, end, inclusive) 276 277 def has_period(self, tzid, period): 278 279 """ 280 Return whether this object, employing the given 'tzid' where no time 281 zone information is defined, has the given 'period'. 282 """ 283 284 return period in self.get_periods(tzid, period.get_start_point(), inclusive=True) 285 286 def has_recurrence(self, tzid, recurrenceid): 287 288 """ 289 Return whether this object, employing the given 'tzid' where no time 290 zone information is defined, has the given 'recurrenceid'. 291 """ 292 293 start_point = self.get_recurrence_start_point(recurrenceid, tzid) 294 for p in self.get_periods(tzid, start_point, inclusive=True): 295 if p.get_start_point() == start_point: 296 return True 297 return False 298 299 def get_active_periods(self, recurrenceids, tzid, end=None): 300 301 """ 302 Return all periods specified by this object that are not replaced by 303 those defined by 'recurrenceids', using 'tzid' as a fallback time zone 304 to convert floating dates and datetimes, and using 'end' to indicate the 305 end of the time window within which periods are considered. 306 """ 307 308 # Specific recurrences yield all specified periods. 309 310 periods = self.get_periods(tzid, end) 311 312 if self.get_recurrenceid(): 313 return periods 314 315 # Parent objects need to have their periods tested against redefined 316 # recurrences. 317 318 active = [] 319 320 for p in periods: 321 322 # Subtract any recurrences from the free/busy details of a 323 # parent object. 324 325 if not p.is_replaced(recurrenceids): 326 active.append(p) 327 328 return active 329 330 def get_freebusy_period(self, period, only_organiser=False): 331 332 """ 333 Return a free/busy period for the given 'period' provided by this 334 object, using the 'only_organiser' status to produce a suitable 335 transparency value. 336 """ 337 338 return FreeBusyPeriod( 339 period.get_start_point(), 340 period.get_end_point(), 341 self.get_value("UID"), 342 only_organiser and "ORG" or self.get_value("TRANSP") or "OPAQUE", 343 self.get_recurrenceid(), 344 self.get_value("SUMMARY"), 345 get_uri(self.get_value("ORGANIZER")) 346 ) 347 348 def get_participation_status(self, participant): 349 350 """ 351 Return the participation status of the given 'participant', with the 352 special value "ORG" indicating organiser-only participation. 353 """ 354 355 attendees = uri_dict(self.get_value_map("ATTENDEE")) 356 organiser = get_uri(self.get_value("ORGANIZER")) 357 358 attendee_attr = attendees.get(participant) 359 if attendee_attr: 360 return attendee_attr.get("PARTSTAT", "NEEDS-ACTION") 361 elif organiser == participant: 362 return "ORG" 363 364 return None 365 366 def get_participation(self, partstat, include_needs_action=False): 367 368 """ 369 Return whether 'partstat' indicates some kind of participation in an 370 event. If 'include_needs_action' is specified as a true value, events 371 not yet responded to will be treated as events with tentative 372 participation. 373 """ 374 375 return not partstat in ("DECLINED", "DELEGATED", "NEEDS-ACTION") or \ 376 include_needs_action and partstat == "NEEDS-ACTION" or \ 377 partstat == "ORG" 378 379 def get_tzid(self): 380 381 """ 382 Return a time zone identifier used by the start or end datetimes, 383 potentially suitable for converting dates to datetimes. 384 """ 385 386 if not self.has_key("DTSTART"): 387 return None 388 dtstart, dtstart_attr = self.get_datetime_item("DTSTART") 389 if self.has_key("DTEND"): 390 dtend, dtend_attr = self.get_datetime_item("DTEND") 391 else: 392 dtend_attr = None 393 return get_tzid(dtstart_attr, dtend_attr) 394 395 def is_shared(self): 396 397 """ 398 Return whether this object is shared based on the presence of a SEQUENCE 399 property. 400 """ 401 402 return self.get_value("SEQUENCE") is not None 403 404 def possibly_active_from(self, dt, tzid): 405 406 """ 407 Return whether the object is possibly active from or after the given 408 datetime 'dt' using 'tzid' to convert any dates or floating datetimes. 409 """ 410 411 dt = to_datetime(dt, tzid) 412 periods = self.get_periods(tzid) 413 414 for p in periods: 415 if p.get_end_point() > dt: 416 return True 417 418 return self.possibly_recurring_indefinitely() 419 420 def possibly_recurring_indefinitely(self): 421 422 "Return whether this object may recur indefinitely." 423 424 rrule = self.get_value("RRULE") 425 parameters = rrule and get_parameters(rrule) 426 until = parameters and parameters.get("UNTIL") 427 count = parameters and parameters.get("COUNT") 428 429 # Non-recurring periods or constrained recurrences. 430 431 if not rrule or until or count: 432 return False 433 434 # Unconstrained recurring periods will always lie beyond any specified 435 # datetime. 436 437 else: 438 return True 439 440 # Modification methods. 441 442 def set_datetime(self, name, dt, tzid=None): 443 444 """ 445 Set a datetime for property 'name' using 'dt' and the optional fallback 446 'tzid', returning whether an update has occurred. 447 """ 448 449 if dt: 450 old_value = self.get_value(name) 451 self[name] = [get_item_from_datetime(dt, tzid)] 452 return format_datetime(dt) != old_value 453 454 return False 455 456 def set_period(self, period): 457 458 "Set the given 'period' as the main start and end." 459 460 result = self.set_datetime("DTSTART", period.get_start()) 461 result = self.set_datetime("DTEND", period.get_end()) or result 462 if self.has_key("DURATION"): 463 del self["DURATION"] 464 465 return result 466 467 def set_periods(self, periods): 468 469 """ 470 Set the given 'periods' as recurrence date properties, replacing the 471 previous RDATE properties and ignoring any RRULE properties. 472 """ 473 474 old_values = set(self.get_date_value_item_periods("RDATE") or []) 475 new_rdates = [] 476 477 if self.has_key("RDATE"): 478 del self["RDATE"] 479 480 main_changed = False 481 482 for p in periods: 483 if p.origin == "RDATE" and p != self.get_main_period(): 484 new_rdates.append(get_period_item(p.get_start(), p.get_end())) 485 elif p.origin == "DTSTART": 486 main_changed = self.set_period(p) 487 488 if new_rdates: 489 self["RDATE"] = new_rdates 490 491 return main_changed or old_values != set(self.get_date_value_item_periods("RDATE") or []) 492 493 def set_rule(self, rule): 494 495 """ 496 Set the given 'rule' in this object, replacing the previous RRULE 497 property, returning whether the object has changed. The provided 'rule' 498 must be an item. 499 """ 500 501 if not rule: 502 return False 503 504 old_rrule = self.get_item("RRULE") 505 self["RRULE"] = [rule] 506 return old_rrule != rule 507 508 def set_exceptions(self, exceptions): 509 510 """ 511 Set the given 'exceptions' in this object, replacing the previous EXDATE 512 properties, returning whether the object has changed. The provided 513 'exceptions' must be a collection of items. 514 """ 515 516 old_exdates = set(self.get_date_value_item_periods("EXDATE") or []) 517 if exceptions: 518 self["EXDATE"] = exceptions 519 return old_exdates != set(self.get_date_value_item_periods("EXDATE") or []) 520 elif old_exdates: 521 del self["EXDATE"] 522 return True 523 else: 524 return False 525 526 def update_dtstamp(self): 527 528 "Update the DTSTAMP in the object." 529 530 dtstamp = self.get_utc_datetime("DTSTAMP") 531 utcnow = get_time() 532 dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow) 533 self["DTSTAMP"] = [(dtstamp, {})] 534 return dtstamp 535 536 def update_sequence(self, increment=False): 537 538 "Set or update the SEQUENCE in the object." 539 540 sequence = self.get_value("SEQUENCE") or "0" 541 self["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] 542 return sequence 543 544 def update_exceptions(self, excluded, asserted): 545 546 """ 547 Update the exceptions to any rule by applying the list of 'excluded' 548 periods. Where 'asserted' periods are provided, exceptions will be 549 removed corresponding to those periods. 550 """ 551 552 old_exdates = self.get_date_value_item_periods("EXDATE") or [] 553 new_exdates = set(old_exdates) 554 new_exdates.update(excluded) 555 new_exdates.difference_update(asserted) 556 557 if not new_exdates and self.has_key("EXDATE"): 558 del self["EXDATE"] 559 else: 560 self["EXDATE"] = [] 561 for p in new_exdates: 562 self["EXDATE"].append(get_period_item(p.get_start(), p.get_end())) 563 564 return set(old_exdates) != new_exdates 565 566 def correct_object(self, tzid, permitted_values): 567 568 """ 569 Correct the object's period details using the given 'tzid' and 570 'permitted_values'. 571 """ 572 573 corrected = set() 574 rdates = [] 575 576 for period in self.get_periods(tzid): 577 corrected_period = period.get_corrected(permitted_values) 578 579 if corrected_period is period: 580 if period.origin == "RDATE": 581 rdates.append(period) 582 continue 583 584 if period.origin == "DTSTART": 585 self.set_period(corrected_period) 586 corrected.add("DTSTART") 587 elif period.origin == "RDATE": 588 rdates.append(corrected_period) 589 corrected.add("RDATE") 590 591 if "RDATE" in corrected: 592 self.set_periods(rdates) 593 594 return corrected 595 596 # Construction and serialisation. 597 598 def make_calendar(nodes, method=None): 599 600 """ 601 Return a complete calendar node wrapping the given 'nodes' and employing the 602 given 'method', if indicated. 603 """ 604 605 return ("VCALENDAR", {}, 606 (method and [("METHOD", {}, method)] or []) + 607 [("VERSION", {}, "2.0")] + 608 nodes 609 ) 610 611 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None, 612 attendee_attr=None, period=None): 613 614 """ 615 Return a calendar node defining the free/busy details described in the given 616 'freebusy' list, employing the given 'uid', for the given 'organiser' and 617 optional 'organiser_attr', with the optional 'attendee' providing recipient 618 details together with the optional 'attendee_attr'. 619 620 The result will be constrained to the 'period' if specified. 621 """ 622 623 record = [] 624 rwrite = record.append 625 626 rwrite(("ORGANIZER", organiser_attr or {}, organiser)) 627 628 if attendee: 629 rwrite(("ATTENDEE", attendee_attr or {}, attendee)) 630 631 rwrite(("UID", {}, uid)) 632 633 if freebusy: 634 635 # Get a constrained view if start and end limits are specified. 636 637 if period: 638 periods = freebusy.get_overlapping([period]) 639 else: 640 periods = freebusy 641 642 # Write the limits of the resource. 643 644 if periods: 645 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(periods[0].get_start_point()))) 646 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(periods[-1].get_end_point()))) 647 else: 648 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_start_point()))) 649 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_end_point()))) 650 651 for p in periods: 652 if p.transp == "OPAQUE": 653 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 654 map(format_datetime, [p.get_start_point(), p.get_end_point()]) 655 ))) 656 657 return ("VFREEBUSY", {}, record) 658 659 def parse_object(f, encoding, objtype=None): 660 661 """ 662 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 663 given, only objects of that type will be returned. Otherwise, the root of 664 the content will be returned as a dictionary with a single key indicating 665 the object type. 666 667 Return None if the content was not readable or suitable. 668 """ 669 670 try: 671 try: 672 doctype, attrs, elements = obj = parse(f, encoding=encoding) 673 if objtype and doctype == objtype: 674 return to_dict(obj)[objtype][0] 675 elif not objtype: 676 return to_dict(obj) 677 finally: 678 f.close() 679 680 # NOTE: Handle parse errors properly. 681 682 except (ParseError, ValueError): 683 pass 684 685 return None 686 687 def parse_string(s, encoding, objtype=None): 688 689 """ 690 Parse the iTIP content from 's' having the given 'encoding'. If 'objtype' is 691 given, only objects of that type will be returned. Otherwise, the root of 692 the content will be returned as a dictionary with a single key indicating 693 the object type. 694 695 Return None if the content was not readable or suitable. 696 """ 697 698 return parse_object(StringIO(s), encoding, objtype) 699 700 def to_part(method, fragments, encoding="utf-8", line_length=None): 701 702 """ 703 Write using the given 'method', the given 'fragments' to a MIME 704 text/calendar part. 705 """ 706 707 out = StringIO() 708 try: 709 to_stream(out, make_calendar(fragments, method), encoding, line_length) 710 part = MIMEText(out.getvalue(), "calendar", encoding) 711 part.set_param("method", method) 712 return part 713 714 finally: 715 out.close() 716 717 def to_stream(out, fragment, encoding="utf-8", line_length=None): 718 719 "Write to the 'out' stream the given 'fragment'." 720 721 iterwrite(out, encoding=encoding, line_length=line_length).append(fragment) 722 723 def to_string(fragment, encoding="utf-8", line_length=None): 724 725 "Return a string encoding the given 'fragment'." 726 727 out = StringIO() 728 try: 729 to_stream(out, fragment, encoding, line_length) 730 return out.getvalue() 731 732 finally: 733 out.close() 734 735 def new_object(object_type): 736 737 "Make a new object of the given 'object_type'." 738 739 return Object({object_type : ({}, {})}) 740 741 def make_uid(user): 742 743 "Return a unique identifier for a new object by the given 'user'." 744 745 utcnow = get_timestamp() 746 return "imip-agent-%s-%s" % (utcnow, get_address(user)) 747 748 # Structure access functions. 749 750 def get_items(d, name, all=True): 751 752 """ 753 Get all items from 'd' for the given 'name', returning single items if 754 'all' is specified and set to a false value and if only one value is 755 present for the name. Return None if no items are found for the name or if 756 many items are found but 'all' is set to a false value. 757 """ 758 759 if d.has_key(name): 760 items = [(value or None, attr) for value, attr in d[name]] 761 if all: 762 return items 763 elif len(items) == 1: 764 return items[0] 765 else: 766 return None 767 else: 768 return None 769 770 def get_item(d, name): 771 return get_items(d, name, False) 772 773 def get_value_map(d, name): 774 775 """ 776 Return a dictionary for all items in 'd' having the given 'name'. The 777 dictionary will map values for the name to any attributes or qualifiers 778 that may have been present. 779 """ 780 781 items = get_items(d, name) 782 if items: 783 return dict(items) 784 else: 785 return {} 786 787 def values_from_items(items): 788 return map(lambda x: x[0], items) 789 790 def get_values(d, name, all=True): 791 if d.has_key(name): 792 items = d[name] 793 if not all and len(items) == 1: 794 return items[0][0] 795 else: 796 return values_from_items(items) 797 else: 798 return None 799 800 def get_value(d, name): 801 return get_values(d, name, False) 802 803 def get_date_value_items(d, name, tzid=None): 804 805 """ 806 Obtain items from 'd' having the given 'name', where a single item yields 807 potentially many values. Return a list of tuples of the form (value, 808 attributes) where the attributes have been given for the property in 'd'. 809 """ 810 811 items = get_items(d, name) 812 if items: 813 all_items = [] 814 for item in items: 815 values, attr = item 816 if not attr.has_key("TZID") and tzid: 817 attr["TZID"] = tzid 818 if not isinstance(values, list): 819 values = [values] 820 for value in values: 821 all_items.append((get_datetime(value, attr) or get_period(value, attr), attr)) 822 return all_items 823 else: 824 return None 825 826 def get_date_value_item_periods(d, name, duration, tzid=None): 827 828 """ 829 Obtain items from 'd' having the given 'name', where a single item yields 830 potentially many values. The 'duration' must be provided to define the 831 length of periods having only a start datetime. Return a list of periods 832 corresponding to the property in 'd'. 833 """ 834 835 items = get_date_value_items(d, name, tzid) 836 if not items: 837 return items 838 839 periods = [] 840 841 for value, attr in items: 842 if isinstance(value, tuple): 843 periods.append(RecurringPeriod(value[0], value[1], tzid, name, attr)) 844 else: 845 periods.append(RecurringPeriod(value, value + duration, tzid, name, attr)) 846 847 return periods 848 849 def get_period_values(d, name, tzid=None): 850 851 """ 852 Return period values from 'd' for the given property 'name', using 'tzid' 853 where specified to indicate the time zone. 854 """ 855 856 values = [] 857 for value, attr in get_items(d, name) or []: 858 if not attr.has_key("TZID") and tzid: 859 attr["TZID"] = tzid 860 start, end = get_period(value, attr) 861 values.append(Period(start, end, tzid=tzid)) 862 return values 863 864 def get_utc_datetime(d, name, date_tzid=None): 865 866 """ 867 Return the value provided by 'd' for 'name' as a datetime in the UTC zone 868 or as a date, converting any date to a datetime if 'date_tzid' is specified. 869 If no datetime or date is available, None is returned. 870 """ 871 872 t = get_datetime_item(d, name) 873 if not t: 874 return None 875 else: 876 dt, attr = t 877 return dt is not None and to_utc_datetime(dt, date_tzid) or None 878 879 def get_datetime_item(d, name): 880 881 """ 882 Return the value provided by 'd' for 'name' as a datetime or as a date, 883 together with the attributes describing it. Return None if no value exists 884 for 'name' in 'd'. 885 """ 886 887 t = get_item(d, name) 888 if not t: 889 return None 890 else: 891 value, attr = t 892 dt = get_datetime(value, attr) 893 tzid = get_datetime_tzid(dt) 894 if tzid: 895 attr["TZID"] = tzid 896 return dt, attr 897 898 # Conversion functions. 899 900 def get_address_parts(values): 901 902 "Return name and address tuples for each of the given 'values'." 903 904 l = [] 905 for name, address in values and email.utils.getaddresses(values) or []: 906 if is_mailto_uri(name): 907 name = name[7:] # strip "mailto:" 908 l.append((name, address)) 909 return l 910 911 def get_addresses(values): 912 913 """ 914 Return only addresses from the given 'values' which may be of the form 915 "Common Name <recipient@domain>", with the latter part being the address 916 itself. 917 """ 918 919 return [address for name, address in get_address_parts(values)] 920 921 def get_address(value): 922 923 "Return an e-mail address from the given 'value'." 924 925 if not value: return None 926 return get_addresses([value])[0] 927 928 def get_verbose_address(value, attr=None): 929 930 """ 931 Return a verbose e-mail address featuring any name from the given 'value' 932 and any accompanying 'attr' dictionary. 933 """ 934 935 l = get_address_parts([value]) 936 if not l: 937 return value 938 name, address = l[0] 939 if not name: 940 name = attr and attr.get("CN") 941 if name and address: 942 return "%s <%s>" % (name, address) 943 else: 944 return address 945 946 def is_mailto_uri(value): 947 return value.lower().startswith("mailto:") 948 949 def get_uri(value): 950 951 "Return a URI for the given 'value'." 952 953 if not value: return None 954 return is_mailto_uri(value) and ("mailto:%s" % value[7:]) or \ 955 ":" in value and value or \ 956 "mailto:%s" % get_address(value) 957 958 def uri_parts(values): 959 960 "Return any common name plus the URI for each of the given 'values'." 961 962 return [(name, get_uri(address)) for name, address in get_address_parts(values)] 963 964 uri_value = get_uri 965 966 def uri_values(values): 967 return map(get_uri, values) 968 969 def uri_dict(d): 970 return dict([(get_uri(key), value) for key, value in d.items()]) 971 972 def uri_item(item): 973 return get_uri(item[0]), item[1] 974 975 def uri_items(items): 976 return [(get_uri(value), attr) for value, attr in items] 977 978 # Operations on structure data. 979 980 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, ignore_dtstamp): 981 982 """ 983 Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and 984 'new_dtstamp', and the 'ignore_dtstamp' indication, whether the object 985 providing the new information is really newer than the object providing the 986 old information. 987 """ 988 989 have_sequence = old_sequence is not None and new_sequence is not None 990 is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence) 991 992 have_dtstamp = old_dtstamp and new_dtstamp 993 is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp 994 995 is_old_sequence = have_sequence and ( 996 int(new_sequence) < int(old_sequence) or 997 is_same_sequence and is_old_dtstamp 998 ) 999 1000 return is_same_sequence and ignore_dtstamp or not is_old_sequence 1001 1002 def check_delegation(attendee_map, attendee, attendee_attr): 1003 1004 """ 1005 Using the 'attendee_map', check the attributes for the given 'attendee' 1006 provided as 'attendee_attr', following the delegation chain back to the 1007 delegators and forward again to yield the delegate identities in each 1008 case. Pictorially... 1009 1010 attendee -> DELEGATED-FROM -> delegator 1011 ? <- DELEGATED-TO <--- 1012 1013 Return whether 'attendee' was identified as a delegate by providing the 1014 identity of any delegators referencing the attendee. 1015 """ 1016 1017 delegators = [] 1018 1019 # The recipient should have a reference to the delegator. 1020 1021 delegated_from = attendee_attr and attendee_attr.get("DELEGATED-FROM") 1022 if delegated_from: 1023 1024 # Examine all delegators. 1025 1026 for delegator in delegated_from: 1027 delegator_attr = attendee_map.get(delegator) 1028 1029 # The delegator should have a reference to the recipient. 1030 1031 delegated_to = delegator_attr and delegator_attr.get("DELEGATED-TO") 1032 if delegated_to and attendee in delegated_to: 1033 delegators.append(delegator) 1034 1035 return delegators 1036 1037 def get_periods(obj, tzid, end=None, inclusive=False): 1038 1039 """ 1040 Return periods for the given object 'obj', employing the given 'tzid' where 1041 no time zone information is available (for whole day events, for example), 1042 confining materialised periods to before the given 'end' datetime. 1043 1044 If 'end' is omitted, only explicit recurrences and recurrences from 1045 explicitly-terminated rules will be returned. 1046 1047 If 'inclusive' is set to a true value, any period occurring at the 'end' 1048 will be included. 1049 """ 1050 1051 rrule = obj.get_value("RRULE") 1052 parameters = rrule and get_parameters(rrule) 1053 1054 # Use localised datetimes. 1055 1056 main_period = obj.get_main_period(tzid) 1057 1058 dtstart = main_period.get_start() 1059 dtstart_attr = main_period.get_start_attr() 1060 1061 # Attempt to get time zone details from the object, using the supplied zone 1062 # only as a fallback. 1063 1064 obj_tzid = obj.get_tzid() 1065 1066 if not rrule: 1067 periods = [main_period] 1068 1069 elif end or parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT"): 1070 1071 # Recurrence rules create multiple instances to be checked. 1072 # Conflicts may only be assessed within a period defined by policy 1073 # for the agent, with instances outside that period being considered 1074 # unchecked. 1075 1076 selector = get_rule(dtstart, rrule) 1077 periods = [] 1078 1079 until = parameters.get("UNTIL") 1080 if until: 1081 until_dt = to_timezone(get_datetime(until, dtstart_attr), obj_tzid) 1082 end = end and min(until_dt, end) or until_dt 1083 inclusive = True 1084 1085 for recurrence_start in selector.materialise(dtstart, end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive): 1086 create = len(recurrence_start) == 3 and date or datetime 1087 recurrence_start = to_timezone(create(*recurrence_start), obj_tzid) 1088 recurrence_end = recurrence_start + main_period.get_duration() 1089 periods.append(RecurringPeriod(recurrence_start, recurrence_end, tzid, "RRULE", dtstart_attr)) 1090 1091 else: 1092 periods = [] 1093 1094 # Add recurrence dates. 1095 1096 rdates = obj.get_date_value_item_periods("RDATE", tzid) 1097 if rdates: 1098 periods += rdates 1099 1100 # Return a sorted list of the periods. 1101 1102 periods.sort() 1103 1104 # Exclude exception dates. 1105 1106 exdates = obj.get_date_value_item_periods("EXDATE", tzid) 1107 1108 if exdates: 1109 for period in exdates: 1110 i = bisect_left(periods, period) 1111 while i < len(periods) and periods[i] == period: 1112 del periods[i] 1113 1114 return periods 1115 1116 def get_sender_identities(mapping): 1117 1118 """ 1119 Return a mapping from actual senders to the identities for which they 1120 have provided data, extracting this information from the given 1121 'mapping'. 1122 """ 1123 1124 senders = {} 1125 1126 for value, attr in mapping.items(): 1127 sent_by = attr.get("SENT-BY") 1128 if sent_by: 1129 sender = get_uri(sent_by) 1130 else: 1131 sender = value 1132 1133 if not senders.has_key(sender): 1134 senders[sender] = [] 1135 1136 senders[sender].append(value) 1137 1138 return senders 1139 1140 def get_window_end(tzid, days=100): 1141 1142 """ 1143 Return a datetime in the time zone indicated by 'tzid' marking the end of a 1144 window of the given number of 'days'. 1145 """ 1146 1147 return to_timezone(datetime.now(), tzid) + timedelta(days) 1148 1149 # vim: tabstop=4 expandtab shiftwidth=4