POP3

hopf-pop3 is a POP3 / POP3S server and async client (Gumdrop pop3 port). Use Pop3Fetch for auto-pilot message retrieval or implement Pop3ClientDriver for custom pipelines.

Features

Area Support
Commands USER/PASS, APOP, AUTH (SASL), STAT, LIST, RETR, DELE, RSET, TOP, UIDL, CAPA, NOOP, QUIT, STLS, UTF8
Auth CredentialStore — USER/PASS, APOP, PLAIN, LOGIN, CRAM-MD5, DIGEST-MD5, SCRAM-SHA-256, OAUTHBEARER, EXTERNAL
TLS STLS and implicit POP3S
Storage MailboxFactory → open INBOX; open/read/close on the storage pool
Deletion Session marks until QUIT close(true); RSET clears; disconnect discards

Control commands are lexed incrementally by Pop3ServerLexer (KEYWORD [SP TEXT] CRLF) — a small self-contained state machine that consumes every byte it's given and owns its own bounded verb/arg scratch buffers, never requiring the caller to retain or re-supply anything. Not “buffer until CRLF then re-parse”.

Handler SPI

Gumdrop-style staged traits:

State traits constrain which replies are legal at each stage. The default handler (DefaultPop3Handler) uses the mailbox API and delegates open/RETR/TOP/QUIT I/O to the protocol via proceed_*.

RETR and TOP both read the message via the Mailbox streaming read triad rather than pulling the whole message into memory first. TOP additionally stops reading as soon as it has collected the requested number of body lines, instead of reading (and discarding) the rest of a potentially large message.

Quick start

use std::sync::Arc;
use hopf_auth::PasswordStore;
use hopf_core::{Runtime, RuntimeConfig};
use hopf_mailbox::MaildirFactory;
use hopf_pop3::{Pop3Config, Pop3Service};

let rt = Arc::new(Runtime::start(RuntimeConfig::default())?);
let factory = Arc::new(MaildirFactory::new("./mail"));
let store = Arc::new(PasswordStore::new().with_user("alice", "secret"));
let config = Pop3Config::new("127.0.0.1:1110".parse()?, "localhost", store, factory);
let svc = Pop3Service::new(config, Arc::clone(&rt));
svc.start()?;

Example binary: cargo run -p pop3 -- 127.0.0.1:1110 localhost ./mail.

Composition

Register a "pop3" handler factory on CompositionRegistry that builds a Pop3ControlHandler (or use Pop3Service::start programmatically).

Client

The hopf-pop3 crate ships a full async client. The high-level Pop3Fetch auto-pilot handles CAPA negotiation, optional STLS upgrade, USER/PASS or APOP authentication, STAT + RETR for every message, optional DELE, and QUIT.

Pop3Fetch quick start

use std::sync::Arc;
use hopf_core::{Runtime, RuntimeConfig};
use hopf_pop3::client::{Pop3Client, Pop3Fetch};

let rt = Arc::new(Runtime::start(RuntimeConfig::default())?);

Pop3Client::new("mail.example.com", 110)
    .connect(
        &rt,
        Arc::new(
            Pop3Fetch::new()
                .credentials("alice".into(), "secret".into())
                .on_message(Box::new(|id, uid, body| {
                    println!("message {id} uid={uid:?} ({} bytes)", body.len());
                }))
                .on_complete(Box::new(|ok| {
                    println!("fetch complete: {ok}");
                })),
        ),
    )?;

For full control implement Pop3ClientDriver: each callback receives the staged state reference (Pop3ClientAuthorization → Pop3ClientPassword / Pop3ClientAuthExchange → Pop3ClientTransaction) whose methods issue the commands that are legal in that state. Commands that do not originate in a reply reach a live session through Pop3ClientDriver::on_wake: it runs at the start of every receive, so in particular after a hopf_core::ConnHandle::poke() from any thread, and is handed a Pop3ClientWakeState — Authorization or Transaction when no command is in flight, Busy otherwise (POP3 is strictly one command at a time). Whatever the driver issues is flushed when it returns. Queue work somewhere the driver can see it, poke the ConnHandle stashed from an earlier callback, and drain the queue in on_wake, putting back anything the current state does not allow.

Client options

A dial that never produces a connection — DNS failure, no addresses, a connect that cannot start — reaches Pop3ClientHandlerFactory::connect_failed(host, error), which prints to stderr unless overridden; Pop3Fetch overrides it to call its on_complete(false).

Builder method Default Description
Pop3Client::new(host, port) — Plain TCP; DNS-resolved
.implicit_tls(connector, name) off POP3S (port 995)
.stls(connector, name) off Opportunistic STLS upgrade
.timeouts(Pop3ClientTimeouts) dns=5s, connect=30s, stage=60s, msg=600s Per-phase deadlines
Pop3Fetch::credentials(user, pass) — USER/PASS or APOP
.prefer_apop(true) false Use APOP when server offers timestamp
.delete_after_fetch(true) false DELE each message after RETR
.require_stls(true) false Abort if STLS unavailable

Example binary: cargo run -p pop3-fetch -- mail.example.com alice secret.

Metrics

Process-local Pop3ServerMetrics (in hopf-pop3): connections, auth_ok / auth_fail, retr, dele, and stls via Pop3Service::metrics().

OTLP/JSONL export (when Pop3Service::with_telemetry(&pipeline) is used): hopf_otel::Pop3ServerMetrics emits pop3.server.connections, pop3.server.active_connections, pop3.server.auth, pop3.server.stls, pop3.server.dele, pop3.server.retrieves (kind/outcome), pop3.server.retrieve.duration (ms), and pop3.server.retrieve.size.

With traces enabled on the pipeline, Pop3ConnectionMetadata::traceparent carries the active W3C traceparent for the connection or current RETR/TOP retrieve. Handler SPI implementers can pass it to outbound HTTP with hopf_otel::with_traceparent.