carlos / platform Public

cmd/console: no console mailer while CARLOS_HOST_CLAIM is on (#250)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 0da9b965f6030082443d39a311f449a317a6342e parent 9bac4ab
2 files changed, +27 −10
  • cmd/console/mailbackends_test.go +16 −5
  • cmd/console/main.go +11 −5
diff --git a/cmd/console/mailbackends_test.go b/cmd/console/mailbackends_test.go
index 37cff9efe..2ffd0ed32 100644
--- a/cmd/console/mailbackends_test.go
+++ b/cmd/console/mailbackends_test.go
@@ -141,23 +141,34 @@ func TestConsoleMailerWiring(t *testing.T) {
prov := mail.NewMemProvisioner("1")
t.Setenv("CARLOS_CONSOLE_MAIL_FROM", "")
- if m, _ := consoleMailer(dep, store, prov, nil, sesOK); m == nil {
+ if m, _ := consoleMailer(dep, store, prov, nil, sesOK, false); m == nil {
t.Error("suffix set, no explicit From: want a mailer on the default address")
}
- if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK); m != nil {
+ if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK, false); m != nil {
t.Error("no suffix, no explicit From: want no mailer")
}
- if m, _ := consoleMailer(dep, store, nil, nil, sesOK); m != nil {
+ if m, _ := consoleMailer(dep, store, nil, nil, sesOK, false); m != nil {
t.Error("no provisioner: want no mailer")
}
t.Setenv("CARLOS_CONSOLE_MAIL_FROM", "invites@shop.example")
- if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK); m == nil {
+ if m, _ := consoleMailer(mail.Deployment{Region: "eu-west-1"}, store, prov, nil, sesOK, false); m == nil {
t.Error("explicit From, no suffix: want a mailer")
}
sesFail := func(context.Context, string) (mail.EmailSender, error) { return nil, errors.New("no creds") }
- if m, _ := consoleMailer(dep, store, prov, nil, sesFail); m != nil {
+ if m, _ := consoleMailer(dep, store, prov, nil, sesFail, false); m != nil {
t.Error("SES client construction failed: want no mailer, not a crash")
}
+ // #250: a console under claims must not have console mail until a gate
+ // route exists — hostClaim true refuses even when every other coordinate
+ // (suffix, region, provisioner) is set, and must do so before touching
+ // SES at all.
+ sesMustNotBeCalled := func(context.Context, string) (mail.EmailSender, error) {
+ t.Fatal("newSES called — consoleMailer must not construct a SES client when hostClaim is true")
+ return nil, nil
+ }
+ if m, _ := consoleMailer(dep, store, prov, nil, sesMustNotBeCalled, true); m != nil {
+ t.Error("hostClaim true: want no mailer, even with suffix, region and provisioner set")
+ }
}
type fakeSESClient struct{}
diff --git a/cmd/console/main.go b/cmd/console/main.go
index d0f4e392f..192e023d4 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -77,7 +77,9 @@
// CARLOS_MAIL_REGION. With neither a From nor a suffix there is no console
// email, and invites go by Keymail or by hand. If the domain is already an
// SES identity the console did not create, mail.EnsureDomain refuses it
-// until a _carlos-mail.<domain> TXT proof is published (#216).
+// until a _carlos-mail.<domain> TXT proof is published (#216). There is no
+// console email while CARLOS_HOST_CLAIM is on, because the console mailer
+// does not yet go through the claim mail gate (#250).
//
// grant/revoke/owners need only CONSOLE_DB.
package main
@@ -286,7 +288,8 @@ func serve() {
} else {
log.Print("console: CARLOS_RELEASE_KEY unset — promotions disabled, running read-only")
}
- st := openServeDB(hostClaimEnabled(os.Getenv))
+ hostClaim := hostClaimEnabled(os.Getenv)
+ st := openServeDB(hostClaim)
storeProvisioner, storeDeployment := storeBackend()
storeScanner, seedAntivirus := storeScannerFor(storeDeployment)
mailDep := mailDeployment()
@@ -294,9 +297,8 @@ func serve() {
// One release.Store, named, because the senders sweep below needs the
// same one the server got.
rel := openStore()
- consoleMail, ensureConsoleMail := consoleMailer(mailDep, rel, mailProvisioner, dnsWriter, mail.NewSESEmailSender)
+ consoleMail, ensureConsoleMail := consoleMailer(mailDep, rel, mailProvisioner, dnsWriter, mail.NewSESEmailSender, hostClaim)
dbBucket, dbRegion, dbPrefix, placement := dbDoorWiring()
- hostClaim := hostClaimEnabled(os.Getenv)
var dbVersions lease.VersionedStore
if hostClaim {
buckets := map[string]string{}
@@ -993,7 +995,11 @@ func mailBackends(
// Like mailBackends, nothing here is fatal: no From, no provisioner or no
// SES client leaves invites on Keymail and the hand-off link.
func consoleMailer(dep mail.Deployment, releases release.Store, p mail.Provisioner, w dnsrec.Writer,
- newSES func(context.Context, string) (mail.EmailSender, error)) (console.ConsoleMailer, func(context.Context)) {
+ newSES func(context.Context, string) (mail.EmailSender, error), hostClaim bool) (console.ConsoleMailer, func(context.Context)) {
+ if hostClaim {
+ log.Print("console: no console email while CARLOS_HOST_CLAIM is on (the console mailer does not go through the claim mail gate yet; #250); invites go by Keymail or by hand")
+ return nil, func(context.Context) {}
+ }
from := console.ConsoleMailFrom(os.Getenv("CARLOS_CONSOLE_MAIL_FROM"), dep.PlatformSuffix)
if from == "" || p == nil {
log.Printf("console: no console email (set CARLOS_CONSOLE_MAIL_FROM or CARLOS_MAIL_PLATFORM_SUFFIX, and CARLOS_MAIL_REGION); invites go by Keymail or by hand")