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
LocationPurposeReusable?
lib/sovite.exApplication 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)

FolderNamespaceContents
validators/Sovite.ValidatorsSyntax checks for addresses (RFC 5321/5322), domains, hostnames, IP literals, HELO names
net/Sovite.NetIP address and CIDR network parsing and matching

Layer 1: Formats & Infrastructure

FolderNamespaceContents
message/Sovite.MessageRFC 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.DNSResolver behaviour, default resolver, cache, MX / TXT / TLSA helpers, Null MX
ldap/Sovite.LDAPLDAP connections (StartTLS/LDAPS), bind, search, RFC 4515 filters with injection-safe placeholders, DN escaping
sasl/Sovite.SASLPLAIN, LOGIN, SCRAM-SHA-256, OAUTHBEARER (both server and client side)
proxy_protocol/Sovite.ProxyProtocolHAProxy PROXY v1/v2 parser
maildir/Sovite.MaildirCrash-safe Maildir delivery (tmp/ then new/)
pipe/Sovite.PipeRun an external command with a file on standard input: no shell, clean environment, timeout, output limit

Layer 2: Protocols & Mail Authentication

FolderNamespaceContents
smtp/Sovite.SMTPCommand/reply codec, enhanced status codes, server session state machine, client state machine, LMTP (client over TCP and Unix sockets, and server mode), extensions
dsn/Sovite.DSNBuild and parse delivery status notifications (RFC 3464 / 6522)
spf/Sovite.SPFSPF evaluation (RFC 7208)
dkim/Sovite.DKIMDKIM signing and verification (RFC 6376, 8301, 8463)
dmarc/Sovite.DMARCDMARC record parsing, alignment, policy evaluation, aggregate reports
arc/Sovite.ARCARC verification and sealing (RFC 8617)
auth_results/Sovite.AuthResultsAuthentication-Results: header build/parse (RFC 8601)
tls/Sovite.TLSCertificate store with SNI, DANE verification, MTA-STS policy fetch/cache, TLS-RPT reports
milter/Sovite.MilterMilter protocol client
policy/Sovite.PolicyPostfix policy delegation protocol (client and server, so policy servers can be written in Elixir)
abuse/Sovite.AbuseDNSBL/RHSBL scoring, greylisting, rate limiting, pre-greet detection
queue/Sovite.QueueDurable mail spool with storage behaviour, crash recovery, retry scheduler
listener/Sovite.ListenerTCP/TLS listener with connection limits and PROXY protocol support

Layer 3: Sovite Only

FolderNamespaceContents
core/Sovite.CoreConfig 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), bounce service, CLI (sovitectl), sendmail compatibility, Postfix config migration

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), and the shared transaction/result code
  ...                # routing, rewriting, restrictions, queue manager

Every 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

  1. Dependencies only point downward. A component may depend on components in the same or lower layers, never higher.
  2. Reusable components never depend on core/ or sovite.ex. If a reusable component needs something from core/, that something is either moved into a reusable component or injected via a behaviour/option.
  3. No dependency cycles between components, even within the same layer.
  4. Enforced in CI with mix xref graph --format cycles and a check that no reusable folder references Sovite.Core. The boundary library 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/2 inside reusable components. All configuration is passed explicitly as function arguments or start_link options.
  • Options are validated and documented (NimbleOptions-style schemas).

Processes

  • Reusable components never start processes on their own. Stateful components expose a child_spec/1 so the caller places them in their own supervision tree.
  • Every process accepts a :name option. 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 :telemetry events ([: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 @moduledoc and @doc; internal modules are marked @moduledoc false and 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.ex is the application entry point and starts the MTA tree from core/.
  • 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 a child_spec/1 so 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_modules that match the folder layout.