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