imap

package
v0.0.0-...-056225c Latest Latest
Warning

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

Go to latest
Published: Dec 3, 2025 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidSearchQuery = errors.New("invalid search query")

ErrInvalidSearchQuery is returned when a search query cannot be parsed.

Functions

func ConnectToIMAP

func ConnectToIMAP(server string, useTLS bool) (*client.Client, error)

ConnectToIMAP connects to the IMAP server with a 5-second timeout. useTLS: true for production (TLS), false for tests (non-TLS).

func ExtractReferences

func ExtractReferences(imapMsg *imap.Message) []string

ExtractReferences extracts the References header from an IMAP message. The References header contains a space-separated list of Message-IDs from the root message to the parent (the message being replied to). Returns nil if no References header is found.

func ExtractStableThreadID

func ExtractStableThreadID(envelope *imap.Envelope) string

ExtractStableThreadID extracts the stable thread ID from a message. This uses the Message-ID header of the root message.

func FetchFullMessage

func FetchFullMessage(c *client.Client, uid uint32) (*imap.Message, error)

FetchFullMessage fetches the full message body for the given UID. First fetches headers and body structure, then fetches the actual body content.

func FetchMessageHeaders

func FetchMessageHeaders(c *client.Client, uids []uint32) ([]*imap.Message, error)

FetchMessageHeaders fetches message headers for the given UIDs. Returns envelope, body structure, flags, UID, and References header for each message. The References header is stored in the Body map under the "HEADER.FIELDS (REFERENCES)" key.

func ListFolders

func ListFolders(c *client.Client) ([]*models.Folder, error)

ListFolders lists all folders on the IMAP server with their roles determined by SPECIAL-USE attributes (RFC 6154). Returns an error if the server doesn't support SPECIAL-USE extension.

func Login

func Login(c *client.Client, username, password string) error

Login authenticates with the IMAP server.

func ParseMessage

func ParseMessage(imapMsg *imap.Message, threadID, userID, folderName string) (*models.Message, error)

ParseMessage converts an IMAP message to our Message model. Extracts headers, flags, and body (if available). Body parsing errors are logged but don't fail the parse.

func ParseSearchQuery

func ParseSearchQuery(query string) (*imap.SearchCriteria, string, error)

ParseSearchQuery parses a Gmail-like search query into IMAP SearchCriteria. Returns the parsed criteria, extracted folder name (or empty), and error. Supported syntax:

  • from:george → criteria.Header["From"] = "george"
  • to:alice → criteria.Header["To"] = "alice"
  • subject:meeting → criteria.Header["Subject"] = "meeting"
  • after:2025-01-01 → criteria.Since = time.Date(...)
  • before:2025-12-31 → criteria.Before = time.Date(...)
  • folder:Inbox or label:Inbox → extract folder name (returned separately)
  • Plain text → criteria.Text = []string{text}
  • Combinations: from:george after:2025-01-01 cabbage

func RunThreadCommand

func RunThreadCommand(c *client.Client) ([]*sortthread.Thread, error)

RunThreadCommand runs the THREAD command and returns the thread structure. Uses the REFERENCES algorithm to build thread relationships.

func SearchUIDsSince

func SearchUIDsSince(c *client.Client, minUID uint32) ([]uint32, error)

SearchUIDsSince searches for all UIDs greater than or equal to the given UID. This is used for incremental sync to find only new messages.

Performance note: This function fetches all UIDs and filters them client-side. While IMAP supports UID SEARCH with ranges (e.g., "UID minUID:*"), the go-imap library's SearchCriteria doesn't expose this capability directly. The current approach is acceptable because: 1. We're only fetching UID numbers (not message content), which is fast 2. Client-side filtering is efficient for typical mailbox sizes 3. Most mailboxes have < 100k messages, making this approach practical

For very large mailboxes (> 1M messages), consider: - Using IMAP's native UID SEARCH with ranges if go-imap adds support - Implementing batch fetching with pagination - Using server-side filtering if the IMAP server supports extensions

Types

type ClientWrapper

type ClientWrapper struct {
	// contains filtered or unexported fields
}

ClientWrapper wraps a go-imap client.Client to implement IMAPClient interface.

func (*ClientWrapper) ListFolders

func (w *ClientWrapper) ListFolders() ([]*models.Folder, error)

ListFolders lists all folders on the IMAP server with their roles determined by SPECIAL-USE attributes.

type IMAPClient

type IMAPClient interface {
	// ListFolders lists all folders on the IMAP server with their roles determined by SPECIAL-USE attributes.
	ListFolders() ([]*models.Folder, error)
}

IMAPClient defines the interface for IMAP client operations needed by handlers. This allows handlers to be tested with mock implementations. Note: The stutter in the naming is intentional because go-imap already has a client.Client.

type IMAPPool

type IMAPPool interface {
	// WithClient gets an IMAP client for a user and calls the provided function with it.
	// The client is automatically released when the function returns, ensuring worker slots
	// are freed promptly. This is the safe way to use the pool - it's impossible to forget
	// to release the client.
	WithClient(userID, server, username, password string, fn func(IMAPClient) error) error

	// RemoveClient removes a client from the pool (useful when a connection is broken).
	RemoveClient(userID string)

	// Close closes all connections in the pool.
	Close()

	// GetListenerConnection gets or creates a dedicated listener client for IDLE.
	// Returns a locked client that must be unlocked by the caller.
	GetListenerConnection(userID, server, username, password string) (ListenerClient, error)

	// RemoveListenerConnection removes a listener connection from the pool.
	RemoveListenerConnection(userID string)
}

IMAPPool defines the interface for the IMAP connection pool. This allows handlers to be tested with mock implementations. Note: The stutter in the naming is intentional because we have a struct called Pool.

type IMAPService

type IMAPService interface {
	// ShouldSyncFolder checks if we should sync the folder based on cache TTL.
	ShouldSyncFolder(ctx context.Context, userID, folderName string) (bool, error)

	// SyncThreadsForFolder syncs threads from IMAP for a specific folder.
	SyncThreadsForFolder(ctx context.Context, userID, folderName string) error

	// SyncFullMessage syncs the full message body from IMAP.
	SyncFullMessage(ctx context.Context, userID, folderName string, imapUID int64) error

	// SyncFullMessages syncs multiple message bodies from IMAP in a batch.
	// Messages are grouped by folder and synced efficiently.
	SyncFullMessages(ctx context.Context, userID string, messages []MessageToSync) error

	// Search searches for threads matching the query.
	// Returns threads, total count, and error.
	Search(ctx context.Context, userID string, query string, page, limit int) ([]*models.Thread, int, error)

	// StartIdleListener runs an IMAP IDLE loop for a user and pushes events to the WebSocket hub.
	// This function blocks until the context is canceled.
	StartIdleListener(ctx context.Context, userID string, hub *websocket.Hub)

	// FetchAttachmentContent fetches attachment content directly from IMAP without storing it.
	// It identifies the attachment by content ID (for inline) or filename (for regular attachments).
	// Returns the content bytes, MIME type, and filename.
	FetchAttachmentContent(ctx context.Context, userID, folderName string, imapUID int64, contentID, filename string) ([]byte, string, string, error)

	// Close closes the service and cleans up connections.
	Close()
}

IMAPService defines the interface for IMAP operations. This interface allows handlers to be tested with mock implementations. Note: The stutter in the naming is intentional because we have a struct called Service.

type ListenerClient

type ListenerClient interface {
	// Lock acquires the mutex for thread-safe access to the underlying client.
	Lock()
	// Unlock releases the mutex.
	Unlock()
	// GetClient returns the underlying IMAP client.
	// Caller must hold the lock before calling this.
	GetClient() *client.Client
}

ListenerClient defines the interface for listener client operations. This allows the IDLE feature to work with the thread-safe wrapper without exposing implementation details.

type MessageToSync

type MessageToSync struct {
	FolderName string
	IMAPUID    int64
}

MessageToSync represents a message that needs to be synced.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool manages IMAP connections per user. Supports two types of connections: - Worker connections: 1-3 connections per user for API handlers (SEARCH, FETCH, STORE) - Listener connections: 1 dedicated connection per user for IDLE command

Thread safety: Each connection is wrapped with a mutex to ensure thread-safe access. Multiple goroutines can use different connections concurrently, but access to the same connection is serialized.

func NewPool

func NewPool() *Pool

NewPool creates a new IMAP connection pool with the default worker limit.

func NewPoolWithMaxWorkers

func NewPoolWithMaxWorkers(maxWorkers int) *Pool

NewPoolWithMaxWorkers creates a new IMAP connection pool with a configurable maximum number of worker connections per user.

func (*Pool) Close

func (p *Pool) Close()

Close closes all connections in the pool and stops the cleanup goroutine.

func (*Pool) GetListenerConnection

func (p *Pool) GetListenerConnection(userID, server, username, password string) (ListenerClient, error)

GetListenerConnection gets or creates a listener client for a user. Listener clients are dedicated clients for IDLE command. Returns a locked client that must be unlocked by the caller. Thread-safe: uses double-check locking pattern.

func (*Pool) RemoveClient

func (p *Pool) RemoveClient(userID string)

RemoveClient removes all connections (worker and listener) for a user from the pool.

func (*Pool) RemoveListenerConnection

func (p *Pool) RemoveListenerConnection(userID string)

RemoveListenerConnection removes a listener connection from the pool.

func (*Pool) WithClient

func (p *Pool) WithClient(userID, server, username, password string, fn func(IMAPClient) error) error

WithClient gets an IMAP client for a user and calls the provided function with it. The client is automatically released when the function returns. Implements IMAPPool interface.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service handles IMAP operations and caching. The IMAP pool is injected so that a single shared pool can be used across handlers and services, ensuring per-user connection limits are enforced consistently.

func NewService

func NewService(dbPool *pgxpool.Pool, imapPool IMAPPool, encryptor *crypto.Encryptor) *Service

NewService creates a new IMAP service.

func NewServiceWithThreadCountUpdater

func NewServiceWithThreadCountUpdater(dbPool *pgxpool.Pool, imapPool IMAPPool, encryptor *crypto.Encryptor, updater db.ThreadCountUpdater) *Service

NewServiceWithThreadCountUpdater creates a new IMAP service with a custom thread count updater. This is primarily for testing purposes.

func (*Service) Close

func (s *Service) Close()

Close closes the service and cleans up connections.

func (*Service) FetchAttachmentContent

func (s *Service) FetchAttachmentContent(ctx context.Context, userID, folderName string, imapUID int64, contentID, filename string) ([]byte, string, string, error)

FetchAttachmentContent fetches attachment content directly from IMAP without storing it. It identifies the attachment by content ID (for inline) or filename (for regular attachments).

func (*Service) Search

func (s *Service) Search(ctx context.Context, userID string, query string, page, limit int) ([]*models.Thread, int, error)

Search searches for threads matching the query in the specified folder. Supports Gmail-like syntax via ParseSearchQuery (from:, to:, subject:, after:, before:, folder:, label:). If no folder is specified in the query, defaults to INBOX. Returns threads sorted by latest sent_at (newest first), total count, and error. Note: Error handling tests for getClientAndSelectFolder, UidSearch, and FetchMessageHeaders require complex IMAP server mocking and are covered through integration tests.

func (*Service) ShouldSyncFolder

func (s *Service) ShouldSyncFolder(ctx context.Context, userID, folderName string) (bool, error)

ShouldSyncFolder checks if we should sync the folder based on cache TTL.

func (*Service) StartIdleListener

func (s *Service) StartIdleListener(ctx context.Context, userID string, hub *websocket.Hub)

StartIdleListener runs an IMAP IDLE loop for a user and pushes new email events to the Hub. It listens on the INBOX folder only. This function blocks until the context is canceled.

func (*Service) SyncFullMessage

func (s *Service) SyncFullMessage(ctx context.Context, userID, folderName string, imapUID int64) error

SyncFullMessage syncs the full message body from IMAP.

func (*Service) SyncFullMessages

func (s *Service) SyncFullMessages(ctx context.Context, userID string, messages []MessageToSync) error

SyncFullMessages syncs multiple message bodies from IMAP in a batch. It groups messages by folder and syncs them efficiently to reduce network calls. Thread-safe: Each folder selection uses a locked connection from the pool, ensuring that concurrent syncs for the same user use different connections or are serialized.

func (*Service) SyncThreadsForFolder

func (s *Service) SyncThreadsForFolder(ctx context.Context, userID, folderName string) error

SyncThreadsForFolder syncs threads from IMAP for a specific folder. Uses incremental sync if possible (only syncs new messages since last sync). After syncing, it completes thread chains by fetching referenced messages from other folders.

Jump to

Keyboard shortcuts

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