@bobfrankston/rmfmail 1.2.49 → 1.2.65
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/client/android-bootstrap.bundle.js +63 -2
- package/client/android-bootstrap.bundle.js.map +4 -4
- package/client/app.bundle.js +417 -63
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +182 -24
- package/client/app.js.map +1 -1
- package/client/app.ts +166 -21
- package/client/components/calendar-sidebar.js +5 -5
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +5 -5
- package/client/components/folder-tree.js +81 -0
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +70 -0
- package/client/components/message-list.js +282 -38
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +264 -36
- package/client/compose/compose.bundle.js +70 -14
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.css +28 -7
- package/client/compose/compose.js +64 -4
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +66 -4
- package/client/compose/editor.js +23 -7
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +23 -7
- package/client/help/search-help.js +4 -3
- package/client/help/search-help.js.map +1 -1
- package/client/help/search-help.ts +4 -3
- package/client/index.html +5 -1
- package/client/lib/api-client.js +4 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -2
- package/client/lib/mailxapi.js +2 -2
- package/client/lib/rmf-tiny.js +39 -6
- package/client/styles/components.css +6 -0
- package/docs/azure.md +58 -0
- package/docs/config-help.md +131 -0
- package/docs/edit-in-word-docx.md +98 -0
- package/docs/host-abstraction-plan.md +169 -0
- package/docs/local-first-plan.md +303 -0
- package/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/docs/outlook.md +80 -18
- package/docs/outlook.txt +35 -0
- package/docs/rules-design.md +172 -0
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts +48 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +332 -70
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +323 -67
- package/packages/mailx-imap/microsoft-credentials.json +8 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-imap/providers/outlook-api.d.ts +7 -0
- package/packages/mailx-imap/providers/outlook-api.d.ts.map +1 -0
- package/packages/mailx-imap/providers/outlook-api.js +7 -0
- package/packages/mailx-imap/providers/outlook-api.js.map +1 -0
- package/packages/mailx-service/index.d.ts +7 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +8 -8
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +8 -7
- package/packages/mailx-service/jsonrpc.js +1 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +1 -1
- package/packages/mailx-settings/docs/azure.md +58 -0
- package/packages/mailx-settings/docs/config-help.md +131 -0
- package/packages/mailx-settings/docs/edit-in-word-docx.md +98 -0
- package/packages/mailx-settings/docs/host-abstraction-plan.md +169 -0
- package/packages/mailx-settings/docs/local-first-plan.md +303 -0
- package/packages/mailx-settings/docs/npmglobalize-transitive-workspace-deps.md +107 -0
- package/packages/mailx-settings/docs/outlook.md +80 -18
- package/packages/mailx-settings/docs/rules-design.md +172 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +9 -8
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +9 -8
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +50 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +120 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +126 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-types/contact-rules.d.ts +1 -1
- package/packages/mailx-types/contact-rules.js +1 -1
- package/packages/mailx-types/contact-rules.js.map +1 -1
- package/packages/mailx-types/contact-rules.jsonc +1 -1
- package/packages/mailx-types/contact-rules.ts +1 -1
- package/packages/mailx-types/package.json +1 -1
|
@@ -14,6 +14,7 @@ import { EventEmitter } from "node:events";
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { GmailApiProvider } from "./providers/gmail-api.js";
|
|
17
|
+
import { OutlookApiProvider } from "./providers/outlook-api.js";
|
|
17
18
|
import type { MailProvider, ProviderMessage } from "./providers/types.js";
|
|
18
19
|
import { SmtpClient } from "@bobfrankston/smtp-direct";
|
|
19
20
|
import * as os from "node:os";
|
|
@@ -243,6 +244,16 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, client: any, labe
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
/** Outlook.com / Microsoft 365 detection from an account config (host or email
|
|
248
|
+
* domain). Shared by the addAccount tokenProvider branch and isOutlookAccount
|
|
249
|
+
* so the credential/scope choice and the provider-routing choice agree. */
|
|
250
|
+
function isOutlookCfg(account: AccountConfig): boolean {
|
|
251
|
+
const host = account.imap?.host?.toLowerCase() || "";
|
|
252
|
+
const email = account.email?.toLowerCase() || "";
|
|
253
|
+
if (/outlook\.|office365|hotmail|\.live\.com/.test(host)) return true;
|
|
254
|
+
return /@(outlook|hotmail|live|msn)\.com$/.test(email);
|
|
255
|
+
}
|
|
256
|
+
|
|
246
257
|
export class ImapManager extends EventEmitter {
|
|
247
258
|
private configs: Map<string, ReturnType<typeof createAutoImapConfig>> = new Map();
|
|
248
259
|
private watchers: Map<string, () => void> = new Map();
|
|
@@ -503,6 +514,99 @@ export class ImapManager extends EventEmitter {
|
|
|
503
514
|
} finally { try { await client.logout(); } catch { /* */ } }
|
|
504
515
|
}
|
|
505
516
|
|
|
517
|
+
/** Special-use roles a folder may NOT be renamed/reparented out of. Renaming
|
|
518
|
+
* INBOX/Sent/Drafts/Trash/Junk/Archive would break the special-folder
|
|
519
|
+
* mapping (compose's draft auto-save, the trash target, sent-copy append),
|
|
520
|
+
* and most servers reject it outright — so we refuse before touching the
|
|
521
|
+
* server. */
|
|
522
|
+
private static readonly PROTECTED_ROLES = new Set(["inbox", "sent", "drafts", "trash", "junk", "archive"]);
|
|
523
|
+
|
|
524
|
+
/** Rename and/or reparent a folder. Both operations are a single server-side
|
|
525
|
+
* mailbox rename (IMAP RENAME / Gmail label PATCH / Graph move+patch) —
|
|
526
|
+
* messages never move, no copy-and-delete. Mirrors moveMessage's plumbing:
|
|
527
|
+
* validate + branch API-vs-IMAP + commit locally.
|
|
528
|
+
*
|
|
529
|
+
* - `newName` is the new LEAF name.
|
|
530
|
+
* - `newParentId` (optional) reparents under that folder; omit for an
|
|
531
|
+
* in-place rename. Reparenting and renaming share one path computation:
|
|
532
|
+
* newPath = (newParent ? newParent.path + delim : oldParentPrefix) + newName.
|
|
533
|
+
*
|
|
534
|
+
* Guard rails (all throw a UI-surfaced error): protected special-use
|
|
535
|
+
* folders, empty/whitespace names, names containing the hierarchy
|
|
536
|
+
* delimiter, a destination that already exists, and reparenting a folder
|
|
537
|
+
* under itself or its own descendant (which would orphan the subtree). */
|
|
538
|
+
async renameFolder(accountId: string, folderId: number, newName: string, newParentId?: number): Promise<void> {
|
|
539
|
+
const folders = this.db.getFolders(accountId);
|
|
540
|
+
const folder = folders.find(f => f.id === folderId);
|
|
541
|
+
if (!folder) throw new Error("Folder not found");
|
|
542
|
+
|
|
543
|
+
// Use the folder's OWN delimiter — never borrow folders[0]'s (a
|
|
544
|
+
// different-delimiter folder would build a wrong newPath).
|
|
545
|
+
const delimiter = folder.delimiter || "/";
|
|
546
|
+
|
|
547
|
+
// ── Guard rails ──────────────────────────────────────────────────────
|
|
548
|
+
if (folder.specialUse && ImapManager.PROTECTED_ROLES.has(folder.specialUse)) {
|
|
549
|
+
throw new Error(`"${folder.name}" is a special folder (${folder.specialUse}) and can't be renamed.`);
|
|
550
|
+
}
|
|
551
|
+
const leaf = (newName || "").trim();
|
|
552
|
+
if (!leaf) throw new Error("Folder name can't be empty.");
|
|
553
|
+
if (leaf.includes(delimiter)) {
|
|
554
|
+
throw new Error(`Folder name can't contain the "${delimiter}" hierarchy separator.`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Resolve the destination parent (reparent) or keep the existing parent
|
|
558
|
+
// prefix (in-place rename).
|
|
559
|
+
let newPath: string;
|
|
560
|
+
let parent: typeof folder | undefined;
|
|
561
|
+
if (newParentId != null) {
|
|
562
|
+
parent = folders.find(f => f.id === newParentId);
|
|
563
|
+
if (!parent) throw new Error("Target parent folder not found");
|
|
564
|
+
if (parent.id === folder.id) throw new Error("Can't move a folder into itself.");
|
|
565
|
+
// Reparenting under a descendant would orphan the moved subtree.
|
|
566
|
+
if (parent.path === folder.path || parent.path.startsWith(folder.path + delimiter)) {
|
|
567
|
+
throw new Error("Can't move a folder into one of its own subfolders.");
|
|
568
|
+
}
|
|
569
|
+
newPath = parent.path + delimiter + leaf;
|
|
570
|
+
} else {
|
|
571
|
+
const idx = folder.path.lastIndexOf(delimiter);
|
|
572
|
+
const parentPrefix = idx >= 0 ? folder.path.slice(0, idx + delimiter.length) : "";
|
|
573
|
+
newPath = parentPrefix + leaf;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (newPath === folder.path) return; // no-op (same name + same parent)
|
|
577
|
+
if (folders.some(f => f.path === newPath)) {
|
|
578
|
+
throw new Error(`A folder named "${leaf}" already exists here.`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── Server op ────────────────────────────────────────────────────────
|
|
582
|
+
// API accounts (Gmail labels / Outlook Graph folders) have native
|
|
583
|
+
// rename; IMAP accounts use RFC 3501 RENAME, which renames the whole
|
|
584
|
+
// subtree server-side in one command.
|
|
585
|
+
if (this.isApiAccount(accountId)) {
|
|
586
|
+
const api = this.getApiProvider(accountId) as MailProvider;
|
|
587
|
+
if (!api.renameFolder) {
|
|
588
|
+
throw new Error("This account's provider doesn't support folder rename.");
|
|
589
|
+
}
|
|
590
|
+
// Gmail wants the parent label PATH; Outlook wants the parent Graph
|
|
591
|
+
// id — both are the parent folder's `path` in our model.
|
|
592
|
+
await api.renameFolder(folder.path, leaf, parent?.path);
|
|
593
|
+
} else {
|
|
594
|
+
await this.renameFolderViaServer(accountId, folder.path, newPath);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Local commit ─────────────────────────────────────────────────────
|
|
598
|
+
// All providers now use a HIERARCHICAL display path:
|
|
599
|
+
// - IMAP: server mailbox path (renameFolderViaServer also resyncs).
|
|
600
|
+
// - Gmail: label name (which is the slash-path).
|
|
601
|
+
// - Outlook: display-name path — the provider keeps a path↔Graph-id
|
|
602
|
+
// map internally, so renaming the path is safe (Graph ops resolve
|
|
603
|
+
// the id from the new path on the next list-sync).
|
|
604
|
+
// So the predicted newPath matches the server result; rewrite the row +
|
|
605
|
+
// descendant prefixes for an immediate tree update.
|
|
606
|
+
this.db.renameFolderPath(accountId, folder.path, newPath, leaf, delimiter);
|
|
607
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
608
|
+
}
|
|
609
|
+
|
|
506
610
|
/** Delete `path` on the server (tolerating already-gone). Local folder-row
|
|
507
611
|
* removal stays with the caller (it knows the folderId). */
|
|
508
612
|
async deleteFolderViaServer(accountId: string, path: string): Promise<void> {
|
|
@@ -907,21 +1011,50 @@ export class ImapManager extends EventEmitter {
|
|
|
907
1011
|
// createAutoImapConfig auto-detects Gmail from server/username and sets up OAuth
|
|
908
1012
|
// For OAuth accounts, provide a tokenProvider using oauthsupport
|
|
909
1013
|
let tokenProvider: (() => Promise<string>) | undefined;
|
|
910
|
-
if (account.imap.auth === "oauth2" || (!account.imap.password && account.imap.host?.includes("gmail"))) {
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1014
|
+
if (account.imap.auth === "oauth2" || (!account.imap.password && account.imap.host?.includes("gmail")) || (!account.imap.password && isOutlookCfg(account))) {
|
|
1015
|
+
// Pick the OAuth provider by account kind. Outlook accounts use
|
|
1016
|
+
// Microsoft Graph credentials + scopes; everything else is Google.
|
|
1017
|
+
// Both flow through the same provider-agnostic oauthsupport code —
|
|
1018
|
+
// only the creds file and scope string differ (see docs/outlook.md).
|
|
1019
|
+
const outlook = isOutlookCfg(account);
|
|
1020
|
+
let credPath: string;
|
|
1021
|
+
if (outlook) {
|
|
1022
|
+
// Microsoft app registration creds. Check the per-machine
|
|
1023
|
+
// override (~/.rmfmail) first, then fall back to the file
|
|
1024
|
+
// BUNDLED IN THIS PACKAGE — same model as Google's creds
|
|
1025
|
+
// shipping inside iflow-direct. Because the client is PUBLIC
|
|
1026
|
+
// (PKCE, no secret), the bundled file holds only the public
|
|
1027
|
+
// client_id and is safe to commit/ship. So a fresh install
|
|
1028
|
+
// needs NO manual creds placement (see docs/outlook.md §A/B).
|
|
1029
|
+
credPath = path.join(getConfigDir(), "microsoft-credentials.json");
|
|
1030
|
+
if (!fs.existsSync(credPath)) {
|
|
1031
|
+
const bundled = path.join(path.dirname(fileURLToPath(import.meta.url)), "microsoft-credentials.json");
|
|
1032
|
+
if (fs.existsSync(bundled)) credPath = bundled;
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
// Find Google OAuth credentials — check ~/.rmfmail first, then iflow-direct package
|
|
1036
|
+
credPath = path.join(getConfigDir(), "google-credentials.json");
|
|
1037
|
+
if (!fs.existsSync(credPath)) {
|
|
1038
|
+
try {
|
|
1039
|
+
// Use fileURLToPath, NOT string-replace on "file://" — on Linux,
|
|
1040
|
+
// file:///usr/local/... loses its leading slash via .replace("file:///",
|
|
1041
|
+
// "") and becomes relative, so fs.existsSync silently fails.
|
|
1042
|
+
const pkgDir = path.dirname(fileURLToPath(import.meta.resolve("@bobfrankston/iflow-direct")));
|
|
1043
|
+
for (const name of ["iflow-credentials.json"]) {
|
|
1044
|
+
const p = path.join(pkgDir, name);
|
|
1045
|
+
if (fs.existsSync(p)) { credPath = p; break; }
|
|
1046
|
+
}
|
|
1047
|
+
} catch { /* iflow-direct not resolvable */ }
|
|
1048
|
+
}
|
|
924
1049
|
}
|
|
1050
|
+
// Scope set by provider. Google: full two-way sync of all
|
|
1051
|
+
// mailx-managed stores. Microsoft Graph: mail read/write + send +
|
|
1052
|
+
// offline refresh. (Graph and Outlook-IMAP scopes are DIFFERENT
|
|
1053
|
+
// audiences — we mint Graph because Outlook syncs/sends via Graph,
|
|
1054
|
+
// not IMAP/SMTP. See docs/outlook.md "Decision".)
|
|
1055
|
+
const oauthScope = outlook
|
|
1056
|
+
? "https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.Send offline_access openid profile"
|
|
1057
|
+
: "https://mail.google.com/ https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/drive";
|
|
925
1058
|
const tokenDir = path.join(getConfigDir(), "tokens", tokenDirName(account.imap.user));
|
|
926
1059
|
tokenProvider = async () => {
|
|
927
1060
|
// Wall-clock timeout on OAuth. Use a longer timeout when no
|
|
@@ -930,11 +1063,9 @@ export class ImapManager extends EventEmitter {
|
|
|
930
1063
|
const hasToken = fs.existsSync(path.join(tokenDir, "oauth-token.json"));
|
|
931
1064
|
const TOKEN_FETCH_TIMEOUT_MS = hasToken ? 30_000 : 120_000;
|
|
932
1065
|
const authPromise = authenticateOAuth(credPath, {
|
|
933
|
-
// Scope set
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
// (full), drive (for shared accounts.jsonc).
|
|
937
|
-
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks https://www.googleapis.com/auth/drive",
|
|
1066
|
+
// Scope set chosen per provider above (Google full-store
|
|
1067
|
+
// sync, or Microsoft Graph mail).
|
|
1068
|
+
scope: oauthScope,
|
|
938
1069
|
tokenDirectory: tokenDir,
|
|
939
1070
|
credentialsKey: "installed",
|
|
940
1071
|
loginHint: account.imap.user,
|
|
@@ -980,6 +1111,15 @@ export class ImapManager extends EventEmitter {
|
|
|
980
1111
|
greetingTimeout: isGmail ? 10000 : 30000,
|
|
981
1112
|
});
|
|
982
1113
|
|
|
1114
|
+
// createAutoImapConfig only threads the tokenProvider through for GMAIL
|
|
1115
|
+
// hosts — its non-Gmail branch drops it (it assumes non-Gmail == password
|
|
1116
|
+
// IMAP). Outlook is OAuth-but-not-Gmail: it never opens IMAP (it syncs via
|
|
1117
|
+
// Graph), but getOutlookProvider reads config.tokenProvider. So re-attach
|
|
1118
|
+
// it here for any OAuth account whose host createAutoImapConfig didn't
|
|
1119
|
+
// recognize. Without this, an Outlook account silently has no provider →
|
|
1120
|
+
// "No tokenProvider for outlook" on every sync (Bob 2026-06-21).
|
|
1121
|
+
if (tokenProvider && !config.tokenProvider) config.tokenProvider = tokenProvider;
|
|
1122
|
+
|
|
983
1123
|
this.configs.set(account.id, config);
|
|
984
1124
|
|
|
985
1125
|
// Register account in DB
|
|
@@ -1020,6 +1160,39 @@ export class ImapManager extends EventEmitter {
|
|
|
1020
1160
|
return new GmailApiProvider(config.tokenProvider);
|
|
1021
1161
|
}
|
|
1022
1162
|
|
|
1163
|
+
/** Check if an account is Outlook.com / Microsoft 365 (should use Graph API
|
|
1164
|
+
* instead of IMAP). Detection mirrors isGmailAccount: host or email domain.
|
|
1165
|
+
* office365 IMAP front-ends and consumer outlook.com both route here. */
|
|
1166
|
+
private isOutlookAccount(accountId: string): boolean {
|
|
1167
|
+
const settings = loadSettings();
|
|
1168
|
+
const account = settings.accounts.find(a => a.id === accountId);
|
|
1169
|
+
if (!account) return false;
|
|
1170
|
+
return isOutlookCfg(account);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** True for any account that syncs via a REST provider (Gmail or Outlook)
|
|
1174
|
+
* rather than IMAP. The branch points that used to ask isGmailAccount to
|
|
1175
|
+
* mean "API, not IMAP" ask this instead. */
|
|
1176
|
+
private isApiAccount(accountId: string): boolean {
|
|
1177
|
+
return this.isGmailAccount(accountId) || this.isOutlookAccount(accountId);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** Get a Microsoft Graph provider for an account (reuses the tokenProvider —
|
|
1181
|
+
* which mints Graph-scoped tokens for Outlook accounts, see addAccount). */
|
|
1182
|
+
private getOutlookProvider(accountId: string): OutlookApiProvider {
|
|
1183
|
+
const config = this.configs.get(accountId);
|
|
1184
|
+
if (!config?.tokenProvider) throw new Error(`No tokenProvider for ${accountId}`);
|
|
1185
|
+
return new OutlookApiProvider(config.tokenProvider);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Get the right REST provider (Gmail or Outlook) for an API account.
|
|
1189
|
+
* Both implement MailProvider plus the setFlags/trashMessage/moveMessage/
|
|
1190
|
+
* fetchBodiesBatch surface the dispatcher uses, so callers stay generic. */
|
|
1191
|
+
private getApiProvider(accountId: string): MailProvider {
|
|
1192
|
+
if (this.isOutlookAccount(accountId)) return this.getOutlookProvider(accountId);
|
|
1193
|
+
return this.getGmailProvider(accountId);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1023
1196
|
/** Convert ProviderMessage to the shape expected by storeMessages/upsertMessage */
|
|
1024
1197
|
private providerMsgToLocal(msg: ProviderMessage): any {
|
|
1025
1198
|
return {
|
|
@@ -1347,7 +1520,13 @@ export class ImapManager extends EventEmitter {
|
|
|
1347
1520
|
excludeUids: Set<number>,
|
|
1348
1521
|
): Promise<{ recoveredCount: number; serverUids: number[] | null; dateBounded: boolean }> {
|
|
1349
1522
|
const folderPath = folder.path;
|
|
1350
|
-
|
|
1523
|
+
// Diff against UIDs we actually have a MESSAGE ROW for (messages table),
|
|
1524
|
+
// NOT message_folders memberships. An orphan membership (member row but
|
|
1525
|
+
// no message row) would otherwise look "present" and never get its real
|
|
1526
|
+
// data fetched — leaving the message permanently missing from the list
|
|
1527
|
+
// while the folder count looks full (Bob 2026-06-20: ~1k old INBOX
|
|
1528
|
+
// messages stuck missing behind orphan memberships).
|
|
1529
|
+
const existingUids = this.db.getStoredUids(accountId, folderId);
|
|
1351
1530
|
// Small folders (Drafts, Sent, …) get a FULL UID fetch so server-side
|
|
1352
1531
|
// deletions of OLDER messages are reflected; large folders (134k INBOX)
|
|
1353
1532
|
// get a date-bounded query so we don't enumerate the whole mailbox.
|
|
@@ -1641,7 +1820,11 @@ export class ImapManager extends EventEmitter {
|
|
|
1641
1820
|
// retries instead of locking out for RECONCILE_THROTTLE_MS
|
|
1642
1821
|
// (Bob 2026-06-20: boot-time "Not connected" failure locked
|
|
1643
1822
|
// INBOX recovery out for 15 min).
|
|
1644
|
-
|
|
1823
|
+
// Compare STORED-ROW count (messages table) to the server's
|
|
1824
|
+
// count — not getMessageCount (message_folders), which orphan
|
|
1825
|
+
// memberships can inflate to match the server while real rows
|
|
1826
|
+
// are still missing.
|
|
1827
|
+
const localCount = this.db.getStoredCount(accountId, folderId);
|
|
1645
1828
|
const hasDeficit = statusMessageCount !== null && localCount < statusMessageCount;
|
|
1646
1829
|
const lastRecon = this.lastReconcileMs.get(folderId) ?? 0;
|
|
1647
1830
|
const throttleElapsed = Date.now() - lastRecon > RECONCILE_THROTTLE_MS;
|
|
@@ -2075,8 +2258,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2075
2258
|
|
|
2076
2259
|
/** Sync a single account — manages its own connection lifecycle */
|
|
2077
2260
|
private async syncAccount(accountId: string, priorityOrder: string[]): Promise<void> {
|
|
2078
|
-
// Gmail: use REST API instead of IMAP
|
|
2079
|
-
if (this.
|
|
2261
|
+
// Gmail / Outlook: use REST API instead of IMAP
|
|
2262
|
+
if (this.isApiAccount(accountId)) {
|
|
2080
2263
|
return this.syncAccountViaApi(accountId, priorityOrder);
|
|
2081
2264
|
}
|
|
2082
2265
|
|
|
@@ -2288,10 +2471,10 @@ export class ImapManager extends EventEmitter {
|
|
|
2288
2471
|
}
|
|
2289
2472
|
}
|
|
2290
2473
|
|
|
2291
|
-
/** Sync a Gmail account via REST API — no IMAP connections */
|
|
2474
|
+
/** Sync a Gmail or Outlook account via REST API — no IMAP connections */
|
|
2292
2475
|
private async syncAccountViaApi(accountId: string, priorityOrder: string[]): Promise<void> {
|
|
2293
2476
|
try {
|
|
2294
|
-
const api = this.
|
|
2477
|
+
const api = this.getApiProvider(accountId);
|
|
2295
2478
|
const t0 = Date.now();
|
|
2296
2479
|
|
|
2297
2480
|
// Step 1: Sync folder list via API
|
|
@@ -2300,9 +2483,28 @@ export class ImapManager extends EventEmitter {
|
|
|
2300
2483
|
console.log(` [api] ${accountId}: ${apiFolders.length} labels in ${Date.now() - t0}ms`);
|
|
2301
2484
|
|
|
2302
2485
|
// Store folders in DB (same as IMAP path)
|
|
2486
|
+
const serverPaths = new Set<string>();
|
|
2303
2487
|
for (const f of apiFolders) {
|
|
2304
2488
|
const specialUse = f.specialUse || "";
|
|
2305
2489
|
this.db.upsertFolder(accountId, f.path, f.name, specialUse, f.delimiter);
|
|
2490
|
+
serverPaths.add(f.path);
|
|
2491
|
+
}
|
|
2492
|
+
// Prune local folders the provider no longer returns. The IMAP path
|
|
2493
|
+
// (syncFolders) already does this; the API path didn't, so a folder
|
|
2494
|
+
// deleted on the server — OR whose path SCHEME changed (e.g. the
|
|
2495
|
+
// Outlook id→hierarchical-path migration) — lingered forever as a
|
|
2496
|
+
// stale/duplicate row. Same guards as syncFolders: only prune on a
|
|
2497
|
+
// non-empty list (empty == transient error, not "all gone") and
|
|
2498
|
+
// never drop INBOX.
|
|
2499
|
+
if (apiFolders.length > 0) {
|
|
2500
|
+
const stale = this.db.getFolders(accountId).filter(f =>
|
|
2501
|
+
!serverPaths.has(f.path) && f.specialUse !== "inbox");
|
|
2502
|
+
for (const f of stale) {
|
|
2503
|
+
console.log(` [api] ${accountId}: pruning stale folder "${f.path}" (id=${f.id}) — not in provider list`);
|
|
2504
|
+
try { this.db.deleteFolder(f.id); } catch (e: any) {
|
|
2505
|
+
console.error(` [api] ${accountId}: prune failed for "${f.path}": ${e.message}`);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2306
2508
|
}
|
|
2307
2509
|
this.emit("folderCountsChanged", accountId, {});
|
|
2308
2510
|
const dbFolders = this.db.getFolders(accountId);
|
|
@@ -2567,7 +2769,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2567
2769
|
* on a large mailbox). The 5-minute STATUS poll path still runs full
|
|
2568
2770
|
* `syncFolder` so deletions and gaps eventually reconcile. */
|
|
2569
2771
|
async syncInboxNewOnly(accountId: string): Promise<void> {
|
|
2570
|
-
if (this.
|
|
2772
|
+
if (this.isApiAccount(accountId)) return; // IDLE is IMAP-only (Gmail/Outlook poll via API)
|
|
2571
2773
|
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
2572
2774
|
if (!inbox) return;
|
|
2573
2775
|
try {
|
|
@@ -2611,8 +2813,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2611
2813
|
// mismatch — API uses hashed IDs, IMAP uses server-assigned
|
|
2612
2814
|
// UIDs. The IMAP reconcile then deletes every API-synced
|
|
2613
2815
|
// message because their UIDs don't appear in the IMAP list.
|
|
2614
|
-
if (this.
|
|
2615
|
-
const api = this.
|
|
2816
|
+
if (this.isApiAccount(accountId)) {
|
|
2817
|
+
const api = this.getApiProvider(accountId);
|
|
2616
2818
|
try {
|
|
2617
2819
|
await this.syncFolderViaApi(accountId, inbox, api);
|
|
2618
2820
|
} finally {
|
|
@@ -2670,9 +2872,27 @@ export class ImapManager extends EventEmitter {
|
|
|
2670
2872
|
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
2671
2873
|
async quickInboxCheckAccount(accountId: string): Promise<void> {
|
|
2672
2874
|
if (this.isGmailAccount(accountId)) return this.quickGmailCheck(accountId);
|
|
2875
|
+
if (this.isOutlookAccount(accountId)) return this.quickApiCheck(accountId);
|
|
2673
2876
|
return this.quickImapCheck(accountId);
|
|
2674
2877
|
}
|
|
2675
2878
|
|
|
2879
|
+
/** Lightweight inbox refresh for a generic API account (Outlook). Unlike
|
|
2880
|
+
* quickGmailCheck there's no cheap "top message id" probe — a single
|
|
2881
|
+
* incremental syncFolderViaApi is itself cheap (Graph delta-ish page), so
|
|
2882
|
+
* we just run it and recalc counts. */
|
|
2883
|
+
private async quickApiCheck(accountId: string): Promise<void> {
|
|
2884
|
+
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
2885
|
+
if (!inbox) return;
|
|
2886
|
+
const api = this.getApiProvider(accountId);
|
|
2887
|
+
try {
|
|
2888
|
+
await this.syncFolderViaApi(accountId, inbox, api);
|
|
2889
|
+
this.db.recalcFolderCounts(inbox.id);
|
|
2890
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
2891
|
+
} finally {
|
|
2892
|
+
try { await api.close(); } catch { /* ignore */ }
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2676
2896
|
private async quickImapCheck(accountId: string): Promise<void> {
|
|
2677
2897
|
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
2678
2898
|
if (!inbox) return;
|
|
@@ -2747,21 +2967,21 @@ export class ImapManager extends EventEmitter {
|
|
|
2747
2967
|
// Gmail uses API sync, not IMAP STATUS. IMAP accounts use IDLE
|
|
2748
2968
|
// which gives instant push — the STATUS poll is just a fallback
|
|
2749
2969
|
// in case IDLE silently dropped.
|
|
2750
|
-
const
|
|
2970
|
+
const isApi = this.isApiAccount(accountId);
|
|
2751
2971
|
// IMAP accounts: IDLE gives instant push; STATUS poll is just a
|
|
2752
2972
|
// safety net for silent IDLE drops — keep it infrequent.
|
|
2753
|
-
//
|
|
2754
|
-
//
|
|
2755
|
-
//
|
|
2756
|
-
//
|
|
2757
|
-
//
|
|
2758
|
-
//
|
|
2759
|
-
const interval =
|
|
2973
|
+
// API accounts (Gmail/Outlook): no IDLE, so the quick poll IS the
|
|
2974
|
+
// primary path to new-mail latency. Drop to 30s so mail appears in
|
|
2975
|
+
// ~15s average. Both providers have ample request budget and their
|
|
2976
|
+
// own rate-limiters, so a fresh poll is cheap. Dovecot accounts
|
|
2977
|
+
// stay at 5min to respect connection limits (each poll = fresh
|
|
2978
|
+
// connection).
|
|
2979
|
+
const interval = isApi ? 30000 : 300000; // API: 30s; IMAP: 5min
|
|
2760
2980
|
const timer = setInterval(() => {
|
|
2761
2981
|
this.quickInboxCheckAccount(accountId).catch(() => {});
|
|
2762
2982
|
}, interval);
|
|
2763
2983
|
this.syncIntervals.set(`quick:${accountId}`, timer);
|
|
2764
|
-
console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${
|
|
2984
|
+
console.log(` [periodic] ${accountId}: STATUS check every ${interval / 1000}s (${isApi ? "API" : "IMAP+IDLE"})`);
|
|
2765
2985
|
}
|
|
2766
2986
|
|
|
2767
2987
|
// DEADMAN: IDLE silently dropped on 2026-05-27 at 10:07 (empty
|
|
@@ -2775,7 +2995,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2775
2995
|
// the log.
|
|
2776
2996
|
const deadmanInterval = setInterval(() => {
|
|
2777
2997
|
for (const [accountId] of this.configs) {
|
|
2778
|
-
if (this.
|
|
2998
|
+
if (this.isApiAccount(accountId)) continue; // Gmail/Outlook = API, no IDLE
|
|
2779
2999
|
if (this.watchers.has(accountId)) continue;
|
|
2780
3000
|
console.log(` [idle-deadman] ${accountId}: no IDLE watcher — attempting restart`);
|
|
2781
3001
|
this.startWatching().catch(e =>
|
|
@@ -3139,8 +3359,8 @@ export class ImapManager extends EventEmitter {
|
|
|
3139
3359
|
const folder = this.db.getFolders(accountId).find(f => f.id === folderId);
|
|
3140
3360
|
if (!folder) return null;
|
|
3141
3361
|
|
|
3142
|
-
// Gmail: REST API, no IMAP connection involved.
|
|
3143
|
-
if (this.
|
|
3362
|
+
// Gmail / Outlook: REST API, no IMAP connection involved.
|
|
3363
|
+
if (this.isApiAccount(accountId)) {
|
|
3144
3364
|
return this.fetchMessageBodyViaApi(accountId, folderId, uid, folder.path);
|
|
3145
3365
|
}
|
|
3146
3366
|
|
|
@@ -3178,7 +3398,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3178
3398
|
* delete the stale row locally instead of showing a generic error. */
|
|
3179
3399
|
private async fetchMessageBodyViaApi(accountId: string, folderId: number, uid: number, folderPath: string): Promise<Buffer | null> {
|
|
3180
3400
|
try {
|
|
3181
|
-
const api = this.
|
|
3401
|
+
const api = this.getApiProvider(accountId);
|
|
3182
3402
|
// Read provider_id from the local row so fetchOne can skip the
|
|
3183
3403
|
// listMessageIds pagination (the dominant Gmail rate-limit cost).
|
|
3184
3404
|
const env = this.db.getMessageByUid(accountId, uid, folderId);
|
|
@@ -3322,7 +3542,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3322
3542
|
const ERROR_BUDGET = 20;
|
|
3323
3543
|
const RATE_LIMIT_PAUSE_MS = 30000;
|
|
3324
3544
|
const BATCH_SIZE = 100;
|
|
3325
|
-
const
|
|
3545
|
+
const isApi = this.isApiAccount(accountId);
|
|
3326
3546
|
// Gmail still uses per-message fetch (HTTP /batch is a separate TODO in this file's
|
|
3327
3547
|
// governing unit). IMAP uses the batched `fetchBodiesBatch` path via iflow-direct —
|
|
3328
3548
|
// one SELECT + one UID FETCH per folder per tick instead of N round trips.
|
|
@@ -3359,13 +3579,13 @@ export class ImapManager extends EventEmitter {
|
|
|
3359
3579
|
}
|
|
3360
3580
|
let madeProgress = false;
|
|
3361
3581
|
|
|
3362
|
-
if (
|
|
3363
|
-
//
|
|
3364
|
-
//
|
|
3365
|
-
//
|
|
3366
|
-
// Note on the model: Gmail has labels, not folders. A
|
|
3367
|
-
// multiple labels gets fetched twice under current
|
|
3368
|
-
// deeper label-native redesign is
|
|
3582
|
+
if (isApi) {
|
|
3583
|
+
// API batch path (Gmail/Outlook): group by folder/label, list
|
|
3584
|
+
// once per folder, bounded-concurrency fetch. Far fewer HTTP
|
|
3585
|
+
// round trips than the old one-list-per-body path.
|
|
3586
|
+
// Note on the Gmail model: Gmail has labels, not folders. A
|
|
3587
|
+
// message in multiple labels gets fetched twice under current
|
|
3588
|
+
// grouping. A deeper label-native redesign is a separate TODO.
|
|
3369
3589
|
const byFolder = new Map<number, number[]>();
|
|
3370
3590
|
for (const m of missing) {
|
|
3371
3591
|
let arr = byFolder.get(m.folderId);
|
|
@@ -3373,7 +3593,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3373
3593
|
arr.push(m.uid);
|
|
3374
3594
|
}
|
|
3375
3595
|
const folders = this.db.getFolders(accountId);
|
|
3376
|
-
const api = this.
|
|
3596
|
+
const api = this.getApiProvider(accountId) as any;
|
|
3377
3597
|
try {
|
|
3378
3598
|
for (const [folderId, uidsInFolder] of byFolder) {
|
|
3379
3599
|
const folder = folders.find(f => f.id === folderId);
|
|
@@ -3744,8 +3964,8 @@ export class ImapManager extends EventEmitter {
|
|
|
3744
3964
|
// through withConnection → IMAP, which silently no-op'd for Gmail
|
|
3745
3965
|
// accounts (REST-only, no IMAP connection) and left local-only stars
|
|
3746
3966
|
// that vanished on the next full sync.
|
|
3747
|
-
if (this.
|
|
3748
|
-
const api = this.
|
|
3967
|
+
if (this.isApiAccount(accountId)) {
|
|
3968
|
+
const api = this.getApiProvider(accountId) as any;
|
|
3749
3969
|
try {
|
|
3750
3970
|
for (const action of actions) {
|
|
3751
3971
|
const folder = folders.find(f => f.id === action.folderId);
|
|
@@ -3869,16 +4089,33 @@ export class ImapManager extends EventEmitter {
|
|
|
3869
4089
|
break;
|
|
3870
4090
|
}
|
|
3871
4091
|
|
|
3872
|
-
case "flags":
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
4092
|
+
case "flags": {
|
|
4093
|
+
// action.flags is the ABSOLUTE desired flag set: the UI
|
|
4094
|
+
// sends the whole array with the toggled flag added or
|
|
4095
|
+
// simply OMITTED — it does NOT use the `-remove` convention.
|
|
4096
|
+
// The old code only ever called addFlags (additive), so
|
|
4097
|
+
// CLEARING a star never reached the server and the flag came
|
|
4098
|
+
// back on the next sync — "I shut off the stars and they
|
|
4099
|
+
// suddenly come back, still flagged on the server" (Bob
|
|
4100
|
+
// 2026-06-26). The old `length > 0` guard also made clearing
|
|
4101
|
+
// the last remaining flag a complete no-op. Reconcile to the
|
|
4102
|
+
// absolute state instead: add the desired flags, and remove
|
|
4103
|
+
// any flag mailx manages (\Seen / \Flagged) that's no longer
|
|
4104
|
+
// present. Mirrors the Gmail provider's add+remove approach.
|
|
4105
|
+
const desired = action.flags.filter(f => !f.startsWith("-"));
|
|
4106
|
+
const explicitRemove = action.flags.filter(f => f.startsWith("-")).map(f => f.slice(1));
|
|
4107
|
+
const MANAGED = ["\\Seen", "\\Flagged"];
|
|
4108
|
+
const impliedRemove = MANAGED.filter(f => !desired.includes(f));
|
|
4109
|
+
const toRemove = [...new Set([...explicitRemove, ...impliedRemove])];
|
|
4110
|
+
if (desired.length > 0) {
|
|
4111
|
+
await client.addFlags(folder.path, action.uid, desired);
|
|
3880
4112
|
}
|
|
4113
|
+
if (toRemove.length > 0) {
|
|
4114
|
+
await client.removeFlags(folder.path, action.uid, toRemove);
|
|
4115
|
+
}
|
|
4116
|
+
console.log(` [sync] Updated flags UID ${action.uid} (+[${desired.join(",")}] -[${toRemove.join(",")}])`);
|
|
3881
4117
|
break;
|
|
4118
|
+
}
|
|
3882
4119
|
|
|
3883
4120
|
case "append": {
|
|
3884
4121
|
if (action.rawMessage) {
|
|
@@ -4562,11 +4799,14 @@ export class ImapManager extends EventEmitter {
|
|
|
4562
4799
|
}
|
|
4563
4800
|
if (filesToSend.length === 0) return;
|
|
4564
4801
|
|
|
4565
|
-
//
|
|
4802
|
+
// API accounts (Gmail/Outlook): send directly from the local queue (no
|
|
4803
|
+
// IMAP outbox). Gmail goes via SMTP+XOAUTH2 (mail.google.com scope
|
|
4804
|
+
// covers SMTP); Outlook goes via Graph /sendMail (its token is Graph-
|
|
4805
|
+
// scoped, not SMTP) — sendRawForAccount picks the right transport.
|
|
4566
4806
|
const sentDir = path.join(getConfigDir(), "sending", accountId, "sent");
|
|
4567
4807
|
fs.mkdirSync(sentDir, { recursive: true });
|
|
4568
4808
|
|
|
4569
|
-
if (this.
|
|
4809
|
+
if (this.isApiAccount(accountId)) {
|
|
4570
4810
|
const nowMs = Date.now();
|
|
4571
4811
|
for (const { dir, file } of filesToSend) {
|
|
4572
4812
|
const filePath = path.join(dir, file);
|
|
@@ -4607,9 +4847,9 @@ export class ImapManager extends EventEmitter {
|
|
|
4607
4847
|
fs.writeFileSync(claimedPath, raw, "utf-8");
|
|
4608
4848
|
|
|
4609
4849
|
try {
|
|
4610
|
-
await this.
|
|
4850
|
+
await this.sendRawForAccount(accountId, raw);
|
|
4611
4851
|
fs.renameSync(claimedPath, path.join(sentDir, file));
|
|
4612
|
-
console.log(` [outbox] Sent ${file}
|
|
4852
|
+
console.log(` [outbox] Sent ${file} → sent/ (attempt ${attempt})`);
|
|
4613
4853
|
} catch (e: any) {
|
|
4614
4854
|
// Persist a next-attempt timestamp and release the claim so the
|
|
4615
4855
|
// file is visible to the scan loop again.
|
|
@@ -4691,6 +4931,21 @@ export class ImapManager extends EventEmitter {
|
|
|
4691
4931
|
}
|
|
4692
4932
|
}
|
|
4693
4933
|
|
|
4934
|
+
/** Send a raw RFC 2822 message for an account, choosing the transport by
|
|
4935
|
+
* provider. Outlook accounts have a Graph-scoped OAuth token (no SMTP
|
|
4936
|
+
* audience), so they send via Graph POST /sendMail; Gmail and IMAP
|
|
4937
|
+
* accounts send via SMTP (Gmail's mail.google.com token covers XOAUTH2
|
|
4938
|
+
* SMTP). Graph /sendMail auto-files a copy in Sent, so no append needed. */
|
|
4939
|
+
private async sendRawForAccount(accountId: string, raw: string): Promise<void> {
|
|
4940
|
+
if (this.isOutlookAccount(accountId)) {
|
|
4941
|
+
const api = this.getOutlookProvider(accountId);
|
|
4942
|
+
await api.sendRaw(raw);
|
|
4943
|
+
console.log(` [graph] ${accountId}: sent via /sendMail`);
|
|
4944
|
+
return;
|
|
4945
|
+
}
|
|
4946
|
+
await this.sendRawViaSMTP(accountId, raw);
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4694
4949
|
/** Send a raw RFC 2822 message via SMTP for a given account.
|
|
4695
4950
|
* Uses @bobfrankston/smtp-direct with the same TransportFactory as IMAP —
|
|
4696
4951
|
* same TCP byte-stream interface, no nodemailer dependency. */
|
|
@@ -4774,8 +5029,9 @@ export class ImapManager extends EventEmitter {
|
|
|
4774
5029
|
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
4775
5030
|
if (!outboxFolder) return;
|
|
4776
5031
|
|
|
4777
|
-
// Gmail: skip IMAP outbox
|
|
4778
|
-
|
|
5032
|
+
// Gmail/Outlook: skip IMAP outbox — sending handled by processLocalQueue
|
|
5033
|
+
// (Gmail via SMTP, Outlook via Graph /sendMail).
|
|
5034
|
+
if (this.isApiAccount(accountId)) return;
|
|
4779
5035
|
|
|
4780
5036
|
const settings = loadSettings();
|
|
4781
5037
|
const account = settings.accounts.find(a => a.id === accountId);
|
|
@@ -5109,7 +5365,7 @@ export class ImapManager extends EventEmitter {
|
|
|
5109
5365
|
if (this.sentSweepInterval) return;
|
|
5110
5366
|
const tick = async (): Promise<void> => {
|
|
5111
5367
|
for (const [accountId] of this.configs) {
|
|
5112
|
-
if (this.
|
|
5368
|
+
if (this.isApiAccount(accountId)) continue;
|
|
5113
5369
|
try { await this.sweepSentOnce(accountId); }
|
|
5114
5370
|
catch (e: any) { console.error(` [sent-sweep] ${accountId}: ${e?.message || e}`); }
|
|
5115
5371
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"installed": {
|
|
3
|
+
"client_id": "f0df4236-5d26-4aa3-a243-47f043b9db36",
|
|
4
|
+
"auth_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
5
|
+
"token_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
6
|
+
"redirect_uris": ["http://localhost:9327"]
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.108",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.108",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|