@diskd-ai/sdk 5.1.4 → 5.1.6

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.
@@ -0,0 +1,8 @@
1
+ import type { AuthModule } from '../auth/types.js';
2
+ import type { InboxClient } from './inboxTypes.js';
3
+ export declare const createInboxClient: (params: {
4
+ readonly auth: AuthModule;
5
+ readonly driveUrl?: string;
6
+ readonly mcpUrl?: string;
7
+ }) => InboxClient;
8
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/inbox/inbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOnD,OAAO,KAAK,EAEV,WAAW,EAYZ,MAAM,iBAAiB,CAAC;AAiVzB,eAAO,MAAM,iBAAiB,GAAI,QAAQ;IACxC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,KAAG,WA8dH,CAAC"}
@@ -0,0 +1,675 @@
1
+ import { createDriveClient } from '../drive/drive.js';
2
+ import { createMcpToolsClient } from '../mcpTools/mcpTools.js';
3
+ import { createMessagesStoreClient } from '../messagesStore/messagesStore.js';
4
+ const MAIL_ROOT = '/.profile/mail';
5
+ const DEFAULT_EXCHANGE_FOLDER = 'INBOX';
6
+ const SEARCH_SCAN_LIMIT = 100;
7
+ const SYSTEM_HYDRATE_EMAIL_BODIES_TOOL = 'system_hydrate_email_bodies';
8
+ const SYSTEM_HYDRATE_EMAIL_ATTACHMENT_TOOL = 'system_hydrate_email_attachment';
9
+ const REF_PREFIX = 'op-inbox:';
10
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ const isString = (value) => typeof value === 'string';
12
+ const isBool = (value) => typeof value === 'boolean';
13
+ const isNumber = (value) => typeof value === 'number';
14
+ const nonEmpty = (value) => {
15
+ const trimmed = value?.trim();
16
+ return trimmed && trimmed.length > 0 ? trimmed : null;
17
+ };
18
+ const exchangeMailboxId = (account) => {
19
+ const lower = account.toLowerCase();
20
+ const slug = lower.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
21
+ if (slug.length === 0)
22
+ return 'exchange-default';
23
+ if (slug.startsWith('exchange-'))
24
+ return slug.slice(0, 64);
25
+ return `exchange-${slug.slice(0, 55)}`;
26
+ };
27
+ const inboxPath = (account) => `${MAIL_ROOT}/${account}/inbox`;
28
+ const isJsonFile = (entry) => entry.type === 'file' && entry.name.endsWith('.json');
29
+ const isDirectory = (entry) => entry.type === 'dir';
30
+ const encodeRef = (ref) => `${REF_PREFIX}${Buffer.from(JSON.stringify(ref), 'utf-8').toString('base64url')}`;
31
+ const decodeRef = (value) => {
32
+ if (!value.startsWith(REF_PREFIX))
33
+ return null;
34
+ try {
35
+ const decoded = JSON.parse(Buffer.from(value.slice(REF_PREFIX.length), 'base64url').toString('utf-8'));
36
+ if (!isObject(decoded))
37
+ return null;
38
+ const source = decoded.source;
39
+ const account = decoded.account;
40
+ const messageId = decoded.messageId;
41
+ if (!isString(account) || !isString(messageId))
42
+ return null;
43
+ if (source === 'legacy')
44
+ return { source, account, messageId };
45
+ if (source === 'exchange' && isString(decoded.folderId)) {
46
+ return { source, account, folderId: decoded.folderId, messageId };
47
+ }
48
+ return null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ };
54
+ const parseContact = (value) => {
55
+ if (!isObject(value))
56
+ return { name: '', address: '' };
57
+ return {
58
+ name: isString(value.name) ? value.name : '',
59
+ address: isString(value.address) ? value.address : '',
60
+ };
61
+ };
62
+ const parseContactList = (value) => Array.isArray(value) ? value.map(parseContact) : [];
63
+ const stringArray = (value) => Array.isArray(value) ? value.filter(isString) : [];
64
+ const hasFlag = (payload, flag) => Array.isArray(payload.flags) &&
65
+ payload.flags.some((item) => isString(item) && item.toLowerCase() === flag.toLowerCase());
66
+ const parseAttachment = (value) => {
67
+ if (!isObject(value)) {
68
+ return { filename: '', contentType: '', size: 0, drivePath: '' };
69
+ }
70
+ return {
71
+ filename: isString(value.filename) ? value.filename : '',
72
+ contentType: isString(value.contentType) ? value.contentType : '',
73
+ size: isNumber(value.size)
74
+ ? value.size
75
+ : isNumber(value.sizeBytes)
76
+ ? value.sizeBytes
77
+ : isNumber(value.storedSizeBytes)
78
+ ? value.storedSizeBytes
79
+ : 0,
80
+ drivePath: isString(value.drivePath) ? value.drivePath : '',
81
+ ...(isString(value.attachmentId) ? { attachmentId: value.attachmentId } : {}),
82
+ ...(isString(value.storageState) ? { storageState: value.storageState } : {}),
83
+ ...(isNumber(value.storedSizeBytes) ? { storedSizeBytes: value.storedSizeBytes } : {}),
84
+ ...(isString(value.storedAt) ? { storedAt: value.storedAt } : {}),
85
+ ...(isString(value.lastLoadError) ? { lastLoadError: value.lastLoadError } : {}),
86
+ };
87
+ };
88
+ const parseStoredEmail = (value) => {
89
+ if (!isObject(value) || !isString(value.messageId))
90
+ return null;
91
+ return {
92
+ messageId: value.messageId,
93
+ uid: isNumber(value.uid) ? value.uid : null,
94
+ account: isString(value.account) ? value.account : '',
95
+ folder: isString(value.folder) ? value.folder : 'inbox',
96
+ from: parseContact(value.from),
97
+ to: parseContactList(value.to),
98
+ cc: parseContactList(value.cc),
99
+ subject: isString(value.subject) ? value.subject : '',
100
+ date: isString(value.date) ? value.date : '',
101
+ receivedAt: isString(value.receivedAt) ? value.receivedAt : '',
102
+ snippet: isString(value.snippet) ? value.snippet : '',
103
+ bodyText: isString(value.bodyText) ? value.bodyText : '',
104
+ bodyHtml: isString(value.bodyHtml) ? value.bodyHtml : '',
105
+ hasAttachments: isBool(value.hasAttachments) ? value.hasAttachments : false,
106
+ attachments: Array.isArray(value.attachments) ? value.attachments.map(parseAttachment) : [],
107
+ labels: stringArray(value.labels),
108
+ isRead: isBool(value.isRead) ? value.isRead : false,
109
+ isFlagged: isBool(value.isFlagged) ? value.isFlagged : false,
110
+ priority: isString(value.priority) ? value.priority : 'normal',
111
+ webhookEvent: isString(value.webhookEvent) ? value.webhookEvent : '',
112
+ rule: isString(value.rule) ? value.rule : null,
113
+ };
114
+ };
115
+ const legacyEnvelope = (email, account, drivePath) => ({
116
+ messageRef: encodeRef({ source: 'legacy', account, messageId: email.messageId }),
117
+ folderId: email.folder || 'inbox',
118
+ account: email.account || account,
119
+ messageId: email.messageId,
120
+ from: email.from,
121
+ subject: email.subject,
122
+ snippet: email.snippet,
123
+ date: email.date,
124
+ hasAttachments: email.hasAttachments,
125
+ isRead: email.isRead,
126
+ isFlagged: email.isFlagged,
127
+ priority: email.priority,
128
+ labels: email.labels,
129
+ drivePath,
130
+ });
131
+ const withLegacyRef = (email, account) => ({
132
+ ...email,
133
+ messageRef: encodeRef({ source: 'legacy', account, messageId: email.messageId }),
134
+ folderId: email.folder || 'inbox',
135
+ });
136
+ const payloadObject = (row) => (isObject(row.payload) ? row.payload : {});
137
+ const exchangeStoredEmail = (row, account, folderId) => {
138
+ const payload = payloadObject(row);
139
+ const messageId = row.externalId;
140
+ const folder = isString(payload.mailbox)
141
+ ? payload.mailbox
142
+ : isString(payload.folderId)
143
+ ? payload.folderId
144
+ : folderId;
145
+ return {
146
+ messageRef: encodeRef({ source: 'exchange', account, folderId: folder, messageId }),
147
+ folderId: folder,
148
+ messageId,
149
+ uid: isNumber(payload.uid) ? payload.uid : null,
150
+ account: isString(payload.accountId)
151
+ ? payload.accountId
152
+ : isString(payload.account)
153
+ ? payload.account
154
+ : account,
155
+ folder,
156
+ from: parseContact(payload.from),
157
+ to: parseContactList(payload.to),
158
+ cc: parseContactList(payload.cc),
159
+ subject: isString(payload.subject) ? payload.subject : '',
160
+ date: isString(payload.date) ? payload.date : '',
161
+ receivedAt: isString(payload.receivedAt)
162
+ ? payload.receivedAt
163
+ : isString(payload.fetchedAt)
164
+ ? payload.fetchedAt
165
+ : '',
166
+ snippet: isString(payload.snippet) ? payload.snippet : '',
167
+ bodyText: isString(payload.bodyText) ? payload.bodyText : '',
168
+ bodyHtml: isString(payload.bodyHtml) ? payload.bodyHtml : '',
169
+ hasAttachments: isBool(payload.hasAttachments) ? payload.hasAttachments : false,
170
+ attachments: Array.isArray(payload.attachments) ? payload.attachments.map(parseAttachment) : [],
171
+ labels: stringArray(payload.labels),
172
+ isRead: isBool(payload.isRead) ? payload.isRead : hasFlag(payload, '\\Seen'),
173
+ isFlagged: isBool(payload.isFlagged) ? payload.isFlagged : hasFlag(payload, '\\Flagged'),
174
+ priority: isString(payload.priority) ? payload.priority : 'normal',
175
+ webhookEvent: 'exchange.messagesStore',
176
+ rule: null,
177
+ };
178
+ };
179
+ const exchangeEnvelope = (row, account, folderId) => {
180
+ const email = exchangeStoredEmail(row, account, folderId);
181
+ return {
182
+ messageRef: email.messageRef,
183
+ folderId: email.folderId,
184
+ account: email.account,
185
+ messageId: email.messageId,
186
+ from: email.from,
187
+ subject: email.subject,
188
+ snippet: email.snippet,
189
+ date: email.date,
190
+ hasAttachments: email.hasAttachments,
191
+ isRead: email.isRead,
192
+ isFlagged: email.isFlagged,
193
+ priority: email.priority,
194
+ labels: email.labels,
195
+ drivePath: '',
196
+ };
197
+ };
198
+ const shouldHydrateBody = (row) => {
199
+ const payload = payloadObject(row);
200
+ if (payload.bodyState === 'loaded')
201
+ return false;
202
+ if (payload.bodyState === undefined &&
203
+ (isString(payload.bodyText) || isString(payload.bodyHtml))) {
204
+ return false;
205
+ }
206
+ return (payload.bodyState === undefined ||
207
+ payload.bodyState === null ||
208
+ payload.bodyState === 'not_loaded' ||
209
+ payload.bodyState === 'failed_retryable');
210
+ };
211
+ const readStreamToBuffer = async (stream) => {
212
+ const reader = stream.getReader();
213
+ const chunks = [];
214
+ try {
215
+ while (true) {
216
+ const { done, value } = await reader.read();
217
+ if (done)
218
+ break;
219
+ if (value)
220
+ chunks.push(value);
221
+ }
222
+ }
223
+ finally {
224
+ reader.releaseLock();
225
+ }
226
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
227
+ };
228
+ const isNotFound = (error) => error instanceof Error &&
229
+ /not.?found|MAILBOX_NOT_FOUND|FOLDER_NOT_FOUND|MESSAGE_NOT_FOUND/i.test(error.message);
230
+ const findSystemToolName = async (mcpTools, systemToolName) => {
231
+ const tools = await mcpTools.list();
232
+ const tool = tools.find((item) => item.name.endsWith(`__${systemToolName}`));
233
+ if (!tool) {
234
+ throw new Error(`${systemToolName} tool is not available`);
235
+ }
236
+ return tool.name;
237
+ };
238
+ const splitPath = (fullPath) => {
239
+ const normalized = fullPath.replace(/\/+$/, '');
240
+ if (normalized.length === 0 || normalized === '/') {
241
+ throw new Error('targetPath must include a filename');
242
+ }
243
+ const lastSlash = normalized.lastIndexOf('/');
244
+ if (lastSlash <= 0) {
245
+ return { parentPath: '/', name: normalized.replace(/^\//, '') };
246
+ }
247
+ return {
248
+ parentPath: normalized.slice(0, lastSlash),
249
+ name: normalized.slice(lastSlash + 1),
250
+ };
251
+ };
252
+ const findLegacyAttachment = (email, filename) => {
253
+ const match = email.attachments.find((attachment) => attachment.filename === filename);
254
+ if (!match)
255
+ throw new Error(`Attachment not found: ${filename}`);
256
+ if (!match.drivePath)
257
+ throw new Error(`Attachment has no drivePath: ${filename}`);
258
+ return match;
259
+ };
260
+ const findAttachmentByHandle = (email, attachmentId, filename) => {
261
+ const resolvedAttachmentId = nonEmpty(attachmentId);
262
+ if (resolvedAttachmentId) {
263
+ const match = email.attachments.find((attachment) => attachment.attachmentId === resolvedAttachmentId);
264
+ if (!match)
265
+ throw new Error(`Attachment not found: ${resolvedAttachmentId}`);
266
+ return match;
267
+ }
268
+ const resolvedFilename = nonEmpty(filename);
269
+ if (!resolvedFilename)
270
+ throw new Error('attachmentId or filename is required');
271
+ const matches = email.attachments.filter((attachment) => attachment.filename === resolvedFilename);
272
+ if (matches.length === 0)
273
+ throw new Error(`Attachment not found: ${resolvedFilename}`);
274
+ if (matches.length > 1) {
275
+ throw new Error(`Multiple attachments named ${resolvedFilename}; use attachmentId`);
276
+ }
277
+ return matches[0];
278
+ };
279
+ const shouldHydrateAttachment = (attachment) => attachment.storageState === 'not_loaded' || attachment.storageState === 'failed_retryable';
280
+ export const createInboxClient = (params) => {
281
+ const drive = createDriveClient({
282
+ version: 'v1',
283
+ auth: params.auth,
284
+ url: params.driveUrl,
285
+ });
286
+ const messagesStore = createMessagesStoreClient({
287
+ auth: params.auth,
288
+ url: params.driveUrl,
289
+ });
290
+ const mcpTools = createMcpToolsClient({ auth: params.auth, url: params.mcpUrl });
291
+ let hydrateBodyToolName = null;
292
+ let hydrateAttachmentToolName = null;
293
+ const listLegacyAccounts = async () => {
294
+ try {
295
+ const entries = await drive.list({ path: MAIL_ROOT });
296
+ return entries.filter(isDirectory).map((entry) => entry.name);
297
+ }
298
+ catch {
299
+ return [];
300
+ }
301
+ };
302
+ const readLegacyFile = async (path) => {
303
+ const result = await drive.download.file({ path });
304
+ const buffer = await readStreamToBuffer(result.stream);
305
+ return parseStoredEmail(JSON.parse(buffer.toString('utf-8')));
306
+ };
307
+ const listLegacy = async (account, limit, cursor) => {
308
+ const dirPath = inboxPath(account);
309
+ const entries = await drive.list({ path: dirPath });
310
+ const jsonFiles = entries
311
+ .filter(isJsonFile)
312
+ .slice()
313
+ .sort((a, b) => b.name.localeCompare(a.name));
314
+ const total = jsonFiles.length;
315
+ const startIndex = cursor
316
+ ? Math.max(jsonFiles.findIndex((entry) => entry.id === cursor) + 1, 0)
317
+ : 0;
318
+ const page = jsonFiles.slice(startIndex, startIndex + limit);
319
+ const items = [];
320
+ for (const entry of page) {
321
+ const path = `${dirPath}/${entry.name}`;
322
+ const email = await readLegacyFile(path);
323
+ if (email)
324
+ items.push(legacyEnvelope(email, account, path));
325
+ }
326
+ const last = page[page.length - 1];
327
+ return {
328
+ items,
329
+ nextCursor: startIndex + limit < total && last ? last.id : null,
330
+ total,
331
+ };
332
+ };
333
+ const readLegacy = async (account, messageId) => {
334
+ const dirPath = inboxPath(account);
335
+ const entries = await drive.list({ path: dirPath });
336
+ for (const entry of entries.filter(isJsonFile)) {
337
+ const email = await readLegacyFile(`${dirPath}/${entry.name}`);
338
+ if (email?.messageId === messageId)
339
+ return withLegacyRef(email, account);
340
+ }
341
+ throw new Error(`Email not found: ${messageId}`);
342
+ };
343
+ const listExchangeFolderIds = async (account) => {
344
+ try {
345
+ const folders = await messagesStore
346
+ .mailbox({ mailboxId: exchangeMailboxId(account) })
347
+ .listFolders();
348
+ const ids = folders.map((folder) => folder.folderId);
349
+ if (ids.length === 0)
350
+ return [DEFAULT_EXCHANGE_FOLDER];
351
+ const rest = ids.filter((id) => id !== DEFAULT_EXCHANGE_FOLDER);
352
+ return ids.includes(DEFAULT_EXCHANGE_FOLDER) ? [DEFAULT_EXCHANGE_FOLDER, ...rest] : ids;
353
+ }
354
+ catch {
355
+ return [DEFAULT_EXCHANGE_FOLDER];
356
+ }
357
+ };
358
+ const hydrateBody = async (mailboxId, folderId, externalId) => {
359
+ hydrateBodyToolName ??= await findSystemToolName(mcpTools, SYSTEM_HYDRATE_EMAIL_BODIES_TOOL);
360
+ const result = await mcpTools.call(hydrateBodyToolName, {
361
+ messages: [{ mailboxId, folderId, externalId }],
362
+ maxMessages: 1,
363
+ });
364
+ if (result.isError) {
365
+ throw new Error(`${SYSTEM_HYDRATE_EMAIL_BODIES_TOOL} returned error`);
366
+ }
367
+ };
368
+ const readExchangeExact = async (account, folderId, messageId) => {
369
+ const mailboxId = exchangeMailboxId(account);
370
+ const folder = messagesStore.mailbox({ mailboxId }).folder({ folderId });
371
+ let row = await folder.getMessage({ externalId: messageId });
372
+ if (shouldHydrateBody(row)) {
373
+ await hydrateBody(mailboxId, folderId, row.externalId);
374
+ row = await folder.getMessage({ externalId: row.externalId });
375
+ }
376
+ return exchangeStoredEmail(row, account, folderId);
377
+ };
378
+ const readExchange = async (account, messageId, folderId) => {
379
+ if (folderId)
380
+ return readExchangeExact(account, folderId, messageId);
381
+ const folders = await listExchangeFolderIds(account);
382
+ let lastError = null;
383
+ for (const candidate of folders) {
384
+ try {
385
+ return await readExchangeExact(account, candidate, messageId);
386
+ }
387
+ catch (error) {
388
+ lastError = error;
389
+ if (!isNotFound(error))
390
+ throw error;
391
+ }
392
+ }
393
+ throw lastError instanceof Error ? lastError : new Error(`Email not found: ${messageId}`);
394
+ };
395
+ const markLegacyRead = async (account, messageId, isRead) => {
396
+ const dirPath = inboxPath(account);
397
+ const entries = await drive.list({ path: dirPath });
398
+ for (const entry of entries.filter(isJsonFile)) {
399
+ const path = `${dirPath}/${entry.name}`;
400
+ const email = await readLegacyFile(path);
401
+ if (email?.messageId !== messageId)
402
+ continue;
403
+ const updated = { ...email, isRead };
404
+ await drive.tools.writeFile({ path, content: JSON.stringify(updated, null, 2) });
405
+ return withLegacyRef(updated, account);
406
+ }
407
+ throw new Error(`Email not found: ${messageId}`);
408
+ };
409
+ const markExchangeRead = async (account, folderId, messageId, isRead) => {
410
+ const mailboxId = exchangeMailboxId(account);
411
+ const folder = messagesStore.mailbox({ mailboxId }).folder({ folderId });
412
+ const row = await folder.getMessage({ externalId: messageId });
413
+ await folder.upsertBatch({
414
+ items: [{ externalId: row.externalId, payload: { ...row.payload, isRead } }],
415
+ });
416
+ return exchangeStoredEmail({ ...row, payload: { ...row.payload, isRead } }, account, folderId);
417
+ };
418
+ const readLegacyForSave = async (account, messageId) => {
419
+ const directPath = `${inboxPath(account)}/${messageId}`;
420
+ try {
421
+ const email = await readLegacyFile(directPath);
422
+ if (email)
423
+ return withLegacyRef(email, account);
424
+ }
425
+ catch {
426
+ // Fall back to scanning by stored messageId for compatibility with read/list handles.
427
+ }
428
+ return readLegacy(account, messageId);
429
+ };
430
+ const saveLegacyAttachment = async (account, messageId, filename, targetPath) => {
431
+ const email = await readLegacyForSave(account, messageId);
432
+ const attachment = findLegacyAttachment(email, filename);
433
+ const sourceEntries = await drive.resolve({ paths: [attachment.drivePath] });
434
+ const sourceEntry = sourceEntries[0];
435
+ if (!sourceEntry?.fileId) {
436
+ throw new Error(`Cannot resolve fileId for attachment path: ${attachment.drivePath}`);
437
+ }
438
+ const target = splitPath(targetPath);
439
+ const created = await drive.create({
440
+ name: target.name,
441
+ type: 'file',
442
+ parentPath: target.parentPath,
443
+ fileId: sourceEntry.fileId,
444
+ });
445
+ return {
446
+ saved: true,
447
+ entry: {
448
+ id: created.id,
449
+ name: created.name,
450
+ path: targetPath,
451
+ fileId: created.fileId,
452
+ },
453
+ };
454
+ };
455
+ const hydrateAttachment = async (mailboxId, folderId, externalId, attachmentId) => {
456
+ hydrateAttachmentToolName ??= await findSystemToolName(mcpTools, SYSTEM_HYDRATE_EMAIL_ATTACHMENT_TOOL);
457
+ const result = await mcpTools.call(hydrateAttachmentToolName, {
458
+ mailboxId,
459
+ folderId,
460
+ externalId,
461
+ attachmentId,
462
+ });
463
+ if (result.isError) {
464
+ throw new Error(`${SYSTEM_HYDRATE_EMAIL_ATTACHMENT_TOOL} returned error`);
465
+ }
466
+ };
467
+ const ensureExchangeAttachmentLoaded = async (mailboxId, folderId, externalId, attachment) => {
468
+ const attachmentId = nonEmpty(attachment.attachmentId);
469
+ if (!attachmentId)
470
+ throw new Error(`Attachment has no attachmentId: ${attachment.filename}`);
471
+ const scopedMessage = messagesStore
472
+ .mailbox({ mailboxId })
473
+ .folder({ folderId })
474
+ .message({ externalId });
475
+ const hasStoredRow = async () => {
476
+ const rows = await scopedMessage.attachments.list();
477
+ return rows.some((row) => row.attachmentId === attachmentId);
478
+ };
479
+ if (shouldHydrateAttachment(attachment) || !(await hasStoredRow())) {
480
+ await hydrateAttachment(mailboxId, folderId, externalId, attachmentId);
481
+ if (!(await hasStoredRow())) {
482
+ throw new Error(`Attachment not hydrated: ${attachmentId}`);
483
+ }
484
+ }
485
+ return attachmentId;
486
+ };
487
+ const saveExchangeAttachmentExact = async (account, folderId, messageId, attachmentId, filename, targetPath) => {
488
+ const mailboxId = exchangeMailboxId(account);
489
+ const row = await messagesStore
490
+ .mailbox({ mailboxId })
491
+ .folder({ folderId })
492
+ .getMessage({ externalId: messageId });
493
+ const email = exchangeStoredEmail(row, account, folderId);
494
+ const attachment = findAttachmentByHandle(email, attachmentId, filename);
495
+ const resolvedAttachmentId = await ensureExchangeAttachmentLoaded(mailboxId, folderId, row.externalId, attachment);
496
+ const saved = await messagesStore
497
+ .mailbox({ mailboxId })
498
+ .folder({ folderId })
499
+ .message({ externalId: row.externalId })
500
+ .attachments.saveToDrive({ attachmentId: resolvedAttachmentId, targetPath });
501
+ return {
502
+ saved: true,
503
+ entry: {
504
+ id: saved.entry.id,
505
+ name: saved.entry.name,
506
+ path: saved.entry.fullPath ?? targetPath,
507
+ fileId: saved.entry.fileId,
508
+ },
509
+ };
510
+ };
511
+ return {
512
+ listAccounts: async () => {
513
+ const legacyAccounts = await listLegacyAccounts();
514
+ const accountMap = new Map();
515
+ for (const account of legacyAccounts)
516
+ accountMap.set(account, { account, displayName: account });
517
+ try {
518
+ const mailboxes = await messagesStore.listMailboxes();
519
+ for (const mailbox of mailboxes) {
520
+ if (!accountMap.has(mailbox.mailboxId)) {
521
+ accountMap.set(mailbox.mailboxId, {
522
+ account: mailbox.mailboxId,
523
+ displayName: mailbox.displayName,
524
+ });
525
+ }
526
+ }
527
+ }
528
+ catch {
529
+ // Legacy-only workspaces remain valid.
530
+ }
531
+ const items = [...accountMap.values()].sort((a, b) => a.displayName.localeCompare(b.displayName));
532
+ return { accounts: items.map((item) => item.account), items };
533
+ },
534
+ list: async ({ account, folderId, limit = 20, cursor, }) => {
535
+ const selectedFolder = nonEmpty(folderId) ?? DEFAULT_EXCHANGE_FOLDER;
536
+ try {
537
+ const page = await messagesStore
538
+ .mailbox({ mailboxId: exchangeMailboxId(account) })
539
+ .folder({ folderId: selectedFolder })
540
+ .listMessages({ limit, ...(cursor ? { cursor } : {}) });
541
+ return {
542
+ items: page.items.map((row) => exchangeEnvelope(row, account, selectedFolder)),
543
+ nextCursor: page.nextCursor,
544
+ total: page.items.length,
545
+ };
546
+ }
547
+ catch (error) {
548
+ if (!isNotFound(error))
549
+ throw error;
550
+ return listLegacy(account, limit, cursor);
551
+ }
552
+ },
553
+ read: async ({ account, messageId, messageRef, folderId, }) => {
554
+ const ref = messageRef ? decodeRef(messageRef) : messageId ? decodeRef(messageId) : null;
555
+ if (messageRef && !ref)
556
+ throw new Error('Invalid messageRef');
557
+ if (ref?.source === 'legacy')
558
+ return readLegacy(ref.account, ref.messageId);
559
+ if (ref?.source === 'exchange')
560
+ return readExchangeExact(ref.account, ref.folderId, ref.messageId);
561
+ const resolvedAccount = nonEmpty(account);
562
+ const resolvedMessageId = nonEmpty(messageId);
563
+ if (!resolvedAccount || !resolvedMessageId) {
564
+ throw new Error('Either messageRef, or account + messageId is required');
565
+ }
566
+ try {
567
+ return await readExchange(resolvedAccount, resolvedMessageId, folderId);
568
+ }
569
+ catch (error) {
570
+ if (!isNotFound(error))
571
+ throw error;
572
+ return readLegacy(resolvedAccount, resolvedMessageId);
573
+ }
574
+ },
575
+ search: async ({ account, query, folderId, limit = 10 }) => {
576
+ const lower = query.toLowerCase();
577
+ const results = [];
578
+ const exchangeFolders = folderId ? [folderId] : await listExchangeFolderIds(account);
579
+ for (const exchangeFolderId of exchangeFolders) {
580
+ if (results.length >= limit)
581
+ break;
582
+ try {
583
+ const page = await messagesStore
584
+ .mailbox({ mailboxId: exchangeMailboxId(account) })
585
+ .folder({ folderId: exchangeFolderId })
586
+ .listMessages({ limit: SEARCH_SCAN_LIMIT });
587
+ for (const row of page.items) {
588
+ if (results.length >= limit)
589
+ break;
590
+ const item = exchangeEnvelope(row, account, exchangeFolderId);
591
+ const searchable = [item.subject, item.from.name, item.from.address, item.snippet]
592
+ .join(' ')
593
+ .toLowerCase();
594
+ if (searchable.includes(lower))
595
+ results.push(item);
596
+ }
597
+ }
598
+ catch (error) {
599
+ if (!isNotFound(error))
600
+ throw error;
601
+ }
602
+ }
603
+ if (results.length < limit) {
604
+ try {
605
+ const legacy = await listLegacy(account, SEARCH_SCAN_LIMIT);
606
+ for (const item of legacy.items) {
607
+ if (results.length >= limit)
608
+ break;
609
+ const searchable = [item.subject, item.from.name, item.from.address, item.snippet]
610
+ .join(' ')
611
+ .toLowerCase();
612
+ if (searchable.includes(lower))
613
+ results.push(item);
614
+ }
615
+ }
616
+ catch {
617
+ // Exchange-only accounts remain valid.
618
+ }
619
+ }
620
+ return { results };
621
+ },
622
+ markRead: async ({ account, messageId, messageRef, folderId, isRead }) => {
623
+ const ref = messageRef ? decodeRef(messageRef) : messageId ? decodeRef(messageId) : null;
624
+ if (messageRef && !ref)
625
+ throw new Error('Invalid messageRef');
626
+ if (ref?.source === 'legacy')
627
+ return markLegacyRead(ref.account, ref.messageId, isRead);
628
+ if (ref?.source === 'exchange')
629
+ return markExchangeRead(ref.account, ref.folderId, ref.messageId, isRead);
630
+ const resolvedAccount = nonEmpty(account);
631
+ const resolvedMessageId = nonEmpty(messageId);
632
+ if (!resolvedAccount || !resolvedMessageId) {
633
+ throw new Error('Either messageRef, or account + messageId is required');
634
+ }
635
+ try {
636
+ return await markExchangeRead(resolvedAccount, nonEmpty(folderId) ?? DEFAULT_EXCHANGE_FOLDER, resolvedMessageId, isRead);
637
+ }
638
+ catch (error) {
639
+ if (!isNotFound(error))
640
+ throw error;
641
+ return markLegacyRead(resolvedAccount, resolvedMessageId, isRead);
642
+ }
643
+ },
644
+ saveAttachment: async ({ account, messageId, messageRef, folderId, attachmentId, filename, targetPath, }) => {
645
+ const ref = messageRef ? decodeRef(messageRef) : messageId ? decodeRef(messageId) : null;
646
+ if (messageRef && !ref)
647
+ throw new Error('Invalid messageRef');
648
+ const resolvedFilename = nonEmpty(filename);
649
+ if (ref?.source === 'legacy') {
650
+ if (!resolvedFilename)
651
+ throw new Error('filename is required for legacy attachments');
652
+ return saveLegacyAttachment(ref.account, ref.messageId, resolvedFilename, targetPath);
653
+ }
654
+ if (ref?.source === 'exchange') {
655
+ return saveExchangeAttachmentExact(ref.account, ref.folderId, ref.messageId, attachmentId, filename, targetPath);
656
+ }
657
+ const resolvedAccount = nonEmpty(account);
658
+ const resolvedMessageId = nonEmpty(messageId);
659
+ if (!resolvedAccount || !resolvedMessageId) {
660
+ throw new Error('Either messageRef, or account + messageId is required');
661
+ }
662
+ try {
663
+ return await saveExchangeAttachmentExact(resolvedAccount, nonEmpty(folderId) ?? DEFAULT_EXCHANGE_FOLDER, resolvedMessageId, attachmentId, filename, targetPath);
664
+ }
665
+ catch (error) {
666
+ if (!isNotFound(error))
667
+ throw error;
668
+ if (!resolvedFilename)
669
+ throw new Error('filename is required for legacy attachments');
670
+ return saveLegacyAttachment(resolvedAccount, resolvedMessageId, resolvedFilename, targetPath);
671
+ }
672
+ },
673
+ };
674
+ };
675
+ //# sourceMappingURL=inbox.js.map