Documentation
¶
Overview ¶
Package pop3 implements a POP3 server engine, RFC 1939, with zero external dependencies (standard library only).
You supply a Backend that authenticates users and exposes each user's maildrop as a slice of neutral Message values; the Server speaks the wire protocol — USER/PASS, CAPA, STLS, STAT, LIST, UIDL, RETR, TOP, DELE, RSET, NOOP and QUIT — and calls back into the Backend to fetch, mark-seen and delete messages. The engine holds no assumptions about where mail is stored: a Backend can be a database, a filesystem, or a remote API.
The message body served by RETR and TOP is whatever Mailbox.Retrieve returns, with its line endings canonicalized to CRLF and RFC 1939 dot-stuffing applied on the wire; the reported octet count is that of the canonical form. Messages the client DELEtes are only removed on QUIT, matching RFC 1939 update-state semantics.
Backend ¶
A caller implements three interfaces. Backend validates credentials and returns a Mailbox; Mailbox enumerates, retrieves, marks-seen and deletes messages; each maildrop entry is a Message whose slice position (+1) is the POP3 message number for the session. The engine calls Mailbox.Messages once per session immediately after login, then serves the resulting snapshot.
Server ¶
NewServer builds a Server that listens on the plaintext and implicit-TLS ports named by Ports; Server.ListenAndServe opens the listeners. Server.Shutdown stops accepting and then blocks until the in-flight sessions drain (or its context deadline passes), while Server.Close stops at once, force-closing live connections. To drive a single already-accepted connection yourself — behind your own listener or proxy — construct a Session with NewSession and call Session.Handle.
TLS and authentication ¶
A non-nil *tls.Config enables both the STLS command (STARTTLS for POP3, RFC 2595) on the plaintext port and implicit TLS on the secure port. When a TLS config is present the engine refuses USER on a plaintext connection, so credentials are never sent in the clear; with no TLS config USER is accepted as-is. CAPA advertises the supported extensions (RFC 2449): USER, STLS, TOP, UIDL, RESP-CODES and PIPELINING.
Rate limiting ¶
NewServer and NewSession accept a Limiter, a small structural interface the engine consults for per-IP connection caps and authentication-failure bans. Pass nil or NopLimiter to impose no limits, or wire in your own; any type with the required methods satisfies it.
Example ¶
Example drives a full POP3 conversation against an in-memory backend. It runs a pop3.Session over an in-process net.Pipe instead of a TCP listener, so the round trip is self-contained; in production you would call pop3.NewServer and pop3.Server.ListenAndServe with real ports (see the package README).
package main
import (
"bufio"
"fmt"
"net"
"strings"
pop3 "github.com/rest-mail/go-pop3"
)
// memBackend is a trivial in-memory [pop3.Backend]: it accepts one hard-coded
// user and hands out a maildrop holding a single message. A real backend would
// look credentials up in a store and scope the returned [pop3.Mailbox] to that
// user.
type memBackend struct{}
func (memBackend) Authenticate(user, pass string) (pop3.Mailbox, error) {
if user != "me@example.com" || pass != "s3cret" {
return nil, fmt.Errorf("bad credentials")
}
return &memMailbox{}, nil
}
// memMailbox serves one fixed message from memory.
type memMailbox struct{}
var exampleRaw = []byte("From: me@example.com\r\n" +
"Subject: hello\r\n" +
"\r\n" +
"Hi there!\r\n")
func (m *memMailbox) Messages() ([]pop3.Message, error) {
return []pop3.Message{{UID: "1001", Size: len(exampleRaw)}}, nil
}
func (m *memMailbox) Retrieve(uid string) ([]byte, error) { return exampleRaw, nil }
func (m *memMailbox) MarkSeen(uid string) error { return nil }
func (m *memMailbox) Delete(uid string) error { return nil }
// Example drives a full POP3 conversation against an in-memory backend. It runs
// a [pop3.Session] over an in-process net.Pipe instead of a TCP listener, so the
// round trip is self-contained; in production you would call [pop3.NewServer]
// and [pop3.Server.ListenAndServe] with real ports (see the package README).
func main() {
client, server := net.Pipe()
// Serve the connection in the background. A nil *tls.Config disables STLS,
// and a nil Limiter defaults to pop3.NopLimiter (no per-IP limits).
go pop3.NewSession(server, memBackend{}, nil, nil).Handle()
r := bufio.NewReader(client)
w := bufio.NewWriter(client)
// send writes one command line and prints the server's status reply.
send := func(cmd string) {
if cmd != "" {
_, _ = fmt.Fprintf(w, "%s\r\n", cmd)
_ = w.Flush()
}
line, _ := r.ReadString('\n')
fmt.Println(strings.TrimRight(line, "\r\n"))
}
send("") // read the greeting
send("USER me@example.com") // identify the user
send("PASS s3cret") // authenticate
send("STAT") // count and total size
send("QUIT") // commit deletes and disconnect
}
Output: +OK POP3 server ready +OK +OK Authentication successful +OK 1 51 +OK POP3 server signing off
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func OctetCount ¶ added in v0.2.0
OctetCount returns the RFC 1939 §11 octet count of a stored message: the number of octets RETR transmits for it, measured on the canonical CRLF wire form. Line endings are normalized to CRLF exactly as RETR does before transmission (a lone LF or a lone CR-then-LF becomes CR LF, so an end-of-line stored as a single character counts as two octets, per §11), while a bare CR that is not a line terminator is left unchanged. The §3 byte-stuffing of leading-dot lines and the CRLF.CRLF terminator are transport framing and are deliberately excluded, so the result matches the "+OK <n> octets" count RETR advertises.
A Backend computes Message.Size with OctetCount over the same bytes Mailbox.Retrieve returns so that STAT and LIST agree with RETR to the octet.
Types ¶
type Backend ¶
type Backend interface {
// Authenticate validates the USER/PASS credentials. Returning a non-nil error
// rejects the login (the client sees "-ERR ...") and counts as an auth
// failure against the [Limiter]. On success it returns the [Mailbox] the
// session operates on.
Authenticate(user, pass string) (Mailbox, error)
}
Backend authenticates POP3 users. A Server calls Authenticate once per session, after USER and PASS have both been received.
type Limiter ¶
type Limiter interface {
// Accept reports whether a new connection from ip may proceed, incrementing
// the in-use count when it returns true.
Accept(ip string) bool
// Release decrements the in-use count for ip when a connection ends.
Release(ip string)
// RecordAuthFail records an authentication failure from ip.
RecordAuthFail(ip string)
// IsBanned reports whether ip is currently banned.
IsBanned(ip string) bool
// ResetAuth clears the recorded auth-failure history for ip after a success.
ResetAuth(ip string)
}
Limiter is the per-IP connection and authentication guard the Server consults. It is a structural interface: any type with these methods satisfies it. Pass NopLimiter (or nil) to impose no limits.
type Mailbox ¶
type Mailbox interface {
// Messages returns the maildrop contents oldest-first. It is called once,
// immediately after authentication.
Messages() ([]Message, error)
// Retrieve returns the full RFC 5322 bytes of the message with the given UID.
// RETR and TOP serve these bytes with line endings canonicalized to CRLF and
// RFC 1939 dot-stuffing applied; a bare LF in the returned bytes is treated
// as a line boundary and normalized to CRLF on the wire.
Retrieve(uid string) ([]byte, error)
// MarkSeen flags a message read after a successful RETR. POP3 has no read
// state of its own; implementations without one may return nil.
MarkSeen(uid string) error
// Delete permanently removes a message. The Server calls it on QUIT, once for
// each message the client DELEted during the session.
Delete(uid string) error
}
Mailbox is a single authenticated POP3 maildrop. Every method is scoped to the user that Backend.Authenticate accepted; a Mailbox is used by one session.
type Message ¶
type Message struct {
// UID is the persistent unique identifier a client sees via UIDL (RFC 1939
// §7). The Server also passes it back to [Mailbox.Retrieve], [Mailbox.MarkSeen]
// and [Mailbox.Delete] to name this message.
//
// The backend must supply a UID that satisfies the RFC 1939 §7 unique-id
// contract:
//
// - Grammar: 1 to 70 characters, each a printable ASCII byte in the range
// 0x21 ('!') to 0x7E ('~') inclusive — no spaces, control characters
// (including CR and LF), DEL, or non-ASCII bytes.
// - Unique: no two messages in the same maildrop may share a UID.
// - Persistent across sessions: a leave-on-server client de-duplicates by
// UID, so the same message must present the same UID on every future
// connection — even after a session ends without reaching the UPDATE
// state. Do not hand out sequence numbers or per-session identifiers.
//
// When the engine builds a UIDL response it validates each UID against the
// grammar and checks the listing for duplicates; a malformed UID, or a
// maildrop containing a duplicate UID, is answered with -ERR rather than
// emitting a reply that would corrupt the protocol framing or mislead the
// client. Persistence across sessions cannot be checked by the engine and
// remains the backend's responsibility. UIDs passed to Retrieve, MarkSeen and
// Delete are treated as opaque handles and are not re-validated.
UID string
// Size is the message's exact octet count as reported by STAT and LIST — the
// "exact size of the message in octets" of RFC 1939 §5's scan listing. It MUST
// be the number of octets RETR transmits for this message, measured on the
// canonical CRLF wire form: RFC 1939 §11 defines the count by normalizing the
// stored end-of-line convention to CRLF (a lone LF counts as the two octets
// CR LF), and the §3 byte-stuffing of leading-dot lines and the CRLF.CRLF
// terminator are transport framing that is NOT counted. This is exactly the
// value RETR advertises in its "+OK <n> octets" reply, so a client that
// pre-allocates or verifies against the LIST/STAT size matches what it
// receives. Compute it with [OctetCount] on the same bytes [Mailbox.Retrieve]
// returns; a size derived any other way (e.g. the raw on-disk length of
// bare-LF content) will disagree with RETR.
Size int
// Seen reports whether the message is already marked read. RETR issues a
// MarkSeen only for a message that was previously unseen; TOP never does.
Seen bool
}
Message is one message in a POP3 maildrop, presented oldest-first. Its slice position (+1) is the POP3 message number the client uses for the session.
type NopLimiter ¶
type NopLimiter struct{}
NopLimiter is a Limiter that imposes no limits: it accepts every connection and never bans. It is the default when a nil Limiter is passed to NewServer or NewSession.
func (NopLimiter) Accept ¶
func (NopLimiter) Accept(string) bool
func (NopLimiter) IsBanned ¶
func (NopLimiter) IsBanned(string) bool
func (NopLimiter) RecordAuthFail ¶
func (NopLimiter) RecordAuthFail(string)
func (NopLimiter) Release ¶
func (NopLimiter) Release(string)
func (NopLimiter) ResetAuth ¶
func (NopLimiter) ResetAuth(string)
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server listens for POP3 connections and spawns a session handler per client.
func NewServer ¶
NewServer creates a POP3 Server backed by the given Backend. A non-nil tlsConfig enables STLS (STARTTLS) and implicit-TLS listeners. A nil limiter defaults to NopLimiter.
func (*Server) Close ¶ added in v0.2.0
Close immediately stops the server: it stops accepting new connections and force-closes every active connection, aborting any in-flight sessions without waiting for them to drain. Unlike Server.Shutdown it does not block on the sessions; use Shutdown for a graceful stop. Close mirrors the semantics of net/http.Server.Close and always returns nil.
func (*Server) ListenAndServe ¶
ListenAndServe starts POP3 listeners on the specified ports. A zero port is skipped. It returns once the listeners are open; connections are served in the background until Server.Shutdown or Server.Close.
func (*Server) Shutdown ¶
Shutdown gracefully stops the server: it stops accepting new connections and then blocks until every in-flight session has finished, or until ctx is done. It returns nil once all sessions have drained, or ctx.Err() if the deadline passes first (the sessions keep running; call Server.Close to force them to stop). Shutdown mirrors the semantics of net/http.Server.Shutdown.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents a single POP3 conversation with a client.
func NewSession ¶
NewSession creates a POP3 session over conn, authenticating against backend. A nil limiter defaults to NopLimiter. Call Session.Handle to run it.
func (*Session) Handle ¶
func (s *Session) Handle()
Handle runs the POP3 state machine until the client disconnects or QUITs.
A panic in any command handler or Backend/Mailbox call is recovered here so a single misbehaving session is isolated — logged and answered with -ERR before the connection is closed — rather than unwinding the per-connection goroutine and crashing the whole process along with every concurrent session.