Initial commit on Forgejo
Some checks are pending
CI and deploy / Test (push) Waiting to run
CI and deploy / Build, push, and deploy (push) Blocked by required conditions

Fresh repository history for elektrine/elektrine-haraka hosted at
https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:42 -04:00
commit 67c1d41a0c
82 changed files with 8713 additions and 0 deletions

328
lib/config.js Normal file
View file

@ -0,0 +1,328 @@
/**
* 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
};