refactor(i-g): BUG-018 Pilot Split Schritt 2 — serializers+text_utils extrahiert
Check Cross-Plugin Imports / check (push) Has been cancelled
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:
@@ -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,
|
||||
}
|
||||
@@ -29,25 +29,37 @@ from app.plugins.builtins.mail.crypto import ( # noqa: E402
|
||||
encrypt_password,
|
||||
generate_salt,
|
||||
)
|
||||
from app.plugins.builtins.mail.models import (
|
||||
Mail,
|
||||
MailAccount,
|
||||
MailAttachment,
|
||||
MailFolder,
|
||||
MailLabelAssignment,
|
||||
MailRule,
|
||||
MailSignature,
|
||||
VacationSentLog,
|
||||
)
|
||||
from app.plugins.builtins.mail.pgp import ( # noqa: E402,F401
|
||||
import_pgp_private_key,
|
||||
import_pgp_public_key,
|
||||
pgp_decrypt_message,
|
||||
pgp_encrypt_message,
|
||||
)
|
||||
from app.plugins.builtins.mail.models import (
|
||||
Mail,
|
||||
MailAccount,
|
||||
MailAttachment,
|
||||
MailFolder,
|
||||
MailLabel,
|
||||
MailLabelAssignment,
|
||||
MailRule,
|
||||
MailSignature,
|
||||
MailTemplate,
|
||||
VacationSentLog,
|
||||
)
|
||||
from app.plugins.builtins.mail.sanitize import sanitize_html # noqa: E402,F401
|
||||
from app.plugins.builtins.mail.serializers import ( # noqa: E402,F401
|
||||
account_to_response,
|
||||
attachment_to_response,
|
||||
folder_to_response,
|
||||
label_to_response,
|
||||
mail_to_response,
|
||||
rule_to_response,
|
||||
signature_to_response,
|
||||
template_to_response,
|
||||
)
|
||||
from app.plugins.builtins.mail.text_utils import ( # noqa: E402,F401
|
||||
_strip_html,
|
||||
extract_email_addresses,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -124,21 +136,6 @@ async def _save_attachment_to_storage(
|
||||
return storage_path
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# ─── IMAP Quota Parser ───
|
||||
|
||||
|
||||
@@ -276,31 +273,6 @@ async def get_account_password(account: MailAccount) -> str:
|
||||
return decrypt_password(account.encrypted_password, account.password_salt or 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,
|
||||
}
|
||||
|
||||
|
||||
# ─── IMAP Sync Service (F-MAIL-01) ───
|
||||
|
||||
|
||||
@@ -1867,137 +1839,6 @@ async def log_vacation_sent(
|
||||
# ─── Contact Linking (F-MAIL-10) ───
|
||||
|
||||
|
||||
def extract_email_addresses(text: str) -> list[str]:
|
||||
"""Extract email addresses from a text string."""
|
||||
if not text:
|
||||
return []
|
||||
return re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text)
|
||||
|
||||
|
||||
# ─── Utility ───
|
||||
|
||||
|
||||
def _strip_html(html: str) -> str:
|
||||
"""Simple HTML to text conversion for plain text fallback."""
|
||||
if not html:
|
||||
return ""
|
||||
# Remove tags
|
||||
text = re.sub(r"<[^>]+>", "", html)
|
||||
# Replace HTML entities
|
||||
text = (
|
||||
text.replace(" ", " ")
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", '"')
|
||||
)
|
||||
return text.strip()
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# ─── IMAP Flag Sync ───
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Text utilities for the Mail plugin.
|
||||
|
||||
Extracted from services.py as part of the God-object split (BUG-018 pilot).
|
||||
Re-exported by ``app.plugins.builtins.mail.services``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def extract_email_addresses(text: str) -> list[str]:
|
||||
"""Extract email addresses from a text string."""
|
||||
if not text:
|
||||
return []
|
||||
return re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text)
|
||||
|
||||
|
||||
def _strip_html(html: str) -> str:
|
||||
"""Simple HTML to text conversion for plain text fallback."""
|
||||
if not html:
|
||||
return ""
|
||||
# Remove tags
|
||||
text = re.sub(r"<[^>]+>", "", html)
|
||||
# Replace HTML entities
|
||||
text = (
|
||||
text.replace(" ", " ")
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", '"')
|
||||
.replace("'", "'")
|
||||
)
|
||||
# Collapse whitespace
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
Reference in New Issue
Block a user