fix: let a deployment name itself in the SMTP greeting
A real deployment could not send mail through an IP-authorized relay. Every attempt surfaced as MAIL_PROVIDER_UNAVAILABLE while the relay was healthy and swaks sent from the same machine. The cause was the greeting, and it is generic rather than tied to any provider. An SMTP client identifies itself in EHLO, and with nothing configured Nodemailer derives that from the machine hostname — substituting the address literal [127.0.0.1] whenever the hostname is not fully qualified, which is the normal state of a container. The relay closed the connection on it with a 421 before any message was offered, which is indistinguishable from the relay being down. EKKLESIA_SMTP_HELO_NAME now lets the deployment say what to greet as. It is validated as a fully qualified domain name — no scheme, port, brackets, whitespace, or control characters, and at least one dot, since a single unqualified label is exactly the input Nodemailer already discards. It is not a credential: it travels in the clear on every connection, so Doctor prints it where an operator can compare it against what the relay expects, and Doctor's probe performs the same greeting the runtime does. Absent preserves Nodemailer's behaviour exactly. It is deliberately not defaulted to the sender's domain: a machine whose hostname is already a valid FQDN may be greeting correctly, and some relays check that against reverse DNS, so replacing it silently would trade one deployment's bug for another's. The regression guard is a raw SMTP server that refuses any greeting but the expected one, because smtp-server exposes no EHLO hook and its earliest hook is never reached by a send-nothing probe. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
450d65116f
commit
3563549e86
@@ -161,15 +161,44 @@ describe("outbound email stays server-only", () => {
|
||||
});
|
||||
|
||||
it("hardcodes no provider hostname in Core", async () => {
|
||||
// Google Workspace is a deployment target, not the architecture.
|
||||
// Google Workspace is a deployment target, not the architecture. It is the deployment that
|
||||
// exposed the generic `EHLO` defect the HELO-name setting fixes, and it is named nowhere the
|
||||
// code could come to depend on it.
|
||||
for (const file of await readTextFiles("core/src/mail")) {
|
||||
expect(file.text.toLowerCase(), file.path).not.toContain("gmail");
|
||||
expect(file.text.toLowerCase(), file.path).not.toContain("smtp-relay");
|
||||
expect(file.text.toLowerCase(), file.path).not.toContain("resend");
|
||||
expect(file.text.toLowerCase(), file.path).not.toContain("brevo");
|
||||
const text = file.text.toLowerCase();
|
||||
for (const forbidden of [
|
||||
"gmail",
|
||||
"smtp-relay",
|
||||
"googlemail",
|
||||
"google.com",
|
||||
"outlook",
|
||||
"office365",
|
||||
"sendgrid",
|
||||
"mailgun",
|
||||
"postmark",
|
||||
"resend",
|
||||
"brevo",
|
||||
]) {
|
||||
expect(text, `${file.path} names ${forbidden}`).not.toContain(forbidden);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults the SMTP client identity to nothing, so no deployment's name is baked in", async () => {
|
||||
const config = await readFile(path.join(repositoryRoot, "core/src/mail/config.ts"), "utf8");
|
||||
const provider = await readFile(
|
||||
path.join(repositoryRoot, "core/src/mail/smtp-provider.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// The value that fixed a real deployment is configuration, never a constant. A fallback literal
|
||||
// here would be one church's domain compiled into every installation.
|
||||
expect(config).not.toContain("riverofglorymn");
|
||||
expect(provider).not.toContain("riverofglorymn");
|
||||
// The provider passes it through and supplies nothing of its own when it is absent.
|
||||
expect(provider).toContain("config.heloName === undefined ? {} : { name: config.heloName }");
|
||||
});
|
||||
|
||||
it("never disables TLS certificate verification", async () => {
|
||||
for (const file of await readTextFiles("core/src/mail")) {
|
||||
expect(file.text, file.path).not.toContain("rejectUnauthorized");
|
||||
|
||||
Reference in New Issue
Block a user