From 4d7e8b44fbfcdc54c8e3571b2581fa63ae6821af Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gerg=C5=91=20Tisza?= Date: Thu, 1 Oct 2015 03:05:23 +0000 Subject: [PATCH] Add UserMailerTransformX and UserMailerSplitTo hooks UserMailerTransformContent allows extensions to change the body of an email sent via UserMailer::send(). This is applied before low-level transformations such as multipart or content encoding. UserMailerTransformMessage is similar but it is run after those transformations. UserMailerSplitTo allows extensions to request that a certain user should always be emailed separately (so when UserMailer::send() is called with an array of target addresses, that user will be split out into a separate call). This is intended for content transformations which need to be different per user, such as encryption. A side effect is that while before a call to UserMailer::send() was either fully succeeded or fully failed, now the message might be delivered to some targets but not others. send() will return a failed Status object in those cases. Bug: T12453 Change-Id: I4c3a018110173c3b5d52a753fdcbec397b590ced --- RELEASE-NOTES-1.27 | 8 ++++ docs/hooks.txt | 20 ++++++++ includes/mail/UserMailer.php | 91 ++++++++++++++++++++++++++++++++---- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index 1670552a02..e3af80217e 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -19,6 +19,14 @@ production. * $wgDataCenterId and $wgDataCenterRoles where added, which will serve as basic configuration settings needed for multi-datacenter setups. $wgDataCenterUpdateStickTTL was also added. +* Added a new hook, 'UserMailerTransformContent', to transform the contents + of an email. This is similar to the EmailUser hook but applies to all mail + sent via UserMailer. +* Added a new hook, 'UserMailerTransformMessage', to transform the contents + of an emai after MIME encoding. +* Added a new hook, 'UserMailerSplitTo', to control which users have to be + emailed separately (ie. there is a single address in the To: field) so + user-specific changes to the email can be applied safely. ==== External libraries ==== diff --git a/docs/hooks.txt b/docs/hooks.txt index 2d268b8ab3..bf595e48bf 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -3292,6 +3292,26 @@ when UserMailer sends an email, with a bounce handling extension. $to: Array of MailAddress objects for the recipients &$returnPath: The return address string +'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance +to split up an email with multiple the To: field into separate emails. +$to: array of MailAddress objects; unset the ones which should be mailed separately + +'UserMailerTransformContent': Called in UserMailer::send() to change email contents. +Extensions can block sending the email by returning false and setting $error. +$to: array of MailAdresses of the targets +$from: MailAddress of the sender +&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys +&$error: should be set to an error message string + +'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through +the MIME transform. Extensions can block sending the email by returning false and setting $error. +$to: array of MailAdresses of the targets +$from: MailAddress of the sender +&$subject: email subject (not MIME encoded) +&$headers: email headers (except To: and Subject:) as an array of header name => value pairs +&$body: email body (in MIME format) as a string +&$error: should be set to an error message string + 'UserRemoveGroup': Called when removing a group; return false to override stock group removal. $user: the user object that is to have a group removed diff --git a/includes/mail/UserMailer.php b/includes/mail/UserMailer.php index 3c28c5f5d0..49ce21c7d2 100644 --- a/includes/mail/UserMailer.php +++ b/includes/mail/UserMailer.php @@ -115,23 +115,17 @@ class UserMailer { * @return Status */ public static function send( $to, $from, $subject, $body, $options = array() ) { - global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail; + global $wgAllowHTMLEmail; $contentType = 'text/plain; charset=UTF-8'; - $headers = array(); - if ( is_array( $options ) ) { - $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null; - $contentType = isset( $options['contentType'] ) ? $options['contentType'] : $contentType; - $headers = isset( $options['headers'] ) ? $options['headers'] : $headers; - } else { + if ( !is_array( $options ) ) { // Old calling style wfDeprecated( __METHOD__ . ' with $replyto as 5th parameter', '1.26' ); - $replyto = $options; + $options = array( 'replyTo' => $options ); if ( func_num_args() === 6 ) { - $contentType = func_get_arg( 5 ); + $options['contentType'] = func_get_arg( 5 ); } } - $mime = null; if ( !is_array( $to ) ) { $to = array( $to ); } @@ -178,6 +172,72 @@ class UserMailer { return Status::newFatal( 'user-mail-no-addy' ); } + // give a chance to UserMailerTransformContents subscribers who need to deal with each + // target differently to split up the address list + if ( count( $to ) > 1 ) { + $oldTo = $to; + Hooks::run( 'UserMailerSplitTo', array( &$to ) ); + if ( $oldTo != $to ) { + $splitTo = array_diff( $oldTo, $to ); + $to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook + // first send to non-split address list, then to split addresses one by one + $status = Status::newGood(); + if ( $to ) { + $status->merge( UserMailer::sendInternal( + $to, $from, $subject, $body, $options ) ); + } + foreach ( $splitTo as $newTo ) { + $status->merge( UserMailer::sendInternal( + array( $newTo ), $from, $subject, $body, $options ) ); + } + return $status; + } + } + + return UserMailer::sendInternal( $to, $from, $subject, $body, $options ); + } + + /** + * Helper function fo UserMailer::send() which does the actual sending. It expects a $to + * list which the UserMailerSplitTo hook would not split further. + * @param MailAddress[] $to Array of recipients' email addresses + * @param MailAddress $from Sender's email + * @param string $subject Email's subject. + * @param string $body Email's text or Array of two strings to be the text and html bodies + * @param array $options: + * 'replyTo' MailAddress + * 'contentType' string default 'text/plain; charset=UTF-8' + * 'headers' array Extra headers to set + * + * @throws MWException + * @throws Exception + * @return Status + */ + protected static function sendInternal( + array $to, + MailAddress $from, + $subject, + $body, + $options = array() + ) { + global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams; + $mime = null; + + $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null; + $contentType = isset( $options['contentType'] ) ? + $options['contentType'] : 'text/plain; charset=UTF-8'; + $headers = isset( $options['headers'] ) ? $options['headers'] : array(); + + // Allow transformation of content, such as encrypting/signing + $error = false; + if ( !Hooks::run( 'UserMailerTransformContent', array( $to, $from, &$body, &$error ) ) ) { + if ( $error ) { + return Status::newFatal( 'php-mail-error', $error ); + } else { + return Status::newFatal( 'php-mail-error-unknown' ); + } + } + /** * Forge email headers * ------------------- @@ -276,6 +336,17 @@ class UserMailer { $headers['Content-transfer-encoding'] = '8bit'; } + // allow transformation of MIME-encoded message + if ( !Hooks::run( 'UserMailerTransformMessage', + array( $to, $from, &$subject, &$headers, &$body, &$error ) ) + ) { + if ( $error ) { + return Status::newFatal( 'php-mail-error', $error ); + } else { + return Status::newFatal( 'php-mail-error-unknown' ); + } + } + $ret = Hooks::run( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) ); if ( $ret === false ) { // the hook implementation will return false to skip regular mail sending -- 2.20.1