/** * Centralized Configuration Module * * Single source of truth for Haraka plugin configuration. * Prioritizes environment variables over .ini file settings. */ 'use strict'; // Default configuration values const DEFAULTS = { // Runtime role role: 'inbound-mx', // Phoenix API endpoints webhook_url: 'https://app.example.com/api/haraka/inbound', verify_url: 'https://app.example.com/api/haraka/verify-recipient', domains_url: 'https://app.example.com/api/haraka/domains', // Directional API authentication // phoenix_api_key: used when Haraka calls Phoenix endpoints // http_api_key: used to authenticate callers to Haraka /api/v1/send phoenix_api_key: '', http_api_key: '', // HTTP server settings http_port: 8080, http_host: '0.0.0.0', cors_origin: '', dkim_storage_dir: '', // HTTP endpoint access controls ops_allowed_cidrs: [], metrics_allowed_cidrs: [], trusted_proxy_cidrs: [ '127.0.0.1/32', '::1/128', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7' ], // Timeouts (milliseconds) webhook_timeout: 30000, verify_timeout: 5000, // Webhook retry controls webhook_max_retries: 5, webhook_retry_base_delay_ms: 1000, // Rate limiting rate_limit_window_ms: 60000, // 1 minute rate_limit_max_requests: 50, // 50 requests per window // Local domains (protected from spoofing, receive inbound mail) local_domains: ['example.com'], // Domain cache behavior domain_cache_ttl_ms: 5 * 60 * 1000, // Async queue settings redis_url: 'redis://redis:6379', queue_name: 'elektrine:inbound', queue_dlq_name: 'elektrine:inbound:dlq', queue_pop_timeout_sec: 5, queue_max_raw_bytes: 25 * 1024 * 1024, // Feature flags webhook_enabled: true, include_headers: true, include_body: true, include_attachments: true }; function to_int(value, fallback) { const parsed = parseInt(value, 10); return Number.isFinite(parsed) ? parsed : fallback; } function to_bool(value, fallback) { if (value === undefined || value === null) return fallback; const normalized = String(value).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; if (['0', 'false', 'no', 'off'].includes(normalized)) return false; return fallback; } function parse_string_list(value, { lowercase = false } = {}) { if (!value) return null; const normalized = Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : String(value).split(/[\s,]+/).map((entry) => entry.trim()).filter(Boolean); if (normalized.length === 0) return null; if (!lowercase) return normalized; return normalized.map((entry) => entry.toLowerCase()); } function parse_domain_list(value) { return parse_string_list(value, { lowercase: true }); } function parse_cidr_list(value) { return parse_string_list(value, { lowercase: true }); } function apply_env_overrides(config) { if (process.env.HARAKA_ROLE) config.role = process.env.HARAKA_ROLE; if (process.env.PHOENIX_WEBHOOK_URL) config.webhook_url = process.env.PHOENIX_WEBHOOK_URL; if (process.env.PHOENIX_VERIFY_URL) config.verify_url = process.env.PHOENIX_VERIFY_URL; if (process.env.PHOENIX_DOMAINS_URL) config.domains_url = process.env.PHOENIX_DOMAINS_URL; if (process.env.PHOENIX_API_KEY) config.phoenix_api_key = process.env.PHOENIX_API_KEY; if (process.env.HARAKA_HTTP_API_KEY) config.http_api_key = process.env.HARAKA_HTTP_API_KEY; if (process.env.HARAKA_API_KEY) { if (!process.env.PHOENIX_API_KEY) { config.phoenix_api_key = process.env.HARAKA_API_KEY; } if (!process.env.HARAKA_HTTP_API_KEY) { config.http_api_key = process.env.HARAKA_API_KEY; } } if (process.env.HARAKA_HTTP_PORT) config.http_port = to_int(process.env.HARAKA_HTTP_PORT, DEFAULTS.http_port); if (process.env.HARAKA_HTTP_HOST) config.http_host = process.env.HARAKA_HTTP_HOST; if (process.env.HARAKA_CORS_ORIGIN !== undefined) config.cors_origin = process.env.HARAKA_CORS_ORIGIN; if (process.env.HARAKA_DKIM_DIR !== undefined) config.dkim_storage_dir = process.env.HARAKA_DKIM_DIR; if (process.env.LOCAL_DOMAINS) config.local_domains = parse_domain_list(process.env.LOCAL_DOMAINS) || DEFAULTS.local_domains; if (process.env.OPS_ALLOWED_CIDRS) { config.ops_allowed_cidrs = parse_cidr_list(process.env.OPS_ALLOWED_CIDRS) || DEFAULTS.ops_allowed_cidrs; } if (process.env.METRICS_ALLOWED_CIDRS) { config.metrics_allowed_cidrs = parse_cidr_list(process.env.METRICS_ALLOWED_CIDRS) || DEFAULTS.metrics_allowed_cidrs; } if (process.env.HARAKA_TRUSTED_PROXY_CIDRS) { config.trusted_proxy_cidrs = parse_cidr_list(process.env.HARAKA_TRUSTED_PROXY_CIDRS) || DEFAULTS.trusted_proxy_cidrs; } if (process.env.REDIS_URL) config.redis_url = process.env.REDIS_URL; if (process.env.ELEKTRINE_QUEUE_NAME) config.queue_name = process.env.ELEKTRINE_QUEUE_NAME; if (process.env.ELEKTRINE_DLQ_NAME) config.queue_dlq_name = process.env.ELEKTRINE_DLQ_NAME; if (process.env.ELEKTRINE_QUEUE_POP_TIMEOUT) { config.queue_pop_timeout_sec = to_int(process.env.ELEKTRINE_QUEUE_POP_TIMEOUT, DEFAULTS.queue_pop_timeout_sec); } if (process.env.ELEKTRINE_QUEUE_MAX_RAW_BYTES) { config.queue_max_raw_bytes = to_int(process.env.ELEKTRINE_QUEUE_MAX_RAW_BYTES, DEFAULTS.queue_max_raw_bytes); } if (process.env.WEBHOOK_MAX_RETRIES) { config.webhook_max_retries = to_int(process.env.WEBHOOK_MAX_RETRIES, DEFAULTS.webhook_max_retries); } if (process.env.WEBHOOK_RETRY_BASE_MS) { config.webhook_retry_base_delay_ms = to_int(process.env.WEBHOOK_RETRY_BASE_MS, DEFAULTS.webhook_retry_base_delay_ms); } if (process.env.DOMAIN_CACHE_TTL_MS) { config.domain_cache_ttl_ms = to_int(process.env.DOMAIN_CACHE_TTL_MS, DEFAULTS.domain_cache_ttl_ms); } if (process.env.HARAKA_INCLUDE_HEADERS !== undefined) { config.include_headers = to_bool(process.env.HARAKA_INCLUDE_HEADERS, config.include_headers); } if (process.env.HARAKA_INCLUDE_BODY !== undefined) { config.include_body = to_bool(process.env.HARAKA_INCLUDE_BODY, config.include_body); } if (process.env.HARAKA_INCLUDE_ATTACHMENTS !== undefined) { config.include_attachments = to_bool(process.env.HARAKA_INCLUDE_ATTACHMENTS, config.include_attachments); } } /** * Load configuration from environment variables and optional Haraka config * @param {Object} haraka_config - Optional Haraka plugin config object * @returns {Object} Merged configuration object */ function load(haraka_config = null) { const config = { ...DEFAULTS }; // Environment variables (highest priority) apply_env_overrides(config); // Haraka .ini config overrides (if provided) if (haraka_config && haraka_config.main) { const main = haraka_config.main; if (main.role) config.role = main.role; if (main.url) config.webhook_url = main.url; if (main.verify_url) config.verify_url = main.verify_url; if (main.domains_url) config.domains_url = main.domains_url; if (main.phoenix_api_key) config.phoenix_api_key = main.phoenix_api_key; if (main.http_api_key) config.http_api_key = main.http_api_key; if (main.api_key) { if (!main.phoenix_api_key) config.phoenix_api_key = main.api_key; if (!main.http_api_key) config.http_api_key = main.api_key; } if (main.port) config.http_port = to_int(main.port, config.http_port); if (main.host) config.http_host = main.host; if (main.cors_origin !== undefined) config.cors_origin = main.cors_origin; if (main.dkim_storage_dir !== undefined) config.dkim_storage_dir = main.dkim_storage_dir; if (main.timeout) config.webhook_timeout = to_int(main.timeout, config.webhook_timeout); if (main.verify_timeout) config.verify_timeout = to_int(main.verify_timeout, config.verify_timeout); if (main.domain_cache_ttl_ms) { config.domain_cache_ttl_ms = to_int(main.domain_cache_ttl_ms, config.domain_cache_ttl_ms); } // Boolean flags if (main.enabled !== undefined) config.webhook_enabled = main.enabled; if (main.include_headers !== undefined) config.include_headers = main.include_headers; if (main.include_body !== undefined) config.include_body = main.include_body; if (main.include_attachments !== undefined) config.include_attachments = main.include_attachments; } if (haraka_config && haraka_config.http_api) { const http_api = haraka_config.http_api; if (http_api.port) config.http_port = to_int(http_api.port, config.http_port); if (http_api.host) config.http_host = http_api.host; if (http_api.cors_origin !== undefined) config.cors_origin = http_api.cors_origin; if (http_api.dkim_storage_dir !== undefined) config.dkim_storage_dir = http_api.dkim_storage_dir; if (http_api.ops_allowed_cidrs) { const parsed_ops_cidrs = parse_cidr_list(http_api.ops_allowed_cidrs); if (parsed_ops_cidrs && parsed_ops_cidrs.length > 0) { config.ops_allowed_cidrs = parsed_ops_cidrs; } } if (http_api.metrics_allowed_cidrs) { const parsed_metrics_cidrs = parse_cidr_list(http_api.metrics_allowed_cidrs); if (parsed_metrics_cidrs && parsed_metrics_cidrs.length > 0) { config.metrics_allowed_cidrs = parsed_metrics_cidrs; } } if (http_api.trusted_proxy_cidrs) { const parsed_trusted_proxy_cidrs = parse_cidr_list(http_api.trusted_proxy_cidrs); if (parsed_trusted_proxy_cidrs && parsed_trusted_proxy_cidrs.length > 0) { config.trusted_proxy_cidrs = parsed_trusted_proxy_cidrs; } } } if (haraka_config && haraka_config.ops && haraka_config.ops.allowed_cidrs) { const parsed_ops_cidrs = parse_cidr_list(haraka_config.ops.allowed_cidrs); if (parsed_ops_cidrs && parsed_ops_cidrs.length > 0) { config.ops_allowed_cidrs = parsed_ops_cidrs; } } if (haraka_config && haraka_config.metrics && haraka_config.metrics.allowed_cidrs) { const parsed_metrics_cidrs = parse_cidr_list(haraka_config.metrics.allowed_cidrs); if (parsed_metrics_cidrs && parsed_metrics_cidrs.length > 0) { config.metrics_allowed_cidrs = parsed_metrics_cidrs; } } if (haraka_config && haraka_config.rate_limit) { const rate_limit = haraka_config.rate_limit; if (rate_limit.window_ms) { config.rate_limit_window_ms = to_int(rate_limit.window_ms, config.rate_limit_window_ms); } if (rate_limit.max_requests) { config.rate_limit_max_requests = to_int(rate_limit.max_requests, config.rate_limit_max_requests); } } if (haraka_config && haraka_config.domains) { const domains = haraka_config.domains; if (domains.local) { const parsed = parse_domain_list(domains.local); if (parsed && parsed.length > 0) { config.local_domains = parsed; } } if (domains.cache_ttl_ms) { config.domain_cache_ttl_ms = to_int(domains.cache_ttl_ms, config.domain_cache_ttl_ms); } } if (haraka_config && haraka_config.queue) { const queue = haraka_config.queue; if (queue.redis_url) config.redis_url = queue.redis_url; if (queue.name) config.queue_name = queue.name; if (queue.dlq_name) config.queue_dlq_name = queue.dlq_name; if (queue.pop_timeout_sec) { config.queue_pop_timeout_sec = to_int(queue.pop_timeout_sec, config.queue_pop_timeout_sec); } if (queue.max_raw_bytes) { config.queue_max_raw_bytes = to_int(queue.max_raw_bytes, config.queue_max_raw_bytes); } } if (haraka_config && haraka_config.dkim && haraka_config.dkim.storage_dir !== undefined) { config.dkim_storage_dir = haraka_config.dkim.storage_dir; } if (haraka_config && haraka_config.worker) { const worker = haraka_config.worker; if (worker.webhook_max_retries) { config.webhook_max_retries = to_int(worker.webhook_max_retries, config.webhook_max_retries); } if (worker.webhook_retry_base_delay_ms) { config.webhook_retry_base_delay_ms = to_int(worker.webhook_retry_base_delay_ms, config.webhook_retry_base_delay_ms); } } // Re-apply env values last so they always win over any .ini value. apply_env_overrides(config); config.api_key = config.phoenix_api_key; return config; } /** * Get a specific configuration value with fallback * @param {string} key - Configuration key * @param {*} fallback - Fallback value if key not found * @returns {*} Configuration value */ function get(key, fallback = null) { const loaded = load(); return Object.prototype.hasOwnProperty.call(loaded, key) ? loaded[key] : fallback; } module.exports = { load, get, DEFAULTS };