Add bulk calendar invitation delivery

This commit is contained in:
2026-08-02 16:38:48 +02:00
parent 7733265cc8
commit bf6e07f307
17 changed files with 1356 additions and 20 deletions
+76 -6
View File
@@ -73,6 +73,7 @@ from govoplan_campaign.backend.integrations import (
MailProfileError,
SmtpConfigurationError,
SmtpSendError,
calendar_integration,
files_integration,
mail_integration,
)
@@ -86,6 +87,75 @@ class SendJobError(RuntimeError):
pass
def _mark_accepted_job_artifacts(session: Session, job: CampaignJob) -> None:
files_integration().mark_job_attachment_uses_sent(session, job)
provenance = (
dict(job.delivery_provenance)
if isinstance(job.delivery_provenance, dict)
else {}
)
invitation = provenance.get("calendar_invitation")
if not isinstance(invitation, dict) or invitation.get("state") == "mirrored":
return
request_payload = invitation.get("request")
if not isinstance(request_payload, dict):
return
integration = calendar_integration()
next_invitation = dict(invitation)
if not integration.available:
next_invitation.update(
{
"state": "unavailable",
"last_error": (
"The Calendar invitation capability is not active; Mail "
"delivery was accepted without a Calendar mirror."
),
}
)
else:
try:
request = integration.request_from_payload(request_payload)
ref = integration.upsert_invitation(
session,
tenant_id=job.tenant_id,
user_id=(
str(request.metadata.get("prepared_by_user_id"))
if request.metadata.get("prepared_by_user_id")
else None
),
request=request,
)
except Exception as exc: # noqa: BLE001 - delivery must remain accepted.
next_invitation.update(
{
"state": "mirror_failed",
"last_error": (
"Calendar mirror failed after delivery acceptance "
f"({type(exc).__name__})."
),
}
)
else:
next_invitation.update(
{
"state": "mirrored",
"event_id": ref.event_id,
"calendar_id": ref.calendar_id,
"uid": ref.uid,
"external_state": ref.external_state,
"outbox_operation_id": ref.outbox_operation_id,
"reply_ingress": ref.reply_ingress,
"recurrence_supported": ref.recurrence_supported,
"degraded_reasons": list(ref.degraded_reasons),
"mirrored_at": datetime.now(timezone.utc).isoformat(),
"last_error": None,
}
)
provenance["calendar_invitation"] = next_invitation
job.delivery_provenance = provenance
session.add(job)
class SynchronousSendRejected(QueueingError):
def __init__(
self,
@@ -2148,7 +2218,7 @@ def _send_single_message_direct(
if delivery_context.snapshot.delivery.imap_append_sent.enabled
else JobImapStatus.NOT_REQUESTED.value
)
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
session.add(job)
if campaign.current_version_id == version.id:
_update_campaign_after_job(
@@ -2342,7 +2412,7 @@ def reconcile_job_outcome(
if snapshot.delivery.imap_append_sent.enabled
else JobImapStatus.NOT_REQUESTED.value
)
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
attempt_status = "reconciled_smtp_accepted"
elif decision == "not_sent":
job.send_status = JobSendStatus.FAILED_TEMPORARY.value
@@ -2535,7 +2605,7 @@ def _reconcile_imap_append_outcome(
if decision == "imap_appended":
job.imap_status = JobImapStatus.APPENDED.value
attempt_status = "reconciled_imap_appended"
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
elif decision == "imap_not_appended":
# FAILED is deliberately retryable only after this explicit,
# evidence-backed operator decision.
@@ -3532,7 +3602,7 @@ def _finalize_multichannel_job(
)
):
job.sent_at = job.sent_at or _utcnow()
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
if not (mail and mail.accepted):
job.imap_status = JobImapStatus.NOT_REQUESTED.value
session.add(job)
@@ -3719,7 +3789,7 @@ def _record_smtp_send_success(
else JobImapStatus.NOT_REQUESTED.value
)
job.last_error = refused_warning
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
session.add(attempt)
session.add(job)
_update_campaign_after_job(session, job.campaign_id, job.campaign_version_id)
@@ -4061,7 +4131,7 @@ def _record_imap_append_success(
attempt.folder = folder
attempt.error_message = None
session.add(attempt)
files_integration().mark_job_attachment_uses_sent(session, job)
_mark_accepted_job_artifacts(session, job)
session.commit()
session.expire_all()
return AppendSentResult(