Status: implemented standalone behavior and recipient privacy, pre-alpha
The IRC adapter has two ordinary direct-message paths plus one live-only DCC negotiation path. This keeps ordinary IRC clients useful without pretending that a disposable nickname or expiring host/port offer is a safe authorization key for retained private history.
Any registered client can send to a currently connected nickname:
PRIVMSG Bob :hello
The same route accepts a notice:
NOTICE Bob :quiet heads-up
If both connections authenticated with SASL, the server commits the message to the single durable conversation for those two account IDs before delivery. It then fans the event out to every registered connection on the sender and recipient accounts. The source keeps the nickname and username presentation used when the message was sent; each receiving account device sees its own current nickname as the incoming target.
If either endpoint is anonymous, the message goes only to the addressed live connection, plus a negotiated sender echo. It is not persisted. Other devices do not receive it, and reconnecting or taking the same nickname does not expose it.
Notices obey those same live-only or durable account-pair rules. Their command
type is retained in history, so replay emits NOTICE rather than PRIVMSG.
The server sends no automatic error or away reply in response to a notice,
including when the target is invalid or unavailable.
An exact CTCP DCC ... PRIVMSG is always connection-scoped, even when both
users authenticated. It must target one currently connected nickname; the
experimental @account form is rejected because it could fan an expiring
offer out to several devices or persist it for an offline recipient. The frame
crosses the recipient's ordinary inbound DM policy, goes only to that exact
connection plus a negotiated sender echo, and is never inserted into the
durable account-pair thread. Reconnect, history, and another device cannot
replay it.
This preserves ordinary user-to-user DCC negotiation without making Telex a file relay. The two clients still establish their own data connection, disclose the addresses contained in that negotiation to one another, and own any encryption or file-safety decision. Registered XDCC providers use the same legacy CTCP presentation but a separately bounded server/provider control path described in external content references.
An authenticated client can send to an existing local account even when none of its devices are connected:
PRIVMSG @alice :this will be waiting for you
NOTICE @alice :this will also be waiting for you
Account names are case-insensitive. @account is an experimental adapter syntax,
not an IRCv3 capability or a valid IRC nickname. Existing clients that cannot
enter it as a message target may need a raw-command input; a future reference
client can expose accounts directly.
The stable target also works for retained history and both cursor types:
CHATHISTORY LATEST @alice * 100
MARKREAD @alice timestamp=2026-07-17T12:34:56.789Z
CHATHISTORY RESUME @alice * 100
MARKDELIVERED @alice msgid=019...
These commands require the corresponding negotiated capabilities. A nickname
can also name an online authenticated peer in these commands, but @account is
the reliable form across disconnects and nick changes.
Each durable thread is keyed by an unordered pair of stable account IDs. The requester's authenticated account is always one member; clients never supply a conversation ID. History references and read or delivery acknowledgements must name events in exactly that pair's conversation. A third account using a valid message ID from another pair receives the same invalid-reference failure as an unknown ID.
Anonymous sessions cannot access durable direct history, and anonymous DMs do not create it. Nick ownership is only a live routing fact, so nick reuse carries no private-message authority.
Every account owns one inbound policy:
openadmits authenticated and anonymous senders unless an authenticated account has an explicit block;allowlistadmits only authenticated accounts with an explicit allow; andclosedadmits nobody, even when an allow rule is retained.
open is the compatibility-preserving default for new and migrated accounts.
Changing policy does not erase explicit rules, so a user can close DMs
temporarily and later return to the previous allowlist. Each account may keep at
most 256 case-insensitive allow/block entries. Rules are private recipient
preferences: channel membership, invitations, prior messages, and replies never
create them implicitly.
Rules accept any syntactically valid canonical account name without checking whether it currently exists. This permits preemptive blocks and, importantly, keeps the management command from becoming an offline account-enumeration oracle. The current account lifecycle does not delete and reassign names.
An authenticated IRC client can inspect or update only its own settings:
DMCONTROL
DMCONTROL LIST
DMCONTROL POLICY open
DMCONTROL POLICY allowlist
DMCONTROL POLICY closed
DMCONTROL ALLOW alice
DMCONTROL BLOCK bob
DMCONTROL REMOVE bob
The server advertises the vendor command as the DMCONTROL ISUPPORT token. A
successful query or mutation returns the complete current state, bounded by the
same rule limit:
:irc.example DMCONTROL Alice POLICY allowlist
:irc.example DMCONTROL Alice ALLOW bob
:irc.example DMCONTROL Alice BLOCK mallory
:irc.example DMCONTROL Alice END
The response composes with labeled-response; a multi-line state response is
placed in the normal labeled batch. The shared JavaScript session exposes
query/set/allow/block/remove helpers and emits dmsettings after a complete
response. The terminal client wraps them with /dm, /dm allowlist, and
/dm allow|block|remove account. This command is a local extension, not an
IRCv3 standard.
Policy is evaluated before a direct conversation is created and before an event
is inserted. Blocked, closed, missing, disabled, self-account, and otherwise
undisclosable targets all produce the same wire result. With
standard-replies, a rejected private message receives:
FAIL PRIVMSG DM_UNAVAILABLE target :Direct messages are unavailable
Older clients receive generic numeric 401 text. Rejected notices remain silent,
as IRC requires. In every case the purported recipient receives no message,
notification, away interaction, or attempt record. The server creates no empty
direct thread, message event, or authority-audit row. Ordinary CTCP content
crosses the same check and follows the normal message path; exact DCC
negotiation additionally uses the live-only rule above. IRCv3 TAGMSG,
including +typing, crosses the same recipient decision before
any live delivery. It is ephemeral: it neither creates a direct conversation
nor appends an event, and an offline recipient has nothing to replay. Only
active recipient devices that negotiated message-tags receive it. A blocked
or unavailable target gets the same opaque DM_UNAVAILABLE result as
PRIVMSG; no purported recipient learns that an attempt occurred.
For authenticated delivery, recipient resolution, policy evaluation, direct thread creation, and event insertion share one immediate SQLite transaction. This prevents a policy change from racing between authorization and persistence. Anonymous-to-authenticated delivery uses the same recipient decision but stays live-only when admitted.
This is transport encryption and server-side authorization, not end-to-end encryption. The standalone server operator can read message content from the running service or database and can inspect stored privacy preferences. TLS protects traffic in transit; local database, backup, and client-storage protection remain deployment responsibilities.