oficina / memoria Public

fix: actually wire a mailer, and read the env var the platform sets

The F5 fix was half done. I added a refusal to boot without SMTP but
never built a sender, so AuthEnv.Mailer stayed nil and rastrillo would
have substituted mail.Logged anyway.

And the refusal read MEMORIA_SMTP_HOST, which no deployment sets: the
suite passes 'CARLOS' to mail.FromEnv, so the platform config is
CARLOS_SMTP_*. A correctly configured instance would have been refused
boot, and the obvious fix under deadline is to delete the check.

Both HOST and FROM are required, because mail.FromEnv falls back to the
logging sender without both — checking only HOST would let a
half-configured instance log sign-in links while this reported fine.

Found while reading how sheets is configured on the bds account, ahead of
deploying.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SiWVaR69Vb2Lzc9wgfit1v
Paul Campbell pushed by paul@keymail.dev 6d9e97c6143b339e34259423c76f713d2b463667 parent b7b89d2
3 files changed, +35 −11
  • cmd/memoria/main.go +9 −0
  • internal/memoria/config.go +17 −6
  • internal/memoriatest/gates_test.go +9 −5
diff --git a/cmd/memoria/main.go b/cmd/memoria/main.go
index b5687f7..5001c06 100644
--- a/cmd/memoria/main.go
+++ b/cmd/memoria/main.go
@@ -8,6 +8,7 @@ import (
"github.com/carlosframework/rastrillo"
"github.com/carlosframework/rastrillo/db"
+ "github.com/carlosframework/rastrillo/mail"
memoria "amadan.net/oficina/memoria/internal/memoria"
)
@@ -51,10 +52,18 @@ func main() {
os.Exit(1)
}
+ // Wire the mailer. Without this rastrillo substitutes mail.Logged,
+ // which writes the magic link — a live credential — into the process
+ // log. LoadConfig refuses to boot in that state unless asked
+ // explicitly, but refusing was only half the fix: nothing was
+ // building a real sender either.
+ //
+ // "CARLOS" is the prefix the suite uses, matching Sheets.
a, err := memoria.NewAuth(d.Writer(), memoria.AuthEnv{
Origin: cfg.Origin,
InstanceKey: cfg.InstanceKey,
Owners: cfg.Owners,
+ Mailer: mail.FromEnv("CARLOS", logger),
}, logger, memoria.Admits(cfg.Owners))
if err != nil {
logger.Error("wire sign-in", "err", err)
diff --git a/internal/memoria/config.go b/internal/memoria/config.go
index 0667c0b..91e7e25 100644
--- a/internal/memoria/config.go
+++ b/internal/memoria/config.go
@@ -66,13 +66,24 @@ func LoadConfig(getenv func(string) string) (Config, error) {
return c, fmt.Errorf("%w: MEMORIA_OWNERS (no admission gate)", ErrMissing)
}
- // Refuse to log credentials by default. MEMORIA_SMTP_HOST is what
- // rastrillo's mail.FromEnv reads; without it and without an explicit
- // opt-in, every sign-in link would be written to a log nobody can
- // delete.
+ // Refuse to log credentials by default.
+ //
+ // The prefix is CARLOS, not MEMORIA: rastrillo's mail.FromEnv takes
+ // the prefix as an argument and the suite passes "CARLOS", so the
+ // platform config is CARLOS_SMTP_*. An earlier version of this check
+ // read MEMORIA_SMTP_HOST, which no deployment sets — so it would have
+ // refused to boot on a correctly configured instance, and the obvious
+ // fix under deadline would have been to delete the check.
+ //
+ // Both HOST and FROM, because mail.FromEnv falls back to the logging
+ // sender unless it has both. Checking only HOST would let a
+ // half-configured instance log links while this function reported
+ // everything fine.
c.AllowLoggedMail = getenv("MEMORIA_ALLOW_LOGGED_MAIL") == "1"
- if getenv("MEMORIA_SMTP_HOST") == "" && !c.AllowLoggedMail {
- return c, fmt.Errorf("%w: MEMORIA_SMTP_HOST (or MEMORIA_ALLOW_LOGGED_MAIL=1 to log sign-in links instead, which puts a live credential in the process log)", ErrMissing)
+ if !c.AllowLoggedMail {
+ if getenv("CARLOS_SMTP_HOST") == "" || getenv("CARLOS_SMTP_FROM") == "" {
+ return c, fmt.Errorf("%w: CARLOS_SMTP_HOST and CARLOS_SMTP_FROM (or MEMORIA_ALLOW_LOGGED_MAIL=1 to log sign-in links instead, which puts a live credential in a log nobody can delete)", ErrMissing)
+ }
}
// Origin is the only one with a default, and only because the
diff --git a/internal/memoriatest/gates_test.go b/internal/memoriatest/gates_test.go
index 60a33be..1d195e0 100644
--- a/internal/memoriatest/gates_test.go
+++ b/internal/memoriatest/gates_test.go
@@ -271,7 +271,8 @@ func TestConfigRefusesIncompleteEnvironment(t *testing.T) {
"MEMORIA_INSTANCE_KEY": "instance-key",
"MEMORIA_OWNERS": "paul@example.com",
"MEMORIA_ORIGIN": "https://memoria.example.com",
- "MEMORIA_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_FROM": "noreply@example.com",
}
// The complete environment must load, or every subtest below passes
// for the wrong reason.
@@ -279,7 +280,7 @@ func TestConfigRefusesIncompleteEnvironment(t *testing.T) {
t.Fatalf("a complete environment did not load: %v", err)
}
- for _, missing := range []string{"MEMORIA_DEK", "MEMORIA_INSTANCE_KEY", "MEMORIA_OWNERS", "MEMORIA_SMTP_HOST"} {
+ for _, missing := range []string{"MEMORIA_DEK", "MEMORIA_INSTANCE_KEY", "MEMORIA_OWNERS", "CARLOS_SMTP_HOST"} {
t.Run("without "+missing, func(t *testing.T) {
env := map[string]string{}
for k, v := range full {
@@ -317,7 +318,8 @@ func TestConfigRefusesMalformedDEK(t *testing.T) {
"MEMORIA_DEK": dek,
"MEMORIA_INSTANCE_KEY": "k",
"MEMORIA_OWNERS": "a@b.c",
- "MEMORIA_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_FROM": "noreply@example.com",
}))
if err == nil {
t.Fatalf("LoadConfig accepted a %s DEK", name)
@@ -334,7 +336,8 @@ func TestConfigFlagsADefaultedOrigin(t *testing.T) {
"MEMORIA_DEK": base64.StdEncoding.EncodeToString(make([]byte, 32)),
"MEMORIA_INSTANCE_KEY": "k",
"MEMORIA_OWNERS": "a@b.c",
- "MEMORIA_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_FROM": "noreply@example.com",
}))
if err != nil {
t.Fatal(err)
@@ -346,7 +349,8 @@ func TestConfigFlagsADefaultedOrigin(t *testing.T) {
"MEMORIA_DEK": base64.StdEncoding.EncodeToString(make([]byte, 32)),
"MEMORIA_INSTANCE_KEY": "k",
"MEMORIA_OWNERS": "a@b.c",
- "MEMORIA_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_HOST": "smtp.example.com",
+ "CARLOS_SMTP_FROM": "noreply@example.com",
"MEMORIA_ORIGIN": "https://real.example.com",
}))
if err != nil {