Move target account selection to target pane
This commit is contained in:
parent
c5d761db98
commit
d69e7afa82
3 changed files with 164 additions and 16 deletions
|
|
@ -20,6 +20,7 @@ func RegisterRoutes(mux *http.ServeMux) {
|
|||
// Viewer (lokale mbox betrachten + weiterleiten)
|
||||
mux.HandleFunc("/view", viewerHandler)
|
||||
mux.HandleFunc("/view/message", messageHandler)
|
||||
mux.HandleFunc("/view/target", targetMessageHandler)
|
||||
mux.HandleFunc("/view/forward", forwardHandler)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
|
|||
func messageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account := r.URL.Query().Get("account")
|
||||
folder := r.URL.Query().Get("folder")
|
||||
targetAccount := r.URL.Query().Get("target_account")
|
||||
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
||||
if err != nil {
|
||||
http.Error(w, "bad index", http.StatusBadRequest)
|
||||
|
|
@ -52,10 +53,10 @@ func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
renderReadPanePair(w, account, folder, index, raw, "(unlesbar)", "", "", string(raw))
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw, "(unlesbar)", "", "", string(raw))
|
||||
return
|
||||
}
|
||||
renderReadPanePair(w, account, folder, index, raw,
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw,
|
||||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date")),
|
||||
|
|
@ -63,6 +64,29 @@ func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
)
|
||||
}
|
||||
|
||||
func targetMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
account := r.URL.Query().Get("account")
|
||||
folder := r.URL.Query().Get("folder")
|
||||
targetAccount := r.URL.Query().Get("target_account")
|
||||
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
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, 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
|
||||
|
|
@ -71,7 +95,7 @@ func forwardHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func renderViewerTree(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, `<div class="pane-head">Backups</div>`)
|
||||
fmt.Fprint(w, `<div class="pane-head">Originale</div>`)
|
||||
entries, err := os.ReadDir(Cfg.MboxRoot)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||
|
|
@ -88,7 +112,7 @@ func renderViewerTree(w http.ResponseWriter) {
|
|||
continue
|
||||
}
|
||||
found = true
|
||||
fmt.Fprintf(w, `<div class="tree-node account">%s</div>`, html.EscapeString(account))
|
||||
fmt.Fprintf(w, `<div class="tree-node account">%s</div>`, html.EscapeString(sourceAccountLabel(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>`,
|
||||
|
|
@ -132,7 +156,7 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
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>`,
|
||||
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-include="#target-account-select" 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))
|
||||
}
|
||||
|
|
@ -143,10 +167,10 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
}
|
||||
}
|
||||
|
||||
func renderReadPanePair(w http.ResponseWriter, account, folder string, index int, raw []byte, subject, from, date, body string) {
|
||||
func renderReadPanePair(w http.ResponseWriter, account, folder, targetAccount string, index int, raw []byte, subject, from, date, body string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
renderReadPane(w, "Original", subject, from, date, body)
|
||||
targetHTML := renderTargetPane(account, folder, index, raw)
|
||||
targetHTML := renderTargetPane(account, folder, targetAccount, index, raw)
|
||||
fmt.Fprintf(w, `<div id="read-target" hx-swap-oob="innerHTML">%s</div>`, targetHTML)
|
||||
}
|
||||
|
||||
|
|
@ -158,31 +182,40 @@ func renderReadPane(w io.Writer, title, subject, from, date, body string) {
|
|||
html.EscapeString(title), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
||||
}
|
||||
|
||||
func renderTargetPane(account, folder string, index int, raw []byte) string {
|
||||
match, ok := findForwardedCopy(account, folder, index, raw)
|
||||
func renderTargetPane(account, folder, targetAccount string, index int, raw []byte) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(renderTargetAccountSelect(account, folder, targetAccount, index))
|
||||
match, ok := findForwardedCopy(account, targetAccount, raw)
|
||||
if !ok {
|
||||
id := messageIDHeader(raw)
|
||||
if id == "" {
|
||||
id = "(ohne Message-ID)"
|
||||
}
|
||||
return fmt.Sprintf(`<div class="pane-head">Zielkopie</div><div class="ef-empty">Keine lokale Zielkopie gefunden.<br><br>Message-ID: %s</div>`, html.EscapeString(id))
|
||||
fmt.Fprintf(&b, `<div class="ef-empty">Keine lokale Zielkopie gefunden.<br><br>Message-ID: %s</div>`, html.EscapeString(id))
|
||||
return b.String()
|
||||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(match.Raw))
|
||||
if err != nil {
|
||||
var b strings.Builder
|
||||
renderReadPane(&b, "Zielkopie", "(unlesbar)", match.Location(), "", string(match.Raw))
|
||||
renderReadContent(&b, "(unlesbar)", match.Location(), "", string(match.Raw))
|
||||
return b.String()
|
||||
}
|
||||
var b strings.Builder
|
||||
renderReadPane(&b, "Zielkopie: "+match.Location(),
|
||||
renderReadContent(&b,
|
||||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date")),
|
||||
decodeHeader(msg.Header.Get("Date"))+" | "+match.Location(),
|
||||
messageBody(match.Raw),
|
||||
)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderReadContent(w io.Writer, subject, from, date, body string) {
|
||||
if subject == "" {
|
||||
subject = "(ohne Betreff)"
|
||||
}
|
||||
fmt.Fprintf(w, `<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")
|
||||
|
|
@ -238,7 +271,7 @@ func (m forwardedMatch) Location() string {
|
|||
return fmt.Sprintf("%s / %s #%d", m.Account, m.Folder, m.Index)
|
||||
}
|
||||
|
||||
func findForwardedCopy(account, folder string, index int, raw []byte) (forwardedMatch, bool) {
|
||||
func findForwardedCopy(account, targetAccount string, raw []byte) (forwardedMatch, bool) {
|
||||
id := messageIDHeader(raw)
|
||||
if id == "" {
|
||||
return forwardedMatch{}, false
|
||||
|
|
@ -251,6 +284,9 @@ func findForwardedCopy(account, folder string, index int, raw []byte) (forwarded
|
|||
if !entry.IsDir() || entry.Name() == account {
|
||||
continue
|
||||
}
|
||||
if targetAccount != "" && entry.Name() != targetAccount {
|
||||
continue
|
||||
}
|
||||
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, entry.Name(), "*.mbox"))
|
||||
for _, path := range mboxes {
|
||||
msgs, err := ReadMboxList(path)
|
||||
|
|
@ -283,3 +319,100 @@ func messageIDHeader(raw []byte) string {
|
|||
}
|
||||
return strings.TrimSpace(msg.Header.Get("Message-ID"))
|
||||
}
|
||||
|
||||
func renderTargetAccountSelect(account, folder, selected string, index int) string {
|
||||
accounts := viewerAccounts()
|
||||
if selected == account {
|
||||
selected = ""
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, `<div class="pane-head target-head"><span>Zielkopie</span><form class="target-select-form" hx-get="/view/target" hx-target="#read-target" hx-swap="innerHTML" hx-trigger="change from:#target-account-select">
|
||||
<input type="hidden" name="account" value="%s">
|
||||
<input type="hidden" name="folder" value="%s">
|
||||
<input type="hidden" name="index" value="%d">
|
||||
<select id="target-account-select" name="target_account" class="target-select">
|
||||
<option value="">Automatisch</option>`, html.EscapeString(account), html.EscapeString(folder), index)
|
||||
for _, a := range accounts {
|
||||
if a.Name == account {
|
||||
continue
|
||||
}
|
||||
attr := ""
|
||||
if a.Name == selected {
|
||||
attr = ` selected`
|
||||
}
|
||||
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(a.Name), attr, html.EscapeString(a.TargetLabel))
|
||||
}
|
||||
b.WriteString(`</select></form></div>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type viewerAccount struct {
|
||||
Name string
|
||||
SourceLabel string
|
||||
TargetLabel string
|
||||
}
|
||||
|
||||
func viewerAccounts() []viewerAccount {
|
||||
labels := accountLabels()
|
||||
entries, err := os.ReadDir(Cfg.MboxRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []viewerAccount
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, entry.Name(), "*.mbox"))
|
||||
if len(mboxes) == 0 {
|
||||
continue
|
||||
}
|
||||
a := labels[entry.Name()]
|
||||
if a.Name == "" {
|
||||
a = viewerAccount{Name: entry.Name(), SourceLabel: entry.Name(), TargetLabel: entry.Name()}
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sourceAccountLabel(mboxDir string) string {
|
||||
if a := accountLabels()[mboxDir]; a.SourceLabel != "" {
|
||||
return a.SourceLabel
|
||||
}
|
||||
return mboxDir
|
||||
}
|
||||
|
||||
func accountLabels() map[string]viewerAccount {
|
||||
out := map[string]viewerAccount{}
|
||||
if DB == nil {
|
||||
return out
|
||||
}
|
||||
accounts, err := ListAccounts()
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
for _, a := range accounts {
|
||||
dir := accountMboxDir(a)
|
||||
source := a.SrcUser
|
||||
if source == "" {
|
||||
source = a.Name
|
||||
}
|
||||
target := a.DstUser
|
||||
if target == "" {
|
||||
target = a.Name
|
||||
}
|
||||
if a.Name != "" {
|
||||
target = target + " (" + a.Name + ")"
|
||||
}
|
||||
out[dir] = viewerAccount{Name: dir, SourceLabel: source, TargetLabel: target}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func accountMboxDir(a Account) string {
|
||||
if strings.TrimSpace(a.MboxDir) != "" {
|
||||
return a.MboxDir
|
||||
}
|
||||
return a.Name
|
||||
}
|
||||
|
|
|
|||
14
frontend-js/dist/style.css
vendored
14
frontend-js/dist/style.css
vendored
|
|
@ -73,6 +73,20 @@ body.ol2013 {
|
|||
font-weight: 600; color: var(--ol-muted);
|
||||
text-transform: none;
|
||||
}
|
||||
.target-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.target-select-form { margin: 0; min-width: 0; }
|
||||
.target-select {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
padding: 2px 6px;
|
||||
color: var(--ol-text);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.searchbar {
|
||||
position: sticky; top: 30px; z-index: 2;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue