An open API service indexing awesome lists of open source software.

https://github.com/meszmate/imap-go

A production-grade IMAP library for Go with full client, server, extension system, and middleware support. Zero external dependencies in core.
https://github.com/meszmate/imap-go

golang imap imap-client imap-server

Last synced: 3 months ago
JSON representation

A production-grade IMAP library for Go with full client, server, extension system, and middleware support. Zero external dependencies in core.

Awesome Lists containing this project

README

          

# imap-go

[![CI](https://github.com/meszmate/imap-go/actions/workflows/ci.yml/badge.svg)](https://github.com/meszmate/imap-go/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/meszmate/imap-go.svg)](https://pkg.go.dev/github.com/meszmate/imap-go)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

A production-grade IMAP library for Go with full client, server, extension system, and middleware support. Zero external dependencies in core.

## Features

- **IMAP4rev1 and IMAP4rev2** (RFC 3501 / RFC 9051) support
- **Client** with command pipelining, IDLE, STARTTLS, connection pooling
- **Server** with extensible command dispatch, session interface, mailbox tracking
- **50+ extensions** via a registry-based plugin system
- **Middleware pipeline** (logging, rate limiting, metrics, recovery, timeout)
- **Pluggable authentication** (PLAIN, LOGIN, CRAM-MD5, XOAUTH2, OAUTHBEARER, EXTERNAL, ANONYMOUS)
- **Public wire protocol** package (streaming parser and encoder)
- **Explicit state machine** with transition validation
- **Context and structured logging** integration (`context.Context`, `log/slog`)
- **Zero external dependencies** in core packages
- **Go 1.21+** minimum

## Installation

```bash
go get github.com/meszmate/imap-go
```

## Quick Start

### Client

```go
package main

import (
"fmt"
"log"

"github.com/meszmate/imap-go/client"
)

func main() {
c, err := client.DialTLS("imap.example.com:993", nil)
if err != nil {
log.Fatal(err)
}
defer c.Close()

if err := c.Login("user@example.com", "password"); err != nil {
log.Fatal(err)
}

data, err := c.Select("INBOX", nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("INBOX has %d messages\n", data.NumMessages)

if err := c.Logout(); err != nil {
log.Fatal(err)
}
}
```

### Client Lifecycle and Error Handling

- `Close()` is safe to call multiple times.
- If the connection is closed (local close, server disconnect, or EOF), pending commands fail promptly with an error instead of blocking.
- Commands waiting for server continuation (`IDLE`, `APPEND`, SASL `AUTHENTICATE`) also fail promptly on disconnect.
- `Logout()` sends `LOGOUT` and then closes the connection.
- You can detect disconnection even when not using `IDLE` with:
- `c.Done()` channel (closed on disconnect)
- `c.DisconnectErr()` (disconnect cause after `Done` is closed)

Example IDLE usage:

```go
idle, err := c.Idle()
if err != nil {
log.Fatal(err)
}

done := make(chan error, 1)
go func() { done <- idle.Wait() }()

select {
case err := <-done:
if err != nil {
log.Printf("IDLE ended with error: %v", err)
}
case <-time.After(30 * time.Second):
// Stop IDLE after timeout.
if err := idle.Done(); err != nil {
log.Printf("IDLE stop failed: %v", err)
}
}
```

Example non-IDLE disconnect detection:

```go
select {
case <-c.Done():
log.Printf("connection ended: %v", c.DisconnectErr())
case <-time.After(30 * time.Second):
if err := c.Noop(); err != nil {
log.Printf("NOOP failed: %v", err)
}
}
```

### Server

```go
package main

import (
"log"

"github.com/meszmate/imap-go/store/memstore"
)

func main() {
mem := memstore.New()
mem.AddUser("user", "password")

srv := mem.NewServer()

log.Println("Starting IMAP server on :143")
if err := srv.ListenAndServe(":143"); err != nil {
log.Fatal(err)
}
}
```

## Custom Storage Backends

The `store` package provides a simplified, CRUD-oriented interface for implementing
custom IMAP storage backends (e.g., PostgreSQL, S3). All operations are UID-based
and return plain data types -- the adapter handles all IMAP protocol translation
(sequence numbers, body section extraction, envelope parsing, etc.).

```go
package main

import (
"github.com/meszmate/imap-go/server"
"github.com/meszmate/imap-go/store"
)

type MyBackend struct{ /* ... */ }

func (b *MyBackend) Login(username, password string) (store.User, error) {
// authenticate and return a User
}

// Implement store.User (CreateMailbox, DeleteMailbox, OpenMailbox, etc.)
// Implement store.Mailbox (AppendMessage, GetMessages, SetFlags, etc.)

func main() {
backend := &MyBackend{}
srv := server.New(
server.WithNewSession(store.NewSessionFactory(backend)),
)
srv.ListenAndServe(":143")
}
```

Optional features (MOVE, SORT, THREAD, NAMESPACE, ID) can be added by
implementing the corresponding optional interfaces on your types. The adapter
detects these at runtime via type assertions.

An in-memory reference implementation is available at `store/memstore`:

```go
mem := memstore.New()
mem.AddUser("user", "password")
srv := mem.NewServer()
```

A PostgreSQL-backed implementation is available at `store/pgstore` (separate
module to keep the core at zero external deps):

```go
import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/meszmate/imap-go/store/pgstore"
)

pool, _ := pgxpool.New(ctx, "postgres://user:pass@localhost/imap")
pg := pgstore.New(pool)
pg.InitSchema(ctx)
pg.AddUser(ctx, "user", "password")
srv := pg.NewServer()
```

Features: transactional UID allocation, separate flags table, LISTEN/NOTIFY
for real-time IDLE support, atomic MOVE, and configurable password checking
(e.g., bcrypt).

### Direct Session Implementation

For full control over the IMAP protocol (wire-level access, custom writers,
sequence number management), implement `server.Session` directly:

```go
package main

import (
imap "github.com/meszmate/imap-go"
"github.com/meszmate/imap-go/server"
)

type MySession struct {
conn *server.Conn
}

func (s *MySession) Login(username, password string) error { /* ... */ }
func (s *MySession) Select(mailbox string, options *imap.SelectOptions) (*imap.SelectData, error) { /* ... */ }
func (s *MySession) List(w *server.ListWriter, ref string, patterns []string, options *imap.ListOptions) error { /* ... */ }
func (s *MySession) Fetch(w *server.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error { /* ... */ }
func (s *MySession) Store(w *server.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error { /* ... */ }
// ... implement remaining Session methods

func main() {
srv := server.New(
server.WithNewSession(func(conn *server.Conn) (server.Session, error) {
return &MySession{conn: conn}, nil
}),
)
srv.ListenAndServe(":143")
}
```

This gives you direct access to `FetchWriter`, `ListWriter`, `ExpungeWriter`,
and `UpdateWriter` for writing IMAP responses. You manage sequence numbers,
UID mapping, and body section extraction yourself.

See `server/memserver` for a complete working implementation of this approach.
Optional interfaces (`SessionMove`, `SessionNamespace`, `SessionID`,
`SessionSort`, `SessionThread`) can be added for extension support.

## Architecture

```
wire/ Public wire protocol (parser + encoder)
state/ Connection state machine
server/ IMAP server with extensible dispatch
store/ Simplified storage abstraction for server backends
store/memstore/ In-memory store.Backend reference implementation
store/pgstore/ PostgreSQL store.Backend (separate module, uses pgx/v5)
client/ IMAP client with pipelining
extension/ Extension/plugin registry
extensions/ 50+ built-in extension implementations
middleware/ Server middleware pipeline
auth/ Pluggable authentication mechanisms
imaptest/ Test infrastructure (harness + mocks)
```

### Key Design Decisions

- **Registry-based extensions** instead of type assertions
- **Middleware pipeline** for server (like HTTP middleware)
- **Public wire protocol** package (not internal)
- **Explicit state machine** as a first-class component
- **Functional options** for configuration
- **Session interface** for server backends

## Extensions

52 IMAP extensions in `extensions/`. Status legend:
- **Full** = command handlers + session interface + protocol parsing
- **Session** = session interface defined, capability advertised, needs WrapHandler implementation
- **Core** = handled by server core, extension just advertises capability

### Fully implemented (command handlers + session interface)

- [x] **MOVE** (RFC 6851) — MOVE command handler
- [x] **ACL** (RFC 4314) — SETACL, DELETEACL, GETACL, LISTRIGHTS, MYRIGHTS
- [x] **QUOTA** (RFC 9208) — GETQUOTA, GETQUOTAROOT, SETQUOTA
- [x] **METADATA** (RFC 5464) — SETMETADATA, GETMETADATA
- [x] **SORT** (RFC 5256) — SORT command handler
- [x] **THREAD** (RFC 5256) — THREAD command handler
- [x] **NAMESPACE** (RFC 2342) — NAMESPACE command handler
- [x] **ID** (RFC 2971) — ID command handler
- [x] **UNSELECT** (RFC 3691) — UNSELECT command with state transition
- [x] **UNAUTHENTICATE** (RFC 8437) — UNAUTHENTICATE command
- [x] **COMPRESS** (RFC 4978) — COMPRESS command (DEFLATE)
- [x] **LANGUAGE** (RFC 5255) — LANGUAGE command
- [x] **REPLACE** (RFC 8508) — REPLACE command with APPENDUID
- [x] **URLAUTH** (RFC 4467) — GENURLAUTH, RESETKEY, URLFETCH
- [x] **FILTERS** (RFC 5466) — GETFILTER, SETFILTER
- [x] **CONVERT** (RFC 5259) — CONVERT command
- [x] **NOTIFY** (RFC 5465) — NOTIFY SET/NONE
- [x] **CATENATE** (RFC 4469) — APPEND WrapHandler with CATENATE parsing
- [x] **CONDSTORE** (RFC 7162) — FETCH/STORE/SELECT/EXAMINE WrapHandler with CHANGEDSINCE/UNCHANGEDSINCE/MODSEQ parsing
- [x] **QRESYNC** (RFC 7162) — SELECT/EXAMINE WrapHandler with QRESYNC params, VANISHED (EARLIER) responses, FETCH VANISHED modifier
- [x] **UIDPLUS** (RFC 4315) — COPY/EXPUNGE WrapHandler with CopyUIDs/ExpungeUIDs routing, COPYUID response codes
- [x] **ESEARCH** (RFC 4731) — SEARCH WrapHandler with RETURN (MIN MAX COUNT ALL SAVE) options, ESEARCH response format
- [x] **LIST-EXTENDED** (RFC 5258) — LIST WrapHandler with selection options (SUBSCRIBED, REMOTE, RECURSIVEMATCH, SPECIAL-USE) and return options (SUBSCRIBED, CHILDREN, STATUS, MYRIGHTS, SPECIAL-USE)
- [x] **LIST-STATUS** (RFC 5819) — Handled via LIST-EXTENDED RETURN (STATUS) option
- [x] **LIST-MYRIGHTS** (RFC 8440) — Handled via LIST-EXTENDED RETURN (MYRIGHTS) option
- [x] **LIST-METADATA** (RFC 9590) — Handled via LIST-EXTENDED RETURN (METADATA) option
- [x] **SPECIAL-USE** (RFC 6154) — CREATE WrapHandler with USE attribute parsing; LIST handled via LIST-EXTENDED
- [x] **BINARY** (RFC 3516) — BINARY[]/BINARY.PEEK[]/BINARY.SIZE[] fetch items, binary literal ~{N} APPEND support
- [x] **SEARCHRES** (RFC 5182) — SEARCH RETURN (SAVE) with result saving, $ reference in FETCH/STORE/COPY/MOVE sequence sets and SEARCH criteria
- [x] **PARTIAL** (RFC 9394) — SEARCH/SORT RETURN (PARTIAL offset:count) with paginated results in ESEARCH response format
- [x] **SEARCH=FUZZY** (RFC 6203) — SEARCH WrapHandler with FUZZY modifier parsing, session routing to SearchFuzzy/SearchExtended/Search
- [x] **UTF8=ACCEPT** (RFC 6855) — ENABLE WrapHandler for session notification, APPEND WrapHandler with UTF8 (~{N+}) literal parsing
- [x] **UIDONLY** (RFC 9586) — ENABLE WrapHandler with UIDREQUIRED rejection for seq-number commands, UIDFETCH/VANISHED response rewrites
- [x] **MULTIAPPEND** (RFC 3502) — APPEND WrapHandler with multi-message detection, atomic append via SessionMultiAppend
- [x] **ESORT** (RFC 5267) — SORT WrapHandler with RETURN (MIN MAX COUNT ALL SAVE) options, ESEARCH response format
- [x] **CONTEXT=SEARCH** (RFC 5267) — SEARCH WrapHandler with RETURN (UPDATE CONTEXT) options, CANCELUPDATE command, ADDTO/REMOVEFROM ESEARCH notifications
- [x] **MULTISEARCH** (RFC 7377) — ESEARCH command handler with IN (mailboxes/subtree/subtree-one) source, RETURN options, per-mailbox ESEARCH responses with MAILBOX and UIDVALIDITY
- [x] **PREVIEW** (RFC 8970) — FETCH WrapHandler with PREVIEW (LAZY) modifier parsing, PREVIEW NIL response support
- [x] **OBJECTID** (RFC 8474) — EMAILID/THREADID in FETCH, MAILBOXID in STATUS and SELECT/EXAMINE response code
- [x] **SAVEDATE** (RFC 8514) — SAVEDATE in FETCH, SAVEDBEFORE/SAVEDSINCE/SAVEDON in SEARCH

### Core-handled (capability advertisement only)
- [x] **STATUS=SIZE** (RFC 8438) — core handles SIZE in STATUS
- [x] **IDLE** (RFC 2177) — handled in `server/commands/idle.go`
- [x] **ENABLE** (RFC 5161) — handled in `server/commands/enable.go`
- [x] **SASL-IR** (RFC 4959) — handled in `server/commands/authenticate.go`
- [x] **LITERAL+** (RFC 7888) — handled in `wire/` layer
- [x] **CHILDREN** (RFC 3348) — LIST attributes set by session backend
- [x] **WITHIN** (RFC 5032) — SearchCriteria.Older/Younger already in core
- [x] **SORT=DISPLAY** (RFC 5957) — SortKey DISPLAYFROM/DISPLAYTO in core
- [x] **APPENDLIMIT** (RFC 7889) — StatusData.AppendLimit in core
- [x] **INPROGRESS** (RFC 9585) — response code only
- [x] **JMAPACCESS** (RFC 9698) — capability only
- [x] **MESSAGELIMIT** (RFC 9738) — capability only

## Middleware

```go
import "github.com/meszmate/imap-go/middleware"

srv := server.New(
server.WithNewSession(sessionFactory),
)

// Apply middleware to all commands
chain := middleware.Chain(
middleware.Recovery(logger),
middleware.Logging(logger),
middleware.Timeout(30 * time.Second),
middleware.RateLimit(100, 10),
)
```

## Authentication

```go
import (
"github.com/meszmate/imap-go/auth"
"github.com/meszmate/imap-go/auth/plain"
"github.com/meszmate/imap-go/auth/xoauth2"
)

// Client-side
mechanism := plain.NewClient("user", "password")
err := c.Authenticate(mechanism)

// Register mechanisms
auth.DefaultRegistry.RegisterClient("PLAIN", plain.NewClientFactory())
auth.DefaultRegistry.RegisterServer("PLAIN", plain.NewServerFactory(authenticator))
```

## Testing

```go
import (
"testing"
"github.com/meszmate/imap-go/imaptest"
"github.com/meszmate/imap-go/store/memstore"
)

func TestMyFeature(t *testing.T) {
mem := memstore.New()
mem.AddUser("user", "pass")

h := imaptest.NewHarness(t, mem.NewServer())
c := h.Dial()

if err := c.Login("user", "pass"); err != nil {
t.Fatal(err)
}
// ... test your feature
}
```

## License

MIT - see [LICENSE](LICENSE).