Skip to content

Flags, ack & expunge

This page covers what happens to a message after you retrieve it: acknowledging it, flagging it, and removing it. Retrieval itself never changes anything on the server.

All four calls act only on messages in mailboxes you own. An ID that is not yours, or does not exist, is silently skipped, so a message ID cannot be used to touch another peer’s mail. A reader of someone else’s shared or public mailbox cannot acknowledge or delete its messages.

ack, err := cl.MarkDelivered(ctx, []string{msg.MessageID})
if err != nil {
return err
}
fmt.Println(ack.UpdatedCount) // how many of the IDs were yours and were acted on

What MarkDelivered does depends on how the message was sent:

Sent with After MarkDelivered
default (non-persistent) Deleted from the server.
WithPersistent(true) Kept, with the \Seen flag set. It stays until you delete it, the mailbox’s retention removes it, or it expires.

This is the call that drains a mailbox. A client that retrieves and never acknowledges leaves every message on the server until the mailbox reaches its cap (1000 messages by default) and refuses all new mail with client.ErrMailboxFull. Acknowledge once you have durably handled a message, not before.

The result is a *wire.MarkDeliveredAck with Success, UpdatedCount and ErrorMessage.

Each stored message carries IMAP-style flags in wire.Message.MsgFlags (type wire.MessageFlags):

Constant Meaning
wire.MsgFlagSeen Read. Set by MarkDelivered on persistent messages.
wire.MsgFlagFlagged Marked for attention.
wire.MsgFlagDeleted Marked for removal by Expunge.
wire.MsgFlagDraft Draft.

MessageFlags has IsSeen, IsFlagged, IsDeleted, IsDraft, HasFlag, WithFlag and WithoutFlag.

These are separate from the protocol flags in wire.Message.Flags (wire.SFMessageFlags: FlagEncrypted, FlagCompressed, FlagForwarded, …), which describe the payload and routing.

UpdateFlags adds and removes flags on one message. Both arguments are uint32 bitmasks:

// Flag a message
flagAck, err := cl.UpdateFlags(ctx, msg.MessageID, uint32(wire.MsgFlagFlagged), 0)
// Mark it read and clear the flag in one call
flagAck, err = cl.UpdateFlags(ctx, msg.MessageID,
uint32(wire.MsgFlagSeen), uint32(wire.MsgFlagFlagged))
switch {
case err != nil:
return err
case !flagAck.Success:
log.Println(flagAck.ErrorMessage) // "message not found": not yours, or gone
default:
fmt.Println(wire.MessageFlags(*flagAck.NewFlags)) // e.g. "seen"
}

A message you do not own reports Success == false with message not found in the result, not as an error, so a guessed ID does not reveal that the message exists.

Expunge permanently removes every message flagged \Deleted. This is the two-step delete IMAP clients use: flag first, remove later.

_, err = cl.UpdateFlags(ctx, msg.MessageID, uint32(wire.MsgFlagDeleted), 0)
// Remove \Deleted messages in all your mailboxes
expungeAck, err := cl.Expunge(ctx)
// or only in one folder
expungeAck, err = cl.Expunge(ctx, client.WithExpungeFolderPath("orders"))
fmt.Println(expungeAck.DeletedCount)

DeleteMessages removes messages by ID immediately, persistent or not, without the flag step:

deleteAck, err := cl.DeleteMessages(ctx, []string{msg.MessageID})
fmt.Println(deleteAck.DeletedCount) // what was removed, not what was asked
Goal Call
Handled a message, done with it MarkDelivered
Keep a message as a record, but mark it read Send it with WithPersistent(true), then MarkDelivered
Let the user delete and undo before it is final UpdateFlags with MsgFlagDeleted, then Expunge
Remove now DeleteMessages

Mailbox-wide limits and retention are on Mailboxes & ACLs.