Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/api/src/mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ class RoutingMailer implements Mailer {
cfg.smtp.user && cfg.smtp.password
? { user: cfg.smtp.user, pass: cfg.smtp.password }
: undefined,
// Nodemailer's default connection timeout is 2 MINUTES — longer
// than the platform gateway's ~60s, so a misconfigured host (wrong
// hostname, filtered port) surfaced as an opaque 504 instead of
// our clean ETIMEDOUT/ECONNREFUSED error, and a real send (partner
// invite, signup) could pin its request just as long. Fail fast:
// a healthy SMTP handshake completes in single-digit seconds.
connectionTimeout: 10_000,
greetingTimeout: 10_000,
socketTimeout: 30_000,
});
await transporter.sendMail({
from,
Expand Down
7 changes: 4 additions & 3 deletions apps/portal/src/pages/admin/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ function MailSection() {
type="password"
value={smtpPass}
onChange={(e) => setSmtpPass(e.target.value)}
placeholder={hasStoredSmtpPassword ? '•••••••• (leave blank to keep)' : ''}
placeholder={hasStoredSmtpPassword ? 'Saved — leave blank to keep, type to replace' : ''}
/>
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: theme.textMuted, marginBottom: 16 }}>
Expand All @@ -904,7 +904,7 @@ function MailSection() {
type="password"
value={pmToken}
onChange={(e) => setPmToken(e.target.value)}
placeholder={hasStoredPmToken ? '•••••••• (leave blank to keep)' : ''}
placeholder={hasStoredPmToken ? 'Saved — leave blank to keep, type to replace' : ''}
/>
</div>
<div style={{ marginBottom: 16 }}>
Expand Down Expand Up @@ -933,7 +933,8 @@ function MailSection() {
{testResult && (
<div style={{ color: testResult.ok ? theme.success : theme.danger, fontSize: 13, marginTop: 10 }}>
{testResult.text}
{!testResult.ok && ' — unsaved changes aren’t tested; save first, then retry.'}
{!testResult.ok &&
' — check the host/port/credentials above (a connection timeout usually means a wrong hostname or filtered port). The test uses the last SAVED settings, so save any edits before retrying.'}
</div>
)}
</Card>
Expand Down
Loading