1 #!/usr/bin/env python 2 3 """ 4 A text interface to received and new events. 5 6 Copyright (C) 2017 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 parse_itip_part 28 from imiptools.data import get_address, get_main_period, get_recurrence_periods, get_value, parse_object 29 from imiptools.dates import get_datetime_item, get_time, to_timezone 30 from imiptools.editing import EditingClient, PeriodError 31 from imiptools.handlers import person, person_outgoing 32 from imiptools.mail import Messenger 33 from imiptools.stores import get_journal, get_store 34 from imiptools.utils import decode_part, message_as_string 35 import vRecurrence 36 import sys, os 37 38 # User interface functions. 39 40 echo = False 41 42 def read_input(label): 43 44 """ 45 Read input, prompting using 'label', stripping leading and trailing 46 whitespace, echoing the input if the global 'echo' variable is set. 47 """ 48 49 s = raw_input(label).strip() 50 if echo: 51 print s 52 return s 53 54 def input_with_default(label, default): 55 56 """ 57 Read input, prompting using 'label', parameterising the label with the given 58 'default' and returning the default if no input is given. 59 """ 60 61 return read_input(label % default) or default 62 63 def print_title(text): 64 65 "Print 'text' with simple, fixed-width styling as a title." 66 67 print text 68 print len(text) * "-" 69 70 def print_table(rows, separator_index=0): 71 72 """ 73 Print 'rows' as a simple, fixed-width table. If 'separator_index' is set to 74 a row index present in the rows, a table separator will be produced below 75 that row's data. Otherwise, a separator will appear below the first row. 76 """ 77 78 widths = [] 79 for row in rows: 80 for i, col in enumerate(row): 81 if i >= len(widths): 82 widths.append(len(col)) 83 else: 84 widths[i] = max(widths[i], len(col)) 85 86 for i, row in enumerate(rows): 87 for col, width in zip(row, widths): 88 print "%s%s" % (col, " " * (width - len(col))), 89 print 90 if i == separator_index: 91 for width in widths: 92 print "-" * width, 93 print 94 95 def write(s, filename): 96 97 "Write 's' to a file having the given 'filename'." 98 99 f = filename and open(filename, "w") or None 100 try: 101 print >>(f or sys.stdout), s 102 finally: 103 if f: 104 f.close() 105 106 # Interpret an input file containing a calendar resource. 107 108 def get_itip_from_message(filename): 109 110 "Return iTIP details provided by 'filename'." 111 112 f = open(filename) 113 try: 114 msg = message_from_file(f) 115 finally: 116 f.close() 117 118 all_itip = [] 119 120 for part in msg.walk(): 121 if have_itip_part(part): 122 all_itip.append(parse_itip_part(part)) 123 124 return all_itip 125 126 def get_itip_from_data(filename, charset): 127 128 "Return objects provided by 'filename'." 129 130 f = open(filename) 131 try: 132 itip = parse_object(f, charset, "VCALENDAR") 133 finally: 134 f.close() 135 136 return [itip] 137 138 def show_objects(objects, user, store): 139 140 """ 141 Show details of 'objects', accessed by the given 'user' in the given 142 'store'. 143 """ 144 145 print 146 print_title("Objects") 147 print 148 149 for index, obj in enumerate(objects): 150 recurrenceid = obj.get_recurrenceid() 151 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 152 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 153 154 def show_requests(user, store): 155 156 "Show requests available to the given 'user' in the given 'store'." 157 158 requests = store.get_requests(user) 159 160 print 161 print_title("Requests") 162 print 163 164 if not requests: 165 print "No requests are pending." 166 return 167 168 for index, (uid, recurrenceid) in enumerate(requests): 169 obj = store.get_event(user, uid, recurrenceid) 170 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 171 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 172 173 def show_attendee(attendee_item, index): 174 175 "Show the 'attendee_item' (value and attributes) at 'index'." 176 177 attendee, attr = attendee_item 178 partstat = attr.get("PARTSTAT") 179 print "(%d) %s%s" % (index, attendee, partstat and " (%s)" % partstat or "") 180 181 def show_attendees_raw(attendee_map): 182 183 "Show the 'attendee_map' in a simple raw form." 184 185 for attendee, attr in attendee_map.items(): 186 print attendee 187 188 def show_periods(periods, errors=None): 189 190 "Show 'periods' with any indicated 'errors'." 191 192 main = get_main_period(periods) 193 if main: 194 show_period(main, 0, errors) 195 196 recurrences = get_recurrence_periods(periods) 197 if recurrences: 198 print 199 print_title("Recurrences") 200 for index, p in enumerate(recurrences): 201 show_period(p, index + 1, errors) 202 203 def show_period(p, index, errors=None): 204 205 "Show period 'p' at 'index' with any indicated 'errors'." 206 207 errors = errors and errors.get(index) 208 if p.replacement: 209 if p.cancelled: 210 label = "Cancelled" 211 else: 212 label = "Replaced" 213 else: 214 if p.new_replacement: 215 label = "To replace" 216 elif p.recurrenceid: 217 label = "Retained" 218 else: 219 label = "New" 220 221 error_label = errors and " (errors: %s)" % ", ".join(errors) or "" 222 print "(%d) %s%s:" % (index, label, error_label), p.get_start(), p.get_end(), p.origin 223 224 def show_periods_raw(periods): 225 226 "Show 'periods' in a simple raw form." 227 228 periods = periods[:] 229 periods.sort() 230 map(show_period_raw, periods) 231 232 def show_period_raw(p): 233 234 "Show period 'p' in a simple raw form." 235 236 print p.get_start(), p.get_end(), p.origin 237 238 def show_rule(rrule): 239 240 "Show recurrence rule specification 'rrule'." 241 242 print 243 print "Recurrence rule:" 244 245 count = None 246 freq_interval = [] 247 selections = [] 248 249 # Collect limit, selection and frequency details. 250 251 for selector in vRecurrence.order_qualifiers(vRecurrence.get_qualifiers(rrule)): 252 253 # COUNT 254 255 if isinstance(selector, vRecurrence.LimitSelector): 256 count = selector.args["values"][0] 257 258 # BYSETPOS 259 260 elif isinstance(selector, vRecurrence.PositionSelector): 261 for value in selector.args["values"]: 262 selections.append(("-", get_frequency(selector.level), str(value))) 263 264 # BY... 265 266 elif isinstance(selector, vRecurrence.Enum): 267 for value in selector.args["values"]: 268 selections.append(("-", get_frequency(selector.level), str(value))) 269 270 # BYWEEKDAY 271 272 elif isinstance(selector, vRecurrence.WeekDayFilter): 273 for value, index in selector.args["values"]: 274 selections.append((index >= 0 and "Start" or "End", get_weekday(value), str(index))) 275 276 # YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY 277 278 elif isinstance(selector, vRecurrence.Pattern): 279 freq_interval.append((get_frequency(selector.level), str(selector.args.get("interval", 1)))) 280 281 # Show the details. 282 283 if freq_interval: 284 print 285 print_table([("Frequency", "Interval")] + freq_interval) 286 287 if selections: 288 print 289 print_table([("From...", "Selecting", "Instance (1, 2, ...)")] + selections) 290 291 if count: 292 print 293 print "At most", count, "occurrences." 294 295 def get_frequency(level): 296 levels = ["Year", "Month", "Week", None, None, "Day", "Hour", "Minute", "Second"] 297 return levels[level] 298 299 def get_weekday(weekday): 300 weekdays = [None, "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] 301 return weekdays[weekday] 302 303 def show_attendee_changes(new, modified, unmodified, removed): 304 305 "Show 'new', 'modified', 'unmodified' and 'removed' periods." 306 307 print 308 print_title("Changes to attendees") 309 print 310 print "New:" 311 show_attendees_raw(new) 312 print 313 print "Modified:" 314 show_attendees_raw(modified) 315 print 316 print "Unmodified:" 317 show_attendees_raw(unmodified) 318 print 319 print "Removed:" 320 show_attendees_raw(removed) 321 322 def show_period_classification(new, replaced, retained, cancelled, obsolete): 323 324 "Show 'new', 'replaced', 'retained', 'cancelled' and 'obsolete' periods." 325 326 print 327 print_title("Period classification") 328 print 329 print "New:" 330 show_periods_raw(new) 331 print 332 print "Replaced:" 333 show_periods_raw(replaced) 334 print 335 print "Retained:" 336 show_periods_raw(retained) 337 print 338 print "Cancelled:" 339 show_periods_raw(cancelled) 340 print 341 print "Obsolete:" 342 show_periods_raw(obsolete) 343 344 def show_changes(modified, unmodified, removed): 345 346 "Show 'modified', 'unmodified' and 'removed' periods." 347 348 print 349 print_title("Changes to periods") 350 print 351 print "Modified:" 352 show_periods_raw(modified) 353 print 354 print "Unmodified:" 355 show_periods_raw(unmodified) 356 print 357 print "Removed:" 358 show_periods_raw(removed) 359 360 def show_attendee_operations(to_invite, to_cancel, to_modify): 361 362 "Show attendees 'to_invite', 'to_cancel' and 'to_modify'." 363 364 print 365 print_title("Attendee update operations") 366 print 367 print "To invite:" 368 show_attendees_raw(to_invite) 369 print 370 print "To cancel:" 371 show_attendees_raw(to_cancel) 372 print 373 print "To modify:" 374 show_attendees_raw(to_modify) 375 376 def show_period_operations(to_unschedule, to_reschedule, to_add, to_exclude, to_set, 377 all_unscheduled, all_rescheduled): 378 379 """ 380 Show operations for periods 'to_unschedule', 'to_reschedule', 'to_add', 381 'to_exclude' and 'to_set' (for updating other calendar participants), and 382 for periods 'all_unscheduled' and 'all_rescheduled' (for publishing event 383 state). 384 """ 385 386 print 387 print_title("Period update and publishing operations") 388 print 389 print "Unschedule:" 390 show_periods_raw(to_unschedule) 391 print 392 print "Reschedule:" 393 show_periods_raw(to_reschedule) 394 print 395 print "Added:" 396 show_periods_raw(to_add) 397 print 398 print "Excluded:" 399 show_periods_raw(to_exclude) 400 print 401 print "Set in object:" 402 show_periods_raw(to_set) 403 print 404 print "All unscheduled:" 405 show_periods_raw(all_unscheduled) 406 print 407 print "All rescheduled:" 408 show_periods_raw(all_rescheduled) 409 410 class TextClient(EditingClient): 411 412 "Simple client with textual output." 413 414 def new_object(self): 415 416 "Create a new object with the current time." 417 418 utcnow = get_time() 419 now = to_timezone(utcnow, self.get_tzid()) 420 obj = EditingClient.new_object(self, "VEVENT") 421 obj.set_value("SUMMARY", "New event") 422 obj["DTSTART"] = [get_datetime_item(now)] 423 obj["DTEND"] = [get_datetime_item(now)] 424 return obj 425 426 # Editing methods involving interaction. 427 428 def edit_attendee(self, index): 429 430 "Edit the attendee at 'index'." 431 432 t = self.can_edit_attendee(index) 433 if t: 434 attendees = self.state.get("attendees") 435 attendee, attr = t 436 del attendees[attendee] 437 attendee = input_with_default("Attendee (%s)? ", attendee) 438 attendees[attendee] = attr 439 440 def edit_period(self, index, args=None): 441 period = self.can_edit_period(index) 442 if period: 443 edit_period(period, args) 444 period.cancelled = False 445 period.origin = "DTSTART-RECUR" 446 447 # Sort the periods after this change. 448 449 periods = self.state.get("periods") 450 periods.sort() 451 452 def edit_summary(self, summary=None): 453 if self.can_edit_properties(): 454 if not summary: 455 summary = input_with_default("Summary (%s)? ", self.state.get("summary")) 456 self.state.set("summary", summary) 457 458 def finish(self): 459 try: 460 EditingClient.finish(self) 461 except PeriodError: 462 print "Errors exist in the periods." 463 return 464 465 # Diagnostic methods. 466 467 def show_period_classification(self): 468 try: 469 new, replaced, retained, cancelled, obsolete = self.classify_periods() 470 show_period_classification(new, replaced, retained, cancelled, obsolete) 471 except PeriodError: 472 print 473 print "Errors exist in the periods." 474 475 def show_changes(self): 476 try: 477 modified, unmodified, removed = self.classify_period_changes() 478 show_changes(modified, unmodified, removed) 479 except PeriodError: 480 print "Errors exist in the periods." 481 482 is_changed = self.properties_changed() 483 if is_changed: 484 print 485 print "Properties changed:", ", ".join(is_changed) 486 new, modified, unmodified, removed = self.classify_attendee_changes() 487 show_attendee_changes(new, modified, unmodified, removed) 488 489 def show_operations(self): 490 is_changed = self.properties_changed() 491 492 try: 493 to_unschedule, to_reschedule, to_add, to_exclude, to_set, \ 494 all_unscheduled, all_rescheduled = self.classify_period_operations() 495 show_period_operations(to_unschedule, to_reschedule, to_add, 496 to_exclude, to_set, 497 all_unscheduled, all_rescheduled) 498 except PeriodError: 499 print "Errors exist in the periods." 500 501 to_invite, to_cancel, to_modify = self.classify_attendee_operations() 502 show_attendee_operations(to_invite, to_cancel, to_modify) 503 504 # Output methods. 505 506 def show_message(self, message, plain=False, filename=None): 507 if plain: 508 decode_part(message) 509 write(message_as_string(message), filename) 510 511 def show_cancel_message(self, plain=False, filename=None): 512 513 "Show the cancel message for uninvited attendees." 514 515 message = self.prepare_cancel_message() 516 if message: 517 self.show_message(message, plain, filename) 518 519 def show_publish_message(self, plain=False, filename=None): 520 521 "Show the publishing message for the updated event." 522 523 message = self.prepare_publish_message() 524 self.show_message(message, plain, filename) 525 526 def show_update_message(self, plain=False, filename=None): 527 528 "Show the update message for the updated event." 529 530 message = self.prepare_update_message() 531 if message: 532 self.show_message(message, plain, filename) 533 534 # General display methods. 535 536 def show_object(self): 537 print 538 print_title("Object details") 539 print 540 print "Summary:", self.state.get("summary") 541 print 542 print "Organiser:", self.state.get("organiser") 543 self.show_attendees() 544 self.show_periods() 545 self.show_suggested_attendees() 546 self.show_suggested_periods() 547 self.show_conflicting_periods() 548 549 def show_attendees(self): 550 print 551 print_title("Attendees") 552 attendees = self.state.get("attendees") 553 for index, attendee_item in enumerate(attendees.items()): 554 show_attendee(attendee_item, index) 555 556 def show_periods(self): 557 print 558 print_title("Periods") 559 rrule = self.obj.get_value("RRULE") 560 show_periods(self.state.get("periods"), self.state.get("period_errors")) 561 if rrule: 562 show_rule(rrule) 563 564 def show_suggested_attendees(self): 565 current_attendee = None 566 for index, (attendee, suggested_item) in enumerate(self.state.get("suggested_attendees")): 567 if attendee != current_attendee: 568 print 569 print_title("Attendees suggested by %s" % attendee) 570 current_attendee = attendee 571 show_attendee(suggested_item, index) 572 573 def show_suggested_periods(self): 574 periods = self.state.get("suggested_periods") 575 current_attendee = None 576 index = 0 577 for attendee, period, operation in periods: 578 if attendee != current_attendee: 579 print 580 print_title("Periods suggested by %s" % attendee) 581 current_attendee = attendee 582 show_period(period, index) 583 print " %s" % (operation == "add" and "Add this period" or "Remove this period") 584 index += 1 585 586 def show_conflicting_periods(self): 587 conflicts = self.get_conflicting_periods() 588 if not conflicts: 589 return 590 print 591 print_title("Conflicting periods") 592 593 conflicts = list(conflicts) 594 conflicts.sort() 595 596 for p in conflicts: 597 print p.summary, p.uid, p.get_start(), p.get_end() 598 599 # Interaction functions. 600 601 def expand_arg(args): 602 if args[0] and args[0][1:].isdigit(): 603 args[:1] = [args[0][0], args[0][1:]] 604 605 def get_filename_arg(cmd): 606 return (cmd.split()[1:] or [None])[0] 607 608 def next_arg(args): 609 if args: 610 arg = args[0] 611 del args[0] 612 return arg 613 return None 614 615 def edit_period(period, args=None): 616 617 "Edit the given 'period'." 618 619 print "Editing start (%s)" % period.get_start() 620 edit_date(period.start, args) 621 print "Editing end (%s)" % period.get_end() 622 edit_date(period.end, args) 623 624 def edit_date(date, args=None): 625 626 "Edit the given 'date' object attributes." 627 628 date.date = next_arg(args) or input_with_default("Date (%s)? ", date.date) 629 date.hour = next_arg(args) or input_with_default("Hour (%s)? ", date.hour) 630 date.minute = next_arg(args) or input_with_default("Minute (%s)? ", date.minute) 631 date.second = next_arg(args) or input_with_default("Second (%s)? ", date.second) 632 date.tzid = next_arg(args) or input_with_default("Time zone (%s)? ", date.tzid) 633 date.reset() 634 635 def select_object(cl, objects): 636 print 637 638 if objects: 639 label = "Select object number or (n)ew object or (q)uit> " 640 else: 641 label = "Select (n)ew object or (q)uit> " 642 643 while True: 644 try: 645 cmd = read_input(label) 646 except EOFError: 647 return None 648 649 if cmd.isdigit(): 650 index = int(cmd) 651 if 0 <= index < len(objects): 652 obj = objects[index] 653 return cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 654 655 elif cmd in ("n", "new"): 656 return cl.new_object() 657 elif cmd in ("q", "quit", "exit"): 658 return None 659 660 def show_commands(): 661 print 662 print_title("Editing commands") 663 print 664 print """\ 665 a [ <uri> ] 666 attendee [ <uri> ] 667 Add attendee 668 669 A, attend, attendance 670 Change attendance/participation 671 672 a<digit> 673 attendee <digit> 674 Select attendee from list 675 676 as<digit> 677 Add suggested attendee from list 678 679 f, finish 680 Finish editing, confirming changes, proceeding to messaging 681 682 h, help, ? 683 Show this help message 684 685 l, list, show 686 List/show all event details 687 688 p, period 689 Add new period 690 691 p<digit> 692 period <digit> 693 Select period from list 694 695 ps<digit> 696 Add or remove suggested period from list 697 698 q, quit, exit 699 Exit/quit this program 700 701 r, reload, reset, restart 702 Reset event periods (return to editing mode, if already finished) 703 704 rrule <rule specification> 705 Set a recurrence rule in the event, applying to the main period 706 707 s, summary 708 Set event summary 709 """ 710 711 print_title("Messaging commands") 712 print 713 print """\ 714 S, send 715 Send messages to recipients and to self, if appropriate 716 """ 717 718 print_title("Diagnostic commands") 719 print 720 print """\ 721 c, class, classification 722 Show period classification 723 724 C, changes 725 Show changes made by editing 726 727 o, ops, operations 728 Show update operations 729 730 RECURRENCE-ID [ <filename> ] 731 Show event recurrence identifier, writing to <filename> if specified 732 733 UID [ <filename> ] 734 Show event unique identifier, writing to <filename> if specified 735 """ 736 737 print_title("Message inspection commands") 738 print 739 print """\ 740 P [ <filename> ] 741 publish [ <filename> ] 742 Show publishing message, writing to <filename> if specified 743 744 R [ <filename> ] 745 remove [ <filename> ] 746 cancel [ <filename> ] 747 Show cancellation message sent to uninvited/removed recipients, writing to 748 <filename> if specified 749 750 U [ <filename> ] 751 update [ <filename> ] 752 Show update message, writing to <filename> if specified 753 """ 754 755 def edit_object(cl, obj, handle_outgoing=False): 756 cl.show_object() 757 print 758 759 try: 760 while True: 761 role = cl.is_organiser() and "Organiser" or "Attendee" 762 status = cl.state.get("finished") and " (editing complete)" or "" 763 764 cmd = read_input("%s%s> " % (role, status)) 765 766 args = cmd.split() 767 768 if not args or not args[0]: 769 continue 770 771 # Check the status of the periods. 772 773 if cmd in ("c", "class", "classification"): 774 cl.show_period_classification() 775 print 776 777 elif cmd in ("C", "changes"): 778 cl.show_changes() 779 print 780 781 # Finish editing. 782 783 elif cmd in ("f", "finish"): 784 cl.finish() 785 786 # Help. 787 788 elif cmd in ("h", "?", "help"): 789 show_commands() 790 791 # Show object details. 792 793 elif cmd in ("l", "list", "show"): 794 cl.show_object() 795 print 796 797 # Show the operations. 798 799 elif cmd in ("o", "ops", "operations"): 800 cl.show_operations() 801 print 802 803 # Quit or exit. 804 805 elif cmd in ("q", "quit", "exit"): 806 break 807 808 # Restart editing. 809 810 elif cmd in ("r", "reload", "reset", "restart"): 811 obj = cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 812 if not obj: 813 obj = cl.new_object() 814 cl.reset() 815 cl.show_object() 816 print 817 818 # Show UID details. 819 820 elif args[0] == "UID": 821 filename = get_filename_arg(cmd) 822 write(obj.get_uid(), filename) 823 824 elif args[0] == "RECURRENCE-ID": 825 filename = get_filename_arg(cmd) 826 write(obj.get_recurrenceid() or "", filename) 827 828 # Post-editing operations. 829 830 elif cl.state.get("finished"): 831 832 # Show messages. 833 834 if args[0] in ("P", "publish"): 835 filename = get_filename_arg(cmd) 836 cl.show_publish_message(plain=not filename, filename=filename) 837 838 elif args[0] in ("R", "remove", "cancel"): 839 filename = get_filename_arg(cmd) 840 cl.show_cancel_message(plain=not filename, filename=filename) 841 842 elif args[0] in ("U", "update"): 843 filename = get_filename_arg(cmd) 844 cl.show_update_message(plain=not filename, filename=filename) 845 846 # Definitive finishing action. 847 848 elif args[0] in ("S", "send"): 849 850 # Send update and cancellation messages. 851 852 did_send = False 853 854 message = cl.prepare_update_message() 855 if message: 856 cl.send_message(message, cl.get_recipients()) 857 did_send = True 858 859 to_cancel = cl.state.get("attendees_to_cancel") 860 if to_cancel: 861 message = cl.prepare_cancel_message() 862 if message: 863 cl.send_message(message, to_cancel) 864 did_send = True 865 866 # Process the object using the person outgoing handler. 867 868 if handle_outgoing: 869 870 # Handle the parent object plus any rescheduled periods. 871 872 unscheduled_objects, rescheduled_objects, added_objects = \ 873 cl.get_publish_objects() 874 875 handlers = get_handlers(cl, person_outgoing.handlers, 876 [get_address(cl.user)]) 877 878 handle_calendar_object(cl.obj, handlers, "PUBLISH") 879 for o in unscheduled_objects: 880 handle_calendar_object(o, handlers, "CANCEL") 881 for o in rescheduled_objects: 882 handle_calendar_object(o, handlers, "PUBLISH") 883 for o in added_objects: 884 handle_calendar_object(o, handlers, "ADD") 885 886 # Otherwise, send a message to self with the event details. 887 888 else: 889 message = cl.prepare_publish_message() 890 if message: 891 cl.send_message_to_self(message) 892 did_send = True 893 894 # Exit if sending occurred. 895 896 if did_send: 897 break 898 else: 899 print "No messages sent. Try making edits or exit manually." 900 901 # Editing operations. 902 903 elif not cl.state.get("finished"): 904 905 # Expand short-form arguments. 906 907 expand_arg(args) 908 909 # Add or edit attendee. 910 911 if args[0] in ("a", "attendee"): 912 913 args = args[1:] 914 value = next_arg(args) 915 916 if value and value.isdigit(): 917 index = int(value) 918 else: 919 try: 920 index = cl.find_attendee(value) 921 except ValueError: 922 index = None 923 924 # Add an attendee. 925 926 if index is None: 927 cl.add_attendee(value) 928 if not value: 929 cl.edit_attendee(-1) 930 931 # Edit attendee (using index). 932 933 else: 934 attendee_item = cl.can_remove_attendee(index) 935 if attendee_item: 936 while True: 937 show_attendee(attendee_item, index) 938 939 # Obtain a command from any arguments. 940 941 cmd = next_arg(args) 942 if not cmd: 943 cmd = read_input(" (e)dit, (r)emove (or return)> ") 944 if cmd in ("e", "edit"): 945 cl.edit_attendee(index) 946 elif cmd in ("r", "remove"): 947 cl.remove_attendees([index]) 948 elif not cmd: 949 pass 950 else: 951 continue 952 break 953 954 cl.show_attendees() 955 print 956 957 # Add suggested attendee (using index). 958 959 elif args[0] in ("as", "attendee-suggested", "suggested-attendee"): 960 try: 961 index = int(args[1]) 962 cl.add_suggested_attendee(index) 963 except ValueError: 964 pass 965 cl.show_attendees() 966 print 967 968 # Edit attendance. 969 970 elif args[0] in ("A", "attend", "attendance"): 971 972 args = args[1:] 973 974 if not cl.is_attendee() and cl.is_organiser(): 975 cl.add_attendee(cl.user) 976 977 # NOTE: Support delegation. 978 979 if cl.can_edit_attendance(): 980 while True: 981 982 # Obtain a command from any arguments. 983 984 cmd = next_arg(args) 985 if not cmd: 986 cmd = read_input(" (a)ccept, (d)ecline, (t)entative (or return)> ") 987 if cmd in ("a", "accept", "accepted", "attend"): 988 cl.edit_attendance("ACCEPTED") 989 elif cmd in ("d", "decline", "declined"): 990 cl.edit_attendance("DECLINED") 991 elif cmd in ("t", "tentative"): 992 cl.edit_attendance("TENTATIVE") 993 elif not cmd: 994 pass 995 else: 996 continue 997 break 998 999 cl.show_attendees() 1000 print 1001 1002 # Add or edit period. 1003 1004 elif args[0] in ("p", "period"): 1005 1006 args = args[1:] 1007 value = next_arg(args) 1008 1009 if value and value.isdigit(): 1010 index = int(value) 1011 else: 1012 index = None 1013 1014 # Add a new period. 1015 1016 if index is None: 1017 cl.add_period() 1018 cl.edit_period(-1) 1019 1020 # Edit period (using index). 1021 1022 else: 1023 period = cl.can_edit_period(index) 1024 if period: 1025 while True: 1026 show_period_raw(period) 1027 1028 # Obtain a command from any arguments. 1029 1030 cmd = next_arg(args) 1031 if not cmd: 1032 cmd = read_input(" (e)dit, (c)ancel, (u)ncancel (or return)> ") 1033 if cmd in ("e", "edit"): 1034 cl.edit_period(index, args) 1035 elif cmd in ("c", "cancel"): 1036 cl.cancel_periods([index]) 1037 elif cmd in ("u", "uncancel", "restore"): 1038 cl.cancel_periods([index], False) 1039 elif not cmd: 1040 pass 1041 else: 1042 continue 1043 break 1044 1045 cl.show_periods() 1046 print 1047 1048 # Apply suggested period (using index). 1049 1050 elif args[0] in ("ps", "period-suggested", "suggested-period"): 1051 try: 1052 index = int(args[1]) 1053 cl.apply_suggested_period(index) 1054 except ValueError: 1055 pass 1056 cl.show_periods() 1057 print 1058 1059 # Specify a recurrence rule. 1060 1061 elif args[0] == "rrule": 1062 pass 1063 1064 # Set the summary. 1065 1066 elif args[0] in ("s", "summary"): 1067 t = cmd.split(None, 1) 1068 cl.edit_summary(len(t) > 1 and t[1] or None) 1069 cl.show_object() 1070 print 1071 1072 except EOFError: 1073 return 1074 1075 def main(args): 1076 global echo 1077 1078 if "--help" in args: 1079 show_help(os.path.split(sys.argv[0])[-1]) 1080 return 1081 1082 # Parse command line arguments using the standard options plus some extra 1083 # options. 1084 1085 args = parse_args(args, { 1086 "--calendar-data" : ("calendar_data", False), 1087 "--charset" : ("charset", "utf-8"), 1088 "--echo" : ("echo", False), 1089 "-f" : ("filename", None), 1090 "--handle-data" : ("handle_data", False), 1091 "--suppress-bcc" : ("suppress_bcc", False), 1092 "-u" : ("user", None), 1093 }) 1094 1095 charset = args["charset"] 1096 calendar_data = args["calendar_data"] 1097 echo = args["echo"] 1098 filename = args["filename"] 1099 handle_data = args["handle_data"] 1100 sender = (args["senders"] or [None])[0] 1101 suppress_bcc = args["suppress_bcc"] 1102 user = args["user"] 1103 1104 # Determine the user and sender identities. 1105 1106 if sender and not user: 1107 user = get_uri(sender) 1108 elif user and not sender: 1109 sender = get_address(user) 1110 elif not sender and not user: 1111 print >>sys.stderr, "A sender or a user must be specified." 1112 sys.exit(1) 1113 1114 # Open a store. 1115 1116 store_type = args.get("store_type") 1117 store_dir = args.get("store_dir") 1118 preferences_dir = args.get("preferences_dir") 1119 1120 store = get_store(store_type, store_dir) 1121 journal = None 1122 1123 # Open a messenger for the user. 1124 1125 messenger = Messenger(sender=sender, suppress_bcc=suppress_bcc) 1126 1127 # Open a client for the user. 1128 1129 cl = TextClient(user, messenger, store, journal, preferences_dir) 1130 1131 # Read any input resource. 1132 1133 if filename: 1134 if calendar_data: 1135 all_itip = get_itip_from_data(filename, charset) 1136 else: 1137 all_itip = get_itip_from_message(filename) 1138 1139 objects = [] 1140 1141 # Process the objects using the person handler. 1142 1143 if handle_data: 1144 for itip in all_itip: 1145 objects += handle_calendar_data(itip, get_handlers(cl, person.handlers, None)) 1146 1147 # Or just obtain objects from the data. 1148 1149 else: 1150 for itip in all_itip: 1151 objects += get_objects_from_itip(itip, ["VEVENT"]) 1152 1153 # Choose an object to edit. 1154 1155 show_objects(objects, user, store) 1156 obj = select_object(cl, objects) 1157 1158 # Exit without any object. 1159 1160 if not obj: 1161 print >>sys.stderr, "No object loaded." 1162 sys.exit(1) 1163 1164 # Or create a new object. 1165 1166 else: 1167 obj = cl.new_object() 1168 1169 # Edit the object. 1170 1171 edit_object(cl, obj, handle_outgoing=handle_data) 1172 1173 def show_help(progname): 1174 print >>sys.stderr, help_text % progname 1175 1176 help_text = """\ 1177 Usage: %s -s <sender> | -u <user> \\ 1178 [ -f <filename> ] \\ 1179 [ --calendar-data --charset ] \\ 1180 [ --handle-data ] \\ 1181 [ -T <store type ] [ -S <store directory> ] \\ 1182 [ -p <preferences directory> ] \\ 1183 [ --echo ] 1184 1185 Identity options: 1186 1187 -s Indicate the user by specifying a sender address 1188 -u Indicate the user by specifying their URI 1189 1190 Input options: 1191 1192 -f Indicates a filename containing a MIME-encoded message or calendar object 1193 1194 --calendar-data Indicates that the specified file contains a calendar object 1195 as opposed to a mail message 1196 --charset Specifies the character encoding used by a calendar object 1197 description 1198 1199 Processing options: 1200 1201 --handle-data Cause the input to be handled and stored in the configured 1202 data store 1203 1204 Configuration options (overriding configured defaults): 1205 1206 -p Indicates the location of user preference directories 1207 -S Indicates the location of the calendar data store containing user storage 1208 directories 1209 -T Indicates the store type (the configured value if omitted) 1210 1211 Output options: 1212 1213 --echo Echo received input, useful if consuming input from the 1214 standard input stream and producing a log of the program's 1215 activity 1216 """ 1217 1218 if __name__ == "__main__": 1219 main(sys.argv[1:]) 1220 1221 # vim: tabstop=4 expandtab shiftwidth=4