library/library/interface/sendemails.py

191 lines
6.4 KiB
Python

#Import smtp for the email sending function
import smtplib
#Import argparse to parse command line args
import argparse
#Import sys so that we can exit when given bad command line args
import sys
#Import datetime and time to check dates
from datetime import datetime
import time
#Import getpass for password input
import getpass
#Import subprocess for validation of if a user is in a group, and running
#a sendmail process
import subprocess
#Import librarian permissions
from library import permissions
from library.exceptions import *
import library.database as db
from library.interface.form import FormWindow, BookForm, SuccessForm, catch_error_with, error_form
from library.emails import format_reminder_email
# email testing folder creation
import os
#Constants
DEFAULT_DAY_VALUE = 21
MAX_LOGIN_ATTEMPTS = 3
class DaysForm(FormWindow):
caption = "Enter the max number of days a book can be signed out for"
blabel = "Enter"
labels = ["Days (default " + str(DEFAULT_DAY_VALUE) + ")"]
def _return_values(self):
ret = self.entries[0].value
if ret is "":
return DEFAULT_DAY_VALUE
else:
#If we didn't get valid input, noisily fail
assert ret.isdigit() and int(ret) > 0, \
"Max signed out days is not positive: " + ret.__repr__()
return int(ret)
class NameForm(FormWindow):
caption = "Enter the name you want in the signature line for the email"
blabel = "Enter"
labels = ["Name"]
def _return_values(self):
if self.entries[0].value is "":
return "Librarian"
else:
assert self.entries[0].value.isprintable()
return self.entries[0].value
#Private functions
def _send_email(quest_id: str,
signed_out_date: str,
max_days_can_be_signed_out: int,
librarian_name: str,
book_name: str,
testing=False) -> bool:
"""Sends an email to quest_id@csclub.uwaterloo.ca if the date the book
was signed out exceeds the days it is supposed to be signed out for.
Set testing to true to output emails to the current directory, instead
of actually sending them.
Returns whether an email was sent.
"""
# Determine the days the book has been signed out
d = datetime.strptime(signed_out_date, "%Y-%m-%d")
days_signed_out = (d.today() - d).days
if days_signed_out > max_days_can_be_signed_out:
#Librarian also gets a copy, so that unreplied emails are more apparent.
email_addresses = [quest_id + "@csclub.uwaterloo.ca",
"librarian@csclub.uwaterloo.ca"]
email_message = format_reminder_email(quest_id, days_signed_out, librarian_name, book_name)
if testing:
assert book_name.isprintable()
os.makedirs("test_emails", exist_ok=True)
#Know that book titles such as "FORTRAN/77" exist, sanitize slashes.
filename = ("test_emails/{}_{}.txt"
.format(quest_id, book_name.replace("/", "-")))
with open(filename, "w") as f:
print(email_message, file=f)
else:
#Equivalent to:
#echo <email_message> | sendmail questid@csclub.uwaterloo.ca
sendmail_process = subprocess.Popen(["sendmail"] + email_addresses,
stdin=subprocess.PIPE)
sendmail_process.communicate(input=email_message.encode("utf-8"))
return True
return False
#Public functions
@permissions.check_permissions(permissions.PERMISSION_LIBCOM)
@catch_error_with(lambda w, hb, *args: (w, hb, None))
def sendemails_procedure(w, hb, cy, cx, mx):
"""Procedure to send emails to those with overdue books
w: ncurses window for the routine
cy,cx: centre coordinates of the screen
mx: max width of screen
"""
#Get the max days a book can be signed out for
step1 = DaysForm(w, hb, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
days = step1.event_loop()
step1.clear()
if days == {}:
return "" #User cancelled the dialog box
#Get the name of the librarian
step2 = NameForm(w, hb, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
librarianName = step2.event_loop()
step2.clear()
if librarianName == {}:
return "" #User cancelled the dialog box
#Don't send out emails if this gets set to True
testing = False
if librarianName == "TESTING12345":
testing = True
#Get the books that are signed out
signed_out_books = db.get_checkedout_books()
earliest_checkout = datetime.now()
emails_sent = 0
for data in signed_out_books:
quest_id = data["uwid"]
date = str(data["date"]).split(" ")[0]
book_name_with_spaces = data["title"]
if _send_email(
str(quest_id),
str(date), days, str(librarianName), book_name_with_spaces,
testing):
emails_sent += 1
earliest_checkout = min(earliest_checkout,
datetime.strptime(date, "%Y-%m-%d"))
#A success message about what emails we just sent
assert earliest_checkout > datetime(2010,1,1)
time_since_earliest_checkout = ""
days_since_earliest_checkout = (datetime.now().toordinal()
- earliest_checkout.toordinal())
assert days >= 0
month_length = 30 #It's wrong because it's an approximation
if days_since_earliest_checkout < month_length:
time_since_earliest_checkout = "{} days ago".format(days_since_earliest_checkout)
else:
time_since_earliest_checkout = "{:0.1f} months ago".format(days_since_earliest_checkout / month_length)
if emails_sent == 0:
success_text = """
No books are overdue, so all zero reminder emails were vacuously sent successfully.
Either members are (hopefully) good at returning books or members don't take books out."""
else:
success_text = """
Automatically sent out {} individual reminder emails, one per overdue book.
The most overdue book was checked out approximately {}.""".format(
emails_sent,
time_since_earliest_checkout
)
step4 = SuccessForm(w, hb, success_text, width=mx - 20)
(r, c) = w.getmaxyx()
w.mvwin(cy - r // 2, cx - c // 2)
step4.event_loop()
step4.clear()
return ""