Add support for mulitpart mime email to email sending code
authorlwelling <lwelling@wikimedia.org>
Fri, 7 Dec 2012 21:41:44 +0000 (16:41 -0500)
committerlwelling <lwelling@wikimedia.org>
Fri, 28 Dec 2012 21:43:55 +0000 (16:43 -0500)
The change is backwards compatible so various places that send email
can be upgraded in their own time, or ignore it.
The first user will be the Echo extension but it is likely to be used elsewhere

This requires adding 'pear install Mime_Mail' to the server install
To see a change you need to add $wgAllowHTMLEmail to your LocalSettings.php
By default it is false in DefaultSettings.php as there is no core code that uses it yet

(patch 11 notes fixed)

Change-Id: I2096f50b789ffa0b8f4fa8c32fdaf43e9b0e4661

includes/DefaultSettings.php
includes/UserMailer.php
languages/messages/MessagesEn.php

index e12b3be..35f23c2 100644 (file)
@@ -1264,6 +1264,12 @@ $wgSMTP = false;
  */
 $wgAdditionalMailParams = null;
 
+/**
+ * For parts of the system that have been updated to provide HTML email content, send
+ * both text and HTML parts as the body of the email
+ */
+$wgAllowHTMLEmail = false;
+
 /**
  * True: from page editor if s/he opted-in. False: Enotif mails appear to come
  * from $wgEmergencyContact
index b8b6aa8..cb7afc0 100644 (file)
@@ -21,6 +21,7 @@
  * @author <brion@pobox.com>
  * @author <mail@tgries.de>
  * @author Tim Starling
+ * @author Luke Welling lwelling@wikimedia.org
  */
 
 
@@ -112,6 +113,10 @@ class UserMailer {
         * @param $headers array Associative Array: keys are header field names,
         *                 values are ... values.
         * @param $endl String: 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" ) {
@@ -149,19 +154,47 @@ 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 $body String: 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)
         * @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
@@ -211,18 +244,53 @@ 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 mulitpart 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 );
                }
 
@@ -231,7 +299,7 @@ class UserMailer {
                        # PEAR MAILER
                        #
 
-                       if ( ! stream_resolve_include_path( 'Mail.php' ) ) {
+                       if ( !stream_resolve_include_path( 'Mail.php' ) ) {
                                throw new MWException( 'PEAR mail package is not installed' );
                        }
                        require_once( 'Mail.php' );
@@ -272,16 +340,6 @@ class UserMailer {
                        #
                        # 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 ) {
                                $headers['To'] = 'undisclosed-recipients:;';
                        }
@@ -295,6 +353,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 );
@@ -342,6 +401,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 = '' ) {
index cd3ef1a..d62b49a 100644 (file)
@@ -1192,6 +1192,7 @@ Please wait before trying again.',
 'php-mail-error'         => '$1', # do not translate or duplicate this message to other languages
 'php-mail-error-unknown' => "Unknown error in PHP's mail() function.",
 'user-mail-no-addy'      => 'Tried to send e-mail without an e-mail address.',
+'user-mail-no-body'      => 'Tried to send e-mail with an empty or unreasonably short body.',
 
 # Change password dialog
 'resetpass'                 => 'Change password',