Files
mesh-catalog/modules/mesh-mail-web/cmd/mesh-mail-web/search.go
T

117 lines
4.1 KiB
Go

package main
// The search page (novox/hq ADR 0440 §4, issue 1265): the form's own field names are exactly mail-store.search's
// own argument names (mesh-mail tools.go searchArgs, issue 1232), so a filled field maps straight onto the
// seat's own argument of the same name; an empty field is left out of the call entirely, never sent as "".
// Paged by date exactly as the folder page (issue 1264): 50 a page, the next link asking again with before the
// last shown mail's own date.
import (
"context"
"encoding/json"
"html/template"
"net/http"
"strings"
"time"
)
var searchTemplate = template.Must(template.New("search").Parse(`<!DOCTYPE html>
<html><head><title>Search mail</title></head><body>
<h1>Search</h1>
<form method="get" action="/search">
<input type="text" name="query" placeholder="words" value="{{.Query}}">
<input type="text" name="from" placeholder="from" value="{{.From}}">
<input type="text" name="to" placeholder="to" value="{{.To}}">
<input type="text" name="on-or-after" placeholder="on or after" value="{{.OnOrAfter}}">
<input type="text" name="before" placeholder="before" value="{{.Before}}">
<label><input type="checkbox" name="has-attachment" value="on"{{if .HasAttachment}} checked{{end}}> has attachment</label>
<button type="submit">Search</button>
</form>
{{if .Error}}<p>{{.Error}}</p>{{else}}
<ul>
{{range .Mails}}<li>{{.Date}} — {{range .From}}{{.Name}} &lt;{{.Address}}&gt; {{end}}— <a href="/mail?account={{.Account}}&amp;folder={{.Folder}}&amp;uid={{.UID}}">{{.Subject}}</a></li>{{end}}
</ul>
{{if .NextBefore}}<p><a href="{{.NextLink}}">Older</a></p>{{end}}
{{end}}
</body></html>`))
type searchTemplateData struct {
Query, From, To, OnOrAfter, Before, Error, NextBefore, NextLink string
HasAttachment bool
Mails []searchResult
}
// searchArgsFromForm builds mail-store.search's own arguments from the request's query parameters: a field
// left empty is left out of the call entirely (issue 1265/AC1) — never sent empty. "from" is split into a list
// on whitespace or commas, mail-store.search's own array argument. limit is always MostFolderResults (AC2),
// mechanical paging the form itself never carries.
func searchArgsFromForm(q map[string][]string) map[string]any {
get := func(name string) string {
if v, ok := q[name]; ok && len(v) > 0 {
return strings.TrimSpace(v[0])
}
return ""
}
args := map[string]any{"limit": float64(MostFolderResults)}
if v := get("query"); v != "" {
args["query"] = v
}
if v := get("from"); v != "" {
var list []string
for _, part := range strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ' ' }) {
if part != "" {
list = append(list, part)
}
}
if len(list) > 0 {
args["from"] = list
}
}
if v := get("to"); v != "" {
args["to"] = v
}
if v := get("on-or-after"); v != "" {
args["on-or-after"] = v
}
if v := get("before"); v != "" {
args["before"] = v
}
if v := get("has-attachment"); v != "" {
args["has-attachment"] = true
}
return args
}
// searchPage is issue 1265/AC1-AC4.
func (b *Bundle) searchPage(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data := searchTemplateData{
Query: q.Get("query"), From: q.Get("from"), To: q.Get("to"),
OnOrAfter: q.Get("on-or-after"), Before: q.Get("before"), HasAttachment: q.Get("has-attachment") != "",
}
args := searchArgsFromForm(q)
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
raw, err := b.searchAsk(ctx, args)
if err != nil {
data.Error = "the search could not be answered"
renderPage(w, searchTemplate, data)
return
}
var answer searchAnswer
if err := json.Unmarshal(raw, &answer); err != nil {
data.Error = "the search could not be answered"
renderPage(w, searchTemplate, data)
return
}
data.Mails = answer.Found
if len(answer.Found) >= MostFolderResults {
last := answer.Found[len(answer.Found)-1].Date
data.NextBefore = last
next := r.URL.Query()
next.Set("before", last)
data.NextLink = "/search?" + next.Encode()
}
renderPage(w, searchTemplate, data)
}