paul@1205 | 1 | #!/usr/bin/env python |
paul@1205 | 2 | |
paul@1205 | 3 | """ |
paul@1205 | 4 | Prepare an invitation message. |
paul@1205 | 5 | |
paul@1205 | 6 | Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk> |
paul@1205 | 7 | |
paul@1205 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@1205 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@1205 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@1205 | 11 | version. |
paul@1205 | 12 | |
paul@1205 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@1205 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@1205 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@1205 | 16 | details. |
paul@1205 | 17 | |
paul@1205 | 18 | You should have received a copy of the GNU General Public License along with |
paul@1205 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@1205 | 20 | """ |
paul@1205 | 21 | |
paul@1205 | 22 | from datetime import datetime |
paul@1205 | 23 | from imiptools.data import get_address, make_uid, new_object |
paul@1205 | 24 | from imiptools.dates import get_datetime, get_datetime_item, \ |
paul@1205 | 25 | get_default_timezone \ |
paul@1205 | 26 | from imiptools.period import Period |
paul@1205 | 27 | from imiptools.mail import Messenger |
paul@1205 | 28 | from os.path import split |
paul@1205 | 29 | import sys |
paul@1205 | 30 | |
paul@1205 | 31 | def make_object(organisers, recipients, summaries, from_datetimes, to_datetimes, |
paul@1205 | 32 | attending, tzids): |
paul@1205 | 33 | |
paul@1205 | 34 | """ |
paul@1205 | 35 | Make an event from the given 'organisers', 'recipients', 'summaries', |
paul@1205 | 36 | 'from_datetimes', 'to_datetimes'. If 'attending' is set to a true value, the |
paul@1205 | 37 | organiser will be added to the attendees list. If 'tzids' is set, any given |
paul@1205 | 38 | timezone is used; otherwise the default timezone is used. |
paul@1205 | 39 | """ |
paul@1205 | 40 | |
paul@1205 | 41 | if len(organisers) != 1: |
paul@1205 | 42 | raise ValueError("An organiser must be specified. More than one is not permitted.") |
paul@1205 | 43 | |
paul@1205 | 44 | if not recipients: |
paul@1205 | 45 | raise ValueError("Recipients must be specified.") |
paul@1205 | 46 | |
paul@1205 | 47 | organiser = organisers[0] |
paul@1205 | 48 | |
paul@1205 | 49 | # Create an event for the calendar with the organiser and attendee details. |
paul@1205 | 50 | |
paul@1205 | 51 | e = new_object("VEVENT") |
paul@1205 | 52 | e["UID"] = [(make_uid(organiser), {})] |
paul@1205 | 53 | e["ORGANIZER"] = [(organiser, {})] |
paul@1205 | 54 | |
paul@1205 | 55 | attendees = [] |
paul@1205 | 56 | |
paul@1205 | 57 | if attending: |
paul@1205 | 58 | attendees.append((organiser, {"PARTSTAT" : "ACCEPTED"})) |
paul@1205 | 59 | |
paul@1205 | 60 | for recipient in recipients: |
paul@1205 | 61 | attendees.append((recipient, {"RSVP" : "TRUE"})) |
paul@1205 | 62 | |
paul@1205 | 63 | e["ATTENDEE"] = attendees |
paul@1205 | 64 | |
paul@1205 | 65 | # Obtain a timezone. |
paul@1205 | 66 | |
paul@1205 | 67 | if len(tzids) > 1: |
paul@1205 | 68 | raise ValueError("Only one timezone identifier should be given.") |
paul@1205 | 69 | |
paul@1205 | 70 | tzid = tzids and tzids[0] or get_default_timezone() |
paul@1205 | 71 | |
paul@1205 | 72 | # Obtain the event periods converting them to datetimes. |
paul@1205 | 73 | |
paul@1205 | 74 | if not from_datetimes: |
paul@1205 | 75 | raise ValueError("The event needs a start datetime.") |
paul@1205 | 76 | if not to_datetimes: |
paul@1205 | 77 | raise ValueError("The event needs an end datetime.") |
paul@1205 | 78 | |
paul@1205 | 79 | periods = [] |
paul@1205 | 80 | |
paul@1205 | 81 | for from_datetime, to_datetime in zip(from_datetimes, to_datetimes): |
paul@1205 | 82 | periods.append(get_period(from_datetime, to_datetime, tzid)) |
paul@1205 | 83 | |
paul@1205 | 84 | # Sort the periods and convert them. |
paul@1205 | 85 | |
paul@1205 | 86 | periods.sort() |
paul@1205 | 87 | dtstart, dtend = periods[0].start, periods[0].end |
paul@1205 | 88 | |
paul@1205 | 89 | # Convert event details to iCalendar values and attributes. |
paul@1205 | 90 | |
paul@1205 | 91 | dtstart, dtstart_attr = get_datetime_item(dtstart, tzid) |
paul@1205 | 92 | dtend, dtend_attr = get_datetime_item(dtend, tzid) |
paul@1205 | 93 | |
paul@1205 | 94 | e["DTSTART"] = [(dtstart, dtstart_attr)] |
paul@1205 | 95 | e["DTEND"] = [(dtend, dtend_attr)] |
paul@1205 | 96 | |
paul@1205 | 97 | # Add recurrences. |
paul@1205 | 98 | |
paul@1205 | 99 | rdates = [] |
paul@1205 | 100 | |
paul@1205 | 101 | for period in periods[1:]: |
paul@1205 | 102 | dtstart, dtend = period.start, period.end |
paul@1205 | 103 | dtstart, dtstart_attr = get_datetime_item(dtstart, tzid) |
paul@1205 | 104 | dtend, dtend_attr = get_datetime_item(dtend, tzid) |
paul@1205 | 105 | rdates.append("%s/%s" % (dtstart, dtend)) |
paul@1205 | 106 | |
paul@1205 | 107 | if rdates: |
paul@1205 | 108 | rdate_attr = {"VALUE" : "PERIOD"} |
paul@1205 | 109 | if tzid: |
paul@1205 | 110 | rdate_attr["TZID"] = tzid |
paul@1205 | 111 | e["RDATE"] = [(rdates, rdate_attr)] |
paul@1205 | 112 | |
paul@1205 | 113 | return e |
paul@1205 | 114 | |
paul@1205 | 115 | def get_period(from_datetime, to_datetime, tzid): |
paul@1205 | 116 | |
paul@1205 | 117 | """ |
paul@1205 | 118 | Return a tuple containing datetimes for 'from_datetime' and 'to_datetime', |
paul@1205 | 119 | using 'tzid' to convert the datetime strings if specified. |
paul@1205 | 120 | """ |
paul@1205 | 121 | |
paul@1205 | 122 | if tzid: |
paul@1205 | 123 | attr = {"TZID" : tzid} |
paul@1205 | 124 | else: |
paul@1205 | 125 | attr = None |
paul@1205 | 126 | |
paul@1205 | 127 | fd = get_datetime(from_datetime, attr) |
paul@1205 | 128 | td = get_datetime(to_datetime, attr) |
paul@1205 | 129 | |
paul@1205 | 130 | if not fd: |
paul@1205 | 131 | raise ValueError("One of the start datetimes (%s) is not recognised." % from_datetime) |
paul@1205 | 132 | |
paul@1205 | 133 | if not td: |
paul@1205 | 134 | raise ValueError("One of the end datetimes (%s) is not recognised." % to_datetime) |
paul@1205 | 135 | |
paul@1205 | 136 | if isinstance(fd, datetime) and not isinstance(td, datetime) or \ |
paul@1205 | 137 | not isinstance(fd, datetime) and isinstance(td, datetime): |
paul@1205 | 138 | |
paul@1205 | 139 | raise ValueError("One period has a mixture of date and datetime: %s - %s" % (from_datetime, to_datetime)) |
paul@1205 | 140 | |
paul@1205 | 141 | if fd > td: |
paul@1205 | 142 | raise ValueError("One period has reversed datetimes: %s - %s" % (from_datetime, to_datetime)) |
paul@1205 | 143 | |
paul@1205 | 144 | return Period(fd, td, tzid) |
paul@1205 | 145 | |
paul@1205 | 146 | # Main program. |
paul@1205 | 147 | |
paul@1205 | 148 | if __name__ == "__main__": |
paul@1205 | 149 | if len(sys.argv) > 1 and sys.argv[1] == "--help": |
paul@1205 | 150 | print >>sys.stderr, """\ |
paul@1205 | 151 | Usage: %s <organiser> -r <recipient>... -s <summary> \\ |
paul@1205 | 152 | -f <from datetime> -t <to datetime> \\ |
paul@1205 | 153 | [ -z <timezone identifier> ] \\ |
paul@1205 | 154 | [ --not-attending ] \\ |
paul@1205 | 155 | [ --send | --encode ] |
paul@1205 | 156 | |
paul@1205 | 157 | Prepare an invitation message to be sent to the indicated recipients, using |
paul@1205 | 158 | the specified <summary>, <from datetime> and <to datetime> to define the event |
paul@1205 | 159 | involved. |
paul@1205 | 160 | |
paul@1205 | 161 | Any <timezone identifier> sets the time zone of any non-UTC datetimes. |
paul@1205 | 162 | |
paul@1205 | 163 | If --not-attending is specified, the organiser will not be added to the |
paul@1205 | 164 | attendees list. |
paul@1205 | 165 | |
paul@1205 | 166 | If --send is specified, attempt to send a message to the recipient addresses |
paul@1205 | 167 | from the logged in user. |
paul@1205 | 168 | |
paul@1205 | 169 | If --encode is specified, encode the message and write it out. The showmail.py |
paul@1205 | 170 | tool can be used to display this encoded output. |
paul@1205 | 171 | |
paul@1205 | 172 | Otherwise, write the iCalendar event object out. |
paul@1205 | 173 | """ % split(sys.argv[0])[1] |
paul@1205 | 174 | sys.exit(1) |
paul@1205 | 175 | |
paul@1205 | 176 | # Gather the information about the invitation. |
paul@1205 | 177 | |
paul@1205 | 178 | organisers = [] |
paul@1205 | 179 | recipients = [] |
paul@1205 | 180 | summaries = [] |
paul@1205 | 181 | from_datetimes = [] |
paul@1205 | 182 | to_datetimes = [] |
paul@1205 | 183 | tzids = [] |
paul@1205 | 184 | send = False |
paul@1205 | 185 | encode = False |
paul@1205 | 186 | attending = True |
paul@1205 | 187 | |
paul@1205 | 188 | l = organisers |
paul@1205 | 189 | |
paul@1205 | 190 | for arg in sys.argv[1:]: |
paul@1205 | 191 | if arg == "-r": |
paul@1205 | 192 | l = recipients |
paul@1205 | 193 | elif arg == "-s": |
paul@1205 | 194 | l = summaries |
paul@1205 | 195 | elif arg == "-f": |
paul@1205 | 196 | l = from_datetimes |
paul@1205 | 197 | elif arg == "-t": |
paul@1205 | 198 | l = to_datetimes |
paul@1205 | 199 | elif arg == "-z": |
paul@1205 | 200 | l = tzids |
paul@1205 | 201 | elif arg == "--send": |
paul@1205 | 202 | send = True |
paul@1205 | 203 | l = [] |
paul@1205 | 204 | elif arg == "--encode": |
paul@1205 | 205 | encode = True |
paul@1205 | 206 | l = [] |
paul@1205 | 207 | elif arg == "--not-attending": |
paul@1205 | 208 | attending = False |
paul@1205 | 209 | l = [] |
paul@1205 | 210 | else: |
paul@1205 | 211 | l.append(arg) |
paul@1205 | 212 | |
paul@1205 | 213 | # Attempt to construct the invitation. |
paul@1205 | 214 | |
paul@1205 | 215 | try: |
paul@1205 | 216 | obj = make_object(organisers, recipients, summaries, from_datetimes, |
paul@1205 | 217 | to_datetimes, attending, tzids) |
paul@1205 | 218 | except ValueError, exc: |
paul@1205 | 219 | print >>sys.stderr, """\ |
paul@1205 | 220 | The invitation could not be prepared due to a problem with the following |
paul@1205 | 221 | details: |
paul@1205 | 222 | |
paul@1205 | 223 | %s |
paul@1205 | 224 | """ % exc.message |
paul@1205 | 225 | sys.exit(1) |
paul@1205 | 226 | |
paul@1205 | 227 | # Produce the invitation output. |
paul@1205 | 228 | |
paul@1205 | 229 | if send or encode: |
paul@1205 | 230 | part = obj.to_part("REQUEST") |
paul@1205 | 231 | |
paul@1205 | 232 | # Create a message and send it. |
paul@1205 | 233 | |
paul@1205 | 234 | if send: |
paul@1205 | 235 | recipients = map(get_address, recipients) |
paul@1205 | 236 | messenger = Messenger() |
paul@1205 | 237 | msg = messenger.make_outgoing_message([part], recipients) |
paul@1205 | 238 | messenger.sendmail(recipients, msg.as_string()) |
paul@1205 | 239 | |
paul@1205 | 240 | # Output the encoded object. |
paul@1205 | 241 | |
paul@1205 | 242 | else: |
paul@1205 | 243 | print msg.as_string() |
paul@1205 | 244 | |
paul@1205 | 245 | # Output the object. |
paul@1205 | 246 | |
paul@1205 | 247 | else: |
paul@1205 | 248 | print obj.to_string() |
paul@1205 | 249 | |
paul@1205 | 250 | # vim: tabstop=4 expandtab shiftwidth=4 |