110 lines
2.9 KiB
Go
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
|
|
}
|