@ever-works/mailgun-plugin 1.0.5 → 1.0.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.
package/README.md CHANGED
@@ -13,11 +13,11 @@ Mailgun email provider plugin for Ever Works (Notifications v2, EW-650).
13
13
  | `apiKey` | yes | `MAILGUN_API_KEY` | yes |
14
14
  | `domain` | no | `MAILGUN_DOMAIN` | yes |
15
15
  | `region` | no | `MAILGUN_REGION` (`us`/`eu`) | no (us) |
16
- | `webhookSigningKey` | yes | `MAILGUN_WEBHOOK_SIGNING_KEY` | no |
16
+ | `webhookSigningKey` | yes | `MAILGUN_WEBHOOK_SIGNING_KEY` | inbound |
17
17
 
18
18
  ## Notes
19
19
 
20
20
  - Outbound is built on the official **`mailgun.js`** SDK (`messages.create`); metadata is forwarded as Mailgun `v:` custom variables.
21
21
  - Inbound HMAC verification uses Node `crypto` — Mailgun ships no SDK helper for inbound-route signature checking.
22
- - Inbound verifies the Mailgun **HMAC-SHA256** signature — `HMAC(signingKey, timestamp + token)` compared in constant time — and decodes both JSON and form-urlencoded webhook shapes. When no signing key is set, verification is skipped (operator opt-in).
22
+ - Inbound verifies the Mailgun **HMAC-SHA256** signature — `HMAC(signingKey, timestamp + token)` compared in constant time — and decodes both JSON and form-urlencoded webhook shapes. When no signing key is set, inbound webhooks are **refused** (`401`); only `EMAIL_INBOUND_ALLOW_UNSIGNED=true` (that exact string, for a local stack with no public webhook URL) accepts them unsigned.
23
23
  - `parseInboundWebhook` maps Mailgun's parsed fields (`sender`/`recipient`/`subject`/`body-plain`/`body-html`). Multipart attachment extraction requires a multipart parser at the controller layer and is left empty here.
package/dist/index.cjs CHANGED
@@ -103,10 +103,14 @@ var USER_SELECTABLE_WORK_KINDS = [
103
103
  "repo",
104
104
  "app"
105
105
  ];
106
- var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
106
+ var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
107
107
  new Set(WORK_KINDS);
108
108
  var GOAL_KINDS = ["metric", "delivery"];
109
109
  new Set(GOAL_KINDS);
110
+ var RECORD_VISIBILITIES = ["private", "team", "organization"];
111
+ new Set(RECORD_VISIBILITIES);
112
+ var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
113
+ new Set(RECORD_LIST_VIEWS);
110
114
  var VERSION = "1.0.0";
111
115
  var GTM_SKILLS = [
112
116
  // ── research ────────────────────────────────────────────────────────
@@ -812,6 +816,155 @@ var ACTION_CATEGORIES = Object.freeze([
812
816
  Object.freeze(
813
817
  ACTION_CATEGORIES.filter((c) => c !== "read.internal")
814
818
  );
819
+ var DOMAIN_EVENT_CATALOG = [
820
+ // EW-P2-6 — `org:write` (and the app) create Organizations.
821
+ { type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
822
+ { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
823
+ {
824
+ type: "organization.member.added",
825
+ domain: "organization",
826
+ aggregateType: "organization",
827
+ phase: "P1",
828
+ child: "member"
829
+ },
830
+ {
831
+ type: "organization.member.updated",
832
+ domain: "organization",
833
+ aggregateType: "organization",
834
+ phase: "P1",
835
+ child: "member"
836
+ },
837
+ {
838
+ type: "organization.member.removed",
839
+ domain: "organization",
840
+ aggregateType: "organization",
841
+ phase: "P1",
842
+ child: "member"
843
+ },
844
+ { type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
845
+ { type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
846
+ { type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
847
+ { type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
848
+ { type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
849
+ { type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
850
+ { type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
851
+ { type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
852
+ { type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
853
+ { type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
854
+ { type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
855
+ { type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
856
+ { type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
857
+ { type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
858
+ { type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
859
+ { type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
860
+ { type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
861
+ { type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
862
+ { type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
863
+ { type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
864
+ { type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
865
+ { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
866
+ { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
867
+ { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
868
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
869
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
870
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
871
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
872
+ { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
873
+ { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
874
+ { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
875
+ { type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
876
+ { type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
877
+ { type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
878
+ { type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
879
+ { type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
880
+ { type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
881
+ { type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
882
+ { type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
883
+ { type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
884
+ { type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
885
+ { type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
886
+ { type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
887
+ { type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
888
+ { type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
889
+ { type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
890
+ { type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
891
+ { type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
892
+ { type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
893
+ { type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
894
+ { type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
895
+ { type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
896
+ { type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
897
+ { type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
898
+ { type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
899
+ { type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
900
+ { type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
901
+ { type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
902
+ { type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
903
+ { type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
904
+ { type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
905
+ { type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
906
+ { type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
907
+ { type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
908
+ { type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
909
+ { type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
910
+ { type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
911
+ { type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
912
+ { type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
913
+ { type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
914
+ { type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
915
+ { type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
916
+ { type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
917
+ { type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
918
+ { type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
919
+ { type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
920
+ { type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
921
+ { type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
922
+ { type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
923
+ ];
924
+ DOMAIN_EVENT_CATALOG.map((entry) => entry.type);
925
+ new Map(
926
+ DOMAIN_EVENT_CATALOG.map((entry) => [entry.type, entry])
927
+ );
928
+ var EMAIL_MESSAGE_ID_MAX_LENGTH = 255;
929
+ var EMAIL_REFERENCES_MAX_IDS = 50;
930
+ function normalizeMessageId(raw) {
931
+ if (typeof raw !== "string") return void 0;
932
+ let value = raw.trim();
933
+ const open = value.indexOf("<");
934
+ if (open !== -1) {
935
+ const close = value.indexOf(">", open + 1);
936
+ value = close === -1 ? value.slice(open + 1) : value.slice(open + 1, close);
937
+ }
938
+ value = value.trim();
939
+ if (!value || value.length > EMAIL_MESSAGE_ID_MAX_LENGTH) return void 0;
940
+ for (let i = 0; i < value.length; i += 1) {
941
+ const code = value.charCodeAt(i);
942
+ if (code <= 32 || code === 127 || value[i] === "<" || value[i] === ">") return void 0;
943
+ }
944
+ return value;
945
+ }
946
+ function parseMessageIdList(raw) {
947
+ if (raw == null) return [];
948
+ const text = Array.isArray(raw) ? raw.join(" ") : String(raw);
949
+ const ids = [];
950
+ if (text.includes("<")) {
951
+ let open = text.indexOf("<");
952
+ while (open !== -1) {
953
+ const close = text.indexOf(">", open + 1);
954
+ if (close === -1) break;
955
+ const id = normalizeMessageId(text.slice(open + 1, close));
956
+ if (id) ids.push(id);
957
+ open = text.indexOf("<", close + 1);
958
+ }
959
+ } else {
960
+ for (const part of text.split(/[\s,]+/)) {
961
+ const id = normalizeMessageId(part);
962
+ if (id) ids.push(id);
963
+ }
964
+ }
965
+ const unique = [...new Set(ids)];
966
+ return unique.slice(Math.max(0, unique.length - EMAIL_REFERENCES_MAX_IDS));
967
+ }
815
968
  var PLUGIN_CAPABILITIES = {
816
969
  // Notifications v2 (EW-650) — Email Providers. Plugins MAY declare
817
970
  // `EMAIL_OUTBOUND`, `EMAIL_INBOUND`, or both. See
@@ -844,6 +997,10 @@ function resolveSigningKey(options) {
844
997
  const fromEnv = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
845
998
  return fromEnv && fromEnv.length > 0 ? fromEnv : void 0;
846
999
  }
1000
+ var EMAIL_INBOUND_ALLOW_UNSIGNED_ENV = "EMAIL_INBOUND_ALLOW_UNSIGNED";
1001
+ function unsignedInboundAllowed() {
1002
+ return process.env[EMAIL_INBOUND_ALLOW_UNSIGNED_ENV] === "true";
1003
+ }
847
1004
  function decodeBody(rawBody) {
848
1005
  const text = rawBody.toString("utf8");
849
1006
  try {
@@ -870,6 +1027,31 @@ function angleAddress(value) {
870
1027
  }
871
1028
  return null;
872
1029
  }
1030
+ function splitAddressList(raw) {
1031
+ const parts = [];
1032
+ let current = "";
1033
+ let inQuote = false;
1034
+ let inAngle = false;
1035
+ for (let i = 0; i < raw.length; i += 1) {
1036
+ const ch = raw[i];
1037
+ if (ch === "\\" && inQuote && i + 1 < raw.length) {
1038
+ current += ch + raw[i + 1];
1039
+ i += 1;
1040
+ continue;
1041
+ }
1042
+ if (ch === '"' && !inAngle) inQuote = !inQuote;
1043
+ else if (ch === "<" && !inQuote) inAngle = true;
1044
+ else if (ch === ">" && !inQuote) inAngle = false;
1045
+ if (ch === "," && !inQuote && !inAngle) {
1046
+ parts.push(current);
1047
+ current = "";
1048
+ continue;
1049
+ }
1050
+ current += ch;
1051
+ }
1052
+ parts.push(current);
1053
+ return parts;
1054
+ }
873
1055
  function extractSignature(body) {
874
1056
  const nested = body.signature;
875
1057
  if (nested && typeof nested === "object") {
@@ -922,6 +1104,7 @@ var MailgunPlugin = class {
922
1104
  const mg = mailgun.client({ username: "api", key: apiKey, url: resolveApiUrl(options) });
923
1105
  const customVars = {};
924
1106
  for (const [k, v] of Object.entries(input.metadata ?? {})) customVars[`v:${k}`] = v;
1107
+ for (const [k, v] of Object.entries(input.headers ?? {})) customVars[`h:${k}`] = v;
925
1108
  let id;
926
1109
  try {
927
1110
  const res = await mg.messages.create(domain, {
@@ -965,7 +1148,17 @@ var MailgunPlugin = class {
965
1148
  }
966
1149
  verifyWebhookSignature(rawBody, _headers, options) {
967
1150
  const signingKey = resolveSigningKey(options);
968
- if (!signingKey) return;
1151
+ if (!signingKey) {
1152
+ if (!unsignedInboundAllowed()) {
1153
+ throw new Error(
1154
+ `Mailgun inbound: no \`webhookSigningKey\`/MAILGUN_WEBHOOK_SIGNING_KEY configured; refusing an unsigned webhook (${EMAIL_INBOUND_ALLOW_UNSIGNED_ENV}=true accepts it on a local stack only).`
1155
+ );
1156
+ }
1157
+ console.warn(
1158
+ `[mailgun] inbound webhook accepted WITHOUT signature verification: no \`webhookSigningKey\`/MAILGUN_WEBHOOK_SIGNING_KEY configured and ${EMAIL_INBOUND_ALLOW_UNSIGNED_ENV}=true. Configure a signing key to authenticate inbound mail.`
1159
+ );
1160
+ return;
1161
+ }
969
1162
  const sig = extractSignature(decodeBody(rawBody));
970
1163
  if (!sig) {
971
1164
  throw new Error("Mailgun inbound: missing timestamp/token/signature.");
@@ -992,11 +1185,39 @@ var MailgunPlugin = class {
992
1185
  const recipient = str("recipient") ?? str("To") ?? "";
993
1186
  const messageId = (str("Message-Id") ?? str("message-id") ?? `mailgun-${Date.now()}`).replace(/^<|>$/g, "");
994
1187
  const tsSeconds = Number(str("timestamp"));
1188
+ const header = (name) => {
1189
+ for (const [key, value] of Object.entries(body)) {
1190
+ if (key.toLowerCase() === name && typeof value === "string") return value;
1191
+ }
1192
+ const raw = body["message-headers"];
1193
+ let pairs = raw;
1194
+ if (typeof raw === "string") {
1195
+ try {
1196
+ pairs = JSON.parse(raw);
1197
+ } catch {
1198
+ pairs = null;
1199
+ }
1200
+ }
1201
+ if (Array.isArray(pairs)) {
1202
+ for (const pair of pairs) {
1203
+ if (Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "string" && pair[0].toLowerCase() === name) {
1204
+ return pair[1];
1205
+ }
1206
+ }
1207
+ }
1208
+ return void 0;
1209
+ };
1210
+ const messageIdHeader = normalizeMessageId(header("message-id"));
1211
+ const cc = splitAddressList(header("cc") ?? "").map((part) => (angleAddress(part.trim()) ?? part).trim()).filter((address) => address.length > 0 && address.includes("@"));
1212
+ const inReplyTo = parseMessageIdList(header("in-reply-to"))[0];
1213
+ const references = parseMessageIdList(header("references"));
995
1214
  return {
996
1215
  provider: this.id,
997
1216
  providerMessageId: messageId,
998
1217
  from: str("sender") ?? str("from") ?? "",
999
1218
  to: parseAddressList(recipient),
1219
+ // EW-P4-3 — Cc is recorded (never routed on).
1220
+ ...cc.length > 0 ? { cc } : {},
1000
1221
  subject: str("subject") ?? "",
1001
1222
  bodyText: str("body-plain") ?? str("stripped-text") ?? "",
1002
1223
  bodyHtml: str("body-html") ?? str("stripped-html"),
@@ -1005,7 +1226,10 @@ var MailgunPlugin = class {
1005
1226
  metadata: {
1006
1227
  recipient,
1007
1228
  signatureVerified: Boolean(resolveSigningKey(_options))
1008
- }
1229
+ },
1230
+ ...messageIdHeader ? { messageIdHeader } : {},
1231
+ ...inReplyTo ? { inReplyTo } : {},
1232
+ ...references.length > 0 ? { references } : {}
1009
1233
  };
1010
1234
  }
1011
1235
  };
package/dist/index.js CHANGED
@@ -96,10 +96,14 @@ var USER_SELECTABLE_WORK_KINDS = [
96
96
  "repo",
97
97
  "app"
98
98
  ];
99
- var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
99
+ var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
100
100
  new Set(WORK_KINDS);
101
101
  var GOAL_KINDS = ["metric", "delivery"];
102
102
  new Set(GOAL_KINDS);
103
+ var RECORD_VISIBILITIES = ["private", "team", "organization"];
104
+ new Set(RECORD_VISIBILITIES);
105
+ var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
106
+ new Set(RECORD_LIST_VIEWS);
103
107
  var VERSION = "1.0.0";
104
108
  var GTM_SKILLS = [
105
109
  // ── research ────────────────────────────────────────────────────────
@@ -805,6 +809,155 @@ var ACTION_CATEGORIES = Object.freeze([
805
809
  Object.freeze(
806
810
  ACTION_CATEGORIES.filter((c) => c !== "read.internal")
807
811
  );
812
+ var DOMAIN_EVENT_CATALOG = [
813
+ // EW-P2-6 — `org:write` (and the app) create Organizations.
814
+ { type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
815
+ { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
816
+ {
817
+ type: "organization.member.added",
818
+ domain: "organization",
819
+ aggregateType: "organization",
820
+ phase: "P1",
821
+ child: "member"
822
+ },
823
+ {
824
+ type: "organization.member.updated",
825
+ domain: "organization",
826
+ aggregateType: "organization",
827
+ phase: "P1",
828
+ child: "member"
829
+ },
830
+ {
831
+ type: "organization.member.removed",
832
+ domain: "organization",
833
+ aggregateType: "organization",
834
+ phase: "P1",
835
+ child: "member"
836
+ },
837
+ { type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
838
+ { type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
839
+ { type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
840
+ { type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
841
+ { type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
842
+ { type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
843
+ { type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
844
+ { type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
845
+ { type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
846
+ { type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
847
+ { type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
848
+ { type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
849
+ { type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
850
+ { type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
851
+ { type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
852
+ { type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
853
+ { type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
854
+ { type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
855
+ { type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
856
+ { type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
857
+ { type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
858
+ { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
859
+ { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
860
+ { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
861
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
862
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
863
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
864
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
865
+ { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
866
+ { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
867
+ { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
868
+ { type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
869
+ { type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
870
+ { type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
871
+ { type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
872
+ { type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
873
+ { type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
874
+ { type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
875
+ { type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
876
+ { type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
877
+ { type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
878
+ { type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
879
+ { type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
880
+ { type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
881
+ { type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
882
+ { type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
883
+ { type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
884
+ { type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
885
+ { type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
886
+ { type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
887
+ { type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
888
+ { type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
889
+ { type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
890
+ { type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
891
+ { type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
892
+ { type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
893
+ { type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
894
+ { type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
895
+ { type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
896
+ { type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
897
+ { type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
898
+ { type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
899
+ { type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
900
+ { type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
901
+ { type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
902
+ { type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
903
+ { type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
904
+ { type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
905
+ { type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
906
+ { type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
907
+ { type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
908
+ { type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
909
+ { type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
910
+ { type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
911
+ { type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
912
+ { type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
913
+ { type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
914
+ { type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
915
+ { type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
916
+ ];
917
+ DOMAIN_EVENT_CATALOG.map((entry) => entry.type);
918
+ new Map(
919
+ DOMAIN_EVENT_CATALOG.map((entry) => [entry.type, entry])
920
+ );
921
+ var EMAIL_MESSAGE_ID_MAX_LENGTH = 255;
922
+ var EMAIL_REFERENCES_MAX_IDS = 50;
923
+ function normalizeMessageId(raw) {
924
+ if (typeof raw !== "string") return void 0;
925
+ let value = raw.trim();
926
+ const open = value.indexOf("<");
927
+ if (open !== -1) {
928
+ const close = value.indexOf(">", open + 1);
929
+ value = close === -1 ? value.slice(open + 1) : value.slice(open + 1, close);
930
+ }
931
+ value = value.trim();
932
+ if (!value || value.length > EMAIL_MESSAGE_ID_MAX_LENGTH) return void 0;
933
+ for (let i = 0; i < value.length; i += 1) {
934
+ const code = value.charCodeAt(i);
935
+ if (code <= 32 || code === 127 || value[i] === "<" || value[i] === ">") return void 0;
936
+ }
937
+ return value;
938
+ }
939
+ function parseMessageIdList(raw) {
940
+ if (raw == null) return [];
941
+ const text = Array.isArray(raw) ? raw.join(" ") : String(raw);
942
+ const ids = [];
943
+ if (text.includes("<")) {
944
+ let open = text.indexOf("<");
945
+ while (open !== -1) {
946
+ const close = text.indexOf(">", open + 1);
947
+ if (close === -1) break;
948
+ const id = normalizeMessageId(text.slice(open + 1, close));
949
+ if (id) ids.push(id);
950
+ open = text.indexOf("<", close + 1);
951
+ }
952
+ } else {
953
+ for (const part of text.split(/[\s,]+/)) {
954
+ const id = normalizeMessageId(part);
955
+ if (id) ids.push(id);
956
+ }
957
+ }
958
+ const unique = [...new Set(ids)];
959
+ return unique.slice(Math.max(0, unique.length - EMAIL_REFERENCES_MAX_IDS));
960
+ }
808
961
  var PLUGIN_CAPABILITIES = {
809
962
  // Notifications v2 (EW-650) — Email Providers. Plugins MAY declare
810
963
  // `EMAIL_OUTBOUND`, `EMAIL_INBOUND`, or both. See
@@ -837,6 +990,10 @@ function resolveSigningKey(options) {
837
990
  const fromEnv = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
838
991
  return fromEnv && fromEnv.length > 0 ? fromEnv : void 0;
839
992
  }
993
+ var EMAIL_INBOUND_ALLOW_UNSIGNED_ENV = "EMAIL_INBOUND_ALLOW_UNSIGNED";
994
+ function unsignedInboundAllowed() {
995
+ return process.env[EMAIL_INBOUND_ALLOW_UNSIGNED_ENV] === "true";
996
+ }
840
997
  function decodeBody(rawBody) {
841
998
  const text = rawBody.toString("utf8");
842
999
  try {
@@ -863,6 +1020,31 @@ function angleAddress(value) {
863
1020
  }
864
1021
  return null;
865
1022
  }
1023
+ function splitAddressList(raw) {
1024
+ const parts = [];
1025
+ let current = "";
1026
+ let inQuote = false;
1027
+ let inAngle = false;
1028
+ for (let i = 0; i < raw.length; i += 1) {
1029
+ const ch = raw[i];
1030
+ if (ch === "\\" && inQuote && i + 1 < raw.length) {
1031
+ current += ch + raw[i + 1];
1032
+ i += 1;
1033
+ continue;
1034
+ }
1035
+ if (ch === '"' && !inAngle) inQuote = !inQuote;
1036
+ else if (ch === "<" && !inQuote) inAngle = true;
1037
+ else if (ch === ">" && !inQuote) inAngle = false;
1038
+ if (ch === "," && !inQuote && !inAngle) {
1039
+ parts.push(current);
1040
+ current = "";
1041
+ continue;
1042
+ }
1043
+ current += ch;
1044
+ }
1045
+ parts.push(current);
1046
+ return parts;
1047
+ }
866
1048
  function extractSignature(body) {
867
1049
  const nested = body.signature;
868
1050
  if (nested && typeof nested === "object") {
@@ -915,6 +1097,7 @@ var MailgunPlugin = class {
915
1097
  const mg = mailgun.client({ username: "api", key: apiKey, url: resolveApiUrl(options) });
916
1098
  const customVars = {};
917
1099
  for (const [k, v] of Object.entries(input.metadata ?? {})) customVars[`v:${k}`] = v;
1100
+ for (const [k, v] of Object.entries(input.headers ?? {})) customVars[`h:${k}`] = v;
918
1101
  let id;
919
1102
  try {
920
1103
  const res = await mg.messages.create(domain, {
@@ -958,7 +1141,17 @@ var MailgunPlugin = class {
958
1141
  }
959
1142
  verifyWebhookSignature(rawBody, _headers, options) {
960
1143
  const signingKey = resolveSigningKey(options);
961
- if (!signingKey) return;
1144
+ if (!signingKey) {
1145
+ if (!unsignedInboundAllowed()) {
1146
+ throw new Error(
1147
+ `Mailgun inbound: no \`webhookSigningKey\`/MAILGUN_WEBHOOK_SIGNING_KEY configured; refusing an unsigned webhook (${EMAIL_INBOUND_ALLOW_UNSIGNED_ENV}=true accepts it on a local stack only).`
1148
+ );
1149
+ }
1150
+ console.warn(
1151
+ `[mailgun] inbound webhook accepted WITHOUT signature verification: no \`webhookSigningKey\`/MAILGUN_WEBHOOK_SIGNING_KEY configured and ${EMAIL_INBOUND_ALLOW_UNSIGNED_ENV}=true. Configure a signing key to authenticate inbound mail.`
1152
+ );
1153
+ return;
1154
+ }
962
1155
  const sig = extractSignature(decodeBody(rawBody));
963
1156
  if (!sig) {
964
1157
  throw new Error("Mailgun inbound: missing timestamp/token/signature.");
@@ -985,11 +1178,39 @@ var MailgunPlugin = class {
985
1178
  const recipient = str("recipient") ?? str("To") ?? "";
986
1179
  const messageId = (str("Message-Id") ?? str("message-id") ?? `mailgun-${Date.now()}`).replace(/^<|>$/g, "");
987
1180
  const tsSeconds = Number(str("timestamp"));
1181
+ const header = (name) => {
1182
+ for (const [key, value] of Object.entries(body)) {
1183
+ if (key.toLowerCase() === name && typeof value === "string") return value;
1184
+ }
1185
+ const raw = body["message-headers"];
1186
+ let pairs = raw;
1187
+ if (typeof raw === "string") {
1188
+ try {
1189
+ pairs = JSON.parse(raw);
1190
+ } catch {
1191
+ pairs = null;
1192
+ }
1193
+ }
1194
+ if (Array.isArray(pairs)) {
1195
+ for (const pair of pairs) {
1196
+ if (Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "string" && pair[0].toLowerCase() === name) {
1197
+ return pair[1];
1198
+ }
1199
+ }
1200
+ }
1201
+ return void 0;
1202
+ };
1203
+ const messageIdHeader = normalizeMessageId(header("message-id"));
1204
+ const cc = splitAddressList(header("cc") ?? "").map((part) => (angleAddress(part.trim()) ?? part).trim()).filter((address) => address.length > 0 && address.includes("@"));
1205
+ const inReplyTo = parseMessageIdList(header("in-reply-to"))[0];
1206
+ const references = parseMessageIdList(header("references"));
988
1207
  return {
989
1208
  provider: this.id,
990
1209
  providerMessageId: messageId,
991
1210
  from: str("sender") ?? str("from") ?? "",
992
1211
  to: parseAddressList(recipient),
1212
+ // EW-P4-3 — Cc is recorded (never routed on).
1213
+ ...cc.length > 0 ? { cc } : {},
993
1214
  subject: str("subject") ?? "",
994
1215
  bodyText: str("body-plain") ?? str("stripped-text") ?? "",
995
1216
  bodyHtml: str("body-html") ?? str("stripped-html"),
@@ -998,7 +1219,10 @@ var MailgunPlugin = class {
998
1219
  metadata: {
999
1220
  recipient,
1000
1221
  signatureVerified: Boolean(resolveSigningKey(_options))
1001
- }
1222
+ },
1223
+ ...messageIdHeader ? { messageIdHeader } : {},
1224
+ ...inReplyTo ? { inReplyTo } : {},
1225
+ ...references.length > 0 ? { references } : {}
1002
1226
  };
1003
1227
  }
1004
1228
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ever-works/mailgun-plugin",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Mailgun email provider plugin (outbound + inbound) for Ever Works platform",
5
5
  "author": {
6
6
  "name": "Ever Co. LTD",
@@ -32,7 +32,7 @@
32
32
  "test:watch": "vitest"
33
33
  },
34
34
  "dependencies": {
35
- "@ever-works/plugin": "^1.2.4",
35
+ "@ever-works/plugin": "^1.2.5",
36
36
  "form-data": "^4.0.1",
37
37
  "mailgun.js": "^10.2.3"
38
38
  },
@@ -40,7 +40,7 @@
40
40
  "@types/node": "^26.6.4",
41
41
  "tsup": "^8.0.0",
42
42
  "typescript": "^5.9.3",
43
- "vitest": "^4.1.0"
43
+ "vitest": "^5.0.5"
44
44
  },
45
45
  "everworks": {
46
46
  "plugin": {
@@ -102,8 +102,8 @@
102
102
  "url": "https://github.com/ever-works/ever-works/issues"
103
103
  },
104
104
  "everworksRelease": {
105
- "contentHash": "sha256:d90bc7b63c624d62898deed732996c7b1d8149cc509c50581097d48521dcfb9a",
106
- "commit": "61b8320b4ad7d30bf5161acf7ee13b250a6172b9",
107
- "source": "https://github.com/ever-works/ever-works/tree/61b8320b4ad7d30bf5161acf7ee13b250a6172b9/packages/plugins/mailgun"
105
+ "contentHash": "sha256:67b3989e2cd280ad55dd8d6de81ecf313d5dcfad08c9be4456db4bfa92cddb6f",
106
+ "commit": "19905c70cf05be3bc523db030597046067cd322a",
107
+ "source": "https://github.com/ever-works/ever-works/tree/19905c70cf05be3bc523db030597046067cd322a/packages/plugins/mailgun"
108
108
  }
109
109
  }