Files
govoplan-mail/webui/src/api/mail.ts
T
zemion 1cbf4acaf4
Module Package Release / publish-packages (push) Successful in 12s
feat(mail): add governed JMAP mailbox sync and search
2026-08-22 17:09:05 +02:00

801 lines
24 KiB
TypeScript

import type {
ApiSettings,
DeltaDeletedItem,
MailConnectionTestResponse,
MailImapFolderListResponse,
MailImapTestPayload,
MailCredentialEnvelope,
MailProfilePolicy,
MailProfilePolicyResponse,
MailProfileScope,
MailSecurity,
MailServerEndpoint,
MailServerProfile,
MailServerProfilePayload,
MailSmtpTestPayload,
MockMailboxMessage,
MockMailboxMessageResponse
} from "@govoplan/core-webui";
import { apiFetch, apiGetList, apiPath, apiPost, apiPostJson } from "./client";
export { mailProfilePatternKeys, mailProfilePolicyLimitKeys } from "@govoplan/core-webui";
export type {
MailConnectionTestResponse,
MailCredentialEnvelope,
MailCredentialPolicy,
MailImapFolderListResponse,
MailImapFolderResponse,
MailImapTestPayload,
MailProfilePatternKey,
MailProfilePatternRules,
MailProfilePolicy,
MailProfilePolicyLimitKey,
MailProfilePolicyLimitPermissions,
MailProfilePolicyResponse,
MailProfileScope,
MailSecurity,
MailServerEndpoint,
MailServerProfile,
MailServerProfileCredentialsPayload,
MailServerProfileListResponse,
MailServerProfilePayload,
MailSmtpTestPayload,
MailTransportCredentialsPayload,
MockMailboxMessage,
MockMailboxMessageResponse
} from "@govoplan/core-webui";
export type { MailPolicySourceStep as PolicySourceStep } from "@govoplan/core-webui";
export type MailAddressLookupCandidate = {
contact_id: string;
address_book_id: string;
display_name: string;
email?: string | null;
email_label?: string | null;
organization?: string | null;
role_title?: string | null;
tags: string[];
source_kind: string;
source_ref?: string | null;
source_revision?: string | null;
provenance: Record<string, unknown>;
};
export type MailAddressLookupResponse = {
available: boolean;
candidates: MailAddressLookupCandidate[];
};
export type MailAddressWriteTarget = {
address_book_id: string;
address_book_label?: string | null;
operation: string;
allowed: boolean;
reason: string;
message: string;
scope_type?: string | null;
scope_id?: string | null;
source_kind?: string | null;
read_only: boolean;
required_scopes: string[];
provenance: Record<string, unknown>;
};
export type MailAddressWriteTargetResponse = {
available: boolean;
targets: MailAddressWriteTarget[];
};
export type MailContactCreatePayload = {
address_book_id: string;
display_name?: string | null;
email: string;
};
export type MailContactCreateResponse = {
contact_id: string;
address_book_id: string;
display_name: string;
email?: string | null;
source_kind: string;
provenance: Record<string, unknown>;
};
export type MailMailboxAttachment = {
filename?: string | null;
content_type: string;
size_bytes: number;
};
export type MailMailboxMessageSummary = {
uid: string;
folder: string;
subject?: string | null;
from_header?: string | null;
to_header?: string | null;
cc_header?: string | null;
bcc_header?: string | null;
date?: string | null;
message_id?: string | null;
flags?: string[];
size_bytes?: number | null;
body_preview?: string | null;
attachment_count?: number;
};
export type MailMailboxMessageDetail = MailMailboxMessageSummary & {
body_text?: string | null;
body_html?: string | null;
headers?: Record<string, string>;
attachments?: MailMailboxAttachment[];
};
export type MailMailboxMessageListResponse = {
profile_id: string;
folder: string;
host?: string | null;
port?: number | null;
security?: MailSecurity | string | null;
total_count?: number;
offset?: number;
limit?: number;
cursor?: string | null;
next_cursor?: string | null;
cursor_stable?: boolean;
full?: boolean;
from_cache?: boolean;
refreshing?: boolean;
indexed_at?: string | null;
messages: MailMailboxMessageSummary[];
};
export type MailMailboxBootstrapResponse = {
profile_id: string;
folder: string;
folders: MailImapFolderListResponse;
messages: MailMailboxMessageListResponse;
};
export type MailMailboxMessageResponse = {
profile_id: string;
folder: string;
host?: string | null;
port?: number | null;
security?: MailSecurity | string | null;
message: MailMailboxMessageDetail;
};
export type MailMailboxProtocol = "imap" | "jmap";
export type MailMailboxChangesResponse = {
profile_id: string;
protocol: "jmap";
account_id: string;
old_state: string;
new_state: string;
has_more_changes: boolean;
created: string[];
updated: string[];
destroyed: string[];
};
export type MailSettingsDeltaResponse = {
profiles: MailServerProfile[];
policy?: MailProfilePolicyResponse | null;
changed_sections?: string[];
deleted: DeltaDeletedItem[];
watermark?: string | null;
has_more: boolean;
full: boolean;
};
export type MailBounceSource = {
id: string;
tenant_id: string;
profile_id: string;
folder: string;
imap_server_id?: string | null;
imap_credential_id?: string | null;
expected_imap_transport_revision: string;
is_active: boolean;
uidvalidity?: string | null;
highest_processed_uid: number;
last_scanned_at?: string | null;
last_success_at?: string | null;
last_error?: string | null;
};
export type MailBounceObservation = {
id: string;
profile_id: string;
folder: string;
uid: string;
original_message_id?: string | null;
command_id?: string | null;
recipient?: string | null;
action: string;
status_code?: string | null;
diagnostic?: string | null;
permanent: boolean;
observed_at: string;
matched: boolean;
evidence: Record<string, unknown>;
};
export type MailBounceSourcePayload = {
profile_id: string;
folder: string;
imap_server_id?: string | null;
imap_credential_id?: string | null;
is_active: boolean;
};
export async function listMailBounceSources(settings: ApiSettings): Promise<MailBounceSource[]> {
const response = await apiFetch<{ sources: MailBounceSource[] }>(settings, "/api/v1/mail/bounce-sources");
return response.sources;
}
export async function saveMailBounceSource(settings: ApiSettings, payload: MailBounceSourcePayload): Promise<MailBounceSource> {
return apiFetch<MailBounceSource>(settings, "/api/v1/mail/bounce-sources", {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function removeMailBounceSource(settings: ApiSettings, sourceId: string): Promise<void> {
await apiFetch<void>(settings, `/api/v1/mail/bounce-sources/${encodeURIComponent(sourceId)}`, { method: "DELETE" });
}
export async function scanMailBounceSource(settings: ApiSettings, sourceId: string): Promise<{ sources: number; processed_messages: number; observations: number; failures: Array<Record<string, string>> }> {
return apiFetch(settings, `/api/v1/mail/bounce-sources/${encodeURIComponent(sourceId)}/scan`, { method: "POST" });
}
export async function listMailBounceObservations(settings: ApiSettings, limit = 100): Promise<MailBounceObservation[]> {
const response = await apiFetch<{ observations: MailBounceObservation[] }>(settings, apiPath("/api/v1/mail/bounce-observations", { limit }));
return response.observations;
}
export async function lookupMailAddresses(settings: ApiSettings, query: string, limit = 25): Promise<MailAddressLookupResponse> {
return apiFetch<MailAddressLookupResponse>(settings, apiPath("/api/v1/mail/address-lookup", { query, limit }));
}
export async function listMailAddressWriteTargets(settings: ApiSettings): Promise<MailAddressWriteTargetResponse> {
return apiFetch<MailAddressWriteTargetResponse>(settings, "/api/v1/mail/address-write-targets");
}
export async function createMailAddressContact(
settings: ApiSettings,
payload: MailContactCreatePayload
): Promise<MailContactCreateResponse> {
return apiFetch<MailContactCreateResponse>(settings, "/api/v1/mail/address-contacts", {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function listMailServerProfiles(settings: ApiSettings, includeInactive = false, campaignId?: string): Promise<MailServerProfile[]> {
return apiGetList<MailServerProfile, "profiles">(settings, "/api/v1/mail/profiles", "profiles", {
include_inactive: includeInactive ? true : undefined,
campaign_id: campaignId
});
}
export async function fetchMailSettingsDelta(
settings: ApiSettings,
params: {
scope_type?: MailProfileScope;
scope_id?: string | null;
include_inactive?: boolean;
campaign_id?: string | null;
since?: string | null;
limit?: number;
} = {}
): Promise<MailSettingsDeltaResponse> {
return apiFetch<MailSettingsDeltaResponse>(settings, apiPath("/api/v1/mail/settings/delta", {
scope_type: params.scope_type,
scope_id: params.scope_id,
include_inactive: params.include_inactive ? true : undefined,
campaign_id: params.campaign_id,
since: params.since,
limit: params.limit
}));
}
export async function createMailServerProfile(settings: ApiSettings, payload: MailServerProfilePayload): Promise<MailServerProfile> {
return apiFetch<MailServerProfile>(settings, "/api/v1/mail/profiles", {
method: "POST",
body: JSON.stringify(payload)
});
}
export type MailServerProfileUpdatePayload = Partial<MailServerProfilePayload> & { clear_imap?: boolean };
export type MailServerEndpointPayload = {
protocol: "smtp" | "imap" | "jmap" | "pop3";
name: string;
config: Record<string, unknown>;
inherit_to_lower_scopes?: boolean | null;
is_default?: boolean;
is_active?: boolean;
};
export type MailPop3ServerConfig = {
host?: string | null;
port?: number | null;
security?: MailSecurity | string;
timeout_seconds?: number;
max_message_bytes?: number;
max_batch_bytes?: number;
preview_body_lines?: number;
legacy_import_enabled?: boolean;
allow_delete_after_import?: boolean;
};
export type MailPop3ServerEndpoint = Omit<MailServerEndpoint, "protocol" | "config"> & {
protocol: "pop3";
config: MailPop3ServerConfig;
};
export type MailPop3MessagePreview = {
message_number: number;
uidl: string;
subject?: string | null;
from_header?: string | null;
to_header?: string | null;
date?: string | null;
message_id?: string | null;
size_bytes: number;
body_preview?: string | null;
already_imported: boolean;
};
export type MailPop3PreviewResponse = {
profile_id: string;
server_id: string;
transport_revision: string;
host: string;
port: number;
security: string;
message_count: number;
mailbox_size_bytes: number;
delete_after_import_allowed: boolean;
messages: MailPop3MessagePreview[];
};
export type MailPop3ImportRecord = {
id: string;
profile_id: string;
pop3_server_id: string;
transport_revision: string;
provider_uidl: string;
message_id?: string | null;
subject?: string | null;
from_header?: string | null;
to_header?: string | null;
date?: string | null;
body_preview?: string | null;
size_bytes: number;
raw_sha256: string;
status: string;
imported_at: string;
deletion_requested: boolean;
deletion_status: string;
deletion_attempted_at?: string | null;
deletion_error?: string | null;
};
export type MailPop3ImportResponse = {
imports: MailPop3ImportRecord[];
duplicate_uidls: string[];
deletion_status: string;
};
export type MailCredentialCreatePayload = {
name: string;
description?: string | null;
credential_kind?: string;
username?: string | null;
password?: string | null;
public_data?: Record<string, unknown>;
secret_data?: Record<string, unknown>;
inherit_to_lower_scopes?: boolean | null;
allowed_modules?: string[];
allowed_server_refs?: string[];
is_default?: boolean;
};
export type MailCredentialUpdatePayload = Partial<MailCredentialCreatePayload> & {
is_active?: boolean;
};
export async function updateMailServerProfile(settings: ApiSettings, profileId: string, payload: MailServerProfileUpdatePayload): Promise<MailServerProfile> {
return apiFetch<MailServerProfile>(settings, `/api/v1/mail/profiles/${encodeURIComponent(profileId)}`, {
method: "PATCH",
body: JSON.stringify(payload)
});
}
export async function deactivateMailServerProfile(settings: ApiSettings, profileId: string): Promise<MailServerProfile> {
return apiFetch<MailServerProfile>(settings, `/api/v1/mail/profiles/${encodeURIComponent(profileId)}`, { method: "DELETE" });
}
export async function createMailServerEndpoint(
settings: ApiSettings,
profileId: string,
payload: MailServerEndpointPayload
): Promise<MailServerEndpoint> {
return apiFetch<MailServerEndpoint>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers`,
{ method: "POST", body: JSON.stringify(payload) }
);
}
export async function updateMailServerEndpoint(
settings: ApiSettings,
profileId: string,
serverId: string,
payload: Partial<Omit<MailServerEndpointPayload, "protocol">>
): Promise<MailServerEndpoint> {
return apiFetch<MailServerEndpoint>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}`,
{ method: "PATCH", body: JSON.stringify(payload) }
);
}
export async function deactivateMailServerEndpoint(
settings: ApiSettings,
profileId: string,
serverId: string
): Promise<MailServerEndpoint> {
return apiFetch<MailServerEndpoint>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}`,
{ method: "DELETE" }
);
}
export async function listAvailableMailCredentials(
settings: ApiSettings,
profileId: string,
serverId: string,
includeInactive = false
): Promise<MailCredentialEnvelope[]> {
return apiGetList<MailCredentialEnvelope, "credentials">(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}/available-credentials`,
"credentials",
{ include_inactive: includeInactive ? true : undefined }
);
}
export async function createMailServerCredential(
settings: ApiSettings,
profileId: string,
serverId: string,
payload: MailCredentialCreatePayload
): Promise<MailCredentialEnvelope> {
return apiFetch<MailCredentialEnvelope>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}/credentials`,
{ method: "POST", body: JSON.stringify(payload) }
);
}
export async function bindMailServerCredential(
settings: ApiSettings,
profileId: string,
serverId: string,
credentialId: string,
isDefault = false
): Promise<MailCredentialEnvelope> {
return apiFetch<MailCredentialEnvelope>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}/credential-bindings`,
{
method: "POST",
body: JSON.stringify({ credential_id: credentialId, is_default: isDefault })
}
);
}
export async function updateMailServerCredential(
settings: ApiSettings,
profileId: string,
serverId: string,
credentialId: string,
payload: MailCredentialUpdatePayload
): Promise<MailCredentialEnvelope> {
return apiFetch<MailCredentialEnvelope>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}/credentials/${encodeURIComponent(credentialId)}`,
{ method: "PATCH", body: JSON.stringify(payload) }
);
}
export async function unlinkMailServerCredential(
settings: ApiSettings,
profileId: string,
serverId: string,
credentialId: string,
retireIfUnused = false
): Promise<void> {
await apiFetch<void>(
settings,
apiPath(
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/servers/${encodeURIComponent(serverId)}/credentials/${encodeURIComponent(credentialId)}`,
{ retire_if_unused: retireIfUnused ? true : undefined }
),
{ method: "DELETE" }
);
}
export async function getMailProfilePolicy(
settings: ApiSettings,
scopeType: MailProfileScope,
scopeId?: string | null,
campaignId?: string | null
): Promise<MailProfilePolicyResponse> {
return apiFetch<MailProfilePolicyResponse>(settings, apiPath(`/api/v1/mail/policies/${encodeURIComponent(scopeType)}`, {
scope_id: scopeId,
campaign_id: campaignId
}));
}
export async function updateMailProfilePolicy(
settings: ApiSettings,
scopeType: MailProfileScope,
policy: MailProfilePolicy,
scopeId?: string | null
): Promise<MailProfilePolicyResponse> {
return apiFetch<MailProfilePolicyResponse>(settings, apiPath(`/api/v1/mail/policies/${encodeURIComponent(scopeType)}`, { scope_id: scopeId }), {
method: "PUT",
body: JSON.stringify({ policy })
});
}
export async function testMailProfileSmtp(
settings: ApiSettings,
profileId: string,
serverId?: string | null,
credentialId?: string | null,
campaignId?: string | null
): Promise<MailConnectionTestResponse> {
return apiPost<MailConnectionTestResponse>(
settings,
apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/test-smtp`, {
server_id: serverId,
credential_id: credentialId,
campaign_id: campaignId
})
);
}
export async function testMailProfileImap(
settings: ApiSettings,
profileId: string,
serverId?: string | null,
credentialId?: string | null,
campaignId?: string | null
): Promise<MailConnectionTestResponse> {
return apiPost<MailConnectionTestResponse>(
settings,
apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/test-imap`, {
server_id: serverId,
credential_id: credentialId,
campaign_id: campaignId
})
);
}
export async function testMailProfileJmap(
settings: ApiSettings,
profileId: string,
serverId?: string | null,
credentialId?: string | null,
campaignId?: string | null
): Promise<MailConnectionTestResponse> {
return apiPost<MailConnectionTestResponse>(
settings,
apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/test-jmap`, {
server_id: serverId,
credential_id: credentialId,
campaign_id: campaignId
})
);
}
export async function testMailProfilePop3(
settings: ApiSettings,
profileId: string,
serverId: string,
credentialId?: string | null
): Promise<MailConnectionTestResponse> {
return apiPost<MailConnectionTestResponse>(
settings,
apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/test-pop3`, {
server_id: serverId,
credential_id: credentialId
})
);
}
export async function previewMailProfilePop3(
settings: ApiSettings,
profileId: string,
payload: { server_id: string; credential_id?: string | null; limit?: number }
): Promise<MailPop3PreviewResponse> {
return apiPostJson<MailPop3PreviewResponse>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/pop3/preview`,
payload
);
}
export async function importMailProfilePop3(
settings: ApiSettings,
profileId: string,
payload: {
server_id: string;
credential_id?: string | null;
expected_transport_revision: string;
uidls: string[];
delete_after_import?: boolean;
}
): Promise<MailPop3ImportResponse> {
return apiPostJson<MailPop3ImportResponse>(
settings,
`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/pop3/import`,
payload
);
}
export async function listMailPop3Imports(
settings: ApiSettings,
profileId?: string | null,
limit = 100
): Promise<MailPop3ImportRecord[]> {
const response = await apiFetch<{ imports: MailPop3ImportRecord[] }>(
settings,
apiPath("/api/v1/mail/pop3/imports", { profile_id: profileId, limit })
);
return response.imports;
}
export async function listMailProfileImapFolders(
settings: ApiSettings,
profileId: string,
serverId?: string | null,
credentialId?: string | null,
campaignId?: string | null
): Promise<MailImapFolderListResponse> {
return apiPost<MailImapFolderListResponse>(
settings,
apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/list-imap-folders`, {
server_id: serverId,
credential_id: credentialId,
campaign_id: campaignId
})
);
}
export async function listMailboxFolders(settings: ApiSettings, profileId: string, includeStatus = false, refresh = false, protocol: MailMailboxProtocol = "imap"): Promise<MailImapFolderListResponse> {
return apiFetch<MailImapFolderListResponse>(settings, apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/mailbox/folders`, {
include_status: includeStatus ? true : undefined,
refresh: refresh ? true : undefined,
protocol
}));
}
export async function bootstrapMailbox(
settings: ApiSettings,
profileId: string,
folder = "INBOX",
limit = 50,
offset = 0,
refresh = false,
protocol: MailMailboxProtocol = "imap"
): Promise<MailMailboxBootstrapResponse> {
return apiFetch<MailMailboxBootstrapResponse>(settings, apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/mailbox/bootstrap`, {
folder,
limit,
offset,
refresh: refresh ? true : undefined,
protocol
}));
}
export async function listMailboxMessages(
settings: ApiSettings,
profileId: string,
folder = "INBOX",
limit = 50,
offset = 0,
cursor?: string | null,
refresh = false,
protocol: MailMailboxProtocol = "imap",
query?: string | null
): Promise<MailMailboxMessageListResponse> {
return apiFetch<MailMailboxMessageListResponse>(settings, apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/mailbox/messages`, {
folder,
limit,
offset,
cursor,
refresh: refresh ? true : undefined,
protocol,
q: query || undefined
}));
}
export async function getMailboxMessage(
settings: ApiSettings,
profileId: string,
folder: string,
uid: string,
protocol: MailMailboxProtocol = "imap"
): Promise<MailMailboxMessageResponse> {
return apiFetch<MailMailboxMessageResponse>(settings, apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/mailbox/messages/${encodeURIComponent(uid)}`, { folder, protocol }));
}
export async function getMailboxChanges(
settings: ApiSettings,
profileId: string,
sinceState: string,
maxChanges = 500
): Promise<MailMailboxChangesResponse> {
return apiFetch<MailMailboxChangesResponse>(settings, apiPath(`/api/v1/mail/profiles/${encodeURIComponent(profileId)}/mailbox/changes`, {
since_state: sinceState,
max_changes: maxChanges
}));
}
export async function testSmtpSettings(
settings: ApiSettings,
payload: MailSmtpTestPayload
): Promise<MailConnectionTestResponse> {
return apiPostJson<MailConnectionTestResponse>(settings, "/api/v1/mail/test-smtp", payload);
}
export async function testImapSettings(
settings: ApiSettings,
payload: MailImapTestPayload
): Promise<MailConnectionTestResponse> {
return apiPostJson<MailConnectionTestResponse>(settings, "/api/v1/mail/test-imap", payload);
}
export async function listImapFolders(
settings: ApiSettings,
payload: MailImapTestPayload
): Promise<MailImapFolderListResponse> {
return apiPostJson<MailImapFolderListResponse>(settings, "/api/v1/mail/list-imap-folders", payload);
}
export type MockMailboxListResponse = {
messages: MockMailboxMessage[];
};
export type MockMailboxFailureConfig = {
fail_next_smtp?: boolean | null;
fail_next_imap?: boolean | null;
smtp_reject_recipients_containing?: string | null;
};
export async function listMockMailboxMessages(settings: ApiSettings, kind?: string): Promise<MockMailboxListResponse> {
return apiFetch<MockMailboxListResponse>(settings, apiPath("/api/v1/dev/mailbox/messages", { kind }));
}
export async function getMockMailboxMessage(settings: ApiSettings, id: string): Promise<MockMailboxMessageResponse> {
return apiFetch<MockMailboxMessageResponse>(settings, `/api/v1/dev/mailbox/messages/${encodeURIComponent(id)}`);
}
export async function clearMockMailboxMessages(settings: ApiSettings): Promise<{ deleted_count: number }> {
return apiFetch<{ deleted_count: number }>(settings, "/api/v1/dev/mailbox/messages", { method: "DELETE" });
}
export async function updateMockMailboxFailures(settings: ApiSettings, payload: MockMailboxFailureConfig): Promise<{ config: MockMailboxFailureConfig }> {
return apiFetch<{ config: MockMailboxFailureConfig }>(settings, "/api/v1/dev/mailbox/failures", {
method: "POST",
body: JSON.stringify(payload)
});
}