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 // 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 .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 }