Page MenuHomePhabricator

One-click email unsubscribe service in MediaWiki core
Open, Needs TriagePublic

Description

MediaWiki core and extensions use email in a number of different ways (see a survey here); most of these only share code at the lowest level of functionality (via UserMailer, responsible for actually sending out the email but not managing contents or targets in any way). Because of T355450: mediawiki support for one-click unsubscribe we'll need to adjust many of these by adding a machine-readable and machine-usable unsubscribe link (see here for technical details). This means we will need to provide an URL which encodes a user identity and an action ("unsubscribe from type X email") and isn't spoofable (you can't unsubscribe someone else by constructing the URL and clicking on it, even though the URL will need to work without session cookies).

This task proposes a way of doing that that can also be reused to do one-click actions from emails more generally - it's no extra work compared to implementing one-click unsubscribe, and could be useful for other things (such as Gmail one-lick actions).

Requirements
  1. The system should not hard-code what "unsubscribe" means. Usually it's unsetting a user preference, but it might be something else (removing a page from a watchlist, muting a user). This also allows easy reusability if we want non-unsubscribe-related one-click actions from emails.
  2. The system needs to be able to assure the action is coming from the right user (ie. that it wouldn't be possible to construct the URL for the action without access to the email) without relying on any other authentication (such as session cookies or interactive login), per RFC 8058.
  3. RFC 8058 specifies a POST endpoint, but it should also work with GET so we can reuse it for unsubscribe links in emails as the user might have an easier way of finding those, or their mail client might not support automatic unsubscribe; or we want to include multiple types of unsubscription as links (e.g. "unsubscribe from this type of Echo notification" vs "mute notifications from this user" vs "mute notifications from this page")
  4. In the GET case, we should show a user-friendly landing page that tells the user what happened and how to undo it (in the case of an misclick / changed minds).
Proposal
  • Create a helper for converting between a tuple of (user, action, ...further action-specific parameters) and a "one-click token", a string that's suitable for inclusion into an URL and cryptographically secured against tampering (signed, or signed and encrypted). Preferably in some standard way, such as a JWT.
  • Provide a mechanism by which any code can register a one-click action, and there is some central dispatcher which can take a one-click token and calls the suitable handler. The registration could be done with hooks, but maybe more elegant to have a registry service and extension.json attributes with ObjectFactory specs.
  • Provide an action handler for the common case of setting a specific user preference to a specific value.
  • Provide an endpoint that can take a one-click token and dispatch it; probably a REST API route. It should support GET and POST, and allow for write on GET (in Wikimedia's case, that means being routed to the primary DC).
  • Make it easy to associate a one-click unsubscribe URL with an email: add an option to UserMailer::send() / Emailer::send() which takes the token, generates the URL and adds the appropriate List-Unsubscribe and List-Unsubscribe-Post headers.

Event Timeline

GET should not be used for any action with side-effect. See T92357/T46602 and for comparible cases, T88044/T135170/T310393

Looks like this feature is already partially implemented. If you check the UserMailer class we already send the List-Unsubscribe header:

The code adding the List-Unsubscrive header lives in UserMailer class.
See https://gerrit.wikimedia.org/g/mediawiki/core/+/9db4dbf46d5010d43cc63e5048ad709316dc0b86/includes/mail/UserMailer.php#281

		$headers['X-Mailer'] = 'MediaWiki mailer';
		$headers['List-Unsubscribe'] = '<' . SpecialPage::getTitleFor( 'Preferences' )
			->getFullURL( '', false, PROTO_CANONICAL ) . '>';

Added in T58315.

I also checked one of the emails I got from English Wikipedia, let me attach headers:

To: "PMiazga (WMF)" <PMiazga@wikimedia.org>
Subject: {REDACTED}
List-Help: https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist
From: Wikipedia <wiki@wikimedia.org>
Reply-To: wiki@wikimedia.org
Date: Tue, 12 Dec 2023 01:37:05 +0000
Message-ID: <enwiki.6577b9416feec1.36016279@en.wikipedia.org>
X-Mailer: MediaWiki mailer
List-Unsubscribe: <https://en.wikipedia.org/wiki/Special:Preferences>
MIME-Version: 1.0
Content-type: text/plain; charset=UTF-8
Content-transfer-encoding: 8bit

The link we send is just a generic link to the user Special:Preferences page.

To summarize, the headers are already injected, we need to implement a page that would handle the one click unsubscription.

Thanks, I missed that. Usually when there is an unsubscribe link, Gmail shows it in the details popup (like in the first screenshot here and I don't see that for MediaWiki emails, so I wonder if we are doing it wrong somehow? Maybe it's related to the DKIM signature issue mentioned in the parent task.

In any case, UserMailer does not know how to handle a one-click unsubscribe (that's going to be different for different kinds of emails) so we still need to arrange for the unsubcribe link to be passed through from higher-level logic.