Skip to main content
The Mail IMAP MCP Server uses a stable, opaque message identifier format to uniquely identify messages across IMAP operations. This message_id is designed to remain consistent as long as the mailbox’s UIDVALIDITY doesn’t change.

Format

Message IDs follow a structured format that encodes all necessary information to locate a message:
imap:{account_id}:{mailbox}:{uidvalidity}:{uid}

Components

ComponentDescriptionExample
imapFixed prefix identifying the formatimap
account_idAccount identifier from configurationdefault, work, personal
mailboxIMAP mailbox/folder nameINBOX, Archive, Projects:2026
uidvalidityIMAP UIDVALIDITY value (session identifier)12345
uidIMAP UID (message identifier)42

Examples

imap:default:INBOX:12345:42
→ Account "default", INBOX, UIDVALIDITY 12345, UID 42

imap:work:Sent:67890:999
→ Account "work", Sent mailbox, UIDVALIDITY 67890, UID 999

imap:personal:Projects:2026:Q1:999:7
→ Account "personal", mailbox "Projects:2026:Q1", UIDVALIDITY 999, UID 7

Handling Mailbox Names with Colons

Mailbox names containing colons are fully preserved in the format. The parsing logic is designed to handle such cases correctly. For a mailbox named Projects:2026:Q1:
  • The message_id would be: imap:personal:Projects:2026:Q1:999:7
  • Parsing correctly extracts:
    • account_id = personal
    • mailbox = Projects:2026:Q1
    • uidvalidity = 999
    • uid = 7
The parser works by extracting the last two segments as uid and uidvalidity, then joining all intermediate segments as the mailbox name.

Implementation Details

The message ID parsing and encoding logic is implemented in src/message_id.rs:
pub struct MessageId {
    /// Account identifier
    pub account_id: String,
    /// Mailbox name (may contain colons)
    pub mailbox: String,
    /// IMAP UIDVALIDITY (mailbox snapshot identifier)
    pub uidvalidity: u32,
    /// Message UID within mailbox
    pub uid: u32,
}

Parsing

The parser validates that:
  • The string starts with imap:
  • At least 5 segments are present (prefix, account, mailbox, uidvalidity, uid)
  • uidvalidity and uid are valid non-negative integers
  • The mailbox segment is not empty
let id = MessageId::parse("imap:default:INBOX:123:42")?;
assert_eq!(id.account_id, "default");
assert_eq!(id.mailbox, "INBOX");
assert_eq!(id.uidvalidity, 123);
assert_eq!(id.uid, 42);

Encoding

The encode() method produces the canonical string format:
let id = MessageId {
    account_id: "default".to_owned(),
    mailbox: "INBOX".to_owned(),
    uidvalidity: 123,
    uid: 42,
};
assert_eq!(id.encode(), "imap:default:INBOX:123:42");

Stability and UIDVALIDITY

The message_id is only stable while the mailbox’s UIDVALIDITY remains unchanged. IMAP servers may change UIDVALIDITY when:
  • The mailbox is deleted and recreated
  • A server-side mailbox migration occurs
  • Certain mailbox operations are performed on the server
If UIDVALIDITY changes, existing message_ids for that mailbox become invalid. The server will return a conflict error:
Error: conflict: mailbox snapshot changed; rerun search
Solution: Rerun imap_search_messages to obtain fresh message_ids with the new UIDVALIDITY.

Usage in Tools

The message_id is required by the following MCP tools:
  • imap_get_message - Fetch message details (src/server.rs:183)
  • imap_get_message_raw - Fetch RFC822 source (src/server.rs:206)
  • imap_update_message_flags - Modify flags (src/server.rs:228)
  • imap_copy_message - Copy to another mailbox (src/server.rs:246)
  • imap_move_message - Move to another mailbox (src/server.rs:266)
  • imap_delete_message - Delete message (src/server.rs:286)
Always obtain message_ids from imap_search_messages output rather than constructing them manually. This ensures the UIDVALIDITY is current and the format is correct.

Validation

When tools receive a message_id, the server performs several validation steps:
  1. Parse the message ID - Extract components and validate format
  2. Validate mailbox name - Check for control characters and length
  3. Match account ID - Ensure the embedded account matches the request
  4. Verify UIDVALIDITY - Confirm it still matches the current mailbox state
From src/server.rs:1809:
fn parse_and_validate_message_id(account_id: &str, message_id: &str) -> AppResult<MessageId> {
    let msg_id = MessageId::parse(message_id)?;
    validate_mailbox(&msg_id.mailbox)?;
    if msg_id.account_id != account_id {
        return Err(AppError::InvalidInput(
            "message_id account does not match account_id".to_owned(),
        ));
    }
    Ok(msg_id)
}

Error Handling

Invalid Format

If the message ID format is invalid:
{
  "error": {
    "code": "invalid_input",
    "message": "message_id must start with 'imap'"
  }
}

Account Mismatch

If the embedded account doesn’t match the request:
{
  "error": {
    "code": "invalid_input",
    "message": "message_id account does not match account_id"
  }
}

UIDVALIDITY Conflict

If the mailbox UIDVALIDITY has changed:
{
  "error": {
    "code": "conflict",
    "message": "message uidvalidity no longer matches mailbox"
  }
}

Best Practices

  1. Don’t construct manually - Always get message_ids from search results
  2. Check UIDVALIDITY conflicts - Handle conflict errors by re-searching
  3. Don’t cache long-term - Message IDs may become invalid if UIDVALIDITY changes
  4. Validate before use - Let the server validate rather than parsing client-side
  5. Use atomic operations - Complete operations quickly to avoid UIDVALIDITY changes mid-workflow

Build docs developers (and LLMs) love