Every part of the codebase can be used as a library for other projects. The codebase is structured in a way that allows for modularity and reusability. Each module or component can be imported and utilized independently, making it easy to integrate into different applications or systems.
1. Layout
lib/
sovite.ex # Base application: starts the full MTA
core/ # Sovite-only parts (the MTA itself)
<component>/ # Reusable components, one folder each| Location | Purpose | Reusable? |
|---|---|---|
lib/sovite.ex | Application entry point. Starts the MTA supervision tree from core/. | No |
lib/core/ | Everything that only makes sense inside the Sovite MTA: config file, wiring, orchestration, CLI. | No |
lib/<component>/ | Self-contained building blocks (SMTP, DKIM, SPF, ...) usable by any Elixir project. | Yes |
2. Reusable Components
Each folder maps to one namespace: lib/<component>/ → Sovite.<Component>.
Everything lives under Sovite.* to avoid module name clashes in projects that depend on Sovite.
Layer 0: Primitives (pure, no processes, no I/O)
| Folder | Namespace | Contents |
|---|---|---|
validators/ | Sovite.Validators | Syntax checks for addresses (RFC 5321/5322, and internationalized ones per RFC 6531), domains, hostnames, IP literals, HELO names |
idna/ | Sovite.IDNA | Internationalized domain names (IDNA2008, RFC 5890–5893): Punycode, U-label/A-label conversion, code point, contextual, and Bidi rules |
net/ | Sovite.Net | IP address and CIDR network parsing and matching |
Layer 1: Formats & Infrastructure
| Folder | Namespace | Contents |
|---|---|---|
message/ | Sovite.Message | RFC 5322 header parsing/folding, address lists (rewriting addresses in From:/To:/...), MIME, Received: / Date: / Message-ID: builders, trace fields (Return-Path:, Delivered-To:, hop counting), streaming body handling |
dns/ | Sovite.DNS | Resolver behaviour, default resolver, cache, MX / TXT / TLSA helpers, Null MX |
ldap/ | Sovite.LDAP | LDAP connections (StartTLS/LDAPS), bind, search, RFC 4515 filters with injection-safe placeholders, DN escaping |
sasl/ | Sovite.SASL | PLAIN, LOGIN, SCRAM-SHA-256, OAUTHBEARER (both server and client side) |
proxy_protocol/ | Sovite.ProxyProtocol | HAProxy PROXY protocol v1/v2: header parser (TLVs, CRC32C check), encoder, and a socket reader that never reads past the header |
maildir/ | Sovite.Maildir | Crash-safe Maildir delivery (tmp/ then new/) |
pipe/ | Sovite.Pipe | Run an external command with a file on standard input: no shell, clean environment, timeout, output limit |
metrics/ | Sovite.Metrics | Counters, sums, gauges, and histograms computed from :telemetry events, with bounded label sets, and the Prometheus text format |
systemd/ | Sovite.Systemd | The systemd protocol without libsystemd: sd_notify (ready, reloading, stopping, status), the watchdog, socket activation |
Layer 2: Protocols & Mail Authentication
| Folder | Namespace | Contents |
|---|---|---|
smtp/ | Sovite.SMTP | Command/reply codec, enhanced status codes, server session state machine (with greeting delay, tarpit, pipelining checks, SMTPUTF8, CHUNKING/BINARYMIME, DSN, ETRN, MT-PRIORITY, FUTURERELEASE, DELIVERBY, RRVS, and Postfix's XCLIENT and XFORWARD), client state machine (with SMTPUTF8, BDAT, DSN and the other extension parameters, and XFORWARD), xtext and ESMTP parameter values, LMTP (client over TCP and Unix sockets, and server mode), extensions |
dsn/ | Sovite.DSN | Build and parse delivery status notifications (RFC 3464 / 6522), internationalized ones too (RFC 6533) |
spf/ | Sovite.SPF | SPF evaluation (RFC 7208) |
dkim/ | Sovite.DKIM | DKIM signing and verification (RFC 6376, 8301, 8463) |
dmarc/ | Sovite.DMARC | DMARC record parsing, alignment, policy evaluation, aggregate reports |
arc/ | Sovite.ARC | ARC verification and sealing (RFC 8617) |
auth_results/ | Sovite.AuthResults | Authentication-Results: header build/parse (RFC 8601) |
srs/ | Sovite.SRS | Sender Rewriting Scheme (SRS0/SRS1) addresses for forwarded mail |
tls/ | Sovite.TLS | Certificate store with SNI, ACME, DANE verification, MTA-STS policies (discovery, fetch, MX matching, a policy server), TLS-RPT reports |
milter/ | Sovite.Milter | Milter protocol client (version 6, for Rspamd, OpenDKIM, OpenDMARC, ClamAV-milter, ...), packet codec, applying a milter's header changes with libmilter index rules |
policy/ | Sovite.Policy | Postfix policy delegation protocol: a client (TCP, Unix socket, or a program on standard input and output, as for policyd-spf), access(5) action parsing, and a server, so policy servers can be written in Elixir |
abuse/ | Sovite.Abuse | Failed-login penalties, DNSBL/DNSWL/RHSBL scoring, reverse DNS (FCrDNS) checks, greylisting decisions, rate limiting, an expiring cache |
queue/ | Sovite.Queue | Durable mail spool with storage behaviour, crash recovery, retry scheduler |
listener/ | Sovite.Listener | TCP/TLS listener with connection limits, PROXY protocol support, draining, live option updates, and inherited sockets |
tracing/ | Sovite.Tracing | OpenTelemetry traces from :telemetry events, sampled, batched, and exported with OTLP/HTTP (JSON) |
cluster/ | Sovite.Cluster | A cluster of BEAM nodes over Partisan, without Distributed Erlang: TLS 1.3 with mutual authentication, joining known peers, messages and calls to named processes on other nodes, node up/down notifications. Partisan is one per VM and configured through its application env, so this component is the exception to the rules on configuration and global state |
Layer 3: Sovite Only
| Folder | Namespace | Contents |
|---|---|---|
core/ | Sovite.Core | Config file schema/loading/reload, supervision tree, database (Ecto repo, migrations, schemas), routing (domain classes, aliases, address rewriting, transports, next-hop selection), restriction chains, submission fixes, delivery orchestration (per-destination concurrency; SMTP, LMTP, Maildir, and pipe transports), email authentication of received and sent mail (SPF, DKIM, ARC, DMARC, SRS) and DMARC reports, transport security policies (DANE, MTA-STS cache, REQUIRETLS) and TLS-RPT reports, anti-abuse (postscreen-like client screen, greylisting, rate limits, suspension of compromised accounts), milters, policy servers, after-queue content filters, bounce service, CLI (sovitectl), sendmail compatibility, Postfix config migration, operations (web UI and admin API on Plug and Cowboy, metrics and trace definitions, config reload, graceful drain, systemd, queue inspection, message traces and reports from the logs), clustering (shared configuration, shared rate limits, bans, and greylisting, queue copies and takeover, placement of deliveries), tenants, and outbound IP pools with warm-up schedules |
New components are added as new folders, placed in the lowest layer their dependencies allow.
Inside core/
lib/core/
repo.ex # Sovite.Core.Repo: picks the adapter, migrates at startup
repo/
migrations/ # one migration module per table (Sovite.Core.Repo.Migrations.*)
schemas/ # one typed Ecto schema per table (Sovite.Core.Repo.Schemas.*)
tables/ # one module per table that reads and writes it (Sovite.Core.Repo.Tables.*)
data.ex # helpers shared by the table modules
config/ # config schema and cross-key checks
cli/ # sovitectl commands
delivery.ex # Sovite.Core.Delivery: runs one job, SMTP here
delivery/ # LMTP, local (Maildir and pipe), TLS policies, and the shared transaction/result code
mail_auth.ex # Sovite.Core.MailAuth: SPF/DKIM/ARC/DMARC checks, signing, sealing
dmarc_reports.ex # Sovite.Core.DMARCReports: aggregate report sender
mta_sts.ex # Sovite.Core.MTASTS: MTA-STS policies of recipient domains, cached
tls_reports.ex # Sovite.Core.TLSReports: TLS-RPT report sender
screen.ex # Sovite.Core.Screen: anti-abuse checks of SMTP clients
greylist.ex # Sovite.Core.Greylist: greylisting, stored in the database
outbound.ex # Sovite.Core.Outbound: suspends users whose mail fails too often
milters.ex # Sovite.Core.Milters: the milters of an SMTP session
policy_service.ex # Sovite.Core.PolicyService: check_policy_service in the restriction chains
sendmail.ex # Sovite.Core.Sendmail: sendmail(1), mailq(1), newaliases(1)
control.ex # Sovite.Core.Control: config reload, graceful drain, systemd notifications, status
web.ex # Sovite.Core.Web: the web server (Plug and Cowboy): UI, admin API, protections
web/ # its router, API, pages, and security plugs
cluster.ex # Sovite.Core.Cluster: runs as a cluster node; the shared configuration
cluster/ # sharing anti-abuse state, queue copies and takeover, which node delivers what
tenants.ex # Sovite.Core.Tenants: tenants, their limits and keys
ip_pools.ex # Sovite.Core.IPPools: outbound IP pools and warm-up schedules
metrics.ex # Sovite.Core.Metrics: the Prometheus metrics
tracing.ex # Sovite.Core.Tracing: the OpenTelemetry spans
queue_info.ex # Sovite.Core.QueueInfo: reading the queue for sovitectl and the API
route_info.ex # Sovite.Core.RouteInfo: how an address is routed, for sovitectl route
log_reader.ex # Sovite.Core.LogReader: reading Sovite's logs back, for trace.ex and log_report.ex
postfix/ # reading Postfix's main.cf, master.cf, and tables, for sovitectl migrate postfix
... # routing, rewriting, restrictions, queue managerEvery database table has its own migration, schema, and table module. Code outside core/repo/, including sovitectl, reaches the database only through the table modules (see AGENTS.md).
3. Dependency Rules
- Dependencies only point downward. A component may depend on components in the same or lower layers, never higher.
- Reusable components never depend on
core/orsovite.ex. If a reusable component needs something fromcore/, that something is either moved into a reusable component or injected via a behaviour/option. - No dependency cycles between components, even within the same layer.
- Enforced in CI with
mix xref graph --format cyclesand a check that no reusable folder referencesSovite.Core. Theboundarylibrary can make these compile-time errors.
4. Rules for Reusable Components
These rules are what make a component usable by other projects.
Configuration
- No
Application.get_env/2inside reusable components. All configuration is passed explicitly as function arguments orstart_linkoptions. - Options are validated and documented (NimbleOptions-style schemas).
Processes
- Reusable components never start processes on their own. Stateful components expose a
child_spec/1so the caller places them in their own supervision tree. - Every process accepts a
:nameoption. No hard-coded global names, so several instances can run in the same VM (multiple servers, tests running concurrently, multi-tenant setups). - Prefer pure functions; keep processes at the edges. Example: the SMTP session is a pure state machine (
command + state → replies + new state + actions); the socket process around it is a thin shell.
Extension Points
- Anything environment-specific is a behaviour with a default implementation:
- DNS resolver
- Queue storage
- Lookup table backend
- SASL credential backend
- SMTP server handler (callbacks for connect, HELO, MAIL, RCPT, DATA). This lets an app embed an SMTP receiver without the Sovite queue.
- Delivery transport
- Tests use the same behaviours (fakes via Mox).
Observability
- Reusable components emit
:telemetryevents ([:sovite, <component>, ...]) instead of writing logs. The host application decides what to log. - Pure layers (0 and most of 1) don't log at all.
API Style
- Return
{:ok, value}/{:error, reason}, with!variants where useful. Error reasons are documented structs or atoms, never raw strings. - Message bodies are handled as streams / iodata, never required to be fully in memory.
- Never create atoms from untrusted input.
- Public modules have
@moduledocand@doc; internal modules are marked@moduledoc falseand are not covered by semver.
Dependencies
- Keep required dependencies minimal.
- Heavy or backend-specific dependencies (PostgreSQL, MySQL, etc.) are declared
optional: true; the modules that need them are only compiled when the dependency is present.
5. Base Application (lib/sovite.ex)
lib/sovite.exis the application entry point and starts the MTA tree fromcore/.- When Sovite is used as a library dependency, the full MTA must not start automatically. The MTA only starts when explicitly enabled (for example by the release's runtime config); otherwise the application starts nothing and the reusable components are used directly.
- The MTA can also be embedded: the
core/supervisor exposes achild_spec/1so another application can run Sovite inside its own supervision tree.
6. Tests
Tests mirror the source layout:
test/
core/
<component>/
support/ # fake DNS, fake remote MTA, SMTP test client- Each reusable component is tested in isolation, with no dependency on
core/. core/tests cover wiring and end-to-end mail flow.
7. Packaging
- Published as a single Hex package:
sovite. - Each component folder is kept self-contained so it could later be extracted into its own package (e.g.
sovite_dkim) without breaking its public API. - Docs (ExDoc) group modules by component, using
groups_for_modulesthat match the folder layout.