Files
EMLy/backend/utils/mail/eml_reader.go

290 lines
7.1 KiB
Go

package internal
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/mail"
"os"
"strings"
"unicode/utf8"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
type EmailAttachment struct {
Filename string `json:"filename"`
ContentType string `json:"contentType"`
Data []byte `json:"data"`
}
type EmailData struct {
From string `json:"from"`
To []string `json:"to"`
Cc []string `json:"cc"`
Bcc []string `json:"bcc"`
Subject string `json:"subject"`
Body string `json:"body"`
Attachments []EmailAttachment `json:"attachments"`
IsPec bool `json:"isPec"`
HasInnerEmail bool `json:"hasInnerEmail"`
}
func ReadEmlFile(filePath string) (*EmailData, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
email, err := Parse(file)
if err != nil {
return nil, fmt.Errorf("failed to parse email: %w", err)
}
// Format addresses
formatAddress := func(addr []*mail.Address) []string {
var result []string
for _, a := range addr {
result = append(result, convertToUTF8(a.String()))
}
return result
}
// Determine body (prefer HTML)
body := email.HTMLBody
if body == "" {
body = email.TextBody
}
// Process attachments list and PEC detection
var attachments []EmailAttachment
var hasDatiCert, hasSmime, hasInnerEmail bool
// Process embedded files (inline images) -> add to body AND add as attachments
for _, ef := range email.EmbeddedFiles {
data, err := io.ReadAll(ef.Data)
if err != nil {
continue
}
// Convert to base64
b64 := base64.StdEncoding.EncodeToString(data)
mimeType := ef.ContentType
if parts := strings.Split(mimeType, ";"); len(parts) > 0 {
mimeType = strings.TrimSpace(parts[0])
}
if mimeType == "" {
mimeType = "application/octet-stream"
}
// Create data URI
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, b64)
// Replace cid:reference with data URI in HTML body
// ef.CID is already trimmed of <>
target := "cid:" + ef.CID
body = strings.ReplaceAll(body, target, dataURI)
// ALSO ADD AS ATTACHMENTS for the viewer
filename := ef.CID
if filename == "" {
filename = "embedded_image"
}
// If no extension, try to infer from mimetype
if !strings.Contains(filename, ".") {
ext := "dat"
switch mimeType {
case "image/jpeg":
ext = "jpg"
case "image/png":
ext = "png"
case "image/gif":
ext = "gif"
case "application/pdf":
ext = "pdf"
default:
if parts := strings.Split(mimeType, "/"); len(parts) > 1 {
ext = parts[1]
}
}
filename = fmt.Sprintf("%s.%s", filename, ext)
}
attachments = append(attachments, EmailAttachment{
Filename: filename,
ContentType: mimeType,
Data: data,
})
}
// Process standard attachments
for _, att := range email.Attachments {
data, err := io.ReadAll(att.Data)
if err != nil {
continue // Handle error or skip? Skipping for now.
}
// PEC Detection Logic
filenameLower := strings.ToLower(att.Filename)
if filenameLower == "daticert.xml" {
hasDatiCert = true
}
if filenameLower == "smime.p7s" {
hasSmime = true
}
if strings.HasSuffix(filenameLower, ".eml") {
hasInnerEmail = true
}
attachments = append(attachments, EmailAttachment{
Filename: att.Filename,
ContentType: att.ContentType,
Data: data,
})
}
isPec := hasDatiCert && hasSmime
// Format From
var from string
if len(email.From) > 0 {
from = email.From[0].String()
}
return &EmailData{
From: convertToUTF8(from),
To: formatAddress(email.To),
Cc: formatAddress(email.Cc),
Bcc: formatAddress(email.Bcc),
Subject: convertToUTF8(email.Subject),
Body: convertToUTF8(body),
Attachments: attachments,
IsPec: isPec,
HasInnerEmail: hasInnerEmail,
}, nil
}
func convertToUTF8(s string) string {
if utf8.ValidString(s) {
return s
}
// If invalid UTF-8, assume Windows-1252 (superset of ISO-8859-1)
decoder := charmap.Windows1252.NewDecoder()
decoded, _, err := transform.String(decoder, s)
if err != nil {
return s // Return as-is if decoding fails
}
return decoded
}
func ReadPecInnerEml(filePath string) (*EmailData, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// 1. Parse outer "Envelope"
outerEmail, err := Parse(file)
if err != nil {
return nil, fmt.Errorf("failed to parse outer email: %w", err)
}
// 2. Look for the real content inside postacert.eml
var innerEmailData []byte
foundPec := false
for _, att := range outerEmail.Attachments {
// Standard PEC puts the real message in postacert.eml
// Using case-insensitive check and substring as per example
if strings.Contains(strings.ToLower(att.Filename), "postacert.eml") {
data, err := io.ReadAll(att.Data)
if err != nil {
return nil, fmt.Errorf("failed to read inner email content: %w", err)
}
innerEmailData = data
foundPec = true
break
}
}
if !foundPec {
return nil, fmt.Errorf("not a signed PEC or 'postacert.eml' attachment is missing")
}
// 3. Parse the inner EML content
innerEmail, err := Parse(bytes.NewReader(innerEmailData))
if err != nil {
return nil, fmt.Errorf("failed to parse inner email structure: %w", err)
}
// Helper to format addresses (reused logic pattern from eml_reader.go)
formatAddress := func(addr []*mail.Address) []string {
var result []string
for _, a := range addr {
// convertToUTF8 is defined in eml_reader.go (same package)
result = append(result, convertToUTF8(a.String()))
}
return result
}
// Determine body (prefer HTML)
body := innerEmail.HTMLBody
if body == "" {
body = innerEmail.TextBody
}
// Process attachments of the inner email
var attachments []EmailAttachment
var hasDatiCert, hasSmime, hasInnerPecEmail bool
for _, att := range innerEmail.Attachments {
data, err := io.ReadAll(att.Data)
if err != nil {
continue
}
// Check internal flags for the inner email (recursive PEC check?)
filenameLower := strings.ToLower(att.Filename)
if filenameLower == "daticert.xml" {
hasDatiCert = true
}
if filenameLower == "smime.p7s" {
hasSmime = true
}
if strings.HasSuffix(filenameLower, ".eml") {
hasInnerPecEmail = true
}
attachments = append(attachments, EmailAttachment{
Filename: att.Filename,
ContentType: att.ContentType,
Data: data,
})
}
isPec := hasDatiCert && hasSmime
// Format From
var from string
if len(innerEmail.From) > 0 {
from = innerEmail.From[0].String()
}
return &EmailData{
From: convertToUTF8(from),
To: formatAddress(innerEmail.To),
Cc: formatAddress(innerEmail.Cc),
Bcc: formatAddress(innerEmail.Bcc),
Subject: convertToUTF8(innerEmail.Subject),
Body: convertToUTF8(body),
Attachments: attachments,
IsPec: isPec,
HasInnerEmail: hasInnerPecEmail,
}, nil
}