feat: add governed postbox delivery and report hardening

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent f11c56e890
commit 5240749ae1
47 changed files with 5538 additions and 288 deletions

View File

@@ -101,6 +101,10 @@ export type CampaignDeltaResponse = {
watermark?: string | null;
has_more: boolean;
full: boolean;
total: number;
page: number;
page_size: number;
pages: number;
};
export type CampaignWorkspaceDeltaResponse = CampaignWorkspaceResponse & {
@@ -185,6 +189,58 @@ export type CampaignRecipientAddressSourcesResponse = {
sources: CampaignRecipientAddressSource[];
};
export type CampaignPostboxDirectoryEntry = {
id: string;
address: string;
address_key: string;
name: string;
status: string;
classification: string;
organization_unit_id?: string | null;
organization_unit_name?: string | null;
function_id?: string | null;
function_name?: string | null;
context_key?: string | null;
holder_count: number;
vacant: boolean;
};
export type CampaignPostboxTemplate = {
id: string;
slug: string;
name: string;
description?: string | null;
published_revision_id: string;
function_type_id?: string | null;
scope_kind: string;
scope_id?: string | null;
classification: string;
allow_vacant_delivery: boolean;
};
export type CampaignPostboxFunction = {
id: string;
slug: string;
name: string;
function_type_id?: string | null;
};
export type CampaignPostboxOrganizationUnit = {
id: string;
slug: string;
name: string;
unit_type_id?: string | null;
parent_id?: string | null;
functions: CampaignPostboxFunction[];
};
export type CampaignPostboxCatalog = {
available: boolean;
postboxes: CampaignPostboxDirectoryEntry[];
templates: CampaignPostboxTemplate[];
organization_units: CampaignPostboxOrganizationUnit[];
};
export type CampaignRecipientSnapshotItem = {
contact_id: string;
display_name: string;
@@ -285,6 +341,7 @@ export type CampaignDeliveryOptions = {
campaign_id: string;
version_id: string;
worker_queue_available: boolean;
postbox_available: boolean;
synchronous_send: {
allowed?: boolean;
reason?: string | null;
@@ -448,6 +505,7 @@ export type CampaignJobDetailResponse = {
attempts: {
smtp?: Record<string, unknown>[];
imap?: Record<string, unknown>[];
postbox?: Record<string, unknown>[];
};
};
@@ -488,6 +546,9 @@ export type AggregateCampaignReport = {
};
outcomes: {
smtp_accepted: AggregateReportCount;
postbox_accepted: AggregateReportCount;
delivered: AggregateReportCount;
partially_accepted: AggregateReportCount;
failed: AggregateReportCount;
outcome_unknown: AggregateReportCount;
queued_or_active: AggregateReportCount;
@@ -582,6 +643,13 @@ campaignId: string)
return apiFetch<CampaignRecipientAddressSourcesResponse>(settings, `/api/v1/campaigns/${campaignId}/recipient-address-sources`);
}
export async function getCampaignPostboxCatalog(
settings: ApiSettings,
campaignId: string)
: Promise<CampaignPostboxCatalog> {
return apiFetch<CampaignPostboxCatalog>(settings, `/api/v1/campaigns/${campaignId}/postbox-catalog`);
}
export async function snapshotCampaignRecipientAddressSource(
settings: ApiSettings,
campaignId: string,
@@ -961,12 +1029,13 @@ export async function resolveCampaignJobOutcome(
settings: ApiSettings,
campaignId: string,
jobId: string,
decision: "smtp_accepted" | "not_sent" | "imap_appended" | "imap_not_appended",
note?: string)
decision: "smtp_accepted" | "not_sent" | "imap_appended" | "imap_not_appended" | "postbox_accepted" | "postbox_not_accepted",
note?: string,
attemptId?: string)
: Promise<Record<string, unknown>> {
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/jobs/${jobId}/resolve-outcome`, {
method: "POST",
body: JSON.stringify({ decision, note: note || null })
body: JSON.stringify({ decision, note: note || null, attempt_id: attemptId || null })
});
}
@@ -1057,8 +1126,15 @@ export async function getCampaignShareTargets(settings: ApiSettings, campaignId:
}
export async function getCampaignShares(settings: ApiSettings, campaignId: string): Promise<CampaignShare[]> {
const response = await apiFetch<{shares: CampaignShare[];}>(settings, `/api/v1/campaigns/${campaignId}/shares`);
return response.shares;
const pageSize = 500;
const shares: CampaignShare[] = [];
for (let page = 1; ; page += 1) {
const response = await apiFetch<{shares: CampaignShare[];pages?: number;}>(settings, `/api/v1/campaigns/${campaignId}/shares?page=${page}&page_size=${pageSize}`);
shares.push(...response.shares);
if (page >= (response.pages ?? 1)) {
return shares;
}
}
}
export async function updateCampaignOwner(