ea6c9e71db
services.py: 3087 -> ~170 Zeilen reine Re-Export-Fassade. Alle Implementierung jetzt in 12 Sub-Modulen: accounts/crypto/drafts_sync/imap_ops/imap_sync/pgp/rules_vacation/sanitize/serializers/smtp_send/text_utils/attachments. Fixes waehrend Extraktion: (1) get_account_password async statt sync (brach send/reply/forward), (2) aiosmtplib als Modulattribut fuer Test-Mocks, (3) conftest Mock-Pfad auf imap_sync statt services, (4) test_mail.py SMTP-Mock-Pfade auf smtp_send umgestellt, (5) Fassade fehlende Symbole ergaenzt: MAX_ATTACHMENT_SIZE/_sanitize_filename/imap_create_folder/imap_delete_folder/mail_to_response. Beweis: mail+sig_label_routes 51/51 passed in 106.88s; alle 13 Sub-Module Import-OK; ruff clean; Symbol-Aufloesung MISSING: NONE.
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""Mail Account Service 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 logging
|
|
import uuid
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.plugins.builtins.mail.crypto import encrypt_password, generate_salt
|
|
from app.plugins.builtins.mail.models import (
|
|
MailAccount,
|
|
MailFolder,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def create_mail_account(
|
|
db: AsyncSession, *, tenant_id: uuid.UUID, user_id: uuid.UUID, data: dict
|
|
) -> MailAccount:
|
|
"""Create a new mail account with encrypted password."""
|
|
salt = generate_salt()
|
|
account = MailAccount(
|
|
tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
owner_id=user_id,
|
|
email_address=data["email_address"],
|
|
display_name=data.get("display_name", ""),
|
|
imap_host=data["imap_host"],
|
|
imap_port=data.get("imap_port", 993),
|
|
imap_ssl=data.get("imap_ssl", True),
|
|
smtp_host=data["smtp_host"],
|
|
smtp_port=data.get("smtp_port", 587),
|
|
smtp_tls=data.get("smtp_tls", True),
|
|
username=data.get("username") or data["email_address"],
|
|
encrypted_password=encrypt_password(data["password"], salt),
|
|
password_salt=salt,
|
|
is_shared=data.get("is_shared", False),
|
|
is_active=True,
|
|
sent_folder_imap_name=data.get("sent_folder_imap_name"),
|
|
drafts_folder_imap_name=data.get("drafts_folder_imap_name"),
|
|
spam_folder_imap_name=data.get("spam_folder_imap_name"),
|
|
trash_folder_imap_name=data.get("trash_folder_imap_name"),
|
|
)
|
|
db.add(account)
|
|
await db.flush()
|
|
|
|
# Create INBOX first so subfolders can reference it as parent
|
|
inbox_folder = MailFolder(
|
|
tenant_id=tenant_id,
|
|
account_id=account.id,
|
|
name="Posteingang",
|
|
imap_name="INBOX",
|
|
is_standard=True,
|
|
)
|
|
db.add(inbox_folder)
|
|
await db.flush()
|
|
|
|
# Create standard subfolders under INBOX (IMAP server uses '.' delimiter)
|
|
for fname, imap_name in [
|
|
("Gesendet", "INBOX.Sent"),
|
|
("Entwürfe", "INBOX.Drafts"),
|
|
("Papierkorb", "INBOX.Trash"),
|
|
("Spam", "INBOX.spam"),
|
|
]:
|
|
folder = MailFolder(
|
|
tenant_id=tenant_id,
|
|
account_id=account.id,
|
|
name=fname,
|
|
imap_name=imap_name,
|
|
parent_id=inbox_folder.id,
|
|
is_standard=True,
|
|
)
|
|
db.add(folder)
|
|
await db.flush()
|
|
return account
|
|
|
|
|
|
async def update_mail_account(db: AsyncSession, account: MailAccount, data: dict) -> MailAccount:
|
|
"""Update a mail account, encrypting password if changed."""
|
|
field_map = {
|
|
"email": "email_address",
|
|
"email_address": "email_address",
|
|
"display_name": "display_name",
|
|
"imap_host": "imap_host",
|
|
"imap_port": "imap_port",
|
|
"imap_ssl": "imap_ssl",
|
|
"smtp_host": "smtp_host",
|
|
"smtp_port": "smtp_port",
|
|
"smtp_tls": "smtp_tls",
|
|
"username": "username",
|
|
"is_shared": "is_shared",
|
|
"is_active": "is_active",
|
|
"sent_folder_imap_name": "sent_folder_imap_name",
|
|
"drafts_folder_imap_name": "drafts_folder_imap_name",
|
|
"spam_folder_imap_name": "spam_folder_imap_name",
|
|
"trash_folder_imap_name": "trash_folder_imap_name",
|
|
}
|
|
for api_field, model_field in field_map.items():
|
|
if api_field in data and data[api_field] is not None:
|
|
setattr(account, model_field, data[api_field])
|
|
if "password" in data and data["password"] is not None:
|
|
new_salt = generate_salt()
|
|
account.password_salt = new_salt
|
|
account.encrypted_password = encrypt_password(data["password"], new_salt)
|
|
await db.flush()
|
|
await db.refresh(account)
|
|
return account
|
|
|
|
|