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