Files
maildev-maildev/packages/cli/src/config/merge.ts
T
dualfroz fd864f1379 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`.
2026-10-03 02:24:38 +02:00

129 lines
4.8 KiB
TypeScript

/**
* MailDev Configuration Merger
*
* Merges configuration from multiple sources with proper priority:
* CLI args (highest) > Environment vars > Config file > Defaults (lowest)
*/
import type { MailDevConfig, PartialMailDevConfig, CLIOptions } from './types.js'
import { getDefaultConfig } from './defaults.js'
import { loadEnvConfig } from './env.js'
import { loadConfig } from './loader.js'
import type { HideableExtension } from '@maildev/smtp'
/**
* Convert CLI options to partial config
* Handles type coercion and naming differences
*/
function cliOptionsToConfig(options: CLIOptions): PartialMailDevConfig {
const config: PartialMailDevConfig = {}
// Port options (string to number)
if (options.smtp !== undefined) {
config.smtp = parseInt(options.smtp, 10)
}
if (options.web !== undefined) {
config.web = parseInt(options.web, 10)
}
if (options.outgoingPort !== undefined) {
config.outgoingPort = parseInt(options.outgoingPort, 10)
}
if (options.maxMessageSize !== undefined) {
config.maxMessageSize = parseInt(options.maxMessageSize, 10)
}
if (options.maxEmails !== undefined) {
config.maxEmails = parseInt(options.maxEmails, 10)
}
// String options
if (options.ip !== undefined) config.ip = options.ip
if (options.incomingUser !== undefined) config.incomingUser = options.incomingUser
if (options.incomingPass !== undefined) config.incomingPass = options.incomingPass
if (options.incomingCert !== undefined) config.incomingCert = options.incomingCert
if (options.incomingKey !== undefined) config.incomingKey = options.incomingKey
if (options.webIp !== undefined) config.webIp = options.webIp
if (options.webUser !== undefined) config.webUser = options.webUser
if (options.webPass !== undefined) config.webPass = options.webPass
if (options.basePathname !== undefined) config.basePathname = options.basePathname
if (options.httpsKey !== undefined) config.httpsKey = options.httpsKey
if (options.httpsCert !== undefined) config.httpsCert = options.httpsCert
if (options.outgoingHost !== undefined) config.outgoingHost = options.outgoingHost
if (options.outgoingUser !== undefined) config.outgoingUser = options.outgoingUser
if (options.outgoingPass !== undefined) config.outgoingPass = options.outgoingPass
if (options.autoRelayRules !== undefined) config.autoRelayRules = options.autoRelayRules
if (options.mailDirectory !== undefined) config.mailDirectory = options.mailDirectory
if (options.config !== undefined) config.config = options.config
// Boolean options
if (options.incomingSecure !== undefined) config.incomingSecure = options.incomingSecure
if (options.disableWeb !== undefined) config.disableWeb = options.disableWeb
if (options.https !== undefined) config.https = options.https
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.silent !== undefined) config.silent = options.silent
if (options.logMailContents !== undefined)
config.logMailContents = options.logMailContents
if (options.mcp !== undefined) config.mcp = options.mcp
// Auto-relay (can be boolean or string)
if (options.autoRelay !== undefined) {
config.autoRelay = options.autoRelay
}
// Hide extensions (comma-separated string to array)
if (options.hideExtensions !== undefined) {
config.hideExtensions = options.hideExtensions
.split(',')
.map((s) => s.trim().toUpperCase())
.filter((s) => s.length > 0) as HideableExtension[]
}
return config
}
/**
* Merge partial config into base config
* Only overwrites defined values
*/
function mergeConfig(
base: MailDevConfig,
partial: PartialMailDevConfig
): MailDevConfig {
const result = { ...base }
for (const [key, value] of Object.entries(partial)) {
if (value !== undefined) {
;(result as Record<string, unknown>)[key] = value
}
}
return result
}
/**
* Resolve complete configuration from all sources
*
* Priority: CLI args > Environment vars > Config file > Defaults
*/
export async function resolveConfig(
cliOptions: CLIOptions = {}
): Promise<{ config: MailDevConfig; configFilePath: string | null }> {
// Start with defaults
let config = getDefaultConfig()
// Load config file (uses explicit path from CLI if provided)
const { config: fileConfig, filePath } = await loadConfig(cliOptions.config)
config = mergeConfig(config, fileConfig)
// Load environment variables
const envConfig = loadEnvConfig()
config = mergeConfig(config, envConfig)
// Apply CLI options (highest priority)
const cliConfig = cliOptionsToConfig(cliOptions)
config = mergeConfig(config, cliConfig)
return { config, configFilePath: filePath }
}