From 4fa664a3979237c0bd9fb7e5eeb31f3d83a2d640 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:15:32 +0200 Subject: [PATCH 01/15] feat: save email contents to local files --- backends/p_content_parser.go | 42 +++++ backends/p_debugger.go | 1 + localfile-processor.conf.json | 4 + mail/envelope.go | 307 ++++++++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 backends/p_content_parser.go create mode 100644 localfile-processor.conf.json diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go new file mode 100644 index 0000000..af12b15 --- /dev/null +++ b/backends/p_content_parser.go @@ -0,0 +1,42 @@ +package backends + +import ( + "github.com/phires/go-guerrilla/mail" +) + +// ---------------------------------------------------------------------------------- +// Processor Name: contentparser +// ---------------------------------------------------------------------------------- +// Description : Parses and decodes the content +// ---------------------------------------------------------------------------------- +// Config Options: Specify the location path to save the parts of the email +// --------------:------------------------------------------------------------------- +// Input : envelope +// ---------------------------------------------------------------------------------- +// Output : Content will be populated in e.Content +// ---------------------------------------------------------------------------------- +func init() { + processors["contentparser"] = func() Decorator { + return ContentParser() + } +} + +func ContentParser() Decorator { + return func(p Processor) Processor { + return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { + if task == TaskSaveMail { + if err := e.ParseContent(); err != nil { + Log().WithError(err).Error("parse content error") + } else { + Log().Info("Parsed Content is: ", e.Content) + } + // next processor + return p.Process(e, task) + } else { + // next processor + return p.Process(e, task) + } + }) + } +} + diff --git a/backends/p_debugger.go b/backends/p_debugger.go index 2bcc72b..d73ac81 100644 --- a/backends/p_debugger.go +++ b/backends/p_debugger.go @@ -47,6 +47,7 @@ func Debugger() Decorator { if config.LogReceivedMails { Log().Infof("Mail from: %s / to: %v", e.MailFrom.String(), e.RcptTo) Log().Info("Headers are:", e.Header) + Log().Info("Body is: ", e.Data.String() ) } if config.SleepSec > 0 { diff --git a/localfile-processor.conf.json b/localfile-processor.conf.json new file mode 100644 index 0000000..b0c1b2c --- /dev/null +++ b/localfile-processor.conf.json @@ -0,0 +1,4 @@ +{ + "path" : "/tmp", + "description" : "This is the path where the files will be stored, no trailing slash" +} diff --git a/mail/envelope.go b/mail/envelope.go index 686b903..e1ec898 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -4,17 +4,25 @@ import ( "bufio" "bytes" "crypto/sha256" + "encoding/base64" "errors" "fmt" "io" "mime" "net" "net/textproto" + "os" "strings" "sync" "time" "github.com/phires/go-guerrilla/mail/rfc5321" + "io/ioutil" + "mime/multipart" + "mime/quotedprintable" + "math/rand" + "encoding/json" + "regexp" ) // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. @@ -119,6 +127,12 @@ func NewAddress(str string) (*Address, error) { return a, nil } +type LocalFileContent struct { + PreferredDisplay string + CharSet string + LocalFile string +} + // Envelope of Email represents a single SMTP message. type Envelope struct { // Remote IP address @@ -133,6 +147,8 @@ type Envelope struct { Data bytes.Buffer // Subject stores the subject of the email, extracted and decoded after calling ParseHeaders() Subject string + // Content stores the decoded content of the email, extracted after calling ParseContent() + Content []LocalFileContent // TLS is true if the email was received using a TLS connection TLS bool // Header stores the results from ParseHeaders() @@ -203,6 +219,297 @@ func (e *Envelope) ParseHeaders() error { return err } +func RandStringBytesRmndr(n int) string { + letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} + + +// ParseContent retrieves the smtp content itself and decodes it based on the content-type header +func (e *Envelope) ParseContent() error { + if e.Header == nil { + return errors.New("headers not parsed") + } + + // Clear the Content slice to prevent accumulation + e.Content = []LocalFileContent{} + + // Read path field from localfile-processor.conf.json + configPath := "localfile-processor.conf.json" + configFile, err := os.Open(configPath) + if err != nil { + return err + } + defer configFile.Close() + + var config struct { + Path string `json:"path"` + } + + decoder := json.NewDecoder(configFile) + err = decoder.Decode(&config) + + if err != nil { + return err + } + + path := config.Path + "/" + + // add the current timestamp to the path + path += fmt.Sprintf("%d", time.Now().UnixNano()) + + // add RandStringBytesRmndr(5) to the path + path += "-" + path += RandStringBytesRmndr(5) + "-goguerrilla" + + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0755) + } + + headerContentType := e.Header.Get("Content-Type") + if headerContentType == "" { + return errors.New("content-type header not found") + } + + content, err := e.GetContent() + if err != nil { + return err + } + + // Single part message + if strings.HasPrefix(headerContentType, "text/") { + var err error + + _, params, err := mime.ParseMediaType(headerContentType) + if err != nil { + return err + } + + file_path := "" + + if params["charset"] == "utf-8" { + decodedContent, err := base64.StdEncoding.DecodeString(content) + + if err != nil { + return err + } + + file_path = path + "/root_utf8.txt" + file, err := os.Create(file_path) + + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(string(decodedContent)) + if err != nil { + return err + } + } else if params["charset"] == "us-ascii" { + file_path = path + "/root_us-ascii.txt" + + file, err := os.Create(file_path) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(string(content)) + if err != nil { + return err + } + } else { // Default to asumption: plain old ASCII + file_path = path + "/root_unknowncharset.txt" + file, err := os.Create(file_path) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(string(content)) + if err != nil { + return err + } + } + var localFileContent LocalFileContent + localFileContent.PreferredDisplay = "INLINE" + localFileContent.CharSet = params["charset"] + localFileContent.LocalFile = file_path + + e.Content = append(e.Content, localFileContent) + return nil + } else if strings.HasPrefix(headerContentType, "multipart/") { // Multipart message + _, params, err := mime.ParseMediaType(headerContentType) + if err != nil { + return err + } + ParsePart(strings.NewReader(content), params["boundary"], path, e) + return nil + } + return errors.New("unsupported media type: " + headerContentType) +} + +// BuildFileName builds a file name for a MIME part +// If the name is provided in the Content-Disposition header, it will be used. +// Otherwise, a name will be generated based on the radix and index. +func BuildFileName(part *multipart.Part, radix string, index int) (filename string) { + + filename = part.FileName() + if len(filename) > 0 { + return filename + } + + mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) + if err == nil { + mime_type, e := mime.ExtensionsByType(mediaType) + fmt.Println("Possible extensions found: ", mime_type) + + // Remove in mime_type any extension not starting with a dot (it SHOULD not happen but it DID happen) + for i, e := range mime_type { + if !strings.HasPrefix(e, ".") { + mime_type = append(mime_type[:i], mime_type[i+1:]...) + } + } + + if e == nil { + if len(mime_type)>0 { // Arbitrary take the first extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) + } else { // No extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, ".unknown") + } + } + } + return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, ".unspecified") +} + +// Parse a given MIME part +func ParsePart(mime_data io.Reader, boundary string, path string, e *Envelope) { + + path += "/" + boundary + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0755) + } + + reader := multipart.NewReader(mime_data, boundary) + if reader == nil { + return + } + + // Go through each of the MIME part of the message Body with NextPart(), + for { + new_part, err := reader.NextPart() + if err == io.EOF { // End of the MIME parts + break + } + if err != nil { + fmt.Println("Error going through the next MIME part - ", err) + break + } + + mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type")) + + if err == nil && strings.HasPrefix(mediaType, "multipart/") { // This is a new multipart to be handled recursively + ParsePart(new_part, params["boundary"], path, e) + } else { + filename := BuildFileName(new_part, boundary, 1) + filepath := "" + if path == "" { + filepath = filename + } else { + filepath = path + "/" + filename + } + + WritePart(new_part, filepath, e) + } + } +} + +// WitePart decodes the data of MIME part and writes it to the file filename. +func WritePart(part *multipart.Part, filepath string, e *Envelope) { + + // Read the data for this MIME part + part_data, err := ioutil.ReadAll(part) + if err != nil { + fmt.Println("Error reading MIME part data - ", err) + return + } + + contentTransferEncoding := strings.ToUpper(part.Header.Get("Content-Transfer-Encoding")) + contentDisposition := strings.ToUpper(part.Header.Get("Content-Disposition")) + contentType := strings.ToUpper(part.Header.Get("Content-Type")) + + contentCharset := "" + // Use a regexp to extract the charset from the content-type header + charsetRegexp := regexp.MustCompile(`(?i)charset=([^;]+)`) + matches := charsetRegexp.FindStringSubmatch(contentType) + + if len(matches) > 1 { + contentCharset = matches[1] + } + + preferredDisplay := "INLINE" // Most SMTP messages don't specify Content-Disposition when it's INLINE + + // Check if the MIME part is an attachment + if strings.HasPrefix(contentDisposition, "ATTACHMENT") { + preferredDisplay = "ATTACHMENT" + } else if strings.HasPrefix(contentDisposition, "INLINE") { + preferredDisplay = "INLINE" + } + + switch { + case strings.Compare(contentTransferEncoding, "BASE64") == 0: + decoded_content, err := base64.StdEncoding.DecodeString(string(part_data)) + if err != nil { + fmt.Println("Error decoding base64 -", err) + } else { + ioutil.WriteFile(filepath, decoded_content, 0644) + } + + case strings.Compare(contentTransferEncoding, "QUOTED-PRINTABLE") == 0: + decoded_content, err := ioutil.ReadAll(quotedprintable.NewReader(bytes.NewReader(part_data))) + if err != nil { + fmt.Println("Error decoding quoted-printable -", err) + } else { + ioutil.WriteFile(filepath, decoded_content, 0644) + } + + default: + ioutil.WriteFile(filepath, part_data, 0644) + } + + var localFileContent LocalFileContent + localFileContent.PreferredDisplay = preferredDisplay + localFileContent.CharSet = contentCharset + localFileContent.LocalFile = filepath + + e.Content = append(e.Content, localFileContent) +} + +// GetContent parses the content of the email, excluding the headers +func (e *Envelope) GetContent() (string, error) { + var err error + + buf := e.Data.Bytes() + // find where the header ends, assuming that over 30 kb would be max + if len(buf) > maxHeaderChunk { + buf = buf[:maxHeaderChunk] + } + + contentStart := bytes.Index(buf, []byte{'\n', '\n'}) // the first two new-lines chars are the End Of Header / Start Of Content + if contentStart > -1 { + content := buf[contentStart+2:] + return string(content), nil + } else { + err = errors.New("header not found") + } + return "", err +} + // Len returns the number of bytes that would be in the reader returned by NewReader() func (e *Envelope) Len() int { return len(e.DeliveryHeader) + e.Data.Len() From 687849cf3a526b5cb54a8fbcfb3c452baa76911c Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:19:35 +0200 Subject: [PATCH 02/15] fix: use tabs --- backends/p_debugger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/p_debugger.go b/backends/p_debugger.go index d73ac81..4930d10 100644 --- a/backends/p_debugger.go +++ b/backends/p_debugger.go @@ -47,7 +47,7 @@ func Debugger() Decorator { if config.LogReceivedMails { Log().Infof("Mail from: %s / to: %v", e.MailFrom.String(), e.RcptTo) Log().Info("Headers are:", e.Header) - Log().Info("Body is: ", e.Data.String() ) + Log().Info("Body is: ", e.Data.String() ) } if config.SleepSec > 0 { From 6a1b6a97d0c52cfd997c34afb8032042dc1d2305 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:26:10 +0200 Subject: [PATCH 03/15] fix: spaces to tab --- mail/envelope.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mail/envelope.go b/mail/envelope.go index e1ec898..d564424 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -128,9 +128,9 @@ func NewAddress(str string) (*Address, error) { } type LocalFileContent struct { - PreferredDisplay string - CharSet string - LocalFile string + PreferredDisplay string + CharSet string + LocalFile string } // Envelope of Email represents a single SMTP message. @@ -378,8 +378,8 @@ func BuildFileName(part *multipart.Part, radix string, index int) (filename stri if e == nil { if len(mime_type)>0 { // Arbitrary take the first extension found - return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) - } else { // No extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) + } else { // No extension found return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, ".unknown") } } From 9aee2e516381a269772aac4ebd3f60337d0deb59 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:42:36 +0200 Subject: [PATCH 04/15] chore: clean and rename --- mail/envelope.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mail/envelope.go b/mail/envelope.go index d564424..57ab5cc 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -275,7 +275,7 @@ func (e *Envelope) ParseContent() error { return errors.New("content-type header not found") } - content, err := e.GetContent() + content, err := e.GetRawContent() if err != nil { return err } @@ -367,7 +367,6 @@ func BuildFileName(part *multipart.Part, radix string, index int) (filename stri mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) if err == nil { mime_type, e := mime.ExtensionsByType(mediaType) - fmt.Println("Possible extensions found: ", mime_type) // Remove in mime_type any extension not starting with a dot (it SHOULD not happen but it DID happen) for i, e := range mime_type { @@ -491,7 +490,7 @@ func WritePart(part *multipart.Part, filepath string, e *Envelope) { } // GetContent parses the content of the email, excluding the headers -func (e *Envelope) GetContent() (string, error) { +func (e *Envelope) GetRawContent() (string, error) { var err error buf := e.Data.Bytes() From 4b0a1f5f1474e751f588c1af82d0f639fbb02fcd Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Fri, 19 Jul 2024 20:41:38 +0200 Subject: [PATCH 05/15] chore: use jhillyerd/enmime --- backends/p_content_parser.go | 2 +- go.mod | 9 ++++++ go.sum | 20 ++++++++++++ mail/envelope.go | 63 ++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go index af12b15..e36c33e 100644 --- a/backends/p_content_parser.go +++ b/backends/p_content_parser.go @@ -25,7 +25,7 @@ func ContentParser() Decorator { return func(p Processor) Processor { return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { if task == TaskSaveMail { - if err := e.ParseContent(); err != nil { + if err := e.ParseContent2(); err != nil { Log().WithError(err).Error("parse content error") } else { Log().Info("Parsed Content is: ", e.Content) diff --git a/go.mod b/go.mod index d251f5f..299404a 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,17 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect + github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect + github.com/jhillyerd/enmime v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stretchr/testify v1.9.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/go.sum b/go.sum index 2c950da..5842a42 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,16 +13,32 @@ github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrt github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk= +github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -30,6 +48,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/mail/envelope.go b/mail/envelope.go index 57ab5cc..b9ccc76 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -23,6 +23,7 @@ import ( "math/rand" "encoding/json" "regexp" + "github.com/jhillyerd/enmime" ) // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. @@ -228,6 +229,68 @@ func RandStringBytesRmndr(n int) string { return string(b) } +func (e *Envelope) ParseContent2() error { + if e.Header == nil { + return errors.New("headers not parsed") + } + + // Clear the Content slice to prevent accumulation + e.Content = []LocalFileContent{} + + // Read path field from localfile-processor.conf.json + configPath := "localfile-processor.conf.json" + configFile, err := os.Open(configPath) + if err != nil { + return err + } + defer configFile.Close() + + var config struct { + Path string `json:"path"` + } + + decoder := json.NewDecoder(configFile) + err = decoder.Decode(&config) + + if err != nil { + return err + } + + path := config.Path + "/" + + // add the current timestamp to the path + path += fmt.Sprintf("%d", time.Now().UnixNano()) + + // add RandStringBytesRmndr(5) to the path + path += "-" + path += RandStringBytesRmndr(5) + "-goguerrilla" + + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0755) + } + + // Parse message body with enmime. + env, err := enmime.ReadEnvelope(bytes.NewReader(e.Data.Bytes())) + if err != nil { + fmt.Print(err) + return err + } + + // The plain text body is available as mime.Text. + fmt.Printf("Text Body: %v chars\n", len(env.Text)) + + // The HTML body is stored in mime.HTML. + fmt.Printf("HTML Body: %v chars\n", len(env.HTML)) + + // mime.Inlines is a slice of inlined attacments. + fmt.Printf("Inlines: %v\n", len(env.Inlines)) + + // mime.Attachments contains the non-inline attachments. + fmt.Printf("Attachments: %v\n", len(env.Attachments)) + + return nil +} + // ParseContent retrieves the smtp content itself and decodes it based on the content-type header func (e *Envelope) ParseContent() error { From 7a73c174b319a650a1d80c0f758eb7e9f836a48f Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:33:37 +0200 Subject: [PATCH 06/15] chore: use enmime --- backends/p_content_parser.go | 2 +- backends/p_debugger.go | 1 - mail/envelope.go | 327 ++++++++--------------------------- 3 files changed, 76 insertions(+), 254 deletions(-) diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go index e36c33e..af12b15 100644 --- a/backends/p_content_parser.go +++ b/backends/p_content_parser.go @@ -25,7 +25,7 @@ func ContentParser() Decorator { return func(p Processor) Processor { return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { if task == TaskSaveMail { - if err := e.ParseContent2(); err != nil { + if err := e.ParseContent(); err != nil { Log().WithError(err).Error("parse content error") } else { Log().Info("Parsed Content is: ", e.Content) diff --git a/backends/p_debugger.go b/backends/p_debugger.go index 4930d10..2bcc72b 100644 --- a/backends/p_debugger.go +++ b/backends/p_debugger.go @@ -47,7 +47,6 @@ func Debugger() Decorator { if config.LogReceivedMails { Log().Infof("Mail from: %s / to: %v", e.MailFrom.String(), e.RcptTo) Log().Info("Headers are:", e.Header) - Log().Info("Body is: ", e.Data.String() ) } if config.SleepSec > 0 { diff --git a/mail/envelope.go b/mail/envelope.go index b9ccc76..bede4af 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/sha256" - "encoding/base64" "errors" "fmt" "io" @@ -16,14 +15,10 @@ import ( "sync" "time" + "encoding/json" + "github.com/jhillyerd/enmime" "github.com/phires/go-guerrilla/mail/rfc5321" - "io/ioutil" - "mime/multipart" - "mime/quotedprintable" "math/rand" - "encoding/json" - "regexp" - "github.com/jhillyerd/enmime" ) // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. @@ -129,9 +124,9 @@ func NewAddress(str string) (*Address, error) { } type LocalFileContent struct { - PreferredDisplay string - CharSet string - LocalFile string + PreferredDisplay string + CharSet string + LocalFile string } // Envelope of Email represents a single SMTP message. @@ -148,8 +143,8 @@ type Envelope struct { Data bytes.Buffer // Subject stores the subject of the email, extracted and decoded after calling ParseHeaders() Subject string - // Content stores the decoded content of the email, extracted after calling ParseContent() - Content []LocalFileContent + // Content stores the path to the part files, extracted after calling ParseContent + Content []string // TLS is true if the email was received using a TLS connection TLS bool // Header stores the results from ParseHeaders() @@ -220,86 +215,53 @@ func (e *Envelope) ParseHeaders() error { return err } -func RandStringBytesRmndr(n int) string { - letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - b := make([]byte, n) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] - } - return string(b) -} +var ( + globalRand *rand.Rand + once sync.Once +) -func (e *Envelope) ParseContent2() error { - if e.Header == nil { - return errors.New("headers not parsed") - } +func initGlobalRand() { + once.Do(func() { + globalRand = rand.New(rand.NewSource(time.Now().UnixNano())) + }) +} - // Clear the Content slice to prevent accumulation - e.Content = []LocalFileContent{} +func RandStringBytesRmndr(n int) string { + initGlobalRand() - // Read path field from localfile-processor.conf.json - configPath := "localfile-processor.conf.json" - configFile, err := os.Open(configPath) - if err != nil { - return err - } - defer configFile.Close() + letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, n) - var config struct { - Path string `json:"path"` - } + // Create a new rand.Rand instance for each request + r := rand.New(rand.NewSource(globalRand.Int63() + time.Now().UnixNano())) - decoder := json.NewDecoder(configFile) - err = decoder.Decode(&config) + for i := range b { + b[i] = letterBytes[r.Intn(len(letterBytes))] + } + return string(b) +} +func writeFile(file_path string, decodedContent []byte) error { + file, err := os.Create(file_path) if err != nil { return err } + defer file.Close() - path := config.Path + "/" - - // add the current timestamp to the path - path += fmt.Sprintf("%d", time.Now().UnixNano()) - - // add RandStringBytesRmndr(5) to the path - path += "-" - path += RandStringBytesRmndr(5) + "-goguerrilla" - - if _, err := os.Stat(path); os.IsNotExist(err) { - os.Mkdir(path, 0755) - } - - // Parse message body with enmime. - env, err := enmime.ReadEnvelope(bytes.NewReader(e.Data.Bytes())) + _, err = file.WriteString(string(decodedContent)) if err != nil { - fmt.Print(err) return err } - - // The plain text body is available as mime.Text. - fmt.Printf("Text Body: %v chars\n", len(env.Text)) - - // The HTML body is stored in mime.HTML. - fmt.Printf("HTML Body: %v chars\n", len(env.HTML)) - - // mime.Inlines is a slice of inlined attacments. - fmt.Printf("Inlines: %v\n", len(env.Inlines)) - - // mime.Attachments contains the non-inline attachments. - fmt.Printf("Attachments: %v\n", len(env.Attachments)) - return nil } - -// ParseContent retrieves the smtp content itself and decodes it based on the content-type header func (e *Envelope) ParseContent() error { if e.Header == nil { return errors.New("headers not parsed") } // Clear the Content slice to prevent accumulation - e.Content = []LocalFileContent{} + e.Content = []string{} // Read path field from localfile-processor.conf.json configPath := "localfile-processor.conf.json" @@ -325,104 +287,64 @@ func (e *Envelope) ParseContent() error { // add the current timestamp to the path path += fmt.Sprintf("%d", time.Now().UnixNano()) - // add RandStringBytesRmndr(5) to the path path += "-" - path += RandStringBytesRmndr(5) + "-goguerrilla" + path += RandStringBytesRmndr(10) + "-goguerrilla" if _, err := os.Stat(path); os.IsNotExist(err) { os.Mkdir(path, 0755) } - headerContentType := e.Header.Get("Content-Type") - if headerContentType == "" { - return errors.New("content-type header not found") - } - - content, err := e.GetRawContent() + // Parse message body with enmime. + env, err := enmime.ReadEnvelope(bytes.NewReader(e.Data.Bytes())) if err != nil { + fmt.Print(err) return err } - // Single part message - if strings.HasPrefix(headerContentType, "text/") { - var err error - - _, params, err := mime.ParseMediaType(headerContentType) - if err != nil { - return err - } - - file_path := "" - - if params["charset"] == "utf-8" { - decodedContent, err := base64.StdEncoding.DecodeString(content) - - if err != nil { - return err - } + var localFilePath []string - file_path = path + "/root_utf8.txt" - file, err := os.Create(file_path) - - if err != nil { - return err - } - defer file.Close() - - _, err = file.WriteString(string(decodedContent)) - if err != nil { - return err - } - } else if params["charset"] == "us-ascii" { - file_path = path + "/root_us-ascii.txt" - - file, err := os.Create(file_path) - if err != nil { - return err - } - defer file.Close() + if len(env.Text) > 0 { + file_path := path + "/plain_text.txt" + writeFile(file_path, []byte(env.Text)) + localFilePath = append(localFilePath, file_path) + } - _, err = file.WriteString(string(content)) - if err != nil { - return err - } - } else { // Default to asumption: plain old ASCII - file_path = path + "/root_unknowncharset.txt" - file, err := os.Create(file_path) - if err != nil { - return err - } - defer file.Close() + if len(env.HTML) > 0 { + file_path := path + "/html_body.html" + writeFile(file_path, []byte(env.HTML)) + localFilePath = append(localFilePath, file_path) + } - _, err = file.WriteString(string(content)) - if err != nil { - return err - } + if len(env.Inlines) > 0 { + for i, inline := range env.Inlines { + fileName := BuildFileName(inline, "inline_"+fmt.Sprintf("%d", i), i) + file_path := path + "/" + fileName + writeFile(file_path, inline.Content) + localFilePath = append(localFilePath, file_path) + fmt.Println("inline: %d", i) } - var localFileContent LocalFileContent - localFileContent.PreferredDisplay = "INLINE" - localFileContent.CharSet = params["charset"] - localFileContent.LocalFile = file_path - - e.Content = append(e.Content, localFileContent) - return nil - } else if strings.HasPrefix(headerContentType, "multipart/") { // Multipart message - _, params, err := mime.ParseMediaType(headerContentType) - if err != nil { - return err + } + + if len(env.Attachments) > 0 { + for i, attachment := range env.Attachments { + fileName := BuildFileName(attachment, "attachment_"+fmt.Sprintf("%d", i), i) + file_path := path + "/" + fileName + writeFile(file_path, attachment.Content) + localFilePath = append(localFilePath, file_path) } - ParsePart(strings.NewReader(content), params["boundary"], path, e) - return nil } - return errors.New("unsupported media type: " + headerContentType) + + e.Content = localFilePath + + return nil } // BuildFileName builds a file name for a MIME part // If the name is provided in the Content-Disposition header, it will be used. // Otherwise, a name will be generated based on the radix and index. -func BuildFileName(part *multipart.Part, radix string, index int) (filename string) { +func BuildFileName(part *enmime.Part, radix string, index int) (filename string) { - filename = part.FileName() + filename = part.FileName if len(filename) > 0 { return filename } @@ -439,120 +361,21 @@ func BuildFileName(part *multipart.Part, radix string, index int) (filename stri } if e == nil { - if len(mime_type)>0 { // Arbitrary take the first extension found + if len(mime_type) > 0 { // Arbitrary take the first extension found return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) - } else { // No extension found - return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, ".unknown") - } - } - } - return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, ".unspecified") -} - -// Parse a given MIME part -func ParsePart(mime_data io.Reader, boundary string, path string, e *Envelope) { - - path += "/" + boundary - if _, err := os.Stat(path); os.IsNotExist(err) { - os.Mkdir(path, 0755) - } - - reader := multipart.NewReader(mime_data, boundary) - if reader == nil { - return - } - - // Go through each of the MIME part of the message Body with NextPart(), - for { - new_part, err := reader.NextPart() - if err == io.EOF { // End of the MIME parts - break - } - if err != nil { - fmt.Println("Error going through the next MIME part - ", err) - break - } - - mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type")) - - if err == nil && strings.HasPrefix(mediaType, "multipart/") { // This is a new multipart to be handled recursively - ParsePart(new_part, params["boundary"], path, e) - } else { - filename := BuildFileName(new_part, boundary, 1) - filepath := "" - if path == "" { - filepath = filename } else { - filepath = path + "/" + filename + return fmt.Sprintf("%s-%d-autogeneratedfilename.unidentified", radix, index) } - - WritePart(new_part, filepath, e) + } else { // No extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename.unknown", radix, index) } } -} - -// WitePart decodes the data of MIME part and writes it to the file filename. -func WritePart(part *multipart.Part, filepath string, e *Envelope) { - - // Read the data for this MIME part - part_data, err := ioutil.ReadAll(part) - if err != nil { - fmt.Println("Error reading MIME part data - ", err) - return - } - - contentTransferEncoding := strings.ToUpper(part.Header.Get("Content-Transfer-Encoding")) - contentDisposition := strings.ToUpper(part.Header.Get("Content-Disposition")) - contentType := strings.ToUpper(part.Header.Get("Content-Type")) - - contentCharset := "" - // Use a regexp to extract the charset from the content-type header - charsetRegexp := regexp.MustCompile(`(?i)charset=([^;]+)`) - matches := charsetRegexp.FindStringSubmatch(contentType) - - if len(matches) > 1 { - contentCharset = matches[1] - } - preferredDisplay := "INLINE" // Most SMTP messages don't specify Content-Disposition when it's INLINE - - // Check if the MIME part is an attachment - if strings.HasPrefix(contentDisposition, "ATTACHMENT") { - preferredDisplay = "ATTACHMENT" - } else if strings.HasPrefix(contentDisposition, "INLINE") { - preferredDisplay = "INLINE" - } - - switch { - case strings.Compare(contentTransferEncoding, "BASE64") == 0: - decoded_content, err := base64.StdEncoding.DecodeString(string(part_data)) - if err != nil { - fmt.Println("Error decoding base64 -", err) - } else { - ioutil.WriteFile(filepath, decoded_content, 0644) - } - - case strings.Compare(contentTransferEncoding, "QUOTED-PRINTABLE") == 0: - decoded_content, err := ioutil.ReadAll(quotedprintable.NewReader(bytes.NewReader(part_data))) - if err != nil { - fmt.Println("Error decoding quoted-printable -", err) - } else { - ioutil.WriteFile(filepath, decoded_content, 0644) - } - - default: - ioutil.WriteFile(filepath, part_data, 0644) - } - - var localFileContent LocalFileContent - localFileContent.PreferredDisplay = preferredDisplay - localFileContent.CharSet = contentCharset - localFileContent.LocalFile = filepath - - e.Content = append(e.Content, localFileContent) + return fmt.Sprintf("%s-%d-autogeneratedfilename.unspecified", radix, index) } -// GetContent parses the content of the email, excluding the headers + +// GetRawContent parses the content of the email, excluding the headers func (e *Envelope) GetRawContent() (string, error) { var err error From b232414e50e0e4e0e043277d6eefdaf1afbd9998 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:24:43 +0200 Subject: [PATCH 07/15] chore: move WriteFile function outside of envelope.go --- mail/envelope.go | 22 ++++------------------ mail/utils.go | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 mail/utils.go diff --git a/mail/envelope.go b/mail/envelope.go index bede4af..66e8d2f 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -241,19 +241,6 @@ func RandStringBytesRmndr(n int) string { return string(b) } -func writeFile(file_path string, decodedContent []byte) error { - file, err := os.Create(file_path) - if err != nil { - return err - } - defer file.Close() - - _, err = file.WriteString(string(decodedContent)) - if err != nil { - return err - } - return nil -} func (e *Envelope) ParseContent() error { if e.Header == nil { @@ -305,13 +292,13 @@ func (e *Envelope) ParseContent() error { if len(env.Text) > 0 { file_path := path + "/plain_text.txt" - writeFile(file_path, []byte(env.Text)) + WriteFile(file_path, []byte(env.Text)) localFilePath = append(localFilePath, file_path) } if len(env.HTML) > 0 { file_path := path + "/html_body.html" - writeFile(file_path, []byte(env.HTML)) + WriteFile(file_path, []byte(env.HTML)) localFilePath = append(localFilePath, file_path) } @@ -319,9 +306,8 @@ func (e *Envelope) ParseContent() error { for i, inline := range env.Inlines { fileName := BuildFileName(inline, "inline_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName - writeFile(file_path, inline.Content) + WriteFile(file_path, inline.Content) localFilePath = append(localFilePath, file_path) - fmt.Println("inline: %d", i) } } @@ -329,7 +315,7 @@ func (e *Envelope) ParseContent() error { for i, attachment := range env.Attachments { fileName := BuildFileName(attachment, "attachment_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName - writeFile(file_path, attachment.Content) + WriteFile(file_path, attachment.Content) localFilePath = append(localFilePath, file_path) } } diff --git a/mail/utils.go b/mail/utils.go new file mode 100644 index 0000000..feb3982 --- /dev/null +++ b/mail/utils.go @@ -0,0 +1,19 @@ +package mail + +import "os" + + +// Wite a file with the given content +func WriteFile(file_path string, decodedContent []byte) error { + file, err := os.Create(file_path) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(string(decodedContent)) + if err != nil { + return err + } + return nil +} \ No newline at end of file From dd44d822e34ee9d518c267682b9b1fcd2bc302ec Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:40:07 +0200 Subject: [PATCH 08/15] chore: use queueId to ensure uniqueness of path --- mail/envelope.go | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/mail/envelope.go b/mail/envelope.go index 66e8d2f..b0aeaa7 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -18,7 +18,6 @@ import ( "encoding/json" "github.com/jhillyerd/enmime" "github.com/phires/go-guerrilla/mail/rfc5321" - "math/rand" ) // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. @@ -215,33 +214,8 @@ func (e *Envelope) ParseHeaders() error { return err } -var ( - globalRand *rand.Rand - once sync.Once -) - -func initGlobalRand() { - once.Do(func() { - globalRand = rand.New(rand.NewSource(time.Now().UnixNano())) - }) -} - -func RandStringBytesRmndr(n int) string { - initGlobalRand() - - letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - b := make([]byte, n) - - // Create a new rand.Rand instance for each request - r := rand.New(rand.NewSource(globalRand.Int63() + time.Now().UnixNano())) - - for i := range b { - b[i] = letterBytes[r.Intn(len(letterBytes))] - } - return string(b) -} - - +// Retrieve the content of the email and save +// each raw or MIME part to the local file system func (e *Envelope) ParseContent() error { if e.Header == nil { return errors.New("headers not parsed") @@ -270,12 +244,7 @@ func (e *Envelope) ParseContent() error { } path := config.Path + "/" - - // add the current timestamp to the path - path += fmt.Sprintf("%d", time.Now().UnixNano()) - - path += "-" - path += RandStringBytesRmndr(10) + "-goguerrilla" + path += e.QueuedId + "-goguerrilla" if _, err := os.Stat(path); os.IsNotExist(err) { os.Mkdir(path, 0755) From 5984a03daff459f6c703bcc9bb02eafa399df665 Mon Sep 17 00:00:00 2001 From: Guilhem Martin Date: Mon, 22 Jul 2024 11:33:54 +0200 Subject: [PATCH 09/15] chore: add preferred display info --- backends/p_content_parser.go | 2 +- mail/envelope.go | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go index af12b15..c4d7a39 100644 --- a/backends/p_content_parser.go +++ b/backends/p_content_parser.go @@ -28,7 +28,7 @@ func ContentParser() Decorator { if err := e.ParseContent(); err != nil { Log().WithError(err).Error("parse content error") } else { - Log().Info("Parsed Content is: ", e.Content) + Log().Info("Parsed Content is: ", e.LocalFileContent) } // next processor return p.Process(e, task) diff --git a/mail/envelope.go b/mail/envelope.go index b0aeaa7..18aefa1 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -124,7 +124,6 @@ func NewAddress(str string) (*Address, error) { type LocalFileContent struct { PreferredDisplay string - CharSet string LocalFile string } @@ -143,7 +142,7 @@ type Envelope struct { // Subject stores the subject of the email, extracted and decoded after calling ParseHeaders() Subject string // Content stores the path to the part files, extracted after calling ParseContent - Content []string + LocalFileContent []LocalFileContent // TLS is true if the email was received using a TLS connection TLS bool // Header stores the results from ParseHeaders() @@ -222,7 +221,7 @@ func (e *Envelope) ParseContent() error { } // Clear the Content slice to prevent accumulation - e.Content = []string{} + e.LocalFileContent = []LocalFileContent{} // Read path field from localfile-processor.conf.json configPath := "localfile-processor.conf.json" @@ -257,18 +256,18 @@ func (e *Envelope) ParseContent() error { return err } - var localFilePath []string + var localFileContent []LocalFileContent if len(env.Text) > 0 { - file_path := path + "/plain_text.txt" + file_path := path + "/body.txt" WriteFile(file_path, []byte(env.Text)) - localFilePath = append(localFilePath, file_path) + localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "PLAIN", LocalFile: file_path}) } if len(env.HTML) > 0 { - file_path := path + "/html_body.html" + file_path := path + "/body.html" WriteFile(file_path, []byte(env.HTML)) - localFilePath = append(localFilePath, file_path) + localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "HTML", LocalFile: file_path}) } if len(env.Inlines) > 0 { @@ -276,7 +275,7 @@ func (e *Envelope) ParseContent() error { fileName := BuildFileName(inline, "inline_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName WriteFile(file_path, inline.Content) - localFilePath = append(localFilePath, file_path) + localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "INLINE", LocalFile: file_path}) } } @@ -285,11 +284,11 @@ func (e *Envelope) ParseContent() error { fileName := BuildFileName(attachment, "attachment_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName WriteFile(file_path, attachment.Content) - localFilePath = append(localFilePath, file_path) + localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "ATTACHMENT", LocalFile: file_path}) } } - e.Content = localFilePath + e.LocalFileContent = localFileContent return nil } From 37d9bf35386e70f7725d1c3e295fc3e3b5daf6e6 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:08:30 +0200 Subject: [PATCH 10/15] docs: improve comment --- mail/envelope.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/envelope.go b/mail/envelope.go index 18aefa1..e304c0f 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -307,7 +307,7 @@ func BuildFileName(part *enmime.Part, radix string, index int) (filename string) if err == nil { mime_type, e := mime.ExtensionsByType(mediaType) - // Remove in mime_type any extension not starting with a dot (it SHOULD not happen but it DID happen) + // Remove any extension not starting with a dot - it should not happen but it might happen for i, e := range mime_type { if !strings.HasPrefix(e, ".") { mime_type = append(mime_type[:i], mime_type[i+1:]...) From f86c8ecbb595f99271d010403c6273c909ef32c2 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:43:29 +0200 Subject: [PATCH 11/15] chore: add local_storage_path parameter to goguerrilla.conf.json for specifying the local folder where to store the parts --- backends/p_content_parser.go | 20 +++++++++++++- goguerrilla.conf.sample | 3 ++- localfile-processor.conf.json | 4 --- mail/envelope.go | 49 ++++++++++++++++------------------- 4 files changed, 43 insertions(+), 33 deletions(-) delete mode 100644 localfile-processor.conf.json diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go index c4d7a39..9a9ee1a 100644 --- a/backends/p_content_parser.go +++ b/backends/p_content_parser.go @@ -21,11 +21,29 @@ func init() { } } +type ContentParserProcessorConfig struct { + LocalStoragePath string `json:"local_storage_path"` +} + func ContentParser() Decorator { + + var config *ContentParserProcessorConfig + + Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error { + configType := BaseConfig(&ContentParserProcessorConfig{}) + bcfg, err := Svc.ExtractConfig(backendConfig, configType) + if err != nil { + return err + } + config = bcfg.(*ContentParserProcessorConfig) + return nil + })) + + return func(p Processor) Processor { return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { if task == TaskSaveMail { - if err := e.ParseContent(); err != nil { + if err := e.ParseContent(config.LocalStoragePath); err != nil { Log().WithError(err).Error("parse content error") } else { Log().Info("Parsed Content is: ", e.LocalFileContent) diff --git a/goguerrilla.conf.sample b/goguerrilla.conf.sample index deaaf10..b902d1e 100644 --- a/goguerrilla.conf.sample +++ b/goguerrilla.conf.sample @@ -15,7 +15,8 @@ "save_process" : "HeadersParser|Header|Debugger", "primary_mail_host" : "mail.example.com", "gw_save_timeout" : "30s", - "gw_val_rcpt_timeout" : "3s" + "gw_val_rcpt_timeout" : "3s", + "local_storage_path" : "/tmp/go-guerrilla/" }, "servers" : [ { diff --git a/localfile-processor.conf.json b/localfile-processor.conf.json deleted file mode 100644 index b0c1b2c..0000000 --- a/localfile-processor.conf.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "path" : "/tmp", - "description" : "This is the path where the files will be stored, no trailing slash" -} diff --git a/mail/envelope.go b/mail/envelope.go index e304c0f..4687c31 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -11,11 +11,11 @@ import ( "net" "net/textproto" "os" + "path/filepath" "strings" "sync" "time" - "encoding/json" "github.com/jhillyerd/enmime" "github.com/phires/go-guerrilla/mail/rfc5321" ) @@ -215,7 +215,7 @@ func (e *Envelope) ParseHeaders() error { // Retrieve the content of the email and save // each raw or MIME part to the local file system -func (e *Envelope) ParseContent() error { +func (e *Envelope) ParseContent(storagePath string) error { if e.Header == nil { return errors.New("headers not parsed") } @@ -223,30 +223,13 @@ func (e *Envelope) ParseContent() error { // Clear the Content slice to prevent accumulation e.LocalFileContent = []LocalFileContent{} - // Read path field from localfile-processor.conf.json - configPath := "localfile-processor.conf.json" - configFile, err := os.Open(configPath) - if err != nil { - return err - } - defer configFile.Close() - - var config struct { - Path string `json:"path"` - } - - decoder := json.NewDecoder(configFile) - err = decoder.Decode(&config) - - if err != nil { - return err - } + path := storagePath + "/" + e.QueuedId - path := config.Path + "/" - path += e.QueuedId + "-goguerrilla" + // Sanitize the path + path = filepath.Clean(path) if _, err := os.Stat(path); os.IsNotExist(err) { - os.Mkdir(path, 0755) + os.MkdirAll(path, 0755) } // Parse message body with enmime. @@ -260,13 +243,19 @@ func (e *Envelope) ParseContent() error { if len(env.Text) > 0 { file_path := path + "/body.txt" - WriteFile(file_path, []byte(env.Text)) + err := WriteFile(file_path, []byte(env.Text)) + if err != nil { + return err + } localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "PLAIN", LocalFile: file_path}) } if len(env.HTML) > 0 { file_path := path + "/body.html" - WriteFile(file_path, []byte(env.HTML)) + err := WriteFile(file_path, []byte(env.HTML)) + if err != nil { + return err + } localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "HTML", LocalFile: file_path}) } @@ -274,7 +263,10 @@ func (e *Envelope) ParseContent() error { for i, inline := range env.Inlines { fileName := BuildFileName(inline, "inline_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName - WriteFile(file_path, inline.Content) + err := WriteFile(file_path, inline.Content) + if err != nil { + return err + } localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "INLINE", LocalFile: file_path}) } } @@ -283,7 +275,10 @@ func (e *Envelope) ParseContent() error { for i, attachment := range env.Attachments { fileName := BuildFileName(attachment, "attachment_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName - WriteFile(file_path, attachment.Content) + err := WriteFile(file_path, attachment.Content) + if err != nil { + return err + } localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "ATTACHMENT", LocalFile: file_path}) } } From fc53759db821c13de95a41812f46ae0823f9ae45 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:15:19 +0200 Subject: [PATCH 12/15] chore: migrate util function outside of envelope --- mail/envelope.go | 36 ------------------------------------ mail/utils.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/mail/envelope.go b/mail/envelope.go index 4687c31..216e35b 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -288,42 +288,6 @@ func (e *Envelope) ParseContent(storagePath string) error { return nil } -// BuildFileName builds a file name for a MIME part -// If the name is provided in the Content-Disposition header, it will be used. -// Otherwise, a name will be generated based on the radix and index. -func BuildFileName(part *enmime.Part, radix string, index int) (filename string) { - - filename = part.FileName - if len(filename) > 0 { - return filename - } - - mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) - if err == nil { - mime_type, e := mime.ExtensionsByType(mediaType) - - // Remove any extension not starting with a dot - it should not happen but it might happen - for i, e := range mime_type { - if !strings.HasPrefix(e, ".") { - mime_type = append(mime_type[:i], mime_type[i+1:]...) - } - } - - if e == nil { - if len(mime_type) > 0 { // Arbitrary take the first extension found - return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) - } else { - return fmt.Sprintf("%s-%d-autogeneratedfilename.unidentified", radix, index) - } - } else { // No extension found - return fmt.Sprintf("%s-%d-autogeneratedfilename.unknown", radix, index) - } - } - - return fmt.Sprintf("%s-%d-autogeneratedfilename.unspecified", radix, index) -} - - // GetRawContent parses the content of the email, excluding the headers func (e *Envelope) GetRawContent() (string, error) { var err error diff --git a/mail/utils.go b/mail/utils.go index feb3982..d750557 100644 --- a/mail/utils.go +++ b/mail/utils.go @@ -1,6 +1,12 @@ package mail -import "os" +import ( + "os" + "mime" + "strings" + "github.com/jhillyerd/enmime" + "fmt" +) // Wite a file with the given content @@ -16,4 +22,39 @@ func WriteFile(file_path string, decodedContent []byte) error { return err } return nil +} + +// BuildFileName builds a file name for a MIME part +// If the name is provided in the Content-Disposition header, it will be used. +// Otherwise, a name will be generated based on the radix and index. +func BuildFileName(part *enmime.Part, radix string, index int) (filename string) { + + filename = part.FileName + if len(filename) > 0 { + return filename + } + + mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) + if err == nil { + mime_type, e := mime.ExtensionsByType(mediaType) + + // Remove any extension not starting with a dot - it should not happen but it might happen + for i, e := range mime_type { + if !strings.HasPrefix(e, ".") { + mime_type = append(mime_type[:i], mime_type[i+1:]...) + } + } + + if e == nil { + if len(mime_type) > 0 { // Arbitrary take the first extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename%s", radix, index, mime_type[0]) + } else { + return fmt.Sprintf("%s-%d-autogeneratedfilename.unidentified", radix, index) + } + } else { // No extension found + return fmt.Sprintf("%s-%d-autogeneratedfilename.unknown", radix, index) + } + } + + return fmt.Sprintf("%s-%d-autogeneratedfilename.unspecified", radix, index) } \ No newline at end of file From deb281f7432f350f4e9965e5b522571d9608e9f1 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:17:03 +0200 Subject: [PATCH 13/15] tests: add unit tests for envelope utils --- mail/utils_test.go | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 mail/utils_test.go diff --git a/mail/utils_test.go b/mail/utils_test.go new file mode 100644 index 0000000..44c89c2 --- /dev/null +++ b/mail/utils_test.go @@ -0,0 +1,129 @@ +package mail + +import ( + "io/ioutil" + "os" + "testing" + "net/textproto" + + "github.com/jhillyerd/enmime" +) + +// Declare global variables +var path string + +func WriteAndReadFileTeardown(t *testing.T) { + // Your teardown code goes here + // Remove the file at path + err := os.Remove(path) + if err != nil { + t.Error("error removing file:", err) + } +} + +func TestWriteAndReadFile(t *testing.T) { + content := "Hello, World!" + path = "tmp_test_file" + err := WriteFile(path, []byte(content)) + if err != nil { + t.Error("error writing file:", err) + } + defer WriteAndReadFileTeardown(t) + + file, err := os.Open(path) + if err != nil { + t.Error("error opening file:", err) + } else { + defer file.Close() + // Read file content + data, err := ioutil.ReadAll(file) + if err != nil { + t.Error("error reading file:", err) + } + + readContent := string(data) + if readContent != content { + t.Errorf("expected %s but got %s", content, readContent) + } + } +} +func TestBuildFileName(t *testing.T) { + part := &enmime.Part{ + FileName: "", + Header: make(textproto.MIMEHeader), + } + part.Header.Set("Content-Type", "image/jpeg") + + radix := "test" + index := 1 + + expected := []string{"test-1-autogeneratedfilename.jpeg", + "test-1-autogeneratedfilename.jpg", + "test-1-autogeneratedfilename.jpe", + "test-1-autogeneratedfilename.jif", + "test-1-autogeneratedfilename.jfif" } + result := BuildFileName(part, radix, index) + + found := false + for _, e := range expected { + if e == result { + found = true + break + } + } + if !found { + t.Errorf("expected %s but got %s", expected, result) + } +} + +func TestBuildFileName_NoContentType(t *testing.T) { + part := &enmime.Part{ + FileName: "", + Header: make(textproto.MIMEHeader), + } + + radix := "test" + index := 1 + + expected := "test-1-autogeneratedfilename.unspecified" + result := BuildFileName(part, radix, index) + + if result != expected { + t.Errorf("expected %s but got %s", expected, result) + } +} + +func TestBuildFileName_InvalidContentType(t *testing.T) { + part := &enmime.Part{ + FileName: "", + Header: make(textproto.MIMEHeader), + } + part.Header.Set("Content-Type", "invalid") + + radix := "test" + index := 1 + + expected := "test-1-autogeneratedfilename.unidentified" + result := BuildFileName(part, radix, index) + + if result != expected { + t.Errorf("expected %s but got %s", expected, result) + } +} + +func TestBuildFileName_WithFileName(t *testing.T) { + part := &enmime.Part{ + FileName: "custom_filename.txt", + Header: make(textproto.MIMEHeader), + } + + radix := "test" + index := 1 + + expected := "custom_filename.txt" + result := BuildFileName(part, radix, index) + + if result != expected { + t.Errorf("expected %s but got %s", expected, result) + } +} \ No newline at end of file From 0fcea48bf0b15aa107cff9438faa9fd7cfb27124 Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Fri, 26 Jul 2024 21:16:05 +0200 Subject: [PATCH 14/15] feat: add local files dumper in addition to content parser --- backends/p_content_parser.go | 24 +++------------ backends/p_local_files.go | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 backends/p_local_files.go diff --git a/backends/p_content_parser.go b/backends/p_content_parser.go index 9a9ee1a..f384873 100644 --- a/backends/p_content_parser.go +++ b/backends/p_content_parser.go @@ -9,11 +9,11 @@ import ( // ---------------------------------------------------------------------------------- // Description : Parses and decodes the content // ---------------------------------------------------------------------------------- -// Config Options: Specify the location path to save the parts of the email +// Config Options: none // --------------:------------------------------------------------------------------- // Input : envelope // ---------------------------------------------------------------------------------- -// Output : Content will be populated in e.Content +// Output : Content will be populated in e.EnvelopeBody // ---------------------------------------------------------------------------------- func init() { processors["contentparser"] = func() Decorator { @@ -21,32 +21,16 @@ func init() { } } -type ContentParserProcessorConfig struct { - LocalStoragePath string `json:"local_storage_path"` -} - func ContentParser() Decorator { - var config *ContentParserProcessorConfig - - Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error { - configType := BaseConfig(&ContentParserProcessorConfig{}) - bcfg, err := Svc.ExtractConfig(backendConfig, configType) - if err != nil { - return err - } - config = bcfg.(*ContentParserProcessorConfig) - return nil - })) - return func(p Processor) Processor { return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { if task == TaskSaveMail { - if err := e.ParseContent(config.LocalStoragePath); err != nil { + if err := e.ParseContent(); err != nil { Log().WithError(err).Error("parse content error") } else { - Log().Info("Parsed Content is: ", e.LocalFileContent) + Log().Info("Parsed Content is: ", e.EnvelopeBody) } // next processor return p.Process(e, task) diff --git a/backends/p_local_files.go b/backends/p_local_files.go new file mode 100644 index 0000000..8920b0a --- /dev/null +++ b/backends/p_local_files.go @@ -0,0 +1,60 @@ +package backends + +import ( + "github.com/phires/go-guerrilla/mail" +) + +// ---------------------------------------------------------------------------------- +// Processor Name: localfiles +// ---------------------------------------------------------------------------------- +// Description : Dump the decoded content to local files +// ---------------------------------------------------------------------------------- +// Config Options: Specify the location path to save the parts of the email +// --------------:------------------------------------------------------------------- +// Input : envelope +// ---------------------------------------------------------------------------------- +// Output : Saved paths will be populated in e.LocalFilesPaths +// ---------------------------------------------------------------------------------- +func init() { + processors["localfiles"] = func() Decorator { + return LocalFiles() + } +} + +type LocalFilesProcessorConfig struct { + LocalStoragePath string `json:"local_storage_path"` +} + +func LocalFiles() Decorator { + + var config *LocalFilesProcessorConfig + + Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error { + configType := BaseConfig(&LocalFilesProcessorConfig{}) + bcfg, err := Svc.ExtractConfig(backendConfig, configType) + if err != nil { + return err + } + config = bcfg.(*LocalFilesProcessorConfig) + return nil + })) + + + return func(p Processor) Processor { + return ProcessWith(func(e *mail.Envelope, task SelectTask) (Result, error) { + if task == TaskSaveMail { + if err := e.SaveLocalFiles(config.LocalStoragePath); err != nil { + Log().WithError(err).Error("save local file error") + } else { + Log().Info("Dumped Content is: ", e.LocalFilesPaths) + } + // next processor + return p.Process(e, task) + } else { + // next processor + return p.Process(e, task) + } + }) + } +} + From 2d680d5ce46a40492677ec8337c0d68ca507dfda Mon Sep 17 00:00:00 2001 From: Guilhem Martin <226993+guilhem-martin@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:40:28 +0200 Subject: [PATCH 15/15] feat: add local file support --- mail/envelope.go | 98 ++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/mail/envelope.go b/mail/envelope.go index 216e35b..e84a0ce 100644 --- a/mail/envelope.go +++ b/mail/envelope.go @@ -122,11 +122,12 @@ func NewAddress(str string) (*Address, error) { return a, nil } -type LocalFileContent struct { +type LocalFilesPaths struct { PreferredDisplay string - LocalFile string + Path string } + // Envelope of Email represents a single SMTP message. type Envelope struct { // Remote IP address @@ -141,8 +142,10 @@ type Envelope struct { Data bytes.Buffer // Subject stores the subject of the email, extracted and decoded after calling ParseHeaders() Subject string - // Content stores the path to the part files, extracted after calling ParseContent - LocalFileContent []LocalFileContent + // EnvelopeBody stores the parsed email content + EnvelopeBody enmime.Envelope + // LocalFilesPaths stores the path to the part files, extracted after calling ParseContent + LocalFilesPaths []LocalFilesPaths // TLS is true if the email was received using a TLS connection TLS bool // Header stores the results from ParseHeaders() @@ -215,13 +218,39 @@ func (e *Envelope) ParseHeaders() error { // Retrieve the content of the email and save // each raw or MIME part to the local file system -func (e *Envelope) ParseContent(storagePath string) error { +func (e *Envelope) ParseContent() error { if e.Header == nil { return errors.New("headers not parsed") } - // Clear the Content slice to prevent accumulation - e.LocalFileContent = []LocalFileContent{} + // Clear the Content to prevent accumulation + e.EnvelopeBody = enmime.Envelope{} + + // Parse message body with enmime. + env, err := enmime.ReadEnvelope(bytes.NewReader(e.Data.Bytes())) + if err != nil { + fmt.Print(err) + return err + } + + e.EnvelopeBody = *env + return nil +} + + +// Retrieve the content of the email and save +// each raw or MIME part to the local file system +func (e *Envelope) SaveLocalFiles(storagePath string) error { + if e.Header == nil { + return errors.New("headers not parsed") + } + + if len(e.EnvelopeBody.Text) == 0 && len(e.EnvelopeBody.HTML) == 0 && len(e.EnvelopeBody.Inlines) == 0 && len(e.EnvelopeBody.Attachments) == 0 { + return errors.New("content not parsed") + } + + // Clear LocalFilesPaths to prevent accumulation + e.LocalFilesPaths = []LocalFilesPaths{} path := storagePath + "/" + e.QueuedId @@ -232,82 +261,55 @@ func (e *Envelope) ParseContent(storagePath string) error { os.MkdirAll(path, 0755) } - // Parse message body with enmime. - env, err := enmime.ReadEnvelope(bytes.NewReader(e.Data.Bytes())) - if err != nil { - fmt.Print(err) - return err - } - - var localFileContent []LocalFileContent + var localFilesPaths []LocalFilesPaths - if len(env.Text) > 0 { + if len(e.EnvelopeBody.Text) > 0 { file_path := path + "/body.txt" - err := WriteFile(file_path, []byte(env.Text)) + err := WriteFile(file_path, []byte(e.EnvelopeBody.Text)) if err != nil { return err } - localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "PLAIN", LocalFile: file_path}) + localFilesPaths = append(localFilesPaths, LocalFilesPaths{PreferredDisplay: "PLAIN", Path: file_path}) } - if len(env.HTML) > 0 { + if len(e.EnvelopeBody.HTML) > 0 { file_path := path + "/body.html" - err := WriteFile(file_path, []byte(env.HTML)) + err := WriteFile(file_path, []byte(e.EnvelopeBody.HTML)) if err != nil { return err } - localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "HTML", LocalFile: file_path}) + localFilesPaths = append(localFilesPaths, LocalFilesPaths{PreferredDisplay: "HTML", Path: file_path}) } - if len(env.Inlines) > 0 { - for i, inline := range env.Inlines { + if len(e.EnvelopeBody.Inlines) > 0 { + for i, inline := range e.EnvelopeBody.Inlines { fileName := BuildFileName(inline, "inline_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName err := WriteFile(file_path, inline.Content) if err != nil { return err } - localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "INLINE", LocalFile: file_path}) + localFilesPaths = append(localFilesPaths, LocalFilesPaths{PreferredDisplay: "INLINE", Path: file_path}) } } - if len(env.Attachments) > 0 { - for i, attachment := range env.Attachments { + if len(e.EnvelopeBody.Attachments) > 0 { + for i, attachment := range e.EnvelopeBody.Attachments { fileName := BuildFileName(attachment, "attachment_"+fmt.Sprintf("%d", i), i) file_path := path + "/" + fileName err := WriteFile(file_path, attachment.Content) if err != nil { return err } - localFileContent = append(localFileContent, LocalFileContent{PreferredDisplay: "ATTACHMENT", LocalFile: file_path}) + localFilesPaths = append(localFilesPaths, LocalFilesPaths{PreferredDisplay: "ATTACHMENT", Path: file_path}) } } - e.LocalFileContent = localFileContent + e.LocalFilesPaths = localFilesPaths return nil } -// GetRawContent parses the content of the email, excluding the headers -func (e *Envelope) GetRawContent() (string, error) { - var err error - - buf := e.Data.Bytes() - // find where the header ends, assuming that over 30 kb would be max - if len(buf) > maxHeaderChunk { - buf = buf[:maxHeaderChunk] - } - - contentStart := bytes.Index(buf, []byte{'\n', '\n'}) // the first two new-lines chars are the End Of Header / Start Of Content - if contentStart > -1 { - content := buf[contentStart+2:] - return string(content), nil - } else { - err = errors.New("header not found") - } - return "", err -} - // Len returns the number of bytes that would be in the reader returned by NewReader() func (e *Envelope) Len() int { return len(e.DeliveryHeader) + e.Data.Len()