← Back to DevBytes

IMAP Protocol: A Complete Reference Guide

What is IMAP?

The Internet Message Access Protocol (IMAP) is a standard internet protocol used by email clients to retrieve messages from a mail server. Unlike its predecessor POP3 (Post Office Protocol), IMAP allows users to access and manage their email messages directly on the server without downloading them to a local device first. This means you can read, organize, and delete emails across multiple devices while keeping everything synchronized.

IMAP was originally developed by Mark Crispin at the University of Washington in 1986. The current widely adopted version is IMAP4rev1, defined in RFC 3501. Modern IMAP implementations support encrypted connections via TLS (IMAPS on port 993 or STARTTLS on port 143), extensive folder management, server-side searching, and partial message fetching β€” making it the backbone of modern email infrastructure.

Key Characteristics of IMAP

Why IMAP Matters

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Understanding IMAP is essential for any developer building email-related applications. Here's why it matters in practical terms:

1. Multi-Device Email Access

In today's world, users check email on phones, tablets, laptops, and desktops. IMAP ensures that reading an email on your phone marks it as read everywhere. Deleting from one device removes it everywhere. This synchronization is impossible with POP3's download-and-delete model.

2. Efficient Bandwidth Usage

IMAP clients can fetch only message headers (subject, from, date) when displaying an inbox list. The full body is retrieved only when the user opens a specific email. For large attachments, clients can fetch just the text part and download attachments on demand. This dramatically reduces bandwidth consumption compared to downloading everything upfront.

3. Server-Side Processing

Need to find all emails from a specific sender across 50,000 messages? With IMAP, you issue a SEARCH command and the server returns matching UIDs β€” no need to download and process everything client-side. This is critical for email migration tools, backup systems, and analytics pipelines.

4. Integration and Automation

IMAP enables automated email processing: ticket systems that create issues from incoming emails, monitoring tools that check for alert messages, and data extraction pipelines that parse structured email content. The protocol's standardized command set makes cross-platform integration reliable and predictable.

How IMAP Works: Protocol Mechanics

Connection and Ports

IMAP typically operates on two ports:

Always prefer port 993 or STARTTLS on port 143 in production. Unencrypted IMAP sends credentials in plaintext and should never be used over untrusted networks.

Protocol State Machine

IMAP is a stateful protocol. Each connection progresses through distinct states:

Command-Response Tagging

Every client command is prefixed with a unique tag (usually an alphanumeric string like "A001", "A002"). Server responses are tagged with the same identifier, allowing the client to match responses to commands. Untagged responses (prefixed with "*") provide status updates, mailbox data, or search results. The server signals command completion with a tagged response ending in "OK" (success), "NO" (failure), or "BAD" (protocol error).

Practical Code Examples

Example 1: Raw IMAP via OpenSSL

This is the most fundamental way to interact with an IMAP server. Understanding raw commands gives you deep insight into how the protocol works before you abstract it away with libraries.

# Connect to Gmail IMAPS and authenticate
openssl s_client -connect imap.gmail.com:993 -crlf -quiet

# --- Server greets you with capability list ---
# * OK Gmail IMAP server ready

# 1. Login (tag: A001)
A001 LOGIN user@example.com "app-specific-password"
# A001 OK user@example.com authenticated

# 2. List available mailboxes
A002 LIST "" "*"
# * LIST (\HasNoChildren) "/" "INBOX"
# * LIST (\HasNoChildren) "/" "Sent"
# * LIST (\HasChildren) "/" "Projects"
# A002 OK List completed

# 3. Select INBOX
A003 SELECT INBOX
# * 150 EXISTS
# * 0 RECENT
# * OK [UIDVALIDITY 1234567] UIDs valid
# A003 OK [READ-WRITE] INBOX selected

# 4. Fetch headers of last 3 messages
A004 FETCH 148:150 (FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])
# * 148 FETCH (FLAGS (\Seen) RFC822.SIZE 4567 BODY[HEADER.FIELDS...
# * 149 FETCH (FLAGS (\Seen) RFC822.SIZE 8912 BODY[HEADER.FIELDS...
# * 150 FETCH (FLAGS () RFC822.SIZE 2341 BODY[HEADER.FIELDS...
# A004 OK Fetch completed

# 5. Fetch full body of message 150
A005 FETCH 150 (BODY[TEXT])
# * 150 FETCH (BODY[TEXT] {2341}
# ...full message body...
# )
# A005 OK Fetch completed

# 6. Search for messages from a sender
A006 SEARCH FROM "newsletter@example.com"
# * SEARCH 12 45 89 134
# A006 OK Search completed

# 7. Logout
A007 LOGOUT
# * BYE Logging out
# A007 OK Logout completed

Example 2: Python imaplib β€” Connect, List Folders, Search, Fetch

Python's standard library includes imaplib, which provides a robust IMAP4 client. Here's a complete working example that connects to a server, lists folders, searches for unread messages, and fetches their content.

import imaplib
import email
from email.header import decode_header
import datetime

# Configuration - use environment variables in production
IMAP_HOST = "imap.example.com"
IMAP_PORT = 993
USERNAME = "user@example.com"
PASSWORD = "your-app-password"  # Use app-specific passwords for Gmail

def connect_imap():
    """Establish a secure IMAP connection"""
    # Use SSL directly on port 993
    conn = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
    
    # Alternatively, use STARTTLS on port 143:
    # conn = imaplib.IMAP4(IMAP_HOST, 143)
    # conn.starttls()
    
    # Login
    status, response = conn.login(USERNAME, PASSWORD)
    if status != 'OK':
        raise Exception(f"Login failed: {response}")
    print("βœ“ Connected and authenticated")
    return conn

def list_all_folders(conn):
    """List all mailboxes/folders with their attributes"""
    status, raw_folders = conn.list()
    if status != 'OK':
        raise Exception("Failed to list folders")
    
    folders = []
    for line in raw_folders:
        # Parse folder attributes and name
        # Format: b'(\\HasNoChildren) "/" "FolderName"'
        parts = line.decode().split('"')
        if len(parts) >= 3:
            folder_name = parts[-2]  # The folder name
            attrs = parts[0].strip('() ')
            folders.append((folder_name, attrs))
    
    print(f"\nFound {len(folders)} folders:")
    for name, attrs in folders:
        has_children = '\\HasChildren' in attrs
        print(f"  {'πŸ“' if has_children else 'πŸ“‚'} {name}")
    return folders

def search_unread_messages(conn, folder="INBOX", limit=10):
    """Search for unread messages and return their UIDs"""
    # Select folder with read-write access
    status, response = conn.select(folder)
    if status != 'OK':
        raise Exception(f"Failed to select folder: {folder}")
    
    # Count total messages
    total = int(response[0].decode())
    print(f"\nFolder '{folder}' has {total} messages")
    
    # Search for UNSEEN messages
    status, uid_list = conn.uid('search', None, 'UNSEEN')
    if status != 'OK':
        print("No unread messages found")
        return []
    
    # Parse UIDs from response
    uids = uid_list[0].decode().split()
    if not uids:
        print("No unread messages")
        return []
    
    # Take the most recent ones (highest UIDs last)
    uids = sorted([int(uid) for uid in uids], reverse=True)[:limit]
    print(f"Found {len(uids)} unread messages, processing top {limit}")
    return uids

def fetch_email_by_uid(conn, uid):
    """Fetch full email by UID and parse it"""
    # Fetch the entire RFC822 message
    status, data = conn.uid('fetch', str(uid).encode(), '(RFC822)')
    if status != 'OK':
        return None
    
    # Parse raw email bytes
    raw_email = data[0][1]
    msg = email.message_from_bytes(raw_email)
    
    # Extract headers with decoding
    subject = decode_email_header(msg['Subject'])
    from_addr = decode_email_header(msg['From'])
    date_str = msg['Date']
    
    # Extract body (handling multipart)
    body = extract_email_body(msg)
    
    return {
        'uid': uid,
        'subject': subject,
        'from': from_addr,
        'date': date_str,
        'body': body[:500] + ('...' if len(body) > 500 else '')  # Truncate for display
    }

def decode_email_header(header):
    """Decode =?utf-8?Q?...?= encoded headers"""
    if header is None:
        return ""
    decoded_parts = decode_header(header)
    result = ""
    for part, charset in decoded_parts:
        if isinstance(part, bytes):
            result += part.decode(charset or 'utf-8', errors='replace')
        else:
            result += part
    return result

def extract_email_body(msg):
    """Extract plain text body from email, preferring text/plain"""
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            if content_type == 'text/plain':
                payload = part.get_payload(decode=True)
                if payload:
                    return payload.decode('utf-8', errors='replace')
        # Fallback: return first part
        payload = msg.get_payload(0, decode=True)
        return payload.decode('utf-8', errors='replace') if payload else ""
    else:
        payload = msg.get_payload(decode=True)
        return payload.decode('utf-8', errors='replace') if payload else ""

def mark_as_read(conn, uid):
    """Mark a message as seen using STORE command"""
    conn.uid('store', str(uid).encode(), '+FLAGS', '\\Seen')

def main():
    conn = connect_imap()
    
    # List all folders
    list_all_folders(conn)
    
    # Search for unread messages
    uids = search_unread_messages(conn, "INBOX", limit=5)
    
    # Fetch and display each unread message
    for uid in uids:
        email_data = fetch_email_by_uid(conn, uid)
        if email_data:
            print(f"\n{'='*60}")
            print(f"UID: {email_data['uid']}")
            print(f"From: {email_data['from']}")
            print(f"Subject: {email_data['subject']}")
            print(f"Date: {email_data['date']}")
            print(f"Body preview: {email_data['body'][:200]}")
            
            # Mark as read after processing
            mark_as_read(conn, uid)
            print("βœ“ Marked as read")
    
    # Cleanup
    conn.logout()
    print("\nβœ“ Logged out")

if __name__ == "__main__":
    main()

Example 3: Node.js with node-imap β€” Monitor New Emails in Real-Time

For real-time email monitoring, Node.js with the node-imap package provides an excellent event-driven approach. This example demonstrates setting up a mailbox listener that fires whenever new emails arrive.

// Install: npm install node-imap mailparser
const Imap = require('node-imap');
const { simpleParser } = require('mailparser');
const { EventEmitter } = require('events');

class EmailMonitor extends EventEmitter {
  constructor(config) {
    super();
    this.config = {
      user: config.user,
      password: config.password,
      host: config.host,
      port: 993,
      tls: true,
      tlsOptions: { rejectUnauthorized: true },
      keepAlive: true,  // Prevent timeout disconnects
    };
    this.imap = new Imap(this.config);
    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.imap.on('ready', () => {
      console.log('βœ“ IMAP connection ready');
      this.openInbox();
    });

    this.imap.on('error', (err) => {
      console.error('IMAP error:', err);
      this.emit('error', err);
    });

    this.imap.on('end', () => {
      console.log('Connection ended, reconnecting in 30s...');
      setTimeout(() => this.connect(), 30000);
    });
  }

  connect() {
    console.log('Connecting to IMAP server...');
    this.imap.connect();
  }

  openInbox() {
    this.imap.openBox('INBOX', false, (err, mailbox) => {
      if (err) {
        console.error('Failed to open INBOX:', err);
        this.emit('error', err);
        return;
      }
      
      console.log(`INBOX opened: ${mailbox.messages.total} messages`);
      
      // Listen for new mail arriving
      this.imap.on('mail', (count) => {
        console.log(`πŸ“¨ ${count} new message(s) arrived`);
        this.fetchLatestMessages(count);
      });

      // Also listen for changes to existing messages (flag updates)
      this.imap.on('update', (seqno, info) => {
        console.log(`Message ${seqno} updated:`, info);
      });
    });
  }

  fetchLatestMessages(count) {
    // Fetch the most recent 'count' messages
    const fetchOptions = {
      bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)', 'TEXT'],
      struct: true,
      markSeen: false,  // Don't auto-mark as read
    };

    // Calculate range: last 'count' messages
    this.imap.search([['UNSEEN']], (err, results) => {
      if (err || !results.length) return;
      
      const fetch = this.imap.fetch(results, fetchOptions);
      
      fetch.on('message', (msg, seqno) => {
        console.log(`Processing message #${seqno}`);
        let attributes = {};
        
        msg.on('attributes', (attrs) => {
          attributes = attrs;
        });

        msg.on('body', (stream, info) => {
          let buffer = '';
          stream.on('data', (chunk) => {
            buffer += chunk.toString('utf8');
          });
          
          stream.on('end', async () => {
            if (info.which === 'TEXT') {
              // Parse the complete email
              const parsed = await simpleParser(stream.buffer || buffer);
              this.emit('newEmail', {
                seqno,
                uid: attributes.uid,
                flags: attributes.flags,
                subject: parsed.subject,
                from: parsed.from?.value[0]?.address,
                date: parsed.date,
                text: parsed.text?.substring(0, 500),
                html: parsed.html,
              });
              console.log(`βœ“ Parsed email: ${parsed.subject}`);
            }
          });
        });
      });

      fetch.on('end', () => {
        console.log('βœ“ Fetch complete');
      });
    });
  }

  disconnect() {
    this.imap.end();
  }
}

// Usage example
const monitor = new EmailMonitor({
  user: 'user@example.com',
  password: process.env.EMAIL_PASSWORD,
  host: 'imap.example.com',
});

monitor.on('newEmail', (email) => {
  console.log(`\nπŸ“§ NEW EMAIL:`);
  console.log(`   From: ${email.from}`);
  console.log(`   Subject: ${email.subject}`);
  console.log(`   Date: ${email.date}`);
  console.log(`   Preview: ${email.text?.substring(0, 150)}`);
});

monitor.on('error', (err) => {
  console.error('Monitor error:', err);
});

// Start monitoring
monitor.connect();

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('\nShutting down...');
  monitor.disconnect();
  process.exit(0);
});

Example 4: IMAP SEARCH with Advanced Criteria

The SEARCH command supports rich criteria for filtering messages. Here's how to construct complex searches across different programming environments.

# --- Raw IMAP search examples ---

# Search for messages from a specific address since a date
A001 SEARCH FROM "alerts@company.com" SINCE 01-Jan-2025
# Returns: * SEARCH 45 67 89

# Search for messages with attachments (larger than 100KB)
A002 SEARCH LARGER 100000
# Returns matching UIDs

# Search for messages with specific subject AND unseen
A003 SEARCH SUBJECT "invoice" UNSEEN
# Combines criteria with AND logic

# Search for messages NOT from a list of addresses
A004 SEARCH UNSEEN NOT FROM "newsletter@example.com" NOT FROM "spam@example.com"

# Search for messages flagged AND from a sender
A005 SEARCH FLAGGED FROM "boss@company.com"

# Complex search: unread, from specific domain, with subject keyword, since date
A006 SEARCH UNSEEN FROM "@example.com" SUBJECT "report" SINCE 15-Feb-2025
# Python: Advanced search with imaplib
import imaplib
from datetime import datetime, timedelta

conn = imaplib.IMAP4_SSL('imap.example.com', 993)
conn.login('user@example.com', 'password')
conn.select('INBOX')

# Search with multiple criteria
criteria = '(FROM "notifications@example.com" SUBJECT "alert" UNSEEN SINCE "20-Feb-2025")'
status, result = conn.uid('search', None, criteria)
uids = result[0].decode().split() if result[0] else []

# Search messages larger than 1MB with attachments
status, result = conn.uid('search', None, '(LARGER 1000000)')

# Search using NOT operator
status, result = conn.uid('search', None, '(UNSEEN NOT FROM "spam@example.com")')

# Search for messages within a date range using SINCE and BEFORE
status, result = conn.uid('search', None, '(SINCE "01-Jan-2025" BEFORE "01-Feb-2025")')

# Search by header fields
status, result = conn.uid('search', None, '(HEADER "X-Priority" "1")')
status, result = conn.uid('search', None, '(HEADER "List-Unsubscribe" "@")')

conn.logout()

Best Practices for IMAP Development

1. Always Use Encryption

Never transmit credentials over plaintext IMAP. Use port 993 (IMAPS) or STARTTLS on port 143. Verify TLS certificates properly β€” avoid disabling certificate validation in production code. For Gmail and most modern providers, TLS 1.2+ is required.

# Bad: Disabling certificate verification
conn = imaplib.IMAP4_SSL('imap.example.com', 993, ssl_context=ssl._create_unverified_context())

# Good: Use default verification
conn = imaplib.IMAP4_SSL('imap.example.com', 993)

# Better: Explicit SSL context with minimum TLS version
import ssl
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
conn = imaplib.IMAP4_SSL('imap.example.com', 993, ssl_context=context)

2. Use UIDs, Not Sequence Numbers

IMAP provides two ways to reference messages: sequence numbers (relative position in mailbox) and UIDs (unique, permanent identifiers). Always prefer UIDs for operations that span multiple connections. Sequence numbers change when messages are expunged; UIDs remain constant (unless the UIDVALIDITY changes). Use conn.uid('fetch', ...) and conn.uid('search', ...) patterns.

# Good: Using UIDs (stable across sessions)
status, result = conn.uid('fetch', '12345:12350', '(RFC822)')

# Avoid: Using sequence numbers for persistent operations
status, result = conn.fetch('1:5', '(RFC822)')  # These numbers shift after expunge

3. Implement Proper Error Handling and Retry Logic

IMAP connections can drop due to network issues, server timeouts, or maintenance. Implement exponential backoff retry logic. Handle the full spectrum of server responses: OK (success), NO (failure β€” often permission-related), BAD (protocol error β€” check your command syntax), and BYE (server-initiated disconnect).

import time
import random

def imap_operation_with_retry(conn_func, max_retries=5, base_delay=1):
    """Execute an IMAP operation with exponential backoff retry"""
    for attempt in range(max_retries):
        try:
            result = conn_func()
            return result
        except (imaplib.IMAP4.abort, imaplib.IMAP4.error, ConnectionError) as e:
            if attempt == max_retries - 1:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
            print(f"Retry {attempt+1}/{max_retries} after {delay:.1f}s: {e}")
            time.sleep(delay)
            # Reconnect before retry
            conn.logout()
            conn.connect()

# Usage
result = imap_operation_with_retry(
    lambda: conn.uid('search', None, 'UNSEEN')
)

4. Respect Rate Limits

Most email providers impose rate limits on IMAP connections. Gmail, for instance, limits you to approximately 15-20 simultaneous IMAP connections per account and throttles excessive command rates. Implement connection pooling, avoid tight loops of FETCH commands, and batch operations where possible. Use the IDLE command for real-time notifications instead of polling.

# Bad: Polling every 2 seconds
while True:
    conn.select('INBOX')
    # ... check for new messages
    time.sleep(2)

# Good: Use IDLE for push-based notifications
conn.idle()
# Server sends updates in real-time without polling
# Exit IDLE with DONE command when you need to take action

5. Handle MIME and Encodings Properly

Email messages are complex MIME structures. A single message can contain plain text, HTML, inline images, and attachments β€” all in nested multipart containers. Always decode headers using the mechanisms provided (RFC 2047 for headers, RFC 2231 for parameter values). Handle character set conversions gracefully with fallback encodings.

# Decoding internationalized headers properly
from email.header import decode_header, make_header

raw_subject = '=?UTF-8?B?w5xibWVy?= '
decoded = str(make_header(decode_header(raw_subject)))
print(decoded)  # Outputs: "Übmer"

# Handling nested multipart messages
def extract_all_text_parts(msg):
    """Recursively extract all text parts from a MIME message"""
    texts = []
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == 'text/plain':
                payload = part.get_payload(decode=True)
                if payload:
                    charset = part.get_content_charset() or 'utf-8'
                    texts.append(payload.decode(charset, errors='replace'))
    else:
        payload = msg.get_payload(decode=True)
        if payload:
            texts.append(payload.decode('utf-8', errors='replace'))
    return texts

6. Manage Mailbox State Carefully

When you SELECT a mailbox, the server provides state information: EXISTS (total messages), RECENT (new messages this session), UIDVALIDITY (epoch for UIDs), and UIDNEXT (next predicted UID). Cache UIDVALIDITY β€” if it changes, all your cached UIDs are invalidated and must be refreshed. After operations that modify messages (STORE, EXPUNGE), be aware that sequence numbers shift.

# Check UIDVALIDITY when selecting a mailbox
status, response = conn.select('INBOX')
# Parse response for UIDVALIDITY
# * OK [UIDVALIDITY 1234567890] UIDs valid
# Store this value and compare on subsequent SELECTs

uidvalidity = None
for line in response:
    if b'UIDVALIDITY' in line:
        uidvalidity = int(line.split(b'UIDVALIDITY')[1].split(b']')[0])
        break

# Cache pattern
CACHED_UIDVALIDITY = load_from_cache()
if uidvalidity != CACHED_UIDVALIDITY:
    print("UIDVALIDITY changed β€” invalidating local cache")
    clear_cache()
    save_to_cache(uidvalidity)

7. Secure Credential Management

Never hardcode passwords in source code. Use environment variables, secure credential stores, or configuration files with restricted permissions. For services like Gmail, generate app-specific passwords rather than using your main account password. For OAuth-enabled providers (Gmail, Microsoft 365), implement OAuth 2.0 authentication via the AUTHENTICATE XOAUTH2 SASL mechanism for token-based access without storing passwords.

# Environment variable approach
import os
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD')
if not EMAIL_PASSWORD:
    raise ValueError("Set EMAIL_PASSWORD environment variable")

# OAuth 2.0 for Gmail (simplified)
def generate_xoauth2_token(client_id, client_secret, refresh_token, user_email):
    """Generate XOAUTH2 SASL token for IMAP authentication"""
    # Use google-auth-library or similar to get access token
    access_token = get_access_token(client_id, client_secret, refresh_token)
    auth_string = f"user={user_email}\x01auth=Bearer {access_token}\x01\x01"
    return auth_string.encode('utf-8')

conn = imaplib.IMAP4_SSL('imap.gmail.com', 993)
conn.authenticate('XOAUTH2', lambda x: generate_xoauth2_token(...))

Common IMAP Pitfalls and How to Avoid Them

Conclusion

IMAP remains the cornerstone protocol for modern email access, powering everything from personal email clients to enterprise-scale email processing pipelines. Its server-centric model, rich command set for partial fetching and server-side search, and robust synchronization capabilities make it indispensable for any application that handles email.

As a developer, understanding IMAP at both the protocol level and through practical library usage gives you the foundation to build reliable, efficient email integrations. The raw command examples in this guide demystify the protocol's inner workings, while the Python and Node.js examples provide production-ready patterns you can adapt directly. By following the best practices outlined here β€” using encryption, preferring UIDs, implementing proper retry logic, handling MIME correctly, and managing credentials securely β€” you'll create email applications that are both robust and maintainable.

The protocol continues to evolve with extensions like CONDSTORE for conditional modification, QRESYNC for quick resynchronization, and COMPRESS for bandwidth optimization. However, the core concepts covered in this guide form the bedrock of all IMAP development and will serve you well regardless of which extensions you later adopt.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles