meeting.py 9.17 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/python3
# Copyright (c) 2018 Muri Nicanor <muri@immerda.ch>. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice,
#     this list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#  3. Neither the name of the copyright holder nor the names of its
#     contributors may be used to endorse or promote products derived from this
#     software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import datetime
from datetime import timedelta
import argparse
intrigeri's avatar
intrigeri committed
31
import logging
32
33
34
35
36
37
38
import smtplib
import socket
from string import Template
from email.mime.text import MIMEText
from email.utils import parseaddr
from pathlib import Path
import sys
39
import requests
40
41


intrigeri's avatar
intrigeri committed
42
43
44
45
LOG_FORMAT = "%(asctime)-15s %(levelname)s %(message)s"
log = logging.getLogger()


46
def next_meeting_date(mon=None, meetingday=None, skip_friday_to_sunday=False):
intrigeri's avatar
intrigeri committed
47
48
49
    """Calculate the date of the next meeting.
    The default is the 3rd of the next month.
    """
50
51
    mon = mon or 0
    meetingday = meetingday or 3
intrigeri's avatar
Lint.    
intrigeri committed
52
    today_date = datetime.date.today()
53
54
55
56
57

    # if no specific month was given, check if the date
    # is in this months future- if not, take the next
    # month
    if mon == 0:
intrigeri's avatar
Lint.    
intrigeri committed
58
59
        if today_date.day >= meetingday:
            if today_date.month == 11:
60
61
                mon = 12
            else:
intrigeri's avatar
Lint.    
intrigeri committed
62
                mon = (today_date.month + 1) % 12
63
        else:
intrigeri's avatar
Lint.    
intrigeri committed
64
            mon = today_date.month
65
66

    # if the month is in the past, calculate for the month next year
intrigeri's avatar
Lint.    
intrigeri committed
67
68
    if mon < today_date.month:
        today_date = today_date.replace(year=today_date.year + 1)
69
70
71

    # calulate the meetingdate
    try:
intrigeri's avatar
Lint.    
intrigeri committed
72
        next_meeting = today_date.replace(month=mon, day=meetingday)
73
74
75
76
    except ValueError as e:
        sys.exit("The month/day combination month: {}, "
                 "day: {} is invalid: {}".format(mon, meetingday, e))

77
78
79
    if skip_friday_to_sunday:
        # if the meetingday falls on a Friday, Saturday, or Sunday,
        # then choose the day three days after
intrigeri's avatar
Lint.    
intrigeri committed
80
81
        if next_meeting.weekday() >= 4:
            next_meeting += timedelta(days=3)
82

intrigeri's avatar
Lint.    
intrigeri committed
83
    return next_meeting
84
85


86
def email_body(date, append_web_page=None):
intrigeri's avatar
intrigeri committed
87
    """Generate the body of the email from the template"""
88
    body = ''
89
90
91
92
93
94
    try:
        with open(template) as t:
            src = Template(t.read())
    except PermissionError as e:
        sys.exit("Could not open {}: {}".format(template, e))
    d = {'date': date.strftime("%A %d. %B %Y")}
95
96
97
98
    body = src.substitute(d)
    if append_web_page:
        body += requests.get(append_web_page).text
    return body
99

intrigeri's avatar
Lint.    
intrigeri committed
100

101
def email_subject(subject, date, append_date_to_subject):
102
    humanreadabledate = date.strftime("%A %B %d")
103
104
    if append_date_to_subject:
        return "{}{}".format(subject, humanreadabledate)
intrigeri's avatar
Lint.    
intrigeri committed
105
    return subject
106
107


108
def send_email(fromaddress, subject, append_date_to_subject, date, address, send=True, append_web_page=None):
intrigeri's avatar
intrigeri committed
109
    """Send an email using sendmail"""
110
    message = email_body(date, append_web_page)
intrigeri's avatar
Lint.    
intrigeri committed
111
112
113
114
    email = MIMEText(message)
    email['Subject'] = email_subject(subject, date, append_date_to_subject)
    email['From'] = fromaddress
    email['To'] = address
115
    if send:
intrigeri's avatar
Lint.    
intrigeri committed
116
        log.debug("email: %s", email)
117
118
        try:
            s = smtplib.SMTP('localhost')
intrigeri's avatar
Lint.    
intrigeri committed
119
            s.sendmail(email['From'], email['To'], email.as_string())
120
121
122
            s.quit()
        except socket.error as e:
            print("Could not connect to mailserver on localhost. "
123
                  "Email wasn't sent to {}: {}".format(address, e))
124
    else:
intrigeri's avatar
Lint.    
intrigeri committed
125
        print(email)
126
127
128
129


# the main function parses and validates the arguments and
# then either prints the info to stdout or calls other
130
# methods to generate an email body or send emails
131
132
133
134
135
136
137
138
139
140
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-m", "--month", type=int,
                        help="Set the month (default is next month).",
                        choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
    parser.add_argument("-r", "--reminders", type=str,
                        help="Set which days before a meeting should be a "
                        "'reminder day'.")
    parser.add_argument("-d", "--day", type=int,
                        help="Set the day the meeting should happen.")
141
142
143
    parser.add_argument("--skip-friday-to-sunday", default=False,
                        action="store_true",
                        help="Consider all days as valid meeting days")
144
    parser.add_argument("-t", "--template", type=str,
145
                        help="Pass a template for the email.", required=True)
146

147
    parser.add_argument("-a", "--addresses", type=str, required=True,
148
                        help="List of addresses the reminder email should be "
149
                        "sent to (seperated by comma)")
150
    parser.add_argument("-f", "--from", type=str, required=True,
151
                        help="Address email is send from",
152
                        default="noreply@tails.boum.org")
153
    parser.add_argument("-s", "--subject", type=str, required=True,
154
                        help="Subject of the email")
155
156
157
158
    parser.add_argument("--append-date-to-subject",
                        default=False,
                        action="store_true",
                        help="Append the meeting date to the subject")
159
    parser.add_argument("--append-web-page", type=str,
intrigeri's avatar
Lint.    
intrigeri committed
160
161
                        help="Append the contents of this web page" +
                        " to the email body")
162
    parser.add_argument("--print-schedule", action="store_true",
intrigeri's avatar
Lint.    
intrigeri committed
163
164
                        help="Display the meeting and reminder schedule" +
                        " but don't generate reminder email")
165
166
    parser.add_argument("--dry-run", action="store_true",
                        help="Don't send email; instead, display it")
intrigeri's avatar
intrigeri committed
167
    parser.add_argument("--debug", action="store_true", help="debug output")
168
169
170

    args = parser.parse_args()

intrigeri's avatar
intrigeri committed
171
172
173
174
175
    if args.debug:
        logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
    else:
        logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)

intrigeri's avatar
intrigeri committed
176
177
    log.debug("Args:\n%s", args)

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    fromaddress = getattr(args, 'from')
    subject = args.subject
    template = args.template
    if not Path(template).is_file():
        sys.exit("{} is not a file".format(template))

    # make a list of the addresses given in --to
    # for every address we test if it is rfc-822 conform
    # and if it contains an '@'
    if args.addresses:
        toaddresses = [address for address in args.addresses.split(',')]
        for address in toaddresses:
            if parseaddr(address) == ('', '') or '@' not in address:
                sys.exit("{} is not a valid email address.".format(address))

    # calculate the next meeting date
intrigeri's avatar
Lint.    
intrigeri committed
194
    date = next_meeting_date(args.month, args.day, args.skip_friday_to_sunday)
intrigeri's avatar
intrigeri committed
195
    log.debug("Next meeting: %s", date)
196
197
198
199
200
201
202

    # make a list of dates that should be reminderdates
    reminders = []
    if args.reminders:
        for item in [int(item) for item in args.reminders.split(',')]:
            reminders.append(date - timedelta(days=item))

intrigeri's avatar
intrigeri committed
203
204
    log.debug("Reminder days: %s", reminders)

205
206
207
208
209
    # is today a reminderday?
    remindertoday = date.today() in reminders

    # print the date of the next meeting(s) to stdout
    # if one or more reminder dates are set, it also lists those
210
    if args.print_schedule:
211
        print("The next occurrence of this event will happen on {}".format(
212
213
214
215
216
217
            date.strftime("%A %d. %B %Y")))
        for reminder in sorted(reminders):
            tail = " - thats today!" if date.today() == reminder else "."
            msg = "One reminder would be sent on {}{}".format(
                    reminder.strftime("%A %d. %B %Y"), tail)
            print(msg)
218
219
    else:
        # if the current date is a reminderday or no reminderdays are specified,
220
        # either print an email to stdout or send an email using sendmail
221
        if remindertoday or not reminders:
222
            for address in toaddresses:
223
224
225
226
                send_email(fromaddress, subject,
                           args.append_date_to_subject,
                           date, address, send=not args.dry_run,
                           append_web_page=args.append_web_page)