Repository navigation
hook/email: [hook] itip_organizer_is_username; calendar-user-address-set lists the organizer addresses - #2330
Merged
Conversation
With itip_enabled, a scheduling message is sent only when the event's ORGANIZER matches the actor's address in itip_owner_map. When the authentication backend already guarantees that the username is the user's email address (single sign-on reverse proxy, LDAP mail as login, admin-created accounts), listing every account as x@y=x@y duplicates the account database in the config, and any account missing from it silently sends nothing. The new option, default False, makes an actor whose username is an email address (one "@", non-empty local part and domain, no whitespace) own that address, compared case-insensitively. It adds to itip_owner_map rather than replacing it, and with the option enabled an empty map is no longer a config error. The mass_email incompatibility and every other check stay as they are. Usernames that are not email addresses get nothing from the option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With [hook] itip_organizer_is_username, the email hook accepts the username as ORGANIZER when it is an email address, but a CalDAV client that takes ORGANIZER from the principal's calendar-user-address-set (RFC 6638 section 2.4.1) only got the principal path, so it had no standard way to learn that address. When the option is on and the authenticated username is an email address, the user's own principal now lists mailto:<username> before the principal path. The path is kept for existing clients. Another user's principal, a request through a share, a non-email username, or the option off: the answer is unchanged. The email rule is the one the hook uses, moved into a public username_email() helper so both share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The email hook accepts the address itip_owner_map gives a user as ORGANIZER, but a CalDAV client that takes ORGANIZER from the principal's calendar-user-address-set (RFC 6638 section 2.4.1) could not learn it: only mailto:<username> (with itip_organizer_is_username) was listed. The user's own principal now lists the addresses the hook would accept for them, in the order it resolves them: the mapped address first, then mailto:<username> with itip_organizer_is_username, then the principal path. A deployment can so give users an organizer address that differs from their login (for example on a domain whose mail it receives), and clients pick it up. Another user's principal, a request through a share, an unmapped user: unchanged. A malformed map, which the hook already refuses at load, lists nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With
itip_enabled = True, a scheduling message is sent only when the event'sORGANIZERmatches the authenticated actor's address initip_owner_map.That map is what stops a user from sending invitations as someone else.
Some deployments authenticate behind a single sign-on reverse proxy
(
[auth] type = http_x_remote_user), where the username is the person'semail address and the identity provider vouches for it. There, the map can
only ever be
x@y=x@yfor every account. That copies the account databaseinto the config: each new user needs a config change and a restart, and a
user missing from the map silently sends nothing.
Change
New boolean
[hook] itip_organizer_is_username, defaultFalse.True, an actor whose username is an email address (one@,non-empty local part and domain, no whitespace) also owns that address,
compared case-insensitively like the map entries.
itip_owner_maprather than replacing it. The owner identitiesare the mapped address (if any) plus the username address. With the option
enabled, an empty map is no longer a config error.
mass_emailis still incompatible withitip_enabled,from_emailis never evidence of ownership, and a bareusername never matches by local part. With the option off, behaviour is
exactly as before (an empty map with
itip_enabledis still an error).The change sits in
resolve_owner_identities()(scheduling.py), the singlefunction both the upsert and the delete paths use to get owner identities.
Second commit: so that CalDAV clients can learn that address, the user's own
principal now lists
mailto:<username>inC:calendar-user-address-set(RFC 6638 section 2.4.1) when the option is on and the username is an email
address. Until now, this property returned only the principal path. The
mailto:comes first and the principal path is kept second: RFC 6638 does notorder the set, and a client that takes the first href as its calendar user
address then gets the one address the hook accepts as
ORGANIZER. Clientsthat look for a
mailto:find it either way, and clients that use the pathstill find it. Nothing changes for another user's principal, a request through
a share, a non-email username, or with the option off. Both places use the
same email rule, through a new
username_email()helper in scheduling.py.Third commit: the same property also lists the address
itip_owner_mapgives the user. The hook already accepted that address as
ORGANIZER, but aclient reading
calendar-user-address-setcould not learn it. The user's ownprincipal now lists the addresses in the order the hook resolves them: the
mapped address first, then
mailto:<username>(with the option), then theprincipal path. A deployment can so give users an organizer address that
differs from their login, for example on a domain whose mail it receives so
it can process attendee replies, and clients pick it up. Another user's
principal, a request through a share, an unmapped user: unchanged. A malformed
map, which the hook already refuses at load, lists nothing here. The lookup is
owner_addresses()in scheduling.py, next toresolve_owner_identities().Security note (also in DOCUMENTATION.md and the reference config)
Only enable the option when usernames are email addresses users cannot choose
freely: a single sign-on reverse proxy, LDAP with the mail attribute as login,
or accounts an administrator creates (e.g. htpasswd). If someone can pick their
own username, a username that looks like another person's email address would
let them send invitations and cancellations as that address.
Tests
New tests in
radicale/tests/test_hook_email_scheduling.py:malformed usernames (
jeff,jeff@,@example.com,a@b@example.com,with whitespace, empty, None) get nothing; option off keeps map-only
behaviour; map and option together (both identities, no duplicate);
mismatched organizer plans nothing;
error;
mass_email+ option still an error;sends; another email user sends nothing; a non-email user sends nothing;
calendar-user-address-set: option on + email user givesmailto:then the path; option off gives the path only; a non-email usergives the path only; another user's principal gives no
mailto:;owner_addresses()order and edge cases; the mapped addresslisted first (with and without the option); an unmapped user gets the path
only; another user's mapped address is never listed on your principal.
Local run (container
python:3.15.0-slim-trixie, branch on current master):tox -e flake8,isort,mypy,py,py_loglevel_info,py_radicale_loglevel_info,py_radicale_loglevel_trace:flake8, isort and mypy OK;
py576 passed, 8 skipped, 0 failed; the threelog-level variants OK as well.
Also checked by hand: Radicale started with
http_x_remote_user, the emailhook pointed at a local SMTP sink,
itip_enabled = True,itip_organizer_is_username = True, no map. A PUT asalice@example.comwithORGANIZER:mailto:alice@example.comproduced onemultipart/mixedmail witha
text/calendar; method="REQUEST"part to the attendee. With anotherORGANIZER, nothing was sent and the existing "does not match owner identities"
error was logged. A PROPFIND Depth 0 on
/alice@example.com/as alicereturned
<href>mailto:alice@example.com</href><href>/alice%40example.com/</href>.In production: the three commits run, backported onto 3.9.0 together with the
iTIP scheduling from master, on a deployment behind a single sign-on reverse
proxy (
http_x_remote_user, usernames are email addresses), withitip_organizer_is_username = Trueand anitip_owner_mapthat gives eachuser an organizer address on a domain whose mail the deployment receives. A
CalDAV web client took
ORGANIZERfromcalendar-user-address-set;invitations (REQUEST) and cancellations (CANCEL) reached real mailboxes, and
the attendees' Accept answers came back to the organizer address, where a
separate tool updated their PARTSTAT.
Changelog
Not edited in this PR. Proposed entries for
## 3.9.1.dev:AI assistance
This change was written with AI assistance (Claude Code). A human reviewed and
tested it before submission.
Reviewed by: William MARTIN
Co-authored-by: Claude Opus 5.5 noreply@anthropic.com