Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
4c74ebc1c7 | |||
7a43b90f67 | |||
5cab29c344 | |||
e129c63b37 | |||
289fc877ee | |||
2dab893c8e | |||
f22bed1f12 | |||
1574f865c9 | |||
d17022fb85 | |||
3e1a7445f6 |
60
README.md
60
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/git.b0zal.io/H0llyW00dzZ/imap.svg)](https://pkg.go.dev/git.b0zal.io/H0llyW00dzZ/imap) [![Go Report Card](https://goreportcard.com/badge/git.b0zal.io/H0llyW00dzZ/imap)](https://goreportcard.com/report/git.b0zal.io/H0llyW00dzZ/imap)
|
||||
|
||||
This package provides a simple interface to manage IMAP connections for single or multiple users. It allows you to connect to an IMAP server, list messages, export messages, and manage multiple user accounts.
|
||||
This package provides a simple interface to manage IMAP connections for single or multiple users. It allows you to connect to an IMAP server, list messages, export messages, manage multiple user accounts, and list available mailboxes.
|
||||
|
||||
## Features
|
||||
|
||||
@ -10,6 +10,7 @@ This package provides a simple interface to manage IMAP connections for single o
|
||||
- List messages in a specified mailbox
|
||||
- Export messages to various formats
|
||||
- Manage multiple users with separate IMAP clients
|
||||
- **New**: List all available mailboxes
|
||||
|
||||
## Installation
|
||||
|
||||
@ -18,6 +19,8 @@ To install the package, use `go get`:
|
||||
```bash
|
||||
go get git.b0zal.io/H0llyW00dzZ/imap
|
||||
```
|
||||
> [!NOTE]
|
||||
> If you're in `Indonesia` and using an internet provider like `Telkom (known as Indihome)`, and you encounter issues installing the package, try using a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network).
|
||||
|
||||
## Usage
|
||||
|
||||
@ -49,6 +52,13 @@ func main() {
|
||||
}
|
||||
defer imapClient.Disconnect()
|
||||
|
||||
// New: List all available mailboxes
|
||||
mailboxes, err := imapClient.ListMailboxes()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list mailboxes: %v", err)
|
||||
}
|
||||
fmt.Println("Mailboxes:", mailboxes)
|
||||
|
||||
messageConfig := client.MessageConfig{
|
||||
GrabID: true,
|
||||
GrabFrom: true,
|
||||
@ -91,6 +101,17 @@ func main() {
|
||||
}
|
||||
defer multiUserIMAP.DisconnectUser("user1@example.com")
|
||||
|
||||
// New: List all available mailboxes for user1
|
||||
client, err := multiUserIMAP.GetClient("user1@example.com")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get client for user1: %v", err)
|
||||
}
|
||||
mailboxes, err := client.ListMailboxes()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list mailboxes for user1: %v", err)
|
||||
}
|
||||
fmt.Println("User1 Mailboxes:", mailboxes)
|
||||
|
||||
messageConfig := client.MessageConfig{
|
||||
GrabID: true,
|
||||
GrabFrom: true,
|
||||
@ -159,6 +180,43 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### List All Available Mailboxes
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.b0zal.io/H0llyW00dzZ/imap/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := &client.Config{
|
||||
Username: "user@example.com",
|
||||
Password: "password",
|
||||
Server: "imap.example.com:993",
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
imapClient := client.NewIMAP(config)
|
||||
|
||||
err := imapClient.Connect()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer imapClient.Disconnect()
|
||||
|
||||
// List all available mailboxes
|
||||
mailboxes, err := imapClient.ListMailboxes()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list mailboxes: %v", err)
|
||||
}
|
||||
fmt.Println("Mailboxes:", mailboxes)
|
||||
}
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- Implement functionality to send emails.
|
||||
|
34
client/cert_test.pem
Normal file
34
client/cert_test.pem
Normal file
@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
JRNGkalOfg==
|
||||
-----END CERTIFICATE-----
|
@ -5,7 +5,11 @@
|
||||
|
||||
package client
|
||||
|
||||
import "github.com/emersion/go-imap/client"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap/client"
|
||||
)
|
||||
|
||||
// Config holds the configuration for the IMAP client
|
||||
type Config struct {
|
||||
@ -17,10 +21,17 @@ type Config struct {
|
||||
|
||||
// MessageConfig defines what parts of the message to retrieve
|
||||
type MessageConfig struct {
|
||||
GrabID bool
|
||||
GrabFrom bool
|
||||
GrabSubject bool
|
||||
GrabBody bool
|
||||
GrabID bool
|
||||
GrabFrom bool
|
||||
GrabSubject bool
|
||||
GrabBody bool
|
||||
GrabSender bool
|
||||
GrabReplyTo bool
|
||||
GrabTo bool
|
||||
GrabCc bool
|
||||
GrabBcc bool
|
||||
GrabInReplyTo bool
|
||||
GrabDate bool
|
||||
}
|
||||
|
||||
// IMAPClient represents an IMAP client
|
||||
@ -28,3 +39,11 @@ type IMAPClient struct {
|
||||
config *Config
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrorsClientIsNotConnected is returned when an operation is attempted on a client that is not connected.
|
||||
ErrorsClientIsNotConnected = errors.New("client is not connected")
|
||||
|
||||
// ErrorsFailedToReadBody is returned when there is a failure in reading the body of a message.
|
||||
ErrorsFailedToReadBody = errors.New("failed to read body")
|
||||
)
|
||||
|
@ -5,13 +5,14 @@
|
||||
|
||||
// Package client provides a simple interface to manage IMAP connections
|
||||
// for single or multiple users. It allows you to connect to an IMAP server,
|
||||
// list messages, export messages, and manage multiple user accounts.
|
||||
// list messages, export messages, manage multiple user accounts, and list available mailboxes.
|
||||
//
|
||||
// # Features
|
||||
// - Connect and disconnect from an IMAP server
|
||||
// - List messages in a specified mailbox
|
||||
// - Export messages to various formats
|
||||
// - Manage multiple users with separate IMAP clients
|
||||
// - List all available mailboxes
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
@ -59,6 +60,40 @@
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// List All Available Mailboxes Example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "fmt"
|
||||
// "log"
|
||||
//
|
||||
// "git.b0zal.io/H0llyW00dzZ/imap/client"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// config := &client.Config{
|
||||
// Username: "user@example.com",
|
||||
// Password: "password",
|
||||
// Server: "imap.example.com:993",
|
||||
// InsecureSkipVerify: true,
|
||||
// }
|
||||
//
|
||||
// imapClient := client.NewIMAP(config)
|
||||
//
|
||||
// err := imapClient.Connect()
|
||||
// if err != nil {
|
||||
// log.Fatalf("Failed to connect: %v", err)
|
||||
// }
|
||||
// defer imapClient.Disconnect()
|
||||
//
|
||||
// mailboxes, err := imapClient.ListMailboxes()
|
||||
// if err != nil {
|
||||
// log.Fatalf("Failed to list mailboxes: %v", err)
|
||||
// }
|
||||
// fmt.Println("Mailboxes:", mailboxes)
|
||||
// }
|
||||
//
|
||||
// Multiple Users Example:
|
||||
//
|
||||
// package main
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
// ExportMessagesTo uses the provided exporter to write messages to the writer
|
||||
func (c *IMAPClient) ExportMessagesTo(writer io.Writer, exporter export.Exporter, mailbox string, numMessages uint32, config MessageConfig) error {
|
||||
if c.client == nil {
|
||||
return fmt.Errorf("client is not connected")
|
||||
return ErrorsClientIsNotConnected
|
||||
}
|
||||
|
||||
// Fetch messages based on the provided MessageConfig and number of messages
|
||||
|
154
client/imap_test.go
Normal file
154
client/imap_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"git.b0zal.io/H0llyW00dzZ/imap/client"
|
||||
"github.com/emersion/go-imap/backend/memory"
|
||||
"github.com/emersion/go-imap/server"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupTestServer() (net.Listener, *server.Server) {
|
||||
// Create a memory backend
|
||||
be := memory.New()
|
||||
|
||||
// Create a new IMAP server with TLS configuration
|
||||
s := server.New(be)
|
||||
s.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: true, // For testing purposes
|
||||
Certificates: []tls.Certificate{loadTLSCertificate()},
|
||||
}
|
||||
|
||||
// Listen on a random port
|
||||
listener, err := tls.Listen("tcp", "localhost:0", s.TLSConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Start the server
|
||||
go func() {
|
||||
if err := s.Serve(listener); err != nil {
|
||||
log.Println("Server stopped:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return listener, s
|
||||
}
|
||||
|
||||
func loadTLSCertificate() tls.Certificate {
|
||||
cert, err := tls.LoadX509KeyPair("cert_test.pem", "key_test.pem")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load TLS certificate:", err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func TestIMAPClient(t *testing.T) {
|
||||
listener, srv := setupTestServer()
|
||||
defer listener.Close()
|
||||
defer srv.Close() // Ensure the server is closed properly
|
||||
|
||||
// Use the existing user in the memory backend
|
||||
config := &client.Config{
|
||||
Username: "username", // Use the predefined username
|
||||
Password: "password", // Use the predefined password
|
||||
Server: listener.Addr().String(),
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
imapClient := client.NewIMAP(config)
|
||||
|
||||
// Test connection
|
||||
err := imapClient.Connect()
|
||||
assert.NoError(t, err, "Failed to connect")
|
||||
|
||||
// Test disconnection
|
||||
err = imapClient.Disconnect()
|
||||
assert.NoError(t, err, "Failed to disconnect")
|
||||
}
|
||||
|
||||
func TestListMessages(t *testing.T) {
|
||||
listener, srv := setupTestServer()
|
||||
defer listener.Close()
|
||||
defer srv.Close() // Ensure the server is closed properly
|
||||
|
||||
// Use the existing user in the memory backend
|
||||
config := &client.Config{
|
||||
Username: "username", // Use the predefined username
|
||||
Password: "password", // Use the predefined password
|
||||
Server: listener.Addr().String(),
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
imapClient := client.NewIMAP(config)
|
||||
|
||||
// Connect to the server
|
||||
err := imapClient.Connect()
|
||||
assert.NoError(t, err, "Failed to connect")
|
||||
defer imapClient.Disconnect()
|
||||
|
||||
// Define the message config to grab specific parts, including body
|
||||
messageConfig := client.MessageConfig{
|
||||
GrabID: true,
|
||||
GrabFrom: true,
|
||||
GrabSubject: true,
|
||||
GrabBody: true, // Grab the full body
|
||||
}
|
||||
|
||||
// List only one message from the INBOX
|
||||
messages, err := imapClient.ListMessages("INBOX", 1, messageConfig)
|
||||
assert.NoError(t, err, "Failed to list messages")
|
||||
|
||||
// Validate that only one message is returned
|
||||
assert.Len(t, messages, 1, "Expected 1 message")
|
||||
|
||||
// Validate the content of the message
|
||||
expectedBody := "From: contact@example.org\r\n" +
|
||||
"To: contact@example.org\r\n" +
|
||||
"Subject: A little message, just for you\r\n" +
|
||||
"Date: Wed, 11 May 2016 14:31:59 +0000\r\n" +
|
||||
"Message-ID: <0000000@localhost/>\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"\r\n" +
|
||||
"Hi there :)"
|
||||
|
||||
assert.Equal(t, expectedBody, messages[0][client.KeyBody], "Unexpected body content")
|
||||
}
|
||||
|
||||
func TestListMailboxes(t *testing.T) {
|
||||
listener, srv := setupTestServer()
|
||||
defer listener.Close()
|
||||
defer srv.Close() // Ensure the server is closed properly
|
||||
|
||||
// Use the existing user in the memory backend
|
||||
config := &client.Config{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
Server: listener.Addr().String(),
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
imapClient := client.NewIMAP(config)
|
||||
|
||||
// Connect to the server
|
||||
err := imapClient.Connect()
|
||||
assert.NoError(t, err, "Failed to connect")
|
||||
defer imapClient.Disconnect()
|
||||
|
||||
// List mailboxes
|
||||
mailboxes, err := imapClient.ListMailboxes()
|
||||
assert.NoError(t, err, "Failed to list mailboxes")
|
||||
|
||||
// Validate the mailboxes
|
||||
expectedMailboxes := []string{"INBOX"}
|
||||
assert.ElementsMatch(t, expectedMailboxes, mailboxes, "Unexpected mailboxes")
|
||||
}
|
52
client/key_test.pem
Normal file
52
client/key_test.pem
Normal file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
EY5pJs7DKuAhXPqtWhEDrO79MoVdqA==
|
||||
-----END PRIVATE KEY-----
|
38
client/list_mailboxes.go
Normal file
38
client/list_mailboxes.go
Normal file
@ -0,0 +1,38 @@
|
||||
// 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"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// ListMailboxes retrieves all available mailboxes for the connected user
|
||||
func (c *IMAPClient) ListMailboxes() ([]string, error) {
|
||||
if c.client == nil {
|
||||
return nil, ErrorsClientIsNotConnected
|
||||
}
|
||||
|
||||
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||
done := make(chan error, 1)
|
||||
|
||||
// Start listing mailboxes
|
||||
go func() {
|
||||
done <- c.client.List("", "*", mailboxes)
|
||||
}()
|
||||
|
||||
var mailboxNames []string
|
||||
for m := range mailboxes {
|
||||
mailboxNames = append(mailboxNames, m.Name)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, fmt.Errorf("failed to list mailboxes: %v", err)
|
||||
}
|
||||
|
||||
return mailboxNames, nil
|
||||
}
|
@ -7,6 +7,7 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
@ -21,31 +22,35 @@ const (
|
||||
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, fmt.Errorf("client is not connected")
|
||||
return nil, ErrorsClientIsNotConnected
|
||||
}
|
||||
|
||||
mbox, err := c.client.Select(mailbox, false)
|
||||
mbox, err := c.selectMailbox(mailbox)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to select %s: %v", mailbox, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
from := uint32(1)
|
||||
to := mbox.Messages
|
||||
if mbox.Messages > numMessages {
|
||||
from = mbox.Messages - numMessages + 1
|
||||
}
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddRange(from, to)
|
||||
|
||||
items := []imap.FetchItem{imap.FetchEnvelope}
|
||||
if config.GrabBody {
|
||||
items = append(items, imap.FetchItem("BODY.PEEK[]"))
|
||||
}
|
||||
seqset := c.createSeqSet(mbox.Messages, numMessages)
|
||||
items := c.getFetchItems(config)
|
||||
|
||||
messages := make(chan *imap.Message, numMessages)
|
||||
done := make(chan error, 1)
|
||||
@ -53,34 +58,9 @@ func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config Mes
|
||||
done <- c.client.Fetch(seqset, items, messages)
|
||||
}()
|
||||
|
||||
var results []map[string]any
|
||||
for msg := range messages {
|
||||
details := make(map[string]any)
|
||||
|
||||
if config.GrabID && msg.Envelope.MessageId != "" {
|
||||
details[KeyID] = msg.Envelope.MessageId
|
||||
}
|
||||
if config.GrabFrom {
|
||||
var from []string
|
||||
for _, addr := range msg.Envelope.From {
|
||||
from = append(from, addr.Address())
|
||||
}
|
||||
details[KeyFrom] = from
|
||||
}
|
||||
if config.GrabSubject && msg.Envelope.Subject != "" {
|
||||
details[KeySubject] = msg.Envelope.Subject
|
||||
}
|
||||
if config.GrabBody {
|
||||
for _, literal := range msg.Body {
|
||||
buf := bytebufferpool.Get()
|
||||
if _, err := buf.ReadFrom(literal); err == nil {
|
||||
details[KeyBody] = buf.String()
|
||||
}
|
||||
buf.Reset() // Reset the buffer before returning it to the pool.
|
||||
bytebufferpool.Put(buf)
|
||||
}
|
||||
}
|
||||
results = append(results, details)
|
||||
results, err := c.processMessages(messages, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
@ -90,6 +70,135 @@ func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config Mes
|
||||
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()
|
||||
|
49
export/exporter_test.go
Normal file
49
export/exporter_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 export_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.b0zal.io/H0llyW00dzZ/imap/export"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
func TestJSONExporter(t *testing.T) {
|
||||
// Create a sample message
|
||||
messages := []map[string]any{
|
||||
{
|
||||
"id": "1",
|
||||
"from": []string{"contact@example.org"},
|
||||
"subject": "A little message, just for you",
|
||||
"body": "Hi there :)",
|
||||
},
|
||||
}
|
||||
|
||||
// Get a buffer from the pool
|
||||
buffer := bytebufferpool.Get()
|
||||
defer func() {
|
||||
buffer.Reset() // Reset the buffer to prevent data leaks
|
||||
bytebufferpool.Put(buffer) // Return the buffer to the pool
|
||||
}()
|
||||
|
||||
// Create a JSONExporter with the default encoder
|
||||
exporter := &export.JSONExporter{
|
||||
Encoder: export.DefaultJSONEncoder,
|
||||
}
|
||||
|
||||
// Export the messages to the buffer
|
||||
err := exporter.Export(messages, buffer)
|
||||
assert.NoError(t, err, "Failed to export messages")
|
||||
|
||||
// Define the expected JSON output
|
||||
expectedJSON := `[{"body":"Hi there :)","from":["contact@example.org"],"id":"1","subject":"A little message, just for you"}]
|
||||
`
|
||||
|
||||
// Assert that the buffer content matches the expected JSON
|
||||
assert.Equal(t, expectedJSON, buffer.String(), "Unexpected JSON output")
|
||||
}
|
6
go.mod
6
go.mod
@ -8,6 +8,12 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-message v0.15.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@ -1,11 +1,19 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
|
||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -14,3 +22,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
Loading…
x
Reference in New Issue
Block a user