feat: add durable mail delivery outbox

This commit is contained in:
2026-07-30 14:26:53 +02:00
parent dec5a4e350
commit b8029c24d6
13 changed files with 1913 additions and 17 deletions

View File

@@ -152,3 +152,137 @@ class MailMailboxMessageIndex(Base, TimestampMixin):
body_preview: Mapped[str | None] = mapped_column(Text, nullable=True)
attachment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
indexed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
class MailDeliveryCommand(Base, TimestampMixin):
__tablename__ = "mail_delivery_commands"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"command_type",
"idempotency_key",
name="uq_mail_delivery_commands_idempotency",
),
Index(
"ix_mail_delivery_commands_dispatch",
"status",
"next_attempt_at",
"created_at",
),
Index(
"ix_mail_delivery_commands_source",
"tenant_id",
"source_module",
"source_resource_type",
"source_resource_id",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
command_type: Mapped[str] = mapped_column(String(60), nullable=False, index=True)
source_module: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
source_resource_type: Mapped[str] = mapped_column(String(80), nullable=False)
source_resource_id: Mapped[str | None] = mapped_column(String(120), nullable=True)
source_version_id: Mapped[str | None] = mapped_column(String(120), nullable=True)
idempotency_key: Mapped[str] = mapped_column(String(200), nullable=False)
canonical_request_hash: Mapped[str] = mapped_column(String(64), nullable=False)
profile_id: Mapped[str] = mapped_column(
ForeignKey("mail_server_profiles.id", ondelete="RESTRICT"),
nullable=False,
index=True,
)
smtp_server_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
smtp_credential_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
expected_smtp_transport_revision: Mapped[str] = mapped_column(String(120), nullable=False)
envelope_from_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
envelope_recipients_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
from_header_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
message_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
message_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
message_size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False)
recipient_count: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(40), nullable=False, default="pending", index=True)
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
next_attempt_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
index=True,
)
claimed_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
effect_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
accepted_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
refused_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
refusal_summary: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
refusal_details_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
failure_code: Mapped[str | None] = mapped_column(String(80), nullable=True)
failure_summary: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("access_users.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
supersedes_command_id: Mapped[str | None] = mapped_column(
ForeignKey("mail_delivery_commands.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
payload_purged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class MailDeliveryAttempt(Base, TimestampMixin):
__tablename__ = "mail_delivery_attempts"
__table_args__ = (
UniqueConstraint(
"command_id",
"attempt_number",
name="uq_mail_delivery_attempts_number",
),
Index("ix_mail_delivery_attempts_command_started", "command_id", "started_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
command_id: Mapped[str] = mapped_column(
ForeignKey("mail_delivery_commands.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
attempt_number: Mapped[int] = mapped_column(Integer, nullable=False)
worker_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
effect_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
accepted_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
refused_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
outcome_code: Mapped[str | None] = mapped_column(String(80), nullable=True)
diagnostic_summary: Mapped[str | None] = mapped_column(String(500), nullable=True)
class MailDeliveryReconciliation(Base, TimestampMixin):
__tablename__ = "mail_delivery_reconciliations"
__table_args__ = (
Index(
"ix_mail_delivery_reconciliations_command_created",
"command_id",
"created_at",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
command_id: Mapped[str] = mapped_column(
ForeignKey("mail_delivery_commands.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
decision: Mapped[str] = mapped_column(String(40), nullable=False)
evidence_reference: Mapped[str] = mapped_column(String(500), nullable=False)
note_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("access_users.id", ondelete="SET NULL"),
nullable=True,
index=True,
)