/** * Scheduler service — JSON file-based email scheduling queue. * * Manages scheduled emails with a local file queue. * Source of truth is the JSON files in XDG state directory. */ import type { ScheduledEmail } from '../types/index.js'; import type ImapService from './imap.service.js'; import type SmtpService from './smtp.service.js'; export default class SchedulerService { private smtpService; private imapService; private readonly sentRetentionDays; private lastPrunedAt; constructor(smtpService: SmtpService, imapService: ImapService, options?: { sentRetentionDays?: number; }); /** * Delete sent history past the retention window. * * Nothing used to remove anything from the sent directory, so it grew one * JSON file per sent email forever. Age-based rather than count-based: the * question a user asks is how far back they can look, never how many * entries to keep. A retention of 0 disables pruning. * * An entry is deleted only when its own `status` says `sent`. That is * redundant against the invariant that the directory holds nothing else, * which is the point: this is the third place in this subsystem where * trusting the directory over the field would destroy a live entry, and * here the cost is a scheduled email deleted before it is sent. */ pruneSent(): Promise<{ deleted: number; kept: number; }>; /** * Prune at most once per PRUNE_INTERVAL_MS per process. * * A short-lived runner (the launchd/cron daemon the README recommends) gets * one prune per invocation, which is the "prune on startup" behaviour; a * long-lived server does not re-walk the history every minute. */ private pruneSentIfDue; schedule(account: string, options: { to: string[]; subject: string; body: string; sendAt: string; cc?: string[]; bcc?: string[]; html?: boolean; inReplyTo?: string; references?: string[]; }): Promise; list(options?: { account?: string; status?: 'pending' | 'sent' | 'failed' | 'all'; }): Promise; cancel(scheduleId: string): Promise<{ cancelled: boolean; draftDeleted: boolean; }>; checkAndSend(): Promise<{ sent: number; failed: number; errors: string[]; }>; /** * Return abandoned claims to the queue. * * A claim file outliving its runner means the process died mid-send, and how * far it got decides what is safe: * * - `sentMessageId` present — delivery was recorded before the move, so the * message definitively went out and the interrupted move is finished. * - `sendStartedAt` only — the server was handed the message and its fate is * unknown. Silently resending is how a user mails a client twice, so this * fails with an explicit reason: recoverable by hand, never duplicated. * - neither — it never reached SMTP, so requeue it. * * @returns whether any claim was released, so the caller can re-read the dir */ private recoverStaleClaims; private static ensureDirs; private static writeScheduledFile; /** Write to an exact path — used for a claim file we already own. */ private static writeFileAt; private static readDir; } //# sourceMappingURL=scheduler.service.d.ts.map