Bind Mail effects to stable delivery attempts

This commit is contained in:
2026-08-03 05:00:30 +02:00
parent 50ce8b0acb
commit 5df26be074
3 changed files with 26 additions and 1 deletions
+7
View File
@@ -76,6 +76,13 @@ and uncertain or stranded effects remain `outcome_unknown` or
`recovery_required` in Ops. Campaign jobs, message actions, and channel-attempt `recovery_required` in Ops. Campaign jobs, message actions, and channel-attempt
records remain the business source of truth. records remain the business source of truth.
For SMTP and Sent-folder APPEND, Campaign also passes the stable job/action and
attempt identifier into Mail. Mail establishes its own provider-bound recovery
fence after profile authorization and policy checks but before network I/O.
This nested ownership is intentional: Campaign proves its business transition,
while Mail proves the transport effect. Neither layer replays a completed or
unknown provider attempt merely to repair the other layer's state.
- `smtp_accepted`: Do not retry. If IMAP append is enabled and pending, run or - `smtp_accepted`: Do not retry. If IMAP append is enabled and pending, run or
enqueue the append action. enqueue the append action.
- `failed_temporary`: Retry explicitly after checking the error and retry count. - `failed_temporary`: Retry explicitly after checking the error and retry count.
@@ -2321,6 +2321,11 @@ def _perform_single_message_direct_effect(
), ),
smtp_server_id=delivery_context.snapshot.smtp_server_id, smtp_server_id=delivery_context.snapshot.smtp_server_id,
smtp_credential_id=delivery_context.snapshot.smtp_credential_id, smtp_credential_id=delivery_context.snapshot.smtp_credential_id,
recovery_effect_id=(
f"campaign-single:{action.id}:smtp-attempt:{attempt.attempt_number}"
),
recovery_resource_type="campaign_message_action",
recovery_resource_id=action.id,
) )
except SmtpSendError as exc: except SmtpSendError as exc:
if exc.outcome_unknown: if exc.outcome_unknown:
@@ -3633,6 +3638,11 @@ def _send_claimed_mail_only_job(
or "", or "",
smtp_server_id=context.snapshot.smtp_server_id, smtp_server_id=context.snapshot.smtp_server_id,
smtp_credential_id=context.snapshot.smtp_credential_id, smtp_credential_id=context.snapshot.smtp_credential_id,
recovery_effect_id=(
f"campaign-job:{job.id}:smtp-attempt:{attempt.attempt_number}"
),
recovery_resource_type="campaign_job",
recovery_resource_id=job.id,
) )
if result.accepted_count <= 0: if result.accepted_count <= 0:
raise SmtpSendError( raise SmtpSendError(
@@ -4780,6 +4790,11 @@ def _invoke_imap_append(
smtp_credential_id=snapshot.smtp_credential_id, smtp_credential_id=snapshot.smtp_credential_id,
imap_server_id=snapshot.imap_server_id, imap_server_id=snapshot.imap_server_id,
imap_credential_id=snapshot.imap_credential_id, imap_credential_id=snapshot.imap_credential_id,
recovery_effect_id=(
f"campaign-job:{job.id}:imap-attempt:{claimed.attempt.attempt_number}"
),
recovery_resource_type="campaign_job",
recovery_resource_id=job.id,
) )
+4 -1
View File
@@ -181,7 +181,10 @@ class CampaignQueueSelectionTests(unittest.TestCase):
) )
with ( with (
patch("govoplan_campaign.backend.sending.jobs.mail_integration", return_value=Mail()), patch("govoplan_campaign.backend.sending.jobs.mail_integration", return_value=Mail()),
patch("govoplan_campaign.backend.sending.jobs._record_attempt_start", return_value=object()), patch(
"govoplan_campaign.backend.sending.jobs._record_attempt_start",
return_value=SimpleNamespace(attempt_number=1),
),
patch( patch(
"govoplan_campaign.backend.sending.jobs._record_smtp_send_success", "govoplan_campaign.backend.sending.jobs._record_smtp_send_success",
side_effect=OSError("storage unavailable"), side_effect=OSError("storage unavailable"),