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