imap

package module
v0.1.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 21, 2025 License: MIT Imports: 24 Imported by: 11

README

Go IMAP Client (go-imap)

Go Reference CI Go Report Card

Simple, pragmatic IMAP client for Go (Golang) with TLS, LOGIN or XOAUTH2 (OAuth 2.0), IDLE notifications, robust reconnects, and batteries‑included helpers for searching, fetching, moving, and flagging messages.

Works great with Gmail, Office 365/Exchange, and most RFC‑compliant IMAP servers.

Features

  • TLS connections and timeouts (DialTimeout, CommandTimeout)
  • Authentication via LOGIN and XOAUTH2
  • Folders: SELECT/EXAMINE, list folders, error-tolerant counting
  • Search: UID SEARCH helpers with RFC 3501 literal syntax for non-ASCII text
  • Fetch: envelope, flags, size, text/HTML bodies, attachments
  • Mutations: move, set flags, delete + expunge
  • IMAP IDLE with event handlers for EXISTS, EXPUNGE, FETCH
  • Automatic reconnect with re‑auth and folder restore
  • Robust folder handling with graceful error recovery for problematic folders

Install

go get github.com/BrianLeishman/go-imap

Requires Go 1.25+ (see go.mod).

Quick Start

Basic Connection (LOGIN)
package main

import (
    "fmt"
    "time"
    imap "github.com/BrianLeishman/go-imap"
)

func main() {
    // Optional configuration
    imap.Verbose = false      // Enable to emit debug-level IMAP logs
    imap.RetryCount = 3        // Number of retries for failed commands
    imap.DialTimeout = 10 * time.Second
    imap.CommandTimeout = 30 * time.Second

    // For self-signed certificates (use with caution!)
    // imap.TLSSkipVerify = true

    // Connect with standard LOGIN authentication
    m, err := imap.New("username", "password", "mail.server.com", 993)
    if err != nil { panic(err) }
    defer m.Close()

    // Quick test
    folders, err := m.GetFolders()
    if err != nil { panic(err) }
    fmt.Printf("Connected! Found %d folders\n", len(folders))
}
OAuth 2.0 Authentication (XOAUTH2)
// Connect with OAuth2 (Gmail, Office 365, etc.)
m, err := imap.NewWithOAuth2("user@example.com", accessToken, "imap.gmail.com", 993)
if err != nil { panic(err) }
defer m.Close()

// The OAuth2 connection works exactly like LOGIN after authentication
if err := m.SelectFolder("INBOX"); err != nil { panic(err) }

Logging

The client uses Go's log/slog package for structured logging. By default it emits info, warning, and error events to standard error with the component attribute set to imap/agent. Opt-in debug output is controlled by the existing imap.Verbose flag:

imap.Verbose = true // Log every IMAP command/response at debug level

You can plug in your own logger implementation via imap.SetLogger. For *slog.Logger specifically, call imap.SetSlogLogger. When unset, the library falls back to a text handler.

import (
    "log/slog"
    "os"
    imap "github.com/BrianLeishman/go-imap"
)

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
imap.SetSlogLogger(slog.New(handler))

Call imap.SetLogger(nil) to reset to the built-in logger. When verbose mode is enabled you can further reduce noise by setting imap.SkipResponses = true to suppress raw server responses.

Examples

Complete, runnable example programs are available in the examples/ directory. Each example demonstrates specific features and can be run directly:

go run examples/basic_connection/main.go
Available Examples
Getting Started
Working with Emails
  • folders - List folders, select/examine folders, get email counts
  • search - Search emails by various criteria (flags, dates, sender, size, etc.)
  • literal_search - Search with non-ASCII characters using RFC 3501 literal syntax
  • fetch_emails - Fetch email headers (fast) and full content with attachments (slower)
  • email_operations - Move emails, set/remove flags, delete and expunge
Advanced Features

Detailed Usage Examples

1. Working with Folders
// List all folders
folders, err := m.GetFolders()
if err != nil { panic(err) }

// Example output:
// folders = []string{
//     "INBOX",
//     "Sent",
//     "Drafts",
//     "Trash",
//     "INBOX/Receipts",
//     "INBOX/Important",
//     "[Gmail]/All Mail",
//     "[Gmail]/Spam",
// }

for _, folder := range folders {
    fmt.Println("Folder:", folder)
}

// Select a folder for operations (read-write mode)
err = m.SelectFolder("INBOX")
if err != nil { panic(err) }

// Select folder in read-only mode
err = m.ExamineFolder("INBOX")
if err != nil { panic(err) }

// Get total email count across all folders
totalCount, err := m.GetTotalEmailCount()
if err != nil { panic(err) }
fmt.Printf("Total emails in all folders: %d\n", totalCount)

// Get count excluding certain folders
excludedFolders := []string{"Trash", "[Gmail]/Spam"}
count, err := m.GetTotalEmailCountExcluding(excludedFolders)
if err != nil { panic(err) }
fmt.Printf("Total emails (excluding spam/trash): %d\n", count)

// Error-tolerant counting (continues even if some folders fail)
// This is especially useful with Gmail or other providers that have inaccessible system folders
safeCount, folderErrors, err := m.GetTotalEmailCountSafe()
if err != nil { panic(err) }
fmt.Printf("Total accessible emails: %d\n", safeCount)

if len(folderErrors) > 0 {
    fmt.Printf("Note: %d folders had errors:\n", len(folderErrors))
    for _, folderErr := range folderErrors {
        fmt.Printf("  - %v\n", folderErr)
    }
}
// Example output:
// Total accessible emails: 1247
// Note: 2 folders had errors:
//   - folder "[Gmail]": NO [NONEXISTENT] Unknown Mailbox
//   - folder "[Gmail]/All Mail": NO [NONEXISTENT] Unknown Mailbox

// Get detailed statistics for each folder (includes max UID)
stats, err := m.GetFolderStats()
if err != nil { panic(err) }

fmt.Printf("Found %d folders:\n", len(stats))
for _, stat := range stats {
    if stat.Error != nil {
        fmt.Printf("  %-20s [ERROR]: %v\n", stat.Name, stat.Error)
    } else {
        fmt.Printf("  %-20s %5d emails, max UID: %d\n",
            stat.Name, stat.Count, stat.MaxUID)
    }
}
// Example output:
// Found 8 folders:
//   INBOX                 342 emails, max UID: 1543
//   Sent                   89 emails, max UID: 234
//   Drafts                  3 emails, max UID: 67
//   Trash                  12 emails, max UID: 89
//   [Gmail]          [ERROR]: NO [NONEXISTENT] Unknown Mailbox
//   [Gmail]/Spam           0 emails, max UID: 0
//   INBOX/Archive        801 emails, max UID: 2156
//   INBOX/Important       45 emails, max UID: 987
1.1. Handling Problematic Folders

Some IMAP servers (especially Gmail) have special system folders that cannot be examined or may return errors. The traditional GetTotalEmailCount() method will fail completely if any folder is inaccessible, but the new safe methods continue processing other folders.

When to Use Safe Methods
  • Gmail users: Gmail's [Gmail] folder often returns "NO [NONEXISTENT] Unknown Mailbox"
  • Exchange/Office 365: Some system folders may be restricted
  • Custom IMAP servers: Servers with permission-restricted folders
  • Production applications: When you need reliable email counting despite folder issues
// Traditional approach - fails if ANY folder has issues
totalCount, err := m.GetTotalEmailCount()
if err != nil {
    // This will fail completely if "[Gmail]" folder is inaccessible
    fmt.Printf("Count failed: %v\n", err)
    // Output: Count failed: EXAMINE command failed: NO [NONEXISTENT] Unknown Mailbox
}

// Safe approach - continues despite folder errors
safeCount, folderErrors, err := m.GetTotalEmailCountSafe()
if err != nil {
    // Only fails on serious connection issues, not individual folder problems
    panic(err)
}

fmt.Printf("Counted %d emails from accessible folders\n", safeCount)
if len(folderErrors) > 0 {
    fmt.Printf("Skipped %d problematic folders\n", len(folderErrors))
}

// Safe exclusion - combine error tolerance with folder filtering
excludedFolders := []string{"Trash", "Junk", "Deleted Items"}
count, folderErrors, err := m.GetTotalEmailCountSafeExcluding(excludedFolders)
if err != nil { panic(err) }

fmt.Printf("Active emails: %d (excluding trash/spam and skipping errors)\n", count)

// Detailed analysis with error handling
stats, err := m.GetFolderStats()
if err != nil { panic(err) }

accessibleFolders := 0
totalEmails := 0
maxUID := 0

for _, stat := range stats {
    if stat.Error != nil {
        fmt.Printf("⚠️  %s: %v\n", stat.Name, stat.Error)
        continue
    }

    accessibleFolders++
    totalEmails += stat.Count
    if stat.MaxUID > maxUID {
        maxUID = stat.MaxUID
    }

    fmt.Printf("✅ %-25s %5d emails (UID range: 1-%d)\n",
        stat.Name, stat.Count, stat.MaxUID)
}

fmt.Printf("\nSummary: %d/%d folders accessible, %d total emails, highest UID: %d\n",
    accessibleFolders, len(stats), totalEmails, maxUID)
Error Types You Might Encounter
stats, err := m.GetFolderStats()
if err != nil { panic(err) }

for _, stat := range stats {
    if stat.Error != nil {
        fmt.Printf("Folder '%s' error: %v\n", stat.Name, stat.Error)

        // Common error patterns:
        if strings.Contains(stat.Error.Error(), "NONEXISTENT") {
            fmt.Printf("  → This is a virtual/system folder that can't be examined\n")
        } else if strings.Contains(stat.Error.Error(), "permission") {
            fmt.Printf("  → This folder requires special permissions\n")
        } else {
            fmt.Printf("  → Unexpected error, might indicate connection issues\n")
        }
    }
}
2. Searching for Emails
// Select folder first
err := m.SelectFolder("INBOX")
if err != nil { panic(err) }

// Basic searches - returns slice of UIDs
allUIDs, _ := m.GetUIDs("ALL")           // All emails
unseenUIDs, _ := m.GetUIDs("UNSEEN")     // Unread emails
recentUIDs, _ := m.GetUIDs("RECENT")     // Recent emails
seenUIDs, _ := m.GetUIDs("SEEN")         // Read emails
flaggedUIDs, _ := m.GetUIDs("FLAGGED")   // Starred/flagged emails

// Example output:
fmt.Printf("Found %d total emails\n", len(allUIDs))      // Found 342 total emails
fmt.Printf("Found %d unread emails\n", len(unseenUIDs))  // Found 12 unread emails
fmt.Printf("UIDs of unread: %v\n", unseenUIDs)           // UIDs of unread: [245 246 247 251 252 253 254 255 256 257 258 259]

// Date-based searches
todayUIDs, _ := m.GetUIDs("ON 15-Sep-2024")
sinceUIDs, _ := m.GetUIDs("SINCE 10-Sep-2024")
beforeUIDs, _ := m.GetUIDs("BEFORE 20-Sep-2024")
rangeUIDs, _ := m.GetUIDs("SINCE 1-Sep-2024 BEFORE 30-Sep-2024")

// From/To searches
fromBossUIDs, _ := m.GetUIDs(`FROM "boss@company.com"`)
toMeUIDs, _ := m.GetUIDs(`TO "me@company.com"`)

// Subject/body searches
subjectUIDs, _ := m.GetUIDs(`SUBJECT "invoice"`)
bodyUIDs, _ := m.GetUIDs(`BODY "payment"`)
textUIDs, _ := m.GetUIDs(`TEXT "urgent"`) // Searches both subject and body

// Complex searches
complexUIDs, _ := m.GetUIDs(`UNSEEN FROM "support@github.com" SINCE 1-Sep-2024`)

// UID ranges (raw IMAP syntax)
firstUID, _ := m.GetUIDs("1")          // UID 1 only
lastUID, _ := m.GetUIDs("*")           // Highest UID only
rangeUIDs, _ := m.GetUIDs("1:10")      // UIDs 1 through 10

// Get the N most recent messages (recommended for "last N" queries)
last10UIDs, _ := m.GetLastNUIDs(10)    // Last 10 messages by UID

// Size-based searches
largeUIDs, _ := m.GetUIDs("LARGER 10485760")  // Emails larger than 10MB
smallUIDs, _ := m.GetUIDs("SMALLER 1024")     // Emails smaller than 1KB

// Non-ASCII searches using RFC 3501 literal syntax
// The library automatically detects and handles literal syntax {n}
// where n is the byte count of the following data

// Search for Cyrillic text in subject (тест = 8 bytes in UTF-8)
cyrillicUIDs, _ := m.GetUIDs("CHARSET UTF-8 Subject {8}\r\nтест")

// Search for Chinese text in subject (测试 = 6 bytes in UTF-8)  
chineseUIDs, _ := m.GetUIDs("CHARSET UTF-8 Subject {6}\r\n测试")

// Search for Japanese text in body (テスト = 9 bytes in UTF-8)
japaneseUIDs, _ := m.GetUIDs("CHARSET UTF-8 BODY {9}\r\nテスト")

// Search for Arabic text (اختبار = 12 bytes in UTF-8)
arabicUIDs, _ := m.GetUIDs("CHARSET UTF-8 TEXT {12}\r\nاختبار")

// Search with emoji (😀👍 = 8 bytes in UTF-8)
emojiUIDs, _ := m.GetUIDs("CHARSET UTF-8 TEXT {8}\r\n😀👍")

// Note: Always specify CHARSET UTF-8 for non-ASCII searches
// The {n} syntax tells the server exactly how many bytes to expect
// This is crucial since Unicode characters use multiple bytes
3. Fetching Email Details
// Get overview (headers only, no body) - FAST
overviews, err := m.GetOverviews(uids...)
if err != nil { panic(err) }

for uid, email := range overviews {
    fmt.Printf("UID %d:\n", uid)
    fmt.Printf("  Subject: %s\n", email.Subject)
    fmt.Printf("  From: %s\n", email.From)
    fmt.Printf("  Date: %s\n", email.Sent)
    fmt.Printf("  Size: %d bytes\n", email.Size)
    fmt.Printf("  Flags: %v\n", email.Flags)
}

// Example output:
// UID 245:
//   Subject: Your order has shipped!
//   From: Amazon <ship-confirm@amazon.com>
//   Date: 2024-09-15 14:23:01 +0000 UTC
//   Size: 45234 bytes
//   Flags: [\Seen]

// Get full emails with bodies - SLOWER
emails, err := m.GetEmails(uids...)
if err != nil { panic(err) }

for uid, email := range emails {
    fmt.Printf("\n=== Email UID %d ===\n", uid)
    fmt.Printf("Subject: %s\n", email.Subject)
    fmt.Printf("From: %s\n", email.From)
    fmt.Printf("To: %s\n", email.To)
    fmt.Printf("CC: %s\n", email.CC)
    fmt.Printf("Date Sent: %s\n", email.Sent)
    fmt.Printf("Date Received: %s\n", email.Received)
    fmt.Printf("Message-ID: %s\n", email.MessageID)
    fmt.Printf("Flags: %v\n", email.Flags)
    fmt.Printf("Size: %d bytes\n", email.Size)

    // Body content
    if len(email.Text) > 0 {
        fmt.Printf("Text (first 200 chars): %.200s...\n", email.Text)
    }
    if len(email.HTML) > 0 {
        fmt.Printf("HTML length: %d bytes\n", len(email.HTML))
    }

    // Attachments
    if len(email.Attachments) > 0 {
        fmt.Printf("Attachments (%d):\n", len(email.Attachments))
        for _, att := range email.Attachments {
            fmt.Printf("  - %s (%s, %d bytes)\n",
                att.Name, att.MimeType, len(att.Content))
        }
    }
}

// Example full output:
// === Email UID 245 ===
// Subject: Your order has shipped!
// From: ship-confirm@amazon.com:Amazon Shipping
// To: customer@example.com:John Doe
// CC:
// Date Sent: 2024-09-15 14:23:01 +0000 UTC
// Date Received: 2024-09-15 14:23:15 +0000 UTC
// Message-ID: <20240915142301.3F4A5B0@amazon.com>
// Flags: [\Seen]
// Size: 45234 bytes
// Text (first 200 chars): Hello John, Your order #123-4567890 has shipped and is on its way! Track your package: ...
// HTML length: 42150 bytes
// Attachments (2):
//   - invoice.pdf (application/pdf, 125432 bytes)
//   - shipping-label.png (image/png, 85234 bytes)

// Using the String() method for a quick summary
email := emails[245]
fmt.Print(email)
// Output:
// Subject: Your order has shipped!
// To: customer@example.com:John Doe
// From: ship-confirm@amazon.com:Amazon Shipping
// Text: Hello John, Your order...(4.5 kB)
// HTML: <html xmlns:v="urn:s... (42 kB)
// 2 Attachment(s): [invoice.pdf (application/pdf 125 kB), shipping-label.png (image/png 85 kB)]
4. Email Operations
// === Moving Emails ===
uid := 245
err = m.MoveEmail(uid, "INBOX/Archive")
if err != nil { panic(err) }
fmt.Printf("Moved email %d to Archive\n", uid)

// === Setting Flags ===
// Mark as read
err = m.MarkSeen(uid)
if err != nil { panic(err) }

// Set multiple flags at once
flags := imap.Flags{
    Seen:     imap.FlagAdd,      // Mark as read
    Flagged:  imap.FlagAdd,      // Star/flag the email
    Answered: imap.FlagRemove,   // Remove answered flag
}
err = m.SetFlags(uid, flags)
if err != nil { panic(err) }

// Custom keywords (if server supports)
flags = imap.Flags{
    Keywords: map[string]bool{
        "$Important": true,      // Add custom keyword
        "$Processed": true,      // Add another
        "$Pending":   false,     // Remove this keyword
    },
}
err = m.SetFlags(uid, flags)
if err != nil { panic(err) }

// === Deleting Emails ===
// Step 1: Mark as deleted (sets \Deleted flag)
err = m.DeleteEmail(uid)
if err != nil { panic(err) }
fmt.Printf("Marked email %d for deletion\n", uid)

// Step 2: Expunge to permanently remove all \Deleted emails
err = m.Expunge()
if err != nil { panic(err) }
fmt.Println("Permanently deleted all marked emails")

// Note: Some servers support UID EXPUNGE for selective expunge
// This library uses regular EXPUNGE which removes ALL \Deleted messages
5. IDLE Notifications (Real-time Updates)
// IDLE allows you to receive real-time notifications when mailbox changes occur
// The connection will automatically refresh IDLE every 5 minutes (RFC requirement)

// Create an event handler
handler := &imap.IdleHandler{
    // New email arrived
    OnExists: func(e imap.ExistsEvent) {
        fmt.Printf("[EXISTS] New message at index: %d\n", e.MessageIndex)
        // Example output: [EXISTS] New message at index: 343

        // You might want to fetch the new email:
        // uids, _ := m.GetUIDs(fmt.Sprintf("%d", e.MessageIndex))
        // emails, _ := m.GetEmails(uids...)
    },

    // Email was deleted/expunged
    OnExpunge: func(e imap.ExpungeEvent) {
        fmt.Printf("[EXPUNGE] Message removed at index: %d\n", e.MessageIndex)
        // Example output: [EXPUNGE] Message removed at index: 125
    },

    // Email flags changed (read, flagged, etc.)
    OnFetch: func(e imap.FetchEvent) {
        fmt.Printf("[FETCH] Flags changed - Index: %d, UID: %d, Flags: %v\n",
            e.MessageIndex, e.UID, e.Flags)
        // Example output: [FETCH] Flags changed - Index: 42, UID: 245, Flags: [\Seen \Flagged]
    },
}

// Start IDLE (non-blocking, runs in background)
err := m.StartIdle(handler)
if err != nil { panic(err) }

// Your application continues running...
// IDLE events will be handled in the background

// When you're done, stop IDLE
err = m.StopIdle()
if err != nil { panic(err) }

// Full example with proper lifecycle:
func monitorInbox(m *imap.Dialer) {
    // Select the folder to monitor
    if err := m.SelectFolder("INBOX"); err != nil {
        panic(err)
    }

    handler := &imap.IdleHandler{
        OnExists: func(e imap.ExistsEvent) {
            fmt.Printf("📬 New email! Total messages now: %d\n", e.MessageIndex)
        },
        OnExpunge: func(e imap.ExpungeEvent) {
            fmt.Printf("🗑️ Email deleted at position %d\n", e.MessageIndex)
        },
        OnFetch: func(e imap.FetchEvent) {
            fmt.Printf("📝 Email %d updated with flags: %v\n", e.UID, e.Flags)
        },
    }

    fmt.Println("Starting IDLE monitoring...")
    if err := m.StartIdle(handler); err != nil {
        panic(err)
    }

    // Monitor for 30 minutes
    time.Sleep(30 * time.Minute)

    fmt.Println("Stopping IDLE monitoring...")
    if err := m.StopIdle(); err != nil {
        panic(err)
    }
}
6. Error Handling and Reconnection
// The library automatically handles reconnection for most operations
// But here's how to handle errors properly:

func robustEmailFetch(m *imap.Dialer) {
    // Set retry configuration
    imap.RetryCount = 5  // Will retry failed operations 5 times
    imap.Verbose = true  // Emit debug logs while retrying commands

    err := m.SelectFolder("INBOX")
    if err != nil {
        // Connection errors are automatically retried
        // This only fails after all retries are exhausted
        fmt.Printf("Failed to select folder after %d retries: %v\n", imap.RetryCount, err)

        // You might want to manually reconnect
        if err := m.Reconnect(); err != nil {
            fmt.Printf("Manual reconnection failed: %v\n", err)
            return
        }
    }

    // Fetch emails with automatic retry on network issues
    uids, err := m.GetUIDs("UNSEEN")
    if err != nil {
        fmt.Printf("Search failed: %v\n", err)
        return
    }

    // The library will automatically:
    // 1. Close the broken connection
    // 2. Create a new connection
    // 3. Re-authenticate (LOGIN or XOAUTH2)
    // 4. Re-select the previously selected folder
    // 5. Retry the failed command

    emails, err := m.GetEmails(uids...)
    if err != nil {
        fmt.Printf("Fetch failed after retries: %v\n", err)
        return
    }

    fmt.Printf("Successfully fetched %d emails\n", len(emails))
}

// Timeout configuration
func configureTimeouts() {
    // Connection timeout (for initial connection)
    imap.DialTimeout = 10 * time.Second

    // Command timeout (for each IMAP command)
    imap.CommandTimeout = 30 * time.Second

    // Now commands will timeout if they take too long
    m, err := imap.New("user", "pass", "mail.server.com", 993)
    if err != nil {
        // Connection failed within 10 seconds
        panic(err)
    }
    defer m.Close()

    // This search will timeout after 30 seconds
    uids, err := m.GetUIDs("ALL")
    if err != nil {
        fmt.Printf("Command timed out or failed: %v\n", err)
    }
}
7. Complete Working Example
package main

import (
    "fmt"
    "log"
    "time"

    imap "github.com/BrianLeishman/go-imap"
)

func main() {
    // Configure the library
    imap.Verbose = false
    imap.RetryCount = 3
    imap.DialTimeout = 10 * time.Second
    imap.CommandTimeout = 30 * time.Second

    // Connect
    fmt.Println("Connecting to IMAP server...")
    m, err := imap.New("your-email@gmail.com", "your-password", "imap.gmail.com", 993)
    if err != nil {
        log.Fatalf("Connection failed: %v", err)
    }
    defer m.Close()

    // List folders
    fmt.Println("\n📁 Available folders:")
    folders, err := m.GetFolders()
    if err != nil {
        log.Fatalf("Failed to get folders: %v", err)
    }
    for _, folder := range folders {
        fmt.Printf("  - %s\n", folder)
    }

    // Select INBOX
    fmt.Println("\n📥 Selecting INBOX...")
    if err := m.SelectFolder("INBOX"); err != nil {
        log.Fatalf("Failed to select INBOX: %v", err)
    }

    // Get unread emails
    fmt.Println("\n🔍 Searching for unread emails...")
    unreadUIDs, err := m.GetUIDs("UNSEEN")
    if err != nil {
        log.Fatalf("Search failed: %v", err)
    }
    fmt.Printf("Found %d unread emails\n", len(unreadUIDs))

    // Fetch first 5 unread (or less)
    limit := 5
    if len(unreadUIDs) < limit {
        limit = len(unreadUIDs)
    }

    if limit > 0 {
        fmt.Printf("\n📧 Fetching first %d unread emails...\n", limit)
        emails, err := m.GetEmails(unreadUIDs[:limit]...)
        if err != nil {
            log.Fatalf("Failed to fetch emails: %v", err)
        }

        for uid, email := range emails {
            fmt.Printf("\n--- Email UID %d ---\n", uid)
            fmt.Printf("From: %s\n", email.From)
            fmt.Printf("Subject: %s\n", email.Subject)
            fmt.Printf("Date: %s\n", email.Sent.Format("Jan 2, 2006 3:04 PM"))
            fmt.Printf("Size: %.1f KB\n", float64(email.Size)/1024)

            if len(email.Text) > 100 {
                fmt.Printf("Preview: %.100s...\n", email.Text)
            } else if len(email.Text) > 0 {
                fmt.Printf("Preview: %s\n", email.Text)
            }

            if len(email.Attachments) > 0 {
                fmt.Printf("Attachments: %d\n", len(email.Attachments))
                for _, att := range email.Attachments {
                    fmt.Printf("  - %s (%.1f KB)\n", att.Name, float64(len(att.Content))/1024)
                }
            }

            // Mark first email as read
            if uid == unreadUIDs[0] {
                fmt.Printf("\n✓ Marking email %d as read...\n", uid)
                if err := m.MarkSeen(uid); err != nil {
                    fmt.Printf("Failed to mark as read: %v\n", err)
                }
            }
        }
    }

    // Get some statistics
    fmt.Println("\n📊 Mailbox Statistics:")
    allUIDs, _ := m.GetUIDs("ALL")
    seenUIDs, _ := m.GetUIDs("SEEN")
    flaggedUIDs, _ := m.GetUIDs("FLAGGED")

    fmt.Printf("  Total emails: %d\n", len(allUIDs))
    fmt.Printf("  Read emails: %d\n", len(seenUIDs))
    fmt.Printf("  Unread emails: %d\n", len(allUIDs)-len(seenUIDs))
    fmt.Printf("  Flagged emails: %d\n", len(flaggedUIDs))

    // Start IDLE monitoring for 10 seconds
    fmt.Println("\n👀 Monitoring for new emails (10 seconds)...")
    handler := &imap.IdleHandler{
        OnExists: func(e imap.ExistsEvent) {
            fmt.Printf("  📬 New email arrived! (message #%d)\n", e.MessageIndex)
        },
    }

    if err := m.StartIdle(handler); err == nil {
        time.Sleep(10 * time.Second)
        _ = m.StopIdle()
    }

    fmt.Println("\n✅ Done!")
}

/* Example Output:

Connecting to IMAP server...

📁 Available folders:
  - INBOX
  - Sent
  - Drafts
  - Trash
  - [Gmail]/All Mail
  - [Gmail]/Spam
  - [Gmail]/Starred
  - [Gmail]/Important

📥 Selecting INBOX...

🔍 Searching for unread emails...
Found 3 unread emails

📧 Fetching first 3 unread emails...

--- Email UID 1247 ---
From: notifications@github.com:GitHub
Subject: [org/repo] New issue: Bug in authentication flow (#123)
Date: Nov 11, 2024 2:15 PM
Size: 8.5 KB
Preview: User johndoe opened an issue: When trying to authenticate with OAuth2, the system returns a 401 error even with valid...
Attachments: 0

✓ Marking email 1247 as read...

--- Email UID 1248 ---
From: team@company.com:Team Update
Subject: Weekly Team Sync - Meeting Notes
Date: Nov 11, 2024 3:30 PM
Size: 12.3 KB
Preview: Hi team, Here are the notes from today's sync: 1. Project Alpha is on track for Dec release 2. Need volunteers for...
Attachments: 1
  - meeting-notes.pdf (156.2 KB)

--- Email UID 1249 ---
From: noreply@service.com:Service Alert
Subject: Your monthly report is ready
Date: Nov 11, 2024 4:45 PM
Size: 45.6 KB
Preview: Your monthly usage report for October 2024 is now available. View it in your dashboard or download the attached PDF...
Attachments: 2
  - october-report.pdf (523.1 KB)
  - usage-chart.png (89.3 KB)

📊 Mailbox Statistics:
  Total emails: 1532
  Read emails: 1530
  Unread emails: 2
  Flagged emails: 23

👀 Monitoring for new emails (10 seconds)...

✅ Done!
*/

Reconnect Behavior

When a command fails, the library closes the socket, reconnects, re‑authenticates (LOGIN or XOAUTH2), and restores the previously selected folder. You can tune retry count via imap.RetryCount.

TLS & Certificates

Connections are TLS by default. For servers with self‑signed certs you can set imap.TLSSkipVerify = true, but be aware this disables certificate validation and can expose you to man‑in‑the‑middle attacks. Prefer real certificates in production.

Server Compatibility

Tested against common providers such as Gmail and Office 365/Exchange. The client targets RFC 3501 and common extensions used for search, fetch, and move.

CI & Quality

This repo runs Go 1.25.1+ on CI with vet and race‑enabled tests. We also track documentation on pkg.go.dev and Go Report Card.

Contributing

Issues and PRs are welcome! If adding public APIs, please include short docs and examples. Make sure go vet and go test -race ./... pass locally.

License

MIT © Brian Leishman


Built With

Documentation

Overview

Package imap provides a simple, pragmatic IMAP client for Go.

It focuses on the handful of operations most applications need:

  • Connecting over TLS (STARTTLS not required)
  • Authenticating with LOGIN or XOAUTH2 (OAuth 2.0)
  • Selecting/Examining folders, searching (UID SEARCH), and fetching messages
  • Moving messages, setting flags, deleting + expunging
  • IMAP IDLE with callbacks for EXISTS/EXPUNGE/FETCH
  • Automatic reconnect with re-authentication and folder restore

The API is intentionally small and easy to adopt without pulling in a full IMAP stack. See the README for end‑to‑end examples and guidance.

Index

Constants

View Source
const (
	StateDisconnected = iota
	StateConnected
	StateSelected
	StateIdlePending
	StateIdling
	StateStoppingIdle
)

Connection state constants

View Source
const (
	IdleEventExists  = "EXISTS"
	IdleEventExpunge = "EXPUNGE"
	IdleEventFetch   = "FETCH"
)

IDLE event type constants

View Source
const (
	EDate uint8 = iota
	ESubject
	EFrom
	ESender
	EReplyTo
	ETo
	ECC
	EBCC
	EInReplyTo
	EMessageID
)

Email parsing constants

View Source
const (
	EEName uint8 = iota
	EESR
	EEMailbox
	EEHost
)
View Source
const (
	TimeFormat = "_2-Jan-2006 15:04:05 -0700"
)

Variables

View Source
var (
	AddSlashes    = strings.NewReplacer(`"`, `\"`)
	RemoveSlashes = strings.NewReplacer(`\"`, `"`)
)

String replacers for escaping/unescaping quotes

View Source
var CommandTimeout time.Duration

CommandTimeout defines how long to wait for a command to complete. Zero means no timeout.

View Source
var DialTimeout time.Duration

DialTimeout defines how long to wait when establishing a new connection. Zero means no timeout.

View Source
var RetryCount = 10
View Source
var SkipResponses = false

SkipResponses skips printing server responses in verbose mode

View Source
var TLSSkipVerify bool

TLSSkipVerify disables certificate verification when establishing new connections. Use with caution; skipping verification exposes the connection to man-in-the-middle attacks.

View Source
var Verbose = false

Verbose outputs every command and its response with the IMAP server

Functions

func GetTokenName

func GetTokenName(tokenType TType) string

GetTokenName returns the string name of a token type

func IsLiteral

func IsLiteral(b rune) bool

IsLiteral checks if a rune is valid for a literal token

func MakeIMAPLiteral added in v0.1.16

func MakeIMAPLiteral(s string) string

MakeIMAPLiteral generates IMAP literal syntax for non-ASCII strings. It returns a string in the format "{bytecount}\r\ntext" where bytecount is the number of bytes (not characters) in the input string. This is useful for search queries with non-ASCII characters. Example: MakeIMAPLiteral("тест") returns "{8}\r\nтест"

func SetLogger added in v0.1.17

func SetLogger(logger Logger)

SetLogger replaces the global logger used by the package. Passing nil restores the built-in slog logger.

func SetSlogLogger added in v0.1.17

func SetSlogLogger(logger *slog.Logger)

SetSlogLogger is a convenience helper for using a *slog.Logger directly.

Types

type Attachment

type Attachment struct {
	Name     string
	MimeType string
	Content  []byte
}

Attachment represents an email attachment

func (Attachment) String

func (a Attachment) String() string

String returns a formatted string representation of an Attachment

type Dialer

type Dialer struct {
	Folder    string
	ReadOnly  bool
	Username  string
	Password  string
	Host      string
	Port      int
	Connected bool
	ConnNum   int
	// contains filtered or unexported fields
}

Dialer represents an IMAP connection

func New

func New(username string, password string, host string, port int) (d *Dialer, err error)

New creates a new IMAP connection using username/password authentication

func NewWithOAuth2 added in v0.1.7

func NewWithOAuth2(username string, accessToken string, host string, port int) (d *Dialer, err error)

NewWithOAuth2 creates a new IMAP connection using OAuth2 authentication

func (*Dialer) Authenticate added in v0.1.7

func (d *Dialer) Authenticate(user string, accessToken string) (err error)

Authenticate performs XOAUTH2 authentication using an access token

func (*Dialer) CheckType

func (d *Dialer) CheckType(token *Token, acceptableTypes []TType, tks []*Token, loc string, v ...interface{}) (err error)

CheckType validates that a token is one of the acceptable types

func (*Dialer) Clone

func (d *Dialer) Clone() (d2 *Dialer, err error)

Clone creates a copy of the dialer with the same configuration

func (*Dialer) Close

func (d *Dialer) Close() (err error)

Close closes the IMAP connection

func (*Dialer) DeleteEmail added in v0.1.9

func (d *Dialer) DeleteEmail(uid int) (err error)

DeleteEmail marks an email for deletion

func (*Dialer) ExamineFolder added in v0.1.8

func (d *Dialer) ExamineFolder(folder string) (err error)

ExamineFolder selects a folder in read-only mode

func (*Dialer) Exec

func (d *Dialer) Exec(command string, buildResponse bool, retryCount int, processLine func(line []byte) error) (response string, err error)

Exec executes an IMAP command with retry logic and response building

func (*Dialer) Expunge added in v0.1.9

func (d *Dialer) Expunge() (err error)

Expunge permanently removes emails marked for deletion

func (*Dialer) GetEmails

func (d *Dialer) GetEmails(uids ...int) (emails map[int]*Email, err error)

GetEmails retrieves full email messages including body content

func (*Dialer) GetFolderStats added in v0.1.16

func (d *Dialer) GetFolderStats() ([]FolderStats, error)

GetFolderStats returns statistics for all folders

func (*Dialer) GetFolderStatsExcluding added in v0.1.16

func (d *Dialer) GetFolderStatsExcluding(excludedFolders []string) ([]FolderStats, error)

GetFolderStatsExcluding returns statistics for folders excluding specified ones

func (*Dialer) GetFolderStatsStartingFrom added in v0.1.16

func (d *Dialer) GetFolderStatsStartingFrom(startFolder string) ([]FolderStats, error)

GetFolderStatsStartingFrom returns statistics for folders starting from a specific one

func (*Dialer) GetFolderStatsStartingFromExcluding added in v0.1.16

func (d *Dialer) GetFolderStatsStartingFromExcluding(startFolder string, excludedFolders []string) ([]FolderStats, error)

GetFolderStatsStartingFromExcluding returns detailed statistics for folders with options

func (*Dialer) GetFolders

func (d *Dialer) GetFolders() (folders []string, err error)

GetFolders retrieves the list of available folders

func (*Dialer) GetLastNUIDs added in v0.1.20

func (d *Dialer) GetLastNUIDs(n int) ([]int, error)

GetLastNUIDs returns the N messages with the highest UIDs in the selected folder. This is useful for fetching the most recent messages.

Note: This method fetches all UIDs from the server and returns the last N. For mailboxes with many thousands of messages, consider using GetUIDs with a specific UID range if you know the approximate UID values you need.

Example:

// Get the 10 most recent messages
uids, err := conn.GetLastNUIDs(10)

func (*Dialer) GetOverviews

func (d *Dialer) GetOverviews(uids ...int) (emails map[int]*Email, err error)

GetOverviews retrieves email overview information (headers, flags, etc.)

func (*Dialer) GetTotalEmailCount

func (d *Dialer) GetTotalEmailCount() (count int, err error)

GetTotalEmailCount returns the total email count across all folders

func (*Dialer) GetTotalEmailCountExcluding

func (d *Dialer) GetTotalEmailCountExcluding(excludedFolders []string) (count int, err error)

GetTotalEmailCountExcluding returns total email count excluding specified folders

func (*Dialer) GetTotalEmailCountSafe added in v0.1.16

func (d *Dialer) GetTotalEmailCountSafe() (count int, folderErrors []error, err error)

GetTotalEmailCountSafe returns total email count with error handling per folder

func (*Dialer) GetTotalEmailCountSafeExcluding added in v0.1.16

func (d *Dialer) GetTotalEmailCountSafeExcluding(excludedFolders []string) (count int, folderErrors []error, err error)

GetTotalEmailCountSafeExcluding returns total email count excluding folders with error handling

func (*Dialer) GetTotalEmailCountSafeStartingFrom added in v0.1.16

func (d *Dialer) GetTotalEmailCountSafeStartingFrom(startFolder string) (count int, folderErrors []error, err error)

GetTotalEmailCountSafeStartingFrom returns total email count starting from folder with error handling

func (*Dialer) GetTotalEmailCountSafeStartingFromExcluding added in v0.1.16

func (d *Dialer) GetTotalEmailCountSafeStartingFromExcluding(startFolder string, excludedFolders []string) (count int, folderErrors []error, err error)

GetTotalEmailCountSafeStartingFromExcluding returns total email count with per-folder error handling

func (*Dialer) GetTotalEmailCountStartingFrom

func (d *Dialer) GetTotalEmailCountStartingFrom(startFolder string) (count int, err error)

GetTotalEmailCountStartingFrom returns total email count starting from a specific folder

func (*Dialer) GetTotalEmailCountStartingFromExcluding

func (d *Dialer) GetTotalEmailCountStartingFromExcluding(startFolder string, excludedFolders []string) (count int, err error)

GetTotalEmailCountStartingFromExcluding returns total email count with options for starting folder and exclusions

func (*Dialer) GetUIDs

func (d *Dialer) GetUIDs(search string) (uids []int, err error)

GetUIDs retrieves message UIDs matching a search criteria. The search parameter is passed directly to the IMAP UID SEARCH command. See RFC 3501 for search criteria syntax.

Common examples:

  • "ALL" - all messages
  • "UNSEEN" - unread messages
  • "SEEN" - read messages
  • "1:10" - UIDs 1 through 10
  • "SINCE 1-Jan-2024" - messages since a date

Note: For retrieving the N most recent messages, use GetLastNUIDs instead.

func (*Dialer) Login

func (d *Dialer) Login(username string, password string) (err error)

Login performs LOGIN authentication using username and password

func (*Dialer) MarkSeen added in v0.1.8

func (d *Dialer) MarkSeen(uid int) (err error)

MarkSeen marks an email as seen/read

func (*Dialer) MoveEmail added in v0.1.3

func (d *Dialer) MoveEmail(uid int, folder string) (err error)

MoveEmail moves an email to a different folder

func (*Dialer) ParseFetchResponse

func (d *Dialer) ParseFetchResponse(responseBody string) (records [][]*Token, err error)

ParseFetchResponse parses a multi-line FETCH response

func (*Dialer) Reconnect

func (d *Dialer) Reconnect() (err error)

Reconnect closes and reopens the IMAP connection with re-authentication

func (*Dialer) SelectFolder

func (d *Dialer) SelectFolder(folder string) (err error)

SelectFolder selects a folder in read-write mode

func (*Dialer) SetFlags added in v0.1.8

func (d *Dialer) SetFlags(uid int, flags Flags) (err error)

SetFlags sets message flags (seen, deleted, etc.)

func (*Dialer) StartIdle added in v0.1.5

func (d *Dialer) StartIdle(handler *IdleHandler) error

StartIdle starts IDLE monitoring with automatic reconnection and timeout handling

func (*Dialer) State added in v0.1.8

func (d *Dialer) State() int

State returns the current connection state with proper locking

func (*Dialer) StopIdle added in v0.1.8

func (d *Dialer) StopIdle() error

StopIdle stops the current IDLE session

type Email

type Email struct {
	Flags       []string
	Received    time.Time
	Sent        time.Time
	Size        uint64
	Subject     string
	UID         int
	MessageID   string
	From        EmailAddresses
	To          EmailAddresses
	ReplyTo     EmailAddresses
	CC          EmailAddresses
	BCC         EmailAddresses
	Text        string
	HTML        string
	Attachments []Attachment
}

Email represents an IMAP email message

func (Email) String

func (e Email) String() string

String returns a formatted string representation of an Email

type EmailAddresses

type EmailAddresses map[string]string

EmailAddresses represents a map of email addresses to display names

func (EmailAddresses) String

func (e EmailAddresses) String() string

String returns a formatted string representation of EmailAddresses

type ExistsEvent added in v0.1.5

type ExistsEvent struct {
	MessageIndex int
}

ExistsEvent represents an EXISTS event from IDLE

type ExpungeEvent added in v0.1.5

type ExpungeEvent struct {
	MessageIndex int
}

ExpungeEvent represents an EXPUNGE event from IDLE

type FetchEvent added in v0.1.5

type FetchEvent struct {
	MessageIndex int
	UID          uint32
	Flags        []string
}

FetchEvent represents a FETCH event from IDLE

type FlagSet added in v0.1.8

type FlagSet int

FlagSet represents the action to take on a flag

const (
	FlagUnset FlagSet = iota
	FlagAdd
	FlagRemove
)

type Flags added in v0.1.8

type Flags struct {
	Seen     FlagSet
	Answered FlagSet
	Flagged  FlagSet
	Deleted  FlagSet
	Draft    FlagSet
	Keywords map[string]bool
}

Flags represents standard IMAP message flags

type FolderStats added in v0.1.16

type FolderStats struct {
	Name   string
	Count  int
	MaxUID int
	Error  error
}

FolderStats represents statistics for a folder

type IdleHandler added in v0.1.5

type IdleHandler struct {
	OnExists  func(event ExistsEvent)
	OnExpunge func(event ExpungeEvent)
	OnFetch   func(event FetchEvent)
}

IdleHandler provides callbacks for IDLE events

type Logger added in v0.1.17

type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
	WithAttrs(args ...any) Logger
}

Logger defines the minimal logging interface used by the IMAP client.

Implementations must be safe for concurrent use.

func SlogLogger added in v0.1.17

func SlogLogger(logger *slog.Logger) Logger

SlogLogger adapts a *slog.Logger to the Logger interface.

type TType

type TType uint8

TType represents the type of an IMAP token

const (
	TUnset TType = iota
	TAtom
	TNumber
	TLiteral
	TQuoted
	TNil
	TContainer
)

type Token

type Token struct {
	Type   TType
	Str    string
	Num    int
	Tokens []*Token
}

Token represents a parsed IMAP token

func (Token) String

func (t Token) String() string

String returns a string representation of a Token

Directories

Path Synopsis
examples
error_handling command
fetch_emails command
folders command
idle_monitoring command
literal_search command
search command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL