Skip to content
Snippets Groups Projects
Commit af227f48 authored by Martin Mareš's avatar Martin Mareš
Browse files

DSN: Ukládání do DB

parent 9b34b334
Branches
No related tags found
1 merge request!138Zpracování nedoručenek
......@@ -10,29 +10,32 @@ from flask import request, Response
from flask.json import jsonify
import re
from typing import Optional
from unidecode import unidecode
import werkzeug.exceptions
import mo.config as config
import mo.db as db
import mo.email
import mo.util_format
from mo.web import app
class DSN:
class DSNParser:
msg: email.message.EmailMessage
message_id: str
verdict: str
parsed: db.EmailDSN
dsn_action: str
dsn_status: str
def __init__(self, body: bytes):
self.msg = email.message_from_bytes(body, policy=email.policy.default) # FIXME: Types
self.message_id = (self.msg['Message-Id'] or '?').strip()
self.parsed = db.EmailDSN()
self.parsed.message_id = (self.msg['Message-Id'] or '?').strip()
self.dsn_action = '?'
self.dsn_status = '?'
self.verdict = self.parse_dsn()
def parse_dsn(self) -> str:
"""Parse DSN as specified by RFC 3464."""
if self.msg.get_content_type() != 'multipart/report':
return 'unexpected content-type'
......@@ -56,9 +59,25 @@ class DSN:
# main = dsn[0]
per_addr = dsn[1]
per_addr: email.message.EmailMessage = dsn[1]
self.dsn_action = per_addr.get('Action', '?')
self.dsn_status = per_addr.get('Status', '?')
status = per_addr.get('Status')
if status and re.fullmatch(r'\d\.\d\.\d{1,8}', status):
self.parsed.status = status
remote_mta = per_addr.get('Remote-MTA') or per_addr.get('Reporting-MTA')
if remote_mta and (rm := re.fullmatch(r'dns;\s+([!-~]{1,256})', remote_mta, re.IGNORECASE)):
self.parsed.remote_mta = rm[1]
diag_code = per_addr.get('Diagnostic-Code')
if diag_code and (dm := re.fullmatch(r'smtp;\s+(.*)', diag_code, re.IGNORECASE)):
dc = unidecode(dm[1]) # Mělo by to být v ASCII, ale pro jistotu...
dc = re.sub(r'\s{2,}', ' ', dc)
MAX_DIAG_LEN = 1000
if len(dc) > MAX_DIAG_LEN:
dc = dc[:MAX_DIAG_LEN] + " [...]"
self.parsed.diag_code = dc
if self.dsn_action != 'failed':
return 'not failed'
......@@ -80,39 +99,64 @@ class DSN:
return None
def process_dsn_user(dsn: db.EmailDSN) -> None:
dsn.user.dsn_id = dsn.dsn_id
def process_dsn_reg(dsn: db.EmailDSN) -> None:
if (dsn.reg.user_id is not None
and dsn.user_id is not None
and dsn.reg.user_id != dsn.user_id):
app.logger.warning('DSN: Nesouhlasí user_id s registrací')
dsn.reg.dsn_id = dsn.dsn_id
@app.route('/api/email-dsn', methods=('POST',))
def api_email_dsn() -> Response:
# FIXME: Authorization?
# FIXME: Add in Flask 3.1: request.max_content_length = 1048576
body = request.get_data(cache=False)
try:
dsn = DSN(body)
parser = DSNParser(body)
dsn = parser.parsed
except MessageError as e:
app.logger.info(f'DSN: Nemohu naparsovat zprávu: {e}')
raise werkzeug.exceptions.UnprocessableEntity()
app.logger.info(f'DSN: Message-ID: {dsn.message_id}')
app.logger.info(f'DSN: Parse: action={dsn.dsn_action} status={dsn.dsn_status} -> {dsn.verdict}')
app.logger.info(f'DSN: Parse: action={parser.dsn_action} status={dsn.status or "-"} -> {parser.verdict}')
if dsn.verdict == 'not failed':
if parser.verdict == 'not failed':
return jsonify({})
elif dsn.verdict != 'ok':
elif parser.verdict != 'ok':
raise werkzeug.exceptions.UnprocessableEntity()
if not (token := dsn.find_token()):
if not (token := parser.find_token()):
app.logger.info('DSN: Token not found')
raise werkzeug.exceptions.UnprocessableEntity()
app.logger.info(f'DSN: Token: {token}')
try:
user, rr, email, when = mo.email.validate_dsn_token(token)
user_info = f'#{user.user_id}' if user is not None else '-'
rr_info = f'#{rr.reg_id}' if rr is not None else '-'
dsn.token = token
dsn.user, dsn.reg, email, when = mo.email.validate_dsn_token(token)
user_info = f'#{dsn.user.user_id}' if dsn.user is not None else '-'
rr_info = f'#{dsn.reg.reg_id}' if dsn.reg is not None else '-'
age = mo.util_format.time_duration_numeric(mo.now - when)
app.logger.info(f'DSN: user={user_info} registration={rr_info} email={email} age={age}')
except ValueError as e:
app.logger.info(f'DSN: {e}')
pass
raise werkzeug.exceptions.UnprocessableEntity()
sess = db.get_session()
if sess.query(db.EmailDSN).filter_by(token=token).one_or_none():
app.logger.info('DSN: Already known')
else:
sess.add(dsn)
sess.flush() # aby dsn získala dsn_id
if dsn.reg:
process_dsn_reg(dsn)
elif dsn.user:
process_dsn_user(dsn)
sess.commit()
return jsonify({})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment