feat(campaign): enforce attachment delivery policies

This commit is contained in:
2026-07-20 20:07:12 +02:00
parent 724ca779d6
commit ad34365f6c
9 changed files with 613 additions and 15 deletions

View File

@@ -245,6 +245,26 @@ def _message_issues_from_attachment_resolution(resolution: EntryAttachmentResolu
]
def _append_no_attachment_coverage_issue(issues: list[MessageIssue]) -> None:
if any(issue.code == "missing_attachment_coverage" for issue in issues):
return
issues.append(
MessageIssue(
severity="error",
code="missing_attachment_coverage",
message="No attachment file was resolved for this message, and sending without attachments is not allowed.",
behavior=Behavior.BLOCK.value,
source="attachments",
)
)
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("._")
@@ -391,6 +411,7 @@ def _attach_files(
for attachment in resolution.attachments:
attachment.message_filenames = []
attachment.zip_entry_names = []
attachment.zip_filename = None
for attachment in resolution.attachments:
if attachment.status != AttachmentMatchStatus.OK or not attachment.matches:
@@ -435,6 +456,7 @@ def _attach_files(
work_dir / "_zip" / f"entry-{entry_index:04d}" / _safe_filename(archive.id, "archive") / filename,
members,
password,
archive.method.value,
)
data, maintype, subtype = _attachment_bytes(archive_path)
message.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename)
@@ -740,6 +762,14 @@ 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:
_append_no_attachment_coverage_issue(context.issues)
return _MimeBuildResult(
message=None,
build_status=BuildStatus.BUILD_FAILED,
validation_status=MessageValidationStatus.BLOCKED,
attachment_count=0,
)
return _MimeBuildResult(
message=message,
build_status=BuildStatus.BUILT,