// Package web is mesh-mail-web, the mail web client (novox/hq ADR 0440), built from mesh-mail's source as mesh-mail-notifier is // (novox/hq ADR 0198 §1, ADR 0175 §1). A Go bundle the node's runtime launches, its own account (runs-as): it // claims mail-webclient (open, status), reaches mail-store's seat verbs (accounts, search, read, send, // send-status), and — once issue 1283 lands — answers its own HTTP endpoint `web`. No database, no state, no // writable directory beyond the one the runtime account needs for its own settings. What this module says, it // says on stderr; stdout is the MCP channel. It lives in a package of its own (issue 1859) so the local run, // cmd/mail-page-dev, serves these same page handlers; cmd/mesh-mail-web is the module's own program. package web import ( "context" "errors" "fmt" "io" "net/http" "os" "sync" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) } // The HTTP endpoint's own state (novox/hq ADR 0440 §8; review round 1 of issue 1283, finding 4): before this // fix, a failure setting up sign-in (the issuer never answering) or starting to listen (its port already // taken) was only logged — mail-webclient.status went on answering serves: true, and mail_web_health healthy, // regardless, since neither asked serveHTTP anything. Kept on the Bundle instead, so both tools say the one // truth. const ( HTTPSettingUpSignIn = "setting up sign-in" HTTPListening = "listening" HTTPEnded = "ended" ) // HTTPStatus is serveHTTP's own phase and (once it has one) its error, read by statusTool and healthTool. type HTTPStatus struct { mu sync.Mutex phase string err error } // NewHTTPStatus starts HTTPSettingUpSignIn: serveHTTP's own first phase, before issuer discovery has even been // tried once. func NewHTTPStatus() *HTTPStatus { return &HTTPStatus{phase: HTTPSettingUpSignIn} } func (h *HTTPStatus) set(phase string, err error) { h.mu.Lock() defer h.mu.Unlock() h.phase = phase h.err = err } // Get answers the endpoint's own phase, and its error once it has one. func (h *HTTPStatus) Get() (phase string, err error) { h.mu.Lock() defer h.mu.Unlock() return h.phase, h.err } // Main is the module's own program (cmd/mesh-mail-web): the seat verbs on the MCP channel and the HTTP // endpoint beside them. func Main() { b := &Bundle{Env: envFromOS(), HTTP: NewHTTPStatus()} go serveHTTP(b) if err := run(b, os.Stdin, os.Stdout); err != nil { logf("%v", err) os.Exit(1) } } // serveHTTP starts the HTTP endpoint `web` (issue 1283), on the port the mesh gave it; a listen it cannot // start is logged, never fatal to the seat verbs stdio.Serve answers on the MCP channel — but it is also kept // on b.HTTP (finding 4), so mail-webclient.status and mail_web_health say it rather than going on claiming to // serve. Sign-in (issue 1263) is set up first, retried on a failure (the issuer may not answer yet): never // served with no session guard at all, which a nil Auth would be (server.go's own fallback, used by its unit // tests alone). func serveHTTP(b *Bundle) { if b.Env.Listen == "" { logf("[mesh-mail-web] started without MESH_MAIL_WEB_LISTEN; its HTTP endpoint is not served") b.HTTP.set(HTTPEnded, errors.New("started without its own listen setting (MESH_MAIL_WEB_LISTEN)")) return } host := InternalHost(b.Env.Node) var auth *Auth for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { a, err := buildAuth(host, b.Env) if err == nil { auth = a break } logf("[mesh-mail-web] sign-in cannot be set up yet (%v); trying again in %s", err, wait) b.HTTP.set(HTTPSettingUpSignIn, err) time.Sleep(wait) } s := NewServer(host, auth, b) b.HTTP.set(HTTPListening, nil) if err := http.ListenAndServe(":"+b.Env.Listen, s); err != nil { logf("[mesh-mail-web] its HTTP endpoint ended: %v", err) b.HTTP.set(HTTPEnded, err) } } // buildAuth reads the oidc-client binding (issue 1262's oidc.json) and discovers the issuer (issue 1263). func buildAuth(host string, env Env) (*Auth, error) { cfg, err := ReadOIDCConfig(env.OIDC) if err != nil { return nil, err } authenticator, err := NewAuthenticator(context.Background(), cfg, "https://"+host+SignedInPath) if err != nil { return nil, err } return &Auth{ Auth: authenticator, Sessions: NewSessions(nil), Pending: NewPendingAuths(nil), Operators: Operators{Path: env.Operators}, Logf: logf, }, nil } // run is main's startup path, factored so a test can run it over a pipe instead of the real stdin/stdout // (mirroring mesh-mail-notifier's own run). func run(b *Bundle, in io.Reader, out io.Writer) error { return stdio.ServeOn("", b.Tools(), in, out) }