From b19c6edd7ff03690aff40218047ede7411e7a1c2 Mon Sep 17 00:00:00 2001 From: s3lph Date: Fri, 7 Sep 2018 22:45:20 +0200 Subject: [PATCH] Implemented SMTP receipt sending. --- doc | 2 +- matemat/webserver/pagelets/__init__.py | 1 + matemat/webserver/pagelets/cron.py | 0 matemat/webserver/pagelets/initialization.py | 25 +++++ matemat/webserver/pagelets/receipt.py | 36 -------- .../webserver/pagelets/receipt_smtp_cron.py | 92 +++++++++++++++++++ matemat/webserver/test/test_config.py | 19 ++++ 7 files changed, 138 insertions(+), 37 deletions(-) delete mode 100644 matemat/webserver/pagelets/cron.py delete mode 100644 matemat/webserver/pagelets/receipt.py create mode 100644 matemat/webserver/pagelets/receipt_smtp_cron.py diff --git a/doc b/doc index cb222b6..411880a 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit cb222b6a7131c24b958740bd5974fca26b450654 +Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364 diff --git a/matemat/webserver/pagelets/__init__.py b/matemat/webserver/pagelets/__init__.py index 43b7f31..3cdcad8 100644 --- a/matemat/webserver/pagelets/__init__.py +++ b/matemat/webserver/pagelets/__init__.py @@ -15,3 +15,4 @@ from .admin import admin from .moduser import moduser from .modproduct import modproduct from .userbootstrap import userbootstrap +from .receipt_smtp_cron import receipt_smtp_cron diff --git a/matemat/webserver/pagelets/cron.py b/matemat/webserver/pagelets/cron.py deleted file mode 100644 index e69de29..0000000 diff --git a/matemat/webserver/pagelets/initialization.py b/matemat/webserver/pagelets/initialization.py index 27636b6..fcb94d9 100644 --- a/matemat/webserver/pagelets/initialization.py +++ b/matemat/webserver/pagelets/initialization.py @@ -23,6 +23,31 @@ def initialization(config: Dict[str, str], if 'DatabaseFile' not in config: config['DatabaseFile'] = './matemat.db' logger.warning('Property \'DatabaseFile\' not set, using \'./matemat.db\'') + if 'SmtpSendReceipts' not in config: + config['SmtpSendReceipts'] = '0' + logger.warning('Property \'SmtpSendReceipts\' not set, using \'0\'') + if config['SmtpSendReceipts'] == '1': + if 'SmtpFrom' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpFrom\' missing.') + raise KeyError() + if 'SmtpSubj' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpSubj\' missing.') + raise KeyError() + if 'SmtpHost' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpHost\' missing.') + raise KeyError() + if 'SmtpPort' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpPort\' missing.') + raise KeyError() + if 'SmtpUser' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpUser\' missing.') + raise KeyError() + if 'SmtpPass' not in config: + logger.fatal('\'SmtpSendReceipts\' set to \'1\', but \'SmtpPass\' missing.') + raise KeyError() + if 'SmtpEnforceTLS' not in config: + config['SmtpEnforceTLS'] = '1' + logger.warning('Property \'SmtpEnforceTLS\' not set, using \'1\'') with MatematDatabase(config['DatabaseFile']): # Connect to the database to create it and perform any schema migrations pass diff --git a/matemat/webserver/pagelets/receipt.py b/matemat/webserver/pagelets/receipt.py deleted file mode 100644 index 696460e..0000000 --- a/matemat/webserver/pagelets/receipt.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any, Dict, List, Union - -from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse -from matemat.db import MatematDatabase -from matemat.db.primitives import Receipt -from matemat.util.currency_format import format_chf - - -@pagelet('/receipt') -def show_receipt(method: str, - path: str, - args: RequestArguments, - session_vars: Dict[str, Any], - headers: Dict[str, str], - config: Dict[str, str]) \ - -> Union[str, bytes, PageletResponse]: - if 'authenticated_user' not in session_vars: - return RedirectResponse('/') - # Connect to the database - with MatematDatabase(config['DatabaseFile']) as db: - # Fetch the authenticated user from the database - uid: int = session_vars['authenticated_user'] - user = db.get_user(uid) - receipt: Receipt = db.create_receipt(user) - headers['Content-Type'] = 'text/plain; charset=utf-8' - fdate: str = receipt.from_date.strftime('%d.%m.%Y, %H:%M') - tdate: str = receipt.to_date.strftime('%d.%m.%Y, %H:%M') - username: str = receipt.user.name.rjust(40) - if len(receipt.transactions) == 0: - fbal: str = format_chf(receipt.user.balance).rjust(12) - else: - fbal = format_chf(receipt.transactions[0].old_balance).rjust(12) - tbal: str = format_chf(receipt.user.balance).rjust(12) - return TemplateResponse('receipt.txt', - fdate=fdate, tdate=tdate, user=username, fbal=fbal, tbal=tbal, - transactions=receipt.transactions, instance_name=config['InstanceName']) diff --git a/matemat/webserver/pagelets/receipt_smtp_cron.py b/matemat/webserver/pagelets/receipt_smtp_cron.py new file mode 100644 index 0000000..6095d7b --- /dev/null +++ b/matemat/webserver/pagelets/receipt_smtp_cron.py @@ -0,0 +1,92 @@ +from typing import Dict, List, Tuple + +import logging + +import smtplib as smtp +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from jinja2 import Environment, Template + +from matemat.webserver import pagelet_cron +from matemat.db import MatematDatabase +from matemat.db.primitives import User, Receipt +from matemat.util.currency_format import format_chf + + +@pagelet_cron(minutes=1) +def receipt_smtp_cron(config: Dict[str, str], + jinja_env: Environment, + logger: logging.Logger) -> None: + if config['SmtpSendReceipts'] != '1': + # Sending receipts via mail is disabled + return + receipts: List[Receipt] = [] + # Connect to the database + with MatematDatabase(config['DatabaseFile']) as db: + users: List[User] = db.list_users() + for user in users: + if db.check_receipt_due(user): + # Generate receipts that are due + receipt: Receipt = db.create_receipt(user, write=True) + receipts.append(receipt) + # Send all generated receipts via e-mail + if len(receipts) > 0: + _send_receipt_mails(receipts, jinja_env, logger, config) + + +def _send_receipt_mails(receipts: List[Receipt], + jinja_env: Environment, + logger: logging.Logger, + config: Dict[str, str]) -> None: + mails: List[Tuple[str, MIMEMultipart]] = [] + for receipt in receipts: + if receipt.user.email is None: + continue + # Create a new message object + msg: MIMEMultipart = MIMEMultipart() + msg['From'] = config['SmtpFrom'] + msg['To'] = receipt.user.email + msg['Subject'] = config['SmtpSubj'] + # Format the receipt properties for the text representation + fdate: str = receipt.from_date.strftime('%d.%m.%Y, %H:%M') + tdate: str = receipt.to_date.strftime('%d.%m.%Y, %H:%M') + username: str = receipt.user.name.rjust(40) + if len(receipt.transactions) == 0: + fbal: str = format_chf(receipt.user.balance).rjust(12) + else: + fbal = format_chf(receipt.transactions[0].old_balance).rjust(12) + tbal: str = format_chf(receipt.user.balance).rjust(12) + # Render the receipt + template: Template = jinja_env.get_template('receipt.txt') + rendered: str = template.render(fdate=fdate, tdate=tdate, user=username, fbal=fbal, tbal=tbal, + receipt_id=receipt.id, transactions=receipt.transactions, + instance_name=config['InstanceName']) + # Put the rendered receipt in the message body + body: MIMEText = MIMEText(rendered) + msg.attach(body) + mails.append((receipt.user.email, msg)) + + # Connect to the SMTP Server + con: smtp.SMTP = smtp.SMTP(config['SmtpHost'], config['SmtpPort']) + try: + # Attempt to upgrade to a TLS connection + try: + con.starttls() + except: + # If STARTTLS failed, only continue if explicitly requested by configuration + if config['SmtpEnforceTLS'] != '0': + logger.error('STARTTLS not supported by SMTP server, aborting!') + return + else: + logger.warning('Sending e-mails in plain text as requested by SmtpEnforceTLS=0.') + # Send SMTP login credentials + con.login(config['SmtpUser'], config['SmtpPass']) + + # Send the e-mails + for to, msg in mails: + logger.info('Sending mail to %s', to) + con.sendmail(config['SmtpFrom'], to, msg.as_string()) + except smtp.SMTPException as e: + logger.exception('Exception while sending receipt e-mails', exc_info=e) + finally: + con.close() diff --git a/matemat/webserver/test/test_config.py b/matemat/webserver/test/test_config.py index e2ad985..4fee839 100644 --- a/matemat/webserver/test/test_config.py +++ b/matemat/webserver/test/test_config.py @@ -30,6 +30,15 @@ Name=Matemat UploadDir= /var/test/static/upload DatabaseFile=/var/test/db/test.db +SmtpSendReceipts=1 +SmtpEnforceTLS=0 +SmtpFrom=matemat@example.com +SmtpSubj=Matemat Receipt +SmtpHost=smtp.example.com +SmtpPort=587 +SmtpUser=matemat@example.com +SmtpPass=SuperSecurePassword + [HttpHeaders] Content-Security-Policy = default-src: 'self'; X-I-Am-A-Header = andthisismyvalue @@ -42,6 +51,8 @@ Port=443 [Pagelets] Name=Matemat (Unit Test 2) +SmtpSendReceipts=1 + [HttpHeaders] X-I-Am-A-Header = andthisismyothervalue ''' @@ -153,6 +164,14 @@ class TestConfig(TestCase): self.assertEqual('Matemat\n(Unit Test)', config['pagelet_variables']['Name']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) + self.assertEqual('1', config['pagelet_variables']['SmtpSendReceipts']) + self.assertEqual('0', config['pagelet_variables']['SmtpEnforceTLS']) + self.assertEqual('matemat@example.com', config['pagelet_variables']['SmtpFrom']) + self.assertEqual('Matemat Receipt', config['pagelet_variables']['SmtpSubj']) + self.assertEqual('smtp.example.com', config['pagelet_variables']['SmtpHost']) + self.assertEqual('587', config['pagelet_variables']['SmtpPort']) + self.assertEqual('matemat@example.com', config['pagelet_variables']['SmtpUser']) + self.assertEqual('SuperSecurePassword', config['pagelet_variables']['SmtpPass']) self.assertIsInstance(config['headers'], dict) self.assertEqual(2, len(config['headers'])) self.assertEqual('default-src: \'self\';', config['headers']['Content-Security-Policy'])