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 ...@@ -10,29 +10,32 @@ from flask import request, Response
from flask.json import jsonify from flask.json import jsonify
import re import re
from typing import Optional from typing import Optional
from unidecode import unidecode
import werkzeug.exceptions import werkzeug.exceptions
import mo.config as config import mo.config as config
import mo.db as db
import mo.email import mo.email
import mo.util_format import mo.util_format
from mo.web import app from mo.web import app
class DSN: class DSNParser:
msg: email.message.EmailMessage msg: email.message.EmailMessage
message_id: str
verdict: str verdict: str
parsed: db.EmailDSN
dsn_action: str dsn_action: str
dsn_status: str
def __init__(self, body: bytes): def __init__(self, body: bytes):
self.msg = email.message_from_bytes(body, policy=email.policy.default) # FIXME: Types 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_action = '?'
self.dsn_status = '?'
self.verdict = self.parse_dsn() self.verdict = self.parse_dsn()
def parse_dsn(self) -> str: def parse_dsn(self) -> str:
"""Parse DSN as specified by RFC 3464."""
if self.msg.get_content_type() != 'multipart/report': if self.msg.get_content_type() != 'multipart/report':
return 'unexpected content-type' return 'unexpected content-type'
...@@ -56,9 +59,25 @@ class DSN: ...@@ -56,9 +59,25 @@ class DSN:
# main = dsn[0] # main = dsn[0]
per_addr = dsn[1] per_addr: email.message.EmailMessage = dsn[1]
self.dsn_action = per_addr.get('Action', '?') 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': if self.dsn_action != 'failed':
return 'not failed' return 'not failed'
...@@ -80,39 +99,64 @@ class DSN: ...@@ -80,39 +99,64 @@ class DSN:
return None 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',)) @app.route('/api/email-dsn', methods=('POST',))
def api_email_dsn() -> Response: def api_email_dsn() -> Response:
# FIXME: Authorization?
# FIXME: Add in Flask 3.1: request.max_content_length = 1048576
body = request.get_data(cache=False) body = request.get_data(cache=False)
try: try:
dsn = DSN(body) parser = DSNParser(body)
dsn = parser.parsed
except MessageError as e: except MessageError as e:
app.logger.info(f'DSN: Nemohu naparsovat zprávu: {e}') app.logger.info(f'DSN: Nemohu naparsovat zprávu: {e}')
raise werkzeug.exceptions.UnprocessableEntity() raise werkzeug.exceptions.UnprocessableEntity()
app.logger.info(f'DSN: Message-ID: {dsn.message_id}') 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({}) return jsonify({})
elif dsn.verdict != 'ok': elif parser.verdict != 'ok':
raise werkzeug.exceptions.UnprocessableEntity() raise werkzeug.exceptions.UnprocessableEntity()
if not (token := dsn.find_token()): if not (token := parser.find_token()):
app.logger.info('DSN: Token not found') app.logger.info('DSN: Token not found')
raise werkzeug.exceptions.UnprocessableEntity() raise werkzeug.exceptions.UnprocessableEntity()
app.logger.info(f'DSN: Token: {token}') app.logger.info(f'DSN: Token: {token}')
try: try:
user, rr, email, when = mo.email.validate_dsn_token(token) dsn.token = token
user_info = f'#{user.user_id}' if user is not None else '-' dsn.user, dsn.reg, email, when = mo.email.validate_dsn_token(token)
rr_info = f'#{rr.reg_id}' if rr is not None else '-' 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) 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}') app.logger.info(f'DSN: user={user_info} registration={rr_info} email={email} age={age}')
except ValueError as e: except ValueError as e:
app.logger.info(f'DSN: {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({}) return jsonify({})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment