mirror of
https://github.com/axllent/mailpit.git
synced 2025-08-13 20:04:49 +02:00
Feature: Add dump feature to export all raw messages to a local directory (#443)
This commit is contained in:
36
cmd/dump.go
Normal file
36
cmd/dump.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/dump"
|
||||||
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dumpCmd represents the dump command
|
||||||
|
var dumpCmd = &cobra.Command{
|
||||||
|
Use: "dump <database> <output-dir>",
|
||||||
|
Short: "Dump all messages from a database to a directory",
|
||||||
|
Long: `Dump all messages stored in Mailpit into a local directory as individual files.
|
||||||
|
|
||||||
|
The database can either be the database file (eg: --database /var/lib/mailpit/mailpit.db) or a
|
||||||
|
URL of a running Mailpit instance (eg: --http http://127.0.0.1/). If dumping over HTTP, the URL
|
||||||
|
should be the base URL of your running Mailpit instance, not the link to the API itself.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := dump.Sync(args[0]); err != nil {
|
||||||
|
logger.Log().Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(dumpCmd)
|
||||||
|
|
||||||
|
dumpCmd.Flags().SortFlags = false
|
||||||
|
|
||||||
|
dumpCmd.Flags().StringVar(&config.Database, "database", config.Database, "Dump messages directly from a database file")
|
||||||
|
dumpCmd.Flags().StringVar(&config.TenantID, "tenant-id", config.TenantID, "Database tenant ID to isolate data (optional)")
|
||||||
|
dumpCmd.Flags().StringVar(&dump.URL, "http", dump.URL, "Dump messages via HTTP API (base URL of running Mailpit instance)")
|
||||||
|
dumpCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
|
||||||
|
}
|
163
internal/dump/dump.go
Normal file
163
internal/dump/dump.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Package dump is used to export all messages from mailpit into a directory
|
||||||
|
package dump
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
|
"github.com/axllent/mailpit/server/apiv1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
|
||||||
|
|
||||||
|
outDir string
|
||||||
|
|
||||||
|
// Base URL of mailpit instance
|
||||||
|
base string
|
||||||
|
|
||||||
|
// URL is the base URL of a remove Mailpit instance
|
||||||
|
URL string
|
||||||
|
|
||||||
|
summary = []storage.MessageSummary{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync will sync all messages from the specified database or API to the specified output directory
|
||||||
|
func Sync(d string) error {
|
||||||
|
|
||||||
|
outDir = path.Clean(d)
|
||||||
|
|
||||||
|
if URL != "" {
|
||||||
|
if !linkRe.MatchString(URL) {
|
||||||
|
return errors.New("Invalid URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
base = strings.TrimRight(URL, "/") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if base == "" && config.Database == "" {
|
||||||
|
return errors.New("No database or API URL specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tools.IsDir(outDir) {
|
||||||
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadIDs(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := saveMessages(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadIDs will load all message IDs from the specified database or API
|
||||||
|
func loadIDs() error {
|
||||||
|
if base != "" {
|
||||||
|
// remote
|
||||||
|
logger.Log().Debugf("Fetching messages summary from %s", base)
|
||||||
|
res, err := http.Get(base + "api/v1/messages?limit=0")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data apiv1.MessagesSummary
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = data.Messages
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// make sure the database isn't pruned while open
|
||||||
|
config.MaxMessages = 0
|
||||||
|
|
||||||
|
var err error
|
||||||
|
// local database
|
||||||
|
if err = storage.InitDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log().Debugf("Fetching messages summary from %s", config.Database)
|
||||||
|
|
||||||
|
summary, err = storage.List(0, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(summary) == 0 {
|
||||||
|
return errors.New("No messages found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveMessages() error {
|
||||||
|
for _, m := range summary {
|
||||||
|
out := path.Join(outDir, m.ID+".eml")
|
||||||
|
|
||||||
|
// skip if message exists
|
||||||
|
if tools.IsFile(out) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
if base != "" {
|
||||||
|
res, err := http.Get(base + "api/v1/message/" + m.ID + "/raw")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().Errorf("Error fetching message %s: %s", m.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = io.ReadAll(res.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().Errorf("Error fetching message %s: %s", m.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
b, err = storage.GetMessageRaw(m.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().Errorf("Error fetching message %s: %s", m.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(out, b, 0644); err != nil {
|
||||||
|
logger.Log().Errorf("Error writing message %s: %s", m.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.Chtimes(out, m.Created, m.Created)
|
||||||
|
|
||||||
|
logger.Log().Debugf("Saved message %s to %s", m.ID, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -175,9 +175,11 @@ func List(start int, beforeTS int64, limit int) ([]MessageSummary, error) {
|
|||||||
|
|
||||||
q := sqlf.From(tenant("mailbox") + " m").
|
q := sqlf.From(tenant("mailbox") + " m").
|
||||||
Select(`m.Created, m.ID, m.MessageID, m.Subject, m.Metadata, m.Size, m.Attachments, m.Read, m.Snippet`).
|
Select(`m.Created, m.ID, m.MessageID, m.Subject, m.Metadata, m.Size, m.Attachments, m.Read, m.Snippet`).
|
||||||
OrderBy("m.Created DESC").
|
OrderBy("m.Created DESC")
|
||||||
Limit(limit).
|
|
||||||
Offset(start)
|
if limit > 0 {
|
||||||
|
q = q.Limit(limit).Offset(start)
|
||||||
|
}
|
||||||
|
|
||||||
if beforeTS > 0 {
|
if beforeTS > 0 {
|
||||||
q = q.Where("Created < ?", beforeTS)
|
q = q.Where("Created < ?", beforeTS)
|
||||||
|
23
internal/tools/fs.go
Normal file
23
internal/tools/fs.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsFile returns whether a file exists and is readable
|
||||||
|
func IsFile(path string) bool {
|
||||||
|
f, err := os.Open(filepath.Clean(path))
|
||||||
|
defer f.Close()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir returns whether a path is a directory
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || os.IsNotExist(err) || !info.IsDir() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
Reference in New Issue
Block a user