refactor(i-g): BUG-018 Pilot Split Schritt 2 — serializers+text_utils extrahiert
Check Cross-Plugin Imports / check (push) Has been cancelled

serializers.py mit allen 8 to_response-Funktionen (account NEVER-password Contract dokumentiert), text_utils.py mit extract_email_addresses+_strip_html als pure functions. Re-Export via noqa F401 in services.py — alle Consumer unveraendert. services.py jetzt 2786 Z. (von 3087).

Beweis: mail+sig_label_routes 51/51 passed nach ruff --fix; ruff clean.
This commit is contained in:
Agent Zero
2026-08-26 09:38:34 +02:00
parent a1d5e56009
commit be81fe52cf
3 changed files with 228 additions and 183 deletions
+169
View File
@@ -0,0 +1,169 @@
"""Response serializers for the Mail plugin ORM models.
Extracted from services.py as part of the God-object split (BUG-018 pilot).
Re-exported by ``app.plugins.builtins.mail.services``.
account_to_response NEVER includes passwords — security-critical contract.
"""
from __future__ import annotations
import json
from app.plugins.builtins.mail.models import (
Mail,
MailAccount,
MailAttachment,
MailFolder,
MailLabel,
MailRule,
MailSignature,
MailTemplate,
)
from app.plugins.builtins.mail.text_utils import (
extract_email_addresses, # noqa: F401 — re-exported for backwards compat
)
def attachment_to_response(att: MailAttachment) -> dict:
"""Convert a MailAttachment ORM object to a response dict."""
return {
"id": str(att.id),
"mail_id": str(att.mail_id),
"filename": att.filename,
"mime_type": att.mime_type,
"size_bytes": att.size_bytes,
"size": att.size_bytes, # alias for frontend compatibility
"content_id": att.content_id,
"is_inline": bool(att.content_id),
"dms_file_id": str(att.dms_file_id) if att.dms_file_id else None,
}
def account_to_response(account: MailAccount) -> dict:
"""Convert MailAccount to response dict, NEVER including password."""
return {
"id": str(account.id),
"email": account.email_address,
"email_address": account.email_address,
"display_name": account.display_name,
"imap_host": account.imap_host,
"imap_port": account.imap_port,
"imap_ssl": account.imap_ssl,
"smtp_host": account.smtp_host,
"smtp_port": account.smtp_port,
"smtp_tls": account.smtp_tls,
"username": account.username,
"is_shared": account.is_shared,
"is_active": account.is_active,
"sent_folder_imap_name": account.sent_folder_imap_name,
"drafts_folder_imap_name": account.drafts_folder_imap_name,
"spam_folder_imap_name": account.spam_folder_imap_name,
"trash_folder_imap_name": account.trash_folder_imap_name,
"created_at": account.created_at,
"updated_at": account.updated_at,
}
def mail_to_response(
mail: Mail,
attachments: list[MailAttachment] | None = None,
labels: list[MailLabel] | None = None,
) -> dict:
"""Convert a Mail ORM object to a response dict."""
resp = {
"id": str(mail.id),
"account_id": str(mail.account_id),
"folder_id": str(mail.folder_id),
"message_id": mail.message_id,
"thread_id": mail.thread_id,
"in_reply_to": mail.in_reply_to,
"subject": mail.subject,
"from_address": mail.from_address,
"from_name": mail.from_address.split("<")[0].strip().strip('"') if "<" in mail.from_address else mail.from_address,
"to_addresses": extract_email_addresses(mail.to_addresses) if mail.to_addresses else [],
"cc_addresses": extract_email_addresses(mail.cc_addresses) if mail.cc_addresses else [],
"bcc_addresses": extract_email_addresses(mail.bcc_addresses) if mail.bcc_addresses else [],
"body_text": mail.body_text,
"body_html": mail.body_html if mail.body_html else None,
"body_html_sanitized": mail.body_html_sanitized,
"sanitized_html": mail.body_html_sanitized,
"date": mail.received_at.isoformat() if mail.received_at else (mail.sent_at.isoformat() if mail.sent_at else None),
"is_seen": mail.is_seen,
"is_flagged": mail.is_flagged,
"flag_type": getattr(mail, 'flag_type', None),
"is_draft": mail.is_draft,
"is_answered": mail.is_answered,
"is_forwarded": mail.is_forwarded,
"has_attachments": mail.has_attachments,
"size_bytes": mail.size_bytes,
"received_at": mail.received_at,
"sent_at": mail.sent_at,
"contact_id": str(mail.contact_id) if mail.contact_id else None,
"attachments": [],
"labels": [],
}
if attachments:
resp["attachments"] = [attachment_to_response(a) for a in attachments]
if labels:
resp["labels"] = [
{"id": str(lbl.id), "name": lbl.name, "color": lbl.color} for lbl in labels
]
return resp
def folder_to_response(folder: MailFolder) -> dict:
"""Convert MailFolder to response dict."""
return {
"id": str(folder.id),
"account_id": str(folder.account_id),
"name": folder.name,
"imap_name": folder.imap_name,
"parent_id": str(folder.parent_id) if folder.parent_id else None,
"is_standard": folder.is_standard,
"unread_count": folder.unread_count,
"total_count": folder.total_count,
}
def rule_to_response(rule: MailRule) -> dict:
"""Convert MailRule to response dict."""
return {
"id": str(rule.id),
"name": rule.name,
"account_id": str(rule.account_id) if rule.account_id else None,
"priority": rule.priority,
"is_active": rule.is_active,
"conditions": json.loads(rule.conditions) if rule.conditions else {},
"actions": json.loads(rule.actions) if rule.actions else {},
}
def template_to_response(template: MailTemplate) -> dict:
"""Convert MailTemplate to response dict."""
return {
"id": str(template.id),
"name": template.name,
"subject": template.subject,
"body_html": template.body_html,
}
def signature_to_response(sig: MailSignature) -> dict:
"""Convert MailSignature to response dict."""
return {
"id": str(sig.id),
"name": sig.name,
"body_html": sig.body_html,
"account_id": str(sig.account_id) if sig.account_id else None,
"is_default": sig.is_default,
}
def label_to_response(label: MailLabel) -> dict:
"""Convert MailLabel to response dict."""
return {
"id": str(label.id),
"name": label.name,
"color": label.color,
}