diff options
Diffstat (limited to 'includes/UserMailer.php')
-rw-r--r-- | includes/UserMailer.php | 245 |
1 files changed, 165 insertions, 80 deletions
diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 01e7132d..6eb99172 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -21,9 +21,9 @@ * @author <brion@pobox.com> * @author <mail@tgries.de> * @author Tim Starling + * @author Luke Welling lwelling@wikimedia.org */ - /** * Stores a single person's name and email address. * These are passed in via the constructor, and will be returned in SMTP @@ -31,9 +31,9 @@ */ class MailAddress { /** - * @param $address string|User string with an email address, or a User object - * @param $name String: human-readable name if a string address is given - * @param $realName String: human-readable real name if a string address is given + * @param string|User $address string with an email address, or a User object + * @param string $name human-readable name if a string address is given + * @param string $realName human-readable real name if a string address is given */ function __construct( $address, $name = null, $realName = null ) { if ( is_object( $address ) && $address instanceof User ) { @@ -77,7 +77,6 @@ class MailAddress { } } - /** * Collection of static functions for sending mail */ @@ -109,9 +108,13 @@ class UserMailer { /** * Creates a single string from an associative array * - * @param $headers array Associative Array: keys are header field names, + * @param array $headers Associative Array: keys are header field names, * values are ... values. - * @param $endl String: The end of line character. Defaults to "\n" + * @param string $endl The end of line character. Defaults to "\n" + * + * Note RFC2822 says newlines must be CRLF (\r\n) + * but php mail naively "corrects" it and requires \n for the "correction" to work + * * @return String */ static function arrayToHeaderString( $headers, $endl = "\n" ) { @@ -131,10 +134,10 @@ class UserMailer { global $wgSMTP, $wgServer; $msgid = uniqid( wfWikiID() . ".", true ); /* true required for cygwin */ - if ( is_array($wgSMTP) && isset($wgSMTP['IDHost']) && $wgSMTP['IDHost'] ) { + if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) { $domain = $wgSMTP['IDHost']; } else { - $url = wfParseUrl($wgServer); + $url = wfParseUrl( $wgServer ); $domain = $url['host']; } return "<$msgid@$domain>"; @@ -148,19 +151,48 @@ class UserMailer { * * @param $to MailAddress: recipient's email (or an array of them) * @param $from MailAddress: sender's email - * @param $subject String: email's subject. - * @param $body String: email's text. + * @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 $replyto MailAddress: optional reply-to email (default: null). - * @param $contentType String: optional custom Content-Type (default: text/plain; charset=UTF-8) + * @param string $contentType optional custom Content-Type (default: text/plain; charset=UTF-8) + * @throws MWException * @return Status object */ public static function send( $to, $from, $subject, $body, $replyto = null, $contentType = 'text/plain; charset=UTF-8' ) { - global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams; - + global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail; + $mime = null; if ( !is_array( $to ) ) { $to = array( $to ); } + // mail body must have some content + $minBodyLen = 10; + // arbitrary but longer than Array or Object to detect casting error + + // body must either be a string or an array with text and body + if ( + !( + !is_array( $body ) && + strlen( $body ) >= $minBodyLen + ) + && + !( + is_array( $body ) && + isset( $body['text'] ) && + isset( $body['html'] ) && + strlen( $body['text'] ) >= $minBodyLen && + strlen( $body['html'] ) >= $minBodyLen + ) + ) { + // if it is neither we have a problem + return Status::newFatal( 'user-mail-no-body' ); + } + + if ( !$wgAllowHTMLEmail && is_array( $body ) ) { + // HTML not wanted. Dump it. + $body = $body['text']; + } + wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" ); # Make sure we have at least one address @@ -194,7 +226,7 @@ class UserMailer { # NOTE: To: is for presentation, the actual recipient is specified # by the mailer using the Rcpt-To: header. # - # Subject: + # Subject: # PHP mail() second argument to pass the subject, passing a Subject # as an additional header will result in a duplicate header. # @@ -210,32 +242,62 @@ class UserMailer { } $headers['Date'] = date( 'r' ); - $headers['MIME-Version'] = '1.0'; - $headers['Content-type'] = ( is_null( $contentType ) ? - 'text/plain; charset=UTF-8' : $contentType ); - $headers['Content-transfer-encoding'] = '8bit'; - $headers['Message-ID'] = self::makeMsgId(); $headers['X-Mailer'] = 'MediaWiki mailer'; + # Line endings need to be different on Unix and Windows due to + # the bug described at http://trac.wordpress.org/ticket/2603 + if ( wfIsWindows() ) { + $endl = "\r\n"; + } else { + $endl = "\n"; + } + + if ( is_array( $body ) ) { + // we are sending a multipart message + wfDebug( "Assembling multipart mime email\n" ); + if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) { + wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" ); + } + else { + require_once( 'Mail/mime.php' ); + if ( wfIsWindows() ) { + $body['text'] = str_replace( "\n", "\r\n", $body['text'] ); + $body['html'] = str_replace( "\n", "\r\n", $body['html'] ); + } + $mime = new Mail_mime( array( 'eol' => $endl ) ); + $mime->setTXTBody( $body['text'] ); + $mime->setHTMLBody( $body['html'] ); + $body = $mime->get(); // must call get() before headers() + $headers = $mime->headers( $headers ); + } + } + if ( !isset( $mime ) ) { + // sending text only, either deliberately or as a fallback + if ( wfIsWindows() ) { + $body = str_replace( "\n", "\r\n", $body ); + } + $headers['MIME-Version'] = '1.0'; + $headers['Content-type'] = ( is_null( $contentType ) ? + 'text/plain; charset=UTF-8' : $contentType ); + $headers['Content-transfer-encoding'] = '8bit'; + } + $ret = wfRunHooks( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) ); if ( $ret === false ) { + // the hook implementation will return false to skip regular mail sending return Status::newGood(); } elseif ( $ret !== true ) { + // the hook implementation will return a string to pass an error message return Status::newFatal( 'php-mail-error', $ret ); } if ( is_array( $wgSMTP ) ) { # # PEAR MAILER - # + # - if ( function_exists( 'stream_resolve_include_path' ) ) { - $found = stream_resolve_include_path( 'Mail.php' ); - } else { - $found = Fallback::stream_resolve_include_path( 'Mail.php' ); - } - if ( !$found ) { + if ( !stream_resolve_include_path( 'Mail.php' ) ) { throw new MWException( 'PEAR mail package is not installed' ); } require_once( 'Mail.php' ); @@ -260,7 +322,7 @@ class UserMailer { } # Split jobs since SMTP servers tends to limit the maximum - # number of possible recipients. + # number of possible recipients. $chunks = array_chunk( $to, $wgEnotifMaxRecips ); foreach ( $chunks as $chunk ) { $status = self::sendWithPear( $mail_object, $chunk, $headers, $body ); @@ -272,21 +334,11 @@ class UserMailer { } wfRestoreWarnings(); return Status::newGood(); - } else { - # + } else { + # # PHP mail() # - - # Line endings need to be different on Unix and Windows due to - # the bug described at http://trac.wordpress.org/ticket/2603 - if ( wfIsWindows() ) { - $body = str_replace( "\n", "\r\n", $body ); - $endl = "\r\n"; - } else { - $endl = "\n"; - } - - if( count($to) > 1 ) { + if( count( $to ) > 1 ) { $headers['To'] = 'undisclosed-recipients:;'; } $headers = self::arrayToHeaderString( $headers, $endl ); @@ -299,6 +351,7 @@ class UserMailer { set_error_handler( 'UserMailer::errorHandler' ); $safeMode = wfIniGetBool( 'safe_mode' ); + foreach ( $to as $recip ) { if ( $safeMode ) { $sent = mail( $recip, self::quotedPrintable( $subject ), $body, $headers ); @@ -327,7 +380,7 @@ class UserMailer { * Set the mail error message in self::$mErrorString * * @param $code Integer: error number - * @param $string String: error message + * @param string $string error message */ static function errorHandler( $code, $string ) { self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); @@ -346,6 +399,12 @@ class UserMailer { /** * Converts a string into quoted-printable format * @since 1.17 + * + * From PHP5.3 there is a built in function quoted_printable_encode() + * This method does not duplicate that. + * This method is doing Q encoding inside encoded-words as defined by RFC 2047 + * This is for email headers. + * The built in quoted_printable_encode() is for email bodies * @return string */ public static function quotedPrintable( $string, $charset = '' ) { @@ -395,7 +454,7 @@ class UserMailer { */ class EmailNotification { protected $subject, $body, $replyto, $from; - protected $timestamp, $summary, $minorEdit, $oldid, $composed_common; + protected $timestamp, $summary, $minorEdit, $oldid, $composed_common, $pageStatus; protected $mailTargets = array(); /** @@ -420,8 +479,9 @@ class EmailNotification { * @param $summary * @param $minorEdit * @param $oldid (default: false) + * @param $pageStatus (default: 'changed') */ - public function notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid = false ) { + public function notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid = false, $pageStatus = 'changed' ) { global $wgEnotifUseJobQ, $wgEnotifWatchlist, $wgShowUpdatedMarker, $wgEnotifMinorEdits, $wgUsersNotifiedOnAllChanges, $wgEnotifUserTalk; @@ -429,7 +489,7 @@ class EmailNotification { return; } - // Build a list of users to notfiy + // Build a list of users to notify $watchers = array(); if ( $wgEnotifWatchlist || $wgShowUpdatedMarker ) { $dbw = wfGetDB( DB_MASTER ); @@ -446,19 +506,23 @@ class EmailNotification { $watchers[] = intval( $row->wl_user ); } if ( $watchers ) { - // Update wl_notificationtimestamp for all watching users except - // the editor - $dbw->begin( __METHOD__ ); - $dbw->update( 'watchlist', - array( /* SET */ - 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) - ), array( /* WHERE */ - 'wl_user' => $watchers, - 'wl_namespace' => $title->getNamespace(), - 'wl_title' => $title->getDBkey(), - ), __METHOD__ + // Update wl_notificationtimestamp for all watching users except the editor + $fname = __METHOD__; + $dbw->onTransactionIdle( + function() use ( $dbw, $timestamp, $watchers, $title, $fname ) { + $dbw->begin( $fname ); + $dbw->update( 'watchlist', + array( /* SET */ + 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) + ), array( /* WHERE */ + 'wl_user' => $watchers, + 'wl_namespace' => $title->getNamespace(), + 'wl_title' => $title->getDBkey(), + ), $fname + ); + $dbw->commit( $fname ); + } ); - $dbw->commit( __METHOD__ ); } } @@ -488,12 +552,13 @@ class EmailNotification { 'summary' => $summary, 'minorEdit' => $minorEdit, 'oldid' => $oldid, - 'watchers' => $watchers + 'watchers' => $watchers, + 'pageStatus' => $pageStatus ); $job = new EnotifNotifyJob( $title, $params ); - $job->insert(); + JobQueueGroup::singleton()->push( $job ); } else { - $this->actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers ); + $this->actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus ); } } @@ -505,13 +570,16 @@ class EmailNotification { * * @param $editor User object * @param $title Title object - * @param $timestamp string Edit timestamp - * @param $summary string Edit summary + * @param string $timestamp Edit timestamp + * @param string $summary Edit summary * @param $minorEdit bool - * @param $oldid int Revision ID - * @param $watchers array of user IDs + * @param int $oldid Revision ID + * @param array $watchers of user IDs + * @param string $pageStatus + * @throws MWException */ - public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers ) { + public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, + $oldid, $watchers, $pageStatus = 'changed' ) { # we use $wgPasswordSender as sender's address global $wgEnotifWatchlist; global $wgEnotifMinorEdits, $wgEnotifUserTalk; @@ -531,6 +599,14 @@ class EmailNotification { $this->oldid = $oldid; $this->editor = $editor; $this->composed_common = false; + $this->pageStatus = $pageStatus; + + $formattedPageStatus = array( 'deleted', 'created', 'moved', 'restored', 'changed' ); + + wfRunHooks( 'UpdateUserMailerFormattedPageStatus', array( &$formattedPageStatus ) ); + if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) { + throw new MWException( 'Not a valid page status!' ); + } $userTalkId = false; @@ -620,25 +696,30 @@ class EmailNotification { $keys = array(); $postTransformKeys = array(); + $pageTitleUrl = $this->title->getCanonicalUrl(); + $pageTitle = $this->title->getPrefixedText(); if ( $this->oldid ) { // Always show a link to the diff which triggered the mail. See bug 32210. - $keys['$NEWPAGE'] = wfMessage( 'enotif_lastdiff', + $keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff', $this->title->getCanonicalUrl( 'diff=next&oldid=' . $this->oldid ) ) ->inContentLanguage()->text(); + if ( !$wgEnotifImpersonal ) { // For personal mail, also show a link to the diff of all changes // since last visited. - $keys['$NEWPAGE'] .= " \n" . wfMessage( 'enotif_lastvisited', + $keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited', $this->title->getCanonicalUrl( 'diff=0&oldid=' . $this->oldid ) ) ->inContentLanguage()->text(); } - $keys['$OLDID'] = $this->oldid; + $keys['$OLDID'] = $this->oldid; + // @deprecated Remove in MediaWiki 1.23. $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text(); } else { - $keys['$NEWPAGE'] = wfMessage( 'enotif_newpagetext' )->inContentLanguage()->text(); # clear $OLDID placeholder in the message template - $keys['$OLDID'] = ''; + $keys['$OLDID'] = ''; + $keys['$NEWPAGE'] = ''; + // @deprecated Remove in MediaWiki 1.23. $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text(); } @@ -653,6 +734,7 @@ class EmailNotification { $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() ) ->inContentLanguage()->text(); $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text(); + } else { $keys['$PAGEEDITOR'] = $wgEnotifUseRealName ? $this->editor->getRealName() : $this->editor->getName(); $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() ); @@ -665,11 +747,12 @@ class EmailNotification { $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary; # Now build message's subject and body + $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage() + ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text(); - $subject = wfMessage( 'enotif_subject' )->inContentLanguage()->plain(); - $subject = strtr( $subject, $keys ); - $subject = MessageCache::singleton()->transform( $subject, false, null, $this->title ); - $this->subject = strtr( $subject, $postTransformKeys ); + $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus ) + ->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl ) + ->text(); $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain(); $body = strtr( $body, $keys ); @@ -686,13 +769,13 @@ class EmailNotification { { $editorAddress = new MailAddress( $this->editor ); if ( $wgEnotifFromEditor ) { - $this->from = $editorAddress; + $this->from = $editorAddress; } else { - $this->from = $adminAddress; + $this->from = $adminAddress; $this->replyto = $editorAddress; } } else { - $this->from = $adminAddress; + $this->from = $adminAddress; $this->replyto = new MailAddress( $wgNoReplyAddress ); } } @@ -761,13 +844,15 @@ class EmailNotification { /** * Same as sendPersonalised but does impersonal mail suitable for bulk * mailing. Takes an array of MailAddress objects. - * @return Status + * @param $addresses array + * @return Status|null */ function sendImpersonal( $addresses ) { global $wgContLang; - if ( empty( $addresses ) ) - return; + if ( empty( $addresses ) ) { + return null; + } $body = str_replace( array( '$WATCHINGUSERNAME', |