Compare commits

...

15 Commits

Author SHA1 Message Date
4c74ebc1c7 Improve [Imap] [Client] Error Handling (#16)
- [+] feat(config.go): add error variables for client connection and body reading failures
- [+] refactor(export_messages.go, list_mailboxes.go, list_messages.go): replace error messages with new error variables

Reviewed-on: #16
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-25 06:40:23 +00:00
7a43b90f67 Chore [Client] [ListMailboxes] copyright notice and license agreement (#15)
- [+] chore(list_mailboxes.go): add copyright notice and license agreement information

Reviewed-on: #15
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 12:36:12 +00:00
5cab29c344 Implement [Client] "ListMailboxes" (#14)
- [+] feat(README.md): add functionality to list available mailboxes in the package description and usage examples
- [+] feat(client): implement ListMailboxes method to retrieve all available mailboxes for the connected user
- [+] test(client): add unit test for ListMailboxes method to validate mailbox retrieval functionality

Reviewed-on: #14
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 12:34:51 +00:00
e129c63b37 Improve [Client] [List Messages] Grab MessageConfig (#13)
- [+] feat(config.go): add additional fields to MessageConfig for enhanced message retrieval
- [+] feat(list_messages.go): refactor message processing to use extractDetails method for cleaner code

Reviewed-on: #13
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 11:51:16 +00:00
289fc877ee Improve Test [Exporter] [JSON Exporter] Fix Typo (#12)
- [+] fix(tests): rename expoter_test.go to exporter_test.go for correct spelling

Reviewed-on: #12
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 10:02:52 +00:00
2dab893c8e Improve Test [Exporter] JSON Exporter (#11)
- [+] fix(exporter_test.go): change type of messages from map[string]interface{} to map[string]any

Reviewed-on: #11
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 10:01:41 +00:00
f22bed1f12 Test [Exporter] JSON Exporter (#10)
- [+] feat(export): add unit test for JSONExporter functionality in expoter_test.go

Reviewed-on: #10
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 09:58:03 +00:00
1574f865c9 Test [Imap] Connection and List Messages (#9)
- [+] feat(client): add test certificates and IMAP client tests with TLS support
- [+] chore(go.mod): update dependencies and add indirect packages
- [+] chore(go.sum): update checksum for new dependencies

Reviewed-on: #9
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 09:53:10 +00:00
d17022fb85 Update README.md (#8)
- [+] docs(README.md): add note for Indonesian users regarding installation issues with Telkom

Reviewed-on: #8
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 08:34:15 +00:00
3e1a7445f6 Improve [Client] [List Messages] Reduce Complexity (#7)
- [+] feat(list_messages.go): refactor ListMessages method to improve mailbox selection and message fetching
- [+] refactor(list_messages.go): extract address and body processing into separate methods for better readability

Reviewed-on: #7
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 08:10:45 +00:00
4de658fca2 Fix [Client] [Exporting Messages] JSON Format (#6)
- [+] fix(json_exporter.go): update Export method to encode messages as a JSON array instead of individually

Reviewed-on: #6
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 08:01:49 +00:00
3d17ce82ff Update Package Documentation (#5)
- [+] feat(docs): update client documentation to include message export functionality and usage examples
- [+] feat(docs): add export package documentation with JSON export example
- [+] chore(docs.go): add copyright notice and license agreement information

Reviewed-on: #5
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 07:13:02 +00:00
ab122177c2 Improve [Client] Support Exporting Messages with various formats (#4)
- [+] feat(export): add message exporting functionality with JSON support
- [+] feat(readme): update documentation to include message export feature
- [+] refactor(.gitignore): change ignored files from emails.csv to test.csv and add test.json
- [+] refactor(client): update message handling to use map structure instead of MessageDetails struct

Reviewed-on: #4
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 07:08:59 +00:00
9ce7f8dc5c Improve Performance [Client] [ListMessages] [Grab Body] Optimize Memory Usage (#3)
- [+] fix(list_messages.go): optimize buffer handling in ListMessages function by resetting buffer before putting it back in the pool

Reviewed-on: #3
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 05:18:55 +00:00
ff4bd67ceb Update Go Module (#2)
- [+] chore(go.mod): update go-sasl to v0.0.0-20241020182733-b788ff22d5a6 and golang.org/x/text to v0.21.0

Reviewed-on: #2
Co-authored-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
Co-committed-by: H0llyW00dzZ <h0llyw00dzz@pm.me>
2025-01-24 05:04:24 +00:00
17 changed files with 904 additions and 67 deletions

3
.gitignore vendored
View File

@ -30,5 +30,6 @@ tmp/
# Project build
bin
emails.csv
test.csv
run.go
test.json

116
README.md
View File

@ -1,13 +1,16 @@
# IMAP Client Package
[![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, 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
- 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
- **New**: List all available mailboxes
## Installation
@ -16,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
@ -47,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,
@ -60,7 +72,7 @@ func main() {
}
for _, msg := range messages {
fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg.ID, msg.From, msg.Subject, msg.Body)
fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg[client.KeyID], msg[client.KeyFrom], msg[client.KeySubject], msg[client.KeyBody])
}
}
```
@ -89,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,
@ -102,11 +125,98 @@ func main() {
}
for _, msg := range messages {
fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg.ID, msg.From, msg.Subject, msg.Body)
fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg[client.KeyID], msg[client.KeyFrom], msg[client.KeySubject], msg[client.KeyBody])
}
}
```
### Exporting Messages
```go
package main
import (
"log"
"os"
"git.b0zal.io/H0llyW00dzZ/imap/client"
"git.b0zal.io/H0llyW00dzZ/imap/export"
)
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()
messageConfig := client.MessageConfig{
GrabID: true,
GrabFrom: true,
GrabSubject: true,
GrabBody: true,
}
file, err := os.Create("messages.json")
if err != nil {
log.Fatalf("Failed to create file: %v", err)
}
defer file.Close()
exporter := &export.JSONExporter{Encoder: export.DefaultJSONEncoder}
err = imapClient.ExportMessagesTo(file, exporter, "INBOX", 10, messageConfig)
if err != nil {
log.Fatalf("Failed to export messages: %v", err)
}
}
```
### 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
View File

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
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
JRNGkalOfg==
-----END CERTIFICATE-----

View File

@ -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")
)

View File

@ -5,12 +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, 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
//
@ -54,10 +56,44 @@
// }
//
// for _, msg := range messages {
// fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg.ID, msg.From, msg.Subject, msg.Body)
// fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg[client.KeyID], msg[client.KeyFrom], msg[client.KeySubject], msg[client.KeyBody])
// }
// }
//
// 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
@ -94,7 +130,55 @@
// }
//
// for _, msg := range messages {
// fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg.ID, msg.From, msg.Subject, msg.Body)
// fmt.Printf("ID: %s, From: %v, Subject: %s, Body: %s\n", msg[client.KeyID], msg[client.KeyFrom], msg[client.KeySubject], msg[client.KeyBody])
// }
// }
//
// Export Messages Example:
//
// package main
//
// import (
// "log"
// "os"
//
// "git.b0zal.io/H0llyW00dzZ/imap/client"
// "git.b0zal.io/H0llyW00dzZ/imap/export"
// )
//
// 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()
//
// messageConfig := client.MessageConfig{
// GrabID: true,
// GrabFrom: true,
// GrabSubject: true,
// GrabBody: true,
// }
//
// file, err := os.Create("messages.json")
// if err != nil {
// log.Fatalf("Failed to create file: %v", err)
// }
// defer file.Close()
//
// exporter := &export.JSONExporter{Encoder: export.DefaultJSONEncoder}
// err = imapClient.ExportMessagesTo(file, exporter, "INBOX", 10, messageConfig)
// if err != nil {
// log.Fatalf("Failed to export messages: %v", err)
// }
// }
package client

29
client/export_messages.go Normal file
View File

@ -0,0 +1,29 @@
// 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"
"io"
"git.b0zal.io/H0llyW00dzZ/imap/export"
)
// 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 ErrorsClientIsNotConnected
}
// Fetch messages based on the provided MessageConfig and number of messages
messages, err := c.ListMessages(mailbox, numMessages, config)
if err != nil {
return fmt.Errorf("failed to list messages: %v", err)
}
// Use the exporter to write messages
return exporter.Export(messages, writer)
}

21
client/get_multi_user.go Normal file
View File

@ -0,0 +1,21 @@
// 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"
// GetClient retrieves the IMAPClient for a specific user
func (m *MultiUserIMAP) GetClient(username string) (*IMAPClient, 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, nil
}

154
client/imap_test.go Normal file
View 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
View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
EY5pJs7DKuAhXPqtWhEDrO79MoVdqA==
-----END PRIVATE KEY-----

38
client/list_mailboxes.go Normal file
View 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
}

View File

@ -7,44 +7,50 @@ package client
import (
"fmt"
"time"
"github.com/emersion/go-imap"
"github.com/valyala/bytebufferpool"
)
// MessageDetails holds the details of an email message.
// This struct allows the caller to access and format the message's
// ID, sender information, subject, and body as needed.
type MessageDetails struct {
ID string
From []string
Subject string
Body string
}
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) ([]MessageDetails, error) {
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)
@ -52,33 +58,9 @@ func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config Mes
done <- c.client.Fetch(seqset, items, messages)
}()
var results []MessageDetails
for msg := range messages {
details := MessageDetails{}
if config.GrabID {
details.ID = msg.Envelope.MessageId
}
if config.GrabFrom {
for _, addr := range msg.Envelope.From {
details.From = append(details.From, addr.Address())
}
}
if config.GrabSubject {
details.Subject = msg.Envelope.Subject
}
if config.GrabBody {
for _, literal := range msg.Body {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
_, err := buf.ReadFrom(literal)
if err == nil {
details.Body = buf.String()
}
}
}
results = append(results, details)
results, err := c.processMessages(messages, config)
if err != nil {
return nil, err
}
if err := <-done; err != nil {
@ -88,8 +70,137 @@ 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) ([]MessageDetails, error) {
func (m *MultiUserIMAP) ListUserMessages(username, mailbox string, numMessages uint32, config MessageConfig) ([]map[string]any, error) {
m.mu.Lock()
defer m.mu.Unlock()

64
export/docs.go Normal file
View File

@ -0,0 +1,64 @@
// 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 provides functionality to export email messages
// in various formats. It defines interfaces and implementations
// for exporting messages, allowing flexibility in how messages
// are written to different outputs.
//
// # Features
// - Define a standard interface for exporting messages
// - Implement JSON export functionality with customizable encoding
//
// # Usage
//
// JSON Export Example:
//
// package main
//
// import (
// "log"
// "os"
//
// "git.b0zal.io/H0llyW00dzZ/imap/client"
// "git.b0zal.io/H0llyW00dzZ/imap/export"
// )
//
// 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()
//
// messageConfig := client.MessageConfig{
// GrabID: true,
// GrabFrom: true,
// GrabSubject: true,
// GrabBody: true,
// }
//
// file, err := os.Create("messages.json")
// if err != nil {
// log.Fatalf("Failed to create file: %v", err)
// }
// defer file.Close()
//
// exporter := &export.JSONExporter{Encoder: export.DefaultJSONEncoder}
// err = imapClient.ExportMessagesTo(file, exporter, "INBOX", 10, messageConfig)
// if err != nil {
// log.Fatalf("Failed to export messages: %v", err)
// }
// }
package export

15
export/exporter.go Normal file
View File

@ -0,0 +1,15 @@
// 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
import (
"io"
)
// Exporter defines an interface for exporting messages
type Exporter interface {
Export(messages []map[string]any, writer io.Writer) error
}

49
export/exporter_test.go Normal file
View 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")
}

35
export/json_exporter.go Normal file
View File

@ -0,0 +1,35 @@
// 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
import (
"encoding/json"
"fmt"
"io"
)
// JSONEncoderFunc defines a function type for custom JSON encoding
type JSONEncoderFunc func(v any, writer io.Writer) error
// JSONExporter implements the Exporter interface for JSON with a custom encoder
type JSONExporter struct {
Encoder JSONEncoderFunc
}
// Export writes messages to the writer as a JSON array using the custom JSON encoder
func (e *JSONExporter) Export(messages []map[string]any, writer io.Writer) error {
// Use the encoder to write the entire slice as a JSON array
if err := e.Encoder(messages, writer); err != nil {
return fmt.Errorf("failed to encode messages to JSON: %v", err)
}
return nil
}
// DefaultJSONEncoder is the default JSON encoding function using the standard library
func DefaultJSONEncoder(v any, writer io.Writer) error {
encoder := json.NewEncoder(writer)
return encoder.Encode(v)
}

10
go.mod
View File

@ -8,6 +8,12 @@ require (
)
require (
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
golang.org/x/text v0.3.7 // indirect
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
)

15
go.sum
View File

@ -1,12 +1,27 @@
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=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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=