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