Skip to content

Messages & retrieval

This page covers the write side (SendMessage, SendMessages) and the read side (RetrieveMessages, RetrievePage) of messaging. Acknowledging and deleting are on Flags, ack & expunge.

res, err := cl.SendMessage(ctx, recipientID, []byte("hello"))
if err != nil {
return err // the request failed
}
if err := res.Err(); err != nil {
return err // the server refused the message
}
fmt.Println(res.MessageID, res.StoredAtServer)

SendMessage returns (*client.SendResult, error). The error is for a request that did not complete: no server reachable, a timeout, a broken stream. A message the server refuses is a normal outcome and comes back in the result with Success == false, Status, ErrorMessage and RetryAfter. res.Err() turns it into a typed error you can test with errors.Is:

switch err := res.Err(); {
case errors.Is(err, client.ErrMailboxFull):
// only the recipient can fix this; do not retry on a timer
case client.IsRetryable(err):
wait, _ := client.RetryAfter(err)
time.Sleep(wait)
}

The client generates the message ID (a UUID) before sending.

Option Default Effect
WithFolderPath(path) inbox The recipient’s mailbox folder the message goes to.
WithPriority(p) client.PriorityNormal PriorityLow, PriorityNormal, PriorityHigh or PriorityUrgent (aliases of the wire constants). Readers can filter on it.
WithExpiry(d) 7 days Expiry measured from now. The server clamps it to the mailbox’s retention. Expired messages are no longer returned and are swept.
WithPersistent(true) false Keeps the message after MarkDelivered, marked \Seen, until deleted or retained out.
WithCompression(threshold...) off LZ4-compresses payloads of at least threshold bytes (default client.DefaultCompressionThreshold, 1024).
WithEncryption() off Seals the payload with NaCl box for the recipient. See Encryption.
res, err = cl.SendMessage(ctx, recipientID, payload,
client.WithFolderPath("orders"),
client.WithPriority(wire.PriorityUrgent),
client.WithExpiry(24*time.Hour),
client.WithCompression(),
client.WithEncryption(),
)

Folder paths are validated by the server: at most 256 bytes, no control characters, no leading, trailing or doubled /.

A message to a folder that does not exist creates it as a private mailbox. To control a folder’s type and who may write to it, the recipient creates it first; see Mailboxes & ACLs.

SendMessages submits up to client.MaxBatchMessages (100) messages in one request, which costs one request against the server’s rate limit instead of one per message. Each message takes its own options, and the results come back in the same order:

results, err := cl.SendMessages(ctx, []client.BatchMessage{
{Recipient: alice, Payload: []byte("1")},
{Recipient: bob, Payload: []byte("2"), Options: []client.SendOption{client.WithEncryption()}},
})
if err != nil {
return err // the whole request failed or was refused
}
for i, r := range results {
if err := r.Err(); err != nil {
log.Printf("message %d: %v", i, err) // e.g. one recipient's mailbox is full
}
}

A batch over 100 messages is rejected by the client before anything is sent. Chunk it yourself.

msgs, err := cl.RetrieveMessages(ctx) // your own inbox
for _, m := range msgs {
fmt.Println(m.SequenceNumber, m.SenderPeerID, string(m.Payload))
}

RetrieveMessages returns []*wire.Message. Encrypted payloads are decrypted and compressed ones decompressed before you see them, and the FlagEncrypted and FlagCompressed bits are cleared. A message that fails to decrypt fails the whole call.

Retrieval is not consumption. The same messages come back until you acknowledge them, delete them, or they expire.

Option Effect
WithRetrieveFolderPath(path) Folder to read. Default inbox.
WithTargetPeer(id) Read another peer’s mailbox. Works for shared mailboxes you have read access to and for public mailboxes.
WithFromSequence(n) Start at sequence number n.
WithMaxMessages(n) Page size. Server default 100, maximum 1000.
WithMinPriority(p) Only messages at or above priority p.
msgs, err = cl.RetrieveMessages(ctx,
client.WithRetrieveFolderPath("orders"),
client.WithMaxMessages(50),
client.WithMinPriority(wire.PriorityHigh),
)

Reading a folder you own that does not exist returns an empty list. Reading someone else’s folder that does not exist is client.ErrNotFound; reading one you may not see is client.ErrForbidden.

A page holds at most the requested count, the server’s cap, and what fits in one 10 MB frame. RetrievePage returns the same messages plus HasMore, which says another page exists:

page, err := cl.RetrievePage(ctx, client.WithMaxMessages(50))
for err == nil {
handle(page.Messages)
if !page.HasMore {
break
}
last := page.Messages[len(page.Messages)-1]
page, err = cl.RetrievePage(ctx, client.WithMaxMessages(50),
client.WithFromSequence(last.SequenceNumber+1))
}

Reader cursors on shared and public mailboxes

Section titled “Reader cursors on shared and public mailboxes”

Shared and public mailboxes keep a cursor per reader. A retrieve without WithFromSequence starts after the last message that reader retrieved, so a second call returns only newer messages. To read from the start again, pass WithFromSequence(1). Private mailboxes keep no cursor: a retrieve without WithFromSequence returns every message still stored.

ForwardMessage(ctx, msg) submits a *wire.Message unchanged on its original sender’s behalf, as a gateway would. Servers accept it only from peers in their trusted list with forwarding enabled; otherwise the result carries a 403. See Trusted peers.