mbrts/maildir.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 ""
}