ADR 0515 §1, §5: / greets the operator with what the mail signal told today and when they last looked, then shows today, this week, later, the promises by kind, good to know, read later, handled today and the scam warnings, from mail-reading.list; it records the page opened. The classical list moves to /all, as All mail.
135 lines
5.4 KiB
Go
135 lines
5.4 KiB
Go
package web
|
|
|
|
// The web server (novox/hq ADR 0440 §2, §5; issue 1283): answers only its internal name mail.<node>.internal
|
|
// (404 for any other Host — a public name, an IP, or none), sets a content security policy allowing only its
|
|
// own origin on every answer, and writes nothing to disk. Pages are rendered through html/template only (its
|
|
// auto-escaping), never written to a file. The seat verb open (issue 1262's claim) answers the page's address:
|
|
// the root, or one mail's, by account, folder and uid (mail_read's own addressing, tools.go of mesh-mail).
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
// OpenTool is the mail-webclient seat's open verb.
|
|
const OpenTool = "mail-webclient.open"
|
|
|
|
// CSP is ADR 0440 §5's own policy, set on every answer: nothing loaded from elsewhere, and this page never
|
|
// framed by another site.
|
|
const CSP = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; " +
|
|
"frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
|
|
|
|
// InternalHost is this machine's own internal name for the module (ADR 0440 §2): the operator's own setting
|
|
// (set on the machine, outside the manifest) assigns it the label `mail`, so it is always mail.<node>.internal.
|
|
func InternalHost(node string) string { return "mail." + node + ".internal" }
|
|
|
|
// Server answers mesh-mail-web's HTTP endpoint: a Host check and a CSP on every answer (issue 1283), wrapping
|
|
// a plain mux every page is added to, each behind the sign-in (RequireSession).
|
|
type Server struct {
|
|
Host string // InternalHost(node)
|
|
Mux *http.ServeMux
|
|
Auth *Auth // nil refuses no page by session (1283 alone); issue 1263 gives one
|
|
Web *Bundle // nil answers only the stand-in root page (1283 alone); issue 1264 gives one
|
|
}
|
|
|
|
// NewServer is a Server for host: with auth given, the sign-in callback (issue 1263) and every page behind a
|
|
// session (RequireSession); with b also given, the real pages (accounts and folders, issue 1264; search,
|
|
// issue 1265; a mail and its attachments, issue 1266; the write page and a send's state, issue 1267) in place
|
|
// of 1283's own stand-in root.
|
|
func NewServer(host string, auth *Auth, b *Bundle) *Server {
|
|
s := &Server{Host: host, Mux: http.NewServeMux(), Auth: auth, Web: b}
|
|
protect := func(h http.HandlerFunc) http.HandlerFunc {
|
|
if auth != nil {
|
|
return auth.RequireSession(h)
|
|
}
|
|
return h
|
|
}
|
|
if auth != nil {
|
|
s.Mux.HandleFunc(SignedInPath, auth.Callback)
|
|
}
|
|
if b != nil {
|
|
s.Mux.HandleFunc("/", protect(b.listPage))
|
|
s.Mux.HandleFunc("/all", protect(b.accountsPage))
|
|
s.Mux.HandleFunc("/folder", protect(b.folderPage))
|
|
s.Mux.HandleFunc("/search", protect(b.searchPage))
|
|
s.Mux.HandleFunc("/mail", protect(b.mailPage))
|
|
s.Mux.HandleFunc("/attachment", protect(b.attachmentPage))
|
|
s.Mux.HandleFunc("/write", protect(b.writePage))
|
|
s.Mux.HandleFunc("/send", protect(RequireFormToken(b.sendHandler)))
|
|
s.Mux.HandleFunc("/send-status", protect(b.sendStatusPage))
|
|
s.Mux.HandleFunc("/mail-correct", protect(RequireFormToken(b.mailCorrectHandler)))
|
|
s.Mux.HandleFunc("/mail-undo", protect(RequireFormToken(b.mailUndoHandler)))
|
|
} else {
|
|
s.Mux.HandleFunc("/", protect(s.rootPage))
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ServeHTTP is issue 1283/AC1, AC2: every answer carries the CSP; a request for any Host but this server's own
|
|
// internal name is 404, naming nothing of the mail.
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Security-Policy", CSP)
|
|
host := r.Host
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
host = h
|
|
}
|
|
if host != s.Host {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
s.Mux.ServeHTTP(w, r)
|
|
}
|
|
|
|
// rootTemplate is 1283/AC4's own stand-in page: no account or mail content yet (issue 1264), only enough to
|
|
// prove the server answers at all, through html/template (auto-escaping), never a file written to disk.
|
|
var rootTemplate = template.Must(template.New("root").Parse(
|
|
`<!DOCTYPE html><html><head><title>Mail</title></head><body><p>mesh-mail-web is up.</p></body></html>`))
|
|
|
|
func (s *Server) rootPage(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = rootTemplate.Execute(w, nil)
|
|
}
|
|
|
|
// openTool is issue 1262's claimed verb open, built here (1283) now that the server's own host exists: with
|
|
// nothing given, the root; with account, folder and uid together, that mail's own address.
|
|
func (b *Bundle) openTool(args map[string]any) (any, error) {
|
|
if err := only(args, "account", "folder", "uid"); err != nil {
|
|
return nil, err
|
|
}
|
|
account, _ := args["account"].(string)
|
|
folder, _ := args["folder"].(string)
|
|
uidArg, hasUID := args["uid"]
|
|
|
|
base := "https://" + InternalHost(b.Env.Node) + "/"
|
|
if account == "" && folder == "" && !hasUID {
|
|
return map[string]any{"url": base}, nil
|
|
}
|
|
uid, isNum := uidArg.(float64)
|
|
if account == "" || folder == "" || !isNum || uid <= 0 {
|
|
return nil, fmt.Errorf("%s needs account, folder and uid together, or none of them", OpenTool)
|
|
}
|
|
q := url.Values{"account": {account}, "folder": {folder}, "uid": {strconv.FormatFloat(uid, 'f', 0, 64)}}
|
|
return map[string]any{"url": "https://" + InternalHost(b.Env.Node) + "/mail?" + q.Encode()}, nil
|
|
}
|
|
|
|
// only refuses an argument the tool does not take (mirroring mesh-mail's own tools.go).
|
|
func only(args map[string]any, takes ...string) error {
|
|
for k := range args {
|
|
ok := false
|
|
for _, t := range takes {
|
|
if k == t {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("the argument %q is not taken", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|