@marlinjai/email-mcp 1.8.0 → 1.9.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.
@@ -892,6 +892,53 @@ function buildGmailQuery(query) {
892
892
  return parts.join(" ");
893
893
  }
894
894
 
895
+ // src/providers/mime.ts
896
+ import MailComposer from "nodemailer/lib/mail-composer/index.js";
897
+ import { convert } from "html-to-text";
898
+ function htmlToText(html) {
899
+ return convert(html, {
900
+ wordwrap: false,
901
+ selectors: [
902
+ { selector: "img", format: "skip" },
903
+ { selector: "a", options: { hideLinkHrefIfSameAsText: true } }
904
+ ]
905
+ }).trim();
906
+ }
907
+ function normalizeBody(body) {
908
+ const html = body.html ? body.html : void 0;
909
+ if (body.text) return { text: body.text, html };
910
+ if (html) return { text: htmlToText(html), html };
911
+ return { text: "" };
912
+ }
913
+ function toAddress(contact) {
914
+ return contact.name ? { name: contact.name, address: contact.email } : contact.email;
915
+ }
916
+ function toAddressList(contacts) {
917
+ return contacts?.length ? contacts.map(toAddress) : void 0;
918
+ }
919
+ async function buildMimeMessage(from, params) {
920
+ const { text, html } = normalizeBody(params.body);
921
+ const mail = {
922
+ from,
923
+ to: toAddressList(params.to),
924
+ cc: toAddressList(params.cc),
925
+ bcc: toAddressList(params.bcc),
926
+ subject: params.subject,
927
+ text,
928
+ html,
929
+ inReplyTo: params.inReplyTo,
930
+ references: params.references?.length ? params.references : void 0,
931
+ attachments: params.attachments?.map((a) => ({
932
+ filename: a.filename,
933
+ content: a.content,
934
+ contentType: a.contentType
935
+ }))
936
+ };
937
+ const node = new MailComposer(mail).compile();
938
+ node.keepBcc = true;
939
+ return node.build();
940
+ }
941
+
895
942
  // src/providers/gmail/adapter.ts
896
943
  var GmailAdapter = class {
897
944
  providerType = ProviderType.Gmail;
@@ -1034,7 +1081,7 @@ var GmailAdapter = class {
1034
1081
  }
1035
1082
  async sendEmail(params) {
1036
1083
  const gmail = this.ensureConnected();
1037
- const raw = this.buildRfc2822(params);
1084
+ const raw = await this.buildRaw(params);
1038
1085
  const res = await gmail.users.messages.send({
1039
1086
  userId: "me",
1040
1087
  requestBody: { raw }
@@ -1046,7 +1093,7 @@ var GmailAdapter = class {
1046
1093
  }
1047
1094
  async createDraft(params) {
1048
1095
  const gmail = this.ensureConnected();
1049
- const raw = this.buildRfc2822(params);
1096
+ const raw = await this.buildRaw(params);
1050
1097
  const res = await gmail.users.drafts.create({
1051
1098
  userId: "me",
1052
1099
  requestBody: {
@@ -1057,7 +1104,7 @@ var GmailAdapter = class {
1057
1104
  }
1058
1105
  async updateDraft(draftId, params) {
1059
1106
  const gmail = this.ensureConnected();
1060
- const raw = this.buildRfc2822(params);
1107
+ const raw = await this.buildRaw(params);
1061
1108
  const res = await gmail.users.drafts.update({
1062
1109
  userId: "me",
1063
1110
  id: draftId,
@@ -1330,23 +1377,36 @@ var GmailAdapter = class {
1330
1377
  requestBody: { addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"] }
1331
1378
  });
1332
1379
  }
1333
- async createBlockRule(rule) {
1334
- const gmail = this.ensureConnected();
1380
+ filterCriteria(matchType, value) {
1335
1381
  const criteria = {};
1336
- switch (rule.matchType) {
1382
+ switch (matchType) {
1337
1383
  case "senderDomain":
1338
- criteria.from = `@${rule.value}`;
1384
+ criteria.from = `@${value}`;
1339
1385
  break;
1340
1386
  case "senderAddress":
1341
- criteria.from = rule.value;
1387
+ criteria.from = value;
1342
1388
  break;
1343
1389
  case "subjectContains":
1344
- criteria.subject = rule.value;
1390
+ criteria.subject = value;
1345
1391
  break;
1346
1392
  case "headerContains":
1347
- criteria.query = rule.value;
1393
+ criteria.query = value;
1348
1394
  break;
1349
1395
  }
1396
+ return criteria;
1397
+ }
1398
+ /** The reverse of filterCriteria, for listing. A best-effort reading of filters made elsewhere. */
1399
+ readCriteria(criteria) {
1400
+ if (typeof criteria?.from === "string" && criteria.from.startsWith("@")) {
1401
+ return { matchType: "senderDomain", value: criteria.from.slice(1) };
1402
+ }
1403
+ if (criteria?.from) return { matchType: "senderAddress", value: criteria.from };
1404
+ if (criteria?.subject) return { matchType: "subjectContains", value: criteria.subject };
1405
+ return { matchType: "headerContains", value: criteria?.query || "" };
1406
+ }
1407
+ async createBlockRule(rule) {
1408
+ const gmail = this.ensureConnected();
1409
+ const criteria = this.filterCriteria(rule.matchType, rule.value);
1350
1410
  const action = rule.action === "delete" ? { addLabelIds: ["TRASH"] } : { removeLabelIds: ["INBOX"] };
1351
1411
  const res = await gmail.users.settings.filters.create({
1352
1412
  userId: "me",
@@ -1357,25 +1417,11 @@ var GmailAdapter = class {
1357
1417
  async listBlockRules() {
1358
1418
  const gmail = this.ensureConnected();
1359
1419
  const res = await gmail.users.settings.filters.list({ userId: "me" });
1360
- return (res.data.filter || []).map((f) => {
1361
- const criteria = f.criteria || {};
1420
+ return (res.data.filter || []).filter((f) => !f.action?.forward).map((f) => {
1362
1421
  const addLabelIds = f.action?.addLabelIds || [];
1363
- let matchType = "headerContains";
1364
- let value = criteria.query || "";
1365
- if (typeof criteria.from === "string" && criteria.from.startsWith("@")) {
1366
- matchType = "senderDomain";
1367
- value = criteria.from.slice(1);
1368
- } else if (criteria.from) {
1369
- matchType = "senderAddress";
1370
- value = criteria.from;
1371
- } else if (criteria.subject) {
1372
- matchType = "subjectContains";
1373
- value = criteria.subject;
1374
- }
1375
1422
  return {
1376
1423
  id: f.id || "",
1377
- matchType,
1378
- value,
1424
+ ...this.readCriteria(f.criteria),
1379
1425
  action: addLabelIds.includes("TRASH") ? "delete" : "moveToJunk",
1380
1426
  createdAt: ""
1381
1427
  // Gmail's filter API does not expose a creation timestamp
@@ -1386,30 +1432,71 @@ var GmailAdapter = class {
1386
1432
  const gmail = this.ensureConnected();
1387
1433
  await gmail.users.settings.filters.delete({ userId: "me", id: ruleId });
1388
1434
  }
1389
- buildRfc2822(params) {
1390
- const lines = [];
1391
- lines.push(`From: ${this.email}`);
1392
- lines.push(
1393
- `To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`
1435
+ /**
1436
+ * Gmail only forwards to an address the user has added and confirmed under
1437
+ * Settings, "Forwarding and POP/IMAP". Adding one through the API needs a
1438
+ * scope this app does not request (gmail.settings.sharing), so a missing or
1439
+ * unconfirmed address is answered with the manual step, before Gmail's own
1440
+ * less helpful error.
1441
+ */
1442
+ async createForwardRule(rule) {
1443
+ const gmail = this.ensureConnected();
1444
+ const target = rule.forwardTo.trim().toLowerCase();
1445
+ const addresses = await gmail.users.settings.forwardingAddresses.list({ userId: "me" });
1446
+ const known = (addresses.data.forwardingAddresses || []).find(
1447
+ (a) => (a.forwardingEmail || "").toLowerCase() === target
1394
1448
  );
1395
- if (params.cc?.length) {
1396
- lines.push(`Cc: ${params.cc.map((c) => c.email).join(", ")}`);
1397
- }
1398
- if (params.bcc?.length) {
1399
- lines.push(`Bcc: ${params.bcc.map((c) => c.email).join(", ")}`);
1449
+ if (!known) {
1450
+ throw new Error(
1451
+ `${rule.forwardTo} is not a forwarding address of this Gmail account yet. Add it once in Gmail under Settings, "Forwarding and POP/IMAP", "Add a forwarding address", confirm the email Google sends to it, then run this again.`
1452
+ );
1400
1453
  }
1401
- lines.push(`Subject: ${params.subject}`);
1402
- if (params.inReplyTo) {
1403
- lines.push(`In-Reply-To: ${params.inReplyTo}`);
1454
+ if (known.verificationStatus !== "accepted") {
1455
+ throw new Error(
1456
+ `${rule.forwardTo} was added to this Gmail account but not confirmed yet. Open the confirmation email Google sent to that address, confirm it, then run this again.`
1457
+ );
1404
1458
  }
1405
- if (params.references?.length) {
1406
- lines.push(`References: ${params.references.join(" ")}`);
1459
+ const existing = (await this.listForwardRules()).find(
1460
+ (r) => r.matchType === rule.matchType && r.value.toLowerCase() === rule.value.toLowerCase() && r.forwardTo.toLowerCase() === target
1461
+ );
1462
+ if (existing) return { id: existing.id, alreadyExisted: true };
1463
+ const action = { forward: known.forwardingEmail };
1464
+ if (!rule.keepInInbox) action.removeLabelIds = ["INBOX"];
1465
+ const res = await gmail.users.settings.filters.create({
1466
+ userId: "me",
1467
+ requestBody: { criteria: this.filterCriteria(rule.matchType, rule.value), action }
1468
+ });
1469
+ return { id: res.data.id || "" };
1470
+ }
1471
+ /** Every filter that forwards, including ones made by hand in Gmail. */
1472
+ async listForwardRules() {
1473
+ const gmail = this.ensureConnected();
1474
+ const res = await gmail.users.settings.filters.list({ userId: "me" });
1475
+ return (res.data.filter || []).filter((f) => f.action?.forward).map((f) => ({
1476
+ id: f.id || "",
1477
+ ...this.readCriteria(f.criteria),
1478
+ forwardTo: f.action.forward,
1479
+ keepInInbox: !(f.action.removeLabelIds || []).includes("INBOX"),
1480
+ createdAt: ""
1481
+ // Gmail's filter API does not expose a creation timestamp
1482
+ }));
1483
+ }
1484
+ async deleteForwardRule(ruleId) {
1485
+ const rules = await this.listForwardRules();
1486
+ if (!rules.some((r) => r.id === ruleId)) {
1487
+ throw new Error(`No forwarding rule with id ${ruleId} on this account. Use email_list_forward_rules to see the ids.`);
1407
1488
  }
1408
- lines.push("MIME-Version: 1.0");
1409
- lines.push("Content-Type: text/plain; charset=UTF-8");
1410
- lines.push("");
1411
- lines.push(params.body.text || params.body.html || "");
1412
- return Buffer.from(lines.join("\r\n")).toString("base64url");
1489
+ await this.deleteBlockRule(ruleId);
1490
+ }
1491
+ /**
1492
+ * Builds the base64url `raw` payload Gmail expects for send, draft create and
1493
+ * draft update: a full MIME message with a text part, an HTML part when one
1494
+ * was given (or a text part derived from HTML-only input), encoded headers
1495
+ * and any attachments. See `buildMimeMessage`.
1496
+ */
1497
+ async buildRaw(params) {
1498
+ const message = await buildMimeMessage(this.email, params);
1499
+ return message.toString("base64url");
1413
1500
  }
1414
1501
  };
1415
1502
 
@@ -1561,7 +1648,7 @@ function buildGraphFilter(query) {
1561
1648
  }
1562
1649
 
1563
1650
  // src/providers/outlook/adapter.ts
1564
- var OutlookAdapter = class {
1651
+ var OutlookAdapter = class _OutlookAdapter {
1565
1652
  providerType = ProviderType.Outlook;
1566
1653
  client = null;
1567
1654
  accountId = "";
@@ -1929,28 +2016,53 @@ var OutlookAdapter = class {
1929
2016
  * before this scope was added need to re-run the setup wizard to
1930
2017
  * re-consent — old tokens will 403 on this call until then.
1931
2018
  */
1932
- async createBlockRule(rule) {
1933
- const client = this.ensureClient();
2019
+ ruleConditions(matchType, value) {
1934
2020
  const conditions = {};
1935
- switch (rule.matchType) {
2021
+ switch (matchType) {
1936
2022
  case "senderDomain":
1937
2023
  case "senderAddress":
1938
- conditions.senderContains = [rule.value];
2024
+ conditions.senderContains = [value];
1939
2025
  break;
1940
2026
  case "subjectContains":
1941
- conditions.subjectContains = [rule.value];
2027
+ conditions.subjectContains = [value];
1942
2028
  break;
1943
2029
  case "headerContains":
1944
- conditions.headerContains = [rule.value];
2030
+ conditions.headerContains = [value];
1945
2031
  break;
1946
2032
  }
2033
+ return conditions;
2034
+ }
2035
+ // Graph doesn't tag which of our four matchTypes a rule came from:
2036
+ // this reconstruction is a best-effort heuristic for listing/auditing,
2037
+ // not a guaranteed round-trip of what was originally requested.
2038
+ readConditions(conditions) {
2039
+ if (conditions?.senderContains?.length) {
2040
+ const value = conditions.senderContains[0];
2041
+ return { matchType: value.includes("@") ? "senderAddress" : "senderDomain", value };
2042
+ }
2043
+ if (conditions?.subjectContains?.length) return { matchType: "subjectContains", value: conditions.subjectContains[0] };
2044
+ if (conditions?.headerContains?.length) return { matchType: "headerContains", value: conditions.headerContains[0] };
2045
+ return { matchType: "headerContains", value: "" };
2046
+ }
2047
+ /** Addresses a rule sends mail on to, whether by "forward" or "redirect". */
2048
+ static forwardTargets(actions) {
2049
+ return [...actions?.forwardTo || [], ...actions?.redirectTo || [], ...actions?.forwardAsAttachmentTo || []].map((r) => r?.emailAddress?.address).filter((a) => typeof a === "string" && a.length > 0);
2050
+ }
2051
+ async listMessageRules() {
2052
+ const client = this.ensureClient();
2053
+ const response = await client.api("/me/mailFolders/inbox/messageRules").get();
2054
+ return response.value || [];
2055
+ }
2056
+ async createBlockRule(rule) {
2057
+ const client = this.ensureClient();
2058
+ const conditions = this.ruleConditions(rule.matchType, rule.value);
1947
2059
  const actions = { stopProcessingRules: true };
1948
2060
  if (rule.action === "delete") {
1949
2061
  actions.delete = true;
1950
2062
  } else {
1951
2063
  actions.moveToFolder = await this.resolveFolder("junkemail");
1952
2064
  }
1953
- const existing = await this.listBlockRules();
2065
+ const existing = await this.listMessageRules();
1954
2066
  const result = await client.api("/me/mailFolders/inbox/messageRules").post({
1955
2067
  displayName: `email-mcp: ${rule.matchType} "${rule.value}"`.slice(0, 255),
1956
2068
  sequence: existing.length + 1,
@@ -1961,36 +2073,66 @@ var OutlookAdapter = class {
1961
2073
  return { id: result.id };
1962
2074
  }
1963
2075
  async listBlockRules() {
1964
- const client = this.ensureClient();
1965
- const response = await client.api("/me/mailFolders/inbox/messageRules").get();
1966
- return (response.value || []).map((r) => {
1967
- const conditions = r.conditions || {};
1968
- let matchType = "headerContains";
1969
- let value = "";
1970
- if (conditions.senderContains?.length) {
1971
- value = conditions.senderContains[0];
1972
- matchType = value.includes("@") ? "senderAddress" : "senderDomain";
1973
- } else if (conditions.subjectContains?.length) {
1974
- matchType = "subjectContains";
1975
- value = conditions.subjectContains[0];
1976
- } else if (conditions.headerContains?.length) {
1977
- matchType = "headerContains";
1978
- value = conditions.headerContains[0];
1979
- }
1980
- return {
1981
- id: r.id,
1982
- matchType,
1983
- value,
1984
- action: r.actions?.delete ? "delete" : "moveToJunk",
1985
- createdAt: ""
1986
- // Graph's messageRule resource has no creation timestamp field
1987
- };
1988
- });
2076
+ return (await this.listMessageRules()).filter((r) => _OutlookAdapter.forwardTargets(r.actions).length === 0).map((r) => ({
2077
+ id: r.id,
2078
+ ...this.readConditions(r.conditions),
2079
+ action: r.actions?.delete ? "delete" : "moveToJunk",
2080
+ createdAt: ""
2081
+ // Graph's messageRule resource has no creation timestamp field
2082
+ }));
1989
2083
  }
1990
2084
  async deleteBlockRule(ruleId) {
1991
2085
  const client = this.ensureClient();
1992
2086
  await client.api(`/me/mailFolders/inbox/messageRules/${encodeURIComponent(ruleId)}`).delete();
1993
2087
  }
2088
+ /**
2089
+ * Outlook forwards to any address without a confirmation step, so the only
2090
+ * check on the target is the allowlist in the tool layer. Needs the same
2091
+ * MailboxSettings.ReadWrite scope as block rules.
2092
+ */
2093
+ async createForwardRule(rule) {
2094
+ const client = this.ensureClient();
2095
+ const forwardTo = rule.forwardTo.trim();
2096
+ const target = forwardTo.toLowerCase();
2097
+ const all = await this.listMessageRules();
2098
+ const existing = all.find((r) => {
2099
+ const read = this.readConditions(r.conditions);
2100
+ const sameMatch = read.value.toLowerCase() === rule.value.toLowerCase() && (read.matchType === rule.matchType || read.matchType.startsWith("sender") && rule.matchType.startsWith("sender"));
2101
+ return sameMatch && _OutlookAdapter.forwardTargets(r.actions).some((a) => a.toLowerCase() === target);
2102
+ });
2103
+ if (existing) return { id: existing.id, alreadyExisted: true };
2104
+ const actions = { forwardTo: [{ emailAddress: { address: forwardTo } }] };
2105
+ if (!rule.keepInInbox) {
2106
+ actions.moveToFolder = await this.resolveFolder("archive");
2107
+ actions.stopProcessingRules = true;
2108
+ }
2109
+ const result = await client.api("/me/mailFolders/inbox/messageRules").post({
2110
+ displayName: `email-mcp: forward ${rule.matchType} "${rule.value}" to ${forwardTo}`.slice(0, 255),
2111
+ sequence: all.length + 1,
2112
+ isEnabled: true,
2113
+ conditions: this.ruleConditions(rule.matchType, rule.value),
2114
+ actions
2115
+ });
2116
+ return { id: result.id };
2117
+ }
2118
+ /** Every inbox rule that forwards or redirects, including ones made by hand in Outlook. */
2119
+ async listForwardRules() {
2120
+ return (await this.listMessageRules()).filter((r) => _OutlookAdapter.forwardTargets(r.actions).length > 0).map((r) => ({
2121
+ id: r.id,
2122
+ ...this.readConditions(r.conditions),
2123
+ forwardTo: _OutlookAdapter.forwardTargets(r.actions).join(", "),
2124
+ keepInInbox: !r.actions?.moveToFolder && !r.actions?.delete,
2125
+ createdAt: ""
2126
+ // Graph's messageRule resource has no creation timestamp field
2127
+ }));
2128
+ }
2129
+ async deleteForwardRule(ruleId) {
2130
+ const rules = await this.listForwardRules();
2131
+ if (!rules.some((r) => r.id === ruleId)) {
2132
+ throw new Error(`No forwarding rule with id ${ruleId} on this account. Use email_list_forward_rules to see the ids.`);
2133
+ }
2134
+ await this.deleteBlockRule(ruleId);
2135
+ }
1994
2136
  async getCategories() {
1995
2137
  const client = this.ensureClient();
1996
2138
  const response = await client.api("/me/outlook/masterCategories").get();
@@ -2094,14 +2236,15 @@ function createSmtpTransport(email, creds) {
2094
2236
  });
2095
2237
  }
2096
2238
  async function sendViaSmtp(transport, from, params) {
2239
+ const { text, html } = normalizeBody(params.body);
2097
2240
  const result = await transport.sendMail({
2098
2241
  from,
2099
- to: params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", "),
2100
- cc: params.cc?.map((c) => c.email).join(", "),
2101
- bcc: params.bcc?.map((c) => c.email).join(", "),
2242
+ to: params.to.map(toAddress),
2243
+ cc: params.cc?.length ? params.cc.map(toAddress) : void 0,
2244
+ bcc: params.bcc?.length ? params.bcc.map(toAddress) : void 0,
2102
2245
  subject: params.subject,
2103
- text: params.body.text,
2104
- html: params.body.html,
2246
+ text,
2247
+ html,
2105
2248
  inReplyTo: params.inReplyTo,
2106
2249
  references: params.references?.join(" "),
2107
2250
  attachments: params.attachments?.map((a) => ({
@@ -2572,26 +2715,17 @@ var ImapAdapter = class {
2572
2715
  const messageId = await sendViaSmtp(transport, this.email, params);
2573
2716
  return { id: messageId };
2574
2717
  }
2575
- buildDraftRfc2822(params) {
2576
- const lines = [];
2577
- lines.push(`From: ${this.email}`);
2578
- lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
2579
- if (params.cc?.length) {
2580
- lines.push(`Cc: ${params.cc.map((c) => c.email).join(", ")}`);
2581
- }
2582
- lines.push(`Subject: ${params.subject}`);
2583
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
2584
- if (params.inReplyTo) lines.push(`In-Reply-To: ${params.inReplyTo}`);
2585
- if (params.references?.length) lines.push(`References: ${params.references.join(" ")}`);
2586
- lines.push("MIME-Version: 1.0");
2587
- lines.push("Content-Type: text/plain; charset=utf-8");
2588
- lines.push("");
2589
- lines.push(params.body.text || "");
2590
- return lines.join("\r\n");
2718
+ /**
2719
+ * Builds the full MIME message a draft is stored as: text and HTML parts,
2720
+ * Bcc kept so the draft remembers its recipients, encoded headers and any
2721
+ * attachments. See `buildMimeMessage`.
2722
+ */
2723
+ buildDraftMessage(params) {
2724
+ return buildMimeMessage(this.email, params);
2591
2725
  }
2592
2726
  async createDraft(params) {
2593
2727
  const client = await this.ensureConnected();
2594
- const rawMessage = this.buildDraftRfc2822(params);
2728
+ const rawMessage = await this.buildDraftMessage(params);
2595
2729
  const draftsFolder = await this.resolveFolder("Drafts");
2596
2730
  const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2597
2731
  if (!result) throw new Error("Failed to append draft");
@@ -2606,6 +2740,7 @@ var ImapAdapter = class {
2606
2740
  async updateDraft(draftId, params, sourceFolder) {
2607
2741
  const client = await this.ensureConnected();
2608
2742
  const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
2743
+ const rawMessage = await this.buildDraftMessage(params);
2609
2744
  let lock;
2610
2745
  try {
2611
2746
  lock = await client.getMailboxLock(draftsFolder);
@@ -2617,7 +2752,6 @@ var ImapAdapter = class {
2617
2752
  } finally {
2618
2753
  lock.release();
2619
2754
  }
2620
- const rawMessage = this.buildDraftRfc2822(params);
2621
2755
  const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
2622
2756
  if (!result) throw new Error("Failed to append updated draft");
2623
2757
  return { id: String(result.uid || result) };
@@ -3366,6 +3500,9 @@ async function main() {
3366
3500
  addMore = await askConfirm("Would you like to add another email account?", false);
3367
3501
  }
3368
3502
  console.log("\nSetup complete! You can now use the email tools in Claude Code.");
3503
+ console.log(
3504
+ "\nemail-mcp is free and open source. Google requires a yearly independent security\naudit (675 US dollars) for the Gmail sign-in it ships. If email-mcp is useful to you,\nyou can help cover it: https://github.com/sponsors/marlinjai"
3505
+ );
3369
3506
  }
3370
3507
  main().catch((err) => {
3371
3508
  console.error(`