@ouro.bot/cli 0.1.0-alpha.469 → 0.1.0-alpha.470
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/changelog.json +8 -0
- package/dist/heart/daemon/doctor.js +51 -0
- package/dist/heart/outlook/readers/mail.js +35 -1
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/core.js +88 -0
- package/dist/mailroom/outbound.js +191 -12
- package/dist/mailroom/reader.js +4 -0
- package/dist/outlook-ui/assets/{index-BBM5EysT.js → index-BbOjyIms.js} +14 -14
- package/dist/outlook-ui/index.html +1 -1
- package/dist/repertoire/tools-mail.js +17 -6
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.470",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Agent Mail native sending now enforces autonomy policy, confirmation fallback, recipient/rate limits, delegated send-as-human refusal, audit records, and kill switch state.",
|
|
8
|
+
"Outbound Mail now records provider submission and delivery state through ACS/Event Grid while keeping status summaries and Outlook audit output body-safe.",
|
|
9
|
+
"Mail recovery docs and `ouro doctor` now check mailbox identity, vault-held mail keys, hosted Blob reader coordinates, and autonomy kill switch state without treating generic vault item notes as machine contracts."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
4
12
|
{
|
|
5
13
|
"version": "0.1.0-alpha.469",
|
|
6
14
|
"changes": [
|
|
@@ -89,6 +89,16 @@ function numberField(record, key, fallback) {
|
|
|
89
89
|
const value = record?.[key];
|
|
90
90
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
91
91
|
}
|
|
92
|
+
function hasStringRecordValue(value) {
|
|
93
|
+
const record = asRecord(value);
|
|
94
|
+
return !!record && Object.values(record).some((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
95
|
+
}
|
|
96
|
+
function mailAutonomyDetail(mailroom) {
|
|
97
|
+
const policy = asRecord(mailroom?.autonomousSendPolicy);
|
|
98
|
+
const autonomy = policy?.enabled === true ? "autonomy enabled" : "autonomy disabled";
|
|
99
|
+
const killSwitch = policy?.killSwitch === true ? "kill switch on" : "kill switch off";
|
|
100
|
+
return `${autonomy}; ${killSwitch}`;
|
|
101
|
+
}
|
|
92
102
|
const SENSITIVE_CONFIG_KEYS = ["apiKey", "token", "secret", "password"];
|
|
93
103
|
function credentialKeyLeaks(raw) {
|
|
94
104
|
return SENSITIVE_CONFIG_KEYS.filter((key) => raw.includes(`"${key}"`));
|
|
@@ -256,6 +266,47 @@ async function checkSenses(deps) {
|
|
|
256
266
|
});
|
|
257
267
|
}
|
|
258
268
|
}
|
|
269
|
+
if (sense === "mail" && senseObj.enabled === true) {
|
|
270
|
+
const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
|
|
271
|
+
if (!runtimeConfig.ok) {
|
|
272
|
+
checks.push({
|
|
273
|
+
label: `${agentDir} mail config`,
|
|
274
|
+
status: "fail",
|
|
275
|
+
detail: `runtime config unavailable: ${runtimeConfig.error}`,
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const mailroom = asRecord(runtimeConfig.config.mailroom);
|
|
280
|
+
const workSubstrate = asRecord(runtimeConfig.config.workSubstrate);
|
|
281
|
+
const mailboxAddress = textField(mailroom, "mailboxAddress");
|
|
282
|
+
const hosted = textField(workSubstrate, "mode") === "hosted";
|
|
283
|
+
const azureAccountUrl = textField(mailroom, "azureAccountUrl");
|
|
284
|
+
const azureContainer = textField(mailroom, "azureContainer") || "mailroom";
|
|
285
|
+
const missing = [];
|
|
286
|
+
if (!mailboxAddress)
|
|
287
|
+
missing.push("mailroom.mailboxAddress");
|
|
288
|
+
if (!hasStringRecordValue(mailroom?.privateKeys))
|
|
289
|
+
missing.push("mailroom.privateKeys");
|
|
290
|
+
if (hosted && !azureAccountUrl)
|
|
291
|
+
missing.push("mailroom.azureAccountUrl for hosted Blob reader");
|
|
292
|
+
if (missing.length > 0) {
|
|
293
|
+
checks.push({
|
|
294
|
+
label: `${agentDir} mail config`,
|
|
295
|
+
status: "fail",
|
|
296
|
+
detail: `missing ${missing.join("/")}`,
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
checks.push({
|
|
301
|
+
label: `${agentDir} mail config`,
|
|
302
|
+
status: "pass",
|
|
303
|
+
detail: [
|
|
304
|
+
mailboxAddress,
|
|
305
|
+
hosted ? `hosted azure-blob ${azureAccountUrl}/${azureContainer}` : "local file Mailroom",
|
|
306
|
+
mailAutonomyDetail(mailroom),
|
|
307
|
+
].join("; "),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
259
310
|
}
|
|
260
311
|
}
|
|
261
312
|
if (checks.length === 0) {
|
|
@@ -88,7 +88,7 @@ function buildFolders(messages, outbound) {
|
|
|
88
88
|
{ id: "discarded", label: "Discarded", count: messages.filter((message) => message.placement === "discarded").length },
|
|
89
89
|
{ id: "quarantine", label: "Quarantine", count: messages.filter((message) => message.placement === "quarantine").length },
|
|
90
90
|
{ id: "draft", label: "Drafts", count: outbound.filter((record) => record.status === "draft").length },
|
|
91
|
-
{ id: "sent", label: "Sent", count: outbound.filter((record) => record.status
|
|
91
|
+
{ id: "sent", label: "Sent", count: outbound.filter((record) => record.status !== "draft").length },
|
|
92
92
|
{ id: "delegated", label: "Delegated", count: messages.filter((message) => message.compartmentKind === "delegated").length },
|
|
93
93
|
{ id: "native", label: "Native", count: messages.filter((message) => message.compartmentKind === "native").length },
|
|
94
94
|
];
|
|
@@ -135,6 +135,7 @@ function screenerCandidate(candidate) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
function outboundRecord(record) {
|
|
138
|
+
const policyDecision = record.policyDecision;
|
|
138
139
|
return {
|
|
139
140
|
id: record.id,
|
|
140
141
|
status: record.status,
|
|
@@ -150,6 +151,39 @@ function outboundRecord(record) {
|
|
|
150
151
|
createdAt: record.createdAt,
|
|
151
152
|
updatedAt: record.updatedAt,
|
|
152
153
|
sentAt: record.sentAt ?? null,
|
|
154
|
+
submittedAt: record.submittedAt ?? null,
|
|
155
|
+
acceptedAt: record.acceptedAt ?? null,
|
|
156
|
+
deliveredAt: record.deliveredAt ?? null,
|
|
157
|
+
failedAt: record.failedAt ?? null,
|
|
158
|
+
sendMode: record.sendMode ?? null,
|
|
159
|
+
policyDecision: policyDecision
|
|
160
|
+
? {
|
|
161
|
+
allowed: policyDecision.allowed,
|
|
162
|
+
mode: policyDecision.mode,
|
|
163
|
+
code: policyDecision.code,
|
|
164
|
+
reason: policyDecision.reason,
|
|
165
|
+
evaluatedAt: policyDecision.evaluatedAt,
|
|
166
|
+
recipients: policyDecision.recipients,
|
|
167
|
+
fallback: policyDecision.fallback,
|
|
168
|
+
policyId: policyDecision.policyId ?? null,
|
|
169
|
+
remainingSendsInWindow: policyDecision.remainingSendsInWindow ?? null,
|
|
170
|
+
}
|
|
171
|
+
: null,
|
|
172
|
+
provider: record.provider ?? null,
|
|
173
|
+
providerMessageId: record.providerMessageId ?? null,
|
|
174
|
+
providerRequestId: record.providerRequestId ?? null,
|
|
175
|
+
operationLocation: record.operationLocation ?? null,
|
|
176
|
+
deliveryEvents: (record.deliveryEvents ?? []).map((event) => ({
|
|
177
|
+
provider: event.provider,
|
|
178
|
+
providerEventId: event.providerEventId,
|
|
179
|
+
providerMessageId: event.providerMessageId,
|
|
180
|
+
outcome: event.outcome,
|
|
181
|
+
recipient: event.recipient ?? null,
|
|
182
|
+
occurredAt: event.occurredAt,
|
|
183
|
+
receivedAt: event.receivedAt,
|
|
184
|
+
bodySafeSummary: event.bodySafeSummary,
|
|
185
|
+
providerStatus: event.providerStatus ?? null,
|
|
186
|
+
})),
|
|
153
187
|
transport: record.transport ?? null,
|
|
154
188
|
reason: record.reason,
|
|
155
189
|
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildNativeMailAutonomyPolicy = buildNativeMailAutonomyPolicy;
|
|
37
|
+
exports.evaluateNativeMailSendPolicy = evaluateNativeMailSendPolicy;
|
|
38
|
+
exports.buildConfirmedMailSendDecision = buildConfirmedMailSendDecision;
|
|
39
|
+
const crypto = __importStar(require("node:crypto"));
|
|
40
|
+
const runtime_1 = require("../nerves/runtime");
|
|
41
|
+
const core_1 = require("./core");
|
|
42
|
+
function stableJson(value) {
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const record = value;
|
|
47
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
48
|
+
}
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
}
|
|
51
|
+
function safeAddressPart(value) {
|
|
52
|
+
return value
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
55
|
+
.replace(/^-+|-+$/g, "");
|
|
56
|
+
}
|
|
57
|
+
function normalizeDomain(value) {
|
|
58
|
+
return value.trim().toLowerCase().replace(/^@/, "");
|
|
59
|
+
}
|
|
60
|
+
function autonomyPolicyId(input) {
|
|
61
|
+
return `mail_auto_${crypto.createHash("sha256").update(stableJson(input)).digest("hex").slice(0, 16)}`;
|
|
62
|
+
}
|
|
63
|
+
function recipientsForDecision(record) {
|
|
64
|
+
return [...record.to, ...record.cc, ...record.bcc].map(core_1.normalizeMailAddress);
|
|
65
|
+
}
|
|
66
|
+
function decision(input) {
|
|
67
|
+
return { schemaVersion: 1, ...input };
|
|
68
|
+
}
|
|
69
|
+
function recipientDomain(recipient) {
|
|
70
|
+
return recipient.slice(recipient.indexOf("@") + 1).toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
function isRecipientAllowed(policy, recipient) {
|
|
73
|
+
return policy.allowedRecipients.includes(recipient) || policy.allowedDomains.includes(recipientDomain(recipient));
|
|
74
|
+
}
|
|
75
|
+
function autonomousSentAt(record) {
|
|
76
|
+
if (record.status !== "sent" || record.sendMode !== "autonomous")
|
|
77
|
+
return null;
|
|
78
|
+
return record.sentAt ?? record.updatedAt;
|
|
79
|
+
}
|
|
80
|
+
function countRecentAutonomousSends(input) {
|
|
81
|
+
const startsAt = input.nowMs - input.windowMs;
|
|
82
|
+
return input.recentOutbound.filter((record) => {
|
|
83
|
+
const sentAt = autonomousSentAt(record);
|
|
84
|
+
if (!sentAt)
|
|
85
|
+
return false;
|
|
86
|
+
const sentMs = Date.parse(sentAt);
|
|
87
|
+
return Number.isFinite(sentMs) && sentMs >= startsAt && sentMs <= input.nowMs;
|
|
88
|
+
}).length;
|
|
89
|
+
}
|
|
90
|
+
function buildNativeMailAutonomyPolicy(input) {
|
|
91
|
+
const normalized = {
|
|
92
|
+
agentId: safeAddressPart(input.agentId) || "agent",
|
|
93
|
+
mailboxAddress: (0, core_1.normalizeMailAddress)(input.mailboxAddress),
|
|
94
|
+
enabled: input.enabled,
|
|
95
|
+
killSwitch: input.killSwitch,
|
|
96
|
+
allowedRecipients: [...new Set((input.allowedRecipients ?? []).map(core_1.normalizeMailAddress))].sort(),
|
|
97
|
+
allowedDomains: [...new Set((input.allowedDomains ?? []).map(normalizeDomain).filter(Boolean))].sort(),
|
|
98
|
+
maxRecipientsPerMessage: Math.max(1, Math.floor(input.maxRecipientsPerMessage)),
|
|
99
|
+
rateLimit: {
|
|
100
|
+
maxSends: Math.max(0, Math.floor(input.rateLimit.maxSends)),
|
|
101
|
+
windowMs: Math.max(1, Math.floor(input.rateLimit.windowMs)),
|
|
102
|
+
},
|
|
103
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
104
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
105
|
+
...(input.updatedAt ? { updatedAt: input.updatedAt } : {}),
|
|
106
|
+
};
|
|
107
|
+
const policy = {
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
policyId: autonomyPolicyId(normalized),
|
|
110
|
+
...normalized,
|
|
111
|
+
};
|
|
112
|
+
(0, runtime_1.emitNervesEvent)({
|
|
113
|
+
component: "senses",
|
|
114
|
+
event: "senses.mail_native_autonomy_policy_built",
|
|
115
|
+
message: "native mail autonomy policy built",
|
|
116
|
+
meta: { agentId: policy.agentId, policyId: policy.policyId, enabled: policy.enabled, killSwitch: policy.killSwitch },
|
|
117
|
+
});
|
|
118
|
+
return policy;
|
|
119
|
+
}
|
|
120
|
+
function evaluateNativeMailSendPolicy(input) {
|
|
121
|
+
const now = input.now ?? new Date();
|
|
122
|
+
const evaluatedAt = now.toISOString();
|
|
123
|
+
const recipients = recipientsForDecision(input.draft);
|
|
124
|
+
const policyId = input.policy.policyId;
|
|
125
|
+
const blocked = (code, reason, mode = "blocked", fallback = "none") => decision({
|
|
126
|
+
allowed: false,
|
|
127
|
+
mode,
|
|
128
|
+
code,
|
|
129
|
+
reason,
|
|
130
|
+
evaluatedAt,
|
|
131
|
+
recipients,
|
|
132
|
+
fallback,
|
|
133
|
+
policyId,
|
|
134
|
+
});
|
|
135
|
+
if (input.draft.status !== "draft") {
|
|
136
|
+
return blocked("draft-not-sendable", `Draft ${input.draft.id} is already ${input.draft.status}`);
|
|
137
|
+
}
|
|
138
|
+
if (input.draft.mailboxRole === "delegated-human-mailbox" || input.draft.ownerEmail || input.draft.source || input.draft.sendAuthority !== "agent-native") {
|
|
139
|
+
return blocked("delegated-send-as-human-not-authorized", "Delegated human mail does not grant send-as-human authority");
|
|
140
|
+
}
|
|
141
|
+
if (safeAddressPart(input.draft.agentId) !== input.policy.agentId) {
|
|
142
|
+
return blocked("agent-mismatch", `Draft belongs to ${input.draft.agentId}, not ${input.policy.agentId}`);
|
|
143
|
+
}
|
|
144
|
+
if ((0, core_1.normalizeMailAddress)(input.draft.from) !== input.policy.mailboxAddress) {
|
|
145
|
+
return blocked("native-mailbox-mismatch", `${input.draft.from} is not the native mailbox ${input.policy.mailboxAddress}`);
|
|
146
|
+
}
|
|
147
|
+
if (!input.policy.enabled) {
|
|
148
|
+
return blocked("autonomy-policy-disabled", "Autonomous native-agent mail policy is disabled", "confirmation-required", "CONFIRM_SEND");
|
|
149
|
+
}
|
|
150
|
+
if (input.policy.killSwitch) {
|
|
151
|
+
return blocked("autonomy-kill-switch", "Autonomous native-agent mail kill switch is enabled", "confirmation-required", "CONFIRM_SEND");
|
|
152
|
+
}
|
|
153
|
+
if (recipients.length > input.policy.maxRecipientsPerMessage) {
|
|
154
|
+
return blocked("recipient-limit-exceeded", `Autonomous native-agent mail is limited to ${input.policy.maxRecipientsPerMessage} recipient(s)`);
|
|
155
|
+
}
|
|
156
|
+
const unallowed = recipients.find((recipient) => !isRecipientAllowed(input.policy, recipient));
|
|
157
|
+
if (unallowed) {
|
|
158
|
+
return blocked("recipient-not-allowed", `${unallowed} is not allowed for autonomous native-agent mail`, "confirmation-required", "CONFIRM_SEND");
|
|
159
|
+
}
|
|
160
|
+
const recentCount = countRecentAutonomousSends({
|
|
161
|
+
recentOutbound: input.recentOutbound,
|
|
162
|
+
nowMs: now.getTime(),
|
|
163
|
+
windowMs: input.policy.rateLimit.windowMs,
|
|
164
|
+
});
|
|
165
|
+
if (recentCount >= input.policy.rateLimit.maxSends) {
|
|
166
|
+
return blocked("autonomous-rate-limit", "Autonomous native-agent mail rate limit is exhausted");
|
|
167
|
+
}
|
|
168
|
+
const allowed = decision({
|
|
169
|
+
allowed: true,
|
|
170
|
+
mode: "autonomous",
|
|
171
|
+
code: "allowed",
|
|
172
|
+
reason: "Autonomous native-agent mail policy allowed this send",
|
|
173
|
+
evaluatedAt,
|
|
174
|
+
recipients,
|
|
175
|
+
fallback: "none",
|
|
176
|
+
policyId,
|
|
177
|
+
remainingSendsInWindow: Math.max(0, input.policy.rateLimit.maxSends - recentCount - 1),
|
|
178
|
+
});
|
|
179
|
+
(0, runtime_1.emitNervesEvent)({
|
|
180
|
+
component: "senses",
|
|
181
|
+
event: "senses.mail_native_autonomy_allowed",
|
|
182
|
+
message: "native mail autonomy policy allowed send",
|
|
183
|
+
meta: { agentId: input.policy.agentId, policyId, recipientCount: recipients.length },
|
|
184
|
+
});
|
|
185
|
+
return allowed;
|
|
186
|
+
}
|
|
187
|
+
function buildConfirmedMailSendDecision(input) {
|
|
188
|
+
const decisionValue = decision({
|
|
189
|
+
allowed: true,
|
|
190
|
+
mode: "confirmed",
|
|
191
|
+
code: "explicit-confirmation",
|
|
192
|
+
reason: "Explicit confirmation authorized this native-agent send",
|
|
193
|
+
evaluatedAt: (input.now ?? new Date()).toISOString(),
|
|
194
|
+
recipients: recipientsForDecision(input.draft),
|
|
195
|
+
fallback: "none",
|
|
196
|
+
...(input.policy ? { policyId: input.policy.policyId } : {}),
|
|
197
|
+
});
|
|
198
|
+
(0, runtime_1.emitNervesEvent)({
|
|
199
|
+
component: "senses",
|
|
200
|
+
event: "senses.mail_native_send_confirmed",
|
|
201
|
+
message: "native mail send confirmed",
|
|
202
|
+
meta: {
|
|
203
|
+
agentId: input.draft.agentId,
|
|
204
|
+
recipientCount: decisionValue.recipients.length,
|
|
205
|
+
...(input.policy ? { policyId: input.policy.policyId } : {}),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return decisionValue;
|
|
209
|
+
}
|
package/dist/mailroom/core.js
CHANGED
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.normalizeMailAddress = normalizeMailAddress;
|
|
37
|
+
exports.buildMailProviderSubmission = buildMailProviderSubmission;
|
|
38
|
+
exports.parseAcsEmailDeliveryReportEvent = parseAcsEmailDeliveryReportEvent;
|
|
39
|
+
exports.reconcileMailDeliveryEvent = reconcileMailDeliveryEvent;
|
|
37
40
|
exports.reverseEmailRoute = reverseEmailRoute;
|
|
38
41
|
exports.sourceAliasForOwner = sourceAliasForOwner;
|
|
39
42
|
exports.generateMailKeyPair = generateMailKeyPair;
|
|
@@ -79,6 +82,91 @@ function normalizeMailAddress(address) {
|
|
|
79
82
|
}
|
|
80
83
|
return normalized;
|
|
81
84
|
}
|
|
85
|
+
function buildMailProviderSubmission(input) {
|
|
86
|
+
return {
|
|
87
|
+
...input.draft,
|
|
88
|
+
status: "submitted",
|
|
89
|
+
provider: input.provider,
|
|
90
|
+
providerMessageId: input.providerMessageId,
|
|
91
|
+
...(input.providerRequestId ? { providerRequestId: input.providerRequestId } : {}),
|
|
92
|
+
...(input.operationLocation ? { operationLocation: input.operationLocation } : {}),
|
|
93
|
+
submittedAt: input.submittedAt,
|
|
94
|
+
updatedAt: input.submittedAt,
|
|
95
|
+
deliveryEvents: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function recordField(value, key) {
|
|
99
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
100
|
+
? value[key]
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
function stringField(value, key) {
|
|
104
|
+
const field = recordField(value, key);
|
|
105
|
+
return typeof field === "string" ? field : "";
|
|
106
|
+
}
|
|
107
|
+
function acsOutcome(status) {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case "Delivered": return "delivered";
|
|
110
|
+
case "Suppressed": return "suppressed";
|
|
111
|
+
case "Bounced": return "bounced";
|
|
112
|
+
case "Quarantined": return "quarantined";
|
|
113
|
+
case "FilteredSpam": return "spam-filtered";
|
|
114
|
+
case "Expanded": return "accepted";
|
|
115
|
+
case "Failed": return "failed";
|
|
116
|
+
default: throw new Error(`unsupported ACS delivery status: ${status || "unknown"}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseAcsEmailDeliveryReportEvent(event) {
|
|
120
|
+
const providerEventId = stringField(event, "id");
|
|
121
|
+
const eventType = stringField(event, "eventType");
|
|
122
|
+
const data = recordField(event, "data");
|
|
123
|
+
if (!providerEventId)
|
|
124
|
+
throw new Error("ACS delivery event is missing id");
|
|
125
|
+
if (eventType !== "Microsoft.Communication.EmailDeliveryReportReceived") {
|
|
126
|
+
throw new Error(`unsupported ACS event type: ${eventType || "unknown"}`);
|
|
127
|
+
}
|
|
128
|
+
const providerMessageId = stringField(data, "messageId");
|
|
129
|
+
const status = stringField(data, "status");
|
|
130
|
+
if (!providerMessageId)
|
|
131
|
+
throw new Error("ACS delivery event is missing messageId");
|
|
132
|
+
const recipient = stringField(data, "recipient");
|
|
133
|
+
const eventTime = stringField(event, "eventTime");
|
|
134
|
+
const occurredAt = stringField(data, "deliveryAttemptTimeStamp") || eventTime || new Date().toISOString();
|
|
135
|
+
const normalizedRecipient = recipient ? normalizeMailAddress(recipient) : "";
|
|
136
|
+
return {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
provider: "azure-communication-services",
|
|
139
|
+
providerEventId,
|
|
140
|
+
providerMessageId,
|
|
141
|
+
outcome: acsOutcome(status),
|
|
142
|
+
...(normalizedRecipient ? { recipient: normalizedRecipient } : {}),
|
|
143
|
+
occurredAt,
|
|
144
|
+
receivedAt: eventTime || occurredAt,
|
|
145
|
+
bodySafeSummary: `ACS delivery report ${status} for ${normalizedRecipient || "unknown recipient"}`,
|
|
146
|
+
providerStatus: status,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function reconcileMailDeliveryEvent(input) {
|
|
150
|
+
if (input.outbound.providerMessageId && input.outbound.providerMessageId !== input.event.providerMessageId) {
|
|
151
|
+
throw new Error("delivery event providerMessageId does not match outbound record");
|
|
152
|
+
}
|
|
153
|
+
const existingEvents = input.outbound.deliveryEvents ?? [];
|
|
154
|
+
if (existingEvents.some((event) => event.providerEventId === input.event.providerEventId)) {
|
|
155
|
+
return input.outbound;
|
|
156
|
+
}
|
|
157
|
+
const timestampKey = input.event.outcome === "delivered"
|
|
158
|
+
? "deliveredAt"
|
|
159
|
+
: input.event.outcome === "accepted"
|
|
160
|
+
? "acceptedAt"
|
|
161
|
+
: "failedAt";
|
|
162
|
+
return {
|
|
163
|
+
...input.outbound,
|
|
164
|
+
status: input.event.outcome,
|
|
165
|
+
updatedAt: input.event.occurredAt,
|
|
166
|
+
deliveryEvents: [...existingEvents, input.event],
|
|
167
|
+
[timestampKey]: input.event.occurredAt,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
82
170
|
function safeAddressPart(value) {
|
|
83
171
|
return value
|
|
84
172
|
.toLowerCase()
|