117 lines
4.1 KiB
Go
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}} <{{.Address}}> {{end}}— <a href="/mail?account={{.Account}}&folder={{.Folder}}&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)
|
|
}
|