@dawnlit/outlook-bridge 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/errors.d.ts +17 -6
- package/dist/errors.js +16 -7
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.d.ts +4 -0
- package/dist/mac/accounts.js +10 -0
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/bounces.js +10 -19
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -3
- package/dist/mac/cleanup.js +26 -17
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.js +3 -16
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +5 -0
- package/dist/mac/folders.js +14 -5
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/profile.d.ts +12 -0
- package/dist/mac/profile.js +75 -1
- package/dist/mac/profile.js.map +1 -1
- package/dist/mac/read.js +14 -26
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +0 -3
- package/dist/mac/run.js +8 -8
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +46 -0
- package/dist/mac/scripts.js +98 -10
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +2 -1
- package/dist/mac/send.js +9 -3
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/templates.js +1 -4
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +6 -0
- package/dist/mail.js +7 -1
- package/dist/mail.js.map +1 -1
- package/dist/types.d.ts +20 -4
- package/dist/types.js.map +1 -1
- package/dist/windows/accounts.js +1 -1
- package/dist/windows/accounts.js.map +1 -1
- package/dist/windows/attachments.js +2 -2
- package/dist/windows/attachments.js.map +1 -1
- package/dist/windows/bounces.js +3 -3
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.js +2 -4
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.js +2 -2
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.js +1 -1
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/json.js.map +1 -0
- package/dist/windows/read.js +17 -14
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/scripts.d.ts +6 -0
- package/dist/windows/scripts.js +11 -2
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +5 -1
- package/dist/windows/send.js +14 -4
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/templates.js +2 -2
- package/dist/windows/templates.js.map +1 -1
- package/package.json +1 -1
- package/dist/shared/json.js.map +0 -1
- /package/dist/{shared → windows}/json.d.ts +0 -0
- /package/dist/{shared → windows}/json.js +0 -0
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,wDAAwD;AACxD,EAAE;AACF,mCAAmC;AACnC,+EAA+E;AAC/E,8EAA8E;AAC9E,YAAY;AACZ,iFAAiF;AACjF,+BAA+B","sourcesContent":["// The public contract: what the package returns, what it takes, and the\n// OutlookBridge interface every entry point answers to.\n//\n// Two conventions hold throughout:\n// - Timestamps are local time as 'yyyy-MM-dd HH:mm', which sorts correctly as\n// a plain string and is what both platforms can produce without a timezone\n// table.\n// - Ids are platform-specific (see EmailLocator). Don't persist one and look it\n// up on the other platform.\n\n// ── Identifying an email ─────────────────────────────────────────────────\n/**\n * Where an email lives, as a listing reported it.\n *\n * Every listing row (`InboxEmail`, `InboxSearchMatch`, `SelectedEmail`, …)\n * carries both fields, so a row can be passed straight back in wherever an\n * `EmailRef` is accepted — which is also the reliable way to keep the two ids\n * from different rows apart.\n */\nexport interface EmailLocator {\n /** Windows: the MAPI EntryID. macOS: Outlook for Mac's integer message id. */\n readonly entryId: string;\n /**\n * Windows: the StoreID the email was listed from, which lets it resolve in\n * any mounted mailbox rather than only the default one. macOS has no\n * equivalent and ignores it.\n */\n readonly storeId?: string;\n}\n\n/** An email to act on: its entry id alone, or anything carrying a locator. */\nexport type EmailRef = string | EmailLocator;\n\n/**\n * Recipients: one address, a comma- or semicolon-separated list, or an array.\n * Display-name forms such as `\"Doe, Jo\" <jo@example.com>` are kept whole.\n */\nexport type Recipients = string | readonly string[];\n\n// ── Sending and replying ─────────────────────────────────────────────────\nexport interface SendEmailParams {\n /** SMTP address of the mailbox to send from. Never falls back to the\n * default account — an address that doesn't resolve is an error. */\n emailAccount: string;\n to?: Recipients;\n cc?: Recipients;\n bcc?: Recipients;\n subject: string;\n htmlBody: string;\n /** Absolute paths of files to attach. Each has to exist. */\n attachments?: readonly string[];\n /**\n * An Outlook signature, by the name listOutlookSignatures reports, added to\n * the body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise\n * at the end. Outlook adds no signature of its own to a body set this way,\n * so this is how a composed email gets one.\n */\n signatureName?: string;\n /** Send now. Default false: the email is staged as a draft instead. */\n sendImmediately?: boolean;\n /**\n * Drafts only. True (the default) opens an Outlook compose window; false\n * files the draft silently into Drafts — the workable choice for batches.\n */\n openDraftWindow?: boolean;\n}\n\nexport interface SendEmailResult {\n /**\n * The draft's entry id when the email was filed silently into Drafts\n * (`openDraftWindow: false`) — the id `listOutlookDrafts` reports for it,\n * so a caller can keep it and later send or delete exactly the drafts it\n * made. '' when the email was sent, or opened in a compose window, which\n * files nothing yet; and on macOS in the rare case Drafts gained another\n * draft with the same subject in the same moment, so this one could not be\n * told apart.\n */\n draftEntryId: string;\n}\n\nexport interface ReplyEmailParams extends EmailLocator {\n /** SMTP address of the mailbox to reply from. */\n emailAccount: string;\n /**\n * HTML inserted above the quoted original (a full document is reduced to\n * its <body> content). Optional when `templateSubject` is given.\n */\n htmlBody?: string;\n /**\n * Reply with a template email saved in the mailbox, found by subject, so\n * the caller never carries a large template's HTML itself.\n */\n templateSubject?: string;\n /** Folder holding the template. Default 'Templates'. */\n templateFolder?: string;\n /**\n * The [[SECTION]] of the template to keep, when one template holds several\n * reply variants (see composeTemplateBody). Required if it has sections.\n */\n templateSection?: string;\n /** `{{PLACEHOLDER}}` → HTML substituted into the composed body. */\n templatePlaceholders?: Readonly<Record<string, string>>;\n /**\n * An Outlook signature (by the name listOutlookSignatures reports), put in\n * the body's `{{SIGNATURE}}` placeholder, or below the new text when there\n * is none. Resolved here, so the signature's HTML never crosses the\n * caller's boundary.\n */\n signatureName?: string;\n /**\n * Answer every recipient of the original instead of only its sender.\n * Default false: widening a reply cannot be taken back, so it is opt-in.\n */\n replyAll?: boolean;\n /** Send now. Default false. */\n sendImmediately?: boolean;\n /** Drafts only: open a compose window (the default) or file silently. */\n openDraftWindow?: boolean;\n}\n\nexport interface ReplyEmailResult {\n /** Who the reply is addressed to. Windows reports Outlook's To line\n * (display names); macOS the resolved addresses. */\n to: string;\n subject: string;\n /** The original email's sender — check it is who you meant to answer. */\n repliedToSender: string;\n}\n\n// ── Per-item outcomes ────────────────────────────────────────────────────\n/** One item a batch operation could not handle, and why. */\nexport interface ItemFailure {\n entryId: string;\n /** The item's subject when it could be read, else ''. */\n subject: string;\n error: string;\n}\n\n// ── Drafts ───────────────────────────────────────────────────────────────\n/** One mail draft belonging to an account. */\nexport interface OutlookDraft {\n entryId: string;\n subject: string;\n /** The To line as Outlook renders it. */\n to: string;\n /** Recipient addresses, best effort — an unresolved Exchange entry falls back to its name. */\n toEmails: string[];\n /** Short plain-text preview, enough to tell similar drafts apart. */\n bodyPreview: string;\n hasAttachments: boolean;\n lastModified: string;\n /** The Drafts folder it sits in; one account's drafts can span two. */\n folderPath: string;\n}\n\nexport interface ListDraftsOptions {\n /** Newest-first cap on the drafts returned. Default 100. */\n limit?: number;\n /** Characters of body preview per draft. Default 300; 0 omits it. */\n previewChars?: number;\n}\n\nexport interface ListDraftsResult {\n account: string;\n /** The Drafts folders scanned, in scan order. */\n foldersScanned: string[];\n /** Matching drafts found, before `limit` was applied. */\n count: number;\n truncated: boolean;\n drafts: OutlookDraft[];\n}\n\nexport interface SendDraftsResult {\n sent: number;\n failed: ItemFailure[];\n}\n\nexport interface DeleteDraftsResult {\n deleted: number;\n failed: ItemFailure[];\n}\n\n// ── Deleting mail ────────────────────────────────────────────────────────\nexport interface DeleteMailOptions {\n /** Lift the refusal to touch Inbox and Sent Items (and their subfolders). */\n allowProtected?: boolean;\n /** Resolve and report every id without deleting anything. */\n dryRun?: boolean;\n}\n\n/** What happened to one id in a deleteOutlookEmails call. */\nexport interface DeleteMailOutcome {\n entryId: string;\n subject: string;\n /** The folder the item was actually in — the audit trail for a wrong id. */\n folderPath: string;\n status: 'deleted' | 'would-delete' | 'refused' | 'failed';\n reason: string;\n}\n\nexport interface DeleteMailResult {\n dryRun: boolean;\n deleted: number;\n refused: number;\n failed: number;\n items: DeleteMailOutcome[];\n}\n\nexport interface PurgeDeletedItemsOptions {\n /** Keep items newer than this many days. Default 0: purge everything. */\n olderThanDays?: number;\n /** Count what would be purged without destroying anything. */\n dryRun?: boolean;\n}\n\nexport interface PurgeDeletedItemsResult {\n folderPath: string;\n dryRun: boolean;\n /** Items old enough to qualify. */\n matched: number;\n purged: number;\n /** Items left because they were newer than `olderThanDays`. */\n kept: number;\n failed: number;\n}\n\n// ── Bounces ──────────────────────────────────────────────────────────────\n/** One bounce-back / non-delivery message found in a mailbox. */\nexport interface UndeliverableEmail {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** Which rule flagged it: an NDR message class, a mail-daemon sender, or a bounce subject phrase. */\n matchedReason: string;\n /** Best-effort addresses parsed from the bounce body — the sends that failed. */\n failedRecipients: string[];\n}\n\nexport interface CleanUndeliverableOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Report without deleting. Default TRUE — deleting has to be asked for. */\n dryRun?: boolean;\n}\n\nexport interface CleanUndeliverableResult {\n account: string;\n scannedDays: number;\n dryRun: boolean;\n matchedCount: number;\n deletedCount: number;\n matched: UndeliverableEmail[];\n /** Matched items that could not be deleted (a delete run only). */\n failed: ItemFailure[];\n}\n\nexport interface CollectBouncedRecipientsOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Scan Deleted Items too, so bounces already cleaned away still count. Default true. */\n includeDeletedItems?: boolean;\n}\n\nexport interface SentRecipientGroupsOptions {\n /** How far back to read Sent Items. Default 30. */\n daysBack?: number;\n /** Newest-first cap on messages. Default 3000. */\n limit?: number;\n}\n\n/** One sent message and every SMTP address it went to (To, CC and BCC). */\nexport interface SentRecipientGroup {\n entryId: string;\n subject: string;\n sentOn: string;\n recipients: string[];\n}\n\nexport interface BounceReportOptions {\n /** How far back to read bounce-backs and Sent Items. Default 30. */\n daysBack?: number;\n /** Count bounces already moved to Deleted Items too. Default true. */\n includeDeletedItems?: boolean;\n}\n\n/** A sent message that at least one of its recipients bounced back from. */\nexport interface BouncedSend {\n entryId: string;\n subject: string;\n sentOn: string;\n /** Every address it went to, lower-cased and deduplicated. */\n recipients: string[];\n /** The recipients a bounce-back reported as failed. */\n failedRecipients: string[];\n /** True when every recipient failed: the message reached nobody. */\n allFailed: boolean;\n}\n\nexport interface BounceReport {\n account: string;\n scannedDays: number;\n /** Every distinct address the bounce-backs reported, lower-cased. */\n bouncedAddresses: string[];\n /** The sent messages with a bounced recipient, newest first. */\n sends: BouncedSend[];\n /**\n * Bounced addresses no sent message in the window went to: an alias the\n * server expanded, or a send older than the window.\n */\n unmatchedAddresses: string[];\n}\n\n// ── Reading ──────────────────────────────────────────────────────────────\nexport interface ReadInboxOptions {\n /**\n * Read this folder instead of the Inbox root: a folder under the Inbox by\n * name or path, a well-known folder ('Sent Items', 'Drafts', …), or a full\n * folder path — see mailFolderRef. Only that folder is read, not its\n * subfolders.\n */\n folder?: string;\n /** Only mail from the last this-many days. Default 60. */\n daysBack?: number;\n /** Newest-first cap on rows. Default 50. */\n limit?: number;\n /** Characters of plain-text body per row. Default 600; 0 skips reading bodies. */\n previewChars?: number;\n}\n\nexport interface InboxEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n /** For outgoing folders (Sent Items, Outbox, Drafts) these carry the\n * RECIPIENT, since a folder of mail \"from\" its own owner says nothing. */\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n bodyPreview: string;\n attachmentNames: string[];\n attachmentCount: number;\n /** The folder the email was read from. */\n folderPath: string;\n}\n\nexport interface InboxSearchFilter {\n /** Only mail from the last this-many days. Default 60 — a scan has no\n * unbounded mode; ask for 365 to reach back a year. */\n daysBack?: number;\n /** Glob-style subject pattern: `*` matches any run, `?` one character,\n * case-insensitive, e.g. '*invoice*'. */\n subjectLike?: string;\n /**\n * Regular expression tested against each subject, case-insensitively\n * whatever the RegExp's own flags; on macOS a `u` or `v` flag is kept,\n * since it decides how the pattern parses. On Windows the pattern runs in\n * .NET's regex engine, so keep to syntax JavaScript and .NET share.\n */\n subjectPattern?: RegExp;\n /** Drop subjects carrying a reply or forward prefix (RE:, FW:, AW:, 回复:, …). */\n excludeReplies?: boolean;\n /** Only return mail with at least one attachment. */\n requireAttachment?: boolean;\n /**\n * Folders under the Inbox to leave out, by full folder path or bare name,\n * case-insensitively. An excluded folder takes its whole subtree with it.\n * Sent, Drafts, Deleted and Junk are always skipped, even where the profile\n * nests them under the Inbox. macOS matches the leaf name only.\n */\n excludeFolders?: readonly string[];\n /**\n * Scan only these folders, given the same way. Matched exactly: naming a\n * folder does not bring its children, and the Inbox root is included only\n * when named. Exclusion wins where the two overlap.\n */\n includeFolders?: readonly string[];\n /** Return each match's full plain-text body. Default true; false makes a\n * scan much lighter when only the metadata is needed. */\n includeBody?: boolean;\n}\n\nexport interface InboxSearchMatch extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n /** SMTP address, resolved from the Exchange DN where the sender is an Exchange user. */\n senderEmail: string;\n receivedTime: string;\n /** The plain-text body; '' when the search asked for no bodies. */\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n folderPath: string;\n}\n\n/**\n * What was kept about an email besides its id, for finding it again once the\n * id no longer resolves: Outlook rewrites an email's id whenever it moves, and\n * an id from one platform means nothing on the other.\n *\n * At least two of `subject`, `sender` and `receivedTime` are needed, since a\n * subject alone names every message in a thread.\n */\nexport interface EmailDescription {\n /** Compared without reply and forward prefixes, spacing or case. */\n subject?: string;\n /**\n * An address, a display name, or both (`Jo Doe <jo@example.com>`). An\n * address is compared as an address; a bare name only against the\n * sender's display name.\n */\n sender?: string;\n /** `yyyy-MM-dd HH:mm` as a listing reported it, or just the day. */\n receivedTime?: string;\n /** The mailbox to search. Default: every account, the one `folderPath` names first. */\n emailAccount?: string;\n /**\n * The folderPath a listing reported (`\\\\team@example.com\\Inbox\\Invoices`).\n * Only decides which mailbox is searched first.\n */\n folderPath?: string;\n}\n\nexport interface LocateEmailOptions {\n /** How far back to search when the description has no receivedTime. Default 60. */\n daysBack?: number;\n}\n\nexport interface SelectedEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n /**\n * The folder it sits in, in the form the listings report\n * (`\\\\team@example.com\\[Gmail]\\Drafts`) and so naming its mailbox. '' when\n * Outlook can't say: an email opened from a file, or on macOS one whose\n * mailbox neither AppleScript nor the profile database names.\n */\n folderPath: string;\n}\n\nexport interface ReadEmailBodyOptions {\n /** Cap on the returned body. Default 8000. */\n maxChars?: number;\n /** Also return the quoted thread below the reply. Default false. */\n includeQuoted?: boolean;\n /** Cap on the quoted thread when it is returned. Default 4000. */\n maxQuotedChars?: number;\n}\n\nexport interface EmailBodyResult {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** The sender's own new text — everything above the quoted thread. */\n body: string;\n /** True when `body` was cut at maxChars. */\n truncated: boolean;\n /** Length of the sender's new text before any cut. */\n bodyLength: number;\n /** What the quoted thread was split on; '' when none was found. */\n quoteSeparator: string;\n /** Length of the quoted thread, reported even when it isn't returned. */\n quotedLength: number;\n /** The quoted thread — only when asked for. */\n quotedOriginal: string;\n attachmentNames: string[];\n attachmentCount: number;\n}\n\n// ── Folders ──────────────────────────────────────────────────────────────\nexport interface MailFolderRef {\n /** olDefaultFolders id of the well-known root the path starts from. */\n rootId: number;\n /** That root as the caller spelled it, for messages. */\n rootLabel: string;\n /** Path segments below the root. */\n segments: string[];\n}\n\nexport interface ListFoldersOptions {\n /** How many levels below the Inbox to list, 1–10. Default 2. */\n maxDepth?: number;\n}\n\nexport interface InboxFolderInfo {\n name: string;\n /** Outlook folder path, e.g. `\\\\mailbox\\Inbox\\Invoices`. */\n folderPath: string;\n itemCount: number;\n /** 1 = direct child of the Inbox, 2 = grandchild, … */\n depth: number;\n}\n\nexport interface MoveEmailsOptions {\n /** Create the destination — the whole missing chain — when absent. Default false. */\n createIfMissing?: boolean;\n}\n\nexport interface MoveEmailsResult {\n folderPath: string;\n folderCreated: boolean;\n moved: number;\n failed: ItemFailure[];\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────\nexport interface SaveAttachmentOptions {\n /**\n * Directory to save into, created if absent. Without one, each call saves\n * into a fresh private directory, so two emails that both carry\n * 'invoice.pdf' can never overwrite each other.\n */\n destDir?: string;\n}\n\nexport interface SavedAttachment {\n /** Where the file was written. A name already taken in the directory gets\n * a numbered suffix rather than overwriting what is there. */\n path: string;\n /** The attachment's name as the email carries it. */\n fileName: string;\n /** Who the email was from — confirm it is the email you meant. */\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n}\n\n// ── Template emails ──────────────────────────────────────────────────────\nexport interface ReadTemplatesOptions {\n /** Mailbox folder holding the templates, searched up to 3 levels deep. Default 'Templates'. */\n folder?: string;\n /** Return only the template with this subject (case-insensitive). */\n subject?: string;\n /** Return each full HTML body. Default true; false returns previews. */\n includeBody?: boolean;\n /** Newest-first cap on templates, 1–200. Default 20. */\n limit?: number;\n}\n\n/** One template email stored in a mailbox folder. */\nexport interface TemplateEmail {\n entryId: string;\n subject: string;\n /** Full HTML body with embedded (cid:) images removed; '' without includeBody. */\n htmlBody: string;\n /** Short plain-text preview; '' with includeBody. */\n bodyPreview: string;\n /** [[SECTION]] names, returned with or without the body. */\n sections: string[];\n /** `{{PLACEHOLDER}}` names the template expects filled. */\n placeholders: string[];\n lastModified: string;\n}\n\nexport interface TemplateFolderResult {\n folderFound: boolean;\n /** The folder's path when found. */\n folderPath: string;\n templates: TemplateEmail[];\n /** When the folder wasn't found: the mailbox's top-level folder names. */\n availableFolders: string[];\n}\n\nexport interface SaveTemplateParams {\n subject: string;\n htmlBody: string;\n /** Folder to save into, created at the mailbox root if absent. Default 'Templates'. */\n folder?: string;\n}\n\nexport interface SaveTemplateResult {\n folderPath: string;\n folderCreated: boolean;\n}\n\n// ── The contract ─────────────────────────────────────────────────────────\n/**\n * Everything the package does, under one signature per operation whatever the\n * platform. A platform that cannot do something says so at run time — see\n * `CapabilityMap` — rather than through a narrower type.\n */\nexport interface OutlookBridge {\n /** Every mailbox this Outlook profile can reach, as SMTP addresses. */\n getOutlookAccounts(): Promise<string[]>;\n\n /** Send an email, or stage it as a draft. */\n sendOutlookEmail(params: SendEmailParams): Promise<SendEmailResult>;\n\n /** Reply to an email, with the new text above the quoted original. */\n replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;\n\n /** Recent mail from the Inbox root or one named folder, newest first. */\n readInboxEmails(emailAccount: string, options?: ReadInboxOptions): Promise<InboxEmail[]>;\n\n /** Walk every folder under the Inbox for the mail matching `filter`. */\n searchInboxByFilter(emailAccount: string, filter?: InboxSearchFilter): Promise<InboxSearchMatch[]>;\n\n /** The email currently selected, or open, in Outlook. */\n readSelectedEmail(): Promise<SelectedEmail>;\n\n /** One email's plain-text body, split from the thread it quotes. */\n readEmailBody(email: EmailRef, options?: ReadEmailBodyOptions): Promise<EmailBodyResult>;\n\n /** Open an email in Outlook. */\n openOutlookEmail(email: EmailRef): Promise<void>;\n\n /**\n * Find an email again from what was kept about it, when its id no longer\n * resolves. Searches the Inbox and its subfolders, and returns null rather\n * than a guess: every field the description gives has to agree.\n */\n locateEmail(description: EmailDescription, options?: LocateEmailOptions): Promise<InboxSearchMatch | null>;\n\n /** The folders under an account's Inbox. */\n listInboxFolders(emailAccount: string, options?: ListFoldersOptions): Promise<InboxFolderInfo[]>;\n\n /** File emails into a folder. Moving rewrites each email's entry id. */\n moveOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n folder: string,\n options?: MoveEmailsOptions,\n ): Promise<MoveEmailsResult>;\n\n /** The mail drafts bound to an account, newest first. */\n listOutlookDrafts(emailAccount: string, options?: ListDraftsOptions): Promise<ListDraftsResult>;\n\n /** Send the named drafts — each has to be one of this account's drafts. */\n sendDrafts(emailAccount: string, entryIds: readonly string[]): Promise<SendDraftsResult>;\n\n /** Send every draft bound to an account. */\n sendAllDrafts(emailAccount: string): Promise<SendDraftsResult>;\n\n /** Delete the named drafts (into Deleted Items) — each has to be one of this account's drafts. */\n deleteOutlookDrafts(emailAccount: string, entryIds: readonly string[]): Promise<DeleteDraftsResult>;\n\n /** Delete mail by id into Deleted Items, refusing received and sent mail unless allowed. */\n deleteOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n options?: DeleteMailOptions,\n ): Promise<DeleteMailResult>;\n\n /** Permanently empty an account's Deleted Items. The one irreversible operation. */\n purgeDeletedItems(emailAccount: string, options?: PurgeDeletedItemsOptions): Promise<PurgeDeletedItemsResult>;\n\n /** Save one attachment to disk. */\n saveEmailAttachment(email: EmailRef, fileName: string, options?: SaveAttachmentOptions): Promise<SavedAttachment>;\n\n /** Save several attachments from one email, in the order named. */\n saveEmailAttachments(\n email: EmailRef,\n fileNames: readonly string[],\n options?: SaveAttachmentOptions,\n ): Promise<SavedAttachment[]>;\n\n /** Find bounce-backs in the Inbox and, unless dry-running, delete them. */\n cleanUndeliverableEmails(\n emailAccount: string,\n options?: CleanUndeliverableOptions,\n ): Promise<CleanUndeliverableResult>;\n\n /** The addresses bounce-backs report as having failed, deduplicated. */\n collectBouncedRecipients(emailAccount: string, options?: CollectBouncedRecipientsOptions): Promise<string[]>;\n\n /** Sent Items, each message with the full set of addresses it went to. */\n readSentRecipientGroups(emailAccount: string, options?: SentRecipientGroupsOptions): Promise<SentRecipientGroup[]>;\n\n /** Which sent messages bounced, and for which of their recipients. */\n readBounceReport(emailAccount: string, options?: BounceReportOptions): Promise<BounceReport>;\n\n /** The names of the Outlook signatures on this machine. */\n listOutlookSignatures(): Promise<string[]>;\n\n /** A signature's HTML, or '' when there is no signature by that name. */\n readOutlookSignatureHtml(name: string): Promise<string>;\n\n /** The template emails saved in a mailbox folder. */\n readTemplateEmails(emailAccount: string, options?: ReadTemplatesOptions): Promise<TemplateFolderResult>;\n\n /** Save a new template email. Always adds; never overwrites. */\n saveTemplateEmail(emailAccount: string, template: SaveTemplateParams): Promise<SaveTemplateResult>;\n\n /**\n * Open HTML in an Outlook compose window, wait for the person editing it to\n * save and close, and return what they saved. Has no timeout; cancel it\n * with an AbortSignal.\n */\n editEmailTemplate(label: string, currentHtml: string): Promise<string>;\n}\n\n// ── Capability discovery ─────────────────────────────────────────────────\n/** Every operation whose availability can be asked about. */\nexport type BridgeCapability = keyof OutlookBridge;\n\n/**\n * Which operations work on this machine: all of them on Windows and macOS,\n * none anywhere else. Lets a UI disable what it cannot back instead of\n * discovering the gap when a user clicks.\n */\nexport type CapabilityMap = Readonly<Record<BridgeCapability, boolean>>;\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,wDAAwD;AACxD,EAAE;AACF,mCAAmC;AACnC,+EAA+E;AAC/E,8EAA8E;AAC9E,YAAY;AACZ,iFAAiF;AACjF,+BAA+B","sourcesContent":["// The public contract: what the package returns, what it takes, and the\n// OutlookBridge interface every entry point answers to.\n//\n// Two conventions hold throughout:\n// - Timestamps are local time as 'yyyy-MM-dd HH:mm', which sorts correctly as\n// a plain string and is what both platforms can produce without a timezone\n// table.\n// - Ids are platform-specific (see EmailLocator). Don't persist one and look it\n// up on the other platform.\n\n// ── Identifying an email ─────────────────────────────────────────────────\n/**\n * Where an email lives, as a listing reported it.\n *\n * Every listing row (`InboxEmail`, `InboxSearchMatch`, `SelectedEmail`, …)\n * carries both fields, so a row can be passed straight back in wherever an\n * `EmailRef` is accepted — which is also the reliable way to keep the two ids\n * from different rows apart.\n */\nexport interface EmailLocator {\n /** Windows: the MAPI EntryID. macOS: Outlook for Mac's integer message id. */\n readonly entryId: string;\n /**\n * Windows: the StoreID the email was listed from, which lets it resolve in\n * any mounted mailbox rather than only the default one. macOS has no\n * equivalent and ignores it.\n */\n readonly storeId?: string;\n}\n\n/** An email to act on: its entry id alone, or anything carrying a locator. */\nexport type EmailRef = string | EmailLocator;\n\n/**\n * Recipients: one address, a comma- or semicolon-separated list, or an array.\n * Display-name forms such as `\"Doe, Jo\" <jo@example.com>` are kept whole.\n */\nexport type Recipients = string | readonly string[];\n\n// ── Sending and replying ─────────────────────────────────────────────────\nexport interface SendEmailParams {\n /** SMTP address of the mailbox to send from. Never falls back to the\n * default account — an address that doesn't resolve is an error. */\n emailAccount: string;\n to?: Recipients;\n cc?: Recipients;\n bcc?: Recipients;\n subject: string;\n htmlBody: string;\n /** Absolute paths of files to attach. Each has to exist. */\n attachments?: readonly string[];\n /**\n * An Outlook signature, by the name listOutlookSignatures reports, added to\n * the body: in its `{{SIGNATURE}}` placeholder when it has one, otherwise\n * at the end. Outlook adds no signature of its own to a body set this way,\n * so this is how a composed email gets one.\n */\n signatureName?: string;\n /** Send now. Default false: the email is staged as a draft instead. */\n sendImmediately?: boolean;\n /**\n * Drafts only. True (the default) opens an Outlook compose window; false\n * files the draft silently into Drafts — the workable choice for batches.\n */\n openDraftWindow?: boolean;\n}\n\nexport interface SendEmailResult {\n /**\n * The draft's entry id when the email was filed silently into Drafts\n * (`openDraftWindow: false`) — the id `listOutlookDrafts` reports for it,\n * so a caller can keep it and later send or delete exactly the drafts it\n * made. '' when the email was sent, or opened in a compose window, which\n * files nothing yet; and on macOS in the rare case Drafts gained another\n * draft with the same subject in the same moment, so this one could not be\n * told apart.\n */\n draftEntryId: string;\n}\n\nexport interface ReplyEmailParams extends EmailLocator {\n /** SMTP address of the mailbox to reply from. */\n emailAccount: string;\n /**\n * HTML inserted above the quoted original (a full document is reduced to\n * its <body> content). Optional when `templateSubject` is given.\n */\n htmlBody?: string;\n /**\n * Reply with a template email saved in the mailbox, found by subject, so\n * the caller never carries a large template's HTML itself.\n */\n templateSubject?: string;\n /** Folder holding the template. Default 'Templates'. */\n templateFolder?: string;\n /**\n * The [[SECTION]] of the template to keep, when one template holds several\n * reply variants (see composeTemplateBody). Required if it has sections.\n */\n templateSection?: string;\n /** `{{PLACEHOLDER}}` → HTML substituted into the composed body. */\n templatePlaceholders?: Readonly<Record<string, string>>;\n /**\n * An Outlook signature (by the name listOutlookSignatures reports), put in\n * the body's `{{SIGNATURE}}` placeholder, or below the new text when there\n * is none. Resolved here, so the signature's HTML never crosses the\n * caller's boundary.\n */\n signatureName?: string;\n /**\n * Answer every recipient of the original instead of only its sender.\n * Default false: widening a reply cannot be taken back, so it is opt-in.\n */\n replyAll?: boolean;\n /** Send now. Default false. */\n sendImmediately?: boolean;\n /** Drafts only: open a compose window (the default) or file silently. */\n openDraftWindow?: boolean;\n}\n\nexport interface ReplyEmailResult {\n /** Who the reply is addressed to. Windows reports Outlook's To line\n * (display names); macOS the resolved addresses. */\n to: string;\n subject: string;\n /** The original email's sender — check it is who you meant to answer. */\n repliedToSender: string;\n /**\n * The draft's entry id when the reply was filed silently into Drafts\n * (`openDraftWindow: false`), as `SendEmailResult.draftEntryId` reports a\n * new email's: the id `listOutlookDrafts` gives it, so a caller can later\n * send or delete exactly the replies it drafted. '' when the reply was\n * sent or opened in a compose window, and on macOS when Drafts gained\n * another draft with the same subject in the same moment.\n */\n draftEntryId: string;\n}\n\n// ── Per-item outcomes ────────────────────────────────────────────────────\n/** One item a batch operation could not handle, and why. */\nexport interface ItemFailure {\n entryId: string;\n /** The item's subject when it could be read, else ''. */\n subject: string;\n error: string;\n}\n\n// ── Drafts ───────────────────────────────────────────────────────────────\n/** One mail draft belonging to an account. */\nexport interface OutlookDraft {\n entryId: string;\n subject: string;\n /** The To line as Outlook renders it. */\n to: string;\n /** Recipient addresses, best effort — an unresolved Exchange entry falls back to its name. */\n toEmails: string[];\n /** Short plain-text preview, enough to tell similar drafts apart. */\n bodyPreview: string;\n hasAttachments: boolean;\n lastModified: string;\n /** The Drafts folder it sits in; one account's drafts can span two. */\n folderPath: string;\n}\n\nexport interface ListDraftsOptions {\n /** Newest-first cap on the drafts returned. Default 100. */\n limit?: number;\n /** Characters of body preview per draft. Default 300; 0 omits it. */\n previewChars?: number;\n}\n\nexport interface ListDraftsResult {\n account: string;\n /** The Drafts folders scanned, in scan order. */\n foldersScanned: string[];\n /** Matching drafts found, before `limit` was applied. */\n count: number;\n truncated: boolean;\n drafts: OutlookDraft[];\n}\n\nexport interface SendDraftsResult {\n sent: number;\n failed: ItemFailure[];\n}\n\nexport interface DeleteDraftsResult {\n deleted: number;\n failed: ItemFailure[];\n}\n\n// ── Deleting mail ────────────────────────────────────────────────────────\nexport interface DeleteMailOptions {\n /** Lift the refusal to touch Inbox and Sent Items (and their subfolders). */\n allowProtected?: boolean;\n /** Resolve and report every id without deleting anything. */\n dryRun?: boolean;\n}\n\n/** What happened to one id in a deleteOutlookEmails call. */\nexport interface DeleteMailOutcome {\n entryId: string;\n subject: string;\n /** The folder the item was actually in — the audit trail for a wrong id. */\n folderPath: string;\n status: 'deleted' | 'would-delete' | 'refused' | 'failed';\n reason: string;\n}\n\nexport interface DeleteMailResult {\n dryRun: boolean;\n deleted: number;\n refused: number;\n failed: number;\n items: DeleteMailOutcome[];\n}\n\nexport interface PurgeDeletedItemsOptions {\n /** Keep items newer than this many days. Default 0: purge everything. */\n olderThanDays?: number;\n /** Count what would be purged without destroying anything. */\n dryRun?: boolean;\n}\n\nexport interface PurgeDeletedItemsResult {\n folderPath: string;\n dryRun: boolean;\n /** Items old enough to qualify. */\n matched: number;\n purged: number;\n /** Items left because they were newer than `olderThanDays`. */\n kept: number;\n failed: number;\n}\n\n// ── Bounces ──────────────────────────────────────────────────────────────\n/** One bounce-back / non-delivery message found in a mailbox. */\nexport interface UndeliverableEmail {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** Which rule flagged it: an NDR message class, a mail-daemon sender, or a bounce subject phrase. */\n matchedReason: string;\n /** Best-effort addresses parsed from the bounce body — the sends that failed. */\n failedRecipients: string[];\n}\n\nexport interface CleanUndeliverableOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Report without deleting. Default TRUE — deleting has to be asked for. */\n dryRun?: boolean;\n}\n\nexport interface CleanUndeliverableResult {\n account: string;\n scannedDays: number;\n dryRun: boolean;\n matchedCount: number;\n deletedCount: number;\n matched: UndeliverableEmail[];\n /** Matched items that could not be deleted (a delete run only). */\n failed: ItemFailure[];\n}\n\nexport interface CollectBouncedRecipientsOptions {\n /** How far back to scan. Default 30. */\n daysBack?: number;\n /** Scan Deleted Items too, so bounces already cleaned away still count. Default true. */\n includeDeletedItems?: boolean;\n}\n\nexport interface SentRecipientGroupsOptions {\n /** How far back to read Sent Items. Default 30. */\n daysBack?: number;\n /** Newest-first cap on messages. Default 3000. */\n limit?: number;\n}\n\n/** One sent message and every SMTP address it went to (To, CC and BCC). */\nexport interface SentRecipientGroup {\n entryId: string;\n subject: string;\n sentOn: string;\n recipients: string[];\n}\n\nexport interface BounceReportOptions {\n /** How far back to read bounce-backs and Sent Items. Default 30. */\n daysBack?: number;\n /** Count bounces already moved to Deleted Items too. Default true. */\n includeDeletedItems?: boolean;\n}\n\n/** A sent message that at least one of its recipients bounced back from. */\nexport interface BouncedSend {\n entryId: string;\n subject: string;\n sentOn: string;\n /** Every address it went to, lower-cased and deduplicated. */\n recipients: string[];\n /** The recipients a bounce-back reported as failed. */\n failedRecipients: string[];\n /** True when every recipient failed: the message reached nobody. */\n allFailed: boolean;\n}\n\nexport interface BounceReport {\n account: string;\n scannedDays: number;\n /** Every distinct address the bounce-backs reported, lower-cased. */\n bouncedAddresses: string[];\n /** The sent messages with a bounced recipient, newest first. */\n sends: BouncedSend[];\n /**\n * Bounced addresses no sent message in the window went to: an alias the\n * server expanded, or a send older than the window.\n */\n unmatchedAddresses: string[];\n}\n\n// ── Reading ──────────────────────────────────────────────────────────────\nexport interface ReadInboxOptions {\n /**\n * Read this folder instead of the Inbox root: a folder under the Inbox by\n * name or path, a well-known folder ('Sent Items', 'Drafts', …), or a full\n * folder path — see mailFolderRef. Only that folder is read, not its\n * subfolders.\n */\n folder?: string;\n /** Only mail from the last this-many days. Default 60. */\n daysBack?: number;\n /** Newest-first cap on rows. Default 50. */\n limit?: number;\n /** Characters of plain-text body per row. Default 600; 0 skips reading bodies. */\n previewChars?: number;\n}\n\nexport interface InboxEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n /** For outgoing folders (Sent Items, Outbox, Drafts) these carry the\n * RECIPIENT, since a folder of mail \"from\" its own owner says nothing. */\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n bodyPreview: string;\n attachmentNames: string[];\n attachmentCount: number;\n /** The folder the email was read from. */\n folderPath: string;\n}\n\nexport interface InboxSearchFilter {\n /** Only mail from the last this-many days. Default 60 — a scan has no\n * unbounded mode; ask for 365 to reach back a year. */\n daysBack?: number;\n /** Glob-style subject pattern: `*` matches any run, `?` one character,\n * case-insensitive, e.g. '*invoice*'. */\n subjectLike?: string;\n /**\n * Regular expression tested against each subject, case-insensitively\n * whatever the RegExp's own flags; on macOS a `u` or `v` flag is kept,\n * since it decides how the pattern parses. On Windows the pattern runs in\n * .NET's regex engine, so keep to syntax JavaScript and .NET share.\n */\n subjectPattern?: RegExp;\n /** Drop subjects carrying a reply or forward prefix (RE:, FW:, AW:, 回复:, …). */\n excludeReplies?: boolean;\n /** Only return mail with at least one attachment. */\n requireAttachment?: boolean;\n /**\n * Folders under the Inbox to leave out, by full folder path or bare name,\n * case-insensitively. An excluded folder takes its whole subtree with it.\n * Sent, Drafts, Deleted, Junk and the Outbox are always skipped, even where\n * the profile nests them under the Inbox. macOS matches the leaf name only.\n */\n excludeFolders?: readonly string[];\n /**\n * Scan only these folders, given the same way. Matched exactly: naming a\n * folder does not bring its children, and the Inbox root is included only\n * when named. Exclusion wins where the two overlap.\n */\n includeFolders?: readonly string[];\n /** Return each match's full plain-text body. Default true; false makes a\n * scan much lighter when only the metadata is needed. */\n includeBody?: boolean;\n}\n\nexport interface InboxSearchMatch extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n /** SMTP address, resolved from the Exchange DN where the sender is an Exchange user. */\n senderEmail: string;\n receivedTime: string;\n /** The plain-text body; '' when the search asked for no bodies. */\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n folderPath: string;\n}\n\n/**\n * What was kept about an email besides its id, for finding it again once the\n * id no longer resolves: Outlook rewrites an email's id whenever it moves, and\n * an id from one platform means nothing on the other.\n *\n * At least two of `subject`, `sender` and `receivedTime` are needed, since a\n * subject alone names every message in a thread.\n */\nexport interface EmailDescription {\n /** Compared without reply and forward prefixes, spacing or case. */\n subject?: string;\n /**\n * An address, a display name, or both (`Jo Doe <jo@example.com>`). An\n * address is compared as an address; a bare name only against the\n * sender's display name.\n */\n sender?: string;\n /** `yyyy-MM-dd HH:mm` as a listing reported it, or just the day. */\n receivedTime?: string;\n /** The mailbox to search. Default: every account, the one `folderPath` names first. */\n emailAccount?: string;\n /**\n * The folderPath a listing reported (`\\\\team@example.com\\Inbox\\Invoices`).\n * Only decides which mailbox is searched first.\n */\n folderPath?: string;\n}\n\nexport interface LocateEmailOptions {\n /** How far back to search when the description has no receivedTime. Default 60. */\n daysBack?: number;\n}\n\nexport interface SelectedEmail extends EmailLocator {\n entryId: string;\n storeId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n body: string;\n /** Not saved to disk — pass the ones you want to saveEmailAttachments. */\n attachmentNames: string[];\n /**\n * The folder it sits in, in the form the listings report\n * (`\\\\team@example.com\\[Gmail]\\Drafts`) and so naming its mailbox. '' when\n * Outlook can't say: an email opened from a file, or on macOS one whose\n * mailbox neither AppleScript nor the profile database names.\n */\n folderPath: string;\n}\n\nexport interface ReadEmailBodyOptions {\n /** Cap on the returned body. Default 8000. */\n maxChars?: number;\n /** Also return the quoted thread below the reply. Default false. */\n includeQuoted?: boolean;\n /** Cap on the quoted thread when it is returned. Default 4000. */\n maxQuotedChars?: number;\n}\n\nexport interface EmailBodyResult {\n entryId: string;\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n /** The sender's own new text — everything above the quoted thread. */\n body: string;\n /** True when `body` was cut at maxChars. */\n truncated: boolean;\n /** Length of the sender's new text before any cut. */\n bodyLength: number;\n /** What the quoted thread was split on; '' when none was found. */\n quoteSeparator: string;\n /** Length of the quoted thread, reported even when it isn't returned. */\n quotedLength: number;\n /** The quoted thread — only when asked for. */\n quotedOriginal: string;\n attachmentNames: string[];\n attachmentCount: number;\n}\n\n// ── Folders ──────────────────────────────────────────────────────────────\nexport interface MailFolderRef {\n /** olDefaultFolders id of the well-known root the path starts from. */\n rootId: number;\n /** That root as the caller spelled it, for messages. */\n rootLabel: string;\n /** Path segments below the root. */\n segments: string[];\n}\n\nexport interface ListFoldersOptions {\n /** How many levels below the Inbox to list, 1–10. Default 2. */\n maxDepth?: number;\n}\n\nexport interface InboxFolderInfo {\n name: string;\n /** Outlook folder path, e.g. `\\\\mailbox\\Inbox\\Invoices`. */\n folderPath: string;\n itemCount: number;\n /** 1 = direct child of the Inbox, 2 = grandchild, … */\n depth: number;\n}\n\nexport interface MoveEmailsOptions {\n /** Create the destination — the whole missing chain — when absent. Default false. */\n createIfMissing?: boolean;\n}\n\nexport interface MoveEmailsResult {\n folderPath: string;\n folderCreated: boolean;\n moved: number;\n failed: ItemFailure[];\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────\nexport interface SaveAttachmentOptions {\n /**\n * Directory to save into, created if absent. Without one, each call saves\n * into a fresh private directory, so two emails that both carry\n * 'invoice.pdf' can never overwrite each other.\n */\n destDir?: string;\n}\n\nexport interface SavedAttachment {\n /** Where the file was written. A name already taken in the directory gets\n * a numbered suffix rather than overwriting what is there. */\n path: string;\n /** The attachment's name as the email carries it. */\n fileName: string;\n /** Who the email was from — confirm it is the email you meant. */\n subject: string;\n senderName: string;\n senderEmail: string;\n receivedTime: string;\n}\n\n// ── Template emails ──────────────────────────────────────────────────────\nexport interface ReadTemplatesOptions {\n /** Mailbox folder holding the templates, searched up to 3 levels deep. Default 'Templates'. */\n folder?: string;\n /** Return only the template with this subject (case-insensitive). */\n subject?: string;\n /** Return each full HTML body. Default true; false returns previews. */\n includeBody?: boolean;\n /** Newest-first cap on templates, 1–200. Default 20. */\n limit?: number;\n}\n\n/** One template email stored in a mailbox folder. */\nexport interface TemplateEmail {\n entryId: string;\n subject: string;\n /** Full HTML body with embedded (cid:) images removed; '' without includeBody. */\n htmlBody: string;\n /** Short plain-text preview; '' with includeBody. */\n bodyPreview: string;\n /** [[SECTION]] names, returned with or without the body. */\n sections: string[];\n /** `{{PLACEHOLDER}}` names the template expects filled. */\n placeholders: string[];\n lastModified: string;\n}\n\nexport interface TemplateFolderResult {\n folderFound: boolean;\n /** The folder's path when found. */\n folderPath: string;\n templates: TemplateEmail[];\n /** When the folder wasn't found: the mailbox's top-level folder names. */\n availableFolders: string[];\n}\n\nexport interface SaveTemplateParams {\n subject: string;\n htmlBody: string;\n /** Folder to save into, created at the mailbox root if absent. Default 'Templates'. */\n folder?: string;\n}\n\nexport interface SaveTemplateResult {\n folderPath: string;\n folderCreated: boolean;\n}\n\n// ── The contract ─────────────────────────────────────────────────────────\n/**\n * Everything the package does, under one signature per operation whatever the\n * platform. A platform that cannot do something says so at run time — see\n * `CapabilityMap` — rather than through a narrower type.\n */\nexport interface OutlookBridge {\n /** Every mailbox this Outlook profile can reach, as SMTP addresses. */\n getOutlookAccounts(): Promise<string[]>;\n\n /** Send an email, or stage it as a draft. */\n sendOutlookEmail(params: SendEmailParams): Promise<SendEmailResult>;\n\n /** Reply to an email, with the new text above the quoted original. */\n replyOutlookEmail(params: ReplyEmailParams): Promise<ReplyEmailResult>;\n\n /** Recent mail from the Inbox root or one named folder, newest first. */\n readInboxEmails(emailAccount: string, options?: ReadInboxOptions): Promise<InboxEmail[]>;\n\n /** Walk every folder under the Inbox for the mail matching `filter`. */\n searchInboxByFilter(emailAccount: string, filter?: InboxSearchFilter): Promise<InboxSearchMatch[]>;\n\n /** The email currently selected, or open, in Outlook. */\n readSelectedEmail(): Promise<SelectedEmail>;\n\n /** One email's plain-text body, split from the thread it quotes. */\n readEmailBody(email: EmailRef, options?: ReadEmailBodyOptions): Promise<EmailBodyResult>;\n\n /** Open an email in Outlook. */\n openOutlookEmail(email: EmailRef): Promise<void>;\n\n /**\n * Find an email again from what was kept about it, when its id no longer\n * resolves. Searches the Inbox and its subfolders, and returns null rather\n * than a guess: every field the description gives has to agree.\n */\n locateEmail(description: EmailDescription, options?: LocateEmailOptions): Promise<InboxSearchMatch | null>;\n\n /** The folders under an account's Inbox. */\n listInboxFolders(emailAccount: string, options?: ListFoldersOptions): Promise<InboxFolderInfo[]>;\n\n /**\n * File emails into a folder. Moving rewrites each email's entry id. Only the\n * account's own mail is moved: an id that resolves in another mailbox fails\n * as that item.\n */\n moveOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n folder: string,\n options?: MoveEmailsOptions,\n ): Promise<MoveEmailsResult>;\n\n /** The mail drafts bound to an account, newest first. */\n listOutlookDrafts(emailAccount: string, options?: ListDraftsOptions): Promise<ListDraftsResult>;\n\n /** Send the named drafts — each has to be one of this account's drafts. */\n sendDrafts(emailAccount: string, entryIds: readonly string[]): Promise<SendDraftsResult>;\n\n /** Send every draft bound to an account. */\n sendAllDrafts(emailAccount: string): Promise<SendDraftsResult>;\n\n /** Delete the named drafts (into Deleted Items) — each has to be one of this account's drafts. */\n deleteOutlookDrafts(emailAccount: string, entryIds: readonly string[]): Promise<DeleteDraftsResult>;\n\n /**\n * Delete mail by id into Deleted Items, refusing received and sent mail\n * unless allowed — and mail in any other mailbox whatever is allowed.\n */\n deleteOutlookEmails(\n emailAccount: string,\n entryIds: readonly string[],\n options?: DeleteMailOptions,\n ): Promise<DeleteMailResult>;\n\n /** Permanently empty an account's Deleted Items. The one irreversible operation. */\n purgeDeletedItems(emailAccount: string, options?: PurgeDeletedItemsOptions): Promise<PurgeDeletedItemsResult>;\n\n /** Save one attachment to disk. */\n saveEmailAttachment(email: EmailRef, fileName: string, options?: SaveAttachmentOptions): Promise<SavedAttachment>;\n\n /** Save several attachments from one email, in the order named. */\n saveEmailAttachments(\n email: EmailRef,\n fileNames: readonly string[],\n options?: SaveAttachmentOptions,\n ): Promise<SavedAttachment[]>;\n\n /** Find bounce-backs in the Inbox and, unless dry-running, delete them. */\n cleanUndeliverableEmails(\n emailAccount: string,\n options?: CleanUndeliverableOptions,\n ): Promise<CleanUndeliverableResult>;\n\n /** The addresses bounce-backs report as having failed, deduplicated. */\n collectBouncedRecipients(emailAccount: string, options?: CollectBouncedRecipientsOptions): Promise<string[]>;\n\n /** Sent Items, each message with the full set of addresses it went to. */\n readSentRecipientGroups(emailAccount: string, options?: SentRecipientGroupsOptions): Promise<SentRecipientGroup[]>;\n\n /** Which sent messages bounced, and for which of their recipients. */\n readBounceReport(emailAccount: string, options?: BounceReportOptions): Promise<BounceReport>;\n\n /** The names of the Outlook signatures on this machine. */\n listOutlookSignatures(): Promise<string[]>;\n\n /** A signature's HTML, or '' when there is no signature by that name. */\n readOutlookSignatureHtml(name: string): Promise<string>;\n\n /** The template emails saved in a mailbox folder. */\n readTemplateEmails(emailAccount: string, options?: ReadTemplatesOptions): Promise<TemplateFolderResult>;\n\n /** Save a new template email. Always adds; never overwrites. */\n saveTemplateEmail(emailAccount: string, template: SaveTemplateParams): Promise<SaveTemplateResult>;\n\n /**\n * Open HTML in an Outlook compose window, wait for the person editing it to\n * save and close, and return what they saved. Has no timeout; cancel it\n * with an AbortSignal.\n */\n editEmailTemplate(label: string, currentHtml: string): Promise<string>;\n}\n\n// ── Capability discovery ─────────────────────────────────────────────────\n/** Every operation whose availability can be asked about. */\nexport type BridgeCapability = keyof OutlookBridge;\n\n/**\n * Which operations work on this machine: all of them on Windows and macOS,\n * none anywhere else. Lets a UI disable what it cannot back instead of\n * discovering the gap when a user clicks.\n */\nexport type CapabilityMap = Readonly<Record<BridgeCapability, boolean>>;\n"]}
|
package/dist/windows/accounts.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.getOutlookAccounts = getOutlookAccounts;
|
|
|
4
4
|
// The mailboxes a Windows Outlook profile can reach.
|
|
5
5
|
const run_1 = require("./run");
|
|
6
6
|
const scripts_1 = require("./scripts");
|
|
7
|
-
const json_1 = require("
|
|
7
|
+
const json_1 = require("./json");
|
|
8
8
|
/**
|
|
9
9
|
* Every mailbox this profile can reach, as SMTP addresses.
|
|
10
10
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/windows/accounts.ts"],"names":[],"mappings":";;AAeA,gDAeC;AA9BD,qDAAqD;AACrD,+BAA0C;AAC1C,uCAAuC;AACvC,
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../../src/windows/accounts.ts"],"names":[],"mappings":";;AAeA,gDAeC;AA9BD,qDAAqD;AACrD,+BAA0C;AAC1C,uCAAuC;AACvC,iCAAiC;AAEjC;;;;;;;;;GASG;AACI,KAAK,UAAU,kBAAkB;IACpC,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,oBAAU;;;;;;;;;;;;CAYvD,EAAE,OAAO,CAAC,CAAC;IACR,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC;AAC3B,CAAC","sourcesContent":["// The mailboxes a Windows Outlook profile can reach.\nimport { runPowerShellJson } from './run';\nimport { SESSION_PS } from './scripts';\nimport { strList } from './json';\n\n/**\n * Every mailbox this profile can reach, as SMTP addresses.\n *\n * Walks Accounts AND the top-level store folders, because they are not the same\n * set: a mailbox mounted as a secondary store has no Account object, and an\n * Accounts-only walk would report a profile missing exactly the mailboxes an\n * operator added on purpose. A store contributes only when its name is an\n * address; a data file's store is named whatever it was called (\"Archive\") and\n * names no mailbox a caller could pass back in.\n */\nexport async function getOutlookAccounts(): Promise<string[]> {\n const output = await runPowerShellJson(`${SESSION_PS}\n$seen = @{}\n$accounts = @()\nforeach ($a in $ns.Accounts) {\n $address = [string]$a.SmtpAddress\n if ($address -and -not $seen.ContainsKey($address.ToLower())) { $seen[$address.ToLower()] = $true; $accounts += $address }\n}\nforeach ($f in $ns.Folders) {\n $name = [string]$f.Name\n if ($name -match '@' -and -not $seen.ContainsKey($name.ToLower())) { $seen[$name.ToLower()] = $true; $accounts += $name }\n}\nConvertTo-Json -Compress -InputObject @($accounts)\n`, 'quick');\n return strList(output);\n}\n"]}
|
|
@@ -4,7 +4,7 @@ exports.saveEmailAttachments = saveEmailAttachments;
|
|
|
4
4
|
// Saving attachments off an email.
|
|
5
5
|
const run_1 = require("./run");
|
|
6
6
|
const scripts_1 = require("./scripts");
|
|
7
|
-
const json_1 = require("
|
|
7
|
+
const json_1 = require("./json");
|
|
8
8
|
const attachmentMatch_1 = require("../shared/attachmentMatch");
|
|
9
9
|
const errors_1 = require("../errors");
|
|
10
10
|
/**
|
|
@@ -65,7 +65,7 @@ try { $subject = [string]$item.Subject } catch {}
|
|
|
65
65
|
$senderName = ''
|
|
66
66
|
try { $senderName = [string]$item.SenderName } catch {}
|
|
67
67
|
$received = ''
|
|
68
|
-
try { $received = $item.ReceivedTime
|
|
68
|
+
try { $received = ${(0, scripts_1.stampPs)('$item.ReceivedTime')} } catch {}
|
|
69
69
|
$attachments = @()
|
|
70
70
|
foreach ($att in $item.Attachments) { $attachments += $att }
|
|
71
71
|
$picks = @()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/windows/attachments.ts"],"names":[],"mappings":";;AAuDA,oDAiEC;AAxHD,mCAAmC;AACnC,+BAAoE;AACpE,
|
|
1
|
+
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/windows/attachments.ts"],"names":[],"mappings":";;AAuDA,oDAiEC;AAxHD,mCAAmC;AACnC,+BAAoE;AACpE,uCAAkF;AAClF,iCAA8C;AAC9C,+DAKmC;AACnC,sCAAuC;AAIvC;;;;GAIG;AACH,MAAM,YAAY,GAAG;iBACJ,IAAA,cAAQ,EAAC,wCAAsB,CAAC;kBAC/B,IAAA,cAAQ,EAAC,oCAAkB,CAAC;aACjC,IAAA,WAAK,EAAC,+BAAa,CAAC;;;;;;;;;;;;;;;;;;;;;CAqBhC,CAAC;AAEF;;;;;;;;;GASG;AACI,KAAK,UAAU,oBAAoB,CAAC,OAA+B;IACtE,MAAM,QAAQ,GAAG,IAAA,2CAAyB,EAAC;QACvC,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,aAAa;QAC1B,OAAO,EAAE,UAAU;QACnB,YAAY,EAAE,WAAW;QACzB,OAAO,EAAE,UAAU;KACtB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,oBAAU;EACtD,IAAA,0BAAgB,EAAC,OAAO,CAAC,KAAK,CAAC;EAC/B,wBAAc;EACd,YAAY;aACD,IAAA,cAAQ,EAAC,OAAO,CAAC,OAAO,CAAC;;;;;;oBAMlB,IAAA,iBAAO,EAAC,oBAAoB,CAAC;;;;sBAI3B,IAAA,aAAO,EAAC,OAAO,CAAC,SAAS,CAAC;;;;;;;;;;;;iBAY/B,IAAA,mBAAU,EAAC,WAAW,EAAE,YAAY,CAAC,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;CAmBhE,EAAE,UAAU,CAAC,CAAC;IACX,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;YACjB,QAAQ,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,QAAQ,CAAC;YACzB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,WAAW,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,WAAW,CAAC;YAC/B,YAAY,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,YAAY,CAAC;SACpC,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Saving attachments off an email.\nimport { psArray, psInt, psString, runPowerShellJson } from './run';\nimport { itemLookupScript, SENDER_SMTP_PS, SESSION_PS, stampPs } from './scripts';\nimport { record, str, toArray } from './json';\nimport {\n attachmentNotFoundMessage,\n MAX_FILE_STEM,\n RESERVED_FILE_NAME,\n UNSAFE_FILE_NAME_CHARS,\n} from '../shared/attachmentMatch';\nimport { failureTag } from '../errors';\nimport type { SaveAttachmentsRequest } from '../backend';\nimport type { SavedAttachment } from '../types';\n\n/**\n * The file-name rules from shared/attachmentMatch.ts, as PowerShell: the same\n * sanitizing `safeFileName` does, and the same never-overwrite numbering as\n * `uniqueSavePath`.\n */\nconst SAVE_PATH_PS = `\n$unsafeChars = ${psString(UNSAFE_FILE_NAME_CHARS)}\n$reservedName = ${psString(RESERVED_FILE_NAME)}\n$maxStem = ${psInt(MAX_FILE_STEM)}\nfunction Get-SafeFileName([string]$name) {\n $safe = [regex]::Replace($name, $unsafeChars, '_').TrimEnd([char[]]'. ')\n if ($safe -match $reservedName) { $safe = '_' + $safe }\n $ext = [IO.Path]::GetExtension($safe)\n $stem = $safe.Substring(0, $safe.Length - $ext.Length)\n if ($stem.Length -gt $maxStem) { $safe = $stem.Substring(0, $maxStem) + $ext }\n if (-not $safe) { $safe = 'attachment' }\n return $safe\n}\nfunction Get-UniqueSavePath([string]$dir, [string]$name) {\n $ext = [IO.Path]::GetExtension($name)\n $stem = $name.Substring(0, $name.Length - $ext.Length)\n $candidate = [IO.Path]::Combine($dir, $name)\n $n = 1\n while (Test-Path -LiteralPath $candidate) {\n $candidate = [IO.Path]::Combine($dir, \"$stem ($n)$ext\")\n $n++\n }\n return $candidate\n}\n`;\n\n/**\n * Save the named attachments of one email, in the order named.\n *\n * Every name is matched before anything is written — exact first, then ignoring\n * case and whitespace runs — so a batch with one bad name fails without having\n * saved half its files. A name that matches nothing fails as NOT_FOUND naming\n * the email that actually resolved and the attachments it does carry: an id can\n * resolve to a different email than the caller meant, and a bare \"not found\"\n * would point at the wrong problem.\n */\nexport async function saveEmailAttachments(request: SaveAttachmentsRequest): Promise<SavedAttachment[]> {\n const notFound = attachmentNotFoundMessage({\n fileName: '$wanted',\n senderEmail: '$senderSmtp',\n subject: '$subject',\n receivedTime: '$received',\n present: '$present',\n });\n const output = await runPowerShellJson(`${SESSION_PS}\n${itemLookupScript(request.email)}\n${SENDER_SMTP_PS}\n${SAVE_PATH_PS}\n$destDir = ${psString(request.destDir)}\n$subject = ''\ntry { $subject = [string]$item.Subject } catch {}\n$senderName = ''\ntry { $senderName = [string]$item.SenderName } catch {}\n$received = ''\ntry { $received = ${stampPs('$item.ReceivedTime')} } catch {}\n$attachments = @()\nforeach ($att in $item.Attachments) { $attachments += $att }\n$picks = @()\nforeach ($wanted in ${psArray(request.fileNames)}) {\n $found = $null\n foreach ($att in $attachments) { if ([string]$att.FileName -ceq $wanted) { $found = $att; break } }\n if ($found -eq $null) {\n $normalized = ($wanted -replace '\\\\s+', ' ').Trim()\n foreach ($att in $attachments) {\n if ((([string]$att.FileName) -replace '\\\\s+', ' ').Trim() -ieq $normalized) { $found = $att; break }\n }\n }\n if ($found -eq $null) {\n $present = (@($attachments | ForEach-Object { [string]$_.FileName }) -join ', ')\n if (-not $present) { $present = '(none)' }\n throw \"${failureTag('NOT_FOUND', 'attachment')}${notFound}\"\n }\n $picks += $found\n}\n$rows = @()\nforeach ($att in $picks) {\n $fileName = [string]$att.FileName\n $savePath = Get-UniqueSavePath $destDir (Get-SafeFileName $fileName)\n $att.SaveAsFile($savePath)\n $rows += [PSCustomObject]@{\n path = $savePath\n fileName = $fileName\n subject = $subject.Trim()\n senderName = $senderName\n senderEmail = $senderSmtp\n receivedTime = $received\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n path: str(e.path),\n fileName: str(e.fileName),\n subject: str(e.subject),\n senderName: str(e.senderName),\n senderEmail: str(e.senderEmail),\n receivedTime: str(e.receivedTime),\n };\n });\n}\n"]}
|
package/dist/windows/bounces.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.readSentRecipientGroups = readSentRecipientGroups;
|
|
|
7
7
|
// addresses that failed.
|
|
8
8
|
const run_1 = require("./run");
|
|
9
9
|
const scripts_1 = require("./scripts");
|
|
10
|
-
const json_1 = require("
|
|
10
|
+
const json_1 = require("./json");
|
|
11
11
|
const bounceRules_1 = require("../shared/bounceRules");
|
|
12
12
|
const mail_1 = require("../mail");
|
|
13
13
|
/**
|
|
@@ -59,7 +59,7 @@ function Get-BounceReport($item) {
|
|
|
59
59
|
if ($failedRecipients.Count -ge $maxFailed) { break }
|
|
60
60
|
}
|
|
61
61
|
$received = ''
|
|
62
|
-
try { $received = $item.ReceivedTime
|
|
62
|
+
try { $received = ${(0, scripts_1.stampPs)('$item.ReceivedTime')} } catch {}
|
|
63
63
|
return [PSCustomObject]@{
|
|
64
64
|
entryId = [string]$item.EntryID
|
|
65
65
|
subject = $subject.Trim()
|
|
@@ -217,7 +217,7 @@ for ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {
|
|
|
217
217
|
$rows += [PSCustomObject]@{
|
|
218
218
|
entryId = [string]$m.EntryID
|
|
219
219
|
subject = ([string]$m.Subject).Trim()
|
|
220
|
-
sentOn = if ($sentOn -ne $null) { $
|
|
220
|
+
sentOn = if ($sentOn -ne $null) { ${(0, scripts_1.stampPs)('$sentOn')} } else { '' }
|
|
221
221
|
recipients = @($addresses)
|
|
222
222
|
}
|
|
223
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounces.js","sourceRoot":"","sources":["../../src/windows/bounces.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounce-backs: finding them, clearing them away, and mining them for the\n// addresses that failed.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS, itemsSinceScript, NAMED_STORE_PS } from './scripts';\nimport { itemFailurePs, itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport {\n BODY_ADDRESS_PATTERN,\n BOUNCE_DAEMON_ADDRESSES,\n BOUNCE_DAEMON_NAMES,\n BOUNCE_REASON,\n BOUNCE_SUBJECT_PHRASES,\n DAEMON_SELF_ADDRESSES,\n MAX_FAILED_RECIPIENTS,\n NDR_MESSAGE_CLASS_PREFIX,\n} from '../shared/bounceRules';\nimport { FolderId } from '../mail';\nimport type { CleanUndeliverableRequest, CollectBouncesRequest, SentGroupsRequest } from '../backend';\nimport type { CleanUndeliverableResult, SentRecipientGroup } from '../types';\n\n/**\n * The classifier, generated from the shared rules so Windows and macOS can't\n * disagree about what a bounce is. Defines `Get-BounceReport`, which returns\n * null for an item that isn't a bounce and a report row for one that is.\n *\n * The body is read only after a cheaper signal (class, sender, subject) has\n * matched — that ordering is what keeps a large-inbox scan affordable.\n */\nfunction bounceClassifierScript(): string {\n return `\n$bouncePhrases = ${psArray(BOUNCE_SUBJECT_PHRASES)}\n$daemonAddresses = ${psArray(BOUNCE_DAEMON_ADDRESSES)}\n$daemonNames = ${psArray(BOUNCE_DAEMON_NAMES)}\n$daemonSelf = ${psArray(DAEMON_SELF_ADDRESSES)}\n$ndrPrefix = ${psString(NDR_MESSAGE_CLASS_PREFIX)}\n$addressPattern = ${psString(BODY_ADDRESS_PATTERN)}\n$maxFailed = ${psInt(MAX_FAILED_RECIPIENTS)}\n$accountLower = $target.ToLower()\nfunction Get-BounceReport($item) {\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43 -and $class -ne 46) { return $null }\n $subject = ''\n try { $subject = [string]$item.Subject } catch {}\n $senderName = ''\n try { $senderName = [string]$item.SenderName } catch {}\n $senderEmail = ''\n try { $senderEmail = [string]$item.SenderEmailAddress } catch {}\n $messageClass = ''\n try { $messageClass = [string]$item.MessageClass } catch {}\n $reason = ''\n if ($messageClass.StartsWith($ndrPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reason = ${psString(BOUNCE_REASON.ndr)} }\n if (-not $reason) { foreach ($d in $daemonAddresses) { if ($senderEmail.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($d in $daemonNames) { if ($senderName.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($p in $bouncePhrases) { if ($subject.ToLower().Contains($p)) { $reason = \"${BOUNCE_REASON.subjectPhrase('$p')}\"; break } } }\n if (-not $reason) { return $null }\n $failedRecipients = @()\n $body = ''\n try { $body = [string]$item.Body } catch {}\n foreach ($m in [regex]::Matches($body, $addressPattern)) {\n $address = $m.Value.ToLower().TrimEnd('.')\n if ($address -eq $accountLower) { continue }\n $isDaemon = $false\n foreach ($d in $daemonSelf) { if ($address.Contains($d)) { $isDaemon = $true; break } }\n if ($isDaemon -or $failedRecipients -contains $address) { continue }\n $failedRecipients += $address\n if ($failedRecipients.Count -ge $maxFailed) { break }\n }\n $received = ''\n try { $received = $item.ReceivedTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n return [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject.Trim()\n senderName = $senderName\n senderEmail = $senderEmail\n receivedTime = $received\n matchedReason = $reason\n failedRecipients = @($failedRecipients)\n }\n}\n`;\n}\n\n/**\n * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to\n * Deleted Items (recoverable).\n *\n * Matched items are collected before the first Delete(): deleting shifts the\n * collection, and deleting mid-enumeration would skip items.\n */\nexport async function cleanUndeliverableEmails(request: CleanUndeliverableRequest): Promise<CleanUndeliverableResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${bounceClassifierScript()}\n$dryRun = ${psBool(request.dryRun)}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('inbox', 'ReceivedTime')}\n$reports = @()\n$matchedItems = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n $reports += $report\n $matchedItems += $item\n}\n$deleted = 0\n$failed = @()\nif (-not $dryRun) {\n foreach ($m in $matchedItems) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$m.EntryID } catch {}\n try { $subject = [string]$m.Subject } catch {}\n try { $m.Delete(); $deleted++ }\n catch { ${itemFailurePs('$entryId')} }\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n deletedCount = $deleted\n matched = @($reports)\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n const matched = toArray(e.matched).map(row => {\n const m = record(row);\n return {\n entryId: str(m.entryId),\n subject: str(m.subject),\n senderName: str(m.senderName),\n senderEmail: str(m.senderEmail),\n receivedTime: str(m.receivedTime),\n matchedReason: str(m.matchedReason),\n failedRecipients: strList(m.failedRecipients),\n };\n });\n return {\n account: request.account,\n scannedDays: request.daysBack,\n dryRun: request.dryRun,\n matchedCount: matched.length,\n deletedCount: num(e.deletedCount),\n matched,\n failed: itemFailures(e.failed),\n };\n}\n\n/**\n * The deduplicated addresses bounce-backs report as failed, from the Inbox and,\n * optionally, Deleted Items — so it still works after the bounces were cleaned\n * away. Never deletes anything.\n */\nexport async function collectBouncedRecipients(request: CollectBouncesRequest): Promise<string[]> {\n const roles = request.includeDeletedItems ? [FolderId.Inbox, FolderId.DeletedItems] : [FolderId.Inbox];\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${bounceClassifierScript()}\n${cutoffScript(request.daysBack)}\n$found = [ordered]@{}\nforeach ($role in @(${roles.map(psInt).join(', ')})) {\n $scanFolder = $null\n try { $scanFolder = $store.GetDefaultFolder($role) } catch {}\n if ($scanFolder -eq $null) { continue }\n ${itemsSinceScript('scanFolder', 'ReceivedTime').trim()}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n foreach ($address in $report.failedRecipients) { $found[$address] = $true }\n }\n}\nConvertTo-Json -Compress -InputObject @($found.Keys)\n`, 'scan');\n return strList(output);\n}\n\n/**\n * Sent Items within the window, each message with the full SMTP address set it\n * went to (To + CC + BCC), newest first.\n */\nexport async function readSentRecipientGroups(request: SentGroupsRequest): Promise<SentRecipientGroup[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$sentFolder = $store.GetDefaultFolder(${psInt(FolderId.SentMail)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('sentFolder', 'SentOn')}\n$filtered.Sort('[SentOn]', $true)\n$limit = ${psInt(request.limit)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $m = $null\n try { $m = $filtered.Item($i) } catch { continue }\n if ($m -eq $null) { continue }\n $sentOn = $null\n try { $sentOn = $m.SentOn } catch {}\n if ($sentOn -ne $null -and $sentOn -lt $cutoff) { break }\n $class = 0\n try { $class = [int]$m.Class } catch {}\n if ($class -ne 43) { continue }\n $addresses = @()\n try {\n foreach ($r in $m.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An internal recipient resolves to an Exchange DN; recover its SMTP address.\n if ($address -notlike '*@*') {\n try { $address = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}\n }\n if ($address -like '*@*') { $addresses += $address.ToLower() }\n }\n } catch {}\n if ($addresses.Count -eq 0) { continue }\n $rows += [PSCustomObject]@{\n entryId = [string]$m.EntryID\n subject = ([string]$m.Subject).Trim()\n sentOn = if ($sentOn -ne $null) { $sentOn.ToString('yyyy-MM-dd HH:mm') } else { '' }\n recipients = @($addresses)\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'scan');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n subject: str(e.subject),\n sentOn: str(e.sentOn),\n recipients: strList(e.recipients),\n };\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bounces.js","sourceRoot":"","sources":["../../src/windows/bounces.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounce-backs: finding them, clearing them away, and mining them for the\n// addresses that failed.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS, itemsSinceScript, NAMED_STORE_PS, stampPs } from './scripts';\nimport { itemFailurePs, itemFailures, num, record, str, strList, toArray } from './json';\nimport {\n BODY_ADDRESS_PATTERN,\n BOUNCE_DAEMON_ADDRESSES,\n BOUNCE_DAEMON_NAMES,\n BOUNCE_REASON,\n BOUNCE_SUBJECT_PHRASES,\n DAEMON_SELF_ADDRESSES,\n MAX_FAILED_RECIPIENTS,\n NDR_MESSAGE_CLASS_PREFIX,\n} from '../shared/bounceRules';\nimport { FolderId } from '../mail';\nimport type { CleanUndeliverableRequest, CollectBouncesRequest, SentGroupsRequest } from '../backend';\nimport type { CleanUndeliverableResult, SentRecipientGroup } from '../types';\n\n/**\n * The classifier, generated from the shared rules so Windows and macOS can't\n * disagree about what a bounce is. Defines `Get-BounceReport`, which returns\n * null for an item that isn't a bounce and a report row for one that is.\n *\n * The body is read only after a cheaper signal (class, sender, subject) has\n * matched — that ordering is what keeps a large-inbox scan affordable.\n */\nfunction bounceClassifierScript(): string {\n return `\n$bouncePhrases = ${psArray(BOUNCE_SUBJECT_PHRASES)}\n$daemonAddresses = ${psArray(BOUNCE_DAEMON_ADDRESSES)}\n$daemonNames = ${psArray(BOUNCE_DAEMON_NAMES)}\n$daemonSelf = ${psArray(DAEMON_SELF_ADDRESSES)}\n$ndrPrefix = ${psString(NDR_MESSAGE_CLASS_PREFIX)}\n$addressPattern = ${psString(BODY_ADDRESS_PATTERN)}\n$maxFailed = ${psInt(MAX_FAILED_RECIPIENTS)}\n$accountLower = $target.ToLower()\nfunction Get-BounceReport($item) {\n $class = 0\n try { $class = [int]$item.Class } catch {}\n if ($class -ne 43 -and $class -ne 46) { return $null }\n $subject = ''\n try { $subject = [string]$item.Subject } catch {}\n $senderName = ''\n try { $senderName = [string]$item.SenderName } catch {}\n $senderEmail = ''\n try { $senderEmail = [string]$item.SenderEmailAddress } catch {}\n $messageClass = ''\n try { $messageClass = [string]$item.MessageClass } catch {}\n $reason = ''\n if ($messageClass.StartsWith($ndrPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reason = ${psString(BOUNCE_REASON.ndr)} }\n if (-not $reason) { foreach ($d in $daemonAddresses) { if ($senderEmail.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($d in $daemonNames) { if ($senderName.ToLower().Contains($d)) { $reason = ${psString(BOUNCE_REASON.daemon)}; break } } }\n if (-not $reason) { foreach ($p in $bouncePhrases) { if ($subject.ToLower().Contains($p)) { $reason = \"${BOUNCE_REASON.subjectPhrase('$p')}\"; break } } }\n if (-not $reason) { return $null }\n $failedRecipients = @()\n $body = ''\n try { $body = [string]$item.Body } catch {}\n foreach ($m in [regex]::Matches($body, $addressPattern)) {\n $address = $m.Value.ToLower().TrimEnd('.')\n if ($address -eq $accountLower) { continue }\n $isDaemon = $false\n foreach ($d in $daemonSelf) { if ($address.Contains($d)) { $isDaemon = $true; break } }\n if ($isDaemon -or $failedRecipients -contains $address) { continue }\n $failedRecipients += $address\n if ($failedRecipients.Count -ge $maxFailed) { break }\n }\n $received = ''\n try { $received = ${stampPs('$item.ReceivedTime')} } catch {}\n return [PSCustomObject]@{\n entryId = [string]$item.EntryID\n subject = $subject.Trim()\n senderName = $senderName\n senderEmail = $senderEmail\n receivedTime = $received\n matchedReason = $reason\n failedRecipients = @($failedRecipients)\n }\n}\n`;\n}\n\n/**\n * Scan an account's Inbox for bounce-backs and, unless dry-running, move each to\n * Deleted Items (recoverable).\n *\n * Matched items are collected before the first Delete(): deleting shifts the\n * collection, and deleting mid-enumeration would skip items.\n */\nexport async function cleanUndeliverableEmails(request: CleanUndeliverableRequest): Promise<CleanUndeliverableResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${NAMED_STORE_PS}\n${bounceClassifierScript()}\n$dryRun = ${psBool(request.dryRun)}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('inbox', 'ReceivedTime')}\n$reports = @()\n$matchedItems = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n $reports += $report\n $matchedItems += $item\n}\n$deleted = 0\n$failed = @()\nif (-not $dryRun) {\n foreach ($m in $matchedItems) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$m.EntryID } catch {}\n try { $subject = [string]$m.Subject } catch {}\n try { $m.Delete(); $deleted++ }\n catch { ${itemFailurePs('$entryId')} }\n }\n}\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n deletedCount = $deleted\n matched = @($reports)\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n const matched = toArray(e.matched).map(row => {\n const m = record(row);\n return {\n entryId: str(m.entryId),\n subject: str(m.subject),\n senderName: str(m.senderName),\n senderEmail: str(m.senderEmail),\n receivedTime: str(m.receivedTime),\n matchedReason: str(m.matchedReason),\n failedRecipients: strList(m.failedRecipients),\n };\n });\n return {\n account: request.account,\n scannedDays: request.daysBack,\n dryRun: request.dryRun,\n matchedCount: matched.length,\n deletedCount: num(e.deletedCount),\n matched,\n failed: itemFailures(e.failed),\n };\n}\n\n/**\n * The deduplicated addresses bounce-backs report as failed, from the Inbox and,\n * optionally, Deleted Items — so it still works after the bounces were cleaned\n * away. Never deletes anything.\n */\nexport async function collectBouncedRecipients(request: CollectBouncesRequest): Promise<string[]> {\n const roles = request.includeDeletedItems ? [FolderId.Inbox, FolderId.DeletedItems] : [FolderId.Inbox];\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${bounceClassifierScript()}\n${cutoffScript(request.daysBack)}\n$found = [ordered]@{}\nforeach ($role in @(${roles.map(psInt).join(', ')})) {\n $scanFolder = $null\n try { $scanFolder = $store.GetDefaultFolder($role) } catch {}\n if ($scanFolder -eq $null) { continue }\n ${itemsSinceScript('scanFolder', 'ReceivedTime').trim()}\n $count = $filtered.Count\n for ($i = 1; $i -le $count; $i++) {\n $item = $null\n try { $item = $filtered.Item($i) } catch { continue }\n if ($item -eq $null) { continue }\n $received = $null\n try { $received = $item.ReceivedTime } catch {}\n if ($received -ne $null -and $received -lt $cutoff) { continue }\n $report = Get-BounceReport $item\n if ($report -eq $null) { continue }\n foreach ($address in $report.failedRecipients) { $found[$address] = $true }\n }\n}\nConvertTo-Json -Compress -InputObject @($found.Keys)\n`, 'scan');\n return strList(output);\n}\n\n/**\n * Sent Items within the window, each message with the full SMTP address set it\n * went to (To + CC + BCC), newest first.\n */\nexport async function readSentRecipientGroups(request: SentGroupsRequest): Promise<SentRecipientGroup[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$sentFolder = $store.GetDefaultFolder(${psInt(FolderId.SentMail)})\n${cutoffScript(request.daysBack)}\n${itemsSinceScript('sentFolder', 'SentOn')}\n$filtered.Sort('[SentOn]', $true)\n$limit = ${psInt(request.limit)}\n$rows = @()\n$count = $filtered.Count\nfor ($i = 1; $i -le $count -and $rows.Count -lt $limit; $i++) {\n $m = $null\n try { $m = $filtered.Item($i) } catch { continue }\n if ($m -eq $null) { continue }\n $sentOn = $null\n try { $sentOn = $m.SentOn } catch {}\n if ($sentOn -ne $null -and $sentOn -lt $cutoff) { break }\n $class = 0\n try { $class = [int]$m.Class } catch {}\n if ($class -ne 43) { continue }\n $addresses = @()\n try {\n foreach ($r in $m.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An internal recipient resolves to an Exchange DN; recover its SMTP address.\n if ($address -notlike '*@*') {\n try { $address = [string]$r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch {}\n }\n if ($address -like '*@*') { $addresses += $address.ToLower() }\n }\n } catch {}\n if ($addresses.Count -eq 0) { continue }\n $rows += [PSCustomObject]@{\n entryId = [string]$m.EntryID\n subject = ([string]$m.Subject).Trim()\n sentOn = if ($sentOn -ne $null) { ${stampPs('$sentOn')} } else { '' }\n recipients = @($addresses)\n }\n}\nConvertTo-Json -Depth 4 -InputObject @($rows)\n`, 'scan');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n subject: str(e.subject),\n sentOn: str(e.sentOn),\n recipients: strList(e.recipients),\n };\n });\n}\n"]}
|
package/dist/windows/cleanup.js
CHANGED
|
@@ -5,10 +5,8 @@ exports.purgeDeletedItems = purgeDeletedItems;
|
|
|
5
5
|
// Deleting mail, and emptying what was already deleted.
|
|
6
6
|
const run_1 = require("./run");
|
|
7
7
|
const scripts_1 = require("./scripts");
|
|
8
|
-
const json_1 = require("
|
|
8
|
+
const json_1 = require("./json");
|
|
9
9
|
const mail_1 = require("../mail");
|
|
10
|
-
/** Folders whose contents are received or sent mail rather than working state. */
|
|
11
|
-
const PROTECTED_FOLDER_IDS = [mail_1.FolderId.Inbox, mail_1.FolderId.SentMail];
|
|
12
10
|
/**
|
|
13
11
|
* Delete mail by entry id from anywhere in the account. Delete() moves each item
|
|
14
12
|
* to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.
|
|
@@ -33,7 +31,7 @@ $allowProtected = ${(0, run_1.psBool)(request.allowProtected)}
|
|
|
33
31
|
$dryRun = ${(0, run_1.psBool)(request.dryRun)}
|
|
34
32
|
$protectedReason = ${(0, run_1.psString)(mail_1.PROTECTED_MAIL_REASON)}
|
|
35
33
|
$protectedIds = @{}
|
|
36
|
-
foreach ($role in @(${
|
|
34
|
+
foreach ($role in @(${mail_1.PROTECTED_ROOTS.map(run_1.psInt).join(', ')})) {
|
|
37
35
|
try { $protectedIds[[string]$store.GetDefaultFolder($role).EntryID] = $true } catch {}
|
|
38
36
|
}
|
|
39
37
|
function Test-UnderProtectedFolder($folder) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/windows/cleanup.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/windows/cleanup.ts"],"names":[],"mappings":";;AAyBA,kDAuEC;AAUD,8CA0CC;AApJD,wDAAwD;AACxD,+BAA4E;AAC5E,uCAA2E;AAC3E,iCAAmD;AACnD,kCAA2G;AAI3G;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,mBAAmB,CAAC,OAA4B;IAClE,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;oBACC,IAAA,YAAM,EAAC,OAAO,CAAC,cAAc,CAAC;YACtC,IAAA,YAAM,EAAC,OAAO,CAAC,MAAM,CAAC;qBACb,IAAA,cAAQ,EAAC,4BAAqB,CAAC;;sBAE9B,sBAAe,CAAC,GAAG,CAAC,WAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;kBAyBzC,IAAA,aAAO,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B1C,EAAE,MAAM,CAAC,CAAC;IACP,MAAM,KAAK,GAAwB,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QACzD,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;YACvB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,MAAM,EAAE,IAAA,mBAAY,EAAC,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;SACxB,CAAC;IACN,CAAC,CAAC,CAAC;IACH,OAAO,IAAA,uBAAgB,EAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAqB;IACzD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;oCACiB,IAAA,WAAK,EAAC,eAAQ,CAAC,YAAY,CAAC;mBAC7C,IAAA,WAAK,EAAC,OAAO,CAAC,aAAa,CAAC;YACnC,IAAA,YAAM,EAAC,OAAO,CAAC,MAAM,CAAC;EAChC,IAAA,sBAAY,EAAC,OAAO,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BpC,EAAE,OAAO,CAAC,CAAC;IACR,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,OAAO,CAAC;QACvB,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;QACjB,MAAM,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,MAAM,CAAC;KACxB,CAAC;AACN,CAAC","sourcesContent":["// Deleting mail, and emptying what was already deleted.\nimport { psArray, psBool, psInt, psString, runPowerShellJson } from './run';\nimport { accountScript, cutoffScript, DELIVERY_STORE_PS } from './scripts';\nimport { num, record, str, toArray } from './json';\nimport { deleteMailResult, deleteStatus, FolderId, PROTECTED_MAIL_REASON, PROTECTED_ROOTS } from '../mail';\nimport type { DeleteEmailsRequest, PurgeRequest } from '../backend';\nimport type { DeleteMailOutcome, DeleteMailResult, PurgeDeletedItemsResult } from '../types';\n\n/**\n * Delete mail by entry id from anywhere in the account. Delete() moves each item\n * to Deleted Items, so this is recoverable — purgeDeletedItems is what destroys.\n *\n * This is the one operation that can reach received mail, and an entry id is an\n * unsafe key for it: a stale or wrong id can resolve to a DIFFERENT message.\n * Four things hold the line:\n * - An id that resolves outside this account's store is refused. The bare\n * lookup falls back to the default store, and the Inbox/Sent check below\n * only knows this account's folders, so another mailbox's received mail\n * would otherwise pass it.\n * - Inbox and Sent Items are refused, INCLUDING their subfolders (a filed\n * folder is still received mail), unless `allowProtected`.\n * - `dryRun` resolves and reports without deleting, so the exact subjects and\n * folders can be checked before anything happens.\n * - Every outcome echoes the subject and folder of the item actually resolved.\n */\nexport async function deleteOutlookEmails(request: DeleteEmailsRequest): Promise<DeleteMailResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$allowProtected = ${psBool(request.allowProtected)}\n$dryRun = ${psBool(request.dryRun)}\n$protectedReason = ${psString(PROTECTED_MAIL_REASON)}\n$protectedIds = @{}\nforeach ($role in @(${PROTECTED_ROOTS.map(psInt).join(', ')})) {\n try { $protectedIds[[string]$store.GetDefaultFolder($role).EntryID] = $true } catch {}\n}\nfunction Test-UnderProtectedFolder($folder) {\n $current = $folder\n for ($guard = 0; $current -ne $null -and $guard -lt 25; $guard++) {\n $folderId = $null\n try { $folderId = [string]$current.EntryID } catch {}\n if (-not $folderId) { return $false }\n if ($protectedIds.ContainsKey($folderId)) { return $true }\n $parent = $null\n try { $parent = $current.Parent } catch {}\n $current = $parent\n }\n return $false\n}\nfunction Test-InAccountStore($folder) {\n $folderStoreId = $null\n try { $folderStoreId = [string]$folder.StoreID } catch {}\n if (-not $folderStoreId) { return $false }\n if ($folderStoreId -ieq $storeId) { return $true }\n # One store can be named by two id strings; MAPI decides whether they match.\n try { return [bool]$ns.CompareEntryIDs($folderStoreId, $storeId) } catch { return $false }\n}\n$items = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n $folderPath = ''\n try {\n $it = $null\n try { $it = $ns.GetItemFromID($id, $store.StoreID) } catch { $it = $null }\n if ($it -eq $null) { try { $it = $ns.GetItemFromID($id) } catch { $it = $null } }\n if ($it -eq $null) { throw \"no item with that entry id in this account\" }\n try { $subject = [string]$it.Subject } catch {}\n $parent = $null\n try { $parent = $it.Parent } catch {}\n if ($parent -eq $null) { throw \"item has no parent folder - refusing to delete\" }\n try { $folderPath = [string]$parent.FolderPath } catch {}\n if (-not (Test-InAccountStore $parent)) { throw \"item is in another mailbox, not this account - refusing to delete\" }\n if ((Test-UnderProtectedFolder $parent) -and -not $allowProtected) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'refused'; reason = $protectedReason }\n } elseif ($dryRun) {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'would-delete'; reason = '' }\n } else {\n $it.Delete()\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'deleted'; reason = '' }\n }\n } catch {\n $items += [PSCustomObject]@{ entryId = $id; subject = $subject; folderPath = $folderPath; status = 'failed'; reason = $_.Exception.Message }\n }\n}\nConvertTo-Json -Depth 3 -InputObject @($items)\n`, 'scan');\n const items: DeleteMailOutcome[] = toArray(output).map(row => {\n const e = record(row);\n return {\n entryId: str(e.entryId),\n subject: str(e.subject),\n folderPath: str(e.folderPath),\n status: deleteStatus(str(e.status)),\n reason: str(e.reason),\n };\n });\n return deleteMailResult(request.dryRun, items);\n}\n\n/**\n * Permanently remove items from the account's Deleted Items. The ONE irreversible\n * operation here, which is why it is folder-scoped rather than keyed on ids: it\n * can only ever destroy what was already thrown away.\n *\n * Walks backwards, since deleting shifts the collection and a forward walk would\n * skip every other item.\n */\nexport async function purgeDeletedItems(request: PurgeRequest): Promise<PurgeDeletedItemsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$folder = $store.GetDefaultFolder(${psInt(FolderId.DeletedItems)})\n$olderThanDays = ${psInt(request.olderThanDays)}\n$dryRun = ${psBool(request.dryRun)}\n${cutoffScript(request.olderThanDays)}\n$matched = 0\n$purged = 0\n$kept = 0\n$failed = 0\n# Bound once: each $folder.Items is a COM call that builds a fresh collection.\n$folderItems = $folder.Items\nfor ($i = $folderItems.Count; $i -ge 1; $i--) {\n $it = $null\n try { $it = $folderItems.Item($i) } catch { continue }\n $stamp = $null\n foreach ($property in @('ReceivedTime', 'LastModificationTime', 'CreationTime')) {\n try { $stamp = $it.$property; if ($stamp -ne $null) { break } } catch {}\n }\n if ($olderThanDays -gt 0 -and $stamp -ne $null -and $stamp -gt $cutoff) { $kept++; continue }\n $matched++\n if ($dryRun) { continue }\n try { $it.Delete(); $purged++ } catch { $failed++ }\n}\nConvertTo-Json -Compress -InputObject ([PSCustomObject]@{\n folderPath = [string]$folder.FolderPath\n matched = $matched\n purged = $purged\n kept = $kept\n failed = $failed\n})\n`, 'purge');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n dryRun: request.dryRun,\n matched: num(e.matched),\n purged: num(e.purged),\n kept: num(e.kept),\n failed: num(e.failed),\n };\n}\n"]}
|
package/dist/windows/drafts.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.deleteOutlookDrafts = deleteOutlookDrafts;
|
|
|
7
7
|
// Drafts: which ones belong to an account, and listing, sending or deleting them.
|
|
8
8
|
const run_1 = require("./run");
|
|
9
9
|
const scripts_1 = require("./scripts");
|
|
10
|
-
const json_1 = require("
|
|
10
|
+
const json_1 = require("./json");
|
|
11
11
|
const mail_1 = require("../mail");
|
|
12
12
|
/**
|
|
13
13
|
* Resolve the Drafts folders belonging to one account into `$scan`, and define
|
|
@@ -96,7 +96,7 @@ foreach ($entry in $scan) {
|
|
|
96
96
|
if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }
|
|
97
97
|
}
|
|
98
98
|
$modified = ''
|
|
99
|
-
try { $modified = $it.LastModificationTime
|
|
99
|
+
try { $modified = ${(0, scripts_1.stampPs)('$it.LastModificationTime')} } catch {}
|
|
100
100
|
$hasAttachments = $false
|
|
101
101
|
try { $hasAttachments = ($it.Attachments.Count -gt 0) } catch {}
|
|
102
102
|
$rows += [PSCustomObject]@{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/windows/drafts.ts"],"names":[],"mappings":"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","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS } from './scripts';\nimport { bool, itemFailurePs, itemFailures, num, record, str, strList, toArray } from '../shared/json';\nimport { FolderId } from '../mail';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ItemFailure, ListDraftsResult, SendDraftsResult } from '../types';\n\n/**\n * Resolve the Drafts folders belonging to one account into `$scan`, and define\n * `Test-DraftMatches` over them. Emitted into every drafts script, so listing,\n * sending and deleting can never disagree about which drafts are this account's.\n *\n * Two Drafts folders can hold one account's drafts, so both are scanned:\n * - the account's OWN store's Drafts, where a draft composed in that mailbox\n * lands. A draft there with no explicit sending account still belongs to it.\n * - the DEFAULT store's Drafts, where a program's `Save()` files a draft\n * whatever account it sends from. Only drafts stamped with this account\n * match there; an unstamped one belongs to the default account.\n * When the two are the same folder it is scanned once, under the first rule.\n *\n * Non-mail items (meeting requests, reports) never match.\n */\nconst DRAFTS_SCAN_PS = `\n$scan = @()\n$seenFolders = @{}\n$homeDrafts = $null\ntry { $homeDrafts = $store.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($homeDrafts -ne $null) {\n $seenFolders[\"$($homeDrafts.StoreID)|$($homeDrafts.EntryID)\"] = $true\n $scan += [PSCustomObject]@{ folder = $homeDrafts; includeUnstamped = $true }\n}\n$defaultDrafts = $null\ntry { $defaultDrafts = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($defaultDrafts -ne $null -and -not $seenFolders.ContainsKey(\"$($defaultDrafts.StoreID)|$($defaultDrafts.EntryID)\")) {\n $scan += [PSCustomObject]@{ folder = $defaultDrafts; includeUnstamped = $false }\n}\nfunction Test-DraftMatches($item, [bool]$includeUnstamped) {\n if ($item.Class -ne 43) { return $false } # olMail only\n $sender = $null\n try { $sender = $item.SendUsingAccount } catch {}\n if ($sender -eq $null) { return $includeUnstamped }\n return ([string]$sender.SmtpAddress -ieq $target)\n}\n# An id is acted on only once it is PROVED to be one of this account's drafts:\n# resolved in one of the scanned folders' stores, sitting in that folder, and\n# bound to this account (or unbound, in its own store).\n$draftFolderRules = @{}\nforeach ($entry in $scan) { $draftFolderRules[[string]$entry.folder.EntryID] = $entry.includeUnstamped }\nfunction Resolve-OwnDraft([string]$id, [string]$verb) {\n $draft = $null\n foreach ($entry in $scan) {\n try { $draft = $ns.GetItemFromID($id, $entry.folder.StoreID) } catch { $draft = $null }\n if ($draft -ne $null) { break }\n }\n if ($draft -eq $null) { throw \"no such item in this account's Drafts folders\" }\n $parentId = ''\n try { $parentId = [string]$draft.Parent.EntryID } catch {}\n if (-not $draftFolderRules.ContainsKey($parentId)) { throw \"item is not in this account's Drafts folder - refusing to $verb\" }\n if (-not (Test-DraftMatches $draft $draftFolderRules[$parentId])) { throw \"draft is not bound to $target - refusing to $verb\" }\n return $draft\n}\n`;\n\nfunction draftsScript(account: string): string {\n return accountScript(account) + DELIVERY_STORE_PS + DRAFTS_SCAN_PS;\n}\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$previewChars = ${psInt(request.previewChars)}\n$limit = ${psInt(request.limit)}\n$rows = @()\n$folderPaths = @()\nforeach ($entry in $scan) {\n $folderPath = ''\n try { $folderPath = [string]$entry.folder.FolderPath } catch {}\n $folderPaths += $folderPath\n foreach ($it in $entry.folder.Items) {\n if (-not (Test-DraftMatches $it $entry.includeUnstamped)) { continue }\n $addresses = @()\n try {\n foreach ($r in $it.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An unresolved Exchange entry has a DN for an address; its name reads better.\n if ($address.StartsWith('/')) { try { $address = [string]$r.Name } catch {} }\n if ($address) { $addresses += $address }\n }\n } catch {}\n $preview = ''\n if ($previewChars -gt 0) {\n try { $preview = ([string]$it.Body -replace '\\\\s+', ' ').Trim() } catch {}\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = $it.LastModificationTime.ToString('yyyy-MM-dd HH:mm') } catch {}\n $hasAttachments = $false\n try { $hasAttachments = ($it.Attachments.Count -gt 0) } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$it.EntryID\n subject = [string]$it.Subject\n to = [string]$it.To\n toEmails = @($addresses)\n bodyPreview = $preview\n hasAttachments = $hasAttachments\n lastModified = $modified\n folderPath = $folderPath\n }\n }\n}\n$sorted = @($rows | Sort-Object -Property lastModified -Descending)\n$total = $sorted.Count\nif ($total -gt $limit) { $sorted = @($sorted[0..($limit - 1)]) }\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n account = $target\n foldersScanned = @($folderPaths)\n count = $total\n truncated = ($total -gt $limit)\n drafts = @($sorted)\n})\n`, 'scan');\n const e = record(output);\n return {\n account: str(e.account) || request.account,\n foldersScanned: toArray(e.foldersScanned).map(str),\n count: num(e.count),\n truncated: bool(e.truncated),\n drafts: toArray(e.drafts).map(row => {\n const d = record(row);\n return {\n entryId: str(d.entryId),\n subject: str(d.subject),\n to: str(d.to),\n toEmails: strList(d.toEmails),\n bodyPreview: str(d.bodyPreview),\n hasAttachments: bool(d.hasAttachments),\n lastModified: str(d.lastModified),\n folderPath: str(d.folderPath),\n };\n }),\n };\n}\n\n/** Run `verb` over the named drafts of the account, each proved to be its own first. */\nasync function actOnDrafts(\n request: EntryIdsRequest,\n verb: 'send' | 'delete',\n): Promise<{ done: number; failed: ItemFailure[] }> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$done = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $draft = Resolve-OwnDraft $id '${verb}'\n try { $subject = [string]$draft.Subject } catch {}\n $draft.${verb === 'send' ? 'Send' : 'Delete'}()\n $done++\n } catch {\n ${itemFailurePs('$id')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ done = $done; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { done: num(e.done), failed: itemFailures(e.failed) };\n}\n\n/** Send the named drafts. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'send');\n return { sent: done, failed };\n}\n\n/**\n * Send every draft belonging to the account.\n *\n * The matching items are snapshotted before the first send: sending moves an\n * item out of Drafts, and sending during a live enumeration would skip every\n * other one.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$drafts = @()\nforeach ($entry in $scan) {\n foreach ($it in $entry.folder.Items) {\n if (Test-DraftMatches $it $entry.includeUnstamped) { $drafts += $it }\n }\n}\n$sent = 0\n$failed = @()\nforeach ($draft in $drafts) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$draft.EntryID } catch {}\n try { $subject = [string]$draft.Subject } catch {}\n try {\n $draft.Send()\n $sent++\n } catch {\n ${itemFailurePs('$entryId')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { sent: num(e.sent), failed: itemFailures(e.failed) };\n}\n\n/**\n * Delete the named drafts. Outlook's Delete() moves an item to Deleted Items,\n * so a mistake stays recoverable. Without the proof that each is this\n * account's draft, this would be a general \"delete any email by id\" call,\n * which is not what it is for.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'delete');\n return { deleted: done, failed };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/windows/drafts.ts"],"names":[],"mappings":"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","sourcesContent":["// Drafts: which ones belong to an account, and listing, sending or deleting them.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS, stampPs } from './scripts';\nimport { bool, itemFailurePs, itemFailures, num, record, str, strList, toArray } from './json';\nimport { FolderId } from '../mail';\nimport type { AccountRequest, EntryIdsRequest, ListDraftsRequest } from '../backend';\nimport type { DeleteDraftsResult, ItemFailure, ListDraftsResult, SendDraftsResult } from '../types';\n\n/**\n * Resolve the Drafts folders belonging to one account into `$scan`, and define\n * `Test-DraftMatches` over them. Emitted into every drafts script, so listing,\n * sending and deleting can never disagree about which drafts are this account's.\n *\n * Two Drafts folders can hold one account's drafts, so both are scanned:\n * - the account's OWN store's Drafts, where a draft composed in that mailbox\n * lands. A draft there with no explicit sending account still belongs to it.\n * - the DEFAULT store's Drafts, where a program's `Save()` files a draft\n * whatever account it sends from. Only drafts stamped with this account\n * match there; an unstamped one belongs to the default account.\n * When the two are the same folder it is scanned once, under the first rule.\n *\n * Non-mail items (meeting requests, reports) never match.\n */\nconst DRAFTS_SCAN_PS = `\n$scan = @()\n$seenFolders = @{}\n$homeDrafts = $null\ntry { $homeDrafts = $store.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($homeDrafts -ne $null) {\n $seenFolders[\"$($homeDrafts.StoreID)|$($homeDrafts.EntryID)\"] = $true\n $scan += [PSCustomObject]@{ folder = $homeDrafts; includeUnstamped = $true }\n}\n$defaultDrafts = $null\ntry { $defaultDrafts = $ns.GetDefaultFolder(${psInt(FolderId.Drafts)}) } catch {}\nif ($defaultDrafts -ne $null -and -not $seenFolders.ContainsKey(\"$($defaultDrafts.StoreID)|$($defaultDrafts.EntryID)\")) {\n $scan += [PSCustomObject]@{ folder = $defaultDrafts; includeUnstamped = $false }\n}\nfunction Test-DraftMatches($item, [bool]$includeUnstamped) {\n if ($item.Class -ne 43) { return $false } # olMail only\n $sender = $null\n try { $sender = $item.SendUsingAccount } catch {}\n if ($sender -eq $null) { return $includeUnstamped }\n return ([string]$sender.SmtpAddress -ieq $target)\n}\n# An id is acted on only once it is PROVED to be one of this account's drafts:\n# resolved in one of the scanned folders' stores, sitting in that folder, and\n# bound to this account (or unbound, in its own store).\n$draftFolderRules = @{}\nforeach ($entry in $scan) { $draftFolderRules[[string]$entry.folder.EntryID] = $entry.includeUnstamped }\nfunction Resolve-OwnDraft([string]$id, [string]$verb) {\n $draft = $null\n foreach ($entry in $scan) {\n try { $draft = $ns.GetItemFromID($id, $entry.folder.StoreID) } catch { $draft = $null }\n if ($draft -ne $null) { break }\n }\n if ($draft -eq $null) { throw \"no such item in this account's Drafts folders\" }\n $parentId = ''\n try { $parentId = [string]$draft.Parent.EntryID } catch {}\n if (-not $draftFolderRules.ContainsKey($parentId)) { throw \"item is not in this account's Drafts folder - refusing to $verb\" }\n if (-not (Test-DraftMatches $draft $draftFolderRules[$parentId])) { throw \"draft is not bound to $target - refusing to $verb\" }\n return $draft\n}\n`;\n\nfunction draftsScript(account: string): string {\n return accountScript(account) + DELIVERY_STORE_PS + DRAFTS_SCAN_PS;\n}\n\n/** The account's drafts, newest first. Bodies are previewed, never returned whole. */\nexport async function listOutlookDrafts(request: ListDraftsRequest): Promise<ListDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$previewChars = ${psInt(request.previewChars)}\n$limit = ${psInt(request.limit)}\n$rows = @()\n$folderPaths = @()\nforeach ($entry in $scan) {\n $folderPath = ''\n try { $folderPath = [string]$entry.folder.FolderPath } catch {}\n $folderPaths += $folderPath\n foreach ($it in $entry.folder.Items) {\n if (-not (Test-DraftMatches $it $entry.includeUnstamped)) { continue }\n $addresses = @()\n try {\n foreach ($r in $it.Recipients) {\n $address = ''\n try { $address = [string]$r.Address } catch {}\n # An unresolved Exchange entry has a DN for an address; its name reads better.\n if ($address.StartsWith('/')) { try { $address = [string]$r.Name } catch {} }\n if ($address) { $addresses += $address }\n }\n } catch {}\n $preview = ''\n if ($previewChars -gt 0) {\n try { $preview = ([string]$it.Body -replace '\\\\s+', ' ').Trim() } catch {}\n if ($preview.Length -gt $previewChars) { $preview = $preview.Substring(0, $previewChars) }\n }\n $modified = ''\n try { $modified = ${stampPs('$it.LastModificationTime')} } catch {}\n $hasAttachments = $false\n try { $hasAttachments = ($it.Attachments.Count -gt 0) } catch {}\n $rows += [PSCustomObject]@{\n entryId = [string]$it.EntryID\n subject = [string]$it.Subject\n to = [string]$it.To\n toEmails = @($addresses)\n bodyPreview = $preview\n hasAttachments = $hasAttachments\n lastModified = $modified\n folderPath = $folderPath\n }\n }\n}\n$sorted = @($rows | Sort-Object -Property lastModified -Descending)\n$total = $sorted.Count\nif ($total -gt $limit) { $sorted = @($sorted[0..($limit - 1)]) }\nConvertTo-Json -Depth 4 -InputObject ([PSCustomObject]@{\n account = $target\n foldersScanned = @($folderPaths)\n count = $total\n truncated = ($total -gt $limit)\n drafts = @($sorted)\n})\n`, 'scan');\n const e = record(output);\n return {\n account: str(e.account) || request.account,\n foldersScanned: toArray(e.foldersScanned).map(str),\n count: num(e.count),\n truncated: bool(e.truncated),\n drafts: toArray(e.drafts).map(row => {\n const d = record(row);\n return {\n entryId: str(d.entryId),\n subject: str(d.subject),\n to: str(d.to),\n toEmails: strList(d.toEmails),\n bodyPreview: str(d.bodyPreview),\n hasAttachments: bool(d.hasAttachments),\n lastModified: str(d.lastModified),\n folderPath: str(d.folderPath),\n };\n }),\n };\n}\n\n/** Run `verb` over the named drafts of the account, each proved to be its own first. */\nasync function actOnDrafts(\n request: EntryIdsRequest,\n verb: 'send' | 'delete',\n): Promise<{ done: number; failed: ItemFailure[] }> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$done = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $draft = Resolve-OwnDraft $id '${verb}'\n try { $subject = [string]$draft.Subject } catch {}\n $draft.${verb === 'send' ? 'Send' : 'Delete'}()\n $done++\n } catch {\n ${itemFailurePs('$id')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ done = $done; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { done: num(e.done), failed: itemFailures(e.failed) };\n}\n\n/** Send the named drafts. */\nexport async function sendDrafts(request: EntryIdsRequest): Promise<SendDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'send');\n return { sent: done, failed };\n}\n\n/**\n * Send every draft belonging to the account.\n *\n * The matching items are snapshotted before the first send: sending moves an\n * item out of Drafts, and sending during a live enumeration would skip every\n * other one.\n */\nexport async function sendAllDrafts(request: AccountRequest): Promise<SendDraftsResult> {\n const output = await runPowerShellJson(`${draftsScript(request.account)}\n$drafts = @()\nforeach ($entry in $scan) {\n foreach ($it in $entry.folder.Items) {\n if (Test-DraftMatches $it $entry.includeUnstamped) { $drafts += $it }\n }\n}\n$sent = 0\n$failed = @()\nforeach ($draft in $drafts) {\n $entryId = ''\n $subject = ''\n try { $entryId = [string]$draft.EntryID } catch {}\n try { $subject = [string]$draft.Subject } catch {}\n try {\n $draft.Send()\n $sent++\n } catch {\n ${itemFailurePs('$entryId')}\n }\n}\nConvertTo-Json -Compress -Depth 3 -InputObject ([PSCustomObject]@{ sent = $sent; failed = @($failed) })\n`, 'scan');\n const e = record(output);\n return { sent: num(e.sent), failed: itemFailures(e.failed) };\n}\n\n/**\n * Delete the named drafts. Outlook's Delete() moves an item to Deleted Items,\n * so a mistake stays recoverable. Without the proof that each is this\n * account's draft, this would be a general \"delete any email by id\" call,\n * which is not what it is for.\n */\nexport async function deleteOutlookDrafts(request: EntryIdsRequest): Promise<DeleteDraftsResult> {\n const { done, failed } = await actOnDrafts(request, 'delete');\n return { deleted: done, failed };\n}\n"]}
|
package/dist/windows/folders.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.moveOutlookEmails = moveOutlookEmails;
|
|
|
5
5
|
// Listing the folders under an Inbox, and filing mail into one.
|
|
6
6
|
const run_1 = require("./run");
|
|
7
7
|
const scripts_1 = require("./scripts");
|
|
8
|
-
const json_1 = require("
|
|
8
|
+
const json_1 = require("./json");
|
|
9
9
|
const mail_1 = require("../mail");
|
|
10
10
|
/** The folders under an account's Inbox, depth-first. */
|
|
11
11
|
async function listInboxFolders(request) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/windows/folders.ts"],"names":[],"mappings":";;AASA,4CA+BC;AAQD,8CA+BC;AA/ED,gEAAgE;AAChE,+BAA0D;AAC1D,uCAA8E;AAC9E,
|
|
1
|
+
{"version":3,"file":"folders.js","sourceRoot":"","sources":["../../src/windows/folders.ts"],"names":[],"mappings":";;AASA,4CA+BC;AAQD,8CA+BC;AA/ED,gEAAgE;AAChE,+BAA0D;AAC1D,uCAA8E;AAC9E,iCAAsF;AACtF,kCAAmC;AAInC,yDAAyD;AAClD,KAAK,UAAU,gBAAgB,CAAC,OAA2B;IAC9D,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;mCACgB,IAAA,WAAK,EAAC,eAAQ,CAAC,KAAK,CAAC;cAC1C,IAAA,WAAK,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;CAiBpC,EAAE,UAAU,CAAC,CAAC;IACX,OAAO,IAAA,cAAO,EAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC;QACtB,OAAO;YACH,IAAI,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,IAAI,CAAC;YACjB,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;YAC7B,SAAS,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,SAAS,CAAC;YAC3B,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;SAC3B,CAAC;IACN,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB,CAAC,OAAoB;IACxD,MAAM,MAAM,GAAG,MAAM,IAAA,uBAAiB,EAAC,GAAG,IAAA,uBAAa,EAAC,OAAO,CAAC,OAAO,CAAC;EAC1E,2BAAiB;EACjB,IAAA,yBAAe,EAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,eAAe,CAAC;;;kBAG7D,IAAA,aAAO,EAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;UAQjC,IAAA,oBAAa,EAAC,KAAK,CAAC;;;;;;;;;CAS7B,EAAE,MAAM,CAAC,CAAC;IACP,MAAM,CAAC,GAAG,IAAA,aAAM,EAAC,MAAM,CAAC,CAAC;IACzB,OAAO;QACH,UAAU,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,UAAU,CAAC;QAC7B,aAAa,EAAE,IAAA,WAAI,EAAC,CAAC,CAAC,aAAa,CAAC;QACpC,KAAK,EAAE,IAAA,UAAG,EAAC,CAAC,CAAC,KAAK,CAAC;QACnB,MAAM,EAAE,IAAA,mBAAY,EAAC,CAAC,CAAC,MAAM,CAAC;KACjC,CAAC;AACN,CAAC","sourcesContent":["// Listing the folders under an Inbox, and filing mail into one.\nimport { psArray, psInt, runPowerShellJson } from './run';\nimport { accountScript, DELIVERY_STORE_PS, mailScopeScript } from './scripts';\nimport { bool, itemFailurePs, itemFailures, num, record, str, toArray } from './json';\nimport { FolderId } from '../mail';\nimport type { ListFoldersRequest, MoveRequest } from '../backend';\nimport type { InboxFolderInfo, MoveEmailsResult } from '../types';\n\n/** The folders under an account's Inbox, depth-first. */\nexport async function listInboxFolders(request: ListFoldersRequest): Promise<InboxFolderInfo[]> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n$inbox = $store.GetDefaultFolder(${psInt(FolderId.Inbox)})\n$maxDepth = ${psInt(request.maxDepth)}\n$rows = New-Object System.Collections.ArrayList\nfunction Add-Folders($folder, [int]$level) {\n foreach ($f in $folder.Folders) {\n $itemCount = 0\n try { $itemCount = [int]$f.Items.Count } catch {}\n [void]$rows.Add([PSCustomObject]@{\n name = [string]$f.Name\n folderPath = [string]$f.FolderPath\n itemCount = $itemCount\n depth = $level\n })\n if ($level -lt $maxDepth) { Add-Folders $f ($level + 1) }\n }\n}\nAdd-Folders $inbox 1\nConvertTo-Json -Depth 3 -InputObject @($rows)\n`, 'standard');\n return toArray(output).map(row => {\n const e = record(row);\n return {\n name: str(e.name),\n folderPath: str(e.folderPath),\n itemCount: num(e.itemCount),\n depth: num(e.depth) || 1,\n };\n });\n}\n\n/**\n * File emails into a folder of the account: one under the Inbox, or a\n * well-known one by name. Moving to Deleted Items is how mail is deleted\n * recoverably. Moving rewrites each item's entry id, so the ids passed in are\n * dead afterwards.\n */\nexport async function moveOutlookEmails(request: MoveRequest): Promise<MoveEmailsResult> {\n const output = await runPowerShellJson(`${accountScript(request.account)}\n${DELIVERY_STORE_PS}\n${mailScopeScript(request.folder, request.folderLabel, request.createIfMissing)}\n$moved = 0\n$failed = @()\nforeach ($id in ${psArray(request.entryIds)}) {\n $subject = ''\n try {\n $it = $ns.GetItemFromID($id, $store.StoreID)\n try { $subject = [string]$it.Subject } catch {}\n [void]$it.Move($scope)\n $moved++\n } catch {\n ${itemFailurePs('$id')}\n }\n}\nConvertTo-Json -Depth 3 -InputObject ([PSCustomObject]@{\n folderPath = [string]$scope.FolderPath\n folderCreated = $scopeCreated\n moved = $moved\n failed = @($failed)\n})\n`, 'scan');\n const e = record(output);\n return {\n folderPath: str(e.folderPath),\n folderCreated: bool(e.folderCreated),\n moved: num(e.moved),\n failed: itemFailures(e.failed),\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/windows/json.ts"],"names":[],"mappings":";;AAUA,0BAEC;AAGD,wBAIC;AAED,kBAEC;AAED,kBAEC;AAED,oBAEC;AAGD,0BAEC;AAGD,gCAEC;AASD,sCAEC;AAGD,oCAKC;AAnDD,iGAAiG;AACjG,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,iEAAiE;AACjE,SAAgB,MAAM,CAAC,KAAc;IACjC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACvE,CAAC,CAAC,KAAgC;QAClC,CAAC,CAAC,EAAE,CAAC;AACb,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,GAAG,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,SAAgB,IAAI,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,IAAI,CAAC;AAC1B,CAAC;AAED,mCAAmC;AACnC,SAAgB,OAAO,CAAC,KAAc;IAClC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,0FAA0F;AAC1F,SAAgB,UAAU,CAAC,KAAc;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,UAA8B;IACxD,OAAO,2CAA2C,UAAU,sDAAsD,CAAC;AACvH,CAAC;AAED,mEAAmE;AACnE,SAAgB,YAAY,CAAC,KAAc;IACvC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;IACrF,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["// Coercing the JSON a PowerShell script prints into the package's types — the\n// Windows counterpart of the record framing in mac/run.ts.\n//\n// ConvertTo-Json has two habits a caller must never see: it unwraps a\n// single-element array into a bare object, and it renders an empty one as\n// nothing at all. Every list crossing that boundary goes through `toArray`, and\n// every scalar through a coercion that survives a missing property.\nimport type { ItemFailure } from '../types';\n\n/** One value as a list: an array stays, null and undefined become empty, anything else wraps. */\nexport function toArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : value == null ? [] : [value];\n}\n\n/** A value as a plain object, for reading fields off one row. */\nexport function record(value: unknown): Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nexport function str(value: unknown): string {\n return value == null ? '' : String(value);\n}\n\nexport function num(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\nexport function bool(value: unknown): boolean {\n return value === true;\n}\n\n/** A list of non-empty strings. */\nexport function strList(value: unknown): string[] {\n return toArray(value).map(str).filter(Boolean);\n}\n\n/** A base64 field decoded as UTF-8 — how scripts pass bodies that must survive intact. */\nexport function base64Text(value: unknown): string {\n return Buffer.from(str(value), 'base64').toString('utf8');\n}\n\n/**\n * The PowerShell that appends one item's failure to `$failed`, inside the\n * `catch` of a per-item loop: the row `itemFailures` reads back, written once\n * so no script can name its fields differently. `idVariable` holds the item's\n * id. `$subject` must be reset before each item's `try`, or a failure reports\n * the previous item's subject.\n */\nexport function itemFailurePs(idVariable: '$id' | '$entryId'): string {\n return `$failed += [PSCustomObject]@{ entryId = ${idVariable}; subject = $subject; error = $_.Exception.Message }`;\n}\n\n/** A list of per-item failures, as `itemFailurePs` writes them. */\nexport function itemFailures(value: unknown): ItemFailure[] {\n return toArray(value).map(row => {\n const e = record(row);\n return { entryId: str(e.entryId), subject: str(e.subject), error: str(e.error) };\n });\n}\n"]}
|