177 lines
4.3 KiB
Go
177 lines
4.3 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/mail"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// mbox ist das lokale Cold-Backup-Format: eine Datei pro IMAP-Ordner, der
|
|
// Ordnerbaum 1:1 auf der Platte. eM Client / Thunderbird importieren mbox
|
|
// direkt. (Kein PST -- das laesst sich in Go nicht lean schreiben.)
|
|
|
|
// MboxWriter haengt Nachrichten an die mbox-Datei eines Ordners an.
|
|
type MboxWriter struct {
|
|
root string
|
|
}
|
|
|
|
// NewMboxWriter legt <mboxRoot>/<account>/ an und spiegelt den Ordnerbaum.
|
|
func NewMboxWriter(dir string) (*MboxWriter, error) {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
return &MboxWriter{root: dir}, nil
|
|
}
|
|
|
|
// Append schreibt eine Mail im mbox-Format ("From "-Trennzeile, >From-Quoting)
|
|
// in <ordner>.mbox.
|
|
func (w *MboxWriter) Append(folder string, m RawMessage) error {
|
|
path := filepath.Join(w.root, safeMboxName(folder)+".mbox")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
fromDate := m.InternalDate
|
|
if fromDate.IsZero() {
|
|
fromDate = time.Now()
|
|
}
|
|
if _, err := fmt.Fprintf(f, "From MAILER-DAEMON %s\r\n", fromDate.Format("Mon Jan _2 15:04:05 2006")); err != nil {
|
|
return err
|
|
}
|
|
body := bytes.ReplaceAll(m.Body, []byte("\r\nFrom "), []byte("\r\n>From "))
|
|
body = bytes.ReplaceAll(body, []byte("\nFrom "), []byte("\n>From "))
|
|
if _, err := f.Write(body); err != nil {
|
|
return err
|
|
}
|
|
if !bytes.HasSuffix(body, []byte("\n")) {
|
|
if _, err := f.WriteString("\r\n"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = f.WriteString("\r\n")
|
|
return err
|
|
}
|
|
|
|
// --- Viewer-Seite: mbox wieder lesen fuers Browser-Betrachten (08-viewer.go) ---
|
|
|
|
// MboxEntry ist eine Nachricht wie im Listen-Pane angezeigt.
|
|
type MboxEntry struct {
|
|
Index int
|
|
From string
|
|
Subject string
|
|
Date string
|
|
}
|
|
|
|
// ReadMboxList parst die Kopfzeilen aller Mails einer mbox-Datei (fuer die
|
|
// Nachrichtenliste). ReadMboxMessage liefert eine einzelne Mail als Rohtext.
|
|
func ReadMboxList(path string) ([]MboxEntry, error) {
|
|
msgs, err := readMboxMessages(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]MboxEntry, 0, len(msgs))
|
|
for i, raw := range msgs {
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
if err != nil {
|
|
out = append(out, MboxEntry{Index: i, From: "(unlesbar)", Subject: "Unlesbare Nachricht"})
|
|
continue
|
|
}
|
|
out = append(out, MboxEntry{
|
|
Index: i,
|
|
From: decodeHeader(msg.Header.Get("From")),
|
|
Subject: decodeHeader(msg.Header.Get("Subject")),
|
|
Date: decodeHeader(msg.Header.Get("Date")),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func ReadMboxMessage(path string, index int) ([]byte, error) {
|
|
msgs, err := readMboxMessages(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if index < 0 || index >= len(msgs) {
|
|
return nil, fmt.Errorf("message index out of range")
|
|
}
|
|
return msgs[index], nil
|
|
}
|
|
|
|
func readMboxMessages(path string) ([][]byte, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
|
|
lines := bytes.Split(b, []byte("\n"))
|
|
var msgs [][]byte
|
|
var cur bytes.Buffer
|
|
inMsg := false
|
|
for _, line := range lines {
|
|
if bytes.HasPrefix(line, []byte("From ")) {
|
|
if inMsg && cur.Len() > 0 {
|
|
msgs = append(msgs, bytes.TrimRight(cur.Bytes(), "\n"))
|
|
cur.Reset()
|
|
}
|
|
inMsg = true
|
|
continue
|
|
}
|
|
if !inMsg {
|
|
continue
|
|
}
|
|
if bytes.HasPrefix(line, []byte(">From ")) {
|
|
line = line[1:]
|
|
}
|
|
_, _ = cur.Write(line)
|
|
_ = cur.WriteByte('\n')
|
|
}
|
|
if inMsg && cur.Len() > 0 {
|
|
msgs = append(msgs, bytes.TrimRight(cur.Bytes(), "\n"))
|
|
}
|
|
return msgs, nil
|
|
}
|
|
|
|
func decodeHeader(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return ""
|
|
}
|
|
decoded, err := new(mime.WordDecoder).DecodeHeader(v)
|
|
if err != nil {
|
|
return v
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
func messageBody(raw []byte) string {
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
if err != nil {
|
|
return string(raw)
|
|
}
|
|
body, err := io.ReadAll(msg.Body)
|
|
if err != nil {
|
|
return string(raw)
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
func safeMboxName(folder string) string {
|
|
folder = strings.TrimSpace(folder)
|
|
folder = strings.ReplaceAll(folder, "\\", "_")
|
|
folder = strings.ReplaceAll(folder, "/", "_")
|
|
folder = strings.ReplaceAll(folder, ":", "_")
|
|
if folder == "" {
|
|
return "INBOX"
|
|
}
|
|
return folder
|
|
}
|