Files
mailgoserver/internal/smtpserver/attachments.go
T
2026-08-12 12:56:22 +01:00

110 lines
2.9 KiB
Go

package smtpserver
import (
"bytes"
"io"
"mime"
"mime/multipart"
"net/mail"
"path/filepath"
"strings"
"time"
)
// attachmentStoragePath mirrors smtp_handler.get_attachment_storage_path:
// {base}/{safe_domain}/{username_or_ip}/{YYYY-DD-MMM}/
func attachmentStoragePath(base, domain, usernameOrIP string, now time.Time) string {
safeDomain := sanitizePathSegment(domain, "/\\")
dateFolder := now.Format("2006-02-Jan")
parts := []string{base, safeDomain}
if usernameOrIP != "" {
parts = append(parts, usernameOrIP)
}
parts = append(parts, dateFolder)
return filepath.Join(parts...)
}
func sanitizePathSegment(s string, chars string) string {
for _, c := range chars {
s = strings.ReplaceAll(s, string(c), "_")
}
return s
}
// cleanMessageIDPrefix strips everything from "@" onward, mirroring the
// clean_message_id computation used to build attachment filenames.
func cleanMessageIDPrefix(messageID string) string {
if i := strings.Index(messageID, "@"); i >= 0 {
return messageID[:i]
}
return messageID
}
type attachmentPart struct {
Filename string
ContentType string
Data []byte
}
type parsedMessage struct {
HeaderLines []string // "Name: value" per header, in order
BodyText string // concatenated text/* parts
Attachments []attachmentPart
}
// parseMessage mirrors the repeated BytesParser(policy=policy.default) passes in
// handle_DATA: it extracts header lines for logging, concatenated text body, and any
// attachment parts (Content-Disposition: attachment with a filename).
func parseMessage(raw []byte) (*parsedMessage, error) {
msg, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
return nil, err
}
out := &parsedMessage{}
for k, vs := range msg.Header {
for _, v := range vs {
out.HeaderLines = append(out.HeaderLines, k+": "+v)
}
}
contentType := msg.Header.Get("Content-Type")
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
mediaType = "text/plain"
}
if strings.HasPrefix(mediaType, "multipart/") {
mr := multipart.NewReader(msg.Body, params["boundary"])
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
break
}
data, _ := io.ReadAll(part)
disp, dispParams, _ := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
partCT := part.Header.Get("Content-Type")
partMediaType, _, _ := mime.ParseMediaType(partCT)
if disp == "attachment" && dispParams["filename"] != "" {
out.Attachments = append(out.Attachments, attachmentPart{
Filename: dispParams["filename"],
ContentType: getContentType(partMediaType, dispParams["filename"]),
Data: data,
})
continue
}
if strings.HasPrefix(partMediaType, "text/") && disp != "attachment" {
out.BodyText += string(data) + "\n"
}
}
} else if strings.HasPrefix(mediaType, "text/") {
data, _ := io.ReadAll(msg.Body)
out.BodyText = string(data)
}
out.BodyText = strings.TrimSpace(out.BodyText)
return out, nil
}