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