forked from s3lph/matemat
Implemented SMTP receipt sending.
This commit is contained in:
parent
2056b0fb81
commit
b19c6edd7f
7 changed files with 138 additions and 37 deletions
2
doc
2
doc
|
@ -1 +1 @@
|
||||||
Subproject commit cb222b6a7131c24b958740bd5974fca26b450654
|
Subproject commit 411880ae72b3a2204fed4b945bdb3a15d3ece364
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
|
92
matemat/webserver/pagelets/receipt_smtp_cron.py
Normal file
92
matemat/webserver/pagelets/receipt_smtp_cron.py
Normal 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()
|
|
@ -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'])
|
||||||
|
|
Loading…
Reference in a new issue