Load selected target mailbox messages
This commit is contained in:
parent
548e0fa741
commit
647ce4bc90
1 changed files with 269 additions and 7 deletions
|
|
@ -44,6 +44,8 @@ func RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/boxes/source", emailBoxHandler)
|
mux.HandleFunc("/boxes/source", emailBoxHandler)
|
||||||
mux.HandleFunc("/boxes/source/messages", sourceMailboxMessagesHandler)
|
mux.HandleFunc("/boxes/source/messages", sourceMailboxMessagesHandler)
|
||||||
mux.HandleFunc("/boxes/source/message", sourceMailboxMessageHandler)
|
mux.HandleFunc("/boxes/source/message", sourceMailboxMessageHandler)
|
||||||
|
mux.HandleFunc("/boxes/target/messages", targetMailboxMessagesHandler)
|
||||||
|
mux.HandleFunc("/boxes/target/message", targetMailboxMessageHandler)
|
||||||
mux.HandleFunc("/boxes/target", emailBoxHandler)
|
mux.HandleFunc("/boxes/target", emailBoxHandler)
|
||||||
mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund
|
mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund
|
||||||
mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll)
|
mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll)
|
||||||
|
|
@ -316,7 +318,15 @@ func emailBoxHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
renderSourceEmailBoxPage(w, r, accounts, r.URL.Query().Get("source"))
|
renderSourceEmailBoxPage(w, r, accounts, r.URL.Query().Get("source"))
|
||||||
case "target":
|
case "target":
|
||||||
renderTargetEmailBoxPage(w, r, accounts)
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
selected := selectedTargetAccount(accounts, r.URL.Query().Get("target"))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, renderEmailBoxTargetPane(accounts, selected.Name))
|
||||||
|
fmt.Fprintf(w, `<div id="list" hx-swap-oob="innerHTML">%s</div>`, renderInitialTargetMailboxList(selected))
|
||||||
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderInitialTargetMailboxRead(selected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderTargetEmailBoxPage(w, r, accounts, r.URL.Query().Get("target"))
|
||||||
default:
|
default:
|
||||||
http.Redirect(w, r, "/view", http.StatusSeeOther)
|
http.Redirect(w, r, "/view", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
@ -366,6 +376,50 @@ func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, renderSourceMailboxRead(account, folder, index, messages))
|
fmt.Fprint(w, renderSourceMailboxRead(account, folder, index, messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
|
||||||
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if accountName == "" || folder == "" {
|
||||||
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">Ziel-Postfach und Ordner waehlen.</div>`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account, err := GetAccount(accountName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messages, err := fetchTargetFolderMessages(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
|
||||||
|
}
|
||||||
|
renderTargetMailboxMessageList(w, account, folder, messages)
|
||||||
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderTargetMailboxRead(account, folder, 0, messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
|
||||||
|
folder := strings.TrimSpace(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
|
||||||
|
}
|
||||||
|
account, err := GetAccount(accountName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, renderTargetMailboxRead(account, folder, index, messages))
|
||||||
|
}
|
||||||
|
|
||||||
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
|
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !requireManager(w, r) {
|
if !requireManager(w, r) {
|
||||||
return
|
return
|
||||||
|
|
@ -879,8 +933,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
|
||||||
renderRibbonEmailBoxMenu("source"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxSourcePane(accounts, selectedAccount.Name), renderInitialSourceMailboxList(selectedAccount), renderInitialSourceMailboxRead(selectedAccount))
|
renderRibbonEmailBoxMenu("source"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxSourcePane(accounts, selectedAccount.Name), renderInitialSourceMailboxList(selectedAccount), renderInitialSourceMailboxRead(selectedAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account) {
|
func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account, selected string) {
|
||||||
user := CurrentUser(r)
|
user := CurrentUser(r)
|
||||||
|
selectedAccount := selectedTargetAccount(accounts, selected)
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
@ -911,14 +966,14 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
|
||||||
</aside>
|
</aside>
|
||||||
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
<button class="backstage-shade" type="button" aria-label="Menue schliessen" data-backstage-close></button>
|
||||||
<div class="three-pane email-box-view email-box-target-view">
|
<div class="three-pane email-box-view email-box-target-view">
|
||||||
<section class="pane pane-list"><div class="pane-head">Nachrichten</div><div class="ef-empty">Ziel-Postfach rechts waehlen.</div></section>
|
<section class="pane pane-list" id="list">%s</section>
|
||||||
<div class="pane-resizer" data-resize-index="0"></div>
|
<div class="pane-resizer" data-resize-index="0"></div>
|
||||||
<section class="pane pane-read"><div class="ef-empty">Nachricht waehlen.</div></section>
|
<section class="pane pane-read" id="read">%s</section>
|
||||||
<div class="pane-resizer" data-resize-index="1"></div>
|
<div class="pane-resizer" data-resize-index="1"></div>
|
||||||
<aside class="pane pane-tree">%s</aside>
|
<aside class="pane pane-tree" id="target-tree">%s</aside>
|
||||||
</div>
|
</div>
|
||||||
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
|
<footer class="statusbar"><span id="mig-status" hx-get="/migrate/status" hx-trigger="load,every 3s" hx-swap="innerHTML">bereit</span></footer></body></html>`,
|
||||||
renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxTargetPane(accounts))
|
renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialTargetMailboxList(selectedAccount), renderInitialTargetMailboxRead(selectedAccount), renderEmailBoxTargetPane(accounts, selectedAccount.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderEmailBoxSourcePane(accounts []Account, selected string) string {
|
func renderEmailBoxSourcePane(accounts []Account, selected string) string {
|
||||||
|
|
@ -1124,7 +1179,214 @@ func renderSourceMailboxRead(account Account, folder string, index int, messages
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderEmailBoxTargetPane(accounts []Account) string {
|
func renderEmailBoxTargetPane(accounts []Account, selected string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
targetAccounts := accountsByTarget(accounts)
|
||||||
|
if len(targetAccounts) == 0 {
|
||||||
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span></div><div class="ef-empty">Keine Ziel-Postfaecher eingetragen.</div>`)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
selectedAccount := selectedTargetAccount(accounts, selected)
|
||||||
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span><form class="archive-select-form" hx-get="/boxes/target" hx-target="#target-tree" hx-swap="innerHTML" hx-trigger="change from:#target-mailbox-select"><select id="target-mailbox-select" class="archive-select" name="target">`)
|
||||||
|
for _, a := range targetAccounts {
|
||||||
|
sel := ""
|
||||||
|
if a.Name == selectedAccount.Name {
|
||||||
|
sel = ` selected`
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(a.Name), sel, html.EscapeString(a.DstUser))
|
||||||
|
}
|
||||||
|
b.WriteString(`</select></form></div>`)
|
||||||
|
renderTargetFolders(&b, selectedAccount)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedTargetAccount(accounts []Account, selected string) Account {
|
||||||
|
targetAccounts := accountsByTarget(accounts)
|
||||||
|
if len(targetAccounts) == 0 {
|
||||||
|
return Account{}
|
||||||
|
}
|
||||||
|
if selected != "" {
|
||||||
|
for _, a := range targetAccounts {
|
||||||
|
if a.Name == selected {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetAccounts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTargetFolders(b *strings.Builder, account Account) {
|
||||||
|
fmt.Fprintf(b, `<div class="tree-node account">%s</div>`, html.EscapeString(account.DstUser))
|
||||||
|
dst, err := OpenIMAPTarget(account)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
folders, err := dst.Folders()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(b, `<div class="ef-empty">Ordner konnten nicht geladen werden: %s</div>`, html.EscapeString(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(folders) == 0 {
|
||||||
|
b.WriteString(`<div class="ef-empty">Keine Ordner in diesem Ziel-Postfach.</div>`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, folder := range folders {
|
||||||
|
fmt.Fprintf(b, `<a class="tree-node folder" href="#" title="%s" hx-get="/boxes/target/messages?target=%s&folder=%s" hx-target="#list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%s</span></a>`,
|
||||||
|
html.EscapeString(strings.Join(folder.Attrs, " ")), urlEsc(account.Name), urlEsc(folder.Name), html.EscapeString(folder.Name), html.EscapeString(targetFolderRole(folder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func targetFolderRole(folder TargetFolder) string {
|
||||||
|
for _, attr := range folder.Attrs {
|
||||||
|
switch strings.ToLower(attr) {
|
||||||
|
case `\sent`:
|
||||||
|
return "Gesendet"
|
||||||
|
case `\drafts`:
|
||||||
|
return "Entwurf"
|
||||||
|
case `\trash`:
|
||||||
|
return "Papierkorb"
|
||||||
|
case `\junk`:
|
||||||
|
return "Spam"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchTargetFolderMessages(account Account, folder string) ([]RawMessage, error) {
|
||||||
|
dst, err := OpenIMAPTarget(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
return dst.Fetch(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderInitialTargetMailboxList(account Account) string {
|
||||||
|
if account.Name == "" {
|
||||||
|
return `<div class="pane-head">Nachrichten</div><div class="ef-empty">Ziel-Postfach waehlen.</div>`
|
||||||
|
}
|
||||||
|
folder, err := firstTargetFolder(account)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
|
}
|
||||||
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
renderTargetMailboxMessageList(&b, account, folder, messages)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderInitialTargetMailboxRead(account Account) string {
|
||||||
|
if account.Name == "" {
|
||||||
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
|
}
|
||||||
|
folder, err := firstTargetFolder(account)
|
||||||
|
if err != nil {
|
||||||
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
|
}
|
||||||
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
|
if err != nil {
|
||||||
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
|
}
|
||||||
|
return renderTargetMailboxRead(account, folder, 0, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstTargetFolder(account Account) (string, error) {
|
||||||
|
dst, err := OpenIMAPTarget(account)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
folders, err := dst.Folders()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, folder := range folders {
|
||||||
|
if strings.EqualFold(folder.Name, "INBOX") {
|
||||||
|
return folder.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(folders) == 0 {
|
||||||
|
return "", fmt.Errorf("Keine Ordner in diesem Ziel-Postfach.")
|
||||||
|
}
|
||||||
|
return folders[0].Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTargetMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) {
|
||||||
|
fmt.Fprintf(w, `<div class="pane-head">%s / %s (%d)</div>`, html.EscapeString(account.DstUser), html.EscapeString(folder), len(messages))
|
||||||
|
for i, raw := range messages {
|
||||||
|
subject := "(ohne Betreff)"
|
||||||
|
from := "(ohne Absender)"
|
||||||
|
date := ""
|
||||||
|
if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
|
||||||
|
if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
|
||||||
|
subject = v
|
||||||
|
}
|
||||||
|
if v := decodeHeader(msg.Header.Get("From")); v != "" {
|
||||||
|
from = v
|
||||||
|
}
|
||||||
|
date = decodeHeader(msg.Header.Get("Date"))
|
||||||
|
}
|
||||||
|
active := ""
|
||||||
|
if i == 0 {
|
||||||
|
active = " active"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-target="%s" data-folder="%s" data-index="%d" hx-get="/boxes/target/message?target=%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>`,
|
||||||
|
active,
|
||||||
|
html.EscapeString(account.Name), html.EscapeString(folder), i,
|
||||||
|
urlEsc(account.Name), urlEsc(folder), i,
|
||||||
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
||||||
|
}
|
||||||
|
if len(messages) == 0 {
|
||||||
|
fmt.Fprint(w, `<div class="ef-empty">Keine Nachrichten in diesem Ziel-Ordner.</div>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTargetMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
|
||||||
|
if index < 0 || index >= len(messages) {
|
||||||
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
|
}
|
||||||
|
raw := messages[index].Body
|
||||||
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||||
|
var b strings.Builder
|
||||||
|
if err != nil {
|
||||||
|
renderReadPane(&b, "Ziel", account.Name, folder, "", index, "(unlesbar)", "", "", string(raw))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
renderReadPane(&b, "Ziel", account.Name, folder, "", index,
|
||||||
|
decodeHeader(msg.Header.Get("Subject")),
|
||||||
|
decodeHeader(msg.Header.Get("From")),
|
||||||
|
decodeHeader(msg.Header.Get("Date")),
|
||||||
|
messageBody(raw),
|
||||||
|
)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountsByTarget(accounts []Account) []Account {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
out := make([]Account, 0, len(accounts))
|
||||||
|
for _, a := range accounts {
|
||||||
|
if strings.TrimSpace(a.DstUser) == "" || seen[a.DstUser] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[a.DstUser] = true
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(out); i++ {
|
||||||
|
for j := i + 1; j < len(out); j++ {
|
||||||
|
if strings.ToLower(out[j].DstUser) < strings.ToLower(out[i].DstUser) {
|
||||||
|
out[i], out[j] = out[j], out[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderEmailBoxTargetPaneLegacy(accounts []Account) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span><form class="archive-select-form"><select class="archive-select" name="target">`)
|
b.WriteString(`<div class="pane-head archive-head"><span>Ziel-Postfach</span><form class="archive-select-form"><select class="archive-select" name="target">`)
|
||||||
targets := uniqueAccountValues(accounts, "target")
|
targets := uniqueAccountValues(accounts, "target")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue