# HG changeset patch # User Paul Boddie # Date 1509575218 -3600 # Node ID e5fa4163d214f07ba85f6153500a9b495f081ecd # Parent 462029973f84873331de4ac85bfcce7954af798b Added options to make the text client usable for integration with mail programs. Calendar objects can be processed using the handler classes, thus making the software usable without mail transport agent support. diff -r 462029973f84 -r e5fa4163d214 imip_text_client.py --- a/imip_text_client.py Wed Nov 01 23:25:17 2017 +0100 +++ b/imip_text_client.py Wed Nov 01 23:26:58 2017 +0100 @@ -1,17 +1,19 @@ #!/usr/bin/env python from email import message_from_file -from imiptools import parse_args -from imiptools.client import ClientForObject +from imiptools import get_handlers, parse_args from imiptools.config import settings -from imiptools.content import get_objects_from_itip, have_itip_part, parse_itip_part -from imiptools.data import get_address, get_main_period, get_recurrence_periods, get_value +from imiptools.content import get_objects_from_itip, handle_calendar_data, \ + handle_calendar_object, have_itip_part, \ + parse_itip_part +from imiptools.data import get_address, get_main_period, get_recurrence_periods, get_value, parse_object from imiptools.dates import get_datetime_item, get_time, to_timezone from imiptools.editing import EditingClient, PeriodError +from imiptools.handlers import person, person_outgoing from imiptools.mail import Messenger from imiptools.stores import get_journal, get_store from imiptools.utils import decode_part, message_as_string -import sys +import sys, os # User interface functions. @@ -40,9 +42,9 @@ # Interpret an input file containing a calendar resource. -def get_objects(filename): +def get_itip_from_message(filename): - "Return objects provided by 'filename'." + "Return iTIP details provided by 'filename'." f = open(filename) try: @@ -50,18 +52,25 @@ finally: f.close() - all_objects = [] + all_itip = [] for part in msg.walk(): - itip = parse_itip_part(part) - method = itip and get_value(itip, "METHOD") + if have_itip_part(part): + all_itip.append(parse_itip_part(part)) - # Ignore cancelled objects since only active objects are of interest. + return all_itip + +def get_itip_from_data(filename, charset): - if method != "CANCEL": - all_objects += get_objects_from_itip(itip, ["VEVENT"]) + "Return objects provided by 'filename'." - return all_objects + f = open(filename) + try: + itip = parse_object(f, charset, "VCALENDAR") + finally: + f.close() + + return [itip] def show_objects(objects, user, store): @@ -70,11 +79,34 @@ 'store'. """ + print + print_title("Objects") + print + for index, obj in enumerate(objects): recurrenceid = obj.get_recurrenceid() recurrence_label = recurrenceid and " %s" % recurrenceid or "" print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) +def show_requests(user, store): + + "Show requests available to the given 'user' in the given 'store'." + + requests = store.get_requests(user) + + print + print_title("Requests") + print + + if not requests: + print "No requests are pending." + return + + for index, (uid, recurrenceid) in enumerate(requests): + obj = store.get_event(user, uid, recurrenceid) + recurrence_label = recurrenceid and " %s" % recurrenceid or "" + print "(%d) Summary: %s (%s%s)" % (index, obj.get_value("SUMMARY"), obj.get_uid(), recurrence_label) + def show_attendee(attendee_item, index): "Show the 'attendee_item' (value and attributes) at 'index'." @@ -374,6 +406,8 @@ def show_object(self): print + print_title("Object details") + print print "Summary:", self.state.get("summary") print print "Organiser:", self.state.get("organiser") @@ -468,9 +502,15 @@ def select_object(cl, objects): print + + if objects: + label = "Select object number or (n)ew object or (q)uit> " + else: + label = "Select (n)ew object or (q)uit> " + while True: try: - cmd = read_input("Object or (n)ew object or (q)uit> ") + cmd = read_input(label) except EOFError: return None @@ -479,12 +519,13 @@ if 0 <= index < len(objects): obj = objects[index] return cl.load_object(obj.get_uid(), obj.get_recurrenceid()) + elif cmd in ("n", "new"): return cl.new_object() elif cmd in ("q", "quit", "exit"): return None -def show_help(): +def show_commands(): print print_title("Editing commands") print @@ -532,6 +573,13 @@ Set event summary """ + print_title("Messaging commands") + print + print """\ +S, send + Send messages to recipients and to self, if appropriate +""" + print_title("Diagnostic commands") print print """\ @@ -551,7 +599,7 @@ Show event unique identifier, writing to if specified """ - print_title("Messaging commands") + print_title("Message inspection commands") print print """\ P [ ] @@ -569,7 +617,7 @@ Show update message, writing to if specified """ -def edit_object(cl, obj): +def edit_object(cl, obj, handle_outgoing=False): cl.show_object() print @@ -603,7 +651,7 @@ # Help. elif cmd in ("h", "?", "help"): - show_help() + show_commands() # Show object details. @@ -660,6 +708,61 @@ filename = get_filename_arg(cmd) cl.show_update_message(plain=not filename, filename=filename) + # Definitive finishing action. + + elif args[0] in ("S", "send"): + + # Send update and cancellation messages. + + did_send = False + + message = cl.prepare_update_message() + if message: + cl.send_message(message, cl.get_recipients()) + did_send = True + + to_cancel = cl.state.get("attendees_to_cancel") + if to_cancel: + message = cl.prepare_cancel_message() + if message: + cl.send_message(message, to_cancel) + did_send = True + + # Process the object using the person outgoing handler. + + if handle_outgoing: + + # Handle the parent object plus any rescheduled periods. + + unscheduled_objects, rescheduled_objects, added_objects = \ + cl.get_publish_objects() + + handlers = get_handlers(cl, person_outgoing.handlers, + [get_address(cl.user)]) + + handle_calendar_object(cl.obj, handlers, "PUBLISH") + for o in unscheduled_objects: + handle_calendar_object(o, handlers, "CANCEL") + for o in rescheduled_objects: + handle_calendar_object(o, handlers, "PUBLISH") + for o in added_objects: + handle_calendar_object(o, handlers, "ADD") + + # Otherwise, send a message to self with the event details. + + else: + message = cl.prepare_publish_message() + if message: + cl.send_message_to_self(message) + did_send = True + + # Exit if sending occurred. + + if did_send: + break + else: + print "No messages sent. Try making edits or exit manually." + # Editing operations. elif not cl.state.get("finished"): @@ -821,7 +924,8 @@ # Set the summary. elif args[0] in ("s", "summary"): - cl.edit_summary(cmd.split(None, 1)[1]) + t = cmd.split(None, 1) + cl.edit_summary(len(t) > 1 and t[1] or None) cl.show_object() print @@ -831,18 +935,28 @@ def main(args): global echo + if "--help" in args: + show_help(os.path.split(sys.argv[0])[-1]) + return + # Parse command line arguments using the standard options plus some extra # options. args = parse_args(args, { + "--calendar-data" : ("calendar_data", False), + "--charset" : ("charset", "utf-8"), "--echo" : ("echo", False), "-f" : ("filename", None), + "--handle-data" : ("handle_data", False), "--suppress-bcc" : ("suppress_bcc", False), "-u" : ("user", None), }) + charset = args["charset"] + calendar_data = args["calendar_data"] echo = args["echo"] filename = args["filename"] + handle_data = args["handle_data"] sender = (args["senders"] or [None])[0] suppress_bcc = args["suppress_bcc"] user = args["user"] @@ -877,7 +991,24 @@ # Read any input resource. if filename: - objects = get_objects(filename) + if calendar_data: + all_itip = get_itip_from_data(filename, charset) + else: + all_itip = get_itip_from_message(filename) + + objects = [] + + # Process the objects using the person handler. + + if handle_data: + for itip in all_itip: + objects += handle_calendar_data(itip, get_handlers(cl, person.handlers, None)) + + # Or just obtain objects from the data. + + else: + for itip in all_itip: + objects += get_objects_from_itip(itip, ["VEVENT"]) # Choose an object to edit. @@ -887,7 +1018,7 @@ # Exit without any object. if not obj: - print >>sys.stderr, "Object not loaded." + print >>sys.stderr, "No object loaded." sys.exit(1) # Or create a new object. @@ -897,7 +1028,52 @@ # Edit the object. - edit_object(cl, obj) + edit_object(cl, obj, handle_outgoing=handle_data) + +def show_help(progname): + print >>sys.stderr, help_text % progname + +help_text = """\ +Usage: %s -s | -u \\ + [ -f ] \\ + [ --calendar-data --charset ] \\ + [ --handle-data ] \\ + [ -T ] \\ + [ -p ] \\ + [ --echo ] + +Identity options: + +-s Indicate the user by specifying a sender address +-u Indicate the user by specifying their URI + +Input options: + +-f Indicates a filename containing a MIME-encoded message or calendar object + +--calendar-data Indicates that the specified file contains a calendar object + as opposed to a mail message +--charset Specifies the character encoding used by a calendar object + description + +Processing options: + +--handle-data Cause the input to be handled and stored in the configured + data store + +Configuration options (overriding configured defaults): + +-p Indicates the location of user preference directories +-S Indicates the location of the calendar data store containing user storage + directories +-T Indicates the store type (the configured value if omitted) + +Output options: + +--echo Echo received input, useful if consuming input from the + standard input stream and producing a log of the program's + activity +""" if __name__ == "__main__": main(sys.argv[1:])