163 lines
5.5 KiB
Go
163 lines
5.5 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"net/mail"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Der Viewer betrachtet die LOKALEN mbox-Dateien im Browser (Outlook-2013-
|
|
// Dreispalter): links Konto/Ordner-Baum, Mitte Nachrichtenliste, rechts
|
|
// Lesebereich. Optional Weiterleiten einzelner Mails per SMTP (09-smtp.go).
|
|
|
|
func viewerHandler(w http.ResponseWriter, r *http.Request) {
|
|
account := r.URL.Query().Get("account")
|
|
folder := r.URL.Query().Get("folder")
|
|
if account != "" && folder != "" {
|
|
renderMessageList(w, account, folder)
|
|
return
|
|
}
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
renderViewerTree(w)
|
|
return
|
|
}
|
|
renderShell(w, "", `<div class="ef-empty">Backup-Postfach links waehlen.</div>`)
|
|
}
|
|
|
|
func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|
account := r.URL.Query().Get("account")
|
|
folder := r.URL.Query().Get("folder")
|
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
|
if err != nil {
|
|
http.Error(w, "bad index", http.StatusBadRequest)
|
|
return
|
|
}
|
|
path, err := mboxPath(account, folder)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
raw, err := ReadMboxMessage(path, index)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
if err != nil {
|
|
renderReadPane(w, "(unlesbar)", "", "", string(raw))
|
|
return
|
|
}
|
|
renderReadPane(w,
|
|
decodeHeader(msg.Header.Get("Subject")),
|
|
decodeHeader(msg.Header.Get("From")),
|
|
decodeHeader(msg.Header.Get("Date")),
|
|
messageBody(raw),
|
|
)
|
|
}
|
|
|
|
func forwardHandler(w http.ResponseWriter, r *http.Request) {
|
|
// TODO Codex: GET = Weiterleiten-Formular (An/Betreff/Text), POST =
|
|
// ForwardMessage() aufrufen. NUR hier kommt SMTP zum Einsatz -- der Umzug
|
|
// selbst nutzt IMAP APPEND.
|
|
}
|
|
|
|
func renderViewerTree(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, `<div class="pane-head">Backups</div>`)
|
|
entries, err := os.ReadDir(Cfg.MboxRoot)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
found := false
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
account := entry.Name()
|
|
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, account, "*.mbox"))
|
|
if len(mboxes) == 0 {
|
|
continue
|
|
}
|
|
found = true
|
|
fmt.Fprintf(w, `<div class="tree-node account">%s</div>`, html.EscapeString(account))
|
|
for _, path := range mboxes {
|
|
folder := strings.TrimSuffix(filepath.Base(path), ".mbox")
|
|
fmt.Fprintf(w, `<a class="tree-node" href="/view?account=%s&folder=%s" hx-get="/view?account=%s&folder=%s" hx-target="#list" hx-swap="innerHTML">%s</a>`,
|
|
urlEsc(account), urlEsc(folder), urlEsc(account), urlEsc(folder), html.EscapeString(folder))
|
|
}
|
|
}
|
|
if !found {
|
|
fmt.Fprint(w, `<div class="ef-empty">Noch keine mbox-Backups.</div>`)
|
|
}
|
|
}
|
|
|
|
func renderMessageList(w http.ResponseWriter, account, folder string) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
path, err := mboxPath(account, folder)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
entries, err := ReadMboxList(path)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
|
return
|
|
}
|
|
fmt.Fprintf(w, `<div class="pane-head">%s / %s (%d)</div>`, html.EscapeString(account), html.EscapeString(folder), len(entries))
|
|
for _, entry := range entries {
|
|
subject := entry.Subject
|
|
if subject == "" {
|
|
subject = "(ohne Betreff)"
|
|
}
|
|
from := entry.From
|
|
if from == "" {
|
|
from = "(ohne Absender)"
|
|
}
|
|
fmt.Fprintf(w, `<a class="msg-row" href="/view/message?account=%s&folder=%s&index=%d" hx-get="/view/message?account=%s&folder=%s&index=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
|
urlEsc(account), urlEsc(folder), entry.Index, urlEsc(account), urlEsc(folder), entry.Index,
|
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date))
|
|
}
|
|
if len(entries) == 0 {
|
|
fmt.Fprint(w, `<div class="ef-empty">Keine Nachrichten in dieser mbox.</div>`)
|
|
}
|
|
}
|
|
|
|
func renderReadPane(w http.ResponseWriter, subject, from, date, body string) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if subject == "" {
|
|
subject = "(ohne Betreff)"
|
|
}
|
|
fmt.Fprintf(w, `<div class="pane-head">Lesebereich</div><div class="read-body"><div class="read-head"><h1 class="read-subject">%s</h1><div class="read-meta">Von: %s<br>Datum: %s</div></div><pre class="read-pre">%s</pre></div>`,
|
|
html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
|
}
|
|
|
|
func mboxPath(account, folder string) (string, error) {
|
|
if account == "" || folder == "" || strings.Contains(account, "..") || strings.Contains(folder, "..") {
|
|
return "", fmt.Errorf("ungueltiges Postfach")
|
|
}
|
|
root, err := filepath.Abs(Cfg.MboxRoot)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path, err := filepath.Abs(filepath.Join(root, account, safeMboxName(folder)+".mbox"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return "", fmt.Errorf("ungueltiger mbox-Pfad")
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func urlEsc(s string) string {
|
|
r := strings.NewReplacer("%", "%25", " ", "%20", "&", "%26", "?", "%3F", "#", "%23", "+", "%2B", "/", "%2F")
|
|
return r.Replace(s)
|
|
}
|