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>
This commit is contained in:
H0llyW00dzZ 2025-01-24 07:08:59 +00:00 committed by H0llyW00dzZ
parent 9ce7f8dc5c
commit ab122177c2
7 changed files with 180 additions and 23 deletions

3
.gitignore vendored
View File

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

View File

@ -1,12 +1,14 @@
# IMAP Client Package # 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) [![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, and manage multiple user accounts.
## Features ## Features
- Connect and disconnect from an IMAP server - Connect and disconnect from an IMAP server
- List messages in a specified mailbox - List messages in a specified mailbox
- Export messages to various formats
- Manage multiple users with separate IMAP clients - Manage multiple users with separate IMAP clients
## Installation ## Installation
@ -60,7 +62,7 @@ func main() {
} }
for _, msg := range messages { 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])
} }
} }
``` ```
@ -102,7 +104,57 @@ func main() {
} }
for _, msg := range messages { 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)
} }
} }
``` ```

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 fmt.Errorf("client is not connected")
}
// 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
}

View File

@ -12,18 +12,19 @@ import (
"github.com/valyala/bytebufferpool" "github.com/valyala/bytebufferpool"
) )
// MessageDetails holds the details of an email message. const (
// This struct allows the caller to access and format the message's // KeyID is the key for the message ID
// ID, sender information, subject, and body as needed. KeyID = "id"
type MessageDetails struct { // KeyFrom is the key for the sender's address
ID string KeyFrom = "from"
From []string // KeySubject is the key for the message subject
Subject string KeySubject = "subject"
Body string // KeyBody is the key for the message body
} KeyBody = "body"
)
// ListMessages lists the messages in the specified mailbox based on the MessageConfig // 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 { if c.client == nil {
return nil, fmt.Errorf("client is not connected") return nil, fmt.Errorf("client is not connected")
} }
@ -52,26 +53,28 @@ func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config Mes
done <- c.client.Fetch(seqset, items, messages) done <- c.client.Fetch(seqset, items, messages)
}() }()
var results []MessageDetails var results []map[string]any
for msg := range messages { for msg := range messages {
details := MessageDetails{} details := make(map[string]any)
if config.GrabID { if config.GrabID && msg.Envelope.MessageId != "" {
details.ID = msg.Envelope.MessageId details[KeyID] = msg.Envelope.MessageId
} }
if config.GrabFrom { if config.GrabFrom {
var from []string
for _, addr := range msg.Envelope.From { for _, addr := range msg.Envelope.From {
details.From = append(details.From, addr.Address()) from = append(from, addr.Address())
} }
details[KeyFrom] = from
} }
if config.GrabSubject { if config.GrabSubject && msg.Envelope.Subject != "" {
details.Subject = msg.Envelope.Subject details[KeySubject] = msg.Envelope.Subject
} }
if config.GrabBody { if config.GrabBody {
for _, literal := range msg.Body { for _, literal := range msg.Body {
buf := bytebufferpool.Get() buf := bytebufferpool.Get()
if _, err := buf.ReadFrom(literal); err == nil { if _, err := buf.ReadFrom(literal); err == nil {
details.Body = buf.String() details[KeyBody] = buf.String()
} }
buf.Reset() // Reset the buffer before returning it to the pool. buf.Reset() // Reset the buffer before returning it to the pool.
bytebufferpool.Put(buf) bytebufferpool.Put(buf)
@ -88,7 +91,7 @@ func (c *IMAPClient) ListMessages(mailbox string, numMessages uint32, config Mes
} }
// ListUserMessages lists messages for a specific user based on the MessageConfig // 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() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()

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
}

36
export/json_exporter.go Normal file
View File

@ -0,0 +1,36 @@
// 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 using the custom JSON encoder
func (e *JSONExporter) Export(messages []map[string]any, writer io.Writer) error {
for _, msg := range messages {
if err := e.Encoder(msg, writer); err != nil {
return fmt.Errorf("failed to encode message 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)
}