Add manual target drag and drop
This commit is contained in:
parent
ffa28f6182
commit
a4eee37ab9
4 changed files with 229 additions and 35 deletions
|
|
@ -21,6 +21,7 @@ func RegisterRoutes(mux *http.ServeMux) {
|
|||
mux.HandleFunc("/view", viewerHandler)
|
||||
mux.HandleFunc("/view/message", messageHandler)
|
||||
mux.HandleFunc("/view/target", targetMessageHandler)
|
||||
mux.HandleFunc("/view/manual-copy", manualCopyHandler)
|
||||
mux.HandleFunc("/view/forward", forwardHandler)
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// renderShell ist das Grundgeruest im Outlook-2013-Look:
|
||||
// [ Ribbon: Postfaecher | Betrachten | Betrachen | Einstellungen | Mail-Graveyard ]
|
||||
// [ Ordnerbaum | Nachrichtenliste | Original | Zielkopie ]
|
||||
// [ Archiv-mbox | Nachrichtenliste | Quelle/Target ]
|
||||
func renderShell(w http.ResponseWriter, active, readingPane string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
|
|
@ -53,7 +54,7 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
|
|||
</nav>
|
||||
<div class="ribbon-title">Mail-Graveyard</div>
|
||||
</header>
|
||||
<div class="four-pane">
|
||||
<div class="three-pane">
|
||||
<aside class="pane pane-tree" id="tree"
|
||||
hx-get="/view" hx-trigger="load" hx-target="#tree" hx-swap="innerHTML">
|
||||
<div class="pane-head">Postfächer</div>
|
||||
|
|
@ -64,17 +65,13 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
|
|||
</section>
|
||||
<div class="pane-resizer" data-resize-index="1"></div>
|
||||
<section class="pane pane-read" id="read">
|
||||
<div class="pane-head">Original</div>
|
||||
<div class="pane-head">Quelle</div>
|
||||
<div class="read-body">%s</div>
|
||||
</section>
|
||||
<div class="pane-resizer" data-resize-index="2"></div>
|
||||
<section class="pane pane-read pane-target" id="read-target">
|
||||
%s
|
||||
</section>
|
||||
</div>
|
||||
<footer class="statusbar"><span id="mig-status"
|
||||
hx-get="/migrate/status" hx-trigger="every 3s" hx-swap="innerHTML">bereit</span></footer>
|
||||
</body></html>`, active, readingPane, renderInitialTargetPane())
|
||||
</body></html>`, active, readingPane)
|
||||
}
|
||||
|
||||
// --- Stubs fuer die noch nicht gebauten Handler (TODO Codex) ---
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Der Viewer betrachtet die LOKALEN mbox-Dateien im Browser (Outlook-2013-
|
||||
|
|
@ -53,10 +54,10 @@ func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw, "(unlesbar)", "", "", string(raw))
|
||||
renderReadPaneWithSource(w, account, folder, targetAccount, index, "(unlesbar)", "", "", string(raw))
|
||||
return
|
||||
}
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw,
|
||||
renderReadPaneWithSource(w, account, folder, targetAccount, index,
|
||||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date")),
|
||||
|
|
@ -84,7 +85,7 @@ func targetMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw))
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw, ""))
|
||||
}
|
||||
|
||||
func forwardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -157,7 +158,8 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
if from == "" {
|
||||
from = "(ohne Absender)"
|
||||
}
|
||||
fmt.Fprintf(w, `<a class="msg-row" href="#" hx-get="/view/message?source=%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>`,
|
||||
fmt.Fprintf(w, `<a class="msg-row" href="#" draggable="true" data-source="%s" data-folder="%s" data-index="%d" hx-get="/view/message?source=%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>`,
|
||||
html.EscapeString(sourceDisplayLabel(account)), html.EscapeString(folder), entry.Index,
|
||||
urlEsc(sourceDisplayLabel(account)), urlEsc(folder), entry.Index,
|
||||
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date))
|
||||
}
|
||||
|
|
@ -168,24 +170,60 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
}
|
||||
}
|
||||
|
||||
func renderReadPanePair(w http.ResponseWriter, account, folder, targetAccount string, index int, raw []byte, subject, from, date, body string) {
|
||||
func manualCopyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
account := resolveViewerAccount(r.FormValue("account"), r.FormValue("source"))
|
||||
folder := r.FormValue("folder")
|
||||
targetAccount := r.FormValue("target_account")
|
||||
index, err := strconv.Atoi(r.FormValue("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
|
||||
}
|
||||
note, err := appendManualTargetCopy(account, folder, targetAccount, raw)
|
||||
if err != nil {
|
||||
note = "Fehler beim manuellen Umzug: " + err.Error()
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
renderReadPane(w, "Original", subject, from, date, body)
|
||||
targetHTML := renderTargetPane(account, folder, targetAccount, index, raw)
|
||||
fmt.Fprintf(w, `<div id="read-target" hx-swap-oob="innerHTML">%s</div>`, targetHTML)
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw, note))
|
||||
}
|
||||
|
||||
func renderReadPane(w io.Writer, title, subject, from, date, body string) {
|
||||
func renderReadPaneWithSource(w http.ResponseWriter, account, folder, targetAccount string, index int, subject, from, date, body string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
renderReadPane(w, "Quelle", account, folder, targetAccount, index, subject, from, date, body)
|
||||
}
|
||||
|
||||
func renderReadPane(w io.Writer, title, account, folder, targetAccount string, index int, subject, from, date, body string) {
|
||||
if subject == "" {
|
||||
subject = "(ohne Betreff)"
|
||||
}
|
||||
fmt.Fprintf(w, `<div class="pane-head">%s</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(title), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
||||
fmt.Fprintf(w, `<div class="pane-head read-toolbar"><span>%s</span><button class="btn secondary compact" type="button" hx-get="/view/target?source=%s&folder=%s&index=%d&target_account=%s" hx-target="#read" hx-swap="innerHTML">Target</button></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(title), urlEsc(sourceDisplayLabel(account)), urlEsc(folder), index, urlEsc(targetAccount), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
||||
}
|
||||
|
||||
func renderTargetPane(account, folder, targetAccount string, index int, raw []byte) string {
|
||||
func renderTargetPane(account, folder, targetAccount string, index int, raw []byte, note string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(renderTargetAccountSelect(account, folder, targetAccount, index))
|
||||
if note != "" {
|
||||
class := "ok"
|
||||
if strings.HasPrefix(note, "Fehler") {
|
||||
class = "bad"
|
||||
}
|
||||
fmt.Fprintf(&b, `<div class="audit-note %s">%s</div>`, class, html.EscapeString(note))
|
||||
}
|
||||
if match, ok, err := findTargetIMAPCopy(account, targetAccount, raw); ok {
|
||||
renderTargetMatch(&b, match)
|
||||
return b.String()
|
||||
|
|
@ -220,10 +258,7 @@ func renderTargetMatch(b *strings.Builder, match forwardedMatch) {
|
|||
}
|
||||
|
||||
func renderInitialTargetPane() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(renderTargetAccountSelect("", "", "", -1))
|
||||
b.WriteString(`<div class="read-body"><div class="ef-empty">Nachricht waehlen.</div></div>`)
|
||||
return b.String()
|
||||
return `<div class="pane-head">Quelle</div><div class="read-body"><div class="ef-empty">Nachricht waehlen.</div></div>`
|
||||
}
|
||||
|
||||
func renderReadContent(w io.Writer, subject, from, date, body string) {
|
||||
|
|
@ -399,9 +434,9 @@ func messageIDHeader(raw []byte) string {
|
|||
func renderTargetAccountSelect(account, folder, selected string, index int) string {
|
||||
options := targetAccountOptions()
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, `<div class="pane-head target-head"><span>Zielkopie</span><form class="target-select-form"`)
|
||||
fmt.Fprintf(&b, `<div class="pane-head target-head"><span>Target</span><form class="target-select-form"`)
|
||||
if account != "" && folder != "" && index >= 0 {
|
||||
fmt.Fprintf(&b, ` hx-get="/view/target" hx-target="#read-target" hx-swap="innerHTML" hx-trigger="change from:#target-account-select"`)
|
||||
fmt.Fprintf(&b, ` hx-get="/view/target" hx-target="#read" hx-swap="innerHTML" hx-trigger="change from:#target-account-select"`)
|
||||
}
|
||||
fmt.Fprintf(&b, `>
|
||||
<input type="hidden" name="source" value="%s">
|
||||
|
|
@ -416,10 +451,73 @@ func renderTargetAccountSelect(account, folder, selected string, index int) stri
|
|||
}
|
||||
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(opt.Value), attr, html.EscapeString(opt.Label))
|
||||
}
|
||||
b.WriteString(`</select></form></div>`)
|
||||
fmt.Fprintf(&b, `</select></form><button class="btn secondary compact" type="button" hx-get="/view/message?source=%s&folder=%s&index=%d&target_account=%s" hx-target="#read" hx-swap="innerHTML">Quelle</button></div>`,
|
||||
urlEsc(sourceDisplayLabel(account)), urlEsc(folder), index, urlEsc(selected))
|
||||
if account != "" && folder != "" && index >= 0 {
|
||||
fmt.Fprintf(&b, `<div class="drop-zone" data-drop-target="manual-copy" data-source="%s" data-folder="%s" data-index="%d">Mail hier ablegen fuer manuellen Umzug</div>`,
|
||||
html.EscapeString(sourceDisplayLabel(account)), html.EscapeString(folder), index)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendManualTargetCopy(account, folder, targetAccount string, raw []byte) (string, error) {
|
||||
id := messageIDHeader(raw)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("mail ohne Message-ID kann nicht sauber dedupliziert werden")
|
||||
}
|
||||
if _, ok, err := findTargetIMAPCopy(account, targetAccount, raw); ok {
|
||||
return "Schon vorhanden: Zielkopie wurde per Message-ID gefunden.", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
a, ok := accountByMboxDir(account)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("kein Konto zur mbox %s gefunden", account)
|
||||
}
|
||||
if strings.TrimSpace(targetAccount) != "" && !strings.EqualFold(strings.TrimSpace(a.DstUser), strings.TrimSpace(targetAccount)) {
|
||||
candidates := accountsForTargetUser(targetAccount)
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("kein Zielkonto %s gefunden", targetAccount)
|
||||
}
|
||||
a = candidates[0]
|
||||
}
|
||||
dst, err := OpenIMAPTarget(a)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dst.Close()
|
||||
targets, err := dst.Folders()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dstFolder := MapSourceToTarget(folder, nil, "/", dst.Delim(), targets, "")
|
||||
if dstFolder == "" {
|
||||
dstFolder = folder
|
||||
}
|
||||
if err := dst.EnsureFolder(dstFolder); err != nil {
|
||||
return "", err
|
||||
}
|
||||
msg := RawMessage{MessageID: id, Body: raw, InternalDate: messageDate(raw)}
|
||||
if err := dst.Append(dstFolder, msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.ID != 0 {
|
||||
_ = MarkCopied(a.ID, folder, id)
|
||||
}
|
||||
return "Manuell kopiert nach " + strings.TrimSpace(a.DstUser) + " / " + dstFolder + ".", nil
|
||||
}
|
||||
|
||||
func messageDate(raw []byte) time.Time {
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if t, err := mail.ParseDate(msg.Header.Get("Date")); err == nil {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
type viewerAccount struct {
|
||||
Name string
|
||||
SourceLabel string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue