Implemented SMTP receipt sending.

This commit is contained in:
s3lph 2018-09-07 22:45:20 +02:00
parent 2056b0fb81
commit b19c6edd7f
7 changed files with 138 additions and 37 deletions

2
doc

@ -1 +1 @@
Subproject commit cb222b6a7131c24b958740bd5974fca26b450654 Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364

View file

@ -15,3 +15,4 @@ from .admin import admin
from .moduser import moduser from .moduser import moduser
from .modproduct import modproduct from .modproduct import modproduct
from .userbootstrap import userbootstrap from .userbootstrap import userbootstrap
from .receipt_smtp_cron import receipt_smtp_cron

View file

@ -23,6 +23,31 @@ def initialization(config: Dict[str, str],
if 'DatabaseFile' not in config: if 'DatabaseFile' not in config:
config['DatabaseFile'] = './matemat.db' config['DatabaseFile'] = './matemat.db'
logger.warning('Property \'DatabaseFile\' not set, using \'./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']): with MatematDatabase(config['DatabaseFile']):
# Connect to the database to create it and perform any schema migrations # Connect to the database to create it and perform any schema migrations
pass pass

View file

@ -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'])

View file

@ -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()

View file

@ -30,6 +30,15 @@ Name=Matemat
UploadDir= /var/test/static/upload UploadDir= /var/test/static/upload
DatabaseFile=/var/test/db/test.db 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] [HttpHeaders]
Content-Security-Policy = default-src: 'self'; Content-Security-Policy = default-src: 'self';
X-I-Am-A-Header = andthisismyvalue X-I-Am-A-Header = andthisismyvalue
@ -42,6 +51,8 @@ Port=443
[Pagelets] [Pagelets]
Name=Matemat (Unit Test 2) Name=Matemat (Unit Test 2)
SmtpSendReceipts=1
[HttpHeaders] [HttpHeaders]
X-I-Am-A-Header = andthisismyothervalue 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('Matemat\n(Unit Test)', config['pagelet_variables']['Name'])
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) 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.assertIsInstance(config['headers'], dict)
self.assertEqual(2, len(config['headers'])) self.assertEqual(2, len(config['headers']))
self.assertEqual('default-src: \'self\';', config['headers']['Content-Security-Policy']) self.assertEqual('default-src: \'self\';', config['headers']['Content-Security-Policy'])