package web
// 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(`
Search mail
Search
{{if .Error}}{{.Error}}
{{else}}
{{range .Mails}}- {{.Date}} โ {{range .From}}{{.Name}} <{{.Address}}> {{end}}โ {{.Subject}}
{{end}}
{{if .NextBefore}}Older
{{end}}
{{end}}
`))
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)
}