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

125 lines
4.4 KiB
Go

// 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.
package main
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
}
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)
}