imap/client/list_messages.go

214 lines
6.8 KiB
Go
Raw Normal View History

// 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 != "<nil>" {
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)
}