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 .modproduct import modproduct
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -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
|
||||
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'])
|
||||
|
|
Loading…
Reference in a new issue