nthmail/pkg/mail_utils/mail_utils.go
Guilherme Rugai Freire 30b727b9ce
parse mail content-enconding on text/* mails
before, if the mail content-type was text, it would not consider the
content encoding, only if it was multipart.

now this step on the mail parsing is performed both on multipart mails
and in text mails
2024-07-18 12:37:19 -03:00

251 lines
4.7 KiB
Go

package mail_utils
import (
"bytes"
"encoding/base64"
"errors"
"io"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/mail"
"slices"
"strings"
"time"
)
type MIMEType uint8
type MediaType uint8
const (
PlainText MIMEType = iota
Html
Markdown
)
const (
NotMultipart MediaType = iota
Alternative
Mixed
)
type Mail_body struct {
MimeType MIMEType
Data string
}
type Mail_obj struct {
Id int
From string
Date time.Time
To string
Bcc string
Subject string
Body []Mail_body
MediaType
PreferedBodyIndex int
}
func Parse_mime_format(s string) (MIMEType, bool) {
var t MIMEType
if s == "" {
return t, false
}
switch {
case strings.EqualFold(s, "html"):
t = Html
case strings.EqualFold(s, "md"):
t = Markdown
case strings.EqualFold(s, "text"):
t = PlainText
default:
return t, false
}
return t, true
}
func Parse_mail(m_data []byte, header_only bool) (Mail_obj, error) {
var m Mail_obj
mail_msg, err := mail.ReadMessage(bytes.NewReader(m_data))
if err != nil {
return m, errors.New("Could not read message")
}
// HEADERS
dec := new(mime.WordDecoder)
m.From, _ = dec.DecodeHeader(mail_msg.Header.Get("From"))
m.To, _ = dec.DecodeHeader(mail_msg.Header.Get("To"))
m.Bcc, _ = dec.DecodeHeader(mail_msg.Header.Get("Bcc"))
m.Subject, _ = dec.DecodeHeader(mail_msg.Header.Get("Subject"))
if header_only {
return m, nil
}
content_type := mail_msg.Header.Get("Content-Type")
mediaType, params, err := mime.ParseMediaType(content_type)
if err != nil {
return m, err
}
if content_type == "" || !strings.HasPrefix(mediaType, "multipart/") {
txt_bytes, err := io.ReadAll(mail_msg.Body)
if err != nil {
return m, err
}
var body Mail_body
mail_body, err := Parse_mail_part(mail_msg.Header, txt_bytes)
if err != nil {
return m, err
}
body.Data = mail_body.Data
body.MimeType = mail_body.MimeType
m.MediaType = NotMultipart
m.Body = append(m.Body, body)
return m, nil
}
if mediaType == "multipart/mixed" {
m.MediaType = Mixed
} else if mediaType == "multipart/alternative" {
m.MediaType = Alternative
} else {
return m, errors.New("Not supported multipart type")
}
body, err := Parse_mail_multipart(mail_msg.Body, params["boundary"])
if err != nil {
return m, err
}
m.Body = body
return m, nil
}
type Header interface {
Get(string) string
}
func Parse_mail_part(header Header, body []byte) (Mail_body, error) {
content_transfer_encoding := header.Get("Content-Transfer-Encoding")
content_type := header.Get("Content-Type")
var mail_body Mail_body
switch {
case strings.HasPrefix(content_type, "text/plain"):
mail_body.MimeType = PlainText
case strings.HasPrefix(content_type, "text/markdown"):
mail_body.MimeType = Markdown
case strings.HasPrefix(content_type, "text/html"):
mail_body.MimeType = Html
default:
return mail_body, errors.New("Content type not supported: " + content_type)
}
switch {
case strings.EqualFold(content_transfer_encoding, "BASE64"):
decoded_content, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return mail_body, err
}
mail_body.Data = string(decoded_content)
case strings.EqualFold(content_transfer_encoding, "QUOTED-PRINTABLE"):
decoded_content, err := io.ReadAll(quotedprintable.NewReader(bytes.NewReader(body)))
if err != nil {
return mail_body, err
}
mail_body.Data = string(decoded_content)
default:
mail_body.Data = string(body)
}
return mail_body, nil
}
func Parse_mail_multipart(mime_data io.Reader, boundary string) ([]Mail_body, error) {
var body []Mail_body
reader := multipart.NewReader(mime_data, boundary)
if reader == nil {
return body, nil
}
for {
new_part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return body, err
}
mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type"))
if err != nil {
return body, err
}
if strings.HasPrefix(mediaType, "multipart/") {
body_part, err := Parse_mail_multipart(new_part, params["boundary"])
if err != nil {
return body, err
}
body = append(body, body_part...)
} else {
part_data, err := io.ReadAll(new_part)
if err != nil {
return body, err
}
part_body, err := Parse_mail_part(new_part.Header, part_data)
if err != nil {
return body, err
}
body = append(body, part_body)
}
}
return body, nil
}
func Set_format_index(m Mail_obj, format MIMEType, pref bool) Mail_obj {
priority := []MIMEType{Html, Markdown, PlainText}
m.PreferedBodyIndex = -1
curr_p := len(priority)
for i, b := range m.Body {
if pref && format == b.MimeType {
m.PreferedBodyIndex = i
break
}
p := slices.Index(priority, b.MimeType)
if p < curr_p {
curr_p = p
m.PreferedBodyIndex = i
}
}
return m
}