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