1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/tools/invite.py Tue Dec 20 23:33:05 2016 +0100
1.3 @@ -0,0 +1,250 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Prepare an invitation message.
1.8 +
1.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This program is free software; you can redistribute it and/or modify it under
1.12 +the terms of the GNU General Public License as published by the Free Software
1.13 +Foundation; either version 3 of the License, or (at your option) any later
1.14 +version.
1.15 +
1.16 +This program is distributed in the hope that it will be useful, but WITHOUT
1.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 +details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along with
1.22 +this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from datetime import datetime
1.26 +from imiptools.data import get_address, make_uid, new_object
1.27 +from imiptools.dates import get_datetime, get_datetime_item, \
1.28 + get_default_timezone \
1.29 +from imiptools.period import Period
1.30 +from imiptools.mail import Messenger
1.31 +from os.path import split
1.32 +import sys
1.33 +
1.34 +def make_object(organisers, recipients, summaries, from_datetimes, to_datetimes,
1.35 + attending, tzids):
1.36 +
1.37 + """
1.38 + Make an event from the given 'organisers', 'recipients', 'summaries',
1.39 + 'from_datetimes', 'to_datetimes'. If 'attending' is set to a true value, the
1.40 + organiser will be added to the attendees list. If 'tzids' is set, any given
1.41 + timezone is used; otherwise the default timezone is used.
1.42 + """
1.43 +
1.44 + if len(organisers) != 1:
1.45 + raise ValueError("An organiser must be specified. More than one is not permitted.")
1.46 +
1.47 + if not recipients:
1.48 + raise ValueError("Recipients must be specified.")
1.49 +
1.50 + organiser = organisers[0]
1.51 +
1.52 + # Create an event for the calendar with the organiser and attendee details.
1.53 +
1.54 + e = new_object("VEVENT")
1.55 + e["UID"] = [(make_uid(organiser), {})]
1.56 + e["ORGANIZER"] = [(organiser, {})]
1.57 +
1.58 + attendees = []
1.59 +
1.60 + if attending:
1.61 + attendees.append((organiser, {"PARTSTAT" : "ACCEPTED"}))
1.62 +
1.63 + for recipient in recipients:
1.64 + attendees.append((recipient, {"RSVP" : "TRUE"}))
1.65 +
1.66 + e["ATTENDEE"] = attendees
1.67 +
1.68 + # Obtain a timezone.
1.69 +
1.70 + if len(tzids) > 1:
1.71 + raise ValueError("Only one timezone identifier should be given.")
1.72 +
1.73 + tzid = tzids and tzids[0] or get_default_timezone()
1.74 +
1.75 + # Obtain the event periods converting them to datetimes.
1.76 +
1.77 + if not from_datetimes:
1.78 + raise ValueError("The event needs a start datetime.")
1.79 + if not to_datetimes:
1.80 + raise ValueError("The event needs an end datetime.")
1.81 +
1.82 + periods = []
1.83 +
1.84 + for from_datetime, to_datetime in zip(from_datetimes, to_datetimes):
1.85 + periods.append(get_period(from_datetime, to_datetime, tzid))
1.86 +
1.87 + # Sort the periods and convert them.
1.88 +
1.89 + periods.sort()
1.90 + dtstart, dtend = periods[0].start, periods[0].end
1.91 +
1.92 + # Convert event details to iCalendar values and attributes.
1.93 +
1.94 + dtstart, dtstart_attr = get_datetime_item(dtstart, tzid)
1.95 + dtend, dtend_attr = get_datetime_item(dtend, tzid)
1.96 +
1.97 + e["DTSTART"] = [(dtstart, dtstart_attr)]
1.98 + e["DTEND"] = [(dtend, dtend_attr)]
1.99 +
1.100 + # Add recurrences.
1.101 +
1.102 + rdates = []
1.103 +
1.104 + for period in periods[1:]:
1.105 + dtstart, dtend = period.start, period.end
1.106 + dtstart, dtstart_attr = get_datetime_item(dtstart, tzid)
1.107 + dtend, dtend_attr = get_datetime_item(dtend, tzid)
1.108 + rdates.append("%s/%s" % (dtstart, dtend))
1.109 +
1.110 + if rdates:
1.111 + rdate_attr = {"VALUE" : "PERIOD"}
1.112 + if tzid:
1.113 + rdate_attr["TZID"] = tzid
1.114 + e["RDATE"] = [(rdates, rdate_attr)]
1.115 +
1.116 + return e
1.117 +
1.118 +def get_period(from_datetime, to_datetime, tzid):
1.119 +
1.120 + """
1.121 + Return a tuple containing datetimes for 'from_datetime' and 'to_datetime',
1.122 + using 'tzid' to convert the datetime strings if specified.
1.123 + """
1.124 +
1.125 + if tzid:
1.126 + attr = {"TZID" : tzid}
1.127 + else:
1.128 + attr = None
1.129 +
1.130 + fd = get_datetime(from_datetime, attr)
1.131 + td = get_datetime(to_datetime, attr)
1.132 +
1.133 + if not fd:
1.134 + raise ValueError("One of the start datetimes (%s) is not recognised." % from_datetime)
1.135 +
1.136 + if not td:
1.137 + raise ValueError("One of the end datetimes (%s) is not recognised." % to_datetime)
1.138 +
1.139 + if isinstance(fd, datetime) and not isinstance(td, datetime) or \
1.140 + not isinstance(fd, datetime) and isinstance(td, datetime):
1.141 +
1.142 + raise ValueError("One period has a mixture of date and datetime: %s - %s" % (from_datetime, to_datetime))
1.143 +
1.144 + if fd > td:
1.145 + raise ValueError("One period has reversed datetimes: %s - %s" % (from_datetime, to_datetime))
1.146 +
1.147 + return Period(fd, td, tzid)
1.148 +
1.149 +# Main program.
1.150 +
1.151 +if __name__ == "__main__":
1.152 + if len(sys.argv) > 1 and sys.argv[1] == "--help":
1.153 + print >>sys.stderr, """\
1.154 +Usage: %s <organiser> -r <recipient>... -s <summary> \\
1.155 + -f <from datetime> -t <to datetime> \\
1.156 + [ -z <timezone identifier> ] \\
1.157 + [ --not-attending ] \\
1.158 + [ --send | --encode ]
1.159 +
1.160 +Prepare an invitation message to be sent to the indicated recipients, using
1.161 +the specified <summary>, <from datetime> and <to datetime> to define the event
1.162 +involved.
1.163 +
1.164 +Any <timezone identifier> sets the time zone of any non-UTC datetimes.
1.165 +
1.166 +If --not-attending is specified, the organiser will not be added to the
1.167 +attendees list.
1.168 +
1.169 +If --send is specified, attempt to send a message to the recipient addresses
1.170 +from the logged in user.
1.171 +
1.172 +If --encode is specified, encode the message and write it out. The showmail.py
1.173 +tool can be used to display this encoded output.
1.174 +
1.175 +Otherwise, write the iCalendar event object out.
1.176 +""" % split(sys.argv[0])[1]
1.177 + sys.exit(1)
1.178 +
1.179 + # Gather the information about the invitation.
1.180 +
1.181 + organisers = []
1.182 + recipients = []
1.183 + summaries = []
1.184 + from_datetimes = []
1.185 + to_datetimes = []
1.186 + tzids = []
1.187 + send = False
1.188 + encode = False
1.189 + attending = True
1.190 +
1.191 + l = organisers
1.192 +
1.193 + for arg in sys.argv[1:]:
1.194 + if arg == "-r":
1.195 + l = recipients
1.196 + elif arg == "-s":
1.197 + l = summaries
1.198 + elif arg == "-f":
1.199 + l = from_datetimes
1.200 + elif arg == "-t":
1.201 + l = to_datetimes
1.202 + elif arg == "-z":
1.203 + l = tzids
1.204 + elif arg == "--send":
1.205 + send = True
1.206 + l = []
1.207 + elif arg == "--encode":
1.208 + encode = True
1.209 + l = []
1.210 + elif arg == "--not-attending":
1.211 + attending = False
1.212 + l = []
1.213 + else:
1.214 + l.append(arg)
1.215 +
1.216 + # Attempt to construct the invitation.
1.217 +
1.218 + try:
1.219 + obj = make_object(organisers, recipients, summaries, from_datetimes,
1.220 + to_datetimes, attending, tzids)
1.221 + except ValueError, exc:
1.222 + print >>sys.stderr, """\
1.223 +The invitation could not be prepared due to a problem with the following
1.224 +details:
1.225 +
1.226 +%s
1.227 +""" % exc.message
1.228 + sys.exit(1)
1.229 +
1.230 + # Produce the invitation output.
1.231 +
1.232 + if send or encode:
1.233 + part = obj.to_part("REQUEST")
1.234 +
1.235 + # Create a message and send it.
1.236 +
1.237 + if send:
1.238 + recipients = map(get_address, recipients)
1.239 + messenger = Messenger()
1.240 + msg = messenger.make_outgoing_message([part], recipients)
1.241 + messenger.sendmail(recipients, msg.as_string())
1.242 +
1.243 + # Output the encoded object.
1.244 +
1.245 + else:
1.246 + print msg.as_string()
1.247 +
1.248 + # Output the object.
1.249 +
1.250 + else:
1.251 + print obj.to_string()
1.252 +
1.253 +# vim: tabstop=4 expandtab shiftwidth=4