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