oficina / calendar Public

app: read the platform's own SMTP prefix

`carlos email enable` delivers CARLOS_SMTP_*. This app read only
CALENDAR_SMTP_*, so its first deploy refused to boot eleven times in a
row on an instance whose mail was configured perfectly well:

  mail is not configured (set $CALENDAR_SMTP_HOST and
  $CALENDAR_SMTP_FROM) ... refusing to start

The refusal was right and the prefix was wrong. main.go now tries
CALENDAR_ then CARLOS_, so the app's own prefix still wins where an
operator sets it — which is how docs points at its own relay — and the
platform's default works untouched. The refusal message names both, in
the order they are tried, because the message is what somebody reads at
3am and it previously named only the one that would not be set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QG3FX6qzynri8yTgMUm7q7
Paul Campbell pushed by paul@keymail.dev 21558fd40d44ba180d13231c64da77b594d64323 parent 9abf873
2 files changed, +41 −10
  • cmd/calendar/main.go +35 −9
  • internal/calendar/auth.go +6 −1
diff --git a/cmd/calendar/main.go b/cmd/calendar/main.go
index a016cb0..85af4b4 100644
--- a/cmd/calendar/main.go
+++ b/cmd/calendar/main.go
@@ -47,16 +47,13 @@ func main() {
origin = "http://" + opts.Addr
}
- // A mailer only when both halves are configured. mail.FromEnv reads
- // the rest of the CALENDAR_SMTP_* set itself; without a host and a
- // from address there is nothing to build, and calendar.New's own
- // fail-closed rule turns that into a startup refusal outside
- // development rather than a silent no-mail instance.
+ // A mailer only when both halves of some prefix are configured.
+ // calendar.New's own fail-closed rule turns "none were" into a
+ // startup refusal outside development rather than a silent
+ // no-mail instance.
var mailer mail.Sender
- if host := strings.TrimSpace(os.Getenv("CALENDAR_SMTP_HOST")); host != "" {
- if from := strings.TrimSpace(os.Getenv("CALENDAR_SMTP_FROM")); from != "" {
- mailer = mail.FromEnv("CALENDAR", logger)
- }
+ if prefix, ok := smtpPrefix(); ok {
+ mailer = mail.FromEnv(prefix, logger)
}
app, err := calendar.New(calendar.Config{
@@ -85,6 +82,35 @@ func main() {
}
}
+// smtpPrefix picks which <PREFIX>_SMTP_* set to build the mailer from,
+// preferring the app's own over the platform's.
+//
+// The platform delivers CARLOS_SMTP_* — that is what `carlos email
+// enable` writes, and an app that reads only its own prefix boots into
+// a fail-closed refusal on an instance whose mail is configured
+// perfectly well. This app did exactly that on its first deploy: eleven
+// consecutive wake failures, every one of them
+//
+// mail is not configured (set $CALENDAR_SMTP_HOST and
+// $CALENDAR_SMTP_FROM) ... refusing to start
+//
+// on an instance where CARLOS_SMTP_HOST and CARLOS_SMTP_FROM were both
+// set. The refusal was right and the prefix was wrong.
+//
+// CALENDAR_ still wins where it is set, so an operator can point this
+// one app at a different relay without touching the platform's own
+// settings — which is how docs is configured today.
+func smtpPrefix() (string, bool) {
+ for _, prefix := range []string{"CALENDAR", "CARLOS"} {
+ host := strings.TrimSpace(os.Getenv(prefix + "_SMTP_HOST"))
+ from := strings.TrimSpace(os.Getenv(prefix + "_SMTP_FROM"))
+ if host != "" && from != "" {
+ return prefix, true
+ }
+ }
+ return "", false
+}
+
// chooseEnv decides the environment, and is deliberately asymmetric.
//
// "development" is honoured ONLY from the literal -env flag, never from
diff --git a/internal/calendar/auth.go b/internal/calendar/auth.go
index b075613..f738551 100644
--- a/internal/calendar/auth.go
+++ b/internal/calendar/auth.go
@@ -67,7 +67,12 @@ func buildMailer(cfg Config, logger *slog.Logger) (mail.Sender, *recordingMailer
sender := cfg.Mailer
if sender == nil {
if cfg.Env != "development" {
- return nil, nil, fmt.Errorf("calendar: mail is not configured (set $CALENDAR_SMTP_HOST and $CALENDAR_SMTP_FROM) and Env is %q, not %q — refusing to start: a sign-in that cannot send its link is not sign-in, and a reminder that cannot be delivered is not a reminder", cfg.Env, "development")
+ // Both prefixes named, in the order main.go tries them:
+ // the platform's own CARLOS_SMTP_* is what `carlos email
+ // enable` writes, and naming only the app's prefix sent
+ // the first deploy of this app into a wake loop against
+ // an instance whose mail was configured correctly.
+ return nil, nil, fmt.Errorf("calendar: mail is not configured (set $CARLOS_SMTP_HOST and $CARLOS_SMTP_FROM with `carlos email enable`, or $CALENDAR_SMTP_HOST and $CALENDAR_SMTP_FROM to override) and Env is %q, not %q — refusing to start: a sign-in that cannot send its link is not sign-in, and a reminder that cannot be delivered is not a reminder", cfg.Env, "development")
}
sender = mail.Logged(logger)
}