diff --git a/mo/web/api_dsn.py b/mo/web/api_dsn.py
index 64de762c82662260dbc34b36ff9b9f8eb34d8d41..99194148f339e5526240fa03d1400914b91f0987 100644
--- a/mo/web/api_dsn.py
+++ b/mo/web/api_dsn.py
@@ -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({})