From ed6b568d605bfef7801fd19d501459bed25f97d1 Mon Sep 17 00:00:00 2001
From: Claude
Date: Fri, 25 Sep 2026 18:42:39 +0000
Subject: [PATCH] Hand the mail settings to the container, and rebuild on
deploy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two silent failures sitting between Phase A and a working invitation.
The compose file never passed MAIL_* through. Compose does not give a
container the contents of .env — it only substitutes into the compose
file — so the settings could be filled in correctly and read by nobody,
with the app reporting mail as unconfigured and the values sitting right
there on disk. test_deployment.py now fails the build whenever
.env.example and docker-compose.yml drift apart, which is how this
happened and how it would happen again.
The app's code is baked into the image (COPY apps /srv/apps), so a pull
followed by --force-recreate runs the old code and says nothing. Added
scripts/deploy-board.sh: rebuild, sync the compose file, restart, print
the log, and name any setting present in .env.example and missing from
the live .env. It never touches .env itself.
Also: "no mail server configured" is now told apart from "the mail
server refused". They want different things done about them, and the
first one sent an admin looking for an SMTP error that never existed.
The new-member form says so before it is filled in rather than after.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
---
apps/board/members.py | 16 ++++--
apps/board/templates/member_created.html | 20 +++++--
apps/board/templates/member_new.html | 10 ++++
apps/board/tests/test_deployment.py | 46 ++++++++++++++++
apps/board/tests/test_invites.py | 32 +++++++++++
docs/server-setup.md | 40 +++++++++++++-
infra/board/docker-compose.yml | 17 ++++++
scripts/deploy-board.sh | 67 ++++++++++++++++++++++++
8 files changed, 241 insertions(+), 7 deletions(-)
create mode 100644 apps/board/tests/test_deployment.py
create mode 100755 scripts/deploy-board.sh
diff --git a/apps/board/members.py b/apps/board/members.py
index e4d58f7..d6d2218 100644
--- a/apps/board/members.py
+++ b/apps/board/members.py
@@ -129,6 +129,10 @@ def new():
# The form says so before it is filled in; offering a ticked checkbox
# and reporting the problem on submit wastes the work of filling it.
can_create_accounts=bool(current_app.config.get("ADMIN_TOKEN")),
+ # Same courtesy for mail: without a server configured the invitation
+ # cannot leave the box, and the admin has to pass the link on by
+ # hand. Worth knowing before filling the form rather than after.
+ can_send_mail=mail.configured(),
gitea_url=current_app.config["GITEA_URL"].rstrip("/"),
)
@@ -175,19 +179,25 @@ def new():
return redirect(url_for("members.new"))
invite_link = None
+ mail_problem = None
if create_account:
link = auth.invite_url(invites.issue(cursor.lastrowid, "invite"))
try:
mail.send_invite(email, display_name or login, link)
- except (mail.MailFailed, mail.MailNotConfigured):
+ except mail.MailNotConfigured:
+ # Nothing is broken — this server has simply never been given a mail
+ # server. Told apart from a failure on purpose: an admin sent looking
+ # for an SMTP error that does not exist is an afternoon wasted.
+ invite_link, mail_problem = link, "unconfigured"
+ except mail.MailFailed:
# The account exists and the member cannot reach it. Showing the
# admin the link is the difference between a delayed invitation and
# a person who simply never gets in.
- invite_link = link
+ invite_link, mail_problem = link, "failed"
return render_template("member_created.html", login=login,
email=email, created=create_account,
- invite_link=invite_link,
+ invite_link=invite_link, mail_problem=mail_problem,
role_label=ROLE_LABELS[role])
diff --git a/apps/board/templates/member_created.html b/apps/board/templates/member_created.html
index 6b8bad9..2c24020 100644
--- a/apps/board/templates/member_created.html
+++ b/apps/board/templates/member_created.html
@@ -12,14 +12,28 @@
{# The account exists but the invitation never left the building. Showing the
link is the difference between a delayed invitation and a person who
simply never gets in. #}
-
- No se pudo enviar el correo. Pásale este enlace por un canal privado —
- sirve una sola vez y caduca en 7 días.
+ {% if mail_problem == 'unconfigured' %}
+
+ Este servidor todavía no envía correo. Pásale este enlace por un canal
+ privado — sirve una sola vez y caduca en 7 días.
+ {% else %}
+
+ No se pudo enviar el correo: el servidor de correo rechazó el envío o no
+ respondió. Pásale este enlace por un canal privado — sirve una sola vez y
+ caduca en 7 días.
+
+ {% endif %}
{{ invite_link }}
+ {% if mail_problem == 'unconfigured' %}
+ Añade los valores MAIL_* en
+ /srv/board/.env para que la próxima invitación
+ salga sola.
+ {% else %}
Revisa la configuración de correo del servidor para que la próxima
invitación salga sola.
+ {% endif %}
{% endif %}
+ {% if can_create_accounts and not can_send_mail %}
+ {# Not an error: the account is still created and the invitation still
+ issued. But it leaves by hand, and finding that out after filling the
+ form is how an invitation ends up believed sent and never delivered. #}
+
+ Este servidor todavía no envía correo, así que la invitación no se
+ enviará sola: al terminar te mostraremos el enlace para que se lo pases.
+