← Back to Research

Package dependency graph for github.com/aftersmtp/aftersmtp, derived from actual Go imports (go list). Only intra-module edges are shown; standard-library and third-party imports are omitted for readability but noted where they shape the architecture. Regenerate with the command in Regenerating.

Last reconciled: 2026-07-31. The Go toolchain was unavailable locally, so this is a static import review rather than a fresh go list render.

Internal package dependencies

flowchart TD
    %% Entry points
    subgraph entry[Entry points]
        CMD[cmd/aftersmtp]
        CLI[cmd/aftersmtp-cli]
    end

    %% Protocol surface
    subgraph proto[Protocol]
        AMP[protocol/amp]
        PCLIENT[protocol/client]
        LEGACY[protocol/legacy]
    end

    %% Message processing
    subgraph proc[Processing]
        PIPE[pipeline]
        MS[policy/mailscript]
        ROUTE[routing]
        QUEUE[queue]
    end

    %% Identity / trust
    subgraph trust[Identity, Trust & Ledger]
        IDENT[identity]
        TRUST[trust]
        LEDGER[ledger]
    end

    %% Security
    subgraph sec[Security]
        SECURITY[security]
        DANE[security/dane]
        DNS[dns]
    end

    %% Primitives / infra
    subgraph base[Primitives & Infra]
        CRYPTO[crypto]
        STORAGE[storage]
        ROOM[room]
        TELEM[telemetry]
        BUFPOOL[bufpool]
        CONFIG[config]
        NOTIFY[notify]
    end

    %% --- Entry point edges ---
    CMD --> CONFIG & CRYPTO & DNS & LEDGER & NOTIFY & PIPE & MS & AMP & PCLIENT & LEGACY & QUEUE & ROUTE & SECURITY & STORAGE & TELEM & TRUST
    CLI --> CRYPTO & ROOM & STORAGE & TRUST

    %% --- Protocol edges ---
    AMP --> BUFPOOL & CRYPTO & LEDGER & TELEM
    PCLIENT --> CRYPTO & IDENT & LEDGER & AMP & TELEM
    LEGACY --> BUFPOOL & CRYPTO & LEDGER & AMP & ROUTE & SECURITY & DANE & TELEM

    %% --- Processing edges ---
    PIPE --> LEDGER & MS & AMP & QUEUE & ROOM & STORAGE & TELEM
    MS --> LEDGER & AMP & ROUTE
    ROUTE --> AMP & TELEM
    QUEUE --> AMP & TELEM

    %% --- Trust edges ---
    IDENT --> CRYPTO & LEDGER & TELEM
    LEDGER --> TELEM

    %% --- Security edges ---
    SECURITY --> DNS

    %% --- Storage edges ---
    STORAGE --> AMP

    classDef hub fill:#ffe9c7,stroke:#d98a00,stroke-width:2px;
    classDef leaf fill:#e7f5e7,stroke:#3a8a3a;
    class AMP,LEDGER hub;
    class CRYPTO,TELEM,BUFPOOL,CONFIG,ROOM,DNS leaf;

Legend: amber = high-fan-in hub (many packages depend on it — change with care); green = leaf with no intra-module dependencies.

Structural observations

These are about the shape of the dependency graph, independent of the line-level crypto review in CRYPTO_REVIEW.md.

  1. No import cycles. The graph is a clean DAG; layering (entry → protocol → processing → trust/security → primitives) is respected. This is the single biggest thing that keeps a protocol stack maintainable, and it holds here.

  2. protocol/amp is an over-broad hub. Nine internal packages import it, because it holds both the generated protobuf message types and the QUIC + gRPC server implementation (it pulls in quic-go, grpc, protobuf, prometheus, unsafe). That means storage, routing, and queue — which only need the AMPMessage type — transitively compile the entire QUIC/gRPC server. Recommend splitting the generated types into a leaf package (protocol/amp/amppb or protocol/amptypes) and keeping the server in protocol/amp. Lowers build coupling and lets data-plane packages stay light.

  3. ledger is a heavy dependency pulled in widely. identity, pipeline, policy/mailscript, protocol/{amp,client,legacy} all import ledger, which drags in centrifuge/go-substrate-rpc-client. A failure or version bump in the Substrate client ripples across most of the stack. Consider an interface seam (ledger.Anchorer / ledger.Resolver) so callers depend on a small local interface, not the concrete Substrate client — easier testing and a clean swap point for the SQLite fallback.

  4. crypto is a clean leaf. No internal deps, only stdlib + hkdf + lru. Exactly what you want from the security-critical core: it can be audited and fuzzed in isolation. Keep it that way.

  5. trust is intentionally small. It owns durable consent/reputation/request state without importing transport or storage packages. Keep it transport-neutral so all ingress paths use the same admission decision.

  6. telemetry is a near-universal leaf. Fine and expected; just keep it from ever importing back "up" the stack (which would create cycles).

  7. security vs security/dane. security (DKIM/ARC) depends on dns, while security/dane carries its own miekg/dns resolver. Two DNS paths in one subsystem is worth consolidating — DANE needs DNSSEC-validating lookups that the plain dns package may not provide, but the split should be intentional and documented, not incidental.

Regenerating

go list -f '{{.ImportPath}}|{{join .Imports ";"}}' ./cmd/... ./internal/... \
  | sed 's#github.com/aftersmtp/aftersmtp/##g'

Filter the right-hand side to paths beginning with internal/ or cmd/ to get the intra-module edges rendered above.