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