104 lines
2.4 KiB
Go
104 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func writeMaildir(folderPath string, msg []byte) error {
|
|
for _, dir := range []string{"tmp", "new"} {
|
|
if err := os.MkdirAll(filepath.Join(folderPath, dir), 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
filename := fmt.Sprintf("%d.M%06dP%d.%s",
|
|
now.Unix(),
|
|
now.Nanosecond()/1000,
|
|
os.Getpid(),
|
|
maildirHostname(),
|
|
)
|
|
|
|
tmpPath := filepath.Join(folderPath, "tmp", filename)
|
|
newPath := filepath.Join(folderPath, "new", filename)
|
|
|
|
if err := os.WriteFile(tmpPath, msg, 0600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmpPath, newPath)
|
|
}
|
|
|
|
func maildirHostname() string {
|
|
h, err := os.Hostname()
|
|
if err != nil {
|
|
return "localhost"
|
|
}
|
|
return h
|
|
}
|
|
|
|
// maildirHasMessageID reports whether any file in folderPath/new or folderPath/cur
|
|
// contains msgID in its headers.
|
|
func maildirHasMessageID(folderPath, msgID string) bool {
|
|
needle := []byte(msgID)
|
|
for _, sub := range []string{"new", "cur"} {
|
|
dir := filepath.Join(folderPath, sub)
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
if fileContainsInHeaders(filepath.Join(dir, e.Name()), needle) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// headerBytes returns the header section of an RFC 5322 message (everything
|
|
// before the first blank line). Returns the full slice if no blank line found.
|
|
func headerBytes(b []byte) []byte {
|
|
if i := bytes.Index(b, []byte("\r\n\r\n")); i >= 0 {
|
|
return b[:i]
|
|
}
|
|
if i := bytes.Index(b, []byte("\n\n")); i >= 0 {
|
|
return b[:i]
|
|
}
|
|
return b
|
|
}
|
|
|
|
// fileContainsInHeaders reads only the header section of an RFC 5322 message
|
|
// (up to the first blank line) and reports whether needle appears in it.
|
|
func fileContainsInHeaders(path string, needle []byte) bool {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
buf := make([]byte, 8192)
|
|
n, _ := f.Read(buf)
|
|
return bytes.Contains(headerBytes(buf[:n]), needle)
|
|
}
|
|
|
|
// extractMessageID returns the value of the Message-ID header, or "" if absent.
|
|
func extractMessageID(msg []byte) string {
|
|
for _, line := range bytes.Split(headerBytes(msg), []byte("\n")) {
|
|
line = bytes.TrimRight(line, "\r")
|
|
key, val, ok := bytes.Cut(line, []byte(":"))
|
|
if !ok {
|
|
continue
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(string(key)), "message-id") {
|
|
return strings.TrimSpace(string(val))
|
|
}
|
|
}
|
|
return ""
|
|
}
|