imip-agent

Annotated tools/invite.py

1465:0c623c86704f
2020-08-03 Paul Boddie Changed diagram font to sans-serif.
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