Documentation
¶
Overview ¶
Package mailbox provides an email-like messaging library for Go.
It supports sending messages to recipients (identified by user IDs), marking messages as read/unread, archiving, listing, and searching. All functionality is exposed via interfaces, with pluggable storage backends (MongoDB, PostgreSQL, in-memory).
Basic Usage ¶
// Create in-memory store for testing
store := memory.New()
// Create mailbox service with automatic background maintenance
svc, err := mailbox.New(
mailbox.Config{
TrashCleanupInterval: 1 * time.Hour,
ExpiredMessageCleanupInterval: 1 * time.Hour,
},
mailbox.WithStore(store),
)
if err != nil {
log.Fatal(err)
}
// Connect initializes indexes/schema and starts background tasks
if err := svc.Connect(ctx); err != nil {
log.Fatal(err)
}
defer svc.Close(ctx) // stops background goroutines and waits
// Get a mailbox client for a user
mb := svc.Client("user123")
// Send a message
draft, _ := mb.Compose()
msg, err := draft.
SetSubject("Hello").
SetBody("World").
SetRecipients("user456").
Send(ctx)
Mailbox Operations ¶
- Compose: Create and send messages
- Get: Retrieve a message by ID
- Folder: List messages in any folder, using the store.FolderInbox, store.FolderSent, store.FolderArchived, and store.FolderTrash constants
- Search: Full-text search
- Stream: Iterator-based streaming with filters
Storage Backends ¶
The store package provides implementations for:
- MongoDB (store/mongo) - accepts *mongo.Client
- PostgreSQL (store/postgres) - accepts *sqlx.DB (or *sql.DB via NewFromDB)
- In-memory (store/memory) - for testing
Events ¶
Mailbox provides typed events for message lifecycle notifications. Events use the github.com/rbaliyan/event/v3 library which supports multiple transports (Redis Streams, NATS, Kafka, in-memory channel).
To enable events, pass WithRedisClient or WithEventTransport when creating the service:
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(store),
mailbox.WithRedisClient(redisClient),
)
Events are automatically registered during Connect(). Access per-service events via the Events() method:
events := svc.Events() events.MessageSent.Subscribe(ctx, handler) events.MessageRead.Subscribe(ctx, handler) events.MessageDeleted.Subscribe(ctx, handler)
Available events:
- MessageSent - when a message is sent
- MessageReceived - when a message is delivered to a recipient
- MessageRead - when a message is marked as read
- MessageDeleted - when a message is permanently deleted
- MessageMoved - when a message is moved between folders
- MarkAllRead - when all messages in a folder are marked as read
Threads ¶
Every sent message is automatically assigned a thread ID. When no ThreadID is provided, one is generated as follows:
- If ReplyToID is set and the sender owns the referenced message, the thread ID is inherited from that message (or the message ID itself is used as the thread root for the first reply to a threadless message).
- Otherwise a new UUID is generated, starting a fresh conversation.
Access per-user thread views via the Mailbox interface:
thread, _ := mb.GetThread(ctx, threadID, store.ListOptions{Limit: 50})
replies, _ := mb.GetReplies(ctx, messageID, store.ListOptions{Limit: 20})
For external delivery systems (e.g., SMTP gateways) that know only the thread_id and need to find all participants:
participants, err := svc.ThreadParticipants(ctx, threadID)
if err != nil { ... } // store.ErrNotFound when thread does not exist
_, err = svc.Client(senderID).SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: participants,
ThreadID: threadID,
Subject: "Re: original",
Body: body,
})
ThreadParticipants is a cross-owner query (not scoped to a single user) so it lives on Service rather than Mailbox.
Transactional Outbox ¶
For guaranteed event delivery, enable the transactional outbox on your store:
store := mongostore.New(client, mongostore.WithOutbox(true))
Events are then persisted atomically with DB writes via the event library's outbox package. The store provides an event.OutboxStore to the bus, and a background relay (outbox.Relay) publishes pending events to the transport. See store.OutboxPersister and store.EventOutboxProvider for the interfaces.
Example (CleanupExpiredMessages) ¶
Example_cleanupExpiredMessages demonstrates message retention cleanup.
package main
import (
"context"
"fmt"
"time"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
memStore := memory.New()
svc, _ := mailbox.New(mailbox.Config{MessageRetention: 24 * time.Hour},
mailbox.WithStore(memStore),
)
svc.Connect(ctx)
defer svc.Close(ctx)
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Old message",
Body: "body",
})
// Age messages past retention period.
memStore.AgeMessages(48 * time.Hour)
// Send a fresh message that should survive.
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Fresh message",
Body: "body",
})
result, _ := svc.CleanupExpiredMessages(ctx)
fmt.Println("expired:", result.DeletedCount)
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("surviving:", len(inbox.All()))
}
Output: expired: 2 surviving: 1
Example (CleanupTrash) ¶
Example_cleanupTrash demonstrates scheduled trash cleanup.
package main
import (
"context"
"fmt"
"time"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
memStore := memory.New()
svc, _ := mailbox.New(mailbox.Config{TrashRetention: 24 * time.Hour},
mailbox.WithStore(memStore),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Send and delete a message.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Old message",
Body: "body",
})
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
bob.Delete(ctx, inbox.All()[0].GetID())
// Age messages past retention period.
memStore.AgeMessages(48 * time.Hour)
// Run cleanup.
result, _ := svc.CleanupTrash(ctx)
fmt.Println("cleaned:", result.DeletedCount)
}
Output: cleaned: 1
Example (EncryptedMessage) ¶
Example_encryptedMessage demonstrates E2E encryption: send encrypted, receive and decrypt.
package main
import (
"context"
"fmt"
"crypto/rand"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/compress"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
// Generate X25519 keypairs for sender and recipient.
keys := crypto.NewStaticKeyResolver()
for _, user := range []string{"alice", "bob"} {
priv := make([]byte, 32)
rand.Read(priv)
pub, _ := curve25519.X25519(priv, curve25519.Basepoint)
keys.AddUser(user, pub, priv)
}
// Create service with compression + encryption plugins.
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(
compress.NewPlugin(compress.Gzip), // compress first
crypto.NewEncryptionPlugin(keys), // then encrypt
),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Alice sends an encrypted message — subject stays searchable.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Confidential report",
Body: "Q3 revenue: $1.2M",
Metadata: map[string]any{"department": "finance"},
})
// Bob receives and decrypts.
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
msg := inbox.All()[0]
// Subject is plaintext (searchable).
fmt.Println("subject:", msg.GetSubject())
fmt.Println("encrypted:", crypto.IsEncrypted(msg))
fmt.Println("compressed:", compress.IsCompressed(msg))
// Body is encrypted — decrypt with Open.
plaintext, _ := crypto.Open(ctx, msg, "bob", keys)
fmt.Println("body:", string(plaintext))
// Application metadata is preserved.
fmt.Println("department:", msg.GetMetadata()["department"])
}
Output: subject: Confidential report encrypted: true compressed: true body: Q3 revenue: $1.2M department: finance
Example (EventSubscription) ¶
Example_eventSubscription demonstrates subscribing to mailbox events.
package main
import (
"context"
"fmt"
"log"
"sort"
"sync"
"time"
"github.com/rbaliyan/event/v3"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestService creates a connected service for examples using in-memory backends.
// Channel transport enables synchronous event delivery without Redis.
func newTestService() mailbox.Service {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestService()
defer svc.Close(ctx)
// Track events.
var mu sync.Mutex
var received []string
svc.Events().MessageSent.Subscribe(ctx,
func(ctx context.Context, _ event.Event[mailbox.MessageSentEvent], data mailbox.MessageSentEvent) error {
mu.Lock()
received = append(received, "sent:"+data.Subject)
mu.Unlock()
return nil
},
)
svc.Events().MessageReceived.Subscribe(ctx,
func(ctx context.Context, _ event.Event[mailbox.MessageReceivedEvent], data mailbox.MessageReceivedEvent) error {
mu.Lock()
received = append(received, "received:"+data.RecipientID)
mu.Unlock()
return nil
},
)
// Send a message (events fire synchronously with channel transport).
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Hello",
Body: "body",
})
// Block until both events (sent + received) have been delivered, rather
// than sleeping for a fixed duration. The handlers append under mu, so the
// count is read under the same lock. A deadline bounds the wait so a
// delivery regression fails fast instead of hanging.
deadline := time.Now().Add(5 * time.Second)
for {
mu.Lock()
done := len(received) == 2
mu.Unlock()
if done || time.Now().After(deadline) {
break
}
}
mu.Lock()
sort.Strings(received)
for _, e := range received {
fmt.Println(e)
}
mu.Unlock()
}
Output: received:bob sent:Hello
Example (FilterBulkOps) ¶
Example_filterBulkOps demonstrates filter-based bulk operations.
package main
import (
"context"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
svc, _ := mailbox.New(mailbox.Config{}, mailbox.WithStore(memory.New()))
svc.Connect(ctx)
defer svc.Close(ctx)
// Send 5 messages to bob.
for i := 0; i < 5; i++ {
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: fmt.Sprintf("Msg %d", i),
Body: "body",
})
}
bob := svc.Client("bob")
// Mark all unread in inbox as read using filter.
count, _ := bob.UpdateByFilter(ctx, []store.Filter{
store.InFolder(store.FolderInbox),
store.IsReadFilter(false),
}, mailbox.MarkRead())
fmt.Println("marked read:", count)
// Move all from alice to archive.
moved, _ := bob.MoveByFilter(ctx, []store.Filter{
store.SenderIs("alice"),
}, store.FolderArchived)
fmt.Println("archived:", moved)
}
Output: marked read: 5 archived: 5
Example (Filters) ¶
Example_filters demonstrates type-safe message filters.
package main
import (
"context"
"fmt"
"log"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestService creates a connected service for examples using in-memory backends.
// Channel transport enables synchronous event delivery without Redis.
func newTestService() mailbox.Service {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestService()
defer svc.Close(ctx)
alice := svc.Client("alice")
for i := 1; i <= 5; i++ {
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: fmt.Sprintf("Msg %d", i),
Body: "body",
})
}
bob := svc.Client("bob")
// Mark first 2 messages as read.
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
for i, m := range inbox.All() {
if i < 2 {
bob.UpdateFlags(ctx, m.GetID(), mailbox.MarkRead())
}
}
// Stream only unread messages using filters.
iter, _ := bob.Stream(ctx, []store.Filter{
store.InFolder(store.FolderInbox),
store.IsReadFilter(false),
}, mailbox.StreamOptions{})
var count int
for {
ok, _ := iter.Next(ctx)
if !ok {
break
}
count++
}
fmt.Println("unread messages:", count)
}
Output: unread messages: 3
Example (ListFolders) ¶
Example_listFolders demonstrates discovering folders.
package main
import (
"context"
"fmt"
"log"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestService creates a connected service for examples using in-memory backends.
// Channel transport enables synchronous event delivery without Redis.
func newTestService() mailbox.Service {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestService()
defer svc.Close(ctx)
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "test",
Body: "body",
})
bob := svc.Client("bob")
// Move to a custom folder.
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
bob.MoveToFolder(ctx, inbox.All()[0].GetID(), "work-queue")
// List all folders with counts.
folders, _ := bob.ListFolders(ctx)
for _, f := range folders {
if f.MessageCount > 0 {
fmt.Printf("%s: %d messages (%d unread)\n", f.ID, f.MessageCount, f.UnreadCount)
}
}
}
Output: work-queue: 1 messages (1 unread)
Example (MessageTTL) ¶
Example_messageTTL demonstrates per-message TTL (time-to-live). Messages with TTL are automatically deleted by CleanupExpiredMessages.
package main
import (
"context"
"fmt"
"time"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
memStore := memory.New()
svc, _ := mailbox.New(mailbox.Config{MessageRetention: 24 * time.Hour},
mailbox.WithStore(memStore),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Send a message with 2-hour TTL.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Ephemeral message",
Body: "This self-destructs in 2 hours.",
TTL: 2 * time.Hour,
})
// Message is visible now.
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("before expiry:", len(inbox.All()))
// Age messages past TTL.
memStore.AgeTTLAll(3 * time.Hour)
// CleanupExpiredMessages deletes TTL-expired messages.
result, _ := svc.CleanupExpiredMessages(ctx)
fmt.Println("cleaned:", result.DeletedCount)
inbox, _ = bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("after expiry:", len(inbox.All()))
}
Output: before expiry: 1 cleaned: 2 after expiry: 0
Example (Notifications) ¶
Example_notifications demonstrates real-time notification delivery. This example uses the in-memory notify store with channel-based delivery. In production, use Redis Streams (notifyredis.New) for cross-instance delivery.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/notify"
notifymem "github.com/rbaliyan/mailbox/notify/memory"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestServiceWithNotifications creates a connected service with notification support.
func newTestServiceWithNotifications() mailbox.Service {
notifier := notify.NewNotifier(
notify.WithStore(notifymem.New()),
notify.WithPollInterval(50*time.Millisecond),
)
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
mailbox.WithNotifier(notifier),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestServiceWithNotifications()
defer svc.Close(ctx)
// Open notification stream for bob BEFORE sending the message.
stream, err := svc.Notifications(ctx, "bob", "")
if err != nil {
log.Fatal(err)
}
defer stream.Close()
// Send a message to bob. The stream's store poller backfills persisted
// events, so no artificial delay is needed before reading.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Real-time!",
Body: "body",
})
// Block on Next until we get the received event.
// Multiple events may fire (sent, received) — find the one for bob.
readCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
for {
evt, err := stream.Next(readCtx)
if err != nil {
log.Fatal(err)
}
if evt.Type == "mailbox.message.received" {
fmt.Println("type:", evt.Type)
break
}
}
}
Output: type: mailbox.message.received
Example (PartialDelivery) ¶
Example_partialDelivery demonstrates handling partial delivery errors.
package main
import (
"context"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
// Set up with a small quota so one recipient hits the limit.
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithGlobalQuota(mailbox.QuotaPolicy{
MaxMessages: 2,
ExceedAction: mailbox.QuotaActionReject,
}),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Fill bob's quota.
for i := 0; i < 2; i++ {
svc.Client(fmt.Sprintf("sender%d", i)).SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "fill",
Body: "body",
})
}
// Try to send to both bob (over quota) and charlie (fine).
alice := svc.Client("alice")
msg, err := alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob", "charlie"},
Subject: "Partial",
Body: "body",
})
if pde, ok := mailbox.IsPartialDelivery(err); ok {
fmt.Println("delivered to:", pde.DeliveredTo)
fmt.Println("failed:", len(pde.FailedRecipients))
fmt.Println("message sent:", msg != nil)
}
}
Output: delivered to: [charlie] failed: 1 message sent: true
Example (ScheduledDelivery) ¶
Example_scheduledDelivery demonstrates scheduled message delivery. Messages with ScheduleAt are hidden until the scheduled time.
package main
import (
"context"
"fmt"
"time"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
memStore := memory.New()
svc, _ := mailbox.New(mailbox.Config{}, mailbox.WithStore(memStore))
svc.Connect(ctx)
defer svc.Close(ctx)
// Schedule a message for 1 hour from now.
future := time.Now().UTC().Add(1 * time.Hour)
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Good morning!",
Body: "Scheduled for 9am.",
ScheduleAt: &future,
})
// Bob's inbox is empty — message not yet available.
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("before schedule:", len(inbox.All()))
// Age schedule past availability.
memStore.AgeScheduleAll(2 * time.Hour)
// Now visible.
inbox, _ = bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("after schedule:", len(inbox.All()))
if len(inbox.All()) > 0 {
fmt.Println("subject:", inbox.All()[0].GetSubject())
}
}
Output: before schedule: 0 after schedule: 1 subject: Good morning!
Example (SseServer) ¶
Example_sseServer demonstrates a complete HTTP server with SSE notifications and a client that consumes the stream. This is a self-contained server+client example that runs in-process using httptest.
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httptest"
"time"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/notify"
notifymem "github.com/rbaliyan/mailbox/notify/memory"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestServiceWithNotifications creates a connected service with notification support.
func newTestServiceWithNotifications() mailbox.Service {
notifier := notify.NewNotifier(
notify.WithStore(notifymem.New()),
notify.WithPollInterval(50*time.Millisecond),
)
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
mailbox.WithNotifier(notifier),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestServiceWithNotifications()
defer svc.Close(ctx)
// --- Server side: SSE endpoint ---
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
userID := r.URL.Query().Get("user_id")
lastEventID := r.Header.Get("Last-Event-ID")
stream, err := svc.Notifications(r.Context(), userID, lastEventID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer stream.Close()
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
w.WriteHeader(http.StatusOK)
flusher.Flush()
for {
evt, err := stream.Next(r.Context())
if err != nil {
return
}
fmt.Fprintf(w, "id: %s\nevent: %s\ndata: %s\n\n", evt.ID, evt.Type, string(evt.Payload))
flusher.Flush()
}
})
ts := httptest.NewServer(handler)
defer ts.Close()
// --- Client side: consume SSE ---
done := make(chan bool, 1)
go func() {
resp, err := http.Get(ts.URL + "?user_id=bob")
if err != nil {
done <- false
return
}
defer resp.Body.Close()
buf := make([]byte, 4096)
n, _ := resp.Body.Read(buf)
done <- n > 0
}()
// Send a message to trigger an SSE event. No connect delay is needed: the
// notification is persisted, and the SSE handler's stream poller backfills
// it from the store regardless of connection ordering.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "SSE test",
Body: "body",
})
select {
case ok := <-done:
fmt.Println("received:", ok)
case <-time.After(5 * time.Second):
fmt.Println("received: false")
}
}
Output: received: true
Example (Stats) ¶
Example_stats demonstrates aggregate statistics.
package main
import (
"context"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
// Use a plain service (no event transport) to avoid stats cache interference
// from other examples running in the same process.
svc, _ := mailbox.New(mailbox.Config{}, mailbox.WithStore(memory.New()))
svc.Connect(ctx)
defer svc.Close(ctx)
alice := svc.Client("alice")
for i := 0; i < 3; i++ {
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: fmt.Sprintf("Msg %d", i),
Body: "body",
})
}
bob := svc.Client("bob")
// Stats returns total messages and unread count.
stats, _ := bob.Stats(ctx)
fmt.Println("total:", stats.TotalMessages)
fmt.Println("unread:", stats.UnreadCount)
// UnreadCount is a convenience method.
unread, _ := bob.UnreadCount(ctx)
fmt.Println("unread count:", unread)
}
Output: total: 3 unread: 3 unread count: 3
Example (SystemMessages) ¶
Example_systemMessages demonstrates sending messages from a system user.
package main
import (
"context"
"fmt"
"log"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestService creates a connected service for examples using in-memory backends.
// Channel transport enables synchronous event delivery without Redis.
func newTestService() mailbox.Service {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestService()
defer svc.Close(ctx)
// System sends a notification to bob.
system := svc.Client("system")
system.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Welcome to the platform",
Body: "<h1>Welcome!</h1><p>Your account is ready.</p>",
Headers: map[string]string{"Content-Type": "text/html"},
Metadata: map[string]any{
"template": "welcome",
"deep_link": "/onboarding",
},
})
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
msg := inbox.All()[0]
fmt.Println("from:", msg.GetSenderID())
fmt.Println("subject:", msg.GetSubject())
fmt.Println("content-type:", msg.GetHeaders()["Content-Type"])
fmt.Println("template:", msg.GetMetadata()["template"])
}
Output: from: system subject: Welcome to the platform content-type: text/html template: welcome
Example (Threads) ¶
ExampleMailbox_threads demonstrates thread-based messaging.
package main
import (
"context"
"fmt"
"log"
"github.com/rbaliyan/event/v3/transport/channel"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
// newTestService creates a connected service for examples using in-memory backends.
// Channel transport enables synchronous event delivery without Redis.
func newTestService() mailbox.Service {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithEventTransport(channel.New()),
)
if err != nil {
log.Fatal(err)
}
if err := svc.Connect(context.Background()); err != nil {
log.Fatal(err)
}
return svc
}
func main() {
ctx := context.Background()
svc := newTestService()
defer svc.Close(ctx)
alice := svc.Client("alice")
// Start a thread.
msg, _ := alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Thread starter",
Body: "Let's discuss.",
ThreadID: "thread-001",
})
// Reply in the same thread.
bob := svc.Client("bob")
bobInbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
bobMsgID := bobInbox.All()[0].GetID()
bob.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"alice"},
Subject: "Re: Thread starter",
Body: "Sounds good!",
ThreadID: "thread-001",
ReplyToID: bobMsgID,
})
// Get all messages in the thread.
thread, _ := alice.GetThread(ctx, "thread-001", store.ListOptions{})
fmt.Println("thread messages:", len(thread.All()))
// Get replies to the original message.
replies, _ := bob.GetReplies(ctx, msg.GetID(), store.ListOptions{})
fmt.Println("replies:", len(replies.All()))
}
Output: thread messages: 2 replies: 0
Index ¶
- Constants
- Variables
- func DefaultBlockedMIMETypes() []string
- func IsRetryableError(err error) bool
- func NewAttachmentManager(metadata store.AttachmentMetadataStore, files store.AttachmentFileStore) store.AttachmentManager
- func SafeAttachmentMIMETypes() []string
- func StatsCacheFolderTotal(folderID string) string
- func StatsCacheFolderUnread(folderID string) string
- func ValidateAttachments(attachments []store.Attachment, limits MessageLimits) error
- func ValidateAttachmentsWithMIME(attachments []store.Attachment, limits MessageLimits, ...) error
- func ValidateBody(body string) error
- func ValidateBodyWithLimits(body string, limits MessageLimits) error
- func ValidateDraft(draft store.DraftMessage, limits MessageLimits) error
- func ValidateHeaders(headers map[string]string, limits MessageLimits) error
- func ValidateMIMEType(contentType string, allowedTypes, blockedTypes []string) error
- func ValidateMessage(msg store.Message, limits MessageLimits) error
- func ValidateMessageContent(subject, body string) error
- func ValidateMessageContentWithLimits(subject, body string, limits MessageLimits) error
- func ValidateMetadata(metadata map[string]any) error
- func ValidateMetadataKey(key string) error
- func ValidateMetadataWithLimits(metadata map[string]any, limits MessageLimits) error
- func ValidateRecipients(recipientIDs []string, limits MessageLimits) error
- func ValidateSubject(subject string) error
- func ValidateSubjectWithLimits(subject string, limits MessageLimits) error
- type AttachmentLoader
- type AttachmentRefError
- type AttachmentResolver
- type AttachmentStore
- type BulkOperationError
- type BulkOperator
- type BulkResult
- type CleanupExpiredMessagesResult
- type CleanupTrashResult
- type Config
- type Draft
- type DraftClient
- type DraftComposer
- type DraftList
- type DraftListMutator
- type DraftListReader
- type DraftLister
- type DraftMutator
- type DraftPreparer
- type DraftPublisher
- type DraftReader
- type DraftSendResult
- type EnforceQuotasResult
- type EventPublishError
- type EventPublishFailureFunc
- type Flags
- type FolderInfo
- type FolderReader
- type ListOptions
- type Mailbox
- type MailboxMutator
- type MarkAllReadEvent
- type Message
- type MessageClient
- type MessageComposer
- type MessageDeletedEvent
- type MessageIterator
- type MessageLimits
- type MessageList
- type MessageListMutator
- type MessageListReader
- type MessageLister
- type MessageMovedEvent
- type MessageMutator
- type MessageReadEvent
- type MessageReader
- type MessageReceivedEvent
- type MessageSearcher
- type MessageSender
- type MessageSentEvent
- type MessageStreamer
- type OperationResult
- type Option
- func WithAttachmentManager(m store.AttachmentManager) Option
- func WithEventBus(bus *event.Bus) Option
- func WithEventErrorsFatal(fatal bool) Option
- func WithEventPublishFailureHandler(fn EventPublishFailureFunc) Option
- func WithEventTransport(t transport.Transport) Option
- func WithGlobalQuota(policy QuotaPolicy) Option
- func WithLogger(l *slog.Logger) Option
- func WithMeterProvider(mp metric.MeterProvider) Option
- func WithMetrics(enabled bool) Option
- func WithNotificationCoalescing(enabled bool) Option
- func WithNotifier(n *notify.Notifier) Option
- func WithOTel(enabled bool) Option
- func WithPlugin(p Plugin) Option
- func WithPlugins(plugins ...Plugin) Option
- func WithQuotaProvider(p QuotaProvider) Option
- func WithRedisClient(client redis.UniversalClient) Option
- func WithRegistrar(r Registrar) Option
- func WithServiceName(name string) Option
- func WithStatsCache(c StatsCache) Option
- func WithStore(s store.Store) Option
- func WithTracerProvider(tp trace.TracerProvider) Option
- func WithTracing(enabled bool) Option
- func WithUserResolver(r UserResolver) Option
- type PartialDeliveryError
- func (e *PartialDeliveryError) AllFailed() bool
- func (e *PartialDeliveryError) Error() string
- func (e *PartialDeliveryError) HasRetryableFailures() bool
- func (e *PartialDeliveryError) PermanentFailures() []string
- func (e *PartialDeliveryError) RetryableRecipients() []string
- func (e *PartialDeliveryError) SuccessRate() float64
- func (e *PartialDeliveryError) Unwrap() error
- type Plugin
- type PluginError
- type QuotaAction
- type QuotaExceededError
- type QuotaPolicy
- type QuotaProvider
- type QuotaUserLister
- type Recipient
- type RecipientResolver
- type Registrar
- type Router
- type SearchQuery
- type SendHook
- type SendRequest
- type Service
- type ServiceEvents
- type SortOrder
- type StaticQuotaProvider
- type StatsCache
- type StatsReader
- type StorageClient
- type StreamOptions
- type ThreadReader
- type User
- type UserResolver
- type ValidationError
Examples ¶
- Package (CleanupExpiredMessages)
- Package (CleanupTrash)
- Package (EncryptedMessage)
- Package (EventSubscription)
- Package (FilterBulkOps)
- Package (Filters)
- Package (ListFolders)
- Package (MessageTTL)
- Package (Notifications)
- Package (PartialDelivery)
- Package (ScheduledDelivery)
- Package (SseServer)
- Package (Stats)
- Package (SystemMessages)
- Package (Threads)
- New
- New (WithConfig)
Constants ¶
const ( EventNameMessageSent = "mailbox.message.sent" EventNameMessageReceived = "mailbox.message.received" EventNameMessageRead = "mailbox.message.read" EventNameMessageDeleted = "mailbox.message.deleted" EventNameMessageMoved = "mailbox.message.moved" EventNameMarkAllRead = "mailbox.mark_all_read" )
Event names for mailbox events.
const ( SortAsc = store.SortAsc SortDesc = store.SortDesc )
Re-exported sort order constants.
const ( DefaultTrashRetention = 30 * 24 * time.Hour // 30 days MinTrashRetention = 24 * time.Hour // 1 day minimum DefaultMessageRetention time.Duration = 0 // disabled by default MinMessageRetention = 24 * time.Hour // 1 day minimum when enabled DefaultShutdownTimeout = 30 * time.Second // default graceful shutdown timeout MinShutdownTimeout = 1 * time.Second // minimum shutdown timeout // Default message limits DefaultMaxSubjectLength = 998 // RFC 5322 max line length DefaultMaxBodySize = 10 * 1024 * 1024 // 10 MB DefaultMaxAttachmentSize = 25 * 1024 * 1024 // 25 MB per attachment DefaultMaxAttachmentCount = 20 // max attachments per message DefaultMaxRecipientCount = 100 // max recipients per message DefaultMaxMetadataSize = 64 * 1024 // 64 KB total metadata DefaultMaxMetadataKeys = 100 // max metadata keys // Default header limits DefaultMaxHeaderCount = 50 // max headers per message DefaultMaxHeaderKeyLength = 128 // max header key length DefaultMaxHeaderValueLength = 8 * 1024 // 8 KB max header value DefaultMaxHeadersTotalSize = 64 * 1024 // 64 KB total headers // Query limits DefaultMaxQueryLimit = 100 // max messages per query DefaultQueryLimit = 20 // default messages per query // Concurrency limits DefaultMaxConcurrentSends = 10 // max concurrent send operations per service // Stats cache DefaultStatsRefreshInterval = 30 * time.Second // TTL for cached stats // Orphan message claiming defaults (Redis transport only) DefaultClaimInterval = 30 * time.Second // scan for orphans every 30s DefaultClaimMinIdle = 60 * time.Second // claim messages idle for 60s+ DefaultClaimBatchSize = int64(100) // claim up to 100 messages per cycle // DefaultEventStreamMaxLen is the default approximate maximum number of entries // per Redis event stream. Set EventStreamMaxLen = -1 to disable trimming. DefaultEventStreamMaxLen = int64(10_000) )
Default configuration values.
const ( StatsCacheFieldTotal = "total" StatsCacheFieldUnread = "unread" StatsCacheFieldDraft = "draft" )
Known field names for StatsCache.IncrBy. Using constants prevents typos and makes it easy to change the encoding in one place.
const ( MetadataSenderFirstName = "sender.firstname" MetadataSenderLastName = "sender.lastname" MetadataSenderEmail = "sender.email" )
Metadata keys populated by UserResolver during message delivery. When a UserResolver is configured, these keys are set on message metadata with the sender's identity before validation and delivery.
const ( // Metadata limits MaxMetadataKeyLength = 256 // Maximum length of a metadata key // MaxTagIDLength is the maximum length for a tag ID. MaxTagIDLength = 256 // MinSubjectLength is the minimum subject length (non-empty after trimming) MinSubjectLength = 1 )
Validation constants for message content. These are the default values; use MessageLimits for configurable validation.
const MetadataMessageID = "message_id"
MetadataMessageID is the event metadata key used for coalescing. When notification coalescing is enabled, events with the same message_id in metadata are collapsed so only the latest is delivered.
Variables ¶
var ( // ErrNotFound is returned when a message cannot be found. // Wraps store.ErrNotFound for consistent error checking. ErrNotFound = fmt.Errorf("mailbox: %w", store.ErrNotFound) ErrUnauthorized = errors.New("mailbox: unauthorized") // ErrInvalidMessage is returned for message validation failures. ErrInvalidMessage = errors.New("mailbox: invalid message") // ErrEmptyRecipients is returned when no recipients are provided. // Wraps store.ErrEmptyRecipients for consistent error checking. ErrEmptyRecipients = fmt.Errorf("mailbox: %w", store.ErrEmptyRecipients) // ErrEmptySubject is returned when subject is empty. // Wraps store.ErrEmptySubject for consistent error checking. ErrEmptySubject = fmt.Errorf("mailbox: %w", store.ErrEmptySubject) // ErrStoreRequired is returned when no store is configured. ErrStoreRequired = errors.New("mailbox: store is required") // ErrNotConnected is returned when operations are attempted before Connect(). // Wraps store.ErrNotConnected for consistent error checking. ErrNotConnected = fmt.Errorf("mailbox: %w", store.ErrNotConnected) // ErrAlreadyConnected is returned when Connect() is called twice. // Wraps store.ErrAlreadyConnected for consistent error checking. ErrAlreadyConnected = fmt.Errorf("mailbox: %w", store.ErrAlreadyConnected) // ErrInvalidID is returned when an invalid ID is provided. // Wraps store.ErrInvalidID for consistent error checking. ErrInvalidID = fmt.Errorf("mailbox: %w", store.ErrInvalidID) // ErrDuplicateEntry is returned when a duplicate entry is detected. // Wraps store.ErrDuplicateEntry for consistent error checking. ErrDuplicateEntry = fmt.Errorf("mailbox: %w", store.ErrDuplicateEntry) // ErrFilterInvalid is returned when a filter is invalid. // Wraps store.ErrFilterInvalid for consistent error checking. ErrFilterInvalid = fmt.Errorf("mailbox: %w", store.ErrFilterInvalid) // ErrEventClientRequired is returned when event client is nil. ErrEventClientRequired = errors.New("mailbox: event client is required") // ErrNotInTrash is returned when trying to restore a message not in trash. ErrNotInTrash = errors.New("mailbox: message not in trash") // ErrAlreadyInTrash is returned when trying to trash an already trashed message. ErrAlreadyInTrash = errors.New("mailbox: message already in trash") // ErrAttachmentNotFound is returned when an attachment cannot be found. ErrAttachmentNotFound = errors.New("mailbox: attachment not found") // ErrAttachmentStoreNotConfigured is returned when attachment store is not configured. ErrAttachmentStoreNotConfigured = errors.New("mailbox: attachment store not configured") // ErrPartialDelivery is returned when some recipients failed to receive the message. ErrPartialDelivery = errors.New("mailbox: partial delivery") // ErrInvalidMetadata is returned when metadata validation fails. ErrInvalidMetadata = errors.New("mailbox: invalid metadata") // ErrMetadataKeyTooLong is returned when a metadata key exceeds the maximum length. ErrMetadataKeyTooLong = errors.New("mailbox: metadata key too long") // ErrMetadataTooLarge is returned when metadata exceeds the maximum size. ErrMetadataTooLarge = errors.New("mailbox: metadata too large") // ErrSubjectTooLong is returned when subject exceeds maximum length. ErrSubjectTooLong = errors.New("mailbox: subject too long") // ErrBodyTooLarge is returned when body exceeds maximum size. ErrBodyTooLarge = errors.New("mailbox: body too large") // ErrInvalidContent is returned when message content contains invalid characters. ErrInvalidContent = errors.New("mailbox: invalid content") // ErrTooManyRecipients is returned when recipient count exceeds the limit. ErrTooManyRecipients = errors.New("mailbox: too many recipients") // ErrInvalidRecipient is returned when a recipient ID is invalid. ErrInvalidRecipient = errors.New("mailbox: invalid recipient") // ErrTooManyAttachments is returned when attachment count exceeds the limit. ErrTooManyAttachments = errors.New("mailbox: too many attachments") // ErrAttachmentTooLarge is returned when an attachment exceeds the size limit. ErrAttachmentTooLarge = errors.New("mailbox: attachment too large") // ErrInvalidAttachment is returned when attachment data is invalid. ErrInvalidAttachment = errors.New("mailbox: invalid attachment") // ErrInvalidMIMEType is returned when an attachment has an invalid or disallowed MIME type. ErrInvalidMIMEType = errors.New("mailbox: invalid mime type") // ErrInvalidHeaders is returned when headers validation fails. ErrInvalidHeaders = errors.New("mailbox: invalid headers") // ErrTooManyHeaders is returned when header count exceeds the limit. ErrTooManyHeaders = errors.New("mailbox: too many headers") // ErrHeaderKeyTooLong is returned when a header key exceeds the maximum length. ErrHeaderKeyTooLong = errors.New("mailbox: header key too long") // ErrHeaderValueTooLong is returned when a header value exceeds the maximum length. ErrHeaderValueTooLong = errors.New("mailbox: header value too long") // ErrHeadersTooLarge is returned when total headers size exceeds the limit. ErrHeadersTooLarge = errors.New("mailbox: headers too large") // ErrInvalidFolderID is returned when a folder ID is invalid. // Wraps store.ErrInvalidFolderID for consistent error checking. ErrInvalidFolderID = fmt.Errorf("mailbox: %w", store.ErrInvalidFolderID) // ErrRateLimited is returned when a user exceeds their rate limit. ErrRateLimited = errors.New("mailbox: rate limited") // ErrInvalidUserID is returned when a user ID contains invalid characters. ErrInvalidUserID = errors.New("mailbox: invalid user id") // ErrInvalidIdempotencyKey is returned when an idempotency key is invalid. // Wraps store.ErrInvalidIdempotencyKey for consistent error checking. ErrInvalidIdempotencyKey = fmt.Errorf("mailbox: %w", store.ErrInvalidIdempotencyKey) // ErrCacheInvalidationFailed is returned when cache invalidation fails in strict mode. // The underlying operation succeeded, but cached data may be stale. ErrCacheInvalidationFailed = errors.New("mailbox: cache invalidation failed") // ErrFolderMismatch is returned by conditional MoveToFolder when the message // exists but is not in the expected source folder. // Wraps store.ErrFolderMismatch for consistent error checking. ErrFolderMismatch = fmt.Errorf("mailbox: %w", store.ErrFolderMismatch) // ErrNotifierNotConfigured is returned when Notifications() is called // without a notifier configured via WithNotifier. ErrNotifierNotConfigured = errors.New("mailbox: notifier not configured") // ErrInvalidTTL is returned when a TTL value is outside configured bounds. ErrInvalidTTL = errors.New("mailbox: invalid TTL") // ErrInvalidSchedule is returned when a schedule delay is outside configured bounds. ErrInvalidSchedule = errors.New("mailbox: invalid schedule") // ErrQuotaExceeded is returned when a user's mailbox has reached its message quota. // Use errors.As with *QuotaExceededError to get details (user ID, current count, limit). ErrQuotaExceeded = errors.New("mailbox: quota exceeded") // ErrQuotaUserListerNotConfigured is returned by RunQuotaEnforcement when no // QuotaUserLister has been configured in Config. ErrQuotaUserListerNotConfigured = errors.New("mailbox: quota user lister not configured") // ErrUserResolveFailed is returned when the UserResolver fails to resolve // the sender's identity. The message is not sent. ErrUserResolveFailed = errors.New("mailbox: user resolve failed") // ErrRecipientNotFound is returned by RecipientResolver implementations // when a user ID cannot be resolved to a recipient. ErrRecipientNotFound = errors.New("mailbox: recipient not found") )
Sentinel errors for the mailbox package. Use errors.Is() to check for these errors.
These errors wrap corresponding store-level errors where applicable, so errors.Is(err, mailbox.ErrNotFound) will match both mailbox-level and store-level "not found" errors.
var ( // EventMessageSent is published when a message is sent. // Deprecated: Use Service.Events().MessageSent instead. EventMessageSent = event.New[MessageSentEvent](EventNameMessageSent) // EventMessageReceived is published when a message is delivered to a recipient. // Deprecated: Use Service.Events().MessageReceived instead. EventMessageReceived = event.New[MessageReceivedEvent](EventNameMessageReceived) // EventMessageRead is published when a message is marked as read. // Deprecated: Use Service.Events().MessageRead instead. EventMessageRead = event.New[MessageReadEvent](EventNameMessageRead) // EventMessageDeleted is published when a message is permanently deleted. // Deprecated: Use Service.Events().MessageDeleted instead. EventMessageDeleted = event.New[MessageDeletedEvent](EventNameMessageDeleted) // EventMessageMoved is published when a message is moved between folders. // Deprecated: Use Service.Events().MessageMoved instead. EventMessageMoved = event.New[MessageMovedEvent](EventNameMessageMoved) // EventMarkAllRead is published when all messages in a folder are marked as read. // Deprecated: Use Service.Events().MarkAllRead instead. EventMarkAllRead = event.New[MarkAllReadEvent](EventNameMarkAllRead) )
Global event instances.
Deprecated: These global events use "first registration wins" semantics, which makes parallel testing unreliable and prevents multiple independent services in the same process. Prefer using Service.Events() for per-service event access.
var ( // FlagsMarkRead marks a message as read. FlagsMarkRead = Flags{Read: ptrTrue} // FlagsMarkUnread marks a message as unread. FlagsMarkUnread = Flags{Read: ptrFalse} // FlagsMarkArchived archives a message. FlagsMarkArchived = Flags{Archived: ptrTrue} // FlagsMarkUnarchived unarchives a message. FlagsMarkUnarchived = Flags{Archived: ptrFalse} )
Pre-allocated flag values for common operations. These are more efficient than calling MarkRead(), etc. in hot paths.
var ErrIteratorOutOfBounds = errors.New("mailbox: iterator out of bounds - call Next() first")
MessageIterator provides streaming access to messages. It implements a pull-based iteration pattern for memory-efficient processing of large result sets.
Iterator vs List: When to Use Each ¶
Use MessageIterator (Stream* methods) when:
- Processing large result sets where memory is a concern
- You need to process messages one at a time
- You want early termination without loading all data
- Building ETL pipelines or data exports
Use MessageList (Inbox, Sent, etc.) when:
- Building paginated UIs with total counts
- You need bulk operations (MarkRead, Move, Delete all)
- Result sets are small and fit comfortably in memory
- You need random access to results
Example comparison:
// Iterator: memory-efficient, process one at a time
iter, _ := mb.Stream(ctx, nil, StreamOptions{BatchSize: 100})
for hasNext, err := iter.Next(ctx); hasNext && err == nil; hasNext, err = iter.Next(ctx) {
msg, _ := iter.Message()
// process each message individually
}
// List: loads page into memory, supports bulk ops
list, _ := mb.Folder(ctx, store.FolderInbox, store.ListOptions{Limit: 50})
list.MarkRead(ctx) // bulk operation on all 50 messages
fmt.Printf("Showing %d of %d total", len(list.All()), list.Total())
Iterator Usage ¶
The iterator is stateless and holds no resources requiring cleanup. Simply stop calling Next() when done - the GC handles the rest.
Example:
iter, _ := mb.Stream(ctx, nil, StreamOptions{BatchSize: 100})
for {
hasNext, err := iter.Next(ctx)
if err != nil {
// handle error (e.g., service disconnected, context cancelled)
break
}
if !hasNext {
break
}
msg, _ := iter.Message()
// process message - can use msg.Update(), msg.Delete(), etc.
}
ErrIteratorOutOfBounds is returned when Message() is called without a successful Next().
Functions ¶
func DefaultBlockedMIMETypes ¶
func DefaultBlockedMIMETypes() []string
DefaultBlockedMIMETypes returns MIME types that are commonly blocked for security.
func IsRetryableError ¶
IsRetryableError determines if an error is retryable. Returns true for temporary/transient errors, false for permanent errors. Handles both mailbox-level and store-level errors.
func NewAttachmentManager ¶
func NewAttachmentManager(metadata store.AttachmentMetadataStore, files store.AttachmentFileStore) store.AttachmentManager
NewAttachmentManager creates an attachment manager with reference-counted deletion.
func SafeAttachmentMIMETypes ¶
func SafeAttachmentMIMETypes() []string
SafeAttachmentMIMETypes returns commonly allowed safe MIME types.
func StatsCacheFolderTotal ¶ added in v0.7.6
StatsCacheFolderTotal returns the IncrBy field name for a folder's total count.
func StatsCacheFolderUnread ¶ added in v0.7.6
StatsCacheFolderUnread returns the IncrBy field name for a folder's unread count.
func ValidateAttachments ¶
func ValidateAttachments(attachments []store.Attachment, limits MessageLimits) error
ValidateAttachments validates the attachment list.
func ValidateAttachmentsWithMIME ¶
func ValidateAttachmentsWithMIME(attachments []store.Attachment, limits MessageLimits, allowedTypes, blockedTypes []string) error
ValidateAttachmentsWithMIME validates attachments with MIME type restrictions. allowedTypes: if non-empty, only these MIME types are allowed. blockedTypes: these MIME types are always blocked, even if in allowedTypes.
func ValidateBody ¶
ValidateBody validates a message body using default limits. For configurable limits, use ValidateBodyWithLimits.
func ValidateBodyWithLimits ¶
func ValidateBodyWithLimits(body string, limits MessageLimits) error
ValidateBodyWithLimits validates a message body against configurable limits.
func ValidateDraft ¶
func ValidateDraft(draft store.DraftMessage, limits MessageLimits) error
ValidateDraft performs full validation of a draft before sending.
func ValidateHeaders ¶ added in v0.5.0
func ValidateHeaders(headers map[string]string, limits MessageLimits) error
ValidateHeaders validates message headers against configurable limits.
func ValidateMIMEType ¶
ValidateMIMEType validates a MIME type against allowed and blocked lists. Returns nil if the MIME type is valid.
func ValidateMessage ¶
func ValidateMessage(msg store.Message, limits MessageLimits) error
ValidateMessage performs full validation of a message.
func ValidateMessageContent ¶
ValidateMessageContent validates subject and body together using default limits.
func ValidateMessageContentWithLimits ¶
func ValidateMessageContentWithLimits(subject, body string, limits MessageLimits) error
ValidateMessageContentWithLimits validates subject and body with configurable limits.
func ValidateMetadata ¶
ValidateMetadata validates metadata against size and key constraints using default limits. For configurable limits, use ValidateMetadataWithLimits.
func ValidateMetadataKey ¶
ValidateMetadataKey validates a single metadata key.
func ValidateMetadataWithLimits ¶
func ValidateMetadataWithLimits(metadata map[string]any, limits MessageLimits) error
ValidateMetadataWithLimits validates metadata against configurable limits.
func ValidateRecipients ¶
func ValidateRecipients(recipientIDs []string, limits MessageLimits) error
ValidateRecipients validates the recipient list.
func ValidateSubject ¶
ValidateSubject validates a message subject using default limits. For configurable limits, use ValidateSubjectWithLimits.
func ValidateSubjectWithLimits ¶
func ValidateSubjectWithLimits(subject string, limits MessageLimits) error
ValidateSubjectWithLimits validates a message subject against configurable limits.
Types ¶
type AttachmentLoader ¶
type AttachmentLoader interface {
LoadAttachment(ctx context.Context, messageID, attachmentID string) (io.ReadCloser, error)
}
AttachmentLoader provides attachment access.
type AttachmentRefError ¶
type AttachmentRefError struct {
// Operation is either "add" or "release"
Operation string
// Failed maps attachment IDs to their errors
Failed map[string]error
// RollbackFailed maps attachment IDs to rollback errors (only for "add" operations)
RollbackFailed map[string]error
}
AttachmentRefError provides details about attachment reference counting failures. This error is returned when adding or releasing attachment references fails. Use errors.As() to extract and inspect the details.
func (*AttachmentRefError) Error ¶
func (e *AttachmentRefError) Error() string
func (*AttachmentRefError) FailedIDs ¶
func (e *AttachmentRefError) FailedIDs() []string
FailedIDs returns the list of attachment IDs that failed.
func (*AttachmentRefError) HasRollbackFailures ¶
func (e *AttachmentRefError) HasRollbackFailures() bool
HasRollbackFailures returns true if rollback also failed during an add operation.
func (*AttachmentRefError) Unwrap ¶
func (e *AttachmentRefError) Unwrap() []error
Unwrap returns the individual errors from failed operations.
type AttachmentResolver ¶
type AttachmentResolver interface {
ResolveAttachments(ctx context.Context, attachmentIDs []string) ([]store.Attachment, error)
}
AttachmentResolver provides attachment metadata resolution by ID. This is useful for server integrations that need to resolve attachment references without loading the full message.
type AttachmentStore ¶
type AttachmentStore = store.AttachmentFileStore
AttachmentStore is an alias for store.AttachmentFileStore. Deprecated: Use store.AttachmentFileStore directly.
type BulkOperationError ¶
type BulkOperationError struct {
Result *BulkResult
}
BulkOperationError is returned when a bulk operation has partial failures. It wraps BulkResult to provide error interface while guaranteeing non-empty Error().
func (*BulkOperationError) Error ¶
func (e *BulkOperationError) Error() string
Error implements the error interface. Always returns a non-empty string describing the failure.
func (*BulkOperationError) Unwrap ¶
func (e *BulkOperationError) Unwrap() []error
Unwrap returns the individual errors from failed operations.
type BulkOperator ¶
type BulkOperator interface {
BulkUpdateFlags(ctx context.Context, messageIDs []string, flags Flags) (*BulkResult, error)
BulkMove(ctx context.Context, messageIDs []string, folderID string, opts ...store.MoveOption) (*BulkResult, error)
BulkDelete(ctx context.Context, messageIDs []string) (*BulkResult, error)
BulkPermanentlyDelete(ctx context.Context, messageIDs []string) (*BulkResult, error)
BulkAddTag(ctx context.Context, messageIDs []string, tagID string) (*BulkResult, error)
BulkRemoveTag(ctx context.Context, messageIDs []string, tagID string) (*BulkResult, error)
}
BulkOperator provides bulk mutation operations by message IDs. Each method operates on a list of message IDs and returns a BulkResult with per-ID success/failure information.
type BulkResult ¶
type BulkResult struct {
// Results contains the outcome of each operation in input order.
Results []OperationResult
}
BulkResult contains the result of a bulk operation. Used consistently across MessageList and DraftList operations.
Results are returned in order, matching the input order. Use helper methods to check status and iterate results.
func (*BulkResult) Err ¶
func (r *BulkResult) Err() error
Err returns an error if there are failures, nil otherwise.
func (*BulkResult) FailedIDs ¶
func (r *BulkResult) FailedIDs() []string
FailedIDs returns the IDs of items that failed.
func (*BulkResult) FailureCount ¶
func (r *BulkResult) FailureCount() int
FailureCount returns the number of failed operations.
func (*BulkResult) HasFailures ¶
func (r *BulkResult) HasFailures() bool
HasFailures returns true if any operations failed.
func (*BulkResult) SuccessCount ¶
func (r *BulkResult) SuccessCount() int
SuccessCount returns the number of successful operations.
func (*BulkResult) SuccessfulIDs ¶
func (r *BulkResult) SuccessfulIDs() []string
SuccessfulIDs returns the IDs of items that succeeded.
func (*BulkResult) TotalCount ¶
func (r *BulkResult) TotalCount() int
TotalCount returns the total number of items processed.
type CleanupExpiredMessagesResult ¶ added in v0.6.3
type CleanupExpiredMessagesResult struct {
// DeletedCount is the number of messages permanently deleted.
DeletedCount int
// Interrupted indicates if the cleanup was interrupted (e.g., context cancelled).
Interrupted bool
}
CleanupExpiredMessagesResult contains the result of a message retention cleanup.
type CleanupTrashResult ¶
type CleanupTrashResult struct {
// DeletedCount is the number of messages permanently deleted.
DeletedCount int
// Interrupted indicates if the cleanup was interrupted (e.g., context cancelled).
Interrupted bool
}
CleanupTrashResult contains the result of a trash cleanup operation.
type Config ¶ added in v0.7.1
type Config struct {
// TrashCleanupInterval is how often to run automatic trash cleanup.
// Zero disables automatic trash cleanup (caller must invoke CleanupTrash manually).
TrashCleanupInterval time.Duration
// ExpiredMessageCleanupInterval is how often to run automatic expired message cleanup.
// This covers both global retention (MessageRetention) and per-message TTL expiry.
// Zero disables automatic cleanup (caller must invoke CleanupExpiredMessages manually).
ExpiredMessageCleanupInterval time.Duration
// QuotaEnforcementInterval is how often to run automatic quota enforcement.
// Requires QuotaUserLister to be set; ignored if nil.
// Zero disables automatic quota enforcement (caller must invoke EnforceQuotas manually).
QuotaEnforcementInterval time.Duration
// QuotaUserLister provides the list of user IDs for background quota enforcement.
// Required when QuotaEnforcementInterval > 0. If nil, quota enforcement is skipped
// even when the interval is set.
QuotaUserLister QuotaUserLister
// TrashRetention is how long messages stay in trash before cleanup.
// Default is 30 days. Minimum is 1 day; shorter values are ignored.
TrashRetention time.Duration
// MessageRetention is the global message TTL based on creation time.
// Messages older than this are permanently deleted by CleanupExpiredMessages.
// Default is 0 (disabled). Minimum is 1 day when enabled.
MessageRetention time.Duration
// MaxSubjectLength is the maximum subject length in characters.
// Default is 998 (RFC 5322).
MaxSubjectLength int
// MaxBodySize is the maximum body size in bytes.
// Default is 10 MB.
MaxBodySize int
// MaxAttachmentSize is the maximum size per attachment in bytes.
// Default is 25 MB.
MaxAttachmentSize int64
// MaxAttachmentCount is the maximum number of attachments per message.
// Default is 20.
MaxAttachmentCount int
// MaxRecipientCount is the maximum number of recipients per message.
// Default is 100.
MaxRecipientCount int
// MaxMetadataSize is the maximum total metadata size in bytes.
// Default is 64 KB.
MaxMetadataSize int
// MaxMetadataKeys is the maximum number of metadata keys per message.
// Default is 100.
MaxMetadataKeys int
// MaxHeaderCount is the maximum number of headers per message.
// Default is 50.
MaxHeaderCount int
// MaxHeaderKeyLength is the maximum length of a single header key.
// Default is 128 bytes.
MaxHeaderKeyLength int
// MaxHeaderValueLength is the maximum length of a single header value.
// Default is 8 KB.
MaxHeaderValueLength int
// MaxHeadersTotalSize is the maximum total size of all headers combined.
// Default is 64 KB.
MaxHeadersTotalSize int
// MaxQueryLimit is the maximum number of messages per query.
// Any query requesting more than this limit will be capped.
// Default is 100.
MaxQueryLimit int
// DefaultQueryLimit is the default number of messages per query
// when no limit is specified. Capped to MaxQueryLimit.
// Default is 20.
DefaultQueryLimit int
// MaxConcurrentSends is the maximum number of concurrent send operations.
// Default is 10.
MaxConcurrentSends int
// ShutdownTimeout is the maximum time to wait for in-flight operations
// during graceful shutdown. Minimum is 1 second.
// Default is 30 seconds.
ShutdownTimeout time.Duration
// DefaultTTL is the default per-message time-to-live. When a message is
// sent without an explicit TTL, this default is applied.
// Default is 0 (disabled — messages do not expire unless explicitly set).
DefaultTTL time.Duration
// MinTTL is the minimum allowed TTL. Messages with a shorter TTL are rejected.
// Default is 1 minute.
MinTTL time.Duration
// MaxTTL is the maximum allowed TTL. Messages with a longer TTL are rejected.
// Default is 0 (unlimited).
MaxTTL time.Duration
// MinScheduleDelay is the minimum allowed schedule delay from now.
// Default is 0 (no minimum).
MinScheduleDelay time.Duration
// MaxScheduleDelay is the maximum allowed schedule delay from now.
// Default is 0 (unlimited).
MaxScheduleDelay time.Duration
// StatsRefreshInterval is the TTL for cached mailbox stats.
// Default is 30 seconds.
StatsRefreshInterval time.Duration
// ClaimInterval is how often the background goroutine scans for orphaned
// messages in Redis consumer groups. Default is 30 seconds.
ClaimInterval time.Duration
// ClaimMinIdle is the minimum idle time before a message is eligible
// for claiming from dead consumers. Default is 60 seconds.
ClaimMinIdle time.Duration
// ClaimBatchSize is the maximum number of orphaned messages to claim per cycle.
// Default is 100.
ClaimBatchSize int64
// EventStreamMaxLen is the approximate maximum number of entries per
// Redis event stream. Older entries are trimmed automatically on each publish.
// Default is 10,000 (see DefaultEventStreamMaxLen).
// Set to -1 to disable trimming (not recommended for production — streams grow without bound).
EventStreamMaxLen int64
}
Config holds service-level configuration. Zero values use library defaults (see Default* constants in option.go). Use DefaultConfig() for a configuration with all defaults, preserving backward-compatible behavior.
func DefaultConfig ¶ added in v0.7.1
func DefaultConfig() Config
DefaultConfig returns a Config with all fields set to their library defaults. Background maintenance tasks are disabled (intervals are zero); enable them by setting the relevant interval fields.
Use DefaultConfig as a starting point and override individual fields:
cfg := mailbox.DefaultConfig() cfg.TrashCleanupInterval = 1 * time.Hour cfg.EventStreamMaxLen = 50_000
func (*Config) Validate ¶ added in v0.7.3
Validate checks that all critical resource limits are explicitly bounded. Call this after constructing a Config to catch missing limits before Connect.
Returns a joined error listing every field that is zero or unlimited. Fields that are intentionally unbounded (MessageRetention, MaxTTL, MaxScheduleDelay) are not checked — they have documented "0 = disabled/unlimited" semantics.
type Draft ¶
type Draft interface {
DraftReader
DraftComposer
DraftPreparer
DraftPublisher
DraftMutator
}
Draft represents a message being composed. Use Mailbox.Compose() to create a new draft.
Composed of:
- DraftReader: Read draft content (ID, Subject, Body, etc.)
- DraftComposer: Fluent setters (SetSubject, SetBody, SetRecipients, SetMetadata)
- DraftPreparer: Failable operations (AddAttachment, ReplyTo)
- DraftPublisher: Lifecycle operations (Send, Save)
- DraftMutator: Mutation operations (Delete)
Usage pattern:
draft, _ := mailbox.Compose()
draft.SetRecipients("user1").SetSubject("Hello").SetBody("World") // fluent chain
if err := draft.AddAttachment(att); err != nil { ... } // separate call
if err := draft.ReplyTo(ctx, parentID); err != nil { ... } // separate call
msg, err := draft.Send(ctx)
type DraftClient ¶
type DraftClient interface {
DraftLister
MessageComposer
}
DraftClient provides draft-related operations. Use this interface when you only need draft access.
type DraftComposer ¶
type DraftComposer interface {
SetRecipients(recipientIDs ...string) DraftComposer
SetDeliverTo(recipientIDs ...string) DraftComposer
SetSubject(subject string) DraftComposer
SetBody(body string) DraftComposer
SetHeader(key, value string) DraftComposer
SetMetadata(key string, value any) DraftComposer
// SetTTL sets the message time-to-live. The message will be eligible for
// automatic deletion after this duration from send time. A zero duration
// clears any previously set TTL.
SetTTL(d time.Duration) DraftComposer
// SetScheduleAt sets the time at which the message becomes visible to
// recipients. Before this time, the message is hidden from queries.
// A zero time clears any previously set schedule.
SetScheduleAt(t time.Time) DraftComposer
// SetExternalID sets a caller-defined external identifier for correlation
// with external systems (e.g. SMTP Message-ID). Stored indexed; use
// store.ExternalIDIs to look up messages by this value.
SetExternalID(id string) DraftComposer
}
DraftComposer provides fluent setter methods for composing a draft. All setter methods return DraftComposer to enable chaining:
draft.SetRecipients("user1").SetSubject("Hello").SetBody("World")
For operations that can fail (AddAttachment, ReplyTo), use the methods on Draft directly — they are not part of the fluent interface.
type DraftList ¶
type DraftList interface {
DraftListReader
DraftListMutator
}
DraftList provides access to a paginated list of drafts with bulk operations.
Composed of:
- DraftListReader: Read-only access (All, Total, HasMore, NextCursor)
- DraftListMutator: Bulk mutations (Delete, Send)
type DraftListMutator ¶
type DraftListMutator interface {
// Delete deletes all drafts in this list.
Delete(ctx context.Context) (*BulkResult, error)
// Send sends all drafts in this list.
// Returns results for each draft (success or failure).
// Use result.SentMessages() to access the sent messages.
Send(ctx context.Context) (*DraftSendResult, error)
}
DraftListMutator provides bulk mutation operations on a list of drafts.
type DraftListReader ¶
type DraftListReader interface {
// All returns all drafts in this list.
All() []Draft
// Total returns the total count of drafts matching the query (not just this page).
Total() int64
// HasMore returns true if there are more drafts after this page.
HasMore() bool
// NextCursor returns the cursor for fetching the next page.
NextCursor() string
}
DraftListReader provides read-only access to a paginated list of drafts.
type DraftLister ¶
type DraftLister interface {
Drafts(ctx context.Context, opts store.ListOptions) (DraftList, error)
GetDraft(ctx context.Context, id string) (Draft, error)
}
DraftLister provides draft listing.
type DraftMutator ¶
type DraftMutator interface {
// Delete deletes the draft.
// If the draft was saved, it's permanently deleted from storage.
// If the draft was not saved, this is a no-op.
Delete(ctx context.Context) error
}
DraftMutator provides mutation operations on a draft.
type DraftPreparer ¶
type DraftPreparer interface {
// AddAttachment adds an attachment after validating it.
// Returns an error if the attachment is invalid or limits are exceeded.
AddAttachment(attachment store.Attachment) error
// ReplyTo sets this draft as a reply to another message.
// It looks up the parent message to inherit the thread ID.
// Returns an error if the parent message cannot be found or accessed.
ReplyTo(ctx context.Context, messageID string) error
// Forward prepares this draft as a forward of another message.
// Copies subject (with "Fwd:" prefix), body, and attachments from the original.
// Returns an error if the original message cannot be found or accessed.
Forward(ctx context.Context, messageID string) error
}
DraftPreparer provides draft preparation methods that can fail. These methods return errors and are intentionally not part of the fluent DraftComposer interface to keep the builder pattern clean.
type DraftPublisher ¶
type DraftPublisher interface {
// Send validates fully and sends the draft.
// Requires: at least one recipient and a non-empty subject.
// Creates recipient copies and moves sender's copy to sent folder.
// Returns ErrEmptyRecipients if no recipients, ErrEmptySubject if no subject.
Send(ctx context.Context) (Message, error)
// Save saves the draft without sending.
// Allows incomplete drafts (empty recipients, empty subject) for later editing.
// Validates: body size, metadata size, attachment limits.
// The draft can be retrieved later via Drafts() and completed.
Save(ctx context.Context) (Draft, error)
}
DraftPublisher provides lifecycle operations that transform or persist a draft.
Validation differences between Save() and Send():
- Send() requires: non-empty recipients, non-empty subject, valid content
- Save() allows: empty recipients, empty subject (for work-in-progress drafts)
- Both validate: body size limits, metadata limits, attachment limits
This allows users to save incomplete drafts and complete them later.
type DraftReader ¶
type DraftReader interface {
ID() string
OwnerID() string
Subject() string
Body() string
RecipientIDs() []string
DeliverTo() []string
Headers() map[string]string
Metadata() map[string]any
Attachments() []store.Attachment
ThreadID() string
ReplyToID() string
ExternalID() string
}
DraftReader provides read access to draft content.
type DraftSendResult ¶ added in v0.3.0
type DraftSendResult struct {
*BulkResult
// contains filtered or unexported fields
}
DraftSendResult extends BulkResult with sent messages from DraftList.Send().
func (*DraftSendResult) SentMessages ¶ added in v0.3.0
func (r *DraftSendResult) SentMessages() []Message
SentMessages returns all successfully sent messages in order.
type EnforceQuotasResult ¶ added in v0.6.3
type EnforceQuotasResult struct {
// UsersChecked is the number of users whose quotas were evaluated.
UsersChecked int
// UsersOverQuota is the number of users found over their quota limit.
UsersOverQuota int
// MessagesDeleted is the total number of messages deleted across all users.
MessagesDeleted int
// Interrupted indicates if the enforcement was stopped early (e.g., context cancelled).
Interrupted bool
}
EnforceQuotasResult contains the result of a quota enforcement run.
type EventPublishError ¶
type EventPublishError struct {
Event string // The event name (e.g., "MessageSent", "MessageRead")
MessageID string // The message ID the event was for
Err error // The underlying publish error
}
EventPublishError is returned when event publishing fails but the operation succeeded. The message was sent/read/deleted, but the event notification failed. Check the MessageID field to identify which message this applies to.
func IsEventPublishError ¶
func IsEventPublishError(err error) (*EventPublishError, bool)
IsEventPublishError checks if the error is an event publish error and returns details. This is useful when eventErrorsFatal=true but you still want to know the message was sent.
func (*EventPublishError) Error ¶
func (e *EventPublishError) Error() string
func (*EventPublishError) Unwrap ¶
func (e *EventPublishError) Unwrap() error
type EventPublishFailureFunc ¶
EventPublishFailureFunc is called when an event fails to publish. The eventName is the name of the event (e.g., "MessageSent"), and err is the publish error.
type Flags ¶
type Flags struct {
Read *bool // nil = no change, true = mark read, false = mark unread
Archived *bool // nil = no change, true = archive, false = unarchive
}
Flags represents message flags that can be updated atomically. Use nil values to indicate no change.
func MarkUnarchived ¶
func MarkUnarchived() Flags
MarkUnarchived returns flags to unarchive a message.
func (Flags) WithArchived ¶
WithArchived returns flags with archived status set.
type FolderInfo ¶
type FolderInfo struct {
// ID is the folder identifier (e.g., "__inbox", "custom-folder").
ID string
// Name is the display name for the folder.
Name string
// IsSystem indicates if this is a system folder (starts with "__").
IsSystem bool
// MessageCount is the total number of messages in the folder.
MessageCount int64
// UnreadCount is the number of unread messages in the folder.
UnreadCount int64
}
FolderInfo provides information about a folder.
type FolderReader ¶
type FolderReader interface {
ListFolders(ctx context.Context) ([]FolderInfo, error)
}
FolderReader provides folder information.
type ListOptions ¶
type ListOptions = store.ListOptions
Type aliases for commonly used store types. These allow users to work with the mailbox package without importing store directly.
type Mailbox ¶
type Mailbox interface {
UserID() string
MessageClient
DraftClient
StorageClient
MailboxMutator
MessageSender
BulkOperator
AttachmentResolver
StatsReader
}
Mailbox provides email-like messaging functionality for a user. This is the main interface for mailbox operations.
Composed of focused client interfaces:
- MessageClient: All message read operations (Get, List, Search, Stream, Threads)
- DraftClient: Draft operations (List drafts, Compose new)
- StorageClient: Storage operations (Folders, Attachments)
- MailboxMutator: Direct mutation by message ID (UpdateFlags, Move, Delete, Tags)
- MessageSender: Direct message sending without drafts (SendMessage)
- BulkOperator: Bulk mutations by message IDs (BulkUpdateFlags, BulkMove, etc.)
- AttachmentResolver: Resolve attachment metadata by ID
- StatsReader: Aggregate statistics (Stats, UnreadCount)
For applications needing only a subset of functionality, use the focused interfaces directly (MessageClient, DraftClient, StorageClient).
For single message operations via a message handle, use the methods on the Message interface returned by Get().
For bulk operations on listed messages, use the methods on MessageList:
inbox, _ := mailbox.Folder(ctx, store.FolderInbox, opts) inbox.MarkRead(ctx) // mark all as read inbox.Move(ctx, "archive") // move all to archive inbox.Delete(ctx) // delete all
type MailboxMutator ¶
type MailboxMutator interface {
UpdateFlags(ctx context.Context, messageID string, flags Flags) error
MoveToFolder(ctx context.Context, messageID string, folderID string, opts ...store.MoveOption) error
Delete(ctx context.Context, messageID string) error
Restore(ctx context.Context, messageID string) error
PermanentlyDelete(ctx context.Context, messageID string) error
AddTag(ctx context.Context, messageID string, tagID string) error
RemoveTag(ctx context.Context, messageID string, tagID string) error
// MarkAllRead marks all unread messages in a folder as read.
// Uses store.BulkReadMarker for a single database operation when available,
// falling back to individual MarkRead calls otherwise.
// Returns the number of messages that were marked as read.
MarkAllRead(ctx context.Context, folderID string) (int64, error)
// UpdateByFilter marks messages matching filters as read/unread.
UpdateByFilter(ctx context.Context, filters []store.Filter, flags Flags) (int64, error)
// MoveByFilter moves messages matching filters to the given folder.
MoveByFilter(ctx context.Context, filters []store.Filter, folderID string) (int64, error)
// DeleteByFilter soft-deletes messages matching filters (moves to trash).
DeleteByFilter(ctx context.Context, filters []store.Filter) (int64, error)
// TagByFilter adds a tag to messages matching filters.
TagByFilter(ctx context.Context, filters []store.Filter, tagID string) (int64, error)
// UntagByFilter removes a tag from messages matching filters.
UntagByFilter(ctx context.Context, filters []store.Filter, tagID string) (int64, error)
}
MailboxMutator provides mutation operations on messages by ID. These methods are equivalent to calling Get() then mutating the Message, but skip the intermediate Get for efficiency in server integrations.
type MarkAllReadEvent ¶ added in v0.5.0
type MarkAllReadEvent struct {
UserID string `json:"user_id"`
FolderID string `json:"folder_id"`
Count int64 `json:"count"`
MarkedAt time.Time `json:"marked_at"`
}
MarkAllReadEvent is published when all messages in a folder are marked as read. Use this for bulk read receipt tracking and cache invalidation.
type Message ¶
type Message interface {
store.Message
MessageMutator
}
Message provides access to a message with mutation capabilities.
This is the application-level message type returned by Mailbox operations. It wraps store.Message (the storage-level type) and adds user-scoped mutations. Read methods (GetID, GetSubject, etc.) come from store.Message; mutation methods (Update, Move, Delete, etc.) come from MessageMutator and are scoped to the owning user's mailbox.
Important: Message is a snapshot of state at retrieval time. After mutations, getter methods (GetIsRead, GetFolderID, etc.) may return stale values. To get fresh state after mutations, call Mailbox.Get() again.
Composed of:
- store.Message: Read-only access (GetID, GetSubject, GetBody, etc.)
- MessageMutator: Mutations (Update, Move, Delete, AddTag, etc.)
type MessageClient ¶
type MessageClient interface {
MessageReader
MessageLister
MessageSearcher
MessageStreamer
ThreadReader
}
MessageClient provides all message-related read operations. Use this interface when you only need message access without draft/folder operations.
type MessageComposer ¶
MessageComposer provides message composition.
type MessageDeletedEvent ¶
type MessageDeletedEvent struct {
MessageID string `json:"message_id"`
UserID string `json:"user_id"`
FolderID string `json:"folder_id"`
WasUnread bool `json:"was_unread"`
DeletedAt time.Time `json:"deleted_at"`
}
MessageDeletedEvent is published when a message is permanently deleted. This event is only published for permanent deletions, not moves to trash.
type MessageIterator ¶
type MessageIterator interface {
// Next advances to the next message.
// Returns (true, nil) if there is a message available.
// Returns (false, nil) if iteration is done (no more messages).
// Returns (false, error) if an error occurred (e.g., service disconnected, context cancelled).
// Must be called before accessing Message().
Next(ctx context.Context) (bool, error)
// Message returns the current message with full mutation capabilities.
// Must be called after a successful Next() call that returned (true, nil).
// Returns ErrIteratorOutOfBounds if called before Next() or after iteration ends.
Message() (Message, error)
}
MessageIterator provides streaming access to messages. Use Next() to advance, Message() to get current message.
Ownership: MessageIterator holds no resources requiring cleanup. There is no Close method — simply stop calling Next() when done.
Thread Safety: MessageIterator is NOT safe for concurrent use. Each iterator should be used by a single goroutine. If you need concurrent access, create separate iterators for each goroutine.
type MessageLimits ¶
type MessageLimits struct {
MaxSubjectLength int
MaxBodySize int
MaxAttachmentSize int64
MaxAttachmentCount int
MaxRecipientCount int
MaxMetadataSize int
MaxMetadataKeys int
MaxHeaderCount int
MaxHeaderKeyLength int
MaxHeaderValueLength int
MaxHeadersTotalSize int
}
MessageLimits holds all message validation limits. Used to pass limits to validation functions.
func DefaultLimits ¶
func DefaultLimits() MessageLimits
DefaultLimits returns the default message limits.
type MessageList ¶
type MessageList interface {
MessageListReader
MessageListMutator
}
MessageList provides access to a paginated list of messages with bulk operations.
Composed of:
- MessageListReader: Read-only access (All, Total, HasMore, NextCursor)
- MessageListMutator: Bulk mutations (MarkRead, Move, Delete, AddTag, etc.)
type MessageListMutator ¶
type MessageListMutator interface {
// MarkRead marks all messages in this list as read.
MarkRead(ctx context.Context) (*BulkResult, error)
// MarkUnread marks all messages in this list as unread.
MarkUnread(ctx context.Context) (*BulkResult, error)
// Move moves all messages in this list to the specified folder.
Move(ctx context.Context, folderID string, opts ...store.MoveOption) (*BulkResult, error)
// Delete moves all messages in this list to trash.
Delete(ctx context.Context) (*BulkResult, error)
// AddTag adds a tag to all messages in this list.
AddTag(ctx context.Context, tagID string) (*BulkResult, error)
// RemoveTag removes a tag from all messages in this list.
RemoveTag(ctx context.Context, tagID string) (*BulkResult, error)
}
MessageListMutator provides bulk mutation operations on a list of messages.
type MessageListReader ¶
type MessageListReader interface {
// All returns all messages in this list.
All() []Message
// Total returns the total count of messages matching the query (not just this page).
Total() int64
// HasMore returns true if there are more messages after this page.
HasMore() bool
// NextCursor returns the cursor for fetching the next page.
NextCursor() string
}
MessageListReader provides read-only access to a paginated list of messages.
type MessageLister ¶
type MessageLister interface {
Folder(ctx context.Context, folderID string, opts store.ListOptions) (MessageList, error)
}
MessageLister provides message listing by folder.
type MessageMovedEvent ¶ added in v0.3.0
type MessageMovedEvent struct {
MessageID string `json:"message_id"`
UserID string `json:"user_id"`
FromFolderID string `json:"from_folder_id"`
ToFolderID string `json:"to_folder_id"`
WasUnread bool `json:"was_unread"`
MovedAt time.Time `json:"moved_at"`
}
MessageMovedEvent is published when a message is moved between folders. This event is published for all folder moves including archive, trash, and restore.
type MessageMutator ¶
type MessageMutator interface {
// Update updates the message flags (read, archived status).
// Use MarkRead(), MarkUnread(), MarkArchived(), MarkUnarchived() helpers.
Update(ctx context.Context, flags Flags) error
// Move moves the message to a folder.
// When called with store.FromFolder, the move is conditional.
Move(ctx context.Context, folderID string, opts ...store.MoveOption) error
// Delete moves the message to trash.
Delete(ctx context.Context) error
// Restore restores the message from trash.
Restore(ctx context.Context) error
// PermanentlyDelete permanently deletes the message from trash.
PermanentlyDelete(ctx context.Context) error
// AddTag adds a tag to the message.
AddTag(ctx context.Context, tagID string) error
// RemoveTag removes a tag from the message.
RemoveTag(ctx context.Context, tagID string) error
}
MessageMutator provides mutation operations on a single message.
type MessageReadEvent ¶
type MessageReadEvent struct {
MessageID string `json:"message_id"`
UserID string `json:"user_id"`
FolderID string `json:"folder_id"`
ReadAt time.Time `json:"read_at"`
}
MessageReadEvent is published when a message is marked as read. Use this for read receipts and tracking message engagement.
type MessageReader ¶
MessageReader provides single message retrieval.
type MessageReceivedEvent ¶ added in v0.3.0
type MessageReceivedEvent struct {
MessageID string `json:"message_id"`
RecipientID string `json:"recipient_id"`
SenderID string `json:"sender_id"`
Subject string `json:"subject"`
ReceivedAt time.Time `json:"received_at"`
}
MessageReceivedEvent is published when a message is delivered to a recipient. One event is published per recipient. Use this for notifications and presence updates.
type MessageSearcher ¶
type MessageSearcher interface {
Search(ctx context.Context, query SearchQuery) (MessageList, error)
}
MessageSearcher provides message search capability.
type MessageSender ¶
type MessageSender interface {
SendMessage(ctx context.Context, req SendRequest) (Message, error)
}
MessageSender provides direct message sending without drafts. This is useful for server integrations where the draft composition flow is handled externally (e.g., via gRPC).
type MessageSentEvent ¶
type MessageSentEvent struct {
MessageID string `json:"message_id"`
SenderID string `json:"sender_id"`
RecipientIDs []string `json:"recipient_ids"`
Subject string `json:"subject"`
SentAt time.Time `json:"sent_at"`
}
MessageSentEvent is published when a message is sent. This is the primary event for notifying recipients of new messages.
type MessageStreamer ¶
type MessageStreamer interface {
// Stream returns an iterator for messages matching the given filters.
// The owner filter and not-deleted filter are automatically added.
Stream(ctx context.Context, filters []store.Filter, opts StreamOptions) (MessageIterator, error)
// StreamSearch returns an iterator for search results.
StreamSearch(ctx context.Context, query SearchQuery, opts StreamOptions) (MessageIterator, error)
}
MessageStreamer provides streaming access to messages. Use streaming for memory-efficient processing of large result sets. For paginated UI with bulk operations, use MessageLister instead.
Example - stream inbox messages:
iter, _ := mb.Stream(ctx, []store.Filter{store.InFolder(store.FolderInbox)}, mailbox.StreamOptions{BatchSize: 100})
for iter.Next(ctx) { msg, _ := iter.Message(); ... }
type OperationResult ¶
type OperationResult struct {
// ID is the identifier of the item that was processed.
ID string
// Success indicates whether the operation succeeded.
Success bool
// Error contains the error if the operation failed (nil if successful).
Error error
}
OperationResult contains the result of a single operation within a bulk operation. Results are returned in the same order as the input items.
type Option ¶
type Option func(*options)
Option configures a mailbox.
func WithAttachmentManager ¶
func WithAttachmentManager(m store.AttachmentManager) Option
WithAttachmentManager sets the attachment manager for reference-counted attachments. When provided, attachments are tracked with reference counting and automatically deleted when no messages reference them.
func WithEventBus ¶ added in v0.7.9
WithEventBus sets a pre-created event bus for the service. The caller retains ownership of the bus — the service will NOT close it. This is useful in tests where multiple services share a single bus, or when the bus must be configured with specific options (outbox, transport, etc.) before the service is created.
WithEventBus takes priority over WithEventTransport and WithRedisClient. Outbox wiring is skipped because the caller configured the bus.
Shutdown order: always call svc.Close(ctx) BEFORE bus.Close(ctx). The service may publish events during its own shutdown (notifier teardown, plugin hooks), and a closed bus will cause those publishes to fail.
Example:
bus, _ := event.NewBus("test", event.WithTransport(channel.New()))
svc, _ := mailbox.New(cfg, mailbox.WithStore(store), mailbox.WithEventBus(bus))
// ...
svc.Close(ctx) // close service first
bus.Close(ctx) // then close bus
func WithEventErrorsFatal ¶
WithEventErrorsFatal configures whether event publishing failures should cause the operation to fail. By default, event failures are logged but the operation succeeds (the message is still sent).
Set to true if your application requires guaranteed event delivery, for example when events drive critical downstream processes. Set to false (default) for fire-and-forget event publishing.
func WithEventPublishFailureHandler ¶
func WithEventPublishFailureHandler(fn EventPublishFailureFunc) Option
WithEventPublishFailureHandler sets a callback for event publishing failures. This callback is invoked whenever an event fails to publish (and eventErrorsFatal is false). Use this for custom logging, metrics, or alerting on event failures.
By default, failures are logged using the configured logger.
func WithEventTransport ¶
WithEventTransport sets the event transport for publishing and subscribing. When provided, events are published via the given transport for reliable delivery. If not provided, a noop transport is used (events are silently dropped).
Example with Redis:
transport, _ := redis.New(redisClient)
svc, _ := mailbox.New(mailbox.Config{}, mailbox.WithEventTransport(transport))
func WithGlobalQuota ¶ added in v0.6.3
func WithGlobalQuota(policy QuotaPolicy) Option
WithGlobalQuota sets a uniform quota policy for all users. This is a convenience wrapper around WithQuotaProvider using a StaticQuotaProvider.
func WithMeterProvider ¶
func WithMeterProvider(mp metric.MeterProvider) Option
WithMeterProvider sets a custom OpenTelemetry meter provider. Default uses the global meter provider from otel.GetMeterProvider().
func WithMetrics ¶
WithMetrics enables or disables OpenTelemetry metrics. When enabled, metrics are collected for all mailbox operations. Default is disabled.
func WithNotificationCoalescing ¶ added in v0.6.5
WithNotificationCoalescing enables event coalescing for notification handlers. When enabled, multiple events for the same message ID within the coalescing window are collapsed — only the latest event is delivered to the notification stream. This reduces SSE noise for rapidly-updated messages (e.g., a message that is sent, then immediately moved or read).
Requires message_id metadata on published events (automatically added). Default is disabled.
func WithNotifier ¶ added in v0.6.1
WithNotifier sets the per-user notification system. When provided, the service subscribes to mailbox events using AsWorker (worker model) and pushes notifications to connected users. Use svc.Notifications(ctx, userID, lastEventID) to open a notification stream.
func WithOTel ¶
WithOTel enables both OpenTelemetry tracing and metrics. This is a convenience function equivalent to calling WithTracing(true) and WithMetrics(true).
func WithPlugin ¶
WithPlugin registers a plugin with the mailbox service. Plugins can hook into message lifecycle events. Multiple plugins can be registered by calling this option multiple times.
func WithPlugins ¶
WithPlugins registers multiple plugins at once.
func WithQuotaProvider ¶ added in v0.6.3
func WithQuotaProvider(p QuotaProvider) Option
WithQuotaProvider sets a custom quota provider for per-user message limits. The provider is consulted during message delivery to check recipient quotas. When nil (default), quotas are disabled and delivery is unrestricted.
func WithRedisClient ¶
func WithRedisClient(client redis.UniversalClient) Option
WithRedisClient sets a Redis client for the event transport. When provided, events are published to Redis Streams for reliable delivery. If not provided, a noop transport is used (events are silently dropped).
Compatible with *redis.Client, *redis.ClusterClient, and redis.UniversalClient.
func WithRegistrar ¶ added in v0.7.1
WithRegistrar sets a registrar that the service uses during Connect to announce this mailbox instance to a shared registry. The registrar returns the mailbox ID assigned to this instance; if Register returns an error, Connect fails. The assigned ID is available via Service.MailboxID().
func WithServiceName ¶
WithServiceName sets the service name for OpenTelemetry telemetry. Default is "mailbox".
func WithStatsCache ¶ added in v0.7.6
func WithStatsCache(c StatsCache) Option
WithStatsCache sets a distributed cache for aggregate mailbox stats. When set, stats reads check this cache (L2) before the primary store, and event-driven incremental updates propagate to it via HINCRBY. This reduces database load during cold starts and across many instances.
func WithTracerProvider ¶
func WithTracerProvider(tp trace.TracerProvider) Option
WithTracerProvider sets a custom OpenTelemetry tracer provider. Default uses the global tracer provider from otel.GetTracerProvider().
func WithTracing ¶
WithTracing enables or disables OpenTelemetry tracing. When enabled, spans are created for all mailbox operations. Default is disabled.
func WithUserResolver ¶ added in v0.7.1
func WithUserResolver(r UserResolver) Option
WithUserResolver sets an optional user resolver for sender identity enrichment. When configured, the service resolves the sender's identity during message delivery and populates metadata keys (sender.firstname, sender.lastname, sender.email). If resolution fails, the send operation is aborted with ErrUserResolveFailed.
type PartialDeliveryError ¶
type PartialDeliveryError struct {
// MessageID is the sender's message ID.
MessageID string
// DeliveredTo contains recipient IDs that received the message.
DeliveredTo []string
// FailedRecipients maps recipient IDs to their delivery errors.
FailedRecipients map[string]error
}
PartialDeliveryError provides details about which recipients failed. Use the helper methods to determine retry strategy.
func IsPartialDelivery ¶
func IsPartialDelivery(err error) (*PartialDeliveryError, bool)
IsPartialDelivery checks if the error is a partial delivery error and returns details.
func (*PartialDeliveryError) AllFailed ¶
func (e *PartialDeliveryError) AllFailed() bool
AllFailed returns true if no recipients received the message.
func (*PartialDeliveryError) Error ¶
func (e *PartialDeliveryError) Error() string
func (*PartialDeliveryError) HasRetryableFailures ¶
func (e *PartialDeliveryError) HasRetryableFailures() bool
HasRetryableFailures returns true if at least one failure can be retried.
func (*PartialDeliveryError) PermanentFailures ¶
func (e *PartialDeliveryError) PermanentFailures() []string
PermanentFailures returns the list of recipient IDs with permanent failures. These recipients should not be retried as the error is deterministic.
func (*PartialDeliveryError) RetryableRecipients ¶
func (e *PartialDeliveryError) RetryableRecipients() []string
RetryableRecipients returns the list of recipient IDs whose delivery can be retried. An error is considered retryable if it's a temporary/transient failure (e.g., timeout, connection error) rather than a permanent failure (e.g., recipient not found, unauthorized).
func (*PartialDeliveryError) SuccessRate ¶
func (e *PartialDeliveryError) SuccessRate() float64
SuccessRate returns the fraction of recipients that received the message (0.0 to 1.0).
func (*PartialDeliveryError) Unwrap ¶
func (e *PartialDeliveryError) Unwrap() error
type Plugin ¶
type Plugin interface {
// Name returns the plugin identifier.
Name() string
// Init initializes the plugin. Called when service connects.
Init(ctx context.Context) error
// Close cleans up plugin resources. Called when service closes.
Close(ctx context.Context) error
}
Plugin defines the interface for mailbox extensions. Plugins can hook into message sending to add custom behavior such as spam filtering, rate limiting, or content validation.
For observing other operations (read, delete, archive, etc.), use the event system instead (EventMessageSent, EventMessageRead, etc.).
type PluginError ¶
PluginError represents an error from a plugin.
func (*PluginError) Error ¶
func (e *PluginError) Error() string
func (*PluginError) Unwrap ¶
func (e *PluginError) Unwrap() error
type QuotaAction ¶ added in v0.6.3
type QuotaAction int
QuotaAction defines what happens when a user exceeds their quota.
const ( // QuotaActionReject rejects new incoming messages for over-quota users. // Delivery to the over-quota recipient fails with ErrQuotaExceeded, // while other recipients receive the message normally (partial delivery). // // Note: The check is best-effort due to the "no distributed locks" principle. // Concurrent deliveries may both pass the check before either creates a message, // resulting in a small overshoot. The background EnforceQuotas method handles drift. QuotaActionReject QuotaAction = iota // QuotaActionDeleteOldest accepts all messages and trims old ones in the background. // Messages older than QuotaPolicy.DeleteOlderThan are deleted when EnforceQuotas is called. // Delivery is never blocked; enforcement happens via the caller-scheduled EnforceQuotas method. QuotaActionDeleteOldest )
func (QuotaAction) String ¶ added in v0.6.3
func (a QuotaAction) String() string
String returns a human-readable name for the quota action.
type QuotaExceededError ¶ added in v0.6.3
QuotaExceededError provides details about a quota violation.
func (*QuotaExceededError) Error ¶ added in v0.6.3
func (e *QuotaExceededError) Error() string
func (*QuotaExceededError) Unwrap ¶ added in v0.6.3
func (e *QuotaExceededError) Unwrap() error
type QuotaPolicy ¶ added in v0.6.3
type QuotaPolicy struct {
// MaxMessages is the maximum number of messages allowed in a user's mailbox.
// Set to 0 for unlimited (no quota enforcement).
MaxMessages int64
// ExceedAction determines what happens when the quota is exceeded.
// Default is QuotaActionReject.
ExceedAction QuotaAction
// DeleteOlderThan is the minimum age of messages eligible for deletion
// when ExceedAction is QuotaActionDeleteOldest. Messages younger than
// this duration are never deleted by quota enforcement.
// Must be positive when ExceedAction is QuotaActionDeleteOldest;
// enforcement is skipped if zero to prevent accidental deletion of recent messages.
DeleteOlderThan time.Duration
}
QuotaPolicy defines the quota limits and enforcement behavior for a user.
type QuotaProvider ¶ added in v0.6.3
type QuotaProvider interface {
// GetQuota returns the quota policy for the given user.
// Return a nil policy or a policy with MaxMessages=0 for unlimited quota.
GetQuota(ctx context.Context, userID string) (*QuotaPolicy, error)
}
QuotaProvider resolves quota policies for users. Implementations can look up per-user quotas from any source (database, config, etc.).
type QuotaUserLister ¶ added in v0.7.1
type QuotaUserLister interface {
// ListUsers returns user IDs that should be checked for quota enforcement.
ListUsers(ctx context.Context) ([]string, error)
}
QuotaUserLister provides the list of user IDs for background quota enforcement. Implementations should be safe for concurrent use.
type Recipient ¶ added in v0.3.0
type Recipient struct {
// UserID is the unique user identifier.
UserID string
// Name is the display name of the user.
Name string
// Email is the user's email address (optional).
Email string
}
Recipient contains resolved information about a user.
type RecipientResolver ¶ added in v0.3.0
type RecipientResolver interface {
// Resolve returns recipient information for a single user ID.
// Returns ErrRecipientNotFound if the user ID is unknown.
Resolve(ctx context.Context, userID string) (*Recipient, error)
// ResolveBatch returns recipient information for multiple user IDs.
// Returns results in the same order as input. Unknown IDs have nil entries.
ResolveBatch(ctx context.Context, userIDs []string) ([]*Recipient, error)
}
RecipientResolver maps user IDs to recipient information. Implementations should be safe for concurrent use.
Example use cases:
- Populate "From" display names in inbox views
- Resolve email addresses for notification delivery
- Validate that recipient IDs are valid users
type Registrar ¶ added in v0.9.0
type Registrar interface {
// Register announces this mailbox instance and returns the mailbox ID
// assigned by the registrar. Returning an error aborts Connect.
Register(ctx context.Context) (mailboxID string, err error)
}
Registrar registers a mailbox instance in a shared registry so that Routers can discover it. A mailbox calls Register during Connect; if Register returns an error, Connect fails. The returned mailbox ID is assigned by the registrar and stored on the service for its lifecycle.
Implementations must be safe for concurrent use.
type Router ¶ added in v0.9.0
type Router interface {
// Route returns the Mailbox that holds messages for the given user.
// Returns an error if the user is unknown or the lookup fails.
Route(ctx context.Context, userID string) (Mailbox, error)
}
Router resolves a user ID to the Mailbox that owns their messages.
It decouples "where are this user's messages stored" from user identity, so user and mailbox registries can evolve and scale independently. Implementations might back this with a consistent-hash ring, a database shard table, or a single-node no-op that returns the local service's client.
Implementations must be safe for concurrent use.
type SearchQuery ¶
type SearchQuery = store.SearchQuery
Type aliases for commonly used store types. These allow users to work with the mailbox package without importing store directly.
type SendHook ¶
type SendHook interface {
Plugin
// BeforeSend is called before a draft is sent. Return an error to abort.
// Use this for spam filtering, rate limiting, or content validation.
BeforeSend(ctx context.Context, userID string, draft store.DraftMessage) error
// AfterSend is called after a message is successfully sent.
// Return an error to signal post-send failures (e.g., notification errors).
// Note: The message is already sent and cannot be rolled back.
AfterSend(ctx context.Context, userID string, msg store.Message) error
}
SendHook is called before/after sending messages. This is the primary extension point for message validation and filtering.
type SendRequest ¶
type SendRequest struct {
RecipientIDs []string
Subject string
Body string
Headers map[string]string
Metadata map[string]any
Attachments []store.Attachment
AttachmentIDs []string
ThreadID string
ReplyToID string
ExternalID string // caller-defined external identifier (e.g. SMTP Message-ID); stored indexed
// DeliverTo optionally restricts which recipients receive inbox copies
// on this instance. The message's RecipientIDs stores the full recipient
// list for display. When empty, delivery targets default to RecipientIDs.
DeliverTo []string
// TTL is an optional message time-to-live. When set, the message will be
// eligible for automatic deletion after this duration from send time.
// Zero means no per-message TTL (the service default TTL may still apply).
TTL time.Duration
// ScheduleAt is an optional delivery schedule. When set, the message is
// hidden from recipient queries until this UTC time.
// Nil means the message is immediately available.
ScheduleAt *time.Time
}
SendRequest contains the data needed to send a message directly, without going through the draft composition flow.
type Service ¶
type Service interface {
// IsConnected returns true if the service is connected and ready.
IsConnected() bool
// Connect establishes connections to storage backends and starts
// background maintenance goroutines (if configured via Config).
Connect(ctx context.Context) error
// Close stops background goroutines, waits for in-flight operations,
// and closes all connections.
Close(ctx context.Context) error
// Client returns a mailbox client for the given user.
// The returned client shares the service's connections.
// Connection state is checked lazily on each operation; if the service
// is not connected, operations will return ErrNotConnected.
Client(userID string) Mailbox
// CleanupTrash permanently deletes messages that have been in trash
// longer than the configured retention period. Called automatically
// when Config.TrashCleanupInterval is set; can also be called manually.
CleanupTrash(ctx context.Context) (*CleanupTrashResult, error)
// CleanupExpiredMessages permanently deletes messages older than the
// configured message retention period (based on created_at).
// Returns a zero result if message retention is not configured.
// Called automatically when Config.ExpiredMessageCleanupInterval is set.
CleanupExpiredMessages(ctx context.Context) (*CleanupExpiredMessagesResult, error)
// EnforceQuotas evaluates quotas for the given users and applies enforcement
// actions. Only users with QuotaActionDeleteOldest policies are processed;
// QuotaActionReject is enforced at delivery time. Called automatically when
// Config.QuotaEnforcementInterval and Config.QuotaUserLister are set.
EnforceQuotas(ctx context.Context, userIDs []string) (*EnforceQuotasResult, error)
// RunQuotaEnforcement lists all users via the configured QuotaUserLister and
// calls EnforceQuotas for them. Returns ErrQuotaUserListerNotConfigured when no
// lister is set. Prefer this over EnforceQuotas in admin/on-demand contexts
// because it does not accept user IDs from external callers.
RunQuotaEnforcement(ctx context.Context) (*EnforceQuotasResult, error)
// Events returns per-service event instances for subscribing and publishing.
// Each service has its own events bound to its own event bus, enabling
// independent event routing and parallel testing.
Events() *ServiceEvents
// Notifications returns a real-time notification stream for the given user.
// Requires WithNotifier to be configured. Returns ErrNotifierNotConfigured otherwise.
// lastEventID enables backfill of missed events ("" for new events only).
// The caller must close the returned Stream when done.
Notifications(ctx context.Context, userID string, lastEventID string) (notify.Stream, error)
// MailboxID returns the mailbox ID assigned by the Registrar during Connect.
// Returns an empty string when no registrar was configured or Connect has not
// completed successfully.
MailboxID() string
// ThreadParticipants returns the distinct user IDs of all non-deleted participants
// in the given thread. A participant is any user who has a non-trash copy of a
// message with that thread_id (sender or recipient).
//
// This is a cross-owner query used by external delivery systems (e.g., SMTP gateways)
// that know the thread_id but not the individual recipient IDs:
//
// participants, err := svc.ThreadParticipants(ctx, threadID)
// if err != nil { ... }
// _, err = svc.Client(senderID).SendMessage(ctx, mailbox.SendRequest{
// RecipientIDs: participants,
// ThreadID: threadID,
// Subject: subject,
// Body: body,
// })
//
// Returns store.ErrNotFound when no messages exist for the given thread_id.
ThreadParticipants(ctx context.Context, threadID string) ([]string, error)
}
Service manages the mailbox system (server-side). It handles connections to storage and creates mailbox clients.
func New ¶ added in v0.7.1
New creates a new mailbox service with the given configuration and options. Call Connect() to establish connections to backends and start background tasks.
When Config specifies non-zero cleanup intervals, background goroutines are started during Connect() and stopped during Close(). Close() blocks until all background goroutines have finished.
Caching is NOT included in this library. If you need caching, wrap your store with a caching decorator (see store/cached package for an example).
Example ¶
ExampleNew demonstrates creating and connecting a mailbox service.
package main
import (
"context"
"fmt"
"log"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
svc, err := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
// Production: use mailbox.WithRedisClient(redisClient) for events
// Production: use mongostore.New(mongoClient) for storage
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := svc.Connect(ctx); err != nil {
log.Fatal(err)
}
defer svc.Close(ctx)
fmt.Println("connected:", svc.IsConnected())
}
Output: connected: true
Example (WithConfig) ¶
ExampleNew_withConfig demonstrates all major configuration options.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
svc, err := mailbox.New(mailbox.Config{
// Message limits
MaxBodySize: 5 * 1024 * 1024, // 5 MB
MaxSubjectLength: 200, // 200 chars
MaxRecipientCount: 50, // 50 recipients
MaxAttachmentCount: 10, // 10 attachments
MaxAttachmentSize: 50 * 1024 * 1024, // 50 MB per attachment
// Query limits
MaxQueryLimit: 200,
DefaultQueryLimit: 25,
// Trash and retention
TrashRetention: 7 * 24 * time.Hour, // 7 days
MessageRetention: 90 * 24 * time.Hour, // 90 days
// Concurrency
MaxConcurrentSends: 20,
ShutdownTimeout: 60 * time.Second,
},
mailbox.WithStore(memory.New()),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if err := svc.Connect(ctx); err != nil {
log.Fatal(err)
}
defer svc.Close(ctx)
fmt.Println("connected:", svc.IsConnected())
}
Output: connected: true
type ServiceEvents ¶
type ServiceEvents struct {
// MessageSent is published when a message is sent.
MessageSent event.Event[MessageSentEvent]
// MessageReceived is published when a message is delivered to a recipient.
MessageReceived event.Event[MessageReceivedEvent]
// MessageRead is published when a message is marked as read.
MessageRead event.Event[MessageReadEvent]
// MessageDeleted is published when a message is permanently deleted.
MessageDeleted event.Event[MessageDeletedEvent]
// MessageMoved is published when a message is moved between folders.
MessageMoved event.Event[MessageMovedEvent]
// MarkAllRead is published when all messages in a folder are marked as read.
MarkAllRead event.Event[MarkAllReadEvent]
}
ServiceEvents provides access to per-service event instances. Each service creates its own events bound to its own event bus, enabling independent event routing and parallel testing.
Subscribe to events:
svc.Events().MessageSent.Subscribe(ctx, handler) svc.Events().MessageRead.Subscribe(ctx, handler) svc.Events().MessageDeleted.Subscribe(ctx, handler)
type SortOrder ¶
Type aliases for commonly used store types. These allow users to work with the mailbox package without importing store directly.
type StaticQuotaProvider ¶ added in v0.6.3
type StaticQuotaProvider struct {
Policy QuotaPolicy
}
StaticQuotaProvider returns the same quota policy for all users. Use this for global quotas that apply uniformly.
func (*StaticQuotaProvider) GetQuota ¶ added in v0.6.3
func (p *StaticQuotaProvider) GetQuota(_ context.Context, _ string) (*QuotaPolicy, error)
GetQuota returns the static policy for any user.
type StatsCache ¶ added in v0.7.6
type StatsCache interface {
// Get retrieves cached stats for a user. Returns nil, nil when not found.
Get(ctx context.Context, ownerID string) (*store.MailboxStats, error)
// Set stores stats for a user with the given TTL.
Set(ctx context.Context, ownerID string, stats *store.MailboxStats, ttl time.Duration) error
// IncrBy atomically increments a named counter for a user.
// field is one of the well-known keys defined in the statscache package.
IncrBy(ctx context.Context, ownerID string, field string, delta int64) error
// Invalidate removes cached stats for a user, forcing the next read
// to fall through to the store.
Invalidate(ctx context.Context, ownerID string) error
}
StatsCache is an optional distributed cache for mailbox stats. When configured via WithStatsCache, it acts as an L2 cache behind the in-process sync.Map (L1), enabling consistent stats across instances. Implementations must be safe for concurrent use.
type StatsReader ¶ added in v0.3.0
type StatsReader interface {
// Stats returns aggregate statistics for this user's mailbox.
// Results are cached with event-driven incremental updates and periodic TTL refresh.
Stats(ctx context.Context) (*store.MailboxStats, error)
// UnreadCount returns the total unread message count for this user.
// This is a convenience method equivalent to calling Stats() and reading UnreadCount.
UnreadCount(ctx context.Context) (int64, error)
}
StatsReader provides access to aggregate mailbox statistics.
type StorageClient ¶
type StorageClient interface {
FolderReader
AttachmentLoader
}
StorageClient provides folder and attachment operations. Use this interface when you only need storage metadata access.
type StreamOptions ¶
type StreamOptions struct {
// BatchSize is the number of messages fetched per batch.
// Larger batches reduce round-trips but use more memory.
// Default: 100
BatchSize int
}
StreamOptions configures streaming behavior.
type ThreadReader ¶
type ThreadReader interface {
// GetThread returns all messages in a thread, ordered by creation time.
GetThread(ctx context.Context, threadID string, opts store.ListOptions) (MessageList, error)
// GetReplies returns all direct replies to a message.
GetReplies(ctx context.Context, messageID string, opts store.ListOptions) (MessageList, error)
}
ThreadReader provides access to message threads.
type User ¶ added in v0.7.1
type User interface {
// ID returns the unique identifier for this principal (typically their email address).
ID() string
FirstName() string
LastName() string
Email() string
// Type classifies the principal: "human", "agent", "service", or "" (unset).
Type() string
// PublicKey returns the principal's Ed25519 public key, base64-encoded.
// Empty for human users; set for agents and services that sign their payloads.
PublicKey() string
// Capabilities returns routing and capability metadata: skills, endpoint, region, model, etc.
// Callers must not mutate the returned map.
Capabilities() map[string]string
}
User is the full principal — identity, type, capabilities, and signing key. Implementations should be safe for concurrent use.
type UserResolver ¶ added in v0.7.1
UserResolver resolves user IDs to the full User principal. When configured via WithUserResolver, the service calls ResolveUser during message delivery to populate sender metadata. If resolution fails, the send operation is aborted.
Implementations should be safe for concurrent use.
type ValidationError ¶
type ValidationError struct {
Field string // The field that failed validation
Message string // Human-readable error message
}
ValidationError provides details about a validation failure.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
Source Files
¶
- attachment.go
- background.go
- bulk.go
- cleanup.go
- config.go
- doc.go
- draft.go
- errors.go
- events.go
- flags.go
- interfaces.go
- iterator.go
- mailbox.go
- message.go
- mutate.go
- notifications.go
- option.go
- otel.go
- outbox_helpers.go
- plugin.go
- query.go
- quota.go
- quota_enforce.go
- recipient.go
- router.go
- send.go
- stats.go
- stats_cache.go
- thread.go
- user.go
- validation.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package admin provides an HTTP management surface for mailbox service operations.
|
Package admin provides an HTTP management surface for mailbox service operations. |
|
Package compress provides message body compression as a mailbox plugin.
|
Package compress provides message body compression as a mailbox plugin. |
|
Package content provides a content-type codec layer for mailbox messages.
|
Package content provides a content-type codec layer for mailbox messages. |
|
Package crypto provides optional E2E encryption for mailbox messages.
|
Package crypto provides optional E2E encryption for mailbox messages. |
|
Package mailboxtest provides test helpers for mailbox integration tests.
|
Package mailboxtest provides test helpers for mailbox integration tests. |
|
Package notify provides per-user real-time notification streams.
|
Package notify provides per-user real-time notification streams. |
|
memory
Package memory provides an in-memory notify.Store for testing.
|
Package memory provides an in-memory notify.Store for testing. |
|
redis
Package redis provides a Redis Streams-backed notify.Store and notify.StreamStore.
|
Package redis provides a Redis Streams-backed notify.Store and notify.StreamStore. |
|
webhook
Package webhook provides a notify.Router that delivers notification events to configured HTTP endpoints via signed JSON POST requests.
|
Package webhook provides a notify.Router that delivers notification events to configured HTTP endpoints via signed JSON POST requests. |
|
Package presence provides user online/offline tracking with optional routing.
|
Package presence provides user online/offline tracking with optional routing. |
|
memory
Package memory provides an in-memory presence.Tracker for testing.
|
Package memory provides an in-memory presence.Tracker for testing. |
|
redis
Package redis provides a Redis Hash-backed presence.Tracker.
|
Package redis provides a Redis Hash-backed presence.Tracker. |
|
proto
|
|
|
Package resolver provides RecipientResolver and UserResolver implementations.
|
Package resolver provides RecipientResolver and UserResolver implementations. |
|
Package retry provides exponential backoff retry logic for transient failures.
|
Package retry provides exponential backoff retry logic for transient failures. |
|
Package router held the Router and Registrar interfaces.
|
Package router held the Router and Registrar interfaces. |
|
Package rules provides a CEL-based message rules engine for the mailbox library.
|
Package rules provides a CEL-based message rules engine for the mailbox library. |
|
Package search provides a pluggable full-text search layer for the mailbox library.
|
Package search provides a pluggable full-text search layer for the mailbox library. |
|
elasticsearch
Package elasticsearch provides an Elasticsearch-backed search.Provider.
|
Package elasticsearch provides an Elasticsearch-backed search.Provider. |
|
meilisearch
Package meilisearch provides a Meilisearch-backed search.Provider.
|
Package meilisearch provides a Meilisearch-backed search.Provider. |
|
Package server provides a gRPC service implementation for the mailbox library.
|
Package server provides a gRPC service implementation for the mailbox library. |
|
statscache
|
|
|
redis
Package redis provides a Redis-backed distributed stats cache for mailbox.
|
Package redis provides a Redis-backed distributed stats cache for mailbox. |
|
Package store provides interfaces and types for mailbox storage.
|
Package store provides interfaces and types for mailbox storage. |
|
attachment/azblob
Package azblob provides an Azure Blob Storage-based attachment file store.
|
Package azblob provides an Azure Blob Storage-based attachment file store. |
|
attachment/cached
Package cached provides a file-based caching wrapper for attachment stores.
|
Package cached provides a file-based caching wrapper for attachment stores. |
|
attachment/gcs
Package gcs provides a Google Cloud Storage-based attachment file store.
|
Package gcs provides a Google Cloud Storage-based attachment file store. |
|
attachment/local
Package local provides a local filesystem-based attachment file store.
|
Package local provides a local filesystem-based attachment file store. |
|
attachment/otel
Package otel provides OpenTelemetry instrumentation for attachment stores.
|
Package otel provides OpenTelemetry instrumentation for attachment stores. |
|
attachment/s3
Package s3 provides an S3-based attachment file store.
|
Package s3 provides an S3-based attachment file store. |
|
memory
Package memory provides an in-memory Store implementation for testing.
|
Package memory provides an in-memory Store implementation for testing. |
|
mongo
Package mongo provides a MongoDB implementation of store.Store.
|
Package mongo provides a MongoDB implementation of store.Store. |
|
postgres
Package postgres provides a PostgreSQL implementation of store.Store.
|
Package postgres provides a PostgreSQL implementation of store.Store. |
|
storetest
Package storetest provides a reusable, backend-agnostic conformance suite for store.Store implementations.
|
Package storetest provides a reusable, backend-agnostic conformance suite for store.Store implementations. |