1 #!/usr/bin/env python 2 3 from email import message_from_file 4 from imiptools import parse_args 5 from imiptools.client import ClientForObject 6 from imiptools.config import settings 7 from imiptools.content import get_objects_from_itip, have_itip_part, parse_itip_part 8 from imiptools.data import get_address, get_main_period, get_recurrence_periods, get_value 9 from imiptools.dates import get_datetime_item, get_time, to_timezone 10 from imiptools.editing import EditingClient, PeriodError 11 from imiptools.mail import Messenger 12 from imiptools.stores import get_journal, get_store 13 from imiptools.utils import decode_part, message_as_string 14 import sys 15 16 # User interface functions. 17 18 echo = False 19 20 def read_input(label): 21 s = raw_input(label).strip() 22 if echo: 23 print s 24 return s 25 26 def input_with_default(label, default): 27 return read_input(label % default) or default 28 29 def print_title(text): 30 print text 31 print len(text) * "-" 32 33 def write(s, filename): 34 f = filename and open(filename, "w") or None 35 try: 36 print >>(f or sys.stdout), s 37 finally: 38 if f: 39 f.close() 40 41 # Interpret an input file containing a calendar resource. 42 43 def get_objects(filename): 44 45 "Return objects provided by 'filename'." 46 47 f = open(filename) 48 try: 49 msg = message_from_file(f) 50 finally: 51 f.close() 52 53 all_objects = [] 54 55 for part in msg.walk(): 56 itip = parse_itip_part(part) 57 method = itip and get_value(itip, "METHOD") 58 59 # Ignore cancelled objects since only active objects are of interest. 60 61 if method != "CANCEL": 62 all_objects += get_objects_from_itip(itip, ["VEVENT"]) 63 64 return all_objects 65 66 def show_objects(objects, user, store): 67 68 """ 69 Show details of 'objects', accessed by the given 'user' in the given 70 'store'. 71 """ 72 73 for index, obj in enumerate(objects): 74 recurrenceid = obj.get_recurrenceid() 75 recurrence_label = recurrenceid and " %s" % recurrenceid or "" 76 print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) 77 78 def show_attendee(attendee_item, index): 79 80 "Show the 'attendee_item' (value and attributes) at 'index'." 81 82 attendee, attr = attendee_item 83 partstat = attr.get("PARTSTAT") 84 print "(%d) %s%s" % (index, attendee, partstat and " (%s)" % partstat or "") 85 86 def show_attendees_raw(attendee_map): 87 88 "Show the 'attendee_map' in a simple raw form." 89 90 for attendee, attr in attendee_map.items(): 91 print attendee 92 93 def show_periods(periods, errors=None): 94 95 "Show 'periods' with any indicated 'errors'." 96 97 main = get_main_period(periods) 98 if main: 99 show_period(main, 0, errors) 100 101 recurrences = get_recurrence_periods(periods) 102 if recurrences: 103 print 104 print_title("Recurrences") 105 for index, p in enumerate(recurrences): 106 show_period(p, index + 1, errors) 107 108 def show_period(p, index, errors=None): 109 110 "Show period 'p' at 'index' with any indicated 'errors'." 111 112 errors = errors and errors.get(index) 113 if p.replacement: 114 if p.cancelled: 115 label = "Cancelled" 116 else: 117 label = "Replaced" 118 else: 119 if p.new_replacement: 120 label = "To replace" 121 elif p.recurrenceid: 122 label = "Retained" 123 else: 124 label = "New" 125 126 error_label = errors and " (errors: %s)" % ", ".join(errors) or "" 127 print "(%d) %s%s:" % (index, label, error_label), p.get_start(), p.get_end(), p.origin 128 129 def show_periods_raw(periods): 130 131 "Show 'periods' in a simple raw form." 132 133 periods = periods[:] 134 periods.sort() 135 map(show_period_raw, periods) 136 137 def show_period_raw(p): 138 139 "Show period 'p' in a simple raw form." 140 141 print p.get_start(), p.get_end(), p.origin 142 143 def show_attendee_changes(new, modified, unmodified, removed): 144 145 "Show 'new', 'modified', 'unmodified' and 'removed' periods." 146 147 print 148 print_title("Changes to attendees") 149 print 150 print "New:" 151 show_attendees_raw(new) 152 print 153 print "Modified:" 154 show_attendees_raw(modified) 155 print 156 print "Unmodified:" 157 show_attendees_raw(unmodified) 158 print 159 print "Removed:" 160 show_attendees_raw(removed) 161 162 def show_period_classification(new, replaced, retained, cancelled, obsolete): 163 164 "Show 'new', 'replaced', 'retained', 'cancelled' and 'obsolete' periods." 165 166 print 167 print_title("Period classification") 168 print 169 print "New:" 170 show_periods_raw(new) 171 print 172 print "Replaced:" 173 show_periods_raw(replaced) 174 print 175 print "Retained:" 176 show_periods_raw(retained) 177 print 178 print "Cancelled:" 179 show_periods_raw(cancelled) 180 print 181 print "Obsolete:" 182 show_periods_raw(obsolete) 183 184 def show_changes(modified, unmodified, removed): 185 186 "Show 'modified', 'unmodified' and 'removed' periods." 187 188 print 189 print_title("Changes to periods") 190 print 191 print "Modified:" 192 show_periods_raw(modified) 193 print 194 print "Unmodified:" 195 show_periods_raw(unmodified) 196 print 197 print "Removed:" 198 show_periods_raw(removed) 199 200 def show_attendee_operations(to_invite, to_cancel, to_modify): 201 202 "Show attendees 'to_invite', 'to_cancel' and 'to_modify'." 203 204 print 205 print_title("Attendee update operations") 206 print 207 print "To invite:" 208 show_attendees_raw(to_invite) 209 print 210 print "To cancel:" 211 show_attendees_raw(to_cancel) 212 print 213 print "To modify:" 214 show_attendees_raw(to_modify) 215 216 def show_period_operations(to_unschedule, to_reschedule, to_add, to_exclude, to_set, 217 all_unscheduled, all_rescheduled): 218 219 """ 220 Show operations for periods 'to_unschedule', 'to_reschedule', 'to_add', 221 'to_exclude' and 'to_set' (for updating other calendar participants), and 222 for periods 'all_unscheduled' and 'all_rescheduled' (for publishing event 223 state). 224 """ 225 226 print 227 print_title("Period update and publishing operations") 228 print 229 print "Unschedule:" 230 show_periods_raw(to_unschedule) 231 print 232 print "Reschedule:" 233 show_periods_raw(to_reschedule) 234 print 235 print "Added:" 236 show_periods_raw(to_add) 237 print 238 print "Excluded:" 239 show_periods_raw(to_exclude) 240 print 241 print "Set in object:" 242 show_periods_raw(to_set) 243 print 244 print "All unscheduled:" 245 show_periods_raw(all_unscheduled) 246 print 247 print "All rescheduled:" 248 show_periods_raw(all_rescheduled) 249 250 class TextClient(EditingClient): 251 252 "Simple client with textual output." 253 254 def new_object(self): 255 256 "Create a new object with the current time." 257 258 utcnow = get_time() 259 now = to_timezone(utcnow, self.get_tzid()) 260 obj = EditingClient.new_object(self, "VEVENT") 261 obj.set_value("SUMMARY", "New event") 262 obj["DTSTART"] = [get_datetime_item(now)] 263 obj["DTEND"] = [get_datetime_item(now)] 264 return obj 265 266 # Editing methods involving interaction. 267 268 def edit_attendee(self, index): 269 270 "Edit the attendee at 'index'." 271 272 t = self.can_edit_attendee(index) 273 if t: 274 attendees = self.state.get("attendees") 275 attendee, attr = t 276 del attendees[attendee] 277 attendee = input_with_default("Attendee (%s)? ", attendee) 278 attendees[attendee] = attr 279 280 def edit_period(self, index, args=None): 281 period = self.can_edit_period(index) 282 if period: 283 edit_period(period, args) 284 period.cancelled = False 285 286 # Sort the periods after this change. 287 288 periods = self.state.get("periods") 289 periods.sort() 290 291 def edit_summary(self, summary=None): 292 if self.can_edit_properties(): 293 if not summary: 294 summary = input_with_default("Summary (%s)? ", self.state.get("summary")) 295 self.state.set("summary", summary) 296 297 def finish(self): 298 try: 299 EditingClient.finish(self) 300 except PeriodError: 301 print "Errors exist in the periods." 302 return 303 304 # Diagnostic methods. 305 306 def show_period_classification(self): 307 try: 308 new, replaced, retained, cancelled, obsolete = self.classify_periods() 309 show_period_classification(new, replaced, retained, cancelled, obsolete) 310 except PeriodError: 311 print 312 print "Errors exist in the periods." 313 314 def show_changes(self): 315 try: 316 modified, unmodified, removed = self.classify_period_changes() 317 show_changes(modified, unmodified, removed) 318 except PeriodError: 319 print "Errors exist in the periods." 320 321 is_changed = self.properties_changed() 322 if is_changed: 323 print 324 print "Properties changed:", ", ".join(is_changed) 325 new, modified, unmodified, removed = self.classify_attendee_changes() 326 show_attendee_changes(new, modified, unmodified, removed) 327 328 def show_operations(self): 329 is_changed = self.properties_changed() 330 331 try: 332 to_unschedule, to_reschedule, to_add, to_exclude, to_set, \ 333 all_unscheduled, all_rescheduled = self.classify_period_operations() 334 show_period_operations(to_unschedule, to_reschedule, to_add, 335 to_exclude, to_set, 336 all_unscheduled, all_rescheduled) 337 except PeriodError: 338 print "Errors exist in the periods." 339 340 to_invite, to_cancel, to_modify = self.classify_attendee_operations() 341 show_attendee_operations(to_invite, to_cancel, to_modify) 342 343 # Output methods. 344 345 def show_message(self, message, plain=False, filename=None): 346 if plain: 347 decode_part(message) 348 write(message_as_string(message), filename) 349 350 def show_cancel_message(self, plain=False, filename=None): 351 352 "Show the cancel message for uninvited attendees." 353 354 message = self.prepare_cancel_message() 355 if message: 356 self.show_message(message, plain, filename) 357 358 def show_publish_message(self, plain=False, filename=None): 359 360 "Show the publishing message for the updated event." 361 362 message = self.prepare_publish_message() 363 self.show_message(message, plain, filename) 364 365 def show_update_message(self, plain=False, filename=None): 366 367 "Show the update message for the updated event." 368 369 message = self.prepare_update_message() 370 if message: 371 self.show_message(message, plain, filename) 372 373 # General display methods. 374 375 def show_object(self): 376 print 377 print "Summary:", self.state.get("summary") 378 print 379 print "Organiser:", self.state.get("organiser") 380 self.show_attendees() 381 self.show_periods() 382 self.show_suggested_attendees() 383 self.show_suggested_periods() 384 self.show_conflicting_periods() 385 386 def show_attendees(self): 387 print 388 print_title("Attendees") 389 attendees = self.state.get("attendees") 390 for index, attendee_item in enumerate(attendees.items()): 391 show_attendee(attendee_item, index) 392 393 def show_periods(self): 394 print 395 print_title("Periods") 396 show_periods(self.state.get("periods"), self.state.get("period_errors")) 397 398 def show_suggested_attendees(self): 399 current_attendee = None 400 for index, (attendee, suggested_item) in enumerate(self.state.get("suggested_attendees")): 401 if attendee != current_attendee: 402 print 403 print_title("Attendees suggested by %s" % attendee) 404 current_attendee = attendee 405 show_attendee(suggested_item, index) 406 407 def show_suggested_periods(self): 408 periods = self.state.get("suggested_periods") 409 current_attendee = None 410 index = 0 411 for attendee, period, operation in periods: 412 if attendee != current_attendee: 413 print 414 print_title("Periods suggested by %s" % attendee) 415 current_attendee = attendee 416 show_period(period, index) 417 print " %s" % (operation == "add" and "Add this period" or "Remove this period") 418 index += 1 419 420 def show_conflicting_periods(self): 421 conflicts = self.get_conflicting_periods() 422 if not conflicts: 423 return 424 print 425 print_title("Conflicting periods") 426 427 conflicts = list(conflicts) 428 conflicts.sort() 429 430 for p in conflicts: 431 print p.summary, p.uid, p.get_start(), p.get_end() 432 433 # Interaction functions. 434 435 def expand_arg(args): 436 if args[0] and args[0][1:].isdigit(): 437 args[:1] = [args[0][0], args[0][1:]] 438 439 def get_filename_arg(cmd): 440 return (cmd.split()[1:] or [None])[0] 441 442 def next_arg(args): 443 if args: 444 arg = args[0] 445 del args[0] 446 return arg 447 return None 448 449 def edit_period(period, args=None): 450 451 "Edit the given 'period'." 452 453 print "Editing start (%s)" % period.get_start() 454 edit_date(period.start, args) 455 print "Editing end (%s)" % period.get_end() 456 edit_date(period.end, args) 457 458 def edit_date(date, args=None): 459 460 "Edit the given 'date' object attributes." 461 462 date.date = next_arg(args) or input_with_default("Date (%s)? ", date.date) 463 date.hour = next_arg(args) or input_with_default("Hour (%s)? ", date.hour) 464 date.minute = next_arg(args) or input_with_default("Minute (%s)? ", date.minute) 465 date.second = next_arg(args) or input_with_default("Second (%s)? ", date.second) 466 date.tzid = next_arg(args) or input_with_default("Time zone (%s)? ", date.tzid) 467 date.reset() 468 469 def select_object(cl, objects): 470 print 471 while True: 472 try: 473 cmd = read_input("Object or (n)ew object or (q)uit> ") 474 except EOFError: 475 return None 476 477 if cmd.isdigit(): 478 index = int(cmd) 479 if 0 <= index < len(objects): 480 obj = objects[index] 481 return cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 482 elif cmd in ("n", "new"): 483 return cl.new_object() 484 elif cmd in ("q", "quit", "exit"): 485 return None 486 487 def show_help(): 488 print 489 print_title("Editing commands") 490 print 491 print """\ 492 a [ <uri> ] 493 attendee [ <uri> ] 494 Add attendee 495 496 A, attend, attendance 497 Change attendance/participation 498 499 a<digit> 500 attendee <digit> 501 Select attendee from list 502 503 as<digit> 504 Add suggested attendee from list 505 506 f, finish 507 Finish editing, confirming changes, proceeding to messaging 508 509 h, help, ? 510 Show this help message 511 512 l, list, show 513 List/show all event details 514 515 p, period 516 Add new period 517 518 p<digit> 519 period <digit> 520 Select period from list 521 522 ps<digit> 523 Add or remove suggested period from list 524 525 q, quit, exit 526 Exit/quit this program 527 528 r, reload, reset, restart 529 Reset event periods (return to editing mode, if already finished) 530 531 s, summary 532 Set event summary 533 """ 534 535 print_title("Diagnostic commands") 536 print 537 print """\ 538 c, class, classification 539 Show period classification 540 541 C, changes 542 Show changes made by editing 543 544 o, ops, operations 545 Show update operations 546 547 RECURRENCE-ID [ <filename> ] 548 Show event recurrence identifier, writing to <filename> if specified 549 550 UID [ <filename> ] 551 Show event unique identifier, writing to <filename> if specified 552 """ 553 554 print_title("Messaging commands") 555 print 556 print """\ 557 P [ <filename> ] 558 publish [ <filename> ] 559 Show publishing message, writing to <filename> if specified 560 561 R [ <filename> ] 562 remove [ <filename> ] 563 cancel [ <filename> ] 564 Show cancellation message sent to uninvited/removed recipients, writing to 565 <filename> if specified 566 567 U [ <filename> ] 568 update [ <filename> ] 569 Show update message, writing to <filename> if specified 570 """ 571 572 def edit_object(cl, obj): 573 cl.show_object() 574 print 575 576 try: 577 while True: 578 role = cl.is_organiser() and "Organiser" or "Attendee" 579 status = cl.state.get("finished") and " (editing complete)" or "" 580 581 cmd = read_input("%s%s> " % (role, status)) 582 583 args = cmd.split() 584 585 if not args or not args[0]: 586 continue 587 588 # Check the status of the periods. 589 590 if cmd in ("c", "class", "classification"): 591 cl.show_period_classification() 592 print 593 594 elif cmd in ("C", "changes"): 595 cl.show_changes() 596 print 597 598 # Finish editing. 599 600 elif cmd in ("f", "finish"): 601 cl.finish() 602 603 # Help. 604 605 elif cmd in ("h", "?", "help"): 606 show_help() 607 608 # Show object details. 609 610 elif cmd in ("l", "list", "show"): 611 cl.show_object() 612 print 613 614 # Show the operations. 615 616 elif cmd in ("o", "ops", "operations"): 617 cl.show_operations() 618 print 619 620 # Quit or exit. 621 622 elif cmd in ("q", "quit", "exit"): 623 break 624 625 # Restart editing. 626 627 elif cmd in ("r", "reload", "reset", "restart"): 628 obj = cl.load_object(obj.get_uid(), obj.get_recurrenceid()) 629 if not obj: 630 obj = cl.new_object() 631 cl.reset() 632 cl.show_object() 633 print 634 635 # Show UID details. 636 637 elif args[0] == "UID": 638 filename = get_filename_arg(cmd) 639 write(obj.get_uid(), filename) 640 641 elif args[0] == "RECURRENCE-ID": 642 filename = get_filename_arg(cmd) 643 write(obj.get_recurrenceid() or "", filename) 644 645 # Post-editing operations. 646 647 elif cl.state.get("finished"): 648 649 # Show messages. 650 651 if args[0] in ("P", "publish"): 652 filename = get_filename_arg(cmd) 653 cl.show_publish_message(plain=not filename, filename=filename) 654 655 elif args[0] in ("R", "remove", "cancel"): 656 filename = get_filename_arg(cmd) 657 cl.show_cancel_message(plain=not filename, filename=filename) 658 659 elif args[0] in ("U", "update"): 660 filename = get_filename_arg(cmd) 661 cl.show_update_message(plain=not filename, filename=filename) 662 663 # Editing operations. 664 665 elif not cl.state.get("finished"): 666 667 # Expand short-form arguments. 668 669 expand_arg(args) 670 671 # Add or edit attendee. 672 673 if args[0] in ("a", "attendee"): 674 675 args = args[1:] 676 value = next_arg(args) 677 678 if value and value.isdigit(): 679 index = int(value) 680 else: 681 try: 682 index = cl.find_attendee(value) 683 except ValueError: 684 index = None 685 686 # Add an attendee. 687 688 if index is None: 689 cl.add_attendee(value) 690 if not value: 691 cl.edit_attendee(-1) 692 693 # Edit attendee (using index). 694 695 else: 696 attendee_item = cl.can_remove_attendee(index) 697 if attendee_item: 698 while True: 699 show_attendee(attendee_item, index) 700 701 # Obtain a command from any arguments. 702 703 cmd = next_arg(args) 704 if not cmd: 705 cmd = read_input(" (e)dit, (r)emove (or return)> ") 706 if cmd in ("e", "edit"): 707 cl.edit_attendee(index) 708 elif cmd in ("r", "remove"): 709 cl.remove_attendees([index]) 710 elif not cmd: 711 pass 712 else: 713 continue 714 break 715 716 cl.show_attendees() 717 print 718 719 # Add suggested attendee (using index). 720 721 elif args[0] in ("as", "attendee-suggested", "suggested-attendee"): 722 try: 723 index = int(args[1]) 724 cl.add_suggested_attendee(index) 725 except ValueError: 726 pass 727 cl.show_attendees() 728 print 729 730 # Edit attendance. 731 732 elif args[0] in ("A", "attend", "attendance"): 733 734 args = args[1:] 735 736 if not cl.is_attendee() and cl.is_organiser(): 737 cl.add_attendee(cl.user) 738 739 # NOTE: Support delegation. 740 741 if cl.can_edit_attendance(): 742 while True: 743 744 # Obtain a command from any arguments. 745 746 cmd = next_arg(args) 747 if not cmd: 748 cmd = read_input(" (a)ccept, (d)ecline, (t)entative (or return)> ") 749 if cmd in ("a", "accept", "accepted", "attend"): 750 cl.edit_attendance("ACCEPTED") 751 elif cmd in ("d", "decline", "declined"): 752 cl.edit_attendance("DECLINED") 753 elif cmd in ("t", "tentative"): 754 cl.edit_attendance("TENTATIVE") 755 elif not cmd: 756 pass 757 else: 758 continue 759 break 760 761 cl.show_attendees() 762 print 763 764 # Add or edit period. 765 766 elif args[0] in ("p", "period"): 767 768 args = args[1:] 769 value = next_arg(args) 770 771 if value and value.isdigit(): 772 index = int(value) 773 else: 774 index = None 775 776 # Add a new period. 777 778 if index is None: 779 cl.add_period() 780 cl.edit_period(-1) 781 782 # Edit period (using index). 783 784 else: 785 period = cl.can_edit_period(index) 786 if period: 787 while True: 788 show_period_raw(period) 789 790 # Obtain a command from any arguments. 791 792 cmd = next_arg(args) 793 if not cmd: 794 cmd = read_input(" (e)dit, (c)ancel, (u)ncancel (or return)> ") 795 if cmd in ("e", "edit"): 796 cl.edit_period(index, args) 797 elif cmd in ("c", "cancel"): 798 cl.cancel_periods([index]) 799 elif cmd in ("u", "uncancel", "restore"): 800 cl.cancel_periods([index], False) 801 elif not cmd: 802 pass 803 else: 804 continue 805 break 806 807 cl.show_periods() 808 print 809 810 # Apply suggested period (using index). 811 812 elif args[0] in ("ps", "period-suggested", "suggested-period"): 813 try: 814 index = int(args[1]) 815 cl.apply_suggested_period(index) 816 except ValueError: 817 pass 818 cl.show_periods() 819 print 820 821 # Set the summary. 822 823 elif args[0] in ("s", "summary"): 824 cl.edit_summary(cmd.split(None, 1)[1]) 825 cl.show_object() 826 print 827 828 except EOFError: 829 return 830 831 def main(args): 832 global echo 833 834 # Parse command line arguments using the standard options plus some extra 835 # options. 836 837 args = parse_args(args, { 838 "--echo" : ("echo", False), 839 "-f" : ("filename", None), 840 "--suppress-bcc" : ("suppress_bcc", False), 841 "-u" : ("user", None), 842 }) 843 844 echo = args["echo"] 845 filename = args["filename"] 846 sender = (args["senders"] or [None])[0] 847 suppress_bcc = args["suppress_bcc"] 848 user = args["user"] 849 850 # Determine the user and sender identities. 851 852 if sender and not user: 853 user = get_uri(sender) 854 elif user and not sender: 855 sender = get_address(user) 856 elif not sender and not user: 857 print >>sys.stderr, "A sender or a user must be specified." 858 sys.exit(1) 859 860 # Open a store. 861 862 store_type = args.get("store_type") 863 store_dir = args.get("store_dir") 864 preferences_dir = args.get("preferences_dir") 865 866 store = get_store(store_type, store_dir) 867 journal = None 868 869 # Open a messenger for the user. 870 871 messenger = Messenger(sender=sender, suppress_bcc=suppress_bcc) 872 873 # Open a client for the user. 874 875 cl = TextClient(user, messenger, store, journal, preferences_dir) 876 877 # Read any input resource. 878 879 if filename: 880 objects = get_objects(filename) 881 882 # Choose an object to edit. 883 884 show_objects(objects, user, store) 885 obj = select_object(cl, objects) 886 887 # Exit without any object. 888 889 if not obj: 890 print >>sys.stderr, "Object not loaded." 891 sys.exit(1) 892 893 # Or create a new object. 894 895 else: 896 obj = cl.new_object() 897 898 # Edit the object. 899 900 edit_object(cl, obj) 901 902 if __name__ == "__main__": 903 main(sys.argv[1:]) 904 905 # vim: tabstop=4 expandtab shiftwidth=4