cmd/console: no console mailer while CARLOS_HOST_CLAIM is on (#250)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+27
−10
cmd/console/mailbackends_test.go+16 −5cmd/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") |