Skip to content

hook/email: [hook] itip_organizer_is_username; calendar-user-address-set lists the organizer addresses - #2330

Merged
pbiering merged 3 commits into
Kozea:masterfrom
caewa:itip-organizer-is-username
Nov 3, 2026
Merged

pbiering merged 3 commits into
Kozea:masterfrom
caewa:itip-organizer-is-username

Conversation

@caewa

@caewa caewa commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

With itip_enabled = True, a scheduling message is sent only when the event's
ORGANIZER matches the authenticated actor's address in itip_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's
email address and the identity provider vouches for it. There, the map can
only ever be x@y=x@y for every account. That copies the account database
into 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, default False.

  • When 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.
  • It adds to itip_owner_map rather than replacing it. The owner identities
    are the mapped address (if any) plus the username address. With the option
    enabled, an empty map is no longer a config error.
  • A username that is not an email address gets nothing from the option.
  • The other checks are unchanged: mass_email is still incompatible with
    itip_enabled, from_email is never evidence of ownership, and a bare
    username never matches by local part. With the option off, behaviour is
    exactly as before (an empty map with itip_enabled is still an error).

The change sits in resolve_owner_identities() (scheduling.py), the single
function 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> in C: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 not
order 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. Clients
that look for a mailto: find it either way, and clients that use the path
still 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_map
gives the user. The hook already accepted that address as ORGANIZER, but a
client reading calendar-user-address-set could not learn it. The user's own
principal now lists the addresses in the order the hook resolves them: the
mapped address first, then mailto:<username> (with the option), 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 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 to resolve_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:

  • unit: email username becomes an identity (case-insensitive); non-email or
    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;
  • config: empty map + option accepted; empty map without option still an
    error; mass_email + option still an error;
  • end-to-end (dry-run hook, empty map): an email user matching the ORGANIZER
    sends; another email user sends nothing; a non-email user sends nothing;
  • PROPFIND calendar-user-address-set: option on + email user gives
    mailto: then the path; option off gives the path only; a non-email user
    gives the path only; another user's principal gives no mailto:;
  • third commit: owner_addresses() order and edge cases; the mapped address
    listed 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; py 576 passed, 8 skipped, 0 failed; the three
log-level variants OK as well.

Also checked by hand: Radicale started with http_x_remote_user, the email
hook pointed at a local SMTP sink, itip_enabled = True,
itip_organizer_is_username = True, no map. A PUT as alice@example.com with
ORGANIZER:mailto:alice@example.com produced one multipart/mixed mail with
a text/calendar; method="REQUEST" part to the attendee. With another
ORGANIZER, nothing was sent and the existing "does not match owner identities"
error was logged. A PROPFIND Depth 0 on /alice@example.com/ as alice
returned <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), with
itip_organizer_is_username = True and an itip_owner_map that gives each
user an organizer address on a domain whose mail the deployment receives. A
CalDAV web client took ORGANIZER from calendar-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:

* Add: [hook] itip_organizer_is_username: iTIP organizer ownership from a username that is an email address, no itip_owner_map entry needed
* Improve: calendar-user-address-set lists the user's itip_owner_map address, and mailto:<username> with itip_organizer_is_username

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

caewa and others added 3 commits November 3, 2026 11:40
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>
@pbiering pbiering added the hook:email related to email (smtp) hook submaintainer: @nwithan8 label Nov 3, 2026
@pbiering pbiering added this to the 3.9.1 milestone Nov 3, 2026

@pbiering pbiering left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@pbiering
pbiering merged commit 311f0b8 into Kozea:master Nov 3, 2026
44 checks passed
@caewa
caewa deleted the itip-organizer-is-username branch November 3, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hook:email related to email (smtp) hook submaintainer: @nwithan8

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants