@marlinjai/email-mcp 1.2.7 → 1.3.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.
@@ -363,6 +363,7 @@ var FolderType = {
363
363
  };
364
364
 
365
365
  // src/providers/gmail/adapter.ts
366
+ import { Readable } from "node:stream";
366
367
  import { google } from "googleapis";
367
368
  import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
368
369
 
@@ -567,20 +568,32 @@ var GmailAdapter = class {
567
568
  async search(query) {
568
569
  const gmail = this.ensureConnected();
569
570
  const q = buildGmailQuery(query);
570
- const params = {
571
- userId: "me",
572
- maxResults: query.limit || 20
573
- };
574
- if (q) params.q = q;
575
- if (query.folder) params.labelIds = [query.folder];
576
- const res = await gmail.users.messages.list(params);
577
- const messages = res.data.messages || [];
578
- if (messages.length === 0) return [];
571
+ const limit = query.limit || 20;
572
+ const offset = query.offset || 0;
573
+ const needed = offset + limit;
574
+ const ids = [];
575
+ let pageToken;
576
+ do {
577
+ const params = {
578
+ userId: "me",
579
+ maxResults: Math.min(500, needed - ids.length)
580
+ };
581
+ if (q) params.q = q;
582
+ if (query.folder) params.labelIds = [query.folder];
583
+ if (pageToken) params.pageToken = pageToken;
584
+ const res = await gmail.users.messages.list(params);
585
+ for (const msg of res.data.messages || []) {
586
+ if (msg.id) ids.push(msg.id);
587
+ }
588
+ pageToken = res.data.nextPageToken || void 0;
589
+ } while (pageToken && ids.length < needed);
590
+ const windowIds = ids.slice(offset, offset + limit);
591
+ if (windowIds.length === 0) return [];
579
592
  const emails = [];
580
- for (const msg of messages) {
593
+ for (const id of windowIds) {
581
594
  const full = await gmail.users.messages.get({
582
595
  userId: "me",
583
- id: msg.id,
596
+ id,
584
597
  format: "full"
585
598
  });
586
599
  emails.push(mapGmailMessage(full.data, this.accountId));
@@ -828,6 +841,40 @@ var GmailAdapter = class {
828
841
  }
829
842
  return result;
830
843
  }
844
+ async getRawMessage(emailId, _sourceFolder) {
845
+ const gmail = this.ensureConnected();
846
+ const res = await gmail.users.messages.get({
847
+ userId: "me",
848
+ id: emailId,
849
+ format: "raw"
850
+ });
851
+ return Buffer.from(res.data.raw || "", "base64url");
852
+ }
853
+ async appendRawMessage(raw, targetFolder, flags) {
854
+ const gmail = this.ensureConnected();
855
+ const labelIds = [];
856
+ labelIds.push(targetFolder ? await this.resolveLabelId(targetFolder) : "INBOX");
857
+ if (flags?.read === false) labelIds.push("UNREAD");
858
+ if (flags?.starred === true) labelIds.push("STARRED");
859
+ const res = await gmail.users.messages.insert({
860
+ userId: "me",
861
+ internalDateSource: "dateHeader",
862
+ requestBody: { labelIds },
863
+ media: { mimeType: "message/rfc822", body: Readable.from(raw) }
864
+ });
865
+ return { id: res.data.id || "" };
866
+ }
867
+ /** Resolve a Gmail label name or id to a label id (accepts either). */
868
+ async resolveLabelId(nameOrId) {
869
+ const gmail = this.ensureConnected();
870
+ const res = await gmail.users.labels.list({ userId: "me" });
871
+ const labels = res.data.labels || [];
872
+ const byId = labels.find((l) => l.id === nameOrId);
873
+ if (byId?.id) return byId.id;
874
+ const byName = labels.find((l) => (l.name || "").toLowerCase() === nameOrId.toLowerCase());
875
+ if (byName?.id) return byName.id;
876
+ return nameOrId;
877
+ }
831
878
  async addLabels(emailId, labels) {
832
879
  const gmail = this.ensureConnected();
833
880
  await gmail.users.messages.modify({
@@ -1209,6 +1256,41 @@ var OutlookAdapter = class {
1209
1256
  destinationId
1210
1257
  });
1211
1258
  }
1259
+ async getRawMessage(emailId, _sourceFolder) {
1260
+ this.ensureClient();
1261
+ const res = await fetch(
1262
+ `https://graph.microsoft.com/v1.0/me/messages/${encodeURIComponent(emailId)}/$value`,
1263
+ { headers: { Authorization: `Bearer ${this.accessToken}` } }
1264
+ );
1265
+ if (!res.ok) {
1266
+ throw new Error(`Graph $value fetch failed for ${emailId}: HTTP ${res.status}`);
1267
+ }
1268
+ return Buffer.from(await res.arrayBuffer());
1269
+ }
1270
+ async appendRawMessage(raw, targetFolder, flags) {
1271
+ this.ensureClient();
1272
+ const endpoint = targetFolder ? `https://graph.microsoft.com/v1.0/me/mailFolders/${encodeURIComponent(await this.resolveFolder(targetFolder))}/messages` : "https://graph.microsoft.com/v1.0/me/messages";
1273
+ const res = await fetch(endpoint, {
1274
+ method: "POST",
1275
+ headers: {
1276
+ Authorization: `Bearer ${this.accessToken}`,
1277
+ "Content-Type": "text/plain"
1278
+ },
1279
+ body: raw.toString("base64")
1280
+ });
1281
+ if (!res.ok) {
1282
+ const detail = await res.text().catch(() => "");
1283
+ throw new Error(`Graph MIME import failed: HTTP ${res.status} ${detail.slice(0, 200)}`);
1284
+ }
1285
+ const data = await res.json().catch(() => ({}));
1286
+ if (flags && data.id && (flags.read !== void 0 || flags.starred !== void 0)) {
1287
+ try {
1288
+ await this.markEmail(data.id, flags);
1289
+ } catch {
1290
+ }
1291
+ }
1292
+ return { id: data.id || "" };
1293
+ }
1212
1294
  async deleteEmail(emailId, permanent, _sourceFolder) {
1213
1295
  const client = this.ensureClient();
1214
1296
  if (permanent) {
@@ -1623,14 +1705,7 @@ var ImapAdapter = class {
1623
1705
  }
1624
1706
  try {
1625
1707
  const mailboxExists = this.client.mailbox?.exists ?? 0;
1626
- let statusCount = 0;
1627
- try {
1628
- const status = await this.client.status(folder, { messages: true });
1629
- statusCount = status.messages ?? 0;
1630
- } catch {
1631
- }
1632
- const effectiveCount = Math.max(mailboxExists, statusCount);
1633
- if (effectiveCount === 0) {
1708
+ if (mailboxExists === 0) {
1634
1709
  return [];
1635
1710
  }
1636
1711
  const criteria = this.buildSearchCriteria(query);
@@ -1643,7 +1718,11 @@ var ImapAdapter = class {
1643
1718
  );
1644
1719
  allUids = Array.isArray(searchResult) ? searchResult : [];
1645
1720
  } catch {
1646
- allUids = await this.collectUidsViaFetch(query);
1721
+ try {
1722
+ allUids = await this.collectUidsViaFetch(query);
1723
+ } catch {
1724
+ return [];
1725
+ }
1647
1726
  }
1648
1727
  const offset = query.offset || 0;
1649
1728
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
@@ -1705,19 +1784,27 @@ var ImapAdapter = class {
1705
1784
  async fetchEmails(uids, folder, returnBody) {
1706
1785
  if (!this.client) return [];
1707
1786
  const emails = [];
1708
- if (returnBody) {
1709
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
1787
+ const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
1788
+ const uidRange = uids.join(",");
1789
+ let messages;
1790
+ try {
1791
+ messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
1792
+ } catch {
1793
+ messages = [];
1794
+ for (const uid of uids) {
1795
+ try {
1796
+ const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
1797
+ messages.push(...batch);
1798
+ } catch {
1799
+ }
1800
+ }
1801
+ }
1802
+ for (const msg of messages) {
1803
+ if (returnBody) {
1710
1804
  const parsed = await simpleParser(msg.source);
1711
1805
  parsed.flags = msg.flags;
1712
1806
  emails.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
1713
- }
1714
- } else {
1715
- for await (const msg of this.client.fetch(uids, {
1716
- envelope: true,
1717
- uid: true,
1718
- flags: true,
1719
- bodyStructure: true
1720
- })) {
1807
+ } else {
1721
1808
  const env = msg.envelope;
1722
1809
  emails.push({
1723
1810
  id: String(msg.uid),
@@ -1926,6 +2013,32 @@ var ImapAdapter = class {
1926
2013
  lock.release();
1927
2014
  }
1928
2015
  }
2016
+ async getRawMessage(emailId, sourceFolder) {
2017
+ if (!this.client) throw new Error("Not connected");
2018
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
2019
+ let lock;
2020
+ try {
2021
+ lock = await this.client.getMailboxLock(folder);
2022
+ } catch (error) {
2023
+ throw formatImapError(error, `Failed to open folder "${folder}"`);
2024
+ }
2025
+ try {
2026
+ const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
2027
+ if (!msg || !msg.source) throw new Error(`Email ${emailId} not found in "${folder}"`);
2028
+ return msg.source;
2029
+ } finally {
2030
+ lock.release();
2031
+ }
2032
+ }
2033
+ async appendRawMessage(raw, targetFolder, flags) {
2034
+ if (!this.client) throw new Error("Not connected");
2035
+ const folder = targetFolder ? await this.resolveFolder(targetFolder) : "INBOX";
2036
+ const imapFlags = [];
2037
+ if (flags?.read) imapFlags.push("\\Seen");
2038
+ if (flags?.starred) imapFlags.push("\\Flagged");
2039
+ const result = await this.client.append(folder, raw, imapFlags);
2040
+ return { id: String(result?.uid ?? "") };
2041
+ }
1929
2042
  async batchDelete(emailIds, permanent, sourceFolder) {
1930
2043
  if (!this.client) throw new Error("Not connected");
1931
2044
  const result = { succeeded: [], failed: [] };