security: separate Campaign reader and operator data
This commit is contained in:
@@ -64,7 +64,11 @@ def _selected_version(
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
def _version_info(version: CampaignVersion | None) -> dict[str, Any] | None:
|
def _version_info(
|
||||||
|
version: CampaignVersion | None,
|
||||||
|
*,
|
||||||
|
include_diagnostics: bool = False,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
if not version:
|
if not version:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
@@ -73,14 +77,25 @@ def _version_info(version: CampaignVersion | None) -> dict[str, Any] | None:
|
|||||||
"schema_version": version.schema_version,
|
"schema_version": version.schema_version,
|
||||||
"source_filename": public_source_filename(version.source_filename),
|
"source_filename": public_source_filename(version.source_filename),
|
||||||
"created_at": version.created_at.isoformat() if version.created_at else None,
|
"created_at": version.created_at.isoformat() if version.created_at else None,
|
||||||
"validation_summary": public_campaign_payload(version.validation_summary),
|
"validation_summary": public_campaign_payload(
|
||||||
"build_summary": public_campaign_payload(version.build_summary),
|
version.validation_summary,
|
||||||
|
include_diagnostics=include_diagnostics,
|
||||||
|
),
|
||||||
|
"build_summary": public_campaign_payload(
|
||||||
|
version.build_summary,
|
||||||
|
include_diagnostics=include_diagnostics,
|
||||||
|
),
|
||||||
"execution_snapshot_hash": version.execution_snapshot_hash,
|
"execution_snapshot_hash": version.execution_snapshot_hash,
|
||||||
"execution_snapshot_at": version.execution_snapshot_at.isoformat() if version.execution_snapshot_at else None,
|
"execution_snapshot_at": version.execution_snapshot_at.isoformat() if version.execution_snapshot_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]) -> dict[str, Any]:
|
def _load_delivery_info(
|
||||||
|
version: CampaignVersion | None,
|
||||||
|
jobs: list[CampaignJob],
|
||||||
|
*,
|
||||||
|
include_diagnostics: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Read deterministic delivery settings from the immutable execution snapshot."""
|
"""Read deterministic delivery settings from the immutable execution snapshot."""
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
@@ -90,18 +105,23 @@ def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]
|
|||||||
"execution_snapshot_hash": None,
|
"execution_snapshot_hash": None,
|
||||||
"execution_snapshot_at": None,
|
"execution_snapshot_at": None,
|
||||||
"snapshot_version": None,
|
"snapshot_version": None,
|
||||||
"build_token": None, # nosec B105 - absent report value, not a credential.
|
|
||||||
"built_at": None,
|
"built_at": None,
|
||||||
"job_manifest_sha256": None,
|
|
||||||
"job_count": 0,
|
"job_count": 0,
|
||||||
"queueable_job_count": 0,
|
"queueable_job_count": 0,
|
||||||
"effective_policy_sha256": None,
|
|
||||||
"smtp_transport_revision": None,
|
|
||||||
"imap_transport_revision": None,
|
|
||||||
"estimated_remaining_send_seconds": None,
|
"estimated_remaining_send_seconds": None,
|
||||||
"estimated_remaining_send_human": None,
|
"estimated_remaining_send_human": None,
|
||||||
"background_workers_enabled": bool(core_settings.celery_enabled),
|
|
||||||
}
|
}
|
||||||
|
if include_diagnostics:
|
||||||
|
default.update(
|
||||||
|
{
|
||||||
|
"build_token": None, # nosec B105 - absent report value, not a credential.
|
||||||
|
"job_manifest_sha256": None,
|
||||||
|
"effective_policy_sha256": None,
|
||||||
|
"smtp_transport_revision": None,
|
||||||
|
"imap_transport_revision": None,
|
||||||
|
"background_workers_enabled": bool(core_settings.celery_enabled),
|
||||||
|
}
|
||||||
|
)
|
||||||
if not version or not isinstance(version.execution_snapshot, dict):
|
if not version or not isinstance(version.execution_snapshot, dict):
|
||||||
default["load_error"] = "No execution snapshot exists; rebuild the current locked version before delivery."
|
default["load_error"] = "No execution snapshot exists; rebuild the current locked version before delivery."
|
||||||
return default
|
return default
|
||||||
@@ -118,7 +138,7 @@ def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]
|
|||||||
if messages_per_minute and pending:
|
if messages_per_minute and pending:
|
||||||
estimated_seconds = int(math.ceil((len(pending) / messages_per_minute) * 60))
|
estimated_seconds = int(math.ceil((len(pending) / messages_per_minute) * 60))
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
"rate_limit": {
|
"rate_limit": {
|
||||||
"messages_per_minute": messages_per_minute,
|
"messages_per_minute": messages_per_minute,
|
||||||
"concurrency": snapshot.delivery.rate_limit.concurrency,
|
"concurrency": snapshot.delivery.rate_limit.concurrency,
|
||||||
@@ -134,18 +154,24 @@ def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]
|
|||||||
"execution_snapshot_hash": version.execution_snapshot_hash,
|
"execution_snapshot_hash": version.execution_snapshot_hash,
|
||||||
"execution_snapshot_at": version.execution_snapshot_at.isoformat() if version.execution_snapshot_at else None,
|
"execution_snapshot_at": version.execution_snapshot_at.isoformat() if version.execution_snapshot_at else None,
|
||||||
"snapshot_version": snapshot.snapshot_version,
|
"snapshot_version": snapshot.snapshot_version,
|
||||||
"build_token": snapshot.build_token,
|
|
||||||
"built_at": snapshot.built_at,
|
"built_at": snapshot.built_at,
|
||||||
"job_manifest_sha256": snapshot.job_manifest_sha256,
|
|
||||||
"job_count": snapshot.job_count,
|
"job_count": snapshot.job_count,
|
||||||
"queueable_job_count": snapshot.queueable_job_count,
|
"queueable_job_count": snapshot.queueable_job_count,
|
||||||
"effective_policy_sha256": snapshot.effective_policy_sha256,
|
|
||||||
"smtp_transport_revision": snapshot.smtp_transport_revision,
|
|
||||||
"imap_transport_revision": snapshot.imap_transport_revision,
|
|
||||||
"estimated_remaining_send_seconds": estimated_seconds,
|
"estimated_remaining_send_seconds": estimated_seconds,
|
||||||
"estimated_remaining_send_human": _human_duration(estimated_seconds),
|
"estimated_remaining_send_human": _human_duration(estimated_seconds),
|
||||||
"background_workers_enabled": bool(core_settings.celery_enabled),
|
|
||||||
}
|
}
|
||||||
|
if include_diagnostics:
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"build_token": snapshot.build_token,
|
||||||
|
"job_manifest_sha256": snapshot.job_manifest_sha256,
|
||||||
|
"effective_policy_sha256": snapshot.effective_policy_sha256,
|
||||||
|
"smtp_transport_revision": snapshot.smtp_transport_revision,
|
||||||
|
"imap_transport_revision": snapshot.imap_transport_revision,
|
||||||
|
"background_workers_enabled": bool(core_settings.celery_enabled),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _human_duration(seconds: int | None) -> str | None:
|
def _human_duration(seconds: int | None) -> str | None:
|
||||||
@@ -243,8 +269,8 @@ def _recent_failures(jobs: list[CampaignJob], *, limit: int = 20) -> list[dict[s
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _job_row(job: CampaignJob) -> dict[str, Any]:
|
def _job_row(job: CampaignJob, *, include_diagnostics: bool = False) -> dict[str, Any]:
|
||||||
return {
|
row = {
|
||||||
"job_id": job.id,
|
"job_id": job.id,
|
||||||
"entry_index": job.entry_index,
|
"entry_index": job.entry_index,
|
||||||
"entry_id": job.entry_id,
|
"entry_id": job.entry_id,
|
||||||
@@ -257,8 +283,6 @@ def _job_row(job: CampaignJob) -> dict[str, Any]:
|
|||||||
"imap_status": job.imap_status,
|
"imap_status": job.imap_status,
|
||||||
"attempt_count": job.attempt_count,
|
"attempt_count": job.attempt_count,
|
||||||
"queued_at": job.queued_at.isoformat() if job.queued_at else None,
|
"queued_at": job.queued_at.isoformat() if job.queued_at else None,
|
||||||
"claimed_at": job.claimed_at.isoformat() if job.claimed_at else None,
|
|
||||||
"smtp_started_at": job.smtp_started_at.isoformat() if job.smtp_started_at else None,
|
|
||||||
"outcome_unknown_at": job.outcome_unknown_at.isoformat() if job.outcome_unknown_at else None,
|
"outcome_unknown_at": job.outcome_unknown_at.isoformat() if job.outcome_unknown_at else None,
|
||||||
"sent_at": job.sent_at.isoformat() if job.sent_at else None,
|
"sent_at": job.sent_at.isoformat() if job.sent_at else None,
|
||||||
"last_error": public_delivery_result_message(
|
"last_error": public_delivery_result_message(
|
||||||
@@ -272,6 +296,14 @@ def _job_row(job: CampaignJob) -> dict[str, Any]:
|
|||||||
"attachment_config_count": len(job.resolved_attachments or []),
|
"attachment_config_count": len(job.resolved_attachments or []),
|
||||||
"matched_file_count": sum(len(item.get("matches") or []) for item in (job.resolved_attachments or []) if isinstance(item, dict)),
|
"matched_file_count": sum(len(item.get("matches") or []) for item in (job.resolved_attachments or []) if isinstance(item, dict)),
|
||||||
}
|
}
|
||||||
|
if include_diagnostics:
|
||||||
|
row.update(
|
||||||
|
{
|
||||||
|
"claimed_at": job.claimed_at.isoformat() if job.claimed_at else None,
|
||||||
|
"smtp_started_at": job.smtp_started_at.isoformat() if job.smtp_started_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def _address_summary(value: Any) -> str:
|
def _address_summary(value: Any) -> str:
|
||||||
@@ -334,8 +366,9 @@ def _job_evidence_row(
|
|||||||
*,
|
*,
|
||||||
latest_smtp: SendAttempt | None = None,
|
latest_smtp: SendAttempt | None = None,
|
||||||
latest_imap: ImapAppendAttempt | None = None,
|
latest_imap: ImapAppendAttempt | None = None,
|
||||||
|
include_diagnostics: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
row = _job_row(job)
|
row = _job_row(job, include_diagnostics=include_diagnostics)
|
||||||
recipients = job.resolved_recipients or {}
|
recipients = job.resolved_recipients or {}
|
||||||
row.update({
|
row.update({
|
||||||
"campaign_id": job.campaign_id,
|
"campaign_id": job.campaign_id,
|
||||||
@@ -369,6 +402,7 @@ def generate_campaign_report(
|
|||||||
version_id: str | None = None,
|
version_id: str | None = None,
|
||||||
include_jobs: bool = False,
|
include_jobs: bool = False,
|
||||||
include_recent_failures: bool = False,
|
include_recent_failures: bool = False,
|
||||||
|
include_diagnostics: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Generate a dashboard/report payload for one campaign.
|
"""Generate a dashboard/report payload for one campaign.
|
||||||
|
|
||||||
@@ -387,9 +421,13 @@ def generate_campaign_report(
|
|||||||
jobs=jobs,
|
jobs=jobs,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
include_recent_failures=include_recent_failures,
|
include_recent_failures=include_recent_failures,
|
||||||
|
include_diagnostics=include_diagnostics,
|
||||||
)
|
)
|
||||||
if include_jobs:
|
if include_jobs:
|
||||||
report["jobs"] = [_job_row(job) for job in jobs]
|
report["jobs"] = [
|
||||||
|
_job_row(job, include_diagnostics=include_diagnostics)
|
||||||
|
for job in jobs
|
||||||
|
]
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
@@ -419,12 +457,13 @@ def _campaign_report_payload(
|
|||||||
jobs: list[CampaignJob],
|
jobs: list[CampaignJob],
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
include_recent_failures: bool,
|
include_recent_failures: bool,
|
||||||
|
include_diagnostics: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
job_ids = [job.id for job in jobs]
|
job_ids = [job.id for job in jobs]
|
||||||
report = {
|
report = {
|
||||||
"generated_at": _utcnow_iso(),
|
"generated_at": _utcnow_iso(),
|
||||||
"campaign": _campaign_report_campaign_payload(campaign),
|
"campaign": _campaign_report_campaign_payload(campaign),
|
||||||
"current_version": _version_info(version),
|
"current_version": _version_info(version, include_diagnostics=include_diagnostics),
|
||||||
"selected_version_id": version.id if version else None,
|
"selected_version_id": version.id if version else None,
|
||||||
"cards": _campaign_report_cards(version, jobs),
|
"cards": _campaign_report_cards(version, jobs),
|
||||||
"status_counts": _campaign_report_status_counts(version, jobs),
|
"status_counts": _campaign_report_status_counts(version, jobs),
|
||||||
@@ -439,7 +478,11 @@ def _campaign_report_payload(
|
|||||||
},
|
},
|
||||||
"attachments": _attachment_summary(jobs),
|
"attachments": _attachment_summary(jobs),
|
||||||
"attempts": _campaign_report_attempt_counts(session, job_ids),
|
"attempts": _campaign_report_attempt_counts(session, job_ids),
|
||||||
"delivery": _load_delivery_info(version, jobs),
|
"delivery": _load_delivery_info(
|
||||||
|
version,
|
||||||
|
jobs,
|
||||||
|
include_diagnostics=include_diagnostics,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if include_recent_failures:
|
if include_recent_failures:
|
||||||
report["recent_failures"] = _recent_failures(jobs)
|
report["recent_failures"] = _recent_failures(jobs)
|
||||||
@@ -544,6 +587,7 @@ def generate_jobs_csv(
|
|||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
campaign_id: str,
|
campaign_id: str,
|
||||||
version_id: str | None = None,
|
version_id: str | None = None,
|
||||||
|
include_diagnostics: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
campaign = _get_campaign(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
campaign = _get_campaign(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
version = _selected_version(session, campaign, version_id)
|
version = _selected_version(session, campaign, version_id)
|
||||||
@@ -584,6 +628,7 @@ def generate_jobs_csv(
|
|||||||
job,
|
job,
|
||||||
latest_smtp=latest_smtp.get(job.id),
|
latest_smtp=latest_smtp.get(job.id),
|
||||||
latest_imap=latest_imap.get(job.id),
|
latest_imap=latest_imap.get(job.id),
|
||||||
|
include_diagnostics=include_diagnostics,
|
||||||
)
|
)
|
||||||
for job in jobs
|
for job in jobs
|
||||||
]
|
]
|
||||||
@@ -608,8 +653,6 @@ def generate_jobs_csv(
|
|||||||
"imap_status",
|
"imap_status",
|
||||||
"attempt_count",
|
"attempt_count",
|
||||||
"queued_at",
|
"queued_at",
|
||||||
"claimed_at",
|
|
||||||
"smtp_started_at",
|
|
||||||
"outcome_unknown_at",
|
"outcome_unknown_at",
|
||||||
"sent_at",
|
"sent_at",
|
||||||
"last_error",
|
"last_error",
|
||||||
@@ -630,6 +673,9 @@ def generate_jobs_csv(
|
|||||||
"latest_imap_created_at",
|
"latest_imap_created_at",
|
||||||
"latest_imap_updated_at",
|
"latest_imap_updated_at",
|
||||||
]
|
]
|
||||||
|
if include_diagnostics:
|
||||||
|
sent_at_index = fieldnames.index("outcome_unknown_at")
|
||||||
|
fieldnames[sent_at_index:sent_at_index] = ["claimed_at", "smtp_started_at"]
|
||||||
buffer = io.StringIO()
|
buffer = io.StringIO()
|
||||||
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
|
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
|
|||||||
@@ -24,20 +24,39 @@ CAMPAIGN_INTERNAL_RESPONSE_KEYS = frozenset(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CAMPAIGN_DIAGNOSTIC_RESPONSE_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"background_workers_enabled",
|
||||||
|
"build_token",
|
||||||
|
"celery_enabled",
|
||||||
|
"claimed_at",
|
||||||
|
"effective_policy_sha256",
|
||||||
|
"execution_input_sha256",
|
||||||
|
"imap_claimed_at",
|
||||||
|
"imap_transport_revision",
|
||||||
|
"job_manifest_sha256",
|
||||||
|
"smtp_started_at",
|
||||||
|
"smtp_transport_revision",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def public_campaign_payload(value: Any) -> Any:
|
|
||||||
|
def public_campaign_payload(value: Any, *, include_diagnostics: bool = False) -> Any:
|
||||||
"""Return a detached payload without infrastructure-only locators."""
|
"""Return a detached payload without infrastructure-only locators."""
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
|
blocked_keys = CAMPAIGN_INTERNAL_RESPONSE_KEYS
|
||||||
|
if not include_diagnostics:
|
||||||
|
blocked_keys = blocked_keys | CAMPAIGN_DIAGNOSTIC_RESPONSE_KEYS
|
||||||
return {
|
return {
|
||||||
key: public_campaign_payload(item)
|
key: public_campaign_payload(item, include_diagnostics=include_diagnostics)
|
||||||
for key, item in value.items()
|
for key, item in value.items()
|
||||||
if key not in CAMPAIGN_INTERNAL_RESPONSE_KEYS
|
if key not in blocked_keys
|
||||||
}
|
}
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
return [public_campaign_payload(item) for item in value]
|
return [public_campaign_payload(item, include_diagnostics=include_diagnostics) for item in value]
|
||||||
if isinstance(value, tuple):
|
if isinstance(value, tuple):
|
||||||
return tuple(public_campaign_payload(item) for item in value)
|
return tuple(public_campaign_payload(item, include_diagnostics=include_diagnostics) for item in value)
|
||||||
return copy.deepcopy(value)
|
return copy.deepcopy(value)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,8 +128,11 @@ class CampaignVersionResponse(BaseModel):
|
|||||||
|
|
||||||
@field_validator("validation_summary", "build_summary", mode="before")
|
@field_validator("validation_summary", "build_summary", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_internal_summary_fields(cls, value: Any) -> Any:
|
def remove_internal_summary_fields(cls, value: Any, info: ValidationInfo) -> Any:
|
||||||
return public_campaign_payload(value)
|
return public_campaign_payload(
|
||||||
|
value,
|
||||||
|
include_diagnostics=bool((info.context or {}).get("include_diagnostics")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CampaignVersionDetailResponse(CampaignVersionResponse):
|
class CampaignVersionDetailResponse(CampaignVersionResponse):
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ def test_invalid_legacy_snapshot_never_echoes_validation_details() -> None:
|
|||||||
assert delivery["load_error"] == "Execution snapshot is invalid; rebuild the selected version before delivery."
|
assert delivery["load_error"] == "Execution snapshot is invalid; rebuild the selected version before delivery."
|
||||||
assert "provider-secret" not in repr(delivery)
|
assert "provider-secret" not in repr(delivery)
|
||||||
assert "smtp.internal.example" not in repr(delivery)
|
assert "smtp.internal.example" not in repr(delivery)
|
||||||
|
assert "background_workers_enabled" not in delivery
|
||||||
|
assert "smtp_transport_revision" not in delivery
|
||||||
|
|
||||||
|
operator_delivery = _load_delivery_info(version, [], include_diagnostics=True)
|
||||||
|
assert "background_workers_enabled" in operator_delivery
|
||||||
|
assert "smtp_transport_revision" in operator_delivery
|
||||||
|
|
||||||
|
|
||||||
def test_aggregate_report_omits_recipient_level_failures_by_default() -> None:
|
def test_aggregate_report_omits_recipient_level_failures_by_default() -> None:
|
||||||
|
|||||||
@@ -129,7 +129,11 @@ def test_version_response_omits_source_base_path_and_sanitizes_summaries() -> No
|
|||||||
created_at=_now(),
|
created_at=_now(),
|
||||||
updated_at=_now(),
|
updated_at=_now(),
|
||||||
validation_summary={"campaign_file": "/tmp/campaign.json", "ok": True},
|
validation_summary={"campaign_file": "/tmp/campaign.json", "ok": True},
|
||||||
build_summary={"messages": [{"eml_path": "/tmp/message.eml", "subject": "Public"}]},
|
build_summary={
|
||||||
|
"build_token": "internal-build-token",
|
||||||
|
"smtp_transport_revision": "internal-smtp-revision",
|
||||||
|
"messages": [{"eml_path": "/tmp/message.eml", "subject": "Public"}],
|
||||||
|
},
|
||||||
execution_snapshot_hash=None,
|
execution_snapshot_hash=None,
|
||||||
execution_snapshot_at=None,
|
execution_snapshot_at=None,
|
||||||
)
|
)
|
||||||
@@ -141,6 +145,13 @@ def test_version_response_omits_source_base_path_and_sanitizes_summaries() -> No
|
|||||||
assert payload["validation_summary"] == {"ok": True}
|
assert payload["validation_summary"] == {"ok": True}
|
||||||
assert payload["build_summary"] == {"messages": [{"subject": "Public"}]}
|
assert payload["build_summary"] == {"messages": [{"subject": "Public"}]}
|
||||||
|
|
||||||
|
operator_payload = CampaignVersionResponse.model_validate(
|
||||||
|
version,
|
||||||
|
context={"include_diagnostics": True},
|
||||||
|
).model_dump()
|
||||||
|
assert operator_payload["build_summary"]["build_token"] == "internal-build-token"
|
||||||
|
assert operator_payload["build_summary"]["smtp_transport_revision"] == "internal-smtp-revision"
|
||||||
|
|
||||||
detail = CampaignVersionDetailResponse.model_validate(
|
detail = CampaignVersionDetailResponse.model_validate(
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
**version.__dict__,
|
**version.__dict__,
|
||||||
@@ -200,6 +211,8 @@ def test_ordinary_job_detail_and_attempts_do_not_expose_diagnostics() -> None:
|
|||||||
|
|
||||||
assert "eml_local_path" not in job_payload
|
assert "eml_local_path" not in job_payload
|
||||||
assert "eml_storage_key" not in job_payload
|
assert "eml_storage_key" not in job_payload
|
||||||
|
assert "claimed_at" not in job_payload
|
||||||
|
assert "smtp_started_at" not in job_payload
|
||||||
assert job_payload["attachments"] == [{"filename": "public.pdf"}]
|
assert job_payload["attachments"] == [{"filename": "public.pdf"}]
|
||||||
assert "claim_token" not in attempts["smtp"][0]
|
assert "claim_token" not in attempts["smtp"][0]
|
||||||
assert "claim_token" not in attempts["imap"][0]
|
assert "claim_token" not in attempts["imap"][0]
|
||||||
|
|||||||
@@ -695,6 +695,12 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS
|
|||||||
reviewJobsRef.current = mergedResult;
|
reviewJobsRef.current = mergedResult;
|
||||||
setReviewJobs(mergedResult);
|
setReviewJobs(mergedResult);
|
||||||
setBuiltReviewRows(jobs);
|
setBuiltReviewRows(jobs);
|
||||||
|
setReviewedMessageKeys(new Set(
|
||||||
|
jobs
|
||||||
|
.filter((row) => row.reviewed === true)
|
||||||
|
.map((row, index) => builtMessageKey(row, index))
|
||||||
|
));
|
||||||
|
setMessageReviewComplete(result.review?.inspection_complete === true);
|
||||||
setReviewPage(1);
|
setReviewPage(1);
|
||||||
setJobsLoadedKey(reviewKey);
|
setJobsLoadedKey(reviewKey);
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user