import * as plugins from '../plugins.js'; import { CachedEmail } from '../db/index.js'; import type { ICachedEmailSmtpTransaction, TCachedEmailDirection } from '../db/documents/classes.cached.email.js'; import type { IAcceptEnvelopeContext, IExtendedSmtpSession, IMessageAcceptanceContext, IMessageAcceptanceDecision, UnifiedEmailServer } from '@push.rocks/smartmta'; import type { DcRouter } from '../classes.dcrouter.js'; export { evaluateInboundAcceptance, type IInboundAcceptanceEvaluation, } from './helpers.inbound-security.js'; export declare const DCROUTER_CACHE_ID_HEADER = "X-Dcrouter-Cached-Email-Id"; /** * Permanent per-email storage failure: the raw RFC822 payload of an accepted * email is unrecoverable, so delivery can never succeed. The spool marks such * rows failed instead of retrying them, so one lost blob cannot wedge the * whole queue. */ export declare class AcceptedEmailRawMessageMissingError extends Error { constructor(messageArg: string); } export type TSmartMtaQueueItemLike = { id?: string; processingResult?: { headers?: Record; email?: { headers?: Record; }; }; status?: 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred'; attempts?: number; nextAttempt?: Date; lastError?: string; smtpTransactions?: ICachedEmailSmtpTransaction[]; }; export interface IEmailQueuePersistedEvent { cachedEmailId: string; status: string; reason: string; } export interface ISmtpTransactionPersistedEvent { cachedEmailId: string; transaction: ICachedEmailSmtpTransaction; } export interface IAcceptRawEmailOptions { rawMessage: string | plugins.buffer.Buffer; envelope: { mailFrom: string; rcptTo: string[]; }; session: IExtendedSmtpSession; messageId?: string; subject?: string; processAfterAccept?: boolean; /** Explicit direction override for programmatic submitters. Defaults to the session-authenticated rule. */ direction?: TCachedEmailDirection; /** Exact authenticated outbound identity and replay metadata. */ submissionCredentialId?: string; submissionIdempotencyKey?: string; submissionDigest?: string; } /** * Accept-then-spool pipeline for inbound SMTP messages: persists accepted * messages as CachedEmail docs, replays them through SmartMTA on an interval, * and mirrors SmartMTA delivery-queue outcomes back onto the cached docs. */ export declare class AcceptedEmailSpool { private dcRouterRef; private spoolTimer?; private spoolRun?; private processing; private stopping; private queueUpdatePromises; private cachedEmailUpdateChains; private smtpTransactionListeners; private emailQueuePersistedListeners; constructor(dcRouterRef: DcRouter); /** * Direction is decided by the RECIPIENT, never by whether the session * authenticated. Authentication grants permission to relay; it does not make * a message addressed to a mailbox we host into outbound mail. A message with * at least one locally hosted recipient is inbound — a local mailbox receives * it — and only an envelope addressed exclusively to remote recipients is * outbound. * * `IResolvedRecipientRoute.localDomain` is smartmta's own per-recipient * verdict (non-null exactly when the domain registry hosts the domain), so * the classification uses the same truth the routing decision used. When an * acceptance context carries no resolution the envelope recipients are * classified against the live registry instead — never against the session, * which is the mistake being fixed. */ private deriveDirectionFromResolvedRoutes; /** * Recipient-derived direction for programmatic submitters, which have no SMTP * recipient resolution to consult. Falls back to the live domain registry. */ private deriveDirectionFromRecipients; acceptMessage(context: IMessageAcceptanceContext, processAfterAccept?: boolean): Promise; acceptRawMessage(optionsArg: IAcceptRawEmailOptions): Promise; /** * Durable acceptance for envelopes containing at least one catch-all store * route: anchors the exact raw message + metadata as a CachedEmail before * SMTP 250, then dispatches any non-store recipients through SmartMTA's * exact-plan executor. Store recipients are fulfilled by the anchor itself. */ acceptEnvelope(context: IAcceptEnvelopeContext, emailServer: UnifiedEmailServer): Promise; /** * Session metadata for `dispatchAcceptedEnvelope`. It feeds the per-recipient * idempotency fingerprint, so it must be JSON-round-trip stable: a replay * reconstructed from persisted state has to produce byte-identical metadata * or upstream refuses it as an idempotency-key reuse. */ private buildEnvelopeDispatchMetadata; /** * Dispatch (or redispatch) the non-store recipients of a durably accepted * envelope. * * The raw bytes handed to upstream are the exact bytes persisted for this row, * because upstream fingerprints every recipient over the raw message: a later * replay with different bytes would be rejected as an idempotency-key reuse * rather than retried. Recipients that already succeeded are short-circuited * by upstream's checkpoints, and `failed` results are deliberately not * checkpointed upstream, so an identical replay retries exactly those. * * A `failed` recipient leaves the row non-terminal so the spool retries it; * a `rejected` recipient is a permanent per-recipient refusal and is recorded * durably instead of being retried. Either way the outcome is persisted — a * relay recipient is never silently dropped after the SMTP 250. */ private dispatchEnvelopeRecipients; /** * Persist a dispatch attempt's outcome on the durable-envelope row. * * The row's status tracks the stored envelope, not the relay: it goes * `deferred` only while relay recipients still need a redispatch, and returns * to its acceptance status once none do. Relay progress itself lives in * `routeData.envelopeDispatch`, so a relay failure can never mark a row whose * local copy stored successfully as failed. */ private persistEnvelopeDispatchOutcome; /** * Take exclusive ownership of a durably accepted envelope row in the spool. * * Runs before live-queue postponement and before the normal spool handoff: a * relay sibling that did enqueue would otherwise postpone this row forever, * and the normal handoff would re-run route evaluation and duplicate a * delivery the stored copy already fulfilled. */ private handleDurableEnvelopeRow; private trackAcceptedInboundEmail; /** * Synthesizes the inbound SMTP transaction for the email log from the * acceptance context: the receiving session where WE are the server. The * Rust frontend does not export a raw line transcript yet, so entries are * reconstructed from the envelope, session, and verdict data. */ private buildInboundTransaction; /** Start the interval-driven spool processor and trigger an immediate run. */ start(): void; /** Mark the spool as stopping and clear the interval without awaiting in-flight work. */ beginStop(): void; /** Stop the spool and wait (bounded) for an in-flight run to settle. */ stop(): Promise; /** Kick off a spool run unless one is already in flight. */ run(): void; trackQueueUpdate(item: TSmartMtaQueueItemLike, status: 'queued' | 'deferred' | 'delivered' | 'failed', failureMessage: string): Promise; trackSmtpTransaction(transactionArg: ICachedEmailSmtpTransaction, emailServerArg: UnifiedEmailServer): Promise; /** Subscribe to updates only after the SMTP transaction is durably stored. */ onSmtpTransactionPersisted(listenerArg: (eventArg: ISmtpTransactionPersistedEvent) => void | Promise): () => void; /** Subscribe to queue state changes only after the CachedEmail row is durable. */ onEmailQueuePersisted(listenerArg: (eventArg: IEmailQueuePersistedEvent) => void | Promise): () => void; private notifyEmailQueuePersisted; drainQueueUpdates(): Promise; /** Reconcile persisted queue traces after restart before queue recovery runs. */ recoverSmtpTransactionHistory(emailServerArg: UnifiedEmailServer): Promise; /** Requeue emails left in 'queued' state by a previous process as pending. */ recoverQueuedEmails(): Promise; private processSpool; /** * Record a per-email spool failure without aborting the batch: permanent * raw-message losses are marked failed, everything else is deferred through * the regular retry mechanism (which terminates at maxAttempts). */ private handleSpoolItemFailure; private processAcceptedCachedEmail; private buildCachedEmailSession; private parseCachedEmailRouteData; private extractHeader; private updateAcceptedEmailFromQueueItem; private waitForPromiseToSettleWithTimeout; private clearSpoolTimer; private setDcRouterCacheIdHeader; private isCachedEmailTerminal; /** Whether this row was durably accepted through the envelope acceptance path. */ private isDurableEnvelopeRow; /** The CachedEmail row a live queue item was spooled from (via the cache-id header). */ getCachedEmailIdFromQueueItem(item: TSmartMtaQueueItemLike): string | undefined; private postponeLiveSmartMtaOwnedEmail; private getLiveQueueItemForCachedEmail; private getHeaderValue; private updateSmartMtaRouteData; readRawMessage(cachedEmailArg: CachedEmail): Promise; deleteRawMessage(cachedEmailArg: CachedEmail): Promise; private cleanupRawMessageAfterSaveFailure; private persistRawMessage; private runCachedEmailUpdate; private trackUpdatePromise; private appendSmtpTransaction; private removeHeader; private throwIfMessageAcceptanceAborted; }