# HG changeset patch # User Paul Boddie # Date 1482273185 -3600 # Node ID 8e9c052f4e5fe6aa7f465e79b68e19619e18748b # Parent 5dbc885f21db4a116ac93238c4dda48ace1f2f9c Added a tool for constructing event invitations. diff -r 5dbc885f21db -r 8e9c052f4e5f tools/invite.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/invite.py Tue Dec 20 23:33:05 2016 +0100 @@ -0,0 +1,250 @@ +#!/usr/bin/env python + +""" +Prepare an invitation message. + +Copyright (C) 2016 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from datetime import datetime +from imiptools.data import get_address, make_uid, new_object +from imiptools.dates import get_datetime, get_datetime_item, \ + get_default_timezone \ +from imiptools.period import Period +from imiptools.mail import Messenger +from os.path import split +import sys + +def make_object(organisers, recipients, summaries, from_datetimes, to_datetimes, + attending, tzids): + + """ + Make an event from the given 'organisers', 'recipients', 'summaries', + 'from_datetimes', 'to_datetimes'. If 'attending' is set to a true value, the + organiser will be added to the attendees list. If 'tzids' is set, any given + timezone is used; otherwise the default timezone is used. + """ + + if len(organisers) != 1: + raise ValueError("An organiser must be specified. More than one is not permitted.") + + if not recipients: + raise ValueError("Recipients must be specified.") + + organiser = organisers[0] + + # Create an event for the calendar with the organiser and attendee details. + + e = new_object("VEVENT") + e["UID"] = [(make_uid(organiser), {})] + e["ORGANIZER"] = [(organiser, {})] + + attendees = [] + + if attending: + attendees.append((organiser, {"PARTSTAT" : "ACCEPTED"})) + + for recipient in recipients: + attendees.append((recipient, {"RSVP" : "TRUE"})) + + e["ATTENDEE"] = attendees + + # Obtain a timezone. + + if len(tzids) > 1: + raise ValueError("Only one timezone identifier should be given.") + + tzid = tzids and tzids[0] or get_default_timezone() + + # Obtain the event periods converting them to datetimes. + + if not from_datetimes: + raise ValueError("The event needs a start datetime.") + if not to_datetimes: + raise ValueError("The event needs an end datetime.") + + periods = [] + + for from_datetime, to_datetime in zip(from_datetimes, to_datetimes): + periods.append(get_period(from_datetime, to_datetime, tzid)) + + # Sort the periods and convert them. + + periods.sort() + dtstart, dtend = periods[0].start, periods[0].end + + # Convert event details to iCalendar values and attributes. + + dtstart, dtstart_attr = get_datetime_item(dtstart, tzid) + dtend, dtend_attr = get_datetime_item(dtend, tzid) + + e["DTSTART"] = [(dtstart, dtstart_attr)] + e["DTEND"] = [(dtend, dtend_attr)] + + # Add recurrences. + + rdates = [] + + for period in periods[1:]: + dtstart, dtend = period.start, period.end + dtstart, dtstart_attr = get_datetime_item(dtstart, tzid) + dtend, dtend_attr = get_datetime_item(dtend, tzid) + rdates.append("%s/%s" % (dtstart, dtend)) + + if rdates: + rdate_attr = {"VALUE" : "PERIOD"} + if tzid: + rdate_attr["TZID"] = tzid + e["RDATE"] = [(rdates, rdate_attr)] + + return e + +def get_period(from_datetime, to_datetime, tzid): + + """ + Return a tuple containing datetimes for 'from_datetime' and 'to_datetime', + using 'tzid' to convert the datetime strings if specified. + """ + + if tzid: + attr = {"TZID" : tzid} + else: + attr = None + + fd = get_datetime(from_datetime, attr) + td = get_datetime(to_datetime, attr) + + if not fd: + raise ValueError("One of the start datetimes (%s) is not recognised." % from_datetime) + + if not td: + raise ValueError("One of the end datetimes (%s) is not recognised." % to_datetime) + + if isinstance(fd, datetime) and not isinstance(td, datetime) or \ + not isinstance(fd, datetime) and isinstance(td, datetime): + + raise ValueError("One period has a mixture of date and datetime: %s - %s" % (from_datetime, to_datetime)) + + if fd > td: + raise ValueError("One period has reversed datetimes: %s - %s" % (from_datetime, to_datetime)) + + return Period(fd, td, tzid) + +# Main program. + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--help": + print >>sys.stderr, """\ +Usage: %s -r ... -s \\ + -f -t \\ + [ -z ] \\ + [ --not-attending ] \\ + [ --send | --encode ] + +Prepare an invitation message to be sent to the indicated recipients, using +the specified , and to define the event +involved. + +Any sets the time zone of any non-UTC datetimes. + +If --not-attending is specified, the organiser will not be added to the +attendees list. + +If --send is specified, attempt to send a message to the recipient addresses +from the logged in user. + +If --encode is specified, encode the message and write it out. The showmail.py +tool can be used to display this encoded output. + +Otherwise, write the iCalendar event object out. +""" % split(sys.argv[0])[1] + sys.exit(1) + + # Gather the information about the invitation. + + organisers = [] + recipients = [] + summaries = [] + from_datetimes = [] + to_datetimes = [] + tzids = [] + send = False + encode = False + attending = True + + l = organisers + + for arg in sys.argv[1:]: + if arg == "-r": + l = recipients + elif arg == "-s": + l = summaries + elif arg == "-f": + l = from_datetimes + elif arg == "-t": + l = to_datetimes + elif arg == "-z": + l = tzids + elif arg == "--send": + send = True + l = [] + elif arg == "--encode": + encode = True + l = [] + elif arg == "--not-attending": + attending = False + l = [] + else: + l.append(arg) + + # Attempt to construct the invitation. + + try: + obj = make_object(organisers, recipients, summaries, from_datetimes, + to_datetimes, attending, tzids) + except ValueError, exc: + print >>sys.stderr, """\ +The invitation could not be prepared due to a problem with the following +details: + +%s +""" % exc.message + sys.exit(1) + + # Produce the invitation output. + + if send or encode: + part = obj.to_part("REQUEST") + + # Create a message and send it. + + if send: + recipients = map(get_address, recipients) + messenger = Messenger() + msg = messenger.make_outgoing_message([part], recipients) + messenger.sendmail(recipients, msg.as_string()) + + # Output the encoded object. + + else: + print msg.as_string() + + # Output the object. + + else: + print obj.to_string() + +# vim: tabstop=4 expandtab shiftwidth=4