From 262b77b0fe98757996c636e6b837439196aa5b30 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sun, 17 Dec 2023 08:46:29 +1300 Subject: [PATCH] Testing: Add new `ingest` subcommand to import an email file or maildir folder over SMTP --- cmd/ingest.go | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 cmd/ingest.go diff --git a/cmd/ingest.go b/cmd/ingest.go new file mode 100644 index 0000000..7c32f96 --- /dev/null +++ b/cmd/ingest.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "bytes" + "io" + "net/mail" + "net/smtp" + "os" + "path/filepath" + "strings" + "time" + + "github.com/axllent/mailpit/internal/logger" + sendmail "github.com/axllent/mailpit/sendmail/cmd" + "github.com/spf13/cobra" + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var ( + ingestRecent int +) + +// ingestCmd represents the ingest command +var ingestCmd = &cobra.Command{ + Use: "ingest ...[file|folder]", + Short: "Ingest a file or folder of emails for testing", + Long: `Ingest a file or folder of emails for testing. + +This command will scan the folder for emails and deliver them via SMTP to a running +Mailpit server. Each email must be a separate file (eg: Maildir format, not mbox). +The --recent flag will only consider files with a modification date within the last X days.`, + // Hidden: true, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var count int + var total int + var per100start = time.Now() + p := message.NewPrinter(language.English) + + for _, a := range args { + err := filepath.Walk(a, + func(path string, info os.FileInfo, err error) error { + if err != nil { + logger.Log().Error(err) + return nil + } + if !isFile(path) { + return nil + } + + info.ModTime() + + if ingestRecent > 0 && time.Now().Sub(info.ModTime()) > time.Duration(ingestRecent)*24*time.Hour { + return nil + } + + f, err := os.Open(filepath.Clean(path)) + if err != nil { + logger.Log().Errorf("%s: %s", path, err.Error()) + return nil + } + defer f.Close() // #nosec + + body, err := io.ReadAll(f) + if err != nil { + logger.Log().Errorf("%s: %s", path, err.Error()) + return nil + } + + msg, err := mail.ReadMessage(bytes.NewReader(body)) + if err != nil { + logger.Log().Errorf("error parsing message body: %s", err.Error()) + return nil + } + + recipients := []string{} + // get all recipients in To, Cc and Bcc + if to, err := msg.Header.AddressList("To"); err == nil { + for _, a := range to { + recipients = append(recipients, a.Address) + } + } + if cc, err := msg.Header.AddressList("Cc"); err == nil { + for _, a := range cc { + recipients = append(recipients, a.Address) + } + } + if bcc, err := msg.Header.AddressList("Bcc"); err == nil { + for _, a := range bcc { + recipients = append(recipients, a.Address) + } + } + + if sendmail.FromAddr == "" { + if fromAddresses, err := msg.Header.AddressList("From"); err == nil { + sendmail.FromAddr = fromAddresses[0].Address + } + } + + if len(recipients) == 0 { + // Bcc + recipients = []string{sendmail.FromAddr} + } + + returnPath := strings.Trim(msg.Header.Get("Return-Path"), "<>") + if returnPath == "" { + if fromAddresses, err := msg.Header.AddressList("From"); err == nil { + returnPath = fromAddresses[0].Address + } + } + + err = smtp.SendMail(sendmail.SMTPAddr, nil, returnPath, recipients, body) + if err != nil { + logger.Log().Errorf("error sending mail: %s", err.Error()) + logger.Log().Errorf(path) + return nil + } + + count++ + total++ + if count%100 == 0 { + formatted := p.Sprintf("%d", total) + logger.Log().Infof("[%s] 100 messages in %s", formatted, time.Since(per100start)) + + per100start = time.Now() + } + + return nil + }) + if err != nil { + logger.Log().Error(err) + } + } + + }, +} + +func init() { + rootCmd.AddCommand(ingestCmd) + + ingestCmd.Flags().StringVarP(&sendmail.SMTPAddr, "smtp-addr", "S", sendmail.SMTPAddr, "SMTP server address") + ingestCmd.Flags().IntVarP(&ingestRecent, "recent", "r", 0, "Only ingest messages from the last X days (default all)") +} + +// IsFile returns if a path is a file +func isFile(path string) bool { + info, err := os.Stat(path) + if os.IsNotExist(err) || !info.Mode().IsRegular() { + return false + } + + return true +}