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