graph: use replyTo instead of from to avoid ErrorSendAsDenied

This commit is contained in:
2026-08-22 22:26:47 +02:00
parent cef7f46718
commit 08bd00f01f
16 changed files with 245 additions and 480 deletions
+25 -18
View File
@@ -89,10 +89,18 @@ import { getSiteIntegrations } from '../payload/index.js';
sent: false
};
}
// From-display comes from the panel; falls back to the caller's from.
// The panel's from-address is used as Reply-To, NOT as the message From.
//
// Why: app-only Graph sends from GRAPH_SENDER's mailbox. If we also set a
// `from` that differs from that mailbox, Exchange demands "Send As"
// permission on it and rejects with ErrorSendAsDenied otherwise. So we
// never override `from` — Graph stamps the mail as GRAPH_SENDER (the
// mailbox we legitimately own) — and route replies to the panel address
// via Reply-To. Recipients see the mail from forms@… but replying reaches
// the real destination. No Send-As needed.
const panel = await getSiteIntegrations(payload);
const fromAddress = panel.smtpFromAddress || undefined;
const fromName = panel.smtpFromName || undefined;
const replyToAddress = panel.smtpFromAddress || undefined;
const replyToName = panel.smtpFromName || undefined;
const to = toRecipients(message.to);
if (to.length === 0) {
payload.logger.error('[ipal] Email not sent: no valid recipient.');
@@ -104,6 +112,17 @@ import { getSiteIntegrations } from '../payload/index.js';
// Graph accepts either HTML or Text; Payload gives us html and/or text.
const isHtml = typeof message.html === 'string' && message.html.length > 0;
const content = isHtml ? String(message.html) : String(message.text ?? '');
// Reply-To: prefer whatever the caller set; otherwise the panel address.
const replyTo = message.replyTo ? toRecipients(message.replyTo) : replyToAddress ? [
{
emailAddress: {
address: replyToAddress,
...replyToName ? {
name: replyToName
} : {}
}
}
] : [];
const graphMessage = {
body: {
content,
@@ -117,21 +136,9 @@ import { getSiteIntegrations } from '../payload/index.js';
...message.bcc ? {
bccRecipients: toRecipients(message.bcc)
} : {},
// from is only honoured if the app has Send-As for that address; when
// it's the shared mailbox itself, omit it and Graph uses the sender.
...fromAddress ? {
from: {
emailAddress: {
address: fromAddress,
...fromName ? {
name: fromName
} : {}
}
}
} : {},
// replyTo lets the recipient reply to the real submitter if the caller set it.
...message.replyTo ? {
replyTo: toRecipients(message.replyTo)
// NO `from` — Graph uses GRAPH_SENDER's own mailbox, so no Send-As.
...replyTo.length > 0 ? {
replyTo
} : {}
};
try {
File diff suppressed because one or more lines are too long