A contact channel validates itself, and says what would confirm it #146

Closed
opened 2026-09-09 10:49:02 +00:00 by tiagoagueda · 1 comment
Owner

Observation

every contact plugin should handle is validation (according to predefined rules) and
confirmation if needed using one of the outside connectors like SMTP for mail

What exists

Three kinds of contact detail, at three different stages, each having invented its own
answer:

Several per account One primary Validated Confirmed Unique on the instance
Email (allauth EmailAddress) yes yes yes, by allauth yes, by allauth over SMTP yes
Telephone (PhoneNumber, #90) yes yes shallow, on purpose no yes
Postal (#92) proposed proposed — — deliberately not

Validation is deliberately shallow for telephone numbers, and the reason expires.
phones.py says it outright:

It does not decide whether a number is real. That needs the whole numbering plan of every
country, which is a multi-megabyte library and a constant stream of updates, and Postulo
has no use for the answer: it is not going to dial anything.

That was true when a number was something a person read off a screen and typed into a
handset. The moment a number is a channel Postulo sends a code to, Postulo is dialling it,
and the argument for not checking the shape goes with it.

Confirmation exists once, for email, and is not reusable. allauth owns the flow: the
token, the expiry, the resend, the link, the verified flag. Nothing in Postulo can ask a
transport to carry a confirmation for something that is not an email address.

What this asks for

One contract that every kind of contact detail obeys: how it is shaped, whether it can be
proved, and which connector proves it.

Worth being careful about

Not everything confirmable is confirmable the same way, and one of them is not confirmable
at all.
An address takes a link; a number takes a short code typed back, because a link in
an SMS is a phishing lesson nobody should be teaching. A postal address can only be confirmed
by posting something to it, which is what some services do and is not a thing this project
will build — so the contract has to allow validated but not confirmable as a legitimate
answer, rather than treating it as a missing feature.

Validation has three depths and they should be named. Syntactic (is this the shape of an
address, does this parse as E.164); plausible (is this dialling code assigned, does this
domain have an MX record); and real (does anybody answer). Postulo currently does the first
for email through allauth and less than the first for numbers. The middle one is where a
library and a data file arrive, which is the cost phones.py refused; it is worth refusing
again, deliberately, rather than by inheritance.

A confirmation is a message, so it needs the thing that carries messages. For email that
is the transport, which already exists and is already locked when it is the last way back in
(#104). For a number it is whatever #143 decides. The contract should ask a channel which
connector confirms me
and get a name back, rather than each kind reaching for its own.

Rate limits belong in the contract, not in each implementation. A resend button is a way
to make somebody's phone buzz forty times, and one of these channels costs money per send.
POSTULO_CAPTURE_RATE and its siblings are the precedent.

Every message is a message somebody reads in their own language, and a confirmation code
sent to a number has no interface around it to carry context — so its text has to say which
instance sent it and what it is for, in the recipient's language, in one line.

allauth is not going to be rewritten to fit this. The contract has to be one an existing,
external, working implementation can be described by rather than one it must adopt: email
satisfies the contract by delegation, and the two new kinds implement it directly. A contract
that requires changing allauth is a contract that will not be adopted.

This is the shape of a plugin kind, and it inherits their rules. If a contact channel is
a plugin (#90 made the first of them one), then #126's surface, #128's data question and
#129's self-containment all apply, and so does the rule that switching one off may not remove
somebody's last way back into their account.

## Observation > every contact plugin should handle is validation (according to predefined rules) and > confirmation if needed using one of the outside connectors like SMTP for mail ## What exists Three kinds of contact detail, at three different stages, each having invented its own answer: | | Several per account | One primary | Validated | Confirmed | Unique on the instance | | --- | --- | --- | --- | --- | --- | | Email (allauth `EmailAddress`) | yes | yes | yes, by allauth | **yes**, by allauth over SMTP | yes | | Telephone (`PhoneNumber`, #90) | yes | yes | shallow, on purpose | **no** | yes | | Postal (#92) | proposed | proposed | — | — | deliberately not | **Validation is deliberately shallow for telephone numbers, and the reason expires.** `phones.py` says it outright: > It does not decide whether a number is real. That needs the whole numbering plan of every > country, which is a multi-megabyte library and a constant stream of updates, and Postulo > has no use for the answer: **it is not going to dial anything**. That was true when a number was something a person read off a screen and typed into a handset. The moment a number is a channel Postulo sends a code to, Postulo *is* dialling it, and the argument for not checking the shape goes with it. **Confirmation exists once, for email, and is not reusable.** allauth owns the flow: the token, the expiry, the resend, the link, the `verified` flag. Nothing in Postulo can ask a transport to carry a confirmation for something that is not an email address. ## What this asks for One contract that every kind of contact detail obeys: how it is shaped, whether it can be proved, and which connector proves it. ## Worth being careful about **Not everything confirmable is confirmable the same way, and one of them is not confirmable at all.** An address takes a link; a number takes a short code typed back, because a link in an SMS is a phishing lesson nobody should be teaching. A postal address can only be confirmed by posting something to it, which is what some services do and is not a thing this project will build — so the contract has to allow **validated but not confirmable** as a legitimate answer, rather than treating it as a missing feature. **Validation has three depths and they should be named.** Syntactic (is this the shape of an address, does this parse as E.164); plausible (is this dialling code assigned, does this domain have an MX record); and real (does anybody answer). Postulo currently does the first for email through allauth and less than the first for numbers. The middle one is where a library and a data file arrive, which is the cost `phones.py` refused; it is worth refusing again, deliberately, rather than by inheritance. **A confirmation is a message, so it needs the thing that carries messages.** For email that is the transport, which already exists and is already locked when it is the last way back in (#104). For a number it is whatever #143 decides. The contract should ask a channel *which connector confirms me* and get a name back, rather than each kind reaching for its own. **Rate limits belong in the contract, not in each implementation.** A resend button is a way to make somebody's phone buzz forty times, and one of these channels costs money per send. `POSTULO_CAPTURE_RATE` and its siblings are the precedent. **Every message is a message somebody reads in their own language**, and a confirmation code sent to a number has no interface around it to carry context — so its text has to say which instance sent it and what it is for, in the recipient's language, in one line. **allauth is not going to be rewritten to fit this.** The contract has to be one an existing, external, working implementation can be described by rather than one it must adopt: email satisfies the contract by delegation, and the two new kinds implement it directly. A contract that requires changing allauth is a contract that will not be adopted. **This is the shape of a plugin kind, and it inherits their rules.** If a contact channel is a plugin (#90 made the first of them one), then #126's surface, #128's data question and #129's self-containment all apply, and so does the rule that switching one off may not remove somebody's last way back into their account.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:49:02 +00:00
Author
Owner

postulo.core.channels holds the contract: a ContactChannel protocol with a
check() that reports shape, a named depth, a proof kind, and a carrier() naming what
would carry a confirmation.

The three depths are named and the middle one is refused again, deliberately rather than
by inheritance. The issue was right that half of phones.py's argument expires when a number
becomes a channel Postulo sends to — and only half: the cost of a numbering-plan library is
unchanged, and the thing that settles the question is the confirmation itself, which is free
and conclusive. A library saying a range is assigned would sit between a typed value and a
check that answers it anyway.

What did change: telephone checking rises from less than syntactic to syntactic — a
country in front, four to fifteen digits, which is E.164 and costs nothing. A number that
fails is still saved.
phones.py means what it says; this reports, and the caller that
refuses is #142.

Validated-but-never-provable is expressible, and kept apart from a second empty answer:
UNPROVABLE (postal, permanently) versus a channel that is provable and has no carrier yet
(telephone, until #143). Somebody locked out of their account deserves to be told which of
the two they met, and #144 now has the vocabulary for it.

allauth is described, not rewritten. EmailChannel delegates: allauth keeps the token,
the expiry, the resend, the link and the verified flag. Its carrier() returns whatever
transport is selected rather than assuming SMTP, so it and the #104 interlock read the same
answer.

Rate limits are in the contract, as asked: POSTULO_CONFIRMATION_RATE, one limit across
every channel, because the channel that costs money per send is not the one whose own setting
anybody would remember to configure. And one_line() builds the message a code arrives in —
naming the instance and saying nobody will ask for it, because a stranger's message containing
six digits is the shape of every scam there is.

Not a plugin kind yet, and the reason is recorded. A plugin can be switched off, and a
channel that can be switched off is somebody's way back into their account that can be switched
off. Transports have an interlock for exactly that (#104), and it would need extending before
the two ideas could safely meet. The protocol is the one a kind would declare, so #128 and
#129 apply to the step rather than to a redesign.

A Postal stand-in lives in the tests rather than in the codebase — a channel for a model
Postulo does not have yet would be speculation, and #147 brings the real one.

27 tests in tests/test_contact_channels.py, a wiki section under Configuration, and six
strings in all 39 European catalogues.

Shipped in 019d7c8 on 0.3.0, with main kept level.

`postulo.core.channels` holds the contract: a `ContactChannel` protocol with a `check()` that reports shape, a named depth, a `proof` kind, and a `carrier()` naming what would carry a confirmation. **The three depths are named and the middle one is refused again**, deliberately rather than by inheritance. The issue was right that half of `phones.py`'s argument expires when a number becomes a channel Postulo sends to — and only half: the cost of a numbering-plan library is unchanged, and the thing that settles the question is the confirmation itself, which is free and conclusive. A library saying a range is assigned would sit between a typed value and a check that answers it anyway. What did change: telephone checking rises from *less than syntactic* to *syntactic* — a country in front, four to fifteen digits, which is E.164 and costs nothing. **A number that fails is still saved.** `phones.py` means what it says; this reports, and the caller that refuses is #142. **Validated-but-never-provable is expressible**, and kept apart from a second empty answer: `UNPROVABLE` (postal, permanently) versus a channel that is provable and has no carrier yet (telephone, until #143). Somebody locked out of their account deserves to be told which of the two they met, and #144 now has the vocabulary for it. **allauth is described, not rewritten.** `EmailChannel` delegates: allauth keeps the token, the expiry, the resend, the link and the verified flag. Its `carrier()` returns whatever transport is selected rather than assuming SMTP, so it and the #104 interlock read the same answer. **Rate limits are in the contract**, as asked: `POSTULO_CONFIRMATION_RATE`, one limit across every channel, because the channel that costs money per send is not the one whose own setting anybody would remember to configure. And `one_line()` builds the message a code arrives in — naming the instance and saying nobody will ask for it, because a stranger's message containing six digits is the shape of every scam there is. **Not a plugin kind yet, and the reason is recorded.** A plugin can be switched off, and a channel that can be switched off is somebody's way back into their account that can be switched off. Transports have an interlock for exactly that (#104), and it would need extending before the two ideas could safely meet. The protocol is the one a kind would declare, so #128 and #129 apply to the step rather than to a redesign. A `Postal` stand-in lives in the tests rather than in the codebase — a channel for a model Postulo does not have yet would be speculation, and #147 brings the real one. 27 tests in `tests/test_contact_channels.py`, a wiki section under *Configuration*, and six strings in all 39 European catalogues. Shipped in `019d7c8` on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#146
No description provided.