// Copyright (c) 2025 H0llyW00dzZ All rights reserved. // // By accessing or using this software, you agree to be bound by the terms // of the License Agreement, which you can find at LICENSE files. package client import ( "fmt" "time" "github.com/emersion/go-imap" "github.com/valyala/bytebufferpool" ) const ( // KeyID is the key for the message ID KeyID = "id" // KeyFrom is the key for the sender's address KeyFrom = "from" // KeySubject is the key for the message subject KeySubject = "subject" // KeyBody is the key for the message body KeyBody = "body" // KeySender is the key for the sender's address in the envelope KeySender = "sender" // KeyReplyTo is the key for the reply-to addresses KeyReplyTo = "replyTo" // KeyTo is the key for the 'To' addresses KeyTo = "to" // KeyCc is the key for the 'Cc' addresses KeyCc = "cc" // KeyBcc is the key for the 'Bcc' addresses KeyBcc = "bcc" // KeyInReplyTo is the key for the in-reply-to identifier KeyInReplyTo = "inReplyTo" // KeyDate is the key for the message date KeyDate = "date" ) // ListMessages lists the messages in the specified mailbox based on the MessageConfig func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config MessageConfig) ([]map[string]any, error) { if c.client == nil { return nil, ErrorsClientIsNotConnected } mbox, err := c.selectMailbox(mailbox) if err != nil { return nil, err } seqset := c.createSeqSet(mbox.Messages, numMessages) items := c.getFetchItems(config) messages := make(chan *imap.Message, numMessages) done := make(chan error, 1) go func() { done <- c.client.Fetch(seqset, items, messages) }() results, err := c.processMessages(messages, config) if err != nil { return nil, err } if err := <-done; err != nil { return nil, fmt.Errorf("failed to fetch messages: %v", err) } return results, nil } // selectMailbox selects the specified mailbox and returns its status. // It returns an error if the mailbox cannot be selected. func (c *IMAPClient) selectMailbox(mailbox string) (*imap.MailboxStatus, error) { mbox, err := c.client.Select(mailbox, false) if err != nil { return nil, fmt.Errorf("failed to select %s: %v", mailbox, err) } return mbox, nil } // createSeqSet creates a sequence set for fetching messages based on the total and desired number of messages. func (c *IMAPClient) createSeqSet(totalMessages, numMessages uint32) *imap.SeqSet { from := uint32(1) to := totalMessages if totalMessages > numMessages { from = totalMessages - numMessages + 1 } seqset := new(imap.SeqSet) seqset.AddRange(from, to) return seqset } // getFetchItems determines which parts of the message to fetch based on the MessageConfig. func (c *IMAPClient) getFetchItems(config MessageConfig) []imap.FetchItem { items := []imap.FetchItem{imap.FetchEnvelope} if config.GrabBody { items = append(items, imap.FetchItem("BODY.PEEK[]")) } return items } // processMessages processes fetched messages and extracts details based on the MessageConfig. func (c *IMAPClient) processMessages(messages chan *imap.Message, config MessageConfig) ([]map[string]any, error) { var results []map[string]any for msg := range messages { details := c.extractDetails(msg, config) if details != nil { results = append(results, details) } } return results, nil } // extractDetails extracts message details based on the MessageConfig. func (c *IMAPClient) extractDetails(msg *imap.Message, config MessageConfig) map[string]any { details := make(map[string]any) // Add message ID if configured c.addIfNotEmpty(details, KeyID, config.GrabID, msg.Envelope.MessageId) // Add 'From' addresses if configured c.addAddresses(details, KeyFrom, config.GrabFrom, msg.Envelope.From) // Add subject if configured c.addIfNotEmpty(details, KeySubject, config.GrabSubject, msg.Envelope.Subject) // Add body if configured c.addBody(details, KeyBody, config.GrabBody, msg.Body) // Add sender if configured c.addAddresses(details, KeySender, config.GrabSender, msg.Envelope.Sender) // Add reply-to addresses if configured c.addAddresses(details, KeyReplyTo, config.GrabReplyTo, msg.Envelope.ReplyTo) // Add 'To' addresses if configured c.addAddresses(details, KeyTo, config.GrabTo, msg.Envelope.To) // Add 'Cc' addresses if configured c.addAddresses(details, KeyCc, config.GrabCc, msg.Envelope.Cc) // Add 'Bcc' addresses if configured c.addAddresses(details, KeyBcc, config.GrabBcc, msg.Envelope.Bcc) // Add in-reply-to ID if configured c.addIfNotEmpty(details, KeyInReplyTo, config.GrabInReplyTo, msg.Envelope.InReplyTo) // Add date if configured c.addIfNotZero(details, KeyDate, config.GrabDate, msg.Envelope.Date) return details } // addIfNotEmpty adds a value to details if it's not empty or nil and grabbing is enabled. func (c *IMAPClient) addIfNotEmpty(details map[string]any, key string, grab bool, value string) { if grab && value != "" && value != "" { details[key] = value } } // addAddresses adds email addresses to details if grabbing is enabled. func (c *IMAPClient) addAddresses(details map[string]any, key string, grab bool, addresses []*imap.Address) { if grab && len(addresses) > 0 { details[key] = c.extractAddresses(addresses) } } // addBody adds the message body to details if grabbing is enabled. func (c *IMAPClient) addBody(details map[string]any, key string, grab bool, body map[*imap.BodySectionName]imap.Literal) { if grab { content, err := c.extractBody(body) if err == nil && content != "" { details[key] = content } } } // addIfNotZero adds a date to details if it's not zero and grabbing is enabled. func (c *IMAPClient) addIfNotZero(details map[string]any, key string, grab bool, date time.Time) { if grab && !date.IsZero() { details[key] = date } } // extractAddresses extracts email addresses from a list of IMAP addresses. func (c *IMAPClient) extractAddresses(addresses []*imap.Address) []string { var from []string for _, addr := range addresses { from = append(from, addr.Address()) } return from } // extractBody reads and returns the body content from the message body literals. func (c *IMAPClient) extractBody(body map[*imap.BodySectionName]imap.Literal) (string, error) { for _, literal := range body { buf := bytebufferpool.Get() if _, err := buf.ReadFrom(literal); err == nil { result := buf.String() buf.Reset() // Reset the buffer before returning it to the pool. bytebufferpool.Put(buf) return result, nil } bytebufferpool.Put(buf) return "", ErrorsFailedToReadBody } return "", nil } // ListUserMessages lists messages for a specific user based on the MessageConfig func (m *MultiUserIMAP) ListUserMessages(username, mailbox string, numMessages uint32, config MessageConfig) ([]map[string]any, error) { m.mu.Lock() defer m.mu.Unlock() client, exists := m.clients[username] if !exists { return nil, fmt.Errorf("user not found: %s", username) } return client.ListMessages(mailbox, numMessages, config) }