Mail Proxy Daemon

1. Overview

The mail proxy daemon implements BastionGuard’s local SMTP relay and outbound email protection service. It acts as a local mail submission endpoint, accepts SMTP clients on local listening sockets, optionally upgrades connections to TLS, applies configuration-driven routing and signature enforcement, and forwards messages to remote SMTP servers through libcurl.

The daemon is designed as a transparent or policy-enforcing outbound relay, depending on the loaded configuration. It supports JSON-driven profiles, Thunderbird profile import fallback, mandatory signature injection, local queueing on delivery failure, STARTTLS and implicit TLS listeners, and Thunderbird certificate/trust bootstrap for the local TLS endpoint.

The module integrates with:

  • POSIX sockets – local SMTP listeners, accept loop, and client session I/O
  • OpenSSL – server-side TLS context creation, certificate generation, STARTTLS, and implicit TLS listeners
  • libcurl – remote SMTP relay delivery
  • nlohmann::json – loading and validating mail.json
  • gettext / libintl – translated runtime messages through _()
  • Thunderbird profile inspection – SMTP account import fallback and runtime redirection of Thunderbird SMTP settings
  • Filesystem persistence – local queue storage, TLS certificate/key storage, and configuration lookup
  • Threading – one detached thread per accepted SMTP client plus parallel listener threads

2. Core Configuration Model

2.1 MailSignatureConfig

The daemon uses a structured signature model for plain-text and HTML signature injection. The signature object contains:

  • display_name
  • job_title
  • company
  • phone
  • website
  • logo_path

2.2 MailRelayProfile

Each outbound relay profile defines how a message is matched and forwarded. A profile includes:

  • id and label
  • match_from – exact sender address matching
  • match_from_domain – sender-domain matching
  • smtp_host and smtp_port
  • starttls and implicit_tls
  • username and password
  • auth_method – numeric authentication mode
  • oauth2_token – token used when XOAUTH2 is selected
  • ssl_verify – remote TLS verification flag
  • signature – embedded signature configuration

2.3 MailSecurityConfig

The global daemon configuration contains:

  • version
  • enabled
  • scan_outgoing
  • inject_signature
  • local_smtp_host
  • local_smtp_port
  • local_smtp_tls_port
  • local_submission_port
  • advertise_starttls
  • enable_implicit_tls_listener
  • default_profile_id
  • profiles

At runtime, this object controls both listener topology and outbound relay behavior.


3. Configuration Loading and Validation

3.1 mail.json Resolution

The daemon resolves the user configuration file through mail_json_path_user(). Resolution order:

  1. Environment override:
    BASTIONGUARD_MAIL_CONFIG

  2. User path:
    ~/.config/BastionGuard/mail.json

  3. Fallback path:
    /var/lib/bastionguard/config/mail.json

3.2 Validation Rules

Loaded configuration is validated by validate_mail_security_config(). Validation includes:

  • version must be valid
  • local_smtp_host must be present and must be a valid IPv4 address
  • local_smtp_port and local_smtp_tls_port must be within 1..65535
  • At least one SMTP profile must exist
  • Each profile must have non-empty id and label
  • When protection is enabled and a profile has a remote SMTP host, smtp_port must be valid
  • If default_profile_id is set, it must resolve to an existing profile

3.3 Thunderbird Import Fallback

If mail.json cannot be loaded or validated, the daemon attempts a fallback import from Thunderbird profiles. A temporary MailSecurityConfig is built with sane defaults:

  • enabled = true
  • scan_outgoing = true
  • inject_signature = true
  • local_host = 127.0.0.1
  • local_port = 2525
  • local_tls_port = 2465
  • local_submission_port = 2587

If Thunderbird import also fails, the daemon starts in a reduced pass-through mode with a placeholder profile and enabled = false, so it can still listen locally without policy-driven relay enforcement.


4. Thunderbird Integration

4.1 Thunderbird Profile Discovery

The daemon can inspect Thunderbird’s local configuration under:

~/.thunderbird

It parses:

  • profiles.ini
  • prefs.js

SMTP server entries are collected into ThunderbirdSmtpEntry objects, while account identities are collected into ThunderbirdIdentityEntry objects.

4.2 Imported Profile Mapping

Thunderbird SMTP settings are converted into BastionGuard relay profiles. Import logic preserves:

  • SMTP hostname and port
  • STARTTLS / implicit TLS mode
  • Authentication method
  • Username
  • Per-identity sender matching
  • Sender-domain matching

If a Thunderbird profile already points to a loopback address, the import logic attempts to recover the real remote SMTP destination from custom backup fields such as:

mail.smtpserver.<id>.bg_orig_hostname
mail.smtpserver.<id>.bg_orig_port

4.3 Runtime SMTP Redirection

At startup, the daemon calls restore_thunderbird_smtp_from_backup() and may also redirect Thunderbird SMTP settings to the local proxy through redirect_thunderbird_smtp_to_proxy().

The redirection logic rewrites Thunderbird SMTP server entries so that Thunderbird submits to the local proxy instead of the original remote relay, while preserving the original destination in BastionGuard-specific backup preferences.

4.4 Thunderbird Certificate Installation

When the daemon generates or reuses a local TLS certificate, it attempts to install that certificate into Thunderbird profiles using:

certutil -A -n 'BastionGuard Mail Proxy' -t 'P,,' -i <cert> -d 'sql:<profile_dir>'

This enables Thunderbird to trust the local BastionGuard TLS endpoint.


5. Listener Topology and Socket Management

5.1 Local Listener Ports

The daemon can expose up to three SMTP listener classes:

  • Plain SMTP listener on local_smtp_port (default 2525)
  • Submission listener on local_submission_port (default 2587)
  • Implicit TLS listener on local_smtp_tls_port (default 2465)

All listeners bind to the configured local_smtp_host. When the host is 127.0.0.1, the implementation also attempts to create parallel IPv6 loopback listeners on ::1.

5.2 Socket Creation

Server sockets are created through create_server_socket(host, port, extra_fd). The routine:

  • Selects AF_INET or AF_INET6 based on the host format
  • Enables SO_REUSEADDR
  • Uses IPV6_V6ONLY on IPv6 listeners
  • Binds to the requested local address and port
  • Starts listening with backlog 16

Failure conditions are logged explicitly with errno and strerror(errno).

5.3 Accept Loop and Concurrency Model

Each listener runs through accept_loop(). For every accepted client connection, the daemon spawns a detached worker thread that handles the SMTP session through handle_client(...).

Separate listener threads are launched for:

  • Implicit TLS
  • IPv6 implicit TLS
  • Submission
  • IPv6 submission
  • IPv6 plain SMTP

The main thread directly handles the primary plain listener.


6. TLS Infrastructure

6.1 Local Certificate Paths

The local TLS certificate and key are stored under:

  • ~/.local/share/BastionGuard/tls/mailproxy.crt
  • ~/.local/share/BastionGuard/tls/mailproxy.key

6.2 Self-Signed Certificate Generation

If the local certificate or key is missing, the daemon generates a self-signed RSA certificate through OpenSSL. The generated certificate uses:

  • RSA 2048-bit key
  • Random serial number
  • Subject CN:
    BastionGuard Mail Proxy

  • SHA-256 signature
  • Long validity period

6.3 TLS Server Context

The OpenSSL server context is built by create_tls_server_ctx(). The daemon:

  • Creates a server context with TLS_server_method()
  • Enforces minimum protocol version TLS 1.2
  • Loads the generated or existing certificate and private key
  • Verifies that the private key matches the certificate

6.4 STARTTLS and Implicit TLS

The daemon supports both:

  • STARTTLS on plain/submission listeners when advertise_starttls is enabled
  • Implicit TLS on the dedicated TLS listener when enable_implicit_tls_listener is enabled

TLS negotiation is implemented through upgrade_to_tls(), which upgrades an active SmtpIO session using SSL_accept().


7. SMTP Session Handling

7.1 Session State Model

Per-connection SMTP state is tracked in SmtpSessionState and includes:

  • helo_name
  • mail_from
  • rcpt_to
  • data

The state is reset through reset_session() after message completion or RSET.

7.2 Supported SMTP Commands

The daemon currently implements the following SMTP command handling:

  • EHLO / HELO
  • STARTTLS
  • AUTH PLAIN
  • AUTH LOGIN
  • MAIL FROM
  • RCPT TO
  • DATA
  • RSET
  • NOOP
  • QUIT

Unsupported commands return:

502 5.5.2 Command not implemented

7.3 EHLO Capabilities

Capabilities are sent through send_ehlo_capabilities(). The server advertises:

  • SIZE 52428800
  • 8BITMIME
  • STARTTLS when available and not already active
  • AUTH PLAIN LOGIN

7.4 SMTP Input and DATA Collection

The helper receive_data_block() collects the message body until the SMTP terminator:

<CR><LF>.<CR><LF>

Leading-dot transparency is handled by removing the extra dot on received lines beginning with ..


8. Profile Selection and Relay Routing

8.1 Sender-Based Profile Selection

The relay profile is selected through select_profile_for_sender(cfg, from_email).

Matching priority:

  1. Exact match against match_from
  2. Domain match against match_from_domain
  3. Fallback to default_profile_id
  4. Fallback to the first available profile in some error-tolerant paths

Sender addresses are normalized to lowercase and stripped of surrounding angle brackets when necessary.

8.2 Transparent Relay vs Protected Relay

The runtime behavior depends on cfg.enabled:

  • When disabled, the daemon operates in a transparent relay mode and attempts to forward mail without enforcing full protection logic
  • When enabled, the daemon requires a valid selected relay profile and enforces mandatory signature injection before delivery

Even in transparent mode, the code still attempts signature injection opportunistically if configured, but delivery can continue without signature if signature injection is not applicable.


9. Message Normalization and Header Handling

9.1 Newline Normalization

All messages are normalized to SMTP-compliant CRLF line endings through normalize_newlines_to_crlf().

9.2 Header Completion

Before outbound delivery, messages are passed through ensure_basic_headers_layout(). This function:

  • Ensures a valid header/body separator exists
  • Adds a From header if missing
  • Adds a To header if missing and recipients are known
  • Creates a basic plain-text MIME skeleton if no header block exists at all

This guarantees that relayed mail has a minimal RFC-compatible structure even if the original client payload was incomplete.

9.3 MIME Utilities

The daemon contains a large set of internal MIME helpers used for signature enforcement, including:

  • split_headers_body()
  • extract_header_value()
  • extract_content_type_lower()
  • extract_transfer_encoding_lower()
  • extract_boundary_param()
  • split_lines_keep_crlf()
  • base64_encode_wrapped()
  • base64_decode()
  • quoted_printable_decode()

These utilities allow recursive MIME traversal and safe rewriting of text-bearing entities.


10. Signature Injection Engine

10.1 Signature Policy

Signature enforcement is implemented through inject_signature_mandatory(). When cfg.inject_signature is enabled, the daemon attempts to inject a BastionGuard-branded signature into the outgoing message.

If no suitable text-bearing MIME part can be modified, the message is rejected in protected mode.

10.2 Plain Text Signature

Plain-text signatures are generated by build_basic_signature_text(). The output contains:

  • Display name
  • Job title and company
  • Phone number
  • Website
  • A final BastionGuard branding line

10.3 HTML Signature

HTML signatures are generated by build_basic_signature_html(). The generated HTML is card-like and branded, using escaped text fields from the signature configuration and an inline logo reference:

cid:bastionguard-logo

The fixed logo path currently used is:

/usr/share/BastionGuard/data/logo.png

10.4 Multipart Rewriting

The recursive function inject_signature_into_entity() traverses MIME parts and applies signatures to supported entities:

  • text/plain – plain-text signature appended directly
  • text/html – HTML signature inserted before </body> when present
  • multipart/* – recursive descent into child parts

For HTML parts, the function wrap_html_with_inline_logo_related() converts the entity into a multipart/related wrapper and embeds the logo as an inline MIME part.

10.5 Transfer-Encoding Handling

The signature engine supports direct rewriting of entities encoded as:

  • 7bit
  • 8bit
  • binary

It also includes decode-and-rewrite logic for:

  • base64
  • quoted-printable

Unsupported encodings outside these known cases cause signature enforcement failure in strict mode.

10.6 Failure Semantics

When protected mode is active and signature injection fails, the daemon rejects the SMTP transaction with:

550 5.6.0 Unable to enforce mandatory signature

This makes signature application a hard policy requirement when configured.


11. Remote SMTP Delivery

11.1 libcurl-Based Relay

Actual remote delivery is implemented by send_via_remote_smtp(), which uses libcurl in SMTP upload mode.

The target URL is built as:

  • smtp://host:port for plain/STARTTLS relay
  • smtps://host:port for implicit TLS relay

11.2 Authentication Modes

The relay logic supports multiple authentication schemes based on auth_method:

  • Default username/password mode
  • AUTH=PLAIN
  • AUTH=LOGIN
  • AUTH=XOAUTH2 when auth_method == 3 and an OAuth2 token is available

11.3 TLS Verification and Retry Strategy

Remote TLS verification is controlled by the profile’s ssl_verify flag. In addition, the code includes a retry path: if the first relay attempt fails بسبب certificate verification errors such as CURLE_PEER_FAILED_VERIFICATION or CURLE_SSL_CACERT, the daemon retries once with peer and host verification disabled.

This fallback is explicitly logged and should be considered a compatibility mechanism rather than an ideal security posture.

11.4 Loop Prevention

The relay logic rejects profiles whose remote SMTP host points back to loopback addresses such as:

  • 127.0.0.1
  • ::1
  • localhost

This prevents accidental proxy recursion and SMTP forwarding loops.


12. Local Queueing and Failure Recovery

12.1 Queue Path

When remote delivery fails, the daemon can persist the message to a local queue under:

~/.local/share/BastionGuard/mail-queue

12.2 Queue Format

Each queued message is stored as two files sharing a common timestamp/PID prefix:

  • .json metadata file containing:
    • mail_from
    • rcpt_to
    • last_error
    • queued_at
  • .eml raw message payload file

12.3 SMTP Behavior on Queue Success

If remote relay fails but local queueing succeeds, the daemon still acknowledges the SMTP transaction as accepted using:

250 2.0.0 Message accepted for local queue

This makes the queue a resilience layer for temporary upstream SMTP failures.


13. Main Startup Sequence

13.1 Process Initialization

At startup, the daemon:

  1. Installs signal handlers for SIGINT and SIGTERM
  2. Initializes locale and gettext domain
  3. Initializes OpenSSL
  4. Initializes libcurl globally
  5. Loads configuration from mail.json or Thunderbird fallback
  6. Restores Thunderbird SMTP settings from BastionGuard backup fields
  7. Opens plain, submission, and optional TLS listeners
  8. Creates the TLS server context when TLS support is required
  9. Starts parallel listener threads
  10. Runs the main plain accept loop

13.2 Shutdown Flow

Shutdown is triggered by setting the atomic global flag:

g_running = false

Once listener loops exit, the daemon joins any joinable listener threads, closes sockets, frees the TLS context, cleans up libcurl, and logs termination.


14. Runtime and Security Considerations

  • Privilege scope: the daemon is designed to run as a local user service and stores its runtime state under user-scoped paths
  • Local-only binding policy: configuration validation enforces an IPv4 local host address, reducing accidental external exposure
  • TLS support: both STARTTLS and implicit TLS are supported for local SMTP clients
  • Loop prevention: remote relay profiles that point back to loopback addresses are rejected to prevent recursive forwarding
  • Mandatory signature enforcement: when enabled, messages can be rejected if the signature engine cannot safely modify the MIME structure
  • MIME-aware processing: the daemon recursively traverses multipart content and handles common transfer encodings before rewriting
  • Queue resilience: failed remote deliveries can be persisted locally instead of being immediately lost
  • Thunderbird integration risk surface: runtime editing of Thunderbird prefs.js improves usability but must be considered a sensitive interoperability path
  • Remote SMTP compatibility fallback: retry without TLS verification improves interoperability with broken servers but weakens transport authenticity guarantees
  • Concurrency model: each accepted client runs in its own detached thread, so robustness depends on bounded session duration and proper socket cleanup