Basisimplementierung für Mailversandt.
This commit is contained in:
parent
8505e9217a
commit
531b9cb10e
14 changed files with 520 additions and 41 deletions
67
app/workers/mail_send_worker.py
Normal file
67
app/workers/mail_send_worker.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
# app/workers/mail_send_worker.py
|
||||
from __future__ import annotations
|
||||
import smtplib, ssl, email.utils
|
||||
from email.message import EmailMessage
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.workers.base_worker import BaseWorker
|
||||
from app.config import Settings
|
||||
from app.db import SessionLocal # <- AsyncSession factory
|
||||
from app.models import Task, TaskStatus
|
||||
from sqlalchemy import update
|
||||
from app.job_registry import routing_key
|
||||
from app.config import settings
|
||||
|
||||
class _SendPayload(BaseModel):
|
||||
mailbox_id: str
|
||||
recipients: List[EmailStr]
|
||||
subject: str
|
||||
body_text: Optional[str] = None
|
||||
body_html: Optional[str] = None
|
||||
sender: Optional[EmailStr] = None
|
||||
|
||||
class MailSendWorker(BaseWorker):
|
||||
|
||||
async def process(self, payload: dict, **meta) -> dict:
|
||||
"""
|
||||
Erwartet: task.job.payload (JSON) mit Feldern wie in _SendPayload.
|
||||
Setzt Task-Status/Result analog zu eurem BaseWorker-Pattern.
|
||||
"""
|
||||
p = _SendPayload(**payload)
|
||||
|
||||
sender = str(p.sender or settings.smtp_sender_fallback)
|
||||
msg = EmailMessage()
|
||||
msg["From"] = sender
|
||||
msg["To"] = ", ".join([str(r) for r in p.recipients])
|
||||
msg["Subject"] = p.subject
|
||||
msg["Date"] = email.utils.formatdate(localtime=True)
|
||||
msg["Message-ID"] = email.utils.make_msgid()
|
||||
|
||||
if p.body_html and p.body_text:
|
||||
msg.set_content(p.body_text)
|
||||
msg.add_alternative(p.body_html, subtype="html")
|
||||
elif p.body_html:
|
||||
msg.add_alternative(p.body_html, subtype="html")
|
||||
else:
|
||||
msg.set_content(p.body_text or "")
|
||||
|
||||
# SMTP – synchrones I/O im Worker-Kontext ist i.d.R. ok.
|
||||
context = ssl.create_default_context()
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30) as server:
|
||||
if settings.smtp_starttls:
|
||||
server.starttls(context=context)
|
||||
if settings.smtp_user and settings.smtp_password:
|
||||
server.login(settings.smtp_user, settings.smtp_password)
|
||||
server.send_message(msg)
|
||||
|
||||
return {"message_id": msg["Message-ID"]}
|
||||
|
||||
async def main():
|
||||
worker =MailSendWorker(routing_key("MailSend", "SendClean"), worker_id="mail-send-clean-1")
|
||||
await worker.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio as _a
|
||||
_a.run(main())
|
||||
|
||||
70
app/workers/mail_store_worker.py
Normal file
70
app/workers/mail_store_worker.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
|
||||
# app/workers/mail_store_worker.py
|
||||
from __future__ import annotations
|
||||
import base64, email.utils
|
||||
from email import policy
|
||||
from email.parser import BytesParser
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from sqlalchemy import select, update
|
||||
from app.workers.base_worker import BaseWorker
|
||||
from app.db import SessionLocal
|
||||
from app.models import Message, Task, TaskStatus
|
||||
|
||||
class MailStoreWorker(BaseWorker):
|
||||
routing_key = "mail.store"
|
||||
|
||||
async def handle(self, task) -> dict:
|
||||
payload = task.job.payload # payload liegt auf dem Job
|
||||
raw = base64.b64decode(payload["raw_eml_b64"])
|
||||
mailbox_id = UUID(payload["mailbox_id"])
|
||||
|
||||
msg = BytesParser(policy=policy.default).parsebytes(raw)
|
||||
message_id = msg.get("Message-ID")
|
||||
subject = msg.get("Subject")
|
||||
from_addr = msg.get("From")
|
||||
to_addr = msg.get("To")
|
||||
dt = None
|
||||
try:
|
||||
dt = email.utils.parsedate_to_datetime(msg.get("Date")) if msg.get("Date") else None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async with SessionLocal() as session:
|
||||
# Idempotenz: bei Message-ID prüfen
|
||||
if message_id:
|
||||
q = select(Message).where(Message.message_id == message_id)
|
||||
existing = (await session.execute(q)).scalar_one_or_none()
|
||||
if existing:
|
||||
result = {"id": str(existing.id), "message_id": message_id, "dedup": True}
|
||||
await session.execute(
|
||||
update(Task)
|
||||
.where(Task.id == task.id)
|
||||
.values(status=TaskStatus.FINISHED, result=result, worker_id=self.worker_id)
|
||||
)
|
||||
await session.commit()
|
||||
return result
|
||||
|
||||
m = Message(
|
||||
mailbox_id=mailbox_id,
|
||||
message_id=message_id,
|
||||
subject=subject,
|
||||
from_addr=from_addr,
|
||||
to_addr=to_addr,
|
||||
date_header=dt,
|
||||
received_at=datetime.utcnow(),
|
||||
raw_eml=raw,
|
||||
)
|
||||
session.add(m)
|
||||
await session.commit() # id verfügbar, da PG/UUID serverseitig
|
||||
|
||||
result = {"id": str(m.id), "message_id": message_id}
|
||||
await session.execute(
|
||||
update(Task)
|
||||
.where(Task.id == task.id)
|
||||
.values(status=TaskStatus.FINISHED, result=result, worker_id=self.worker_id)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
return result
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue