feat: add --outgoing-ignore-tls to keep the relay off STARTTLS (#377)
The relay connection only had `secure`, so with `secure: false` nodemailer still upgraded to STARTTLS whenever the server advertised it, and there was no way to talk cleartext to such a server. Pass nodemailer's `ignoreTLS` through as `ignoreTLS` on the relay config, wired to --outgoing-ignore-tls / MAILDEV_OUTGOING_IGNORE_TLS / `outgoingIgnoreTls`.
This commit is contained in:
1 parent
a9a83f64f1
commit
fd864f1379
13 files changed
+124
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
'@maildev/smtp': patch
|
||||||
|
'maildev': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `--outgoing-ignore-tls` / `MAILDEV_OUTGOING_IGNORE_TLS` (`outgoingIgnoreTls`,
|
||||||
|
`ignoreTLS` on the relay config) to keep the outgoing relay connection on
|
||||||
|
cleartext even when the server advertises STARTTLS (#377).
|
||||||
@@ -68,6 +68,7 @@ const maildev = new MailDev({
|
|||||||
outgoingUser: 'user',
|
outgoingUser: 'user',
|
||||||
outgoingPass: 'pass',
|
outgoingPass: 'pass',
|
||||||
outgoingSecure: true,
|
outgoingSecure: true,
|
||||||
|
outgoingIgnoreTls: false, // Never upgrade the relay connection with STARTTLS
|
||||||
autoRelay: true, // Auto-forward emails
|
autoRelay: true, // Auto-forward emails
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ Usage: maildev [options]
|
|||||||
| `--outgoing-user <user>` | `MAILDEV_OUTGOING_USER` | SMTP user for outgoing mail |
|
| `--outgoing-user <user>` | `MAILDEV_OUTGOING_USER` | SMTP user for outgoing mail |
|
||||||
| `--outgoing-pass <password>` | `MAILDEV_OUTGOING_PASS` | SMTP password for outgoing mail |
|
| `--outgoing-pass <password>` | `MAILDEV_OUTGOING_PASS` | SMTP password for outgoing mail |
|
||||||
| `--outgoing-secure` | `MAILDEV_OUTGOING_SECURE` | Use SMTP SSL for outgoing mail |
|
| `--outgoing-secure` | `MAILDEV_OUTGOING_SECURE` | Use SMTP SSL for outgoing mail |
|
||||||
|
| `--outgoing-ignore-tls` | `MAILDEV_OUTGOING_IGNORE_TLS` | Never use STARTTLS for outgoing mail, even if the server offers it |
|
||||||
| `--auto-relay [email]` | `MAILDEV_AUTO_RELAY` | Use auto-relay mode. Optional relay email address |
|
| `--auto-relay [email]` | `MAILDEV_AUTO_RELAY` | Use auto-relay mode. Optional relay email address |
|
||||||
| `--auto-relay-rules <file>` | `MAILDEV_AUTO_RELAY_RULES` | Filter rules for auto relay mode |
|
| `--auto-relay-rules <file>` | `MAILDEV_AUTO_RELAY_RULES` | Filter rules for auto relay mode |
|
||||||
| `--web-user <user>` | `MAILDEV_WEB_USER` | HTTP user for GUI |
|
| `--web-user <user>` | `MAILDEV_WEB_USER` | HTTP user for GUI |
|
||||||
@@ -176,6 +177,9 @@ maildev --outgoing-host smtp.gmail.com \
|
|||||||
--outgoing-pass '<pass>'
|
--outgoing-pass '<pass>'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
MailDev upgrades a non-secure relay connection with STARTTLS whenever the server offers it. Add
|
||||||
|
`--outgoing-ignore-tls` to keep it on cleartext, e.g. for a relay on port 587 that does not support TLS.
|
||||||
|
|
||||||
### Auto Relay Mode
|
### Auto Relay Mode
|
||||||
|
|
||||||
Enabling the auto relay mode will automatically send each email to its recipient
|
Enabling the auto relay mode will automatically send each email to its recipient
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ describe('loadEnvConfig', () => {
|
|||||||
expect(config.outgoingPass).toBe('relay_pass')
|
expect(config.outgoingPass).toBe('relay_pass')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse MAILDEV_OUTGOING_IGNORE_TLS as boolean', () => {
|
||||||
|
process.env.MAILDEV_OUTGOING_IGNORE_TLS = 'true'
|
||||||
|
expect(loadEnvConfig().outgoingIgnoreTls).toBe(true)
|
||||||
|
|
||||||
|
process.env.MAILDEV_OUTGOING_IGNORE_TLS = 'false'
|
||||||
|
expect(loadEnvConfig().outgoingIgnoreTls).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('should load MAILDEV_MAIL_DIRECTORY', () => {
|
it('should load MAILDEV_MAIL_DIRECTORY', () => {
|
||||||
process.env.MAILDEV_MAIL_DIRECTORY = '/var/mail'
|
process.env.MAILDEV_MAIL_DIRECTORY = '/var/mail'
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ describe('resolveConfig', () => {
|
|||||||
outgoingUser: 'relay_user',
|
outgoingUser: 'relay_user',
|
||||||
outgoingPass: 'relay_pass',
|
outgoingPass: 'relay_pass',
|
||||||
outgoingSecure: true,
|
outgoingSecure: true,
|
||||||
|
outgoingIgnoreTls: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(config.outgoingHost).toBe('smtp.example.com')
|
expect(config.outgoingHost).toBe('smtp.example.com')
|
||||||
@@ -167,6 +168,7 @@ describe('resolveConfig', () => {
|
|||||||
expect(config.outgoingUser).toBe('relay_user')
|
expect(config.outgoingUser).toBe('relay_user')
|
||||||
expect(config.outgoingPass).toBe('relay_pass')
|
expect(config.outgoingPass).toBe('relay_pass')
|
||||||
expect(config.outgoingSecure).toBe(true)
|
expect(config.outgoingSecure).toBe(true)
|
||||||
|
expect(config.outgoingIgnoreTls).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle mail directory option', async () => {
|
it('should handle mail directory option', async () => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const NUMBER_VARS = new Set([
|
|||||||
const BOOLEAN_VARS = new Set([
|
const BOOLEAN_VARS = new Set([
|
||||||
'MAILDEV_INCOMING_SECURE',
|
'MAILDEV_INCOMING_SECURE',
|
||||||
'MAILDEV_OUTGOING_SECURE',
|
'MAILDEV_OUTGOING_SECURE',
|
||||||
|
'MAILDEV_OUTGOING_IGNORE_TLS',
|
||||||
'MAILDEV_HTTPS',
|
'MAILDEV_HTTPS',
|
||||||
'MAILDEV_DISABLE_WEB',
|
'MAILDEV_DISABLE_WEB',
|
||||||
'MAILDEV_MCP',
|
'MAILDEV_MCP',
|
||||||
@@ -149,6 +150,7 @@ function envVarToConfigKey(envVar: string): keyof PartialMailDevConfig | null {
|
|||||||
verbose: 'verbose',
|
verbose: 'verbose',
|
||||||
silent: 'silent',
|
silent: 'silent',
|
||||||
mcp: 'mcp',
|
mcp: 'mcp',
|
||||||
|
outgoingIgnoreTls: 'outgoingIgnoreTls',
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapping[camelCase] || null
|
return mapping[camelCase] || null
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function cliOptionsToConfig(options: CLIOptions): PartialMailDevConfig {
|
|||||||
if (options.disableWeb !== undefined) config.disableWeb = options.disableWeb
|
if (options.disableWeb !== undefined) config.disableWeb = options.disableWeb
|
||||||
if (options.https !== undefined) config.https = options.https
|
if (options.https !== undefined) config.https = options.https
|
||||||
if (options.outgoingSecure !== undefined) config.outgoingSecure = options.outgoingSecure
|
if (options.outgoingSecure !== undefined) config.outgoingSecure = options.outgoingSecure
|
||||||
|
if (options.outgoingIgnoreTls !== undefined) config.outgoingIgnoreTls = options.outgoingIgnoreTls
|
||||||
if (options.verbose !== undefined) config.verbose = options.verbose
|
if (options.verbose !== undefined) config.verbose = options.verbose
|
||||||
if (options.silent !== undefined) config.silent = options.silent
|
if (options.silent !== undefined) config.silent = options.silent
|
||||||
if (options.logMailContents !== undefined)
|
if (options.logMailContents !== undefined)
|
||||||
|
|||||||
@@ -225,6 +225,12 @@ export interface MailDevConfig {
|
|||||||
*/
|
*/
|
||||||
outgoingSecure?: boolean
|
outgoingSecure?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Never upgrade the outgoing relay connection with STARTTLS
|
||||||
|
* CLI: --outgoing-ignore-tls
|
||||||
|
*/
|
||||||
|
outgoingIgnoreTls?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable auto-relay mode
|
* Enable auto-relay mode
|
||||||
* CLI: --auto-relay [email]
|
* CLI: --auto-relay [email]
|
||||||
@@ -330,6 +336,7 @@ export interface CLIOptions {
|
|||||||
outgoingUser?: string
|
outgoingUser?: string
|
||||||
outgoingPass?: string
|
outgoingPass?: string
|
||||||
outgoingSecure?: boolean
|
outgoingSecure?: boolean
|
||||||
|
outgoingIgnoreTls?: boolean
|
||||||
autoRelay?: boolean | string
|
autoRelay?: boolean | string
|
||||||
autoRelayRules?: string
|
autoRelayRules?: string
|
||||||
mailDirectory?: string
|
mailDirectory?: string
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ export function configureOptions(program: Command): void {
|
|||||||
'--outgoing-secure',
|
'--outgoing-secure',
|
||||||
'Use TLS/SSL for outgoing relay'
|
'Use TLS/SSL for outgoing relay'
|
||||||
)
|
)
|
||||||
|
.option(
|
||||||
|
'--outgoing-ignore-tls',
|
||||||
|
'Never use STARTTLS for outgoing relay, even if the server offers it'
|
||||||
|
)
|
||||||
.option(
|
.option(
|
||||||
'--auto-relay [email]',
|
'--auto-relay [email]',
|
||||||
'Enable auto-relay mode (optionally specify recipient email)'
|
'Enable auto-relay mode (optionally specify recipient email)'
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ export class Orchestrator {
|
|||||||
if (this.config.outgoingSecure !== undefined) {
|
if (this.config.outgoingSecure !== undefined) {
|
||||||
config.secure = this.config.outgoingSecure
|
config.secure = this.config.outgoingSecure
|
||||||
}
|
}
|
||||||
|
if (this.config.outgoingIgnoreTls !== undefined) {
|
||||||
|
config.ignoreTLS = this.config.outgoingIgnoreTls
|
||||||
|
}
|
||||||
if (this.config.outgoingUser && this.config.outgoingPass) {
|
if (this.config.outgoingUser && this.config.outgoingPass) {
|
||||||
config.auth = {
|
config.auth = {
|
||||||
user: this.config.outgoingUser,
|
user: this.config.outgoingUser,
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { mkdtemp, rm } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import type { AddressInfo } from 'node:net'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
import { SMTPServer as SMTPServerLib } from 'smtp-server'
|
||||||
|
import { MemoryStorage } from '@maildev/core'
|
||||||
|
import { SMTPServer } from '../server.js'
|
||||||
|
|
||||||
|
describe('relay STARTTLS', () => {
|
||||||
|
let mailDir: string
|
||||||
|
let storage: MemoryStorage
|
||||||
|
let source: SMTPServer
|
||||||
|
let upstream: SMTPServerLib
|
||||||
|
let upstreamPort: number
|
||||||
|
// `session.secure` of every message the upstream server received
|
||||||
|
let deliveries: boolean[]
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mailDir = await mkdtemp(join(tmpdir(), 'maildev-relay-tls-'))
|
||||||
|
storage = new MemoryStorage()
|
||||||
|
await storage.initialize()
|
||||||
|
deliveries = []
|
||||||
|
|
||||||
|
// A plain-port relay that advertises STARTTLS, like a submission server on 587.
|
||||||
|
upstream = new SMTPServerLib({
|
||||||
|
authOptional: true,
|
||||||
|
logger: false,
|
||||||
|
onData(stream, session, callback) {
|
||||||
|
deliveries.push(session.secure)
|
||||||
|
stream.on('end', () => callback())
|
||||||
|
stream.resume()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await new Promise<void>((resolve) => upstream.listen(0, '127.0.0.1', () => resolve()))
|
||||||
|
upstreamPort = (upstream.server.address() as AddressInfo).port
|
||||||
|
|
||||||
|
source = new SMTPServer({ storage, mailDir, port: 0, host: '127.0.0.1' })
|
||||||
|
await source.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await source.stop()
|
||||||
|
await new Promise<void>((resolve) => upstream.close(() => resolve()))
|
||||||
|
await rm(mailDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
const send = () => {
|
||||||
|
const transport = nodemailer.createTransport({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: source.getPort(),
|
||||||
|
secure: false,
|
||||||
|
})
|
||||||
|
return transport.sendMail({
|
||||||
|
from: 'a@example.com',
|
||||||
|
to: 'b@example.com',
|
||||||
|
subject: 'relay me',
|
||||||
|
text: 'hello',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('upgrades the relay connection with STARTTLS when the server offers it', async () => {
|
||||||
|
source.setupRelay({ host: '127.0.0.1', port: upstreamPort, secure: false })
|
||||||
|
source.setAutoRelay({ enabled: true })
|
||||||
|
|
||||||
|
await send()
|
||||||
|
|
||||||
|
expect(deliveries).toEqual([true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stays on cleartext with ignoreTLS even when the server offers STARTTLS', async () => {
|
||||||
|
source.setupRelay({ host: '127.0.0.1', port: upstreamPort, secure: false, ignoreTLS: true })
|
||||||
|
source.setAutoRelay({ enabled: true })
|
||||||
|
|
||||||
|
await send()
|
||||||
|
|
||||||
|
expect(deliveries).toEqual([false])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -36,6 +36,7 @@ export class RelayClient {
|
|||||||
host: config.host || 'localhost',
|
host: config.host || 'localhost',
|
||||||
port: config.port || (config.secure ? 465 : 25),
|
port: config.port || (config.secure ? 465 : 25),
|
||||||
secure: config.secure || false,
|
secure: config.secure || false,
|
||||||
|
ignoreTLS: config.ignoreTLS || false,
|
||||||
auth: config.auth,
|
auth: config.auth,
|
||||||
}
|
}
|
||||||
this.logger = createLogger(logger)
|
this.logger = createLogger(logger)
|
||||||
@@ -152,6 +153,7 @@ export class RelayClient {
|
|||||||
port: this.config.port,
|
port: this.config.port,
|
||||||
host: this.config.host,
|
host: this.config.host,
|
||||||
secure: this.config.secure,
|
secure: this.config.secure,
|
||||||
|
ignoreTLS: this.config.ignoreTLS,
|
||||||
tls: { rejectUnauthorized: false },
|
tls: { rejectUnauthorized: false },
|
||||||
debug: false,
|
debug: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export interface RelayConfig {
|
|||||||
port: number
|
port: number
|
||||||
/** Enable TLS/SSL */
|
/** Enable TLS/SSL */
|
||||||
secure?: boolean
|
secure?: boolean
|
||||||
|
/** Never upgrade a non-secure connection with STARTTLS, even if the server offers it */
|
||||||
|
ignoreTLS?: boolean
|
||||||
/** Authentication credentials */
|
/** Authentication credentials */
|
||||||
auth?: SMTPAuth | undefined
|
auth?: SMTPAuth | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user