package web // The reading pane (novox/hq ADR 0515 §3, §5, §6; issue 1859): one mail, over the list on a phone and beside // it on a wide screen — its headers, mail reading's note of it, its tasks and promises, and its text from // mail-store.read, shown as escaped text through html/template alone (ADR 0440 §5: nothing of the mail is // markup, nothing remote is loaded, an attachment is a download). Corrections of mail reading are made here // (ADR 0515 §5): the mark and the mail group, not a task, not a promise, read later on or off, each a post // calling mail-reading.correct. Opening it records the mail opened (ADR 0517 §3). import ( "context" "encoding/json" "fmt" "html/template" "net/http" "net/url" "strconv" "time" ) // entryAnswer is mail-reading.entries' one entry, whole (mesh-mail-reader tools.go entryOut). type entryAnswer struct { ID int64 `json:"id"` Account string `json:"account"` Date string `json:"date"` SenderName string `json:"sender-name"` SenderAddress string `json:"sender-address"` Subject string `json:"subject"` Group string `json:"group"` Summary string `json:"summary"` Mark string `json:"mark"` Why string `json:"why"` Folder string `json:"folder"` UID int64 `json:"uid"` ReadLater bool `json:"read-later"` Tasks []struct { N int `json:"n"` Line string `json:"line"` DueDate string `json:"due-date"` } `json:"tasks"` Promises []struct { N int `json:"n"` Kind string `json:"kind"` Line string `json:"line"` Who string `json:"who"` Date string `json:"date"` } `json:"promises"` } type paneItem struct { N int Item, Line, Due, Kind, Who string } type attachmentLink struct { Name, Link string Size int64 } // paneData is the reading pane as the page renders it. type paneData struct { ID int64 Mode, Subject, Sender, Date, Account, Folder string Mark, Why, Group, Summary string Scam, ReadLater bool Text, TextMissing string Tasks, Promises []paneItem Attachments []attachmentLink TokenField template.HTML Back, Close string } func init() { template.Must(pageTemplates.Parse(`{{define "pane"}}
Back to the list

{{.Subject}}

From {{.Sender}} · {{.Date}} · {{.Account}} / {{.Folder}}

{{if .Summary}}

{{.Summary}}

{{end}} {{if .Why}}

{{.Mark}}: {{.Why}}

{{end}} {{if .Tasks}}

Tasks

{{end}} {{if .Promises}}

Promises

{{end}} {{if .TextMissing}}

{{.TextMissing}}

{{else}}
{{.Text}}
{{end}} {{if .Attachments}}

Attachments

{{end}}

Correct mail reading

{{.TokenField}}
{{.TokenField}}
{{end}}`)) } // readAddress is the reading pane's own address for one mail in mode. func readAddress(id int64, mode Mode) string { return "/read?" + url.Values{"id": {fmt.Sprint(id)}, "mode": {mode.Name}}.Encode() } // paneOf builds the pane for the mail entry id: the entry from mail-reading.entries, its text from // mail-store.read. nil, and why, when the entry is not held. func (b *Bundle) paneOf(ctx context.Context, r *http.Request, id int64, mode Mode) (*paneData, string) { raw, err := b.mailEntriesAsk(ctx, map[string]any{"id": float64(id)}) if err != nil { return nil, "Mail reading does not answer: " + err.Error() } var answer struct { Entries []entryAnswer `json:"entries"` } if err := json.Unmarshal(raw, &answer); err != nil || len(answer.Entries) != 1 { return nil, fmt.Sprintf("Mail reading holds no mail %d.", id) } e := answer.Entries[0] now := b.now() p := &paneData{ ID: e.ID, Mode: mode.Name, Subject: e.Subject, Sender: SenderShown(e.SenderName, e.SenderAddress), Date: localTime(e.Date, now), Account: e.Account, Folder: e.Folder, Mark: e.Mark, Why: e.Why, Group: e.Group, Summary: e.Summary, Scam: e.Mark == "scam warning", ReadLater: e.ReadLater, Back: readAddress(e.ID, mode), Close: listAddress(mode), } if p.Subject == "" { p.Subject = "(no subject)" } if sess, ok := SessionFrom(r); ok { p.TokenField = FormTokenInput(sess) } for _, t := range e.Tasks { p.Tasks = append(p.Tasks, paneItem{N: t.N, Item: "task " + strconv.Itoa(t.N), Line: t.Line, Due: t.DueDate}) } for _, pr := range e.Promises { p.Promises = append(p.Promises, paneItem{N: pr.N, Item: "promise " + strconv.Itoa(pr.N), Line: pr.Line, Due: pr.Date, Kind: pr.Kind, Who: pr.Who}) } if e.UID <= 0 || e.Folder == "" { p.TextMissing = "This mail's text cannot be shown: mail reading kept it before it kept where the mail is." return p, "" } mailRaw, err := b.readAsk(ctx, map[string]any{"account": e.Account, "folder": e.Folder, "uid": float64(e.UID)}) var mail readAnswer if err == nil { err = json.Unmarshal(mailRaw, &mail) } if err != nil { p.TextMissing = "This mail's text cannot be shown: " + err.Error() return p, "" } p.Text = mail.Text if e.SenderAddress == "" && len(mail.From) > 0 { p.Sender = SenderShown(mail.From[0].Name, mail.From[0].Address) } for _, a := range mail.Attachments { q := url.Values{"account": {e.Account}, "folder": {e.Folder}, "uid": {fmt.Sprint(e.UID)}, "key": {a.Key}} p.Attachments = append(p.Attachments, attachmentLink{Name: a.Name, Size: a.Size, Link: "/attachment?" + q.Encode()}) } return p, "" } // renderRead renders the list with the pane over it, saying notice when there is one. func (b *Bundle) renderRead(w http.ResponseWriter, r *http.Request, id int64, mode Mode, status int, notice string, opened bool) { ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() pane, why := b.paneOf(ctx, r, id, mode) data := b.listDataQuiet(ctx, r, mode) if pane == nil { data.Notice = why renderNamed(w, http.StatusNotFound, "list", data) return } data.Pane = pane data.Title = pane.Subject if opened { if _, err := b.mailRecordAsk(ctx, map[string]any{"what": "opened", "id": float64(id), "mode": mode.Name}); err != nil { notice = "That you opened this mail could not be kept: " + err.Error() } } if notice != "" { data.Notice = notice } renderNamed(w, status, "list", data) } // readPage is GET /read (1859/AC7): one mail in the reading pane, over or beside the list of its mode. func (b *Bundle) readPage(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) if err != nil || id <= 0 { http.NotFound(w, r) return } b.renderRead(w, r, id, modeNamed(r.URL.Query().Get("mode")), http.StatusOK, "", true) } // correctHandler is POST /correct (1859/AC7): a correction of one mail entry — mark and mail group, not a // task, not a promise, or read later on or off — calling mail-reading.correct, kept by it as a reading lesson // and one corrected line (ADR 0516 §4, ADR 0517 §5); then the pane again, or the failure and its cause. func (b *Bundle) correctHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "post only", http.StatusMethodNotAllowed) return } mode := modeNamed(r.PostFormValue("mode")) id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64) if err != nil || id <= 0 { http.Error(w, "a correction needs its mail entry's id", http.StatusBadRequest) return } args := map[string]any{"id": float64(id), "mode": mode.Name} for _, key := range []string{"mark", "group"} { if v := r.PostFormValue(key); v != "" { args[key] = v } } for _, key := range []string{"not-a-task", "remove-promise"} { if v := r.PostFormValue(key); v != "" { n, err := strconv.Atoi(v) if err != nil || n <= 0 { http.Error(w, key+" is an item's number", http.StatusBadRequest) return } args[key] = float64(n) } } switch r.PostFormValue("read-later") { case "on": args["read-later"] = true case "off": args["read-later"] = false case "": default: http.Error(w, "read-later is on or off", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() if _, err := b.mailCorrectAsk(ctx, args); err != nil { cause := "The correction was not kept: " + err.Error() if byScript(r) { answerJSON(w, http.StatusBadGateway, map[string]any{"failed": cause}) return } b.renderRead(w, r, id, mode, http.StatusBadGateway, cause, false) return } actDone(w, r, readAddress(id, mode)) }