nthmail/pkg/mail_utils/mail_utils.go
Guilherme Rugai Freire a8860ea560
add support for cc and bcc
now it can handle when mails are carbon-copied
fixes #1 fixes #2
2025-08-28 09:46:20 -03:00

268 lines
5.1 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
Cc []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.Subject, _ = dec.DecodeHeader(mail_msg.Header.Get("Subject"))
to_addrs, _ := mail_msg.Header.AddressList("To")
m.To = make([]string, len(to_addrs))
for i, a := range to_addrs {
m.To[i] = a.Address
}
cc_addrs, _ := mail_msg.Header.AddressList("Cc")
m.Cc = make([]string, len(to_addrs))
for i, a := range cc_addrs {
m.Cc[i] = a.Address
}
bcc_addrs, _ := mail_msg.Header.AddressList("Bcc")
m.Bcc = make([]string, len(to_addrs))
for i, a := range bcc_addrs {
m.Bcc[i] = a.Address
}
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
}