intermittent commit
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from email import policy
|
||||
from email.message import EmailMessage
|
||||
from typing import Any
|
||||
@@ -19,6 +20,30 @@ class MockCampaignSendError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _MockSendOutcome:
|
||||
row: dict[str, Any]
|
||||
sent_count: int = 0
|
||||
failed_count: int = 0
|
||||
skipped_count: int = 0
|
||||
imap_appended_count: int = 0
|
||||
imap_failed_count: int = 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _MockSendBatch:
|
||||
results: list[dict[str, Any]]
|
||||
sent_count: int = 0
|
||||
failed_count: int = 0
|
||||
skipped_count: int = 0
|
||||
imap_appended_count: int = 0
|
||||
imap_failed_count: int = 0
|
||||
|
||||
@property
|
||||
def attempted_count(self) -> int:
|
||||
return self.sent_count + self.failed_count
|
||||
|
||||
|
||||
def _mock_mailbox() -> Any | None:
|
||||
return mail_integration().mock_mailbox()
|
||||
|
||||
@@ -116,6 +141,163 @@ def _can_mock_send(
|
||||
return False, f"Validation status is {message.validation_status.value}"
|
||||
|
||||
|
||||
def _mock_send_batch(
|
||||
*,
|
||||
config: Any,
|
||||
built_messages: list[Any],
|
||||
mailbox: Any | None,
|
||||
send: bool,
|
||||
include_warnings: bool,
|
||||
include_needs_review: bool,
|
||||
append_sent: bool,
|
||||
) -> _MockSendBatch:
|
||||
batch = _MockSendBatch(results=[])
|
||||
for built in built_messages:
|
||||
outcome = _mock_send_message(
|
||||
config=config,
|
||||
built=built,
|
||||
mailbox=mailbox,
|
||||
send=send,
|
||||
include_warnings=include_warnings,
|
||||
include_needs_review=include_needs_review,
|
||||
append_sent=append_sent,
|
||||
)
|
||||
batch.results.append(outcome.row)
|
||||
batch.sent_count += outcome.sent_count
|
||||
batch.failed_count += outcome.failed_count
|
||||
batch.skipped_count += outcome.skipped_count
|
||||
batch.imap_appended_count += outcome.imap_appended_count
|
||||
batch.imap_failed_count += outcome.imap_failed_count
|
||||
return batch
|
||||
|
||||
|
||||
def _mock_send_message(
|
||||
*,
|
||||
config: Any,
|
||||
built: Any,
|
||||
mailbox: Any | None,
|
||||
send: bool,
|
||||
include_warnings: bool,
|
||||
include_needs_review: bool,
|
||||
append_sent: bool,
|
||||
) -> _MockSendOutcome:
|
||||
draft = built.draft
|
||||
row = _mock_send_row(draft)
|
||||
can_send, skip_reason = _can_mock_send(
|
||||
draft,
|
||||
include_warnings=include_warnings,
|
||||
include_needs_review=include_needs_review,
|
||||
)
|
||||
if not can_send or built.mime is None:
|
||||
row.update({"status": "skipped", "message": skip_reason or "Message has no MIME output"})
|
||||
return _MockSendOutcome(row=row, skipped_count=1)
|
||||
|
||||
recipients = _recipient_emails(draft)
|
||||
envelope_from = _envelope_from(draft)
|
||||
if not recipients:
|
||||
row.update({"status": "skipped", "message": "No envelope recipients"})
|
||||
return _MockSendOutcome(row=row, skipped_count=1)
|
||||
if not send:
|
||||
row.update({
|
||||
"status": "ready",
|
||||
"message": f"Would send to {len(recipients)} recipient(s)",
|
||||
"envelope_from": envelope_from,
|
||||
"envelope_recipients": recipients,
|
||||
})
|
||||
return _MockSendOutcome(row=row)
|
||||
return _send_mock_smtp_message(
|
||||
config=config,
|
||||
built=built,
|
||||
mailbox=mailbox,
|
||||
row=row,
|
||||
recipients=recipients,
|
||||
envelope_from=envelope_from,
|
||||
append_sent=append_sent,
|
||||
)
|
||||
|
||||
|
||||
def _mock_send_row(draft: MessageDraft) -> dict[str, Any]:
|
||||
return {
|
||||
"entry_index": draft.entry_index,
|
||||
"entry_id": draft.entry_id,
|
||||
"subject": draft.subject,
|
||||
"validation_status": draft.validation_status.value,
|
||||
"build_status": str(draft.build_status.value if hasattr(draft.build_status, "value") else draft.build_status),
|
||||
"to": _message_addresses_payload(draft.to),
|
||||
"attachments": _attachment_payloads(draft),
|
||||
"issues": _issue_payloads(draft),
|
||||
}
|
||||
|
||||
|
||||
def _send_mock_smtp_message(
|
||||
*,
|
||||
config: Any,
|
||||
built: Any,
|
||||
mailbox: Any | None,
|
||||
row: dict[str, Any],
|
||||
recipients: list[str],
|
||||
envelope_from: str,
|
||||
append_sent: bool,
|
||||
) -> _MockSendOutcome:
|
||||
try:
|
||||
if mailbox is not None and mailbox.consume_fail_next_smtp():
|
||||
raise MockCampaignSendError("Configured mock failure: next SMTP delivery fails")
|
||||
rejected = _smtp_rejection_matches(recipients)
|
||||
if rejected and len(rejected) == len(recipients):
|
||||
raise MockCampaignSendError(f"Configured mock failure: all recipients rejected ({', '.join(rejected)})")
|
||||
|
||||
accepted = [recipient for recipient in recipients if recipient not in rejected]
|
||||
smtp_record = mailbox.record_smtp_delivery(
|
||||
built.mime,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=accepted,
|
||||
smtp_host="mock.smtp.local",
|
||||
)
|
||||
row.update({
|
||||
"status": "sent",
|
||||
"message": f"Mock SMTP captured as {smtp_record.id}",
|
||||
"smtp_message_id": smtp_record.id,
|
||||
"envelope_from": envelope_from,
|
||||
"envelope_recipients": accepted,
|
||||
"refused_recipients": rejected,
|
||||
})
|
||||
imap_appended, imap_failed = _append_mock_sent_message(config, built, mailbox, row, append_sent=append_sent)
|
||||
return _MockSendOutcome(row=row, sent_count=1, imap_appended_count=imap_appended, imap_failed_count=imap_failed)
|
||||
except Exception as exc:
|
||||
row.update({"status": "failed", "message": str(exc), "envelope_from": envelope_from, "envelope_recipients": recipients})
|
||||
return _MockSendOutcome(row=row, failed_count=1)
|
||||
|
||||
|
||||
def _append_mock_sent_message(
|
||||
config: Any,
|
||||
built: Any,
|
||||
mailbox: Any | None,
|
||||
row: dict[str, Any],
|
||||
*,
|
||||
append_sent: bool,
|
||||
) -> tuple[int, int]:
|
||||
if not append_sent:
|
||||
return 0, 0
|
||||
try:
|
||||
if mailbox is not None and mailbox.consume_fail_next_imap():
|
||||
raise MockCampaignSendError("Configured mock failure: next IMAP append fails")
|
||||
folder = _mock_sent_folder(config)
|
||||
imap_record = mailbox.record_imap_append(_raw_message_bytes(built.mime), folder=folder, imap_host="mock.imap.local")
|
||||
row.update({"imap_status": "appended", "imap_message_id": imap_record.id, "imap_folder": folder})
|
||||
return 1, 0
|
||||
except Exception as exc:
|
||||
row.update({"imap_status": "failed", "imap_error": str(exc)})
|
||||
return 0, 1
|
||||
|
||||
|
||||
def _mock_sent_folder(config: Any) -> str:
|
||||
if config.delivery.imap_append_sent.folder and config.delivery.imap_append_sent.folder != "auto":
|
||||
return config.delivery.imap_append_sent.folder
|
||||
if config.server.imap and config.server.imap.sent_folder and config.server.imap.sent_folder != "auto":
|
||||
return config.server.imap.sent_folder
|
||||
return "Sent"
|
||||
|
||||
|
||||
def run_mock_campaign_send(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -158,7 +340,7 @@ def run_mock_campaign_send(
|
||||
campaign_id=campaign.id,
|
||||
raw_json=version.raw_json if isinstance(version.raw_json, dict) else {},
|
||||
include_bytes=True,
|
||||
prefix="multimailer-mock-send-",
|
||||
prefix="govoplan-mock-send-",
|
||||
) as prepared:
|
||||
prepared_raw = load_campaign_json(prepared.path)
|
||||
config = load_campaign_config_from_json(session, tenant_id=tenant_id, raw_json=prepared_raw, campaign_id=campaign.id)
|
||||
@@ -166,86 +348,15 @@ def run_mock_campaign_send(
|
||||
build_result = build_campaign_messages(config, campaign_file=prepared.path, write_eml=False)
|
||||
files.annotate_built_messages_with_managed_files(build_result.built_messages, prepared.managed_files_by_local_path)
|
||||
|
||||
send_results: list[dict[str, Any]] = []
|
||||
sent_count = 0
|
||||
failed_count = 0
|
||||
skipped_count = 0
|
||||
imap_appended_count = 0
|
||||
imap_failed_count = 0
|
||||
|
||||
for built in build_result.built_messages:
|
||||
draft = built.draft
|
||||
can_send, skip_reason = _can_mock_send(draft, include_warnings=include_warnings, include_needs_review=include_needs_review)
|
||||
row: dict[str, Any] = {
|
||||
"entry_index": draft.entry_index,
|
||||
"entry_id": draft.entry_id,
|
||||
"subject": draft.subject,
|
||||
"validation_status": draft.validation_status.value,
|
||||
"build_status": str(draft.build_status.value if hasattr(draft.build_status, "value") else draft.build_status),
|
||||
"to": _message_addresses_payload(draft.to),
|
||||
"attachments": _attachment_payloads(draft),
|
||||
"issues": _issue_payloads(draft),
|
||||
}
|
||||
|
||||
if not can_send or built.mime is None:
|
||||
skipped_count += 1
|
||||
row.update({"status": "skipped", "message": skip_reason or "Message has no MIME output"})
|
||||
send_results.append(row)
|
||||
continue
|
||||
|
||||
recipients = _recipient_emails(draft)
|
||||
envelope_from = _envelope_from(draft)
|
||||
if not recipients:
|
||||
skipped_count += 1
|
||||
row.update({"status": "skipped", "message": "No envelope recipients"})
|
||||
send_results.append(row)
|
||||
continue
|
||||
|
||||
if not send:
|
||||
row.update({"status": "ready", "message": f"Would send to {len(recipients)} recipient(s)", "envelope_from": envelope_from, "envelope_recipients": recipients})
|
||||
send_results.append(row)
|
||||
continue
|
||||
|
||||
try:
|
||||
if mailbox is not None and mailbox.consume_fail_next_smtp():
|
||||
raise MockCampaignSendError("Configured mock failure: next SMTP delivery fails")
|
||||
rejected = _smtp_rejection_matches(recipients)
|
||||
if rejected and len(rejected) == len(recipients):
|
||||
raise MockCampaignSendError(f"Configured mock failure: all recipients rejected ({', '.join(rejected)})")
|
||||
|
||||
accepted = [recipient for recipient in recipients if recipient not in rejected]
|
||||
smtp_record = mailbox.record_smtp_delivery(built.mime, envelope_from=envelope_from, envelope_recipients=accepted, smtp_host="mock.smtp.local")
|
||||
sent_count += 1
|
||||
row.update({
|
||||
"status": "sent",
|
||||
"message": f"Mock SMTP captured as {smtp_record.id}",
|
||||
"smtp_message_id": smtp_record.id,
|
||||
"envelope_from": envelope_from,
|
||||
"envelope_recipients": accepted,
|
||||
"refused_recipients": rejected,
|
||||
})
|
||||
|
||||
if append_sent:
|
||||
try:
|
||||
if mailbox is not None and mailbox.consume_fail_next_imap():
|
||||
raise MockCampaignSendError("Configured mock failure: next IMAP append fails")
|
||||
folder = "Sent"
|
||||
if config.delivery.imap_append_sent.folder and config.delivery.imap_append_sent.folder != "auto":
|
||||
folder = config.delivery.imap_append_sent.folder
|
||||
elif config.server.imap and config.server.imap.sent_folder and config.server.imap.sent_folder != "auto":
|
||||
folder = config.server.imap.sent_folder
|
||||
imap_record = mailbox.record_imap_append(_raw_message_bytes(built.mime), folder=folder, imap_host="mock.imap.local")
|
||||
imap_appended_count += 1
|
||||
row.update({"imap_status": "appended", "imap_message_id": imap_record.id, "imap_folder": folder})
|
||||
except Exception as exc:
|
||||
imap_failed_count += 1
|
||||
row.update({"imap_status": "failed", "imap_error": str(exc)})
|
||||
|
||||
except Exception as exc:
|
||||
failed_count += 1
|
||||
row.update({"status": "failed", "message": str(exc), "envelope_from": envelope_from, "envelope_recipients": recipients})
|
||||
|
||||
send_results.append(row)
|
||||
send_batch = _mock_send_batch(
|
||||
config=config,
|
||||
built_messages=build_result.built_messages,
|
||||
mailbox=mailbox,
|
||||
send=send,
|
||||
include_warnings=include_warnings,
|
||||
include_needs_review=include_needs_review,
|
||||
append_sent=append_sent,
|
||||
)
|
||||
|
||||
validation_json = validation_report.model_dump(mode="json")
|
||||
validation_json.update({"ok": validation_report.ok, "error_count": validation_report.error_count, "warning_count": validation_report.warning_count})
|
||||
@@ -261,7 +372,6 @@ def run_mock_campaign_send(
|
||||
"messages": [_message_payload(message) for message in build_report.messages],
|
||||
})
|
||||
|
||||
attempted_count = sum(1 for row in send_results if row.get("status") in {"sent", "failed"})
|
||||
return {
|
||||
"campaign_id": campaign.id,
|
||||
"version_id": version.id,
|
||||
@@ -273,19 +383,19 @@ def run_mock_campaign_send(
|
||||
"steps": [
|
||||
{"key": "validate", "label": "Validate campaign JSON", "status": "ok" if validation_report.ok else "needs_review", "summary": validation_json},
|
||||
{"key": "build", "label": "Build messages", "status": "ok" if build_report.queueable_count else "needs_review", "summary": {"built": build_report.built_count, "queueable": build_report.queueable_count, "needs_review": build_report.needs_review_count, "blocked": build_report.blocked_count}},
|
||||
{"key": "send", "label": "Mock SMTP delivery", "status": "skipped" if not send else ("ok" if failed_count == 0 else "needs_review"), "summary": {"attempted": attempted_count, "sent": sent_count, "failed": failed_count, "skipped": skipped_count}},
|
||||
{"key": "imap", "label": "Mock IMAP Sent append", "status": "skipped" if not send or not append_sent else ("ok" if imap_failed_count == 0 else "needs_review"), "summary": {"appended": imap_appended_count, "failed": imap_failed_count}},
|
||||
{"key": "send", "label": "Mock SMTP delivery", "status": "skipped" if not send else ("ok" if send_batch.failed_count == 0 else "needs_review"), "summary": {"attempted": send_batch.attempted_count, "sent": send_batch.sent_count, "failed": send_batch.failed_count, "skipped": send_batch.skipped_count}},
|
||||
{"key": "imap", "label": "Mock IMAP Sent append", "status": "skipped" if not send or not append_sent else ("ok" if send_batch.imap_failed_count == 0 else "needs_review"), "summary": {"appended": send_batch.imap_appended_count, "failed": send_batch.imap_failed_count}},
|
||||
],
|
||||
"validation": validation_json,
|
||||
"build": build_json,
|
||||
"send": {
|
||||
"attempted_count": attempted_count,
|
||||
"sent_count": sent_count,
|
||||
"failed_count": failed_count,
|
||||
"skipped_count": skipped_count,
|
||||
"imap_appended_count": imap_appended_count,
|
||||
"imap_failed_count": imap_failed_count,
|
||||
"results": send_results,
|
||||
"attempted_count": send_batch.attempted_count,
|
||||
"sent_count": send_batch.sent_count,
|
||||
"failed_count": send_batch.failed_count,
|
||||
"skipped_count": send_batch.skipped_count,
|
||||
"imap_appended_count": send_batch.imap_appended_count,
|
||||
"imap_failed_count": send_batch.imap_failed_count,
|
||||
"results": send_batch.results,
|
||||
},
|
||||
"mailbox": {"messages": mailbox.list_records(limit=200) if mailbox is not None else []},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user