1 #!/usr/bin/env python 2 3 """ 4 A text interface to received and new events. 5 6 Copyright (C) 2017, 2018 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 email import message_from_file 23 from imiptools import get_handlers, parse_args 24 from imiptools.config import settings 25 from imiptools.content import get_objects_from_itip, handle_calendar_data, \ 26 handle_calendar_object, have_itip_part, \ 27 is_cancel_itip, parse_itip_part 28 from imiptools.data import get_address, get_periods_using_selector, \ 29 get_main_period, get_recurrence_periods, \ 30 get_value, parse_object 31 from imiptools.dates import get_datetime_item, get_time, to_timezone 32 from imiptools.editing import EditingClient, PeriodError, \ 33 form_periods_from_periods 34 from imiptools.handlers import person, person_outgoing 35 from imiptools.mail import Messenger 36 from imiptools.stores import get_journal, get_store 37 from imiptools.utils import decode_part, message_as_string 38 import vRecurrence 39 import sys, os 40 41 # User interface definitions. 42 43 ATTENDANCE_COMMANDS = ("A", "attend", "attendance") 44 ATTENDEE_COMMANDS = ("a", "attendee") 45 CANCEL_COMMANDS = ("c", "cancel") 46 CHANGE_COMMANDS = ("C", "changes") 47 CLASSIFICATION_COMMANDS = ("c", "class", "classification") 48 FINISH_COMMANDS = ("f", "finish") 49 HELP_COMMANDS = ("h", "help", "?") 50 LIST_COMMANDS = ("l", "list", "show") 51 OPERATION_COMMANDS = ("o", "ops", "operations") 52 PERIOD_COMMANDS = ("p", "period") 53 QUIT_COMMANDS = ("q", "quit", "exit") 54 RECURRENCEID_COMMANDS = ("RECURRENCE-ID", "RID") 55 RESET_COMMANDS = ("r", "reload", "reset", "restart") 56 RULE_COMMANDS = ("rr", "rule", "rrule") 57 SUGGESTED_ATTENDEE_COMMANDS = ("as", "attendee-suggested", "suggested-attendee") 58 SUGGESTED_PERIOD_COMMANDS = ("ps", "period-suggested", "suggested-period") 59 SUMMARY_COMMANDS = ("s", "summary") 60 UID_COMMANDS = ("UID",) 61 UNCANCEL_COMMANDS = ("u", "uncancel", "restore") 62 63 CANCEL_COMMANDS = ("R", "remove", "cancel") 64 PUBLISH_COMMANDS = ("P", "publish") 65 SEND_COMMANDS = ("S", "send") 66 UPDATE_COMMANDS = ("U", "update") 67 68 EDIT_COMMANDS = ("e", "edit") 69 REMOVE_COMMANDS = ("r", "remove") 70 71 ACCEPTED_VALUES = ("a", "accept", "accepted", "attend") 72 DECLINED_VALUES = ("d", "decline", "declined") 73 TENTATIVE_VALUES = ("t", "tentative") 74 75 def commandlist(l, option=None): 76 77 "Show 'l' as a command list string employing any given 'option'." 78 79 if option: 80 l2 = [] 81 for s in l: 82 l2.append("%s %s" % (s, option)) 83 return "\n".join(l2) 84 return ", ".join(l) 85 86 # User interface functions. 87 88 echo = False 89 90 def read_input(label): 91 92 """ 93 Read input, prompting using 'label', stripping leading and trailing 94 whitespace, echoing the input if the global 'echo' variable is set. 95 """ 96 97 s = raw_input(label).strip() 98 if echo: 99 print s 100 return s 101 102 def input_with_default(label, default): 103 104 """ 105 Read input, prompting using 'label', parameterising the label with the given 106 'default' and returning the default if no input is given. 107 """ 108 109 return read_input(label % default) or default 110 111 def to_int_or_none(value): 112 113 "Return 'value' as an integer or None for other inputs." 114 115 try: 116 return int(value) 117 except (TypeError, ValueError): 118 return None 119 120 def format_value_ranges(ranges, null_value="-"): 121 122 "Format 'ranges' as a single descriptive string." 123 124 l = [] 125 126 for value_range in ranges: 127 if isinstance(value_range, tuple): 128 start, end = value_range 129 l.append("%s...%s" % (start, end)) 130 elif isinstance(value_range, list): 131 l.append(", ".join(value_range)) 132 elif isinstance(value_range, dict): 133 l.append(", ".join(value_range.keys())) 134 else: 135 l.append(value_range or null_value) 136 137 return ", ".join(l) 138 139 def print_title(text): 140 141 "Print 'text' with simple, fixed-width styling as a title." 142 143 print text 144 print len(text) * "-" 145 146 def print_table(rows, separator_index=0): 147 148 """ 149 Print 'rows' as a simple, fixed-width table. If 'separator_index' is set to 150 a row index present in the rows, a table separator will be produced below 151 that row's data. Otherwise, a separator will appear below the first row. 152 """ 153 154 widths = [] 155 for row in rows: 156 for i, col in enumerate(row): 157 if i >= len(widths): 158 widths.append(len(col)) 159 else: 160 widths[i] = max(widths[i], len(col)) 161 162 for i, row in enumerate(rows): 163 for col, width in zip(row, widths): 164 print "%s%s" % (col, " " * (width - len(col))), 165 print 166 if i == separator_index: 167 for width in widths: 168 print "-" * width, 169 print 170 171 def write(s, filename): 172 173 "Write 's' to a file having the given 'filename'." 174 175 f = filename and open(filename, "w") or None 176 try: 177 print >>(f or sys.stdout), s 178 finally: 179 if f: 180 f.close() 181 182 # Interpret an input file containing a calendar resource. 183 184 def get_itip_from_message(filename): 185 186 "Return iTIP details provided by 'filename'." 187 188 f = open(filename) 189 try: 190 msg = message_from_file(f) 191 finally: 192 f.close() 193 194 all_itip = [] 195 196 for part in msg.walk(): 197 if have_itip_part(part): 198 all_itip.append(parse_itip_part(part)) 199 200 return all_itip 201 202 def get_itip_from_data(filename, charset): 203 204 "Return objects provided by 'filename'." 205 206 f = open(filename) 207 try: 208 itip = parse_object(f, charset, "VCALENDAR") 209 finally: 210 f.close() 211 212 return [itip] 213 214 # Object and request display. 215 216 def show_objects(objects, user, store): 217 218 """ 219 Show details of 'objects', accessed by the given 'user' in the given 220 'store'. 221 """ 222 223 print 224 print_title("Objects") 225 print 226 227 for index, obj in enumerate(objects): 228 recurrenceid = obj.get_recurrenceid() 229 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 230 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 231 232 def show_requests(user, store): 233 234 "Show requests available to the given 'user' in the given 'store'." 235 236 requests = store.get_requests(user) 237 238 print 239 print_title("Requests") 240 print 241 242 if not requests: 243 print "No requests are pending." 244 return 245 246 for index, (uid, recurrenceid) in enumerate(requests): 247 obj = store.get_event(user, uid, recurrenceid) 248 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 249 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 250 251 # Object details display. 252 253 def show_attendee(attendee_item, index): 254 255 "Show the 'attendee_item' (value and attributes) at 'index'." 256 257 attendee, attr = attendee_item 258 partstat = attr.get("PARTSTAT") 259 print "(%d) %s%s" % (index, attendee, partstat and " (%s)" % partstat or "") 260 261 def show_attendees_raw(attendee_map): 262 263 "Show the 'attendee_map' in a simple raw form." 264 265 for attendee, attr in attendee_map.items(): 266 print attendee 267 268 def show_periods(periods, errors=None): 269 270 "Show 'periods' with any indicated 'errors'." 271 272 main = get_main_period(periods) 273 if main: 274 show_period(main, 0, errors) 275 276 recurrences = get_recurrence_periods(periods) 277 if recurrences: 278 print 279 print_title("Recurrences") 280 for index, p in enumerate(recurrences): 281 show_period(p, index + 1, errors) 282 283 def show_period(p, index, errors=None): 284 285 "Show period 'p' at 'index' with any indicated 'errors'." 286 287 errors = errors and errors.get(index) 288 if p.cancelled: 289 label = "Cancelled" 290 elif p.replacement: 291 label = "Replaced" 292 elif p.new_replacement: 293 label = "To replace" 294 elif p.recurrenceid: 295 label = "Retained" 296 else: 297 label = "New" 298 299 error_label = errors and " (errors: %s)" % ", ".join(errors) or "" 300 print "(%d) %s%s:" % (index, label, error_label), p.get_start(), p.get_end(), p.origin 301 302 def show_periods_raw(periods): 303 304 "Show 'periods' in a simple raw form." 305 306 periods = periods[:] 307 periods.sort() 308 map(show_period_raw, periods) 309 310 def show_period_raw(p): 311 312 "Show period 'p' in a simple raw form." 313 314 print p.get_start(), p.get_end(), p.origin 315 316 def show_rule(selectors): 317 318 "Show recurrence rule specification 'selectors'." 319 320 # Collect limit, selection and frequency details. 321 322 for i, selector in enumerate(selectors): 323 prefix = "(%d) " % i 324 show_rule_selector(selector, prefix) 325 326 print 327 print vRecurrence.to_string(selectors) 328 329 def show_rule_selector(selector, prefix=""): 330 331 "Show the rule 'selector', employing any given 'prefix' for formatting." 332 333 # COUNT 334 335 if isinstance(selector, vRecurrence.LimitSelector): 336 print "%sAt most %d occurrences" % (prefix, selector.args["values"][0]) 337 338 # BYSETPOS 339 340 elif isinstance(selector, vRecurrence.PositionSelector): 341 for value in selector.get_positions(): 342 print "%sSelect occurrence #%d" % (prefix, value) 343 prefix = len(prefix) * " " 344 345 # BYWEEKDAY 346 347 elif isinstance(selector, vRecurrence.WeekDayFilter): 348 for value, index in selector.get_values(): 349 print "%sSelect occurrence #%d (from %s) of weekday %s" % ( 350 prefix, abs(index), index >= 0 and "start" or "end", 351 get_weekday(value)) 352 prefix = len(prefix) * " " 353 354 # BY... 355 356 elif isinstance(selector, vRecurrence.Enum): 357 for value in selector.get_values(): 358 print "%sSelect %s %r" % (prefix, get_resolution(selector.level), 359 value) 360 prefix = len(prefix) * " " 361 362 # YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY 363 364 elif isinstance(selector, vRecurrence.Pattern): 365 print "%sEach %s with interval %d" % (prefix, 366 get_resolution(selector.level), selector.args.get("interval", 1)) 367 368 def get_resolution(level): 369 370 "Return a textual description of the given resolution 'level'." 371 372 levels = ["year", "month", "week", "day in year", "day in month", "day", "hour", "minute", "second"] 373 return levels[level] 374 375 def get_weekday(weekday): 376 377 "Return the name of the given 1-based 'weekday' number." 378 379 weekdays = [None, "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] 380 return weekdays[weekday] 381 382 def show_attendee_changes(new, modified, unmodified, removed): 383 384 "Show 'new', 'modified', 'unmodified' and 'removed' periods." 385 386 print 387 print_title("Changes to attendees") 388 print 389 print "New:" 390 show_attendees_raw(new) 391 print 392 print "Modified:" 393 show_attendees_raw(modified) 394 print 395 print "Unmodified:" 396 show_attendees_raw(unmodified) 397 print 398 print "Removed:" 399 show_attendees_raw(removed) 400 401 def show_period_classification(new, replaced, retained, cancelled, obsolete): 402 403 "Show 'new', 'replaced', 'retained', 'cancelled' and 'obsolete' periods." 404 405 print 406 print_title("Period classification") 407 print 408 print "New:" 409 show_periods_raw(new) 410 print 411 print "Replaced:" 412 show_periods_raw(replaced) 413 print 414 print "Retained:" 415 show_periods_raw(retained) 416 print 417 print "Cancelled:" 418 show_periods_raw(cancelled) 419 print 420 print "Obsolete:" 421 show_periods_raw(obsolete) 422 423 def show_changes(modified, unmodified, removed): 424 425 "Show 'modified', 'unmodified' and 'removed' periods." 426 427 print 428 print_title("Changes to periods") 429 print 430 print "Modified:" 431 show_periods_raw(modified) 432 print 433 print "Unmodified:" 434 show_periods_raw(unmodified) 435 print 436 print "Removed:" 437 show_periods_raw(removed) 438 439 def show_attendee_operations(to_invite, to_cancel, to_modify): 440 441 "Show attendees 'to_invite', 'to_cancel' and 'to_modify'." 442 443 print 444 print_title("Attendee update operations") 445 print 446 print "To invite:" 447 show_attendees_raw(to_invite) 448 print 449 print "To cancel:" 450 show_attendees_raw(to_cancel) 451 print 452 print "To modify:" 453 show_attendees_raw(to_modify) 454 455 def show_period_operations(to_unschedule, to_reschedule, to_add, to_exclude, to_set, 456 all_unscheduled, all_rescheduled): 457 458 """ 459 Show operations for periods 'to_unschedule', 'to_reschedule', 'to_add', 460 'to_exclude' and 'to_set' (for updating other calendar participants), and 461 for periods 'all_unscheduled' and 'all_rescheduled' (for publishing event 462 state). 463 """ 464 465 print 466 print_title("Period update and publishing operations") 467 print 468 print "Unschedule:" 469 show_periods_raw(to_unschedule) 470 print 471 print "Reschedule:" 472 show_periods_raw(to_reschedule) 473 print 474 print "Added:" 475 show_periods_raw(to_add) 476 print 477 print "Excluded:" 478 show_periods_raw(to_exclude) 479 print 480 print "Set in object:" 481 show_periods_raw(to_set) 482 print 483 print "All unscheduled:" 484 show_periods_raw(all_unscheduled) 485 print 486 print "All rescheduled:" 487 show_periods_raw(all_rescheduled) 488 489 class TextClient(EditingClient): 490 491 "Simple client with textual output." 492 493 def new_object(self): 494 495 "Create a new object with the current time." 496 497 utcnow = get_time() 498 now = to_timezone(utcnow, self.get_tzid()) 499 obj = EditingClient.new_object(self, "VEVENT") 500 obj.set_value("SUMMARY", "New event") 501 obj["DTSTART"] = [get_datetime_item(now)] 502 obj["DTEND"] = [get_datetime_item(now)] 503 return obj 504 505 def handle_outgoing_object(self): 506 507 "Handle the current object using the outgoing handlers." 508 509 unscheduled_objects, rescheduled_objects, added_objects = \ 510 self.get_publish_objects() 511 512 handlers = get_handlers(self, person_outgoing.handlers, 513 [get_address(self.user)]) 514 515 # Handle the parent object plus any altered periods. 516 517 handle_calendar_object(self.obj, handlers, "PUBLISH") 518 519 for o in unscheduled_objects: 520 handle_calendar_object(o, handlers, "CANCEL") 521 522 for o in rescheduled_objects: 523 handle_calendar_object(o, handlers, "PUBLISH") 524 525 for o in added_objects: 526 handle_calendar_object(o, handlers, "ADD") 527 528 def update_periods_from_rule(self): 529 530 "Update the periods from the rule." 531 532 selectors = self.state.get("rule") 533 periods = self.state.get("periods") 534 535 main_period = get_main_period(periods) 536 tzid = main_period.get_tzid() 537 538 start = main_period.get_start() 539 end = self.get_window_end() or None 540 541 selector = vRecurrence.get_selector(start, selectors) 542 until = None 543 inclusive = False 544 545 # Generate the periods from the rule. 546 547 rule_periods = form_periods_from_periods( 548 get_periods_using_selector(selector, main_period, tzid, 549 start, end, inclusive)) 550 551 # Retain any applicable replacement periods that either modify or cancel 552 # rule periods. 553 # NOTE: To be done. 554 555 self.state.set("periods", rule_periods) 556 557 # Editing methods involving interaction. 558 559 def add_rule_selectors(self): 560 561 "Add rule selectors to the rule details." 562 563 selectors = self.state.get("rule") 564 565 while True: 566 567 # Obtain a command from any arguments. 568 569 s = read_input("Selector: (c)ount, (f)requency, (s)election (or return)> ") 570 args = s.split() 571 cmd = next_arg(args) 572 573 if cmd in ("c", "count", "limit"): 574 add_rule_selector_count(selectors, args) 575 elif cmd in ("f", "freq", "frequency"): 576 add_rule_selector_frequency(selectors, args) 577 elif cmd in ("s", "select", "selection"): 578 add_rule_selector_selection(selectors, args) 579 580 # Remain in the loop unless explicitly terminated. 581 582 elif not cmd or cmd == "end": 583 break 584 585 def edit_attendee(self, index): 586 587 "Edit the attendee at 'index'." 588 589 t = self.can_edit_attendee(index) 590 if not t: 591 return 592 593 attendees = self.state.get("attendees") 594 old_attendee, attr = t 595 596 attendee = input_with_default("Attendee (%s) (or return)? ", old_attendee) 597 598 # Remove the old attendee if null or a replacement is given. 599 600 if attendee.strip() or not old_attendee: 601 del attendees[old_attendee] 602 603 # Add any replacement. 604 605 if attendee.strip(): 606 attendees[attendee] = attr 607 608 def edit_period(self, index, args=None): 609 610 "Edit the period at 'index'." 611 612 period = self.can_edit_period(index) 613 if period: 614 edit_period(period, args) 615 period.cancelled = False 616 617 # Change the origin of modified rule periods. 618 619 if period.origin == "RRULE": 620 period.origin = "RDATE" 621 622 # Sort the periods after this change. 623 624 periods = self.state.get("periods") 625 periods.sort() 626 627 def edit_rule_selector(self, index, args): 628 629 "Edit the selector having the given 'index'." 630 631 selectors = self.state.get("rule") 632 selector = self.can_edit_rule_selector(index) 633 634 if not selector: 635 return 636 637 while True: 638 show_rule_selector(selector) 639 640 # Obtain a command from any arguments. 641 642 cmd = next_arg(args) 643 if not cmd: 644 s = read_input("Selector: (e)dit, (r)emove (or return)> ") 645 args = s.split() 646 cmd = next_arg(args) 647 648 # Edit an existing selector. 649 650 if cmd in EDIT_COMMANDS: 651 if isinstance(selector, vRecurrence.LimitSelector): 652 add_rule_selector_count(selectors, args, selector) 653 elif isinstance(selector, vRecurrence.Pattern): 654 add_rule_selector_frequency(selectors, args, selector) 655 else: 656 add_rule_selector_selection(selectors, args, selector) 657 658 # Remove an existing selector. 659 660 elif cmd in REMOVE_COMMANDS: 661 del selectors[index] 662 663 # Exit if requested or after a successful 664 # operation. 665 666 elif not cmd: 667 pass 668 else: 669 continue 670 break 671 672 def edit_summary(self, summary=None): 673 674 "Edit or set the 'summary'." 675 676 if self.can_edit_properties(): 677 if not summary: 678 summary = input_with_default("Summary (%s)? ", self.state.get("summary")) 679 self.state.set("summary", summary) 680 681 def finish(self): 682 683 "Finish editing, warning of errors if any occur." 684 685 try: 686 EditingClient.finish(self) 687 except PeriodError: 688 print "Errors exist in the periods." 689 return 690 691 # Diagnostic methods. 692 693 def show_period_classification(self): 694 695 "Show the classification of the periods." 696 697 try: 698 new, replaced, retained, cancelled, obsolete = self.classify_periods() 699 show_period_classification(new, replaced, retained, cancelled, obsolete) 700 except PeriodError: 701 print 702 print "Errors exist in the periods." 703 704 def show_changes(self): 705 706 "Show how the periods have changed." 707 708 try: 709 modified, unmodified, removed = self.classify_period_changes() 710 show_changes(modified, unmodified, removed) 711 except PeriodError: 712 print "Errors exist in the periods." 713 714 is_changed = self.properties_changed() 715 if is_changed: 716 print 717 print "Properties changed:", ", ".join(is_changed) 718 new, modified, unmodified, removed = self.classify_attendee_changes() 719 show_attendee_changes(new, modified, unmodified, removed) 720 721 def show_operations(self): 722 723 "Show the operations required to change the periods for recipients." 724 725 is_changed = self.properties_changed() 726 727 try: 728 to_unschedule, to_reschedule, to_add, to_exclude, to_set, \ 729 all_unscheduled, all_rescheduled = self.classify_period_operations() 730 show_period_operations(to_unschedule, to_reschedule, to_add, 731 to_exclude, to_set, 732 all_unscheduled, all_rescheduled) 733 except PeriodError: 734 print "Errors exist in the periods." 735 736 to_invite, to_cancel, to_modify = self.classify_attendee_operations() 737 show_attendee_operations(to_invite, to_cancel, to_modify) 738 739 # Output methods. 740 741 def show_message(self, message, plain=False, filename=None): 742 743 """ 744 Show the given mail 'message', decoding to plain text if 'plain' is set 745 to a true value, writing it to 'filename' if indicated. 746 """ 747 748 if plain: 749 decode_part(message) 750 write(message_as_string(message), filename) 751 752 def show_cancel_message(self, plain=False, filename=None): 753 754 "Show the cancel message for uninvited attendees." 755 756 message = self.prepare_cancel_message() 757 if message: 758 self.show_message(message, plain, filename) 759 760 def show_publish_message(self, plain=False, filename=None): 761 762 "Show the publishing message for the updated event." 763 764 message = self.prepare_publish_message() 765 self.show_message(message, plain, filename) 766 767 def show_update_message(self, plain=False, filename=None): 768 769 "Show the update message for the updated event." 770 771 message = self.prepare_update_message() 772 if message: 773 self.show_message(message, plain, filename) 774 775 # General display methods. 776 777 def show_object(self): 778 print 779 print_title("Object details") 780 print 781 print "Summary:", self.state.get("summary") 782 print 783 print "Organiser:", self.state.get("organiser") 784 self.show_attendees() 785 self.show_periods() 786 self.show_rule() 787 self.show_suggested_attendees() 788 self.show_suggested_periods() 789 self.show_conflicting_periods() 790 print 791 print "Object is", self.obj.is_shared() and "shared" or "not shared" 792 793 def show_attendees(self): 794 print 795 print_title("Attendees") 796 attendees = self.state.get("attendees") 797 for index, attendee_item in enumerate(attendees.items()): 798 show_attendee(attendee_item, index) 799 800 def show_periods(self): 801 print 802 print_title("Periods") 803 show_periods(self.state.get("periods"), self.state.get("period_errors")) 804 805 def show_rule(self): 806 selectors = self.state.get("rule") 807 if selectors: 808 print 809 print_title("Period recurrence rule") 810 show_rule(selectors) 811 812 def show_suggested_attendees(self): 813 current_attendee = None 814 for index, (attendee, suggested_item) in enumerate(self.state.get("suggested_attendees")): 815 if attendee != current_attendee: 816 print 817 print_title("Attendees suggested by %s" % attendee) 818 current_attendee = attendee 819 show_attendee(suggested_item, index) 820 821 def show_suggested_periods(self): 822 periods = self.state.get("suggested_periods") 823 current_attendee = None 824 index = 0 825 for attendee, period, operation in periods: 826 if attendee != current_attendee: 827 print 828 print_title("Periods suggested by %s" % attendee) 829 current_attendee = attendee 830 show_period(period, index) 831 print " %s" % (operation == "add" and "Add this period" or "Remove this period") 832 index += 1 833 834 def show_conflicting_periods(self): 835 conflicts = self.get_conflicting_periods() 836 if not conflicts: 837 return 838 print 839 print_title("Conflicting periods") 840 841 conflicts = list(conflicts) 842 conflicts.sort() 843 844 for p in conflicts: 845 print p.summary, p.uid, p.get_start(), p.get_end() 846 847 # Interaction functions. 848 849 def expand_arg(args): 850 851 """ 852 Expand the first argument in 'args' to a pair of arguments if having the 853 form <char><digit>... 854 """ 855 856 if args[0] and args[0][1:].isdigit(): 857 args[:1] = [args[0][0], args[0][1:]] 858 859 def get_text_arg(s): 860 861 """ 862 Split 's' after the first whitespace occurrence, returning the remaining 863 text or None if no such text exists. 864 """ 865 866 return (s.split(None, 1)[1:] or [None])[0] 867 868 def next_arg(args): 869 870 """ 871 Return the first argument from 'args', removing it, or return None if no 872 arguments are left. 873 """ 874 875 if args: 876 arg = args[0] 877 del args[0] 878 return arg 879 return None 880 881 # Editing functions. 882 883 def edit_period(period, args=None): 884 885 "Edit the given 'period'." 886 887 print "Editing start (%s)" % period.get_start() 888 edit_date(period.start, args) 889 print "Editing end (%s)" % period.get_end() 890 edit_date(period.end, args) 891 892 def edit_date(date, args=None): 893 894 "Edit the given 'date' object attributes." 895 896 date.date = next_arg(args) or input_with_default("Date (%s)? ", date.date) 897 date.hour = next_arg(args) or input_with_default("Hour (%s)? ", date.hour) 898 date.minute = next_arg(args) or input_with_default("Minute (%s)? ", date.minute) 899 date.second = next_arg(args) or input_with_default("Second (%s)? ", date.second) 900 date.tzid = next_arg(args) or input_with_default("Time zone (%s)? ", date.tzid) 901 date.reset() 902 903 def add_rule_selector_count(selectors, args, selector=None): 904 905 "Add to 'selectors' a selector imposing a count restriction." 906 907 while True: 908 arg = next_arg(args) 909 if not arg: 910 if selector: 911 arg = input_with_default("Number of occurrences (%d)? ", 912 selector.get_limit()) 913 else: 914 arg = read_input("Number of occurrences? ") 915 916 count = to_int_or_none(arg) 917 918 if count is None: 919 arg = None 920 continue 921 922 # Change or add selector. 923 924 selector = selector or selectors and \ 925 isinstance(selectors[0], vRecurrence.LimitSelector) and \ 926 selectors[0] or None 927 928 if not selector: 929 selector = vRecurrence.new_selector("COUNT") 930 selectors.insert(0, selector) 931 932 selector.set_limit(count) 933 break 934 935 def add_rule_selector_frequency(selectors, args, selector=None): 936 937 "Add to 'selectors' a selector for a frequency." 938 939 while not selector: 940 arg = next_arg(args) 941 if not arg: 942 arg = read_input("Select (y)early, (M)onthly, (w)eekly, (d)aily, " 943 "(h)ourly, (m)inutely, (s)econdly (or return)? ") 944 945 if not arg: 946 return 947 948 arg_lower = arg.lower() 949 950 if arg_lower in ("y", "year", "yearly"): 951 qualifier = "YEARLY" 952 elif arg == "M" or arg_lower in ("month", "monthly"): 953 qualifier = "MONTHLY" 954 elif arg_lower in ("w", "week", "weekly"): 955 qualifier = "WEEKLY" 956 elif arg_lower in ("d", "day", "daily"): 957 qualifier = "DAILY" 958 elif arg_lower in ("h", "hour", "hourly"): 959 qualifier = "HOURLY" 960 elif arg_lower in ("m", "minute", "minutely"): 961 qualifier = "MINUTELY" 962 elif arg_lower in ("s", "second", "secondly"): 963 qualifier = "SECONDLY" 964 else: 965 continue 966 967 break 968 969 while True: 970 arg = next_arg(args) 971 if not arg: 972 if selector: 973 arg = input_with_default("Interval (%d)? ", 974 selector.get_interval()) 975 else: 976 arg = input_with_default("Interval (%d)? ", 1) 977 978 interval = to_int_or_none(arg) 979 980 if interval is None: 981 arg = None 982 else: 983 break 984 985 # Update an existing selector. 986 987 if selector: 988 selector.set_interval(interval) 989 return 990 991 # Create a new selector. 992 993 selector = vRecurrence.new_selector(qualifier) 994 selector.set_interval(interval) 995 996 # Remove any existing frequency selector. 997 998 for index, _selector in enumerate(selectors): 999 if isinstance(_selector, vRecurrence.Pattern): 1000 del selectors[index] 1001 break 1002 1003 # Add the new selector and keep the selectors in order. 1004 1005 selectors.append(selector) 1006 vRecurrence.sort_selectors(selectors) 1007 1008 def add_rule_selector_selection(selectors, args, selector=None): 1009 1010 "Add to 'selectors' a selector for a particular point in time." 1011 1012 qualifier = selector and selector.qualifier or None 1013 1014 while not selector: 1015 arg = next_arg(args) 1016 if not arg: 1017 arg = read_input("Select (M)onths, (w)eeks, (y)eardays, " 1018 "m(o)nthdays, week(d)ays, (h)ours, (m)inutes, " 1019 "(s)econds (or return)? ") 1020 1021 if not arg: 1022 return 1023 1024 arg_lower = arg.lower() 1025 1026 if arg == "M" or arg_lower in ("month", "months"): 1027 qualifier = "BYMONTH" 1028 elif arg_lower in ("w", "week", "weeks"): 1029 qualifier = "BYWEEKNO" 1030 elif arg_lower in ("y", "yearday", "yeardays"): 1031 qualifier = "BYYEARDAY" 1032 elif arg_lower in ("o", "monthday", "monthdays"): 1033 qualifier = "BYMONTHDAY" 1034 elif arg_lower in ("d", "weekday", "weekdays"): 1035 qualifier = "BYDAY" 1036 elif arg_lower in ("h", "hour", "hours"): 1037 qualifier = "BYHOUR" 1038 elif arg_lower in ("m", "minute", "minutes"): 1039 qualifier = "BYMINUTE" 1040 elif arg_lower in ("s", "second", "seconds"): 1041 qualifier = "BYSECOND" 1042 else: 1043 continue 1044 1045 break 1046 1047 if not qualifier: 1048 return 1049 1050 ranges = vRecurrence.get_value_ranges(qualifier) 1051 ranges_str = format_value_ranges(ranges[0]) 1052 1053 values = [] 1054 1055 while True: 1056 arg = next_arg(args) 1057 if not arg: 1058 arg = read_input("Value (%s) (return to end)? " % ranges_str) 1059 1060 # Stop if no more arguments. 1061 1062 if not arg or arg == "end": 1063 break 1064 1065 # Handle weekdays. 1066 1067 if qualifier == "BYDAY": 1068 value = arg.upper() # help to match weekdays 1069 1070 arg = next_arg(args) 1071 if not arg: 1072 arg = read_input("Occurrence within a month? ") 1073 1074 index = to_int_or_none(arg) 1075 value = vRecurrence.check_values(qualifier, [value, index]) 1076 1077 # Handle all other values. 1078 1079 else: 1080 value = to_int_or_none(arg) 1081 l = vRecurrence.check_values(qualifier, [value]) 1082 value = l and l[0] 1083 1084 # Append valid values. 1085 1086 if value is not None: 1087 values.append(value) 1088 else: 1089 print "Value not recognised." 1090 1091 if not values: 1092 return 1093 1094 # Update an existing selector. 1095 1096 if selector: 1097 selector.set_values(values) 1098 return 1099 1100 # Create a new selector. 1101 1102 selector = vRecurrence.new_selector(qualifier) 1103 selector.set_values(values) 1104 1105 # Remove any existing selector. 1106 1107 for index, _selector in enumerate(selectors): 1108 if _selector.qualifier == selector.qualifier: 1109 del selectors[index] 1110 break 1111 1112 # Add the new selector and keep the selectors in order. 1113 1114 selectors.append(selector) 1115 vRecurrence.sort_selectors(selectors) 1116 1117 def select_object(cl, objects): 1118 1119 "Select using 'cl' an object from the given 'objects'." 1120 1121 print 1122 1123 if objects: 1124 label = "Select object number or (n)ew object or (q)uit> " 1125 else: 1126 label = "Select (n)ew object or (q)uit> " 1127 1128 while True: 1129 try: 1130 cmd = read_input(label) 1131 except EOFError: 1132 return None 1133 1134 if cmd.isdigit(): 1135 index = to_int_or_none(cmd) 1136 1137 if index is not None and 0 <= index < len(objects): 1138 obj = objects[index] 1139 return cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 1140 1141 elif cmd in ("n", "new"): 1142 return cl.new_object() 1143 elif cmd in ("q", "quit", "exit"): 1144 return None 1145 1146 def show_commands(): 1147 1148 "Show editing and inspection commands." 1149 1150 print 1151 print_title("Editing commands") 1152 print 1153 print """\ 1154 %(ATTENDEE_NEW_COMMANDS)s 1155 Add attendee 1156 1157 %(ATTENDEE_COMMANDS)s 1158 Select attendee from list 1159 1160 %(ATTENDANCE_COMMANDS)s 1161 Change attendance/participation 1162 1163 %(SUGGESTED_ATTENDEE_COMMANDS)s 1164 Add suggested attendee from list 1165 1166 %(FINISH_COMMANDS)s 1167 Finish editing, confirming changes, proceeding to messaging 1168 1169 %(HELP_COMMANDS)s 1170 Show this help message 1171 1172 %(LIST_COMMANDS)s 1173 List/show all event details 1174 1175 %(PERIOD_NEW_COMMANDS)s 1176 Add new period 1177 1178 %(PERIOD_COMMANDS)s 1179 Select period from list 1180 1181 %(SUGGESTED_PERIOD_COMMANDS)s 1182 Add or remove suggested period from list 1183 1184 %(QUIT_COMMANDS)s 1185 Exit/quit this program 1186 1187 %(RESET_COMMANDS)s 1188 Reset event periods (return to editing mode, if already finished) 1189 1190 %(RULE_NEW_COMMANDS)s 1191 Add a period recurrence rule 1192 1193 %(RULE_COMMANDS)s 1194 Select period recurrence rule selector from list 1195 1196 %(SUMMARY_COMMANDS)s 1197 Set event summary 1198 """ % { 1199 "ATTENDEE_NEW_COMMANDS" : commandlist(ATTENDEE_COMMANDS, "[ <uri> ]"), 1200 "ATTENDEE_COMMANDS" : commandlist(ATTENDEE_COMMANDS, "<number>"), 1201 "ATTENDANCE_COMMANDS" : commandlist(ATTENDANCE_COMMANDS), 1202 "SUGGESTED_ATTENDEE_COMMANDS" : commandlist(SUGGESTED_ATTENDEE_COMMANDS, "<number>"), 1203 "FINISH_COMMANDS" : commandlist(FINISH_COMMANDS), 1204 "HELP_COMMANDS" : commandlist(HELP_COMMANDS), 1205 "LIST_COMMANDS" : commandlist(LIST_COMMANDS), 1206 "PERIOD_NEW_COMMANDS" : commandlist(PERIOD_COMMANDS, "[ new ]"), 1207 "PERIOD_COMMANDS" : commandlist(PERIOD_COMMANDS, "<number>"), 1208 "SUGGESTED_PERIOD_COMMANDS" : commandlist(SUGGESTED_PERIOD_COMMANDS, "<number>"), 1209 "QUIT_COMMANDS" : commandlist(QUIT_COMMANDS), 1210 "RESET_COMMANDS" : commandlist(RESET_COMMANDS), 1211 "RULE_NEW_COMMANDS" : commandlist(RULE_COMMANDS), 1212 "RULE_COMMANDS" : commandlist(RULE_COMMANDS, "<number>"), 1213 "SUMMARY_COMMANDS" : commandlist(SUMMARY_COMMANDS), 1214 } 1215 1216 print_title("Messaging commands") 1217 print 1218 print """\ 1219 S, send 1220 Send messages to recipients and to self, if appropriate 1221 """ 1222 1223 print_title("Diagnostic commands") 1224 print 1225 print """\ 1226 c, class, classification 1227 Show period classification 1228 1229 C, changes 1230 Show changes made by editing 1231 1232 o, ops, operations 1233 Show update operations 1234 1235 RECURRENCE-ID [ <filename> ] 1236 Show event recurrence identifier, writing to <filename> if specified 1237 1238 UID [ <filename> ] 1239 Show event unique identifier, writing to <filename> if specified 1240 """ 1241 1242 print_title("Message inspection commands") 1243 print 1244 print """\ 1245 P [ <filename> ] 1246 publish [ <filename> ] 1247 Show publishing message, writing to <filename> if specified 1248 1249 R [ <filename> ] 1250 remove [ <filename> ] 1251 cancel [ <filename> ] 1252 Show cancellation message sent to uninvited/removed recipients, writing to 1253 <filename> if specified 1254 1255 U [ <filename> ] 1256 update [ <filename> ] 1257 Show update message, writing to <filename> if specified 1258 """ 1259 1260 def edit_object(cl, obj, handle_outgoing=False): 1261 1262 """ 1263 Edit using 'cl' the given object 'obj'. If 'handle_outgoing' is specified 1264 and set to a true value, the details from outgoing messages are incorporated 1265 into the stored data. 1266 """ 1267 1268 cl.show_object() 1269 print 1270 1271 try: 1272 while True: 1273 role = cl.is_organiser() and "Organiser" or "Attendee" 1274 status = cl.state.get("finished") and " (editing complete)" or "" 1275 1276 s = read_input("%s%s> " % (role, status)) 1277 args = s.split() 1278 1279 if not args or not args[0]: 1280 continue 1281 1282 # Expand short-form arguments. 1283 1284 expand_arg(args) 1285 cmd = next_arg(args) 1286 1287 # Check the status of the periods. 1288 1289 if cmd in CLASSIFICATION_COMMANDS: 1290 cl.show_period_classification() 1291 print 1292 1293 elif cmd in CHANGE_COMMANDS: 1294 cl.show_changes() 1295 print 1296 1297 # Finish editing. 1298 1299 elif cmd in FINISH_COMMANDS: 1300 cl.finish() 1301 1302 # Help. 1303 1304 elif cmd in HELP_COMMANDS: 1305 show_commands() 1306 1307 # Show object details. 1308 1309 elif cmd in LIST_COMMANDS: 1310 cl.show_object() 1311 print 1312 1313 # Show the operations. 1314 1315 elif cmd in OPERATION_COMMANDS: 1316 cl.show_operations() 1317 print 1318 1319 # Quit or exit. 1320 1321 elif cmd in QUIT_COMMANDS: 1322 break 1323 1324 # Restart editing. 1325 1326 elif cmd in RESET_COMMANDS: 1327 obj = cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 1328 if not obj: 1329 obj = cl.new_object() 1330 cl.reset() 1331 cl.show_object() 1332 print 1333 1334 # Show UID details. 1335 1336 elif cmd in UID_COMMANDS: 1337 filename = get_text_arg(s) 1338 write(obj.get_uid(), filename) 1339 1340 elif cmd in RECURRENCEID_COMMANDS: 1341 filename = get_text_arg(s) 1342 write(obj.get_recurrenceid() or "", filename) 1343 1344 # Post-editing operations. 1345 1346 elif cl.state.get("finished"): 1347 1348 # Show messages. 1349 1350 if cmd in PUBLISH_COMMANDS: 1351 filename = get_text_arg(s) 1352 cl.show_publish_message(plain=not filename, filename=filename) 1353 1354 elif cmd in CANCEL_COMMANDS: 1355 filename = get_text_arg(s) 1356 cl.show_cancel_message(plain=not filename, filename=filename) 1357 1358 elif cmd in UPDATE_COMMANDS: 1359 filename = get_text_arg(s) 1360 cl.show_update_message(plain=not filename, filename=filename) 1361 1362 # Definitive finishing action. 1363 1364 elif cmd in SEND_COMMANDS: 1365 1366 # Send update and cancellation messages. 1367 1368 did_send = False 1369 1370 message = cl.prepare_update_message() 1371 if message: 1372 cl.send_message(message, cl.get_recipients()) 1373 did_send = True 1374 1375 to_cancel = cl.state.get("attendees_to_cancel") 1376 if to_cancel: 1377 message = cl.prepare_cancel_message() 1378 if message: 1379 cl.send_message(message, to_cancel) 1380 did_send = True 1381 1382 # Process the object using the person outgoing handler. 1383 1384 if handle_outgoing: 1385 cl.handle_outgoing_object() 1386 1387 # Otherwise, send a message to self with the event details. 1388 1389 else: 1390 message = cl.prepare_publish_message() 1391 if message: 1392 cl.send_message_to_self(message) 1393 did_send = True 1394 1395 # Exit if sending occurred. 1396 1397 if did_send: 1398 break 1399 else: 1400 print "No messages sent. Try making edits or exit manually." 1401 1402 # Editing operations. 1403 1404 elif not cl.state.get("finished"): 1405 1406 # Add or edit attendee. 1407 1408 if cmd in ATTENDEE_COMMANDS: 1409 value = next_arg(args) 1410 index = to_int_or_none(value) 1411 1412 if index is None: 1413 try: 1414 index = cl.find_attendee(value) 1415 except ValueError: 1416 index = None 1417 1418 # Add an attendee. 1419 1420 if index is None: 1421 cl.add_attendee(value) 1422 if not value: 1423 cl.edit_attendee(-1) 1424 1425 # Edit attendee (using index). 1426 1427 else: 1428 attendee_item = cl.can_remove_attendee(index) 1429 if attendee_item: 1430 while True: 1431 show_attendee(attendee_item, index) 1432 1433 # Obtain a command from any arguments. 1434 1435 cmd = next_arg(args) 1436 if not cmd: 1437 cmd = read_input("Attendee: (e)dit, (r)emove (or return)> ") 1438 if cmd in EDIT_COMMANDS: 1439 cl.edit_attendee(index) 1440 elif cmd in REMOVE_COMMANDS: 1441 cl.remove_attendees([index]) 1442 1443 # Exit if requested or after a successful 1444 # operation. 1445 1446 elif not cmd: 1447 pass 1448 else: 1449 continue 1450 break 1451 1452 cl.show_attendees() 1453 print 1454 1455 # Add suggested attendee (using index). 1456 1457 elif cmd in SUGGESTED_ATTENDEE_COMMANDS: 1458 value = next_arg(args) 1459 index = to_int_or_none(value) 1460 1461 if index is not None: 1462 cl.add_suggested_attendee(index) 1463 1464 cl.show_attendees() 1465 print 1466 1467 # Edit attendance. 1468 1469 elif cmd in ATTENDANCE_COMMANDS: 1470 1471 if not cl.is_attendee() and cl.is_organiser(): 1472 cl.add_attendee(cl.user) 1473 1474 # NOTE: Support delegation. 1475 1476 if cl.can_edit_attendance(): 1477 while True: 1478 1479 # Obtain a command from any arguments. 1480 1481 cmd = next_arg(args) 1482 if not cmd: 1483 cmd = read_input("Attendance: (a)ccept, (d)ecline, (t)entative (or return)> ") 1484 if cmd in ACCEPTED_VALUES: 1485 cl.edit_attendance("ACCEPTED") 1486 elif cmd in DECLINED_VALUES: 1487 cl.edit_attendance("DECLINED") 1488 elif cmd in TENTATIVE_VALUES: 1489 cl.edit_attendance("TENTATIVE") 1490 1491 # Exit if requested or after a successful operation. 1492 1493 elif not cmd: 1494 pass 1495 else: 1496 continue 1497 break 1498 1499 cl.show_attendees() 1500 print 1501 1502 # Add or edit period. 1503 1504 elif cmd in PERIOD_COMMANDS: 1505 value = next_arg(args) 1506 index = to_int_or_none(value) 1507 1508 # Add a new period. 1509 1510 if index is None or value == "new": 1511 cl.add_period() 1512 cl.edit_period(-1, args) 1513 1514 # Edit period (using index). 1515 1516 else: 1517 period = cl.can_edit_period(index) 1518 if period: 1519 while True: 1520 show_period_raw(period) 1521 1522 # Obtain a command from any arguments. 1523 1524 cmd = next_arg(args) 1525 if not cmd: 1526 cmd = read_input("Period: (c)ancel, (e)dit, (u)ncancel (or return)> ") 1527 1528 if cmd in CANCEL_COMMANDS: 1529 cl.cancel_periods([index]) 1530 elif cmd in EDIT_COMMANDS: 1531 cl.edit_period(index, args) 1532 elif cmd in UNCANCEL_COMMANDS: 1533 cl.cancel_periods([index], False) 1534 1535 # Exit if requested or after a successful 1536 # operation. 1537 1538 elif not cmd: 1539 pass 1540 else: 1541 continue 1542 break 1543 1544 cl.show_periods() 1545 print 1546 1547 # Apply suggested period (using index). 1548 1549 elif cmd in SUGGESTED_PERIOD_COMMANDS: 1550 value = next_arg(args) 1551 index = to_int_or_none(value) 1552 1553 if index is not None: 1554 cl.apply_suggested_period(index) 1555 1556 cl.show_periods() 1557 print 1558 1559 # Specify a recurrence rule. 1560 1561 elif cmd in RULE_COMMANDS: 1562 value = next_arg(args) 1563 index = to_int_or_none(value) 1564 1565 # Add a new rule. 1566 1567 if index is None: 1568 cl.add_rule_selectors() 1569 else: 1570 cl.edit_rule_selector(index, args) 1571 1572 cl.show_rule() 1573 cl.update_periods_from_rule() 1574 print 1575 1576 # Set the summary. 1577 1578 elif cmd in SUMMARY_COMMANDS: 1579 cl.edit_summary(get_text_arg(s)) 1580 cl.show_object() 1581 print 1582 1583 except EOFError: 1584 return 1585 1586 def main(args): 1587 1588 """ 1589 The main program, employing command line 'args' to initialise the editing 1590 activity. 1591 """ 1592 1593 global echo 1594 1595 if "--help" in args: 1596 show_help(os.path.split(sys.argv[0])[-1]) 1597 return 0 1598 1599 # Parse command line arguments using the standard options plus some extra 1600 # options. 1601 1602 args = parse_args(args, { 1603 "--calendar-data" : ("calendar_data", False), 1604 "--charset" : ("charset", "utf-8"), 1605 "--echo" : ("echo", False), 1606 "-f" : ("filename", None), 1607 "--handle-data" : ("handle_data", False), 1608 "--suppress-bcc" : ("suppress_bcc", False), 1609 "-u" : ("user", None), 1610 "--uid" : ("uid", None), 1611 "--recurrence-id" : ("recurrenceid", None), 1612 "--show-config" : ("show_config", False) 1613 }) 1614 1615 charset = args["charset"] 1616 calendar_data = args["calendar_data"] 1617 echo = args["echo"] 1618 filename = args["filename"] 1619 handle_data = args["handle_data"] 1620 sender = (args["senders"] or [None])[0] 1621 suppress_bcc = args["suppress_bcc"] 1622 user = args["user"] 1623 uid = args["uid"] 1624 recurrenceid = args["recurrenceid"] 1625 1626 # Open a store. 1627 1628 store_type = args.get("store_type") 1629 store_dir = args.get("store_dir") 1630 preferences_dir = args.get("preferences_dir") 1631 1632 # Show configuration and exit if requested. 1633 1634 if args["show_config"]: 1635 print """\ 1636 Store type: %s (%s) 1637 Store directory: %s (%s) 1638 Preferences directory: %s 1639 """ % ( 1640 store_type, settings["STORE_TYPE"], 1641 store_dir, settings["STORE_DIR"], 1642 preferences_dir) 1643 return 0 1644 1645 # Determine the user and sender identities. 1646 1647 if sender and not user: 1648 user = get_uri(sender) 1649 elif user and not sender: 1650 sender = get_address(user) 1651 elif not sender and not user: 1652 print >>sys.stderr, "A sender or a user must be specified." 1653 return 1 1654 1655 # Obtain a store but not a journal. 1656 1657 store = get_store(store_type, store_dir) 1658 journal = None 1659 1660 # Open a messenger for the user. 1661 1662 messenger = Messenger(sender=sender, suppress_bcc=suppress_bcc) 1663 1664 # Open a client for the user. 1665 1666 cl = TextClient(user, messenger, store, journal, preferences_dir) 1667 1668 # Read any input resource, using it to obtain identifier details. 1669 1670 if filename: 1671 if calendar_data: 1672 all_itip = get_itip_from_data(filename, charset) 1673 else: 1674 all_itip = get_itip_from_message(filename) 1675 1676 objects = [] 1677 1678 # Process the objects using the person handler. 1679 1680 if handle_data: 1681 for itip in all_itip: 1682 handled = handle_calendar_data(itip, get_handlers(cl, person.handlers, None)) 1683 if not is_cancel_itip(itip): 1684 objects += handled 1685 1686 # Or just obtain objects from the data. 1687 1688 else: 1689 for itip in all_itip: 1690 handled = get_objects_from_itip(itip, ["VEVENT"]) 1691 if not is_cancel_itip(itip): 1692 objects += handled 1693 1694 # Choose an object to edit. 1695 1696 show_objects(objects, user, store) 1697 obj = select_object(cl, objects) 1698 1699 # Load any indicated object. 1700 1701 elif uid: 1702 obj = cl.load_object(uid, recurrenceid) 1703 1704 # Or create a new object. 1705 1706 else: 1707 obj = cl.new_object() 1708 1709 # Exit without any object. 1710 1711 if not obj: 1712 print >>sys.stderr, "No object loaded." 1713 return 1 1714 1715 # Edit the object. 1716 1717 edit_object(cl, obj, handle_outgoing=handle_data) 1718 1719 def show_help(progname): 1720 print >>sys.stderr, help_text % progname 1721 1722 help_text = """\ 1723 Usage: %s -s <sender> | -u <user> \\ 1724 [ -f <filename> | --uid <uid> [ --recurrence-id <recurrence-id> ] ] \\ 1725 [ --calendar-data --charset ] \\ 1726 [ --handle-data ] \\ 1727 [ -T <store type ] [ -S <store directory> ] \\ 1728 [ -p <preferences directory> ] \\ 1729 [ --echo ] 1730 1731 Identity options: 1732 1733 -s Indicate the user by specifying a sender address 1734 -u Indicate the user by specifying their URI 1735 1736 Input options: 1737 1738 -f Indicates a filename containing a MIME-encoded message or 1739 calendar object 1740 --uid Indicates the UID of a stored calendar object 1741 --recurrence-id Indicates a stored object with a specific RECURRENCE-ID 1742 1743 --calendar-data Indicates that the specified file contains a calendar object 1744 as opposed to a mail message 1745 --charset Specifies the character encoding used by a calendar object 1746 description 1747 1748 Processing options: 1749 1750 --handle-data Cause the input to be handled and stored in the configured 1751 data store 1752 1753 Configuration options (overriding configured defaults): 1754 1755 -p Indicates the location of user preference directories 1756 -S Indicates the location of the calendar data store containing user storage 1757 directories 1758 -T Indicates the store type (the configured value if omitted) 1759 1760 Output options: 1761 1762 --echo Echo received input, useful if consuming input from the 1763 standard input stream and producing a log of the program's 1764 activity 1765 """ 1766 1767 if __name__ == "__main__": 1768 sys.exit(main(sys.argv[1:])) 1769 1770 # vim: tabstop=4 expandtab shiftwidth=4