fix: use internal mailcow relay (port 25) for all SMTP transports

SMTP auth on port 587 was broken across all modules due to stale
credentials. Since rspace is on the mailcow Docker network, all 6
SMTP transports now use unauthenticated relay on port 25 when the
host is the internal postfix container. Fixes emails for: payment
receipts, space invitations, inbox approvals, agent notifications,
scheduled emails, and publication sharing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-03 15:12:42 -07:00
parent 4919ca1021
commit 42546c9a63
6 changed files with 57 additions and 38 deletions

View File

@ -18,11 +18,13 @@ async function getSmtpTransport() {
try {
const nodemailer = await import("nodemailer");
const createTransport = (nodemailer as any).default?.createTransport || nodemailer.createTransport;
const isInternal = SMTP_HOST.includes('mailcow') || SMTP_HOST.includes('postfix');
_transport = createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined,
port: isInternal ? 25 : SMTP_PORT,
secure: !isInternal && SMTP_PORT === 465,
...(isInternal ? {} : SMTP_USER ? { auth: { user: SMTP_USER, pass: SMTP_PASS } } : {}),
tls: { rejectUnauthorized: false },
});
return _transport;
} catch (e) {

View File

@ -50,13 +50,15 @@ async function getSmtpTransport() {
try {
const nodemailer = await import("nodemailer");
const createTransport = (nodemailer as any).default?.createTransport || nodemailer.createTransport;
const isInternal = SMTP_HOST.includes('mailcow') || SMTP_HOST.includes('postfix');
_smtpTransport = createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined,
port: isInternal ? 25 : SMTP_PORT,
secure: !isInternal && SMTP_PORT === 465,
...(isInternal ? {} : SMTP_USER ? { auth: { user: SMTP_USER, pass: SMTP_PASS } } : {}),
tls: { rejectUnauthorized: false },
});
console.log(`[Inbox] SMTP transport configured: ${SMTP_HOST}:${SMTP_PORT}`);
console.log(`[Inbox] SMTP transport configured: ${SMTP_HOST}:${isInternal ? 25 : SMTP_PORT}`);
return _smtpTransport;
} catch (e) {
console.error("[Inbox] Failed to create SMTP transport:", e);

View File

@ -29,15 +29,19 @@ let _smtpTransport: Transporter | null = null;
function getSmtpTransport(): Transporter | null {
if (_smtpTransport) return _smtpTransport;
if (!process.env.SMTP_PASS) return null;
const host = process.env.SMTP_HOST || "mail.rmail.online";
const isInternal = host.includes('mailcow') || host.includes('postfix');
if (!process.env.SMTP_PASS && !isInternal) return null;
_smtpTransport = createTransport({
host: process.env.SMTP_HOST || "mail.rmail.online",
port: Number(process.env.SMTP_PORT) || 587,
secure: Number(process.env.SMTP_PORT) === 465,
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS,
},
host,
port: isInternal ? 25 : (Number(process.env.SMTP_PORT) || 587),
secure: !isInternal && Number(process.env.SMTP_PORT) === 465,
...(isInternal ? {} : {
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS!,
},
}),
tls: { rejectUnauthorized: false },
});
return _smtpTransport;

View File

@ -52,15 +52,19 @@ let _smtpTransport: Transporter | null = null;
function getSmtpTransport(): Transporter | null {
if (_smtpTransport) return _smtpTransport;
if (!process.env.SMTP_PASS) return null;
const host = process.env.SMTP_HOST || "mail.rmail.online";
const isInternal = host.includes('mailcow') || host.includes('postfix');
if (!process.env.SMTP_PASS && !isInternal) return null;
_smtpTransport = createTransport({
host: process.env.SMTP_HOST || "mail.rmail.online",
port: Number(process.env.SMTP_PORT) || 587,
secure: Number(process.env.SMTP_PORT) === 465,
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS,
},
host,
port: isInternal ? 25 : (Number(process.env.SMTP_PORT) || 587),
secure: !isInternal && Number(process.env.SMTP_PORT) === 465,
...(isInternal ? {} : {
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS!,
},
}),
tls: { rejectUnauthorized: false },
});
return _smtpTransport;

View File

@ -43,15 +43,16 @@ let _smtpTransport: any = null;
async function getSmtpTransport() {
if (_smtpTransport) return _smtpTransport;
if (!SMTP_PASS) return null;
const isInternal = SMTP_HOST.includes('mailcow') || SMTP_HOST.includes('postfix');
if (!SMTP_PASS && !isInternal) return null;
try {
const nodemailer = await import("nodemailer");
const createTransport = (nodemailer as any).default?.createTransport || nodemailer.createTransport;
_smtpTransport = createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined,
port: isInternal ? 25 : SMTP_PORT,
secure: !isInternal && SMTP_PORT === 465,
...(isInternal ? {} : SMTP_USER ? { auth: { user: SMTP_USER, pass: SMTP_PASS } } : {}),
tls: { rejectUnauthorized: false },
});
console.log("[email] SMTP transport configured");

View File

@ -2097,17 +2097,23 @@ spaces.post("/:slug/copy-shapes", async (c) => {
let inviteTransport: Transporter | null = null;
if (process.env.SMTP_PASS) {
inviteTransport = createTransport({
host: process.env.SMTP_HOST || "mail.rmail.online",
port: Number(process.env.SMTP_PORT) || 587,
secure: Number(process.env.SMTP_PORT) === 465,
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS,
},
tls: { rejectUnauthorized: false },
});
{
const host = process.env.SMTP_HOST || "mail.rmail.online";
const isInternal = host.includes('mailcow') || host.includes('postfix');
if (process.env.SMTP_PASS || isInternal) {
inviteTransport = createTransport({
host,
port: isInternal ? 25 : (Number(process.env.SMTP_PORT) || 587),
secure: !isInternal && Number(process.env.SMTP_PORT) === 465,
...(isInternal ? {} : {
auth: {
user: process.env.SMTP_USER || "noreply@rmail.online",
pass: process.env.SMTP_PASS!,
},
}),
tls: { rejectUnauthorized: false },
});
}
}
// ── Enhanced invite by email (with token + role) ──