Documentation
¶
Index ¶
- Variables
- func ConnectToIMAP(server string, useTLS bool) (*client.Client, error)
- func ExtractReferences(imapMsg *imap.Message) []string
- func ExtractStableThreadID(envelope *imap.Envelope) string
- func FetchFullMessage(c *client.Client, uid uint32) (*imap.Message, error)
- func FetchMessageHeaders(c *client.Client, uids []uint32) ([]*imap.Message, error)
- func ListFolders(c *client.Client) ([]*models.Folder, error)
- func Login(c *client.Client, username, password string) error
- func ParseMessage(imapMsg *imap.Message, threadID, userID, folderName string) (*models.Message, error)
- func ParseSearchQuery(query string) (*imap.SearchCriteria, string, error)
- func RunThreadCommand(c *client.Client) ([]*sortthread.Thread, error)
- func SearchUIDsSince(c *client.Client, minUID uint32) ([]uint32, error)
- type ClientWrapper
- type IMAPClient
- type IMAPPool
- type IMAPService
- type ListenerClient
- type MessageToSync
- type Pool
- func (p *Pool) Close()
- func (p *Pool) GetListenerConnection(userID, server, username, password string) (ListenerClient, error)
- func (p *Pool) RemoveClient(userID string)
- func (p *Pool) RemoveListenerConnection(userID string)
- func (p *Pool) WithClient(userID, server, username, password string, fn func(IMAPClient) error) error
- type Service
- func (s *Service) Close()
- func (s *Service) FetchAttachmentContent(ctx context.Context, userID, folderName string, imapUID int64, ...) ([]byte, string, string, error)
- func (s *Service) Search(ctx context.Context, userID string, query string, page, limit int) ([]*models.Thread, int, error)
- func (s *Service) ShouldSyncFolder(ctx context.Context, userID, folderName string) (bool, error)
- func (s *Service) StartIdleListener(ctx context.Context, userID string, hub *websocket.Hub)
- func (s *Service) SyncFullMessage(ctx context.Context, userID, folderName string, imapUID int64) error
- func (s *Service) SyncFullMessages(ctx context.Context, userID string, messages []MessageToSync) error
- func (s *Service) SyncThreadsForFolder(ctx context.Context, userID, folderName string) error
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidSearchQuery = errors.New("invalid search query")
ErrInvalidSearchQuery is returned when a search query cannot be parsed.
Functions ¶
func ConnectToIMAP ¶
ConnectToIMAP connects to the IMAP server with a 5-second timeout. useTLS: true for production (TLS), false for tests (non-TLS).
func ExtractReferences ¶
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 ¶
ExtractStableThreadID extracts the stable thread ID from a message. This uses the Message-ID header of the root message.
func FetchFullMessage ¶
FetchFullMessage fetches the full message body for the given UID. First fetches headers and body structure, then fetches the actual body content.
func FetchMessageHeaders ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
RemoveClient removes all connections (worker and listener) for a user from the pool.
func (*Pool) RemoveListenerConnection ¶
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 ¶
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 ¶
ShouldSyncFolder checks if we should sync the folder based on cache TTL.
func (*Service) StartIdleListener ¶
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 ¶
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.