feat: govern attachment exceptions and ownership transfers

This commit is contained in:
2026-07-30 17:42:10 +02:00
parent cd223cbb95
commit 5f7503598c
28 changed files with 2229 additions and 180 deletions
@@ -16,6 +16,7 @@ from govoplan_campaign.backend.attachments.resolver import (
EntryAttachmentResolution,
MessageAttachmentStatus,
ResolvedAttachment,
effective_send_without_attachments_behavior,
resolve_entry_attachments,
)
from govoplan_campaign.backend.campaign.addressing import effective_address_lists, formatted_recipient
@@ -172,6 +173,11 @@ def _attachment_summaries(resolution: EntryAttachmentResolution) -> list[Message
label=attachment.label,
status=attachment.status.value,
behavior=attachment.behavior.value if attachment.behavior else None,
missing_policy=(
attachment.missing_policy.model_dump(mode="json")
if attachment.missing_policy
else None
),
required=attachment.required,
allow_multiple=attachment.allow_multiple,
zip_enabled=attachment.zip_enabled,
@@ -200,6 +206,7 @@ def _message_issues_from_attachment_resolution(resolution: EntryAttachmentResolu
message=issue.message,
behavior=issue.behavior.value if issue.behavior else None,
source="attachments",
details=issue.details,
)
for issue in resolution.issues
]
@@ -219,12 +226,6 @@ def _append_no_attachment_coverage_issue(issues: list[MessageIssue]) -> None:
)
def _send_without_attachments_behavior(config: CampaignConfig) -> Behavior:
return config.attachments.send_without_attachments_behavior or (
Behavior.CONTINUE if config.attachments.send_without_attachments else Behavior.BLOCK
)
def _safe_filename(value: str | None, fallback: str) -> str:
raw = value or fallback
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", raw).strip("._")
@@ -761,7 +762,11 @@ def _build_mime_message(
values=rendered.values,
work_dir=work_dir,
)
if attachment_count == 0 and context.resolution.attachments and _send_without_attachments_behavior(config) == Behavior.BLOCK:
if (
attachment_count == 0
and context.resolution.attachments
and effective_send_without_attachments_behavior(config) == Behavior.BLOCK
):
_append_no_attachment_coverage_issue(context.issues)
return _MimeBuildResult(
message=None,
@@ -785,6 +790,16 @@ def _build_mime_message(
source="attachments",
)
)
except OSError as exc:
context.issues.append(
MessageIssue(
severity="error",
code="attachment_unreadable",
message="A resolved attachment could not be read while building the message.",
behavior="block",
source=f"attachments:{type(exc).__name__}",
)
)
except Exception as exc:
context.issues.append(
MessageIssue(