Port 465 cannot work: Postulo speaks STARTTLS and not implicit TLS #158
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#151 SMTP that Google and Microsoft will still accept
Postulo/postulo
Reference
Postulo/postulo#158
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
Found on the live instance, configuring a real provider.
ssl0.ovh.net:465with thecredentials that work: the connection test times out, with the checkbox on and with it off.
The credentials were never the problem.
What exists
Two ways of putting TLS on an SMTP session, and Postulo does one of them.
STARTTLS (ports 587 and 25): connect in the clear, say
EHLO, ask the server to upgradethe socket.
core/mail.pydoes exactly this, andnotifications/smtp.pypassesuse_tls=to Django'sEmailBackend, which does the same.Implicit TLS, or SMTPS (port 465): the handshake comes first, before a single byte of
SMTP. It needs
smtplib.SMTP_SSL, and Django's backend needsuse_ssl=True. Neitherappears anywhere in the codebase.
So on 465 Postulo opens a plaintext socket and waits for a greeting the server will never
send, because the server is waiting for a
ClientHello. Ten seconds later the timeout fires.Turning the STARTTLS checkbox on changes nothing:
ehlo()has already failed beforestarttls()would be reached.The settings page offers a port box and a checkbox called STARTTLS, and nothing on it says
465 cannot work. An administrator enters their provider's documented settings and gets an
unexplained timeout.
What this asks for
Implicit TLS as a configuration Postulo supports, and — whatever is decided about that — a
timeout on 465 that says what is wrong instead of just how long it waited.
Worth being careful about
Two booleans that must not both be true. Django's SMTP backend raises
ValueErrorwhenuse_tlsanduse_sslare both set, and rightly: they are alternatives, not layers. Acheckbox each invites the invalid pair. One control with three states — none, STARTTLS,
implicit TLS — cannot express it, and reads better besides. That is a migration on
SiteSettings.email_use_tls, whosenull=Truecurrently means "not chosen", and it has tokeep meaning that: an existing instance's setting must not change under it.
The environment override has the same shape.
POSTULO_EMAIL_USE_TLSis one variable andone boolean. Whatever replaces the field has to keep that variable meaning what it means for
instances that already set it, and needs a companion or an encoding for the third state.
The default port should follow the choice, and must not overwrite a typed one. 587 for
STARTTLS, 465 for implicit, 25 for neither — as a suggestion when the box is untouched, not
as a correction of what somebody typed.
Say it before the timeout, not instead of implementing it. Ten seconds of nothing is the
worst version of this. Even once 465 works, somebody will point implicit TLS at 587 or the
other way round, and
SMTPServerDisconnectedafter a wait is not an answer. The checkalready has the shape for this:
NO_STARTTLSnames the actual problem when a server does notoffer the extension.
Not only OVH. 465 is what Gmail, Microsoft 365, Fastmail, Zoho and most shared hosting
document first. This is not an unusual configuration; it is arguably the more common one, and
it is the one with no downgrade window — there is no cleartext phase for an attacker to
strip.
#151 sits next to this and is not the same. That issue is about authentication Google
and Microsoft will accept (OAuth rather than a password). This is about the transport those
same providers document. Either can land without the other, and both are needed before
"configure your Gmail account" is a sentence Postulo can say.
The plugin's
test()and the send path both need it.core/mail.pyproves aconfiguration;
notifications/smtp.pyuses it. Fixing one and not the other produces theworst outcome available: a test that passes and mail that does not go.
core/mail.pyopens anSMTP_SSLsocket when the choice is implicit TLS, andnotifications/smtp.pyhands Djangouse_sslinstead ofuse_tls. Both halves, becausefixing one and not the other produces the worst outcome available: a test that passes and
mail that does not go.
One control with three states, as the issue asked — none, STARTTLS, after connecting,
TLS from the first byte — so the pair Django refuses cannot be expressed at all.
The migration adds, carries, then removes. Django's autodetector wanted the drop first,
which is correct as a schema change and would have reset every instance to "nothing chosen",
falling through to a variable most people have never set. A test asserts that order, because
losing it would be silent.
POSTULO_EMAIL_USE_TLSstill means what it means.overridden_by()now accepts more thanone variable per field,
POSTULO_EMAIL_SECURITYwins where both are given, andsite.env_variables()exists so nothing that iterates the whole set meets a tuple where itexpected a name.
The timeout says which mismatch it is. 465 without implicit TLS, or 587 with it, gets a
sentence naming the actual problem in front of the original error. A port that is neither is
left alone, and the suggested port only fills a box left empty — a relay on a port of its own
is ordinary for a self-hosted instance.
22 tests in
tests/test_mail_security.py, a wiki section under Configuration → Email, anupdated
.env.example, and eleven strings in all 39 European catalogues.One note for anyone upgrading with 465 already configured: the old boolean was
false, so itcarries to None. The port is kept; the choice has to be set to TLS from the first byte
once, and then it works.
Shipped in
2e51871on0.3.0, withmainkept level.