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.
DialTimeout, CommandTimeout)LOGIN and XOAUTH2UID SEARCH helpers, type-safe SearchBuilder with fluent API, RFC 3501 literal syntax for non-ASCII textEXISTS, EXPUNGE, FETCHgo get github.com/BrianLeishman/go-imap
Requires Go 1.25+ (see go.mod).
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))
}
// 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) }
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.
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
basic_connection - Basic LOGIN authentication and connection setupoauth2_connection - OAuth 2.0 (XOAUTH2) authentication for Gmail/Office 365folders - List, create, rename, delete folders; select/examine; get email countssearch - Search emails with raw criteria and the type-safe SearchBuilderliteral_search - Search with non-ASCII characters using RFC 3501 literal syntaxfetch_emails - Fetch email headers (fast) and full content with attachments (slower)email_operations - Move, copy, append emails; set/remove flags; delete and expungeidle_monitoring - Real-time email notifications with IDLEerror_handling - Robust error handling, reconnection, and timeout configurationcomplete_example - Full-featured example combining multiple operations// 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
// Create, rename, and delete folders
err = m.CreateFolder("INBOX/Projects")
if err != nil { panic(err) }
err = m.RenameFolder("INBOX/Projects", "INBOX/Archive")
if err != nil { panic(err) }
err = m.DeleteFolder("INBOX/Archive")
if err != nil { panic(err) }
// 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
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.
[Gmail] folder often returns "NO [NONEXISTENT] Unknown Mailbox"// 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)
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")
}
}
}
```go // 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
// Cheaper method to retrieve the latest UID (requires RFC-4731; // not all servers support this — check the error). maxUID, _ := m.GetMaxUID() // Highest UID only
// 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
$ claude mcp add go-imap \
-- python -m otcore.mcp_server <graph>