No description
Find a file
Heiko Schlittermann (ai) bff91d9176
All checks were successful
nagonag (Push) / nagonag (push) Successful in 3m8s
polish: review nits on logger feature and docs ai:claude-sonnet-4-5
- NewLogWriter doc + README: state explicitly that the wire
  transcript can carry message content (small FETCH literals are
  transcribed verbatim) — routing it into a log store is a
  data-sensitivity decision, not just a verbosity one
- logWriter.Write: strip trailing \r so foreign writers feeding
  CRLF cannot smuggle a stray carriage return into the line attr
- log message tense: 'imap close' → 'imap closed' (matches
  'connected'/'reconnected')
- README Quick start: full error handling (was eliding errors);
  align env var name with examples (IMAP_PASSWORD)

81 tests pass, race-clean; staticcheck clean.

(co)authored by ai:claude-sonnet-4-5
2026-07-03 17:45:33 +02:00
.forgejo/workflows cicd: add forgejo workflow 2026-07-03 16:59:24 +02:00
docs feat: inject structured logger via Config.Logger ai:claude-sonnet-4-5 2026-07-03 17:21:47 +02:00
acceptance_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
append.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
append_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
client.go polish: review nits on logger feature and docs ai:claude-sonnet-4-5 2026-07-03 17:45:33 +02:00
client_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
conn.go chore: post-review cleanup ai:claude-sonnet-4-5 2026-06-17 12:00:34 +02:00
doc.go docs: expand package doc, examples, and README ai:claude-sonnet-4-5 2026-07-03 17:38:09 +02:00
errors.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
examine_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
example_test.go docs: expand package doc, examples, and README ai:claude-sonnet-4-5 2026-07-03 17:38:09 +02:00
fetch.go docs: add runnable usage examples ai:claude-sonnet-4-5 2026-06-17 12:05:06 +02:00
fixes_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
go.mod go: update dependencies 2026-07-03 16:59:43 +02:00
go.sum feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
idle.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
LICENSE feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
limiter_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
list.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
list_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
logging_test.go polish: review nits on logger feature and docs ai:claude-sonnet-4-5 2026-07-03 17:45:33 +02:00
logwriter.go polish: review nits on logger feature and docs ai:claude-sonnet-4-5 2026-07-03 17:45:33 +02:00
namespace.go chore: post-review cleanup ai:claude-sonnet-4-5 2026-06-17 12:00:34 +02:00
namespace_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
NOTICE feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
notify.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
parser.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
password.go docs: expand package doc, examples, and README ai:claude-sonnet-4-5 2026-07-03 17:38:09 +02:00
password_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
peek.go docs: expand package doc, examples, and README ai:claude-sonnet-4-5 2026-07-03 17:38:09 +02:00
peek_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
README.md polish: review nits on logger feature and docs ai:claude-sonnet-4-5 2026-07-03 17:45:33 +02:00
sanitize.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
sanitize_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
search.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
search_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
security.go feat: inject structured logger via Config.Logger ai:claude-sonnet-4-5 2026-07-03 17:21:47 +02:00
security_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
select.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
store.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
testserver_test.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
types.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
utf7.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00
validate.go feat: initial extraction of imapclient package ai:claude-sonnet-4-5 2026-06-17 11:54:56 +02:00

imapclient

A small, dependency-light IMAP client for Go built directly on RFC 3501, with optional CONDSTORE (RFC 7162) and NAMESPACE (RFC 2342) support.

  • Context-cancellable I/O on every operation
  • Self-redacting Password type — credentials cannot leak via fmt, slog, or JSON
  • Mandatory STARTTLS by default; the zero-value Config is safe
  • Structured logging via *slog.Logger (stdlib); wire transcript via io.Writer
  • Token-bucket rate limiter built in

Status: pre-1.0 (v0.x). The API may change.

Runtime dependency: stdlib + golang.org/x/time/rate (BSD-3).

Install

go get go.schlittermann.de/heiko/imapclient

Requires Go 1.26 or newer.

Quick start

import "go.schlittermann.de/heiko/imapclient"

cfg := imapclient.Config{
    Host:     "imap.example.com",
    Port:     993,
    TLS:      true, // implicit TLS
    Username: "alice@example.com",
    Password: imapclient.NewPassword(os.Getenv("IMAP_PASSWORD")),
    Mailbox:  "INBOX",
    Logger:   slog.Default(), // optional
}

c, mbox, err := imapclient.Connect(ctx, cfg, false)
if err != nil {
    log.Fatal(err)
}
defer c.Close()

uids, err := c.SearchUIDs(ctx, imapclient.SearchCriteria{
    Since:   time.Now().AddDate(0, 0, -7),
    NotFlag: imapclient.FlagSeen,
})
if err != nil {
    log.Fatal(err)
}

for _, uid := range uids {
    body, err := c.FetchBody(ctx, uid)
    if err != nil {
        log.Fatal(err)
    }

    _ = body // parse with net/mail, etc.

    if err := c.Store(ctx, uid, imapclient.FlagAdd, imapclient.FlagSeen); err != nil {
        log.Fatal(err)
    }
}

_ = mbox.Exists // message count from SELECT

See the package examples for FetchBodyPeek, SearchUIDs criteria, Append, Namespace, Idle, and the NewLogWriter bridge.

Transport security

The zero value of Config is safe: it requires STARTTLS before sending any credentials. Dial returns ErrSTARTTLSUnsupported if the server does not advertise it — before LOGIN.

Config Transport Port
TLS: true Implicit TLS (handshake before any IMAP bytes) 993
(default) Mandatory STARTTLS (plaintext upgraded before LOGIN) 143
InsecurePlaintext: true Plaintext only — local testing only any

There is no "STARTTLS optional" mode — that is a downgrade-attack surface.

Each insecure opt-out fires Config.OnSecurityEvent and logs at Warn:

Field Kind Effect
TLSNoVerify TLSNoVerifyActive skip certificate verification
InsecurePlaintext STARTTLSDisabled no TLS at all
InsecureDebug InsecureDebugActive password in debug transcript
cfg.OnSecurityEvent = func(e imapclient.SecurityEvent) {
    // e.Kind.String() → "tls_no_verify_active" etc.
    myAuditLog.Warn(e.Kind.String(), "host", e.Host, "source", e.Name)
}

Credentials

Password redacts itself in every fmt, log/slog, JSON, and TOML path. Reveal() is the only exit, called once inside Login just before the bytes go on the wire.

pw := imapclient.NewPassword(secret)
fmt.Println(pw)        // → [redacted]
slog.Any("pw", pw)     // → pw=[redacted]
json.Marshal(pw)       // → "[redacted]"
pw.Reveal()            // → secret  (use once, immediately)

Error messages are sanitised so a hostile server echoing the LOGIN line back cannot leak the password through the error chain.

Logging

Inject a *slog.Logger via Config.Logger (nil = silent):

cfg.Logger = slog.Default()

// Other stacks:
// zap:    slog.New(zapslog.NewHandler(zapLogger.Core()))
// logrus: slog.New(sloglogrus.Option{Logger: l}.NewLogrusHandler())
// log:    slog.New(slog.NewTextHandler(log.Writer(), nil))
Event Level Attrs
connected Debug name, host, tls=implicit|starttls|plaintext
reconnected Debug name, host
close Debug name, host
insecure opt-out Warn event, name, host

The package never logs credentials or message content.

Wire transcript → slog

Route the raw IMAP wire transcript through the same logger with NewLogWriter:

cfg.DebugWriter = imapclient.NewLogWriter(cfg.Logger, slog.LevelDebug)
// → msg="imap wire" dir=client|server line="A3 UID SEARCH ALL"

Passwords are redacted before they reach the writer (unless InsecureDebug is set). Note that the transcript can include message content (small FETCH literals are transcribed verbatim) — routing it into a log store is a data-sensitivity decision.

Rate limiting

A token-bucket rate limiter is applied before every outgoing command (default: 5 cmd/s, burst 10). Tune or disable it:

cfg.CommandsPerSec = 10   // 10/s, burst 10
cfg.CommandsBurst  = 20   // raise burst

cfg.CommandsPerSec = -1   // unlimited (no limiter)

The limiter respects context.Context cancellation.

Scope and limitations

This package implements the operations its consumers need — not all of RFC 3501:

  • Auth: LOGIN only; no SASL/AUTHENTICATE (OAUTH2, XOAUTH2, etc.)
  • Parsers: ENVELOPE, ADDRESS, NAMESPACE — sufficient for common servers, not a strict RFC parser; unusual formatting may parse as empty fields
  • Concurrency: a Client is not goroutine-safe; use one per goroutine or synchronise externally

License

Apache-2.0. See LICENSE and NOTICE.