first commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user