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

12
config/access.ini Normal file
View file

@ -0,0 +1,12 @@
# Access control configuration
[main]
# Allow or deny based on patterns
# Default action when no rules match
default_action=accept
# Enable access control
enabled=true
# Log access decisions
log_decisions=true

18
config/auth_proxy.ini Normal file
View file

@ -0,0 +1,18 @@
[core]
# Authentication proxy configuration
# Forwards auth requests to Phoenix app for validation
# Only require AUTH on port 587 (submission), not port 25 (for receiving inbound mail)
[port:587]
host=app.example.com
port=443
path=/api/haraka/auth
timeout=5000
protocol=https
[domains]
elektrine.com=elektrine_mail:2587
elektrine.net=elektrine_mail:2587
elektrine.org=elektrine_mail:2587
z.org=elektrine_mail:2587
mail.elektrine.com=elektrine_mail:2587

22
config/clamd.ini Normal file
View file

@ -0,0 +1,22 @@
[main]
; ClamAV daemon configuration
clamd_socket = clamav:3310
; How long to wait for ClamAV response (seconds)
timeout = 30
; What to do when virus is detected
action = reject
; Whether to enable quarantine of infected messages
quarantine = true
; Log level (LOGDEBUG, LOGINFO, LOGNOTICE, LOGWARN, LOGERROR, LOGCRIT)
loglevel = LOGINFO
[reject]
; Enable rejection for virus detection
virus = true
; Reject message text
virus_msg = Message contains a virus and has been rejected

View file

@ -0,0 +1,3 @@
# Blacklist malicious IPs/networks (one per line)
# Example: 192.168.1.100
# Example: 10.0.0.0/8

View file

@ -0,0 +1,3 @@
# Whitelist trusted IPs/networks (one per line)
# Example: 192.168.1.0/24
# Example: 10.0.0.5

70
config/connection.ini Normal file
View file

@ -0,0 +1,70 @@
;
[main]
; Require senders to conform to RFC 1869 and RFC 821 when sending the MAIL FROM and RCPT TO commands. In particular, the inclusion of spurious spaces or missing angle brackets will be rejected.
; Disabled to allow compatibility with real-world mail servers (e.g., QQ Mail) that add extra spaces
strict_rfc1869 = false
; Advertise support for SMTPUTF8 (RFC-6531)
; smtputf8=true
; Limit connections per remote host/IP
max_connections_per_ip = 10
; Reject if too many connections from same IP
max_rate_per_ip_per_minute = 30
[haproxy]
; Array: hosts or CIDRs that Haraka should enable the PROXY protocol from. See docs/HAProxy for format
hosts[] =
; hosts[] = 192.0.2.4
; hosts[] = 192.0.2.5
; hosts[] = [2001:db8::1]
; hosts[] = [2001:db8::2]
[headers]
; add_received=true
; clean_auth_results=true
; show_version=true
max_lines=1000
max_received=100
[max]
; Integer. The maximum SIZE of an email
bytes=26214400
; Integer. Limit a potential denial of service in potentially hostile emails.
mime_parts=1000
; Integer. The maximum length of lines in SMTP session commands (e.g. RCPT, HELO etc). Defaults to 512 (bytes) as mandated by RFC 5321 §4.5.3.1.4. Clients exceeding this limit will be immediately disconnected with a "521 Command line too long" error.
line_length=512
; Integer. The maximum length of lines in the DATA section of emails. Defaults to 992 (bytes), the limit set by Sendmail. When this limit is exceeded the three bytes "\r\n " (0x0d 0x0a 0x20) are inserted into the stream to "fix" it. This has the potential to "break" some email, but makes it more likely to be accepted by upstream/downstream services, and is the same behaviour as Sendmail. Also when the data line length limit is exceeded `transaction.notes.data_line_length_exceeded` is set to `true`.
data_line_length=992
[message]
; Array. The greeting used when a client connects.
; greeting[]=My Custom
; greeting[]=Greeting Message
helo=Haraka is at your service.
; String. Override the default connection close message.
close=closing connection. Have a jolly good day.
[uuid]
; integer, how many UUID chars to show.
; 0 = none, 6 is enough to be unique per day, 40 will include the
; full connection and transaction UUID
banner_chars=6
; include N characters of the uuid (in brackets) at the start of each line of the deny message
deny_chars=0

13
config/dkim.ini Normal file
View file

@ -0,0 +1,13 @@
[main]
[verify]
enabled = true
[sign]
enabled = true
selector = default
domain = example.com
headers = From, Sender, Reply-To, Subject, Date, Message-ID, To, Cc, MIME-Version
[example.com]
selector = default

30
config/dns-list.ini Normal file
View file

@ -0,0 +1,30 @@
# DNS list plugin configuration (haraka-plugin-dns-list)
# IMPORTANT:
# - `dns-list` is currently commented out in inbound plugin profiles for incident mitigation.
# - If re-enabled, this file keeps DNSBL checks informational only (no SMTP hard reject).
[main]
# Explicit zone list to avoid plugin defaults that can include noisy lists.
zones=zen.spamhaus.org,bl.spamcop.net,b.barracudacentral.org
# Query all configured zones and log overlap for analysis.
search=all
periodic_checks=30
[stats]
enable=true
[zen.spamhaus.org]
type=block
reject=false
ipv6=false
[bl.spamcop.net]
type=block
reject=false
ipv6=false
[b.barracudacentral.org]
type=block
reject=false
ipv6=false

114
config/elektrine.ini Normal file
View file

@ -0,0 +1,114 @@
; Haraka plugin configuration
;
; This file provides default configuration for all Elektrine plugins.
; Environment variables take precedence over these settings.
;
; Environment Variables:
; HARAKA_ROLE - Runtime role (inbound-mx/submission/outbound-relay)
; PHOENIX_WEBHOOK_URL - Webhook URL for inbound emails
; PHOENIX_VERIFY_URL - Recipient verification URL
; PHOENIX_DOMAINS_URL - Domains endpoint URL
; PHOENIX_API_KEY - API key for Haraka -> Phoenix calls
; HARAKA_HTTP_API_KEY - API key required by Haraka /api/v1/send
; HARAKA_API_KEY - Optional shared key
; HARAKA_HTTP_PORT - HTTP API port (default: 8080)
; HARAKA_HTTP_HOST - HTTP API host (default: 0.0.0.0)
; HARAKA_CORS_ORIGIN - Allowed CORS origin for browser calls
; HARAKA_DKIM_DIR - Persistent directory for DKIM key storage
; LOCAL_DOMAINS - Comma-separated list of local domains
; OPS_ALLOWED_CIDRS - CIDRs allowed to call /status and /healthz
; METRICS_ALLOWED_CIDRS - CIDRs allowed to call /metrics
; HARAKA_TRUSTED_PROXY_CIDRS - CIDRs trusted for forwarded client IP headers
; REDIS_URL - Redis URL for async queue
; ELEKTRINE_QUEUE_NAME - Queue key name
; ELEKTRINE_DLQ_NAME - Dead letter queue key name
[main]
; Runtime role used by startup profile selection
role = inbound-mx
; Phoenix app webhook URL for inbound emails
url = https://app.example.com/api/haraka/inbound
; Phoenix app recipient verification URL
verify_url = https://app.example.com/api/haraka/verify-recipient
; Phoenix app domains URL
domains_url = https://app.example.com/api/haraka/domains
; Haraka -> Phoenix key
; phoenix_api_key =
; Haraka /api/v1/send key
; http_api_key =
; Optional shared key
; api_key =
; Webhook timeout in milliseconds
timeout = 30000
; Recipient verification timeout in milliseconds
verify_timeout = 5000
; Enable/disable webhook processing
enabled = true
; Include email headers in webhook payload
include_headers = true
; Include email body in webhook payload
include_body = true
; Include attachment content in webhook payload
include_attachments = true
[http_api]
; HTTP API server port
port = 8080
; HTTP API server host
host = 0.0.0.0
; Persistent DKIM key storage directory used by /api/v1/dkim/domains/*
; dkim_storage_dir = /data/haraka/dkim
; Browser API callers origin allowlist (optional)
; cors_origin =
; Trusted reverse proxy CIDRs used when resolving client IP
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
[rate_limit]
; Rate limit window in milliseconds (default: 60000 = 1 minute)
window_ms = 60000
; Maximum requests per window per IP
max_requests = 50
[ops]
; Optional CIDRs allowed to access /status and /healthz without an API key
allowed_cidrs =
[metrics]
; Optional CIDRs allowed to access /metrics without an API key
allowed_cidrs =
[domains]
; Local domains (space or comma separated)
; These domains receive inbound mail and are protected from spoofing
local = example.com
; Domain cache refresh interval in ms
cache_ttl_ms = 300000
[queue]
redis_url = redis://redis:6379
name = elektrine:inbound
dlq_name = elektrine:inbound:dlq
pop_timeout_sec = 5
max_raw_bytes = 26214400
[worker]
webhook_max_retries = 5
webhook_retry_base_delay_ms = 1000

View file

@ -0,0 +1,11 @@
; Elektrine local-domain MX override
; Ensures local/protected domains are delivered to internal inbound Haraka.
;
; Environment overrides:
; ELEKTRINE_LOCAL_MX_HOST
; ELEKTRINE_LOCAL_MX_PORT
[main]
enabled=true
host=haraka-inbound
port=25

View file

@ -0,0 +1,34 @@
; Elektrine async queue + worker configuration
;
; Environment variables override these values:
; REDIS_URL
; ELEKTRINE_QUEUE_NAME
; ELEKTRINE_DLQ_NAME
; ELEKTRINE_QUEUE_POP_TIMEOUT
; ELEKTRINE_QUEUE_MAX_RAW_BYTES
; WEBHOOK_MAX_RETRIES
; WEBHOOK_RETRY_BASE_MS
[main]
; Keep payload fields aligned with the webhook plugin behavior
enabled = true
include_headers = true
include_body = true
include_attachments = true
; Phoenix webhook URL and auth can also be set here,
; but production should use env vars.
; url = https://app.example.com/api/haraka/inbound
; phoenix_api_key =
; timeout = 30000
[queue]
redis_url = redis://redis:6379
name = elektrine:inbound
dlq_name = elektrine:inbound:dlq
pop_timeout_sec = 5
max_raw_bytes = 26214400
[worker]
webhook_max_retries = 5
webhook_retry_base_delay_ms = 1000

View file

@ -0,0 +1,18 @@
[main]
# Enable the webhook
enabled=true
# Elektrine webhook URL - can be overridden by PHOENIX_WEBHOOK_URL env var
# url=https://your-phoenix-app.com/api/haraka/inbound
# Timeout for webhook requests (milliseconds)
timeout=30000
# What data to include
include_headers=true
include_body=true
include_attachments=true
# Authentication - API key for webhook
# Set PHOENIX_API_KEY (or HARAKA_API_KEY)
# api_key=

16
config/helo.checks.ini Normal file
View file

@ -0,0 +1,16 @@
# HELO/EHLO checking configuration
[main]
# Check if HELO hostname is valid
check_valid_hostname=true
# Check if HELO matches rDNS
check_rdns_match=false
# Reject invalid HELO
reject_invalid_helo=false
# Reject non-FQDN HELO
reject_non_fqdn_helo=false
# Log HELO check results
log_results=true

4
config/host_list Normal file
View file

@ -0,0 +1,4 @@
elektrine.com
elektrine.net
elektrine.org
z.org

9
config/http.ini Normal file
View file

@ -0,0 +1,9 @@
[main]
; HTTP server disabled - watch plugin not in use
; listen=[::0]:8081
; Directory for static files (used by watch plugin)
; htdocs=./plugins/watch/html
; Websockets disabled
websockets=false

1
config/internalcmd_key Normal file
View file

@ -0,0 +1 @@
f1c4e6ff4c90a71d4e26eddb5a38fcf7f0b176946e18197914421018fd2a91e2

36
config/karma.ini Normal file
View file

@ -0,0 +1,36 @@
[redis]
host=redis
port=6379
[awards]
# Award points for passing checks
spf_pass=1
dkim_pass=2
fcrdns_pass=1
asn_all_good=1
[penalties]
# Penalize bad behavior
spf_fail=-5
dkim_fail=-3
fcrdns_fail=-1
# Heavy penalty for rapid connections
tarpit=-1
relaying=-10
[concurrency]
# Limit concurrent connections per IP
max=5
[disconnect]
# Disconnect when karma score drops below threshold
karma=-8
[limits]
# Limit messages per connection
message_max=10
[tarpit]
# Delay in seconds for IPs with negative karma
delay=5
max=15

View file

@ -0,0 +1,7 @@
[main]
level=info
timestamps=true
format=default
[file]
filename=/app/logs/haraka-inbound.log

15
config/log.ini Normal file
View file

@ -0,0 +1,15 @@
[main]
; level=data, protocol, debug, info, notice, warn, error, crit, alert, emerg
level=info
; prepend timestamps to log entries? This setting does NOT affect logs emitted
; by logging plugins (like syslog).
timestamps=true
; format=default, logfmt, json
format=default
; Log to file
[file]
filename=/app/logs/haraka.log

View file

@ -0,0 +1,7 @@
[main]
level=info
timestamps=true
format=default
[file]
filename=/app/logs/haraka-outbound.log

View file

@ -0,0 +1,7 @@
[main]
level=info
timestamps=true
format=default
[file]
filename=/app/logs/haraka-submission.log

1
config/me Normal file
View file

@ -0,0 +1 @@
mail.elektrine.com

10
config/outbound.ini Normal file
View file

@ -0,0 +1,10 @@
[main]
disabled=false
concurrency_max=100
enable_tls=true
local_mx_ok=true
; Prefer IPv4 for outbound delivery. This host does not have working outbound
; IPv6 from Docker, so trying AAAA targets first causes noisy ENETUNREACH logs
; and delayed delivery. Haraka 3 uses inet_prefer; ipv6_enabled is obsolete.
inet_prefer=v4

72
config/plugins Normal file
View file

@ -0,0 +1,72 @@
# Default profile for local runs (inbound-mx).
# In container deployments, this file is overwritten at startup by
# scripts/start-haraka.sh using config/plugins.<HARAKA_ROLE>.
# status
# process_title
# syslog
# watch
# CONNECT
# ----------
toobusy
redis
# relay
access
# p0f
# geoip
# asn
fcrdns
# Incident mitigation: DNSBL lookups can block legitimate inbound delivery.
# Keep disabled until logs confirm blacklist false positives are resolved.
# dns-list
# HELO
# ----------
early_talker
helo.checks
# Enable TLS for secure connections
tls
#
# AUTH plugins require TLS before AUTH is advertised, see
# https://github.com/haraka/Haraka/wiki/Require-SSL-TLS
# ----------
# Use auth_proxy to authenticate against Phoenix app
auth/auth_proxy
# MAIL FROM
# ----------
mail_from.is_resolvable
spf
elektrine_spf_enforcer
# RCPT TO
# ----------
# At least one rcpt_to plugin is REQUIRED for inbound email.
# elektrine_rcpt_verify handles both local domain validation AND recipient verification
# by querying Phoenix API for the list of valid domains (including custom domains)
# and verifying recipients exist.
# rcpt_to.in_host_list is no longer needed since elektrine_rcpt_verify handles all cases.
elektrine_rcpt_verify
# qmail-deliverable
# rcpt_to.routes
# DATA
# ----------
attachment
# bounce
# Quick checks first (reject fast)
uribl
# Resource-intensive checks last
clamd
spamassassin
dkim
# headers
# limit
# rspamd
# QUEUE
# ----------
# queues: discard qmail-queue quarantine smtp_forward smtp_proxy
# Inbound path: enqueue for async worker/webhook processing
elektrine_async_queue

29
config/plugins.inbound-mx Normal file
View file

@ -0,0 +1,29 @@
# Haraka role profile: inbound-mx
# Accept internet mail for local domains, run anti-abuse checks,
# then enqueue for async webhook processing.
toobusy
redis
access
fcrdns
# Incident mitigation: DNSBL lookups can block legitimate inbound delivery.
# Keep disabled until logs confirm blacklist false positives are resolved.
# dns-list
early_talker
helo.checks
tls
mail_from.is_resolvable
spf
elektrine_spf_enforcer
elektrine_rcpt_verify
attachment
uribl
clamd
spamassassin
dkim
elektrine_async_queue

View file

@ -0,0 +1,10 @@
# Haraka role profile: outbound-relay
# Expose HTTP send API and perform outbound handoff/signing.
toobusy
redis
elektrine_local_mx
tls
dkim
elektrine_http_api

15
config/plugins.submission Normal file
View file

@ -0,0 +1,15 @@
# Haraka role profile: submission
# Accept authenticated client mail on 587 and relay via native Haraka outbound.
toobusy
redis
access
elektrine_local_mx
tls
auth/auth_proxy
mail_from.is_resolvable
spf
dkim
elektrine_rcpt_verify

View file

@ -0,0 +1,10 @@
[main]
# Public MX listener
listen=[::0]:25
max_connections=2000
max_unrecognized_commands=10
max_errors=10
connect_timeout=60
data_timeout=600
nodes=0

14
config/smtp.ini Normal file
View file

@ -0,0 +1,14 @@
[main]
# Listen on port 25 (SMTP) and 587 (submission)
listen=[::0]:25,[::0]:587
# Connection limits
max_connections=1000
max_unrecognized_commands=10
max_errors=10
# Timeouts (in seconds)
connect_timeout=60
data_timeout=600
nodes=0

View file

@ -0,0 +1,10 @@
[main]
# Keep SMTP listener internal for this role.
listen=127.0.0.1:2525
max_connections=200
max_unrecognized_commands=10
max_errors=10
connect_timeout=60
data_timeout=600
nodes=0

View file

@ -0,0 +1,10 @@
[main]
# Authenticated submission listener
listen=[::0]:587,[::0]:465
max_connections=1000
max_unrecognized_commands=10
max_errors=10
connect_timeout=60
data_timeout=600
nodes=0

38
config/spamassassin.ini Normal file
View file

@ -0,0 +1,38 @@
[main]
; SpamAssassin daemon configuration
spamd_socket = spamassassin:783
; Maximum message size to scan (bytes) - 256KB default
max_size = 262144
; Timeout for spamd connection (seconds)
timeout = 30
; Should we add spam headers to all emails?
add_headers = true
; Should we reject emails over the spam threshold?
reject_spam = false
; Spam threshold for rejection (only if reject_spam = true)
reject_threshold = 15.0
; Should we quarantine spam messages?
quarantine_spam = false
[headers]
; Add X-Spam-Status header
status = true
; Add X-Spam-Score header
score = true
; Add X-Spam-Report header (detailed report)
report = true
[thresholds]
; Score at which to add spam headers
add_header = 5.0
; Score at which to reject (if reject_spam = true)
reject = 15.0

22
config/spf.ini Normal file
View file

@ -0,0 +1,22 @@
[main]
# SPF checking timeout (seconds)
timeout=30
# Enable SPF checking
enabled=true
# Log SPF results
log_result=true
# For general emails, be lenient - elektrine_spf_enforcer plugin handles our domains
[fail]
action=accept
message=SPF validation failed
# Soft fail is treated leniently
[softfail]
action=accept
# Allow relay for outbound emails
[relay]
action=accept

30
config/tls.ini Normal file
View file

@ -0,0 +1,30 @@
[main]
key=/app/ssl/cert.key
cert=/app/ssl/cert.crt
# Strong ciphers only - ECDHE preferred, AES-GCM and ChaCha20
ciphers=ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP
# TLS 1.2 minimum - TLS 1.0 and 1.1 are deprecated (RFC 8996)
minVersion=TLSv1.2
maxVersion=TLSv1.3
# No client certificate requirements
requestCert=false
rejectUnauthorized=false
# Enable renegotiation protection (CVE-2009-3555)
secureRenegotiation=true
# Server chooses cipher order (prefer strongest available)
honorCipherOrder=true
no_starttls_ports[]=465
dhparam=/app/ssl/dhparam.pem
[outbound]
rejectUnauthorized=false
; Local-domain mail is routed to the internal inbound container. That hop does
; not need STARTTLS, and the container hostname does not match the public
; *.elektrine.com certificate, which otherwise logs ERR_TLS_CERT_ALTNAME_INVALID.
no_tls_hosts[]=haraka-inbound

14
config/toobusy.ini Normal file
View file

@ -0,0 +1,14 @@
# Rate limiting configuration
[main]
# Enable rate limiting
enabled=true
# Maximum load average before rejecting connections
max_load=2.0
# Check interval (milliseconds)
check_interval=1000
# Action when too busy
action=defer
message=Server too busy, try again later

72
config/uribl.ini Normal file
View file

@ -0,0 +1,72 @@
[main]
; Enable URIBL checking
enabled = true
; Timeout for DNS lookups (seconds)
timeout = 10
; Maximum number of hosts to check per message
max_uris_per_list = 20
; Should we check redirected URLs? (performance impact)
check_redirects = false
; Disable IPv6 lookups (often faster)
disable_ipv6 = true
[lists]
; SURBL Multi - Commercial spam URLs
; Score: 5 points for spam URLs
surbl.org = 5
; SpamHaus SBL-CSS - Known spam URLs
; Score: 8 points (high confidence)
sbl.spamhaus.org = 8
; URIBL Black - Spam and malicious URLs
; Score: 6 points
black.uribl.com = 6
; URIBL Grey - Suspicious URLs (lower score)
; Score: 3 points
grey.uribl.com = 3
; DBL (Domain Block List) - SpamHaus domain reputation
; Score: 7 points
dbl.spamhaus.org = 7
; Multi.URIBL.COM - Multiple reputation feeds
; Score: 4 points
multi.uribl.com = 4
[whitelist]
; Popular legitimate domains that might get false positives
; These domains will never be blocked
whitelist = google.com,microsoft.com,amazon.com,facebook.com,twitter.com,linkedin.com,github.com,stackoverflow.com,wikipedia.org,apple.com
[thresholds]
; Score at which to add headers but allow message
add_header = 3
; Score at which to soft reject (450 - try again later)
soft_reject = 8
; Score at which to hard reject (550 - permanent failure)
hard_reject = 15
[headers]
; Add X-URIBL-Score header to show scoring
add_score_header = true
; Add X-URIBL-Lists header to show which lists matched
add_lists_header = true
; Add detailed X-URIBL-Report header
add_report_header = false
[reject]
; Enable rejection based on URIBL scores
enable_reject = true
; Custom rejection message
reject_msg = Message contains URLs listed in spam databases

10
config/watch.ini Normal file
View file

@ -0,0 +1,10 @@
[main]
; Enable sampling to limit display connections to one per second
sampling=true
; WebSocket URL configuration - will use same scheme, host, port as http by default
; wss.url=wss://your-domain.com:8080
; Alternative document root for watch HTML files
; Using default haraka-plugin-watch location
; wss.htdocs=./html