pop3

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 12 Imported by: 0

README

go-pop3

CI Go Reference Go Report Card

A POP3 (RFC 1939) server engine for Go — standard library only, no external dependencies.

About

POP3 lets a client download and delete mail from a single maildrop. This package is the server half: you implement a small Backend that authenticates users and exposes each user's maildrop, and the engine speaks the wire protocol on top of it — greeting, capability negotiation, TLS, authentication, listing, retrieval and deletion.

The engine holds no assumptions about where mail is stored. A Backend returns messages as neutral Message values and their raw RFC 5322 bytes on demand, so the store can be a database, a maildir on disk, or a remote API — the protocol code never knows the difference. Message bodies are served byte-for-byte as Mailbox.Retrieve returns them, with RFC 1939 dot-stuffing applied on the wire, so what the client downloads is exactly what you stored.

Features

  • Full RFC 1939 command set: USER, PASS, STAT, LIST, UIDL, RETR, TOP, DELE, RSET, NOOP and QUIT.
  • STARTTLS via STLS (RFC 2595) on the plaintext port, and implicit TLS on the secure port — both from one *tls.Config.
  • Credentials are never sent in the clear: when TLS is configured, USER is refused until the connection is secured.
  • CAPA capability advertisement (RFC 2449): USER, STLS, TOP, UIDL, RESP-CODES, PIPELINING.
  • Storage-agnostic Backend/Mailbox seam — mail can live anywhere.
  • Deferred deletes with correct RFC 1939 update state: DELE marks, QUIT commits, RSET unmarks.
  • On-the-wire dot-stuffing for RETR/TOP, keyed on the real message UID.
  • A MarkSeen hook fired on the first RETR of a message, since POP3 has no read state of its own.
  • Pluggable per-IP Limiter for connection caps and auth-failure bans; NopLimiter (the default) imposes none.
  • Graceful shutdown that drains in-flight sessions.
  • Zero external dependencies.

Install

go get github.com/rest-mail/go-pop3

Quickstart

Implement Backend and Mailbox, then hand the server a listener config. This is the production shape — ListenAndServe binds real ports, so it is shown here rather than as an executed example (see the runnable Example in the docs for a self-contained transcript over an in-memory pipe).

package main

import (
	"crypto/tls"

	pop3 "github.com/rest-mail/go-pop3"
)

// store is your mail store. Authenticate returns a Mailbox scoped to the user.
type store struct{ /* db handle, etc. */ }

func (s *store) Authenticate(user, pass string) (pop3.Mailbox, error) {
	// verify credentials, then return the user's maildrop view
	return &maildrop{ /* ... */ }, nil
}

type maildrop struct{ /* ... */ }

func (m *maildrop) Messages() ([]pop3.Message, error) {
	// oldest-first; UID must be stable. Size is the STAT/LIST octet count and
	// MUST equal what RETR transmits — compute it with pop3.OctetCount over the
	// same bytes Retrieve returns so LIST/STAT and RETR agree (RFC 1939 §11).
	return []pop3.Message{
		{UID: "1001", Size: pop3.OctetCount(raw1001), Seen: false},
		{UID: "1002", Size: pop3.OctetCount(raw1002), Seen: true},
	}, nil
}

func (m *maildrop) Retrieve(uid string) ([]byte, error) { /* full RFC 5322 bytes */ return nil, nil }
func (m *maildrop) MarkSeen(uid string) error           { /* after RETR */ return nil }
func (m *maildrop) Delete(uid string) error             { /* on QUIT */ return nil }

func main() {
	cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem")
	tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}

	// nil Limiter -> pop3.NopLimiter (no per-IP limits).
	srv := pop3.NewServer(&store{}, tlsConfig, nil)
	if err := srv.ListenAndServe(pop3.Ports{POP3: 110, POP3TLS: 995}); err != nil {
		panic(err)
	}
	select {} // serve until srv.Shutdown(ctx) or srv.Close()
}

Backend

You implement three interfaces:

  • BackendAuthenticate(user, pass) validates the USER/PASS credentials and returns the Mailbox for that user, or a non-nil error to reject the login.
  • Mailbox — one authenticated maildrop. Messages() returns its contents oldest-first (called once, right after login); Retrieve(uid) returns the raw message bytes; MarkSeen(uid) and Delete(uid) apply side effects.
  • Message — a maildrop entry: its UID (what the client sees via UIDL and what the engine passes back to Retrieve/MarkSeen/Delete), its Size in octets for STAT/LIST, and whether it is already Seen. Size must be the exact octet count RETR transmits; use pop3.OctetCount(raw) over the bytes Retrieve returns so the maildrop listing and RETR never disagree.

A message's slice position (+1) in the Messages() result is the POP3 message number the client uses for the rest of the session; the engine translates between those numbers and your stable UIDs.

Server

NewServer(backend, tlsConfig, limiter) builds a Server. ListenAndServe opens the plaintext and implicit-TLS listeners named by Ports (a zero port is skipped) and returns immediately, serving connections in the background. Shutdown(ctx) stops accepting and blocks until the in-flight sessions finish (or ctx is cancelled); Close() is the immediate hard stop that force-closes live connections without waiting — the same split as net/http.Server. To drive a single already-accepted connection yourself — behind your own listener or proxy — construct a Session with NewSession(conn, backend, tlsConfig, limiter) and call Handle().

TLS

Pass a *tls.Config to enable both STLS (STARTTLS) on the plaintext port and implicit TLS on the secure port; pass nil to run plaintext only. When a config is present, the engine advertises STLS in CAPA and refuses USER on an un-upgraded plaintext connection so credentials are never exposed.

Rate limiting

NewServer and NewSession accept a Limiter — a small structural interface (Accept/Release/RecordAuthFail/IsBanned/ResetAuth) the engine consults for per-IP connection caps and auth-failure bans. Pass nil (or pop3.NopLimiter{}) for none, or wire in your own; any type with those methods satisfies it.

Documentation

Full API reference: pkg.go.dev/github.com/rest-mail/go-pop3.

License

MIT © 2026 rest-mail

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

func OctetCount(raw []byte) int

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 Ports

type Ports struct {
	POP3    int // 110 (STARTTLS)
	POP3TLS int // 995 (implicit TLS)
}

Ports defines the ports for POP3 services.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server listens for POP3 connections and spawns a session handler per client.

func NewServer

func NewServer(backend Backend, tlsConfig *tls.Config, limiter Limiter) *Server

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

func (s *Server) Close() error

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

func (s *Server) ListenAndServe(ports Ports) error

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

func (s *Server) Shutdown(ctx context.Context) error

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

func NewSession(conn net.Conn, backend Backend, tlsConfig *tls.Config, limiter Limiter) *Session

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL