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 update_dtstamp(self): 428 429 "Update the DTSTAMP in the object." 430 431 dtstamp = self.get_utc_datetime("DTSTAMP") 432 utcnow = get_time() 433 dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow) 434 self["DTSTAMP"] = [(dtstamp, {})] 435 return dtstamp 436 437 def update_sequence(self, increment=False): 438 439 "Set or update the SEQUENCE in the object." 440 441 sequence = self.get_value("SEQUENCE") or "0" 442 self["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] 443 return sequence 444 445 def update_senders(self, user=None): 446 447 "Remove SENT-BY attributes from properties." 448 449 for identity, attr in self.get_items("ATTENDEE") or []: 450 if attr.has_key("SENT-BY") and (not user or get_uri(identity) != user): 451 del attr["SENT-BY"] 452 453 def update_exceptions(self, excluded): 454 455 """ 456 Update the exceptions to any rule by applying the list of 'excluded' 457 periods. 458 """ 459 460 to_exclude = set(excluded).difference(self.get_date_values("EXDATE") or []) 461 if not to_exclude: 462 return False 463 464 if not self.has_key("EXDATE"): 465 self["EXDATE"] = [] 466 467 for p in to_exclude: 468 self["EXDATE"].append(get_period_item(p.get_start(), p.get_end())) 469 470 return True 471 472 def correct_object(self, tzid, permitted_values): 473 474 "Correct the object's period details." 475 476 corrected = set() 477 rdates = [] 478 479 for period in self.get_periods(tzid): 480 start = period.get_start() 481 end = period.get_end() 482 start_errors = check_permitted_values(start, permitted_values) 483 end_errors = check_permitted_values(end, permitted_values) 484 485 if not (start_errors or end_errors): 486 if period.origin == "RDATE": 487 rdates.append(period) 488 continue 489 490 if start_errors: 491 start = correct_datetime(start, permitted_values) 492 if end_errors: 493 end = correct_datetime(end, permitted_values) 494 period = RecurringPeriod(start, end, period.tzid, period.origin, period.get_start_attr(), period.get_end_attr()) 495 496 if period.origin == "DTSTART": 497 self.set_period(period) 498 corrected.add("DTSTART") 499 elif period.origin == "RDATE": 500 rdates.append(period) 501 corrected.add("RDATE") 502 503 if "RDATE" in corrected: 504 self.set_periods(rdates) 505 506 return corrected 507 508 # Construction and serialisation. 509 510 def make_calendar(nodes, method=None): 511 512 """ 513 Return a complete calendar node wrapping the given 'nodes' and employing the 514 given 'method', if indicated. 515 """ 516 517 return ("VCALENDAR", {}, 518 (method and [("METHOD", {}, method)] or []) + 519 [("VERSION", {}, "2.0")] + 520 nodes 521 ) 522 523 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None, 524 attendee_attr=None, period=None): 525 526 """ 527 Return a calendar node defining the free/busy details described in the given 528 'freebusy' list, employing the given 'uid', for the given 'organiser' and 529 optional 'organiser_attr', with the optional 'attendee' providing recipient 530 details together with the optional 'attendee_attr'. 531 532 The result will be constrained to the 'period' if specified. 533 """ 534 535 record = [] 536 rwrite = record.append 537 538 rwrite(("ORGANIZER", organiser_attr or {}, organiser)) 539 540 if attendee: 541 rwrite(("ATTENDEE", attendee_attr or {}, attendee)) 542 543 rwrite(("UID", {}, uid)) 544 545 if freebusy: 546 547 # Get a constrained view if start and end limits are specified. 548 549 if period: 550 periods = period_overlaps(freebusy, period, True) 551 else: 552 periods = freebusy 553 554 # Write the limits of the resource. 555 556 if periods: 557 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(periods[0].get_start_point()))) 558 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(periods[-1].get_end_point()))) 559 else: 560 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_start_point()))) 561 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_end_point()))) 562 563 for p in periods: 564 if p.transp == "OPAQUE": 565 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 566 map(format_datetime, [p.get_start_point(), p.get_end_point()]) 567 ))) 568 569 return ("VFREEBUSY", {}, record) 570 571 def parse_object(f, encoding, objtype=None): 572 573 """ 574 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 575 given, only objects of that type will be returned. Otherwise, the root of 576 the content will be returned as a dictionary with a single key indicating 577 the object type. 578 579 Return None if the content was not readable or suitable. 580 """ 581 582 try: 583 try: 584 doctype, attrs, elements = obj = parse(f, encoding=encoding) 585 if objtype and doctype == objtype: 586 return to_dict(obj)[objtype][0] 587 elif not objtype: 588 return to_dict(obj) 589 finally: 590 f.close() 591 592 # NOTE: Handle parse errors properly. 593 594 except (ParseError, ValueError): 595 pass 596 597 return None 598 599 def to_part(method, calendar): 600 601 """ 602 Write using the given 'method', the 'calendar' details to a MIME 603 text/calendar part. 604 """ 605 606 encoding = "utf-8" 607 out = StringIO() 608 try: 609 to_stream(out, make_calendar(calendar, method), encoding) 610 part = MIMEText(out.getvalue(), "calendar", encoding) 611 part.set_param("method", method) 612 return part 613 614 finally: 615 out.close() 616 617 def to_stream(out, fragment, encoding="utf-8"): 618 iterwrite(out, encoding=encoding).append(fragment) 619 620 # Structure access functions. 621 622 def get_items(d, name, all=True): 623 624 """ 625 Get all items from 'd' for the given 'name', returning single items if 626 'all' is specified and set to a false value and if only one value is 627 present for the name. Return None if no items are found for the name or if 628 many items are found but 'all' is set to a false value. 629 """ 630 631 if d.has_key(name): 632 items = [(value or None, attr) for value, attr in d[name]] 633 if all: 634 return items 635 elif len(items) == 1: 636 return items[0] 637 else: 638 return None 639 else: 640 return None 641 642 def get_item(d, name): 643 return get_items(d, name, False) 644 645 def get_value_map(d, name): 646 647 """ 648 Return a dictionary for all items in 'd' having the given 'name'. The 649 dictionary will map values for the name to any attributes or qualifiers 650 that may have been present. 651 """ 652 653 items = get_items(d, name) 654 if items: 655 return dict(items) 656 else: 657 return {} 658 659 def values_from_items(items): 660 return map(lambda x: x[0], items) 661 662 def get_values(d, name, all=True): 663 if d.has_key(name): 664 items = d[name] 665 if not all and len(items) == 1: 666 return items[0][0] 667 else: 668 return values_from_items(items) 669 else: 670 return None 671 672 def get_value(d, name): 673 return get_values(d, name, False) 674 675 def get_date_value_items(d, name, tzid=None): 676 677 """ 678 Obtain items from 'd' having the given 'name', where a single item yields 679 potentially many values. Return a list of tuples of the form (value, 680 attributes) where the attributes have been given for the property in 'd'. 681 """ 682 683 items = get_items(d, name) 684 if items: 685 all_items = [] 686 for item in items: 687 values, attr = item 688 if not attr.has_key("TZID") and tzid: 689 attr["TZID"] = tzid 690 if not isinstance(values, list): 691 values = [values] 692 for value in values: 693 all_items.append((get_datetime(value, attr) or get_period(value, attr), attr)) 694 return all_items 695 else: 696 return None 697 698 def get_period_values(d, name, tzid=None): 699 700 """ 701 Return period values from 'd' for the given property 'name', using 'tzid' 702 where specified to indicate the time zone. 703 """ 704 705 values = [] 706 for value, attr in get_items(d, name) or []: 707 if not attr.has_key("TZID") and tzid: 708 attr["TZID"] = tzid 709 start, end = get_period(value, attr) 710 values.append(Period(start, end, tzid=tzid)) 711 return values 712 713 def get_utc_datetime(d, name, date_tzid=None): 714 715 """ 716 Return the value provided by 'd' for 'name' as a datetime in the UTC zone 717 or as a date, converting any date to a datetime if 'date_tzid' is specified. 718 If no datetime or date is available, None is returned. 719 """ 720 721 t = get_datetime_item(d, name) 722 if not t: 723 return None 724 else: 725 dt, attr = t 726 return dt is not None and to_utc_datetime(dt, date_tzid) or None 727 728 def get_datetime_item(d, name): 729 730 """ 731 Return the value provided by 'd' for 'name' as a datetime or as a date, 732 together with the attributes describing it. Return None if no value exists 733 for 'name' in 'd'. 734 """ 735 736 t = get_item(d, name) 737 if not t: 738 return None 739 else: 740 value, attr = t 741 dt = get_datetime(value, attr) 742 tzid = get_datetime_tzid(dt) 743 if tzid: 744 attr["TZID"] = tzid 745 return dt, attr 746 747 # Conversion functions. 748 749 def get_address_parts(values): 750 751 "Return name and address tuples for each of the given 'values'." 752 753 l = [] 754 for name, address in values and email.utils.getaddresses(values) or []: 755 if is_mailto_uri(name): 756 name = name[7:] # strip "mailto:" 757 l.append((name, address)) 758 return l 759 760 def get_addresses(values): 761 762 """ 763 Return only addresses from the given 'values' which may be of the form 764 "Common Name <recipient@domain>", with the latter part being the address 765 itself. 766 """ 767 768 return [address for name, address in get_address_parts(values)] 769 770 def get_address(value): 771 772 "Return an e-mail address from the given 'value'." 773 774 if not value: return None 775 return get_addresses([value])[0] 776 777 def get_verbose_address(value, attr=None): 778 779 """ 780 Return a verbose e-mail address featuring any name from the given 'value' 781 and any accompanying 'attr' dictionary. 782 """ 783 784 l = get_address_parts([value]) 785 if not l: 786 return value 787 name, address = l[0] 788 if not name: 789 name = attr and attr.get("CN") 790 if name and address: 791 return "%s <%s>" % (name, address) 792 else: 793 return address 794 795 def is_mailto_uri(value): 796 return value.lower().startswith("mailto:") 797 798 def get_uri(value): 799 800 "Return a URI for the given 'value'." 801 802 if not value: return None 803 return is_mailto_uri(value) and ("mailto:%s" % value[7:]) or \ 804 ":" in value and value or \ 805 "mailto:%s" % get_address(value) 806 807 def uri_parts(values): 808 809 "Return any common name plus the URI for each of the given 'values'." 810 811 return [(name, get_uri(address)) for name, address in get_address_parts(values)] 812 813 uri_value = get_uri 814 815 def uri_values(values): 816 return map(get_uri, values) 817 818 def uri_dict(d): 819 return dict([(get_uri(key), value) for key, value in d.items()]) 820 821 def uri_item(item): 822 return get_uri(item[0]), item[1] 823 824 def uri_items(items): 825 return [(get_uri(value), attr) for value, attr in items] 826 827 # Operations on structure data. 828 829 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, ignore_dtstamp): 830 831 """ 832 Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and 833 'new_dtstamp', and the 'ignore_dtstamp' indication, whether the object 834 providing the new information is really newer than the object providing the 835 old information. 836 """ 837 838 have_sequence = old_sequence is not None and new_sequence is not None 839 is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence) 840 841 have_dtstamp = old_dtstamp and new_dtstamp 842 is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp 843 844 is_old_sequence = have_sequence and ( 845 int(new_sequence) < int(old_sequence) or 846 is_same_sequence and is_old_dtstamp 847 ) 848 849 return is_same_sequence and ignore_dtstamp or not is_old_sequence 850 851 def get_periods(obj, tzid, end=None, inclusive=False): 852 853 """ 854 Return periods for the given object 'obj', employing the given 'tzid' where 855 no time zone information is available (for whole day events, for example), 856 confining materialised periods to before the given 'end' datetime. 857 858 If 'end' is omitted, only explicit recurrences and recurrences from 859 explicitly-terminated rules will be returned. 860 861 If 'inclusive' is set to a true value, any period occurring at the 'end' 862 will be included. 863 """ 864 865 rrule = obj.get_value("RRULE") 866 parameters = rrule and get_parameters(rrule) 867 868 # Use localised datetimes. 869 870 main_period = obj.get_main_period(tzid) 871 872 dtstart = main_period.get_start() 873 dtstart_attr = main_period.get_start_attr() 874 dtend = main_period.get_end() 875 dtend_attr = main_period.get_end_attr() 876 877 duration = dtend - dtstart 878 879 # Attempt to get time zone details from the object, using the supplied zone 880 # only as a fallback. 881 882 obj_tzid = obj.get_tzid() 883 884 if not rrule: 885 periods = [main_period] 886 887 elif end or parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT"): 888 889 # Recurrence rules create multiple instances to be checked. 890 # Conflicts may only be assessed within a period defined by policy 891 # for the agent, with instances outside that period being considered 892 # unchecked. 893 894 selector = get_rule(dtstart, rrule) 895 periods = [] 896 897 until = parameters.get("UNTIL") 898 if until: 899 until_dt = to_timezone(get_datetime(until, dtstart_attr), obj_tzid) 900 end = end and min(until_dt, end) or until_dt 901 inclusive = True 902 903 for recurrence_start in selector.materialise(dtstart, end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive): 904 create = len(recurrence_start) == 3 and date or datetime 905 recurrence_start = to_timezone(create(*recurrence_start), obj_tzid) 906 recurrence_end = recurrence_start + duration 907 periods.append(RecurringPeriod(recurrence_start, recurrence_end, tzid, "RRULE", dtstart_attr)) 908 909 else: 910 periods = [] 911 912 # Add recurrence dates. 913 914 rdates = obj.get_date_value_items("RDATE", tzid) 915 916 if rdates: 917 for rdate, rdate_attr in rdates: 918 if isinstance(rdate, tuple): 919 periods.append(RecurringPeriod(rdate[0], rdate[1], tzid, "RDATE", rdate_attr)) 920 else: 921 periods.append(RecurringPeriod(rdate, rdate + duration, tzid, "RDATE", rdate_attr)) 922 923 # Return a sorted list of the periods. 924 925 periods.sort() 926 927 # Exclude exception dates. 928 929 exdates = obj.get_date_value_items("EXDATE", tzid) 930 931 if exdates: 932 for exdate, exdate_attr in exdates: 933 if isinstance(exdate, tuple): 934 period = RecurringPeriod(exdate[0], exdate[1], tzid, "EXDATE", exdate_attr) 935 else: 936 period = RecurringPeriod(exdate, exdate + duration, tzid, "EXDATE", exdate_attr) 937 i = bisect_left(periods, period) 938 while i < len(periods) and periods[i] == period: 939 del periods[i] 940 941 return periods 942 943 def get_sender_identities(mapping): 944 945 """ 946 Return a mapping from actual senders to the identities for which they 947 have provided data, extracting this information from the given 948 'mapping'. 949 """ 950 951 senders = {} 952 953 for value, attr in mapping.items(): 954 sent_by = attr.get("SENT-BY") 955 if sent_by: 956 sender = get_uri(sent_by) 957 else: 958 sender = value 959 960 if not senders.has_key(sender): 961 senders[sender] = [] 962 963 senders[sender].append(value) 964 965 return senders 966 967 def get_window_end(tzid, days=100): 968 969 """ 970 Return a datetime in the time zone indicated by 'tzid' marking the end of a 971 window of the given number of 'days'. 972 """ 973 974 return to_timezone(datetime.now(), tzid) + timedelta(days) 975 976 # vim: tabstop=4 expandtab shiftwidth=4