Your own email: Encryption starts on None, so a password or XOAUTH2 token can cross the network in the clear #360
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#360
Loading…
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?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the own-mail plugin, the connection form builder and the SMTP sign-in, and building the form's fields from the snapshot.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atcore/mail_choices.py:25(NONE, still the first member ofMailSecurity, which moved there with #248 and is re-exported bycore/mail.py),core/mail_auth.py:133-148(authenticate) andcore/mail.py:113(its call incheck_connection); the plugin and the form builder have not moved.The Encryption field on the Your own email connection has no blank option and no default, and its first choice is None. A form left as it is therefore saves
security="none", which_portturns into port 25, and the sign-in then sends the password or the provider's bearer token without TLS. The field's own help says the opposite of its default: "Nearly every provider wants STARTTLS on 587."plugins/own_mail/__init__.py:86-92(thesecurityfield:choices=tuple(mail.MailSecurity.choices),required=False, nodefault);plugins/forms.py:133-134(form_field_forbuilds a plainChoiceFieldover those choices, with no empty choice);core/mail.py:34(NONEis the first member ofMailSecurity);plugins/own_mail/__init__.py:230-242(_portpicks 25 fornone);core/mail_auth.py:158-173(authenticate), called fromcore/mail.py:123(check_connection) and fromGuardedBackend.open.<select name="plugin_security">withnone,starttls,sslin that order and nothing selected, so the browser submitsnone.check_connectionandsendthen open a plain SMTP session on port 25 and callauthenticate. With a provider chosen,server.auth("XOAUTH2", ..., initial_response_ok=True)puts the base64 bearer token on the firstAUTHline;smtplib.SMTP.authdoes not check that the server advertisedAUTH, let alone that the session is encrypted, so the token is on the wire before the server can answer "530 Must issue a STARTTLS command first". With a password,server.loginsends it in the clear to any server that advertisesAUTHbefore TLS. Nothing inauthenticate,check_connectionorGuardedBackend.openchecks that TLS is active before a credential is sent.SMTP.Sendtoken). Today the connection form for this plugin answers 500 before it can be saved (the password field's help text, reported separately), so the path is latent; it becomes live as soon as that is fixed, and applies to any own-mail connection stored before.securityfielddefault="starttls"and order its choices STARTTLS, TLS from the first byte, None, so an untouched form means what the help text says. In the own-mail path (OwnMail.testandOwnMail.send), refuse to sign in on a session that is not encrypted, with aConnectionFailedsentence that names the fix; a small helper such asmail_auth.require_tls(server)that checksisinstance(server.sock, ssl.SSLSocket)beforeauthenticatedoes it for both. Keep the instance's own transport free to use a loopback relay without TLS, which is the ordinary case forPOSTULO_EMAIL_HOST=localhost.Done when
tests/test_mail_security.pybuilds aConnectionFormforown-mailand asserts the security field's initial value isstarttls.tests/test_mail_xoauth2.pyrunsOwnMail.testwithsecurity="none", a username and a token against a stub SMTP server that offers neither STARTTLS nor AUTH, and asserts that noAUTHcommand reached it and that the result names the missing encryption.