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