[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/mail/ -> UserMailer.php (source)

   1  <?php
   2  /**
   3   * Classes used to send e-mails
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @author <[email protected]>
  22   * @author <[email protected]>
  23   * @author Tim Starling
  24   * @author Luke Welling [email protected]
  25   */
  26  
  27  /**
  28   * Collection of static functions for sending mail
  29   */
  30  class UserMailer {
  31      private static $mErrorString;
  32  
  33      /**
  34       * Send mail using a PEAR mailer
  35       *
  36       * @param UserMailer $mailer
  37       * @param string $dest
  38       * @param string $headers
  39       * @param string $body
  40       *
  41       * @return Status
  42       */
  43  	protected static function sendWithPear( $mailer, $dest, $headers, $body ) {
  44          $mailResult = $mailer->send( $dest, $headers, $body );
  45  
  46          # Based on the result return an error string,
  47          if ( PEAR::isError( $mailResult ) ) {
  48              wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" );
  49              return Status::newFatal( 'pear-mail-error', $mailResult->getMessage() );
  50          } else {
  51              return Status::newGood();
  52          }
  53      }
  54  
  55      /**
  56       * Creates a single string from an associative array
  57       *
  58       * @param array $headers Associative Array: keys are header field names,
  59       *                 values are ... values.
  60       * @param string $endl The end of line character.  Defaults to "\n"
  61       *
  62       * Note RFC2822 says newlines must be CRLF (\r\n)
  63       * but php mail naively "corrects" it and requires \n for the "correction" to work
  64       *
  65       * @return string
  66       */
  67  	static function arrayToHeaderString( $headers, $endl = "\n" ) {
  68          $strings = array();
  69          foreach ( $headers as $name => $value ) {
  70              // Prevent header injection by stripping newlines from value
  71              $value = self::sanitizeHeaderValue( $value );
  72              $strings[] = "$name: $value";
  73          }
  74          return implode( $endl, $strings );
  75      }
  76  
  77      /**
  78       * Create a value suitable for the MessageId Header
  79       *
  80       * @return string
  81       */
  82  	static function makeMsgId() {
  83          global $wgSMTP, $wgServer;
  84  
  85          $msgid = uniqid( wfWikiID() . ".", true ); /* true required for cygwin */
  86          if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) {
  87              $domain = $wgSMTP['IDHost'];
  88          } else {
  89              $url = wfParseUrl( $wgServer );
  90              $domain = $url['host'];
  91          }
  92          return "<$msgid@$domain>";
  93      }
  94  
  95      /**
  96       * This function will perform a direct (authenticated) login to
  97       * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an
  98       * array of parameters. It requires PEAR:Mail to do that.
  99       * Otherwise it just uses the standard PHP 'mail' function.
 100       *
 101       * @param MailAddress|MailAddress[] $to Recipient's email (or an array of them)
 102       * @param MailAddress $from Sender's email
 103       * @param string $subject Email's subject.
 104       * @param string $body Email's text or Array of two strings to be the text and html bodies
 105       * @param MailAddress $replyto Optional reply-to email (default: null).
 106       * @param string $contentType Optional custom Content-Type (default: text/plain; charset=UTF-8)
 107       * @throws MWException
 108       * @throws Exception
 109       * @return Status
 110       */
 111  	public static function send( $to, $from, $subject, $body, $replyto = null,
 112          $contentType = 'text/plain; charset=UTF-8'
 113      ) {
 114          global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail;
 115          $mime = null;
 116          if ( !is_array( $to ) ) {
 117              $to = array( $to );
 118          }
 119  
 120          // mail body must have some content
 121          $minBodyLen = 10;
 122          // arbitrary but longer than Array or Object to detect casting error
 123  
 124          // body must either be a string or an array with text and body
 125          if (
 126              !(
 127                  !is_array( $body ) &&
 128                  strlen( $body ) >= $minBodyLen
 129              )
 130              &&
 131              !(
 132                  is_array( $body ) &&
 133                  isset( $body['text'] ) &&
 134                  isset( $body['html'] ) &&
 135                  strlen( $body['text'] ) >= $minBodyLen &&
 136                  strlen( $body['html'] ) >= $minBodyLen
 137              )
 138          ) {
 139              // if it is neither we have a problem
 140              return Status::newFatal( 'user-mail-no-body' );
 141          }
 142  
 143          if ( !$wgAllowHTMLEmail && is_array( $body ) ) {
 144              // HTML not wanted.  Dump it.
 145              $body = $body['text'];
 146          }
 147  
 148          wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" );
 149  
 150          # Make sure we have at least one address
 151          $has_address = false;
 152          foreach ( $to as $u ) {
 153              if ( $u->address ) {
 154                  $has_address = true;
 155                  break;
 156              }
 157          }
 158          if ( !$has_address ) {
 159              return Status::newFatal( 'user-mail-no-addy' );
 160          }
 161  
 162          # Forge email headers
 163          # -------------------
 164          #
 165          # WARNING
 166          #
 167          # DO NOT add To: or Subject: headers at this step. They need to be
 168          # handled differently depending upon the mailer we are going to use.
 169          #
 170          # To:
 171          #  PHP mail() first argument is the mail receiver. The argument is
 172          #  used as a recipient destination and as a To header.
 173          #
 174          #  PEAR mailer has a recipient argument which is only used to
 175          #  send the mail. If no To header is given, PEAR will set it to
 176          #  to 'undisclosed-recipients:'.
 177          #
 178          #  NOTE: To: is for presentation, the actual recipient is specified
 179          #  by the mailer using the Rcpt-To: header.
 180          #
 181          # Subject:
 182          #  PHP mail() second argument to pass the subject, passing a Subject
 183          #  as an additional header will result in a duplicate header.
 184          #
 185          #  PEAR mailer should be passed a Subject header.
 186          #
 187          # -- hashar 20120218
 188  
 189          $headers['From'] = $from->toString();
 190          $returnPath = $from->address;
 191          $extraParams = $wgAdditionalMailParams;
 192  
 193          // Hook to generate custom VERP address for 'Return-Path'
 194          wfRunHooks( 'UserMailerChangeReturnPath', array( $to, &$returnPath ) );
 195          # Add the envelope sender address using the -f command line option when PHP mail() is used.
 196          # Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the
 197          # generated VERP address when the hook runs effectively.
 198          $extraParams .= ' -f ' . $returnPath;
 199  
 200          $headers['Return-Path'] = $returnPath;
 201  
 202          if ( $replyto ) {
 203              $headers['Reply-To'] = $replyto->toString();
 204          }
 205  
 206          $headers['Date'] = MWTimestamp::getLocalInstance()->format( 'r' );
 207          $headers['Message-ID'] = self::makeMsgId();
 208          $headers['X-Mailer'] = 'MediaWiki mailer';
 209  
 210          # Line endings need to be different on Unix and Windows due to
 211          # the bug described at http://trac.wordpress.org/ticket/2603
 212          if ( wfIsWindows() ) {
 213              $endl = "\r\n";
 214          } else {
 215              $endl = "\n";
 216          }
 217  
 218          if ( is_array( $body ) ) {
 219              // we are sending a multipart message
 220              wfDebug( "Assembling multipart mime email\n" );
 221              if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) {
 222                  wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" );
 223                  // remove the html body for text email fall back
 224                  $body = $body['text'];
 225              } else {
 226                  require_once 'Mail/mime.php';
 227                  if ( wfIsWindows() ) {
 228                      $body['text'] = str_replace( "\n", "\r\n", $body['text'] );
 229                      $body['html'] = str_replace( "\n", "\r\n", $body['html'] );
 230                  }
 231                  $mime = new Mail_mime( array(
 232                      'eol' => $endl,
 233                      'text_charset' => 'UTF-8',
 234                      'html_charset' => 'UTF-8'
 235                  ) );
 236                  $mime->setTXTBody( $body['text'] );
 237                  $mime->setHTMLBody( $body['html'] );
 238                  $body = $mime->get(); // must call get() before headers()
 239                  $headers = $mime->headers( $headers );
 240              }
 241          }
 242          if ( $mime === null ) {
 243              // sending text only, either deliberately or as a fallback
 244              if ( wfIsWindows() ) {
 245                  $body = str_replace( "\n", "\r\n", $body );
 246              }
 247              $headers['MIME-Version'] = '1.0';
 248              $headers['Content-type'] = ( is_null( $contentType ) ?
 249                  'text/plain; charset=UTF-8' : $contentType );
 250              $headers['Content-transfer-encoding'] = '8bit';
 251          }
 252  
 253          $ret = wfRunHooks( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) );
 254          if ( $ret === false ) {
 255              // the hook implementation will return false to skip regular mail sending
 256              return Status::newGood();
 257          } elseif ( $ret !== true ) {
 258              // the hook implementation will return a string to pass an error message
 259              return Status::newFatal( 'php-mail-error', $ret );
 260          }
 261  
 262          if ( is_array( $wgSMTP ) ) {
 263              #
 264              # PEAR MAILER
 265              #
 266  
 267              if ( !stream_resolve_include_path( 'Mail.php' ) ) {
 268                  throw new MWException( 'PEAR mail package is not installed' );
 269              }
 270              require_once 'Mail.php';
 271  
 272              wfSuppressWarnings();
 273  
 274              // Create the mail object using the Mail::factory method
 275              $mail_object =& Mail::factory( 'smtp', $wgSMTP );
 276              if ( PEAR::isError( $mail_object ) ) {
 277                  wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" );
 278                  wfRestoreWarnings();
 279                  return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() );
 280              }
 281  
 282              wfDebug( "Sending mail via PEAR::Mail\n" );
 283  
 284              $headers['Subject'] = self::quotedPrintable( $subject );
 285  
 286              # When sending only to one recipient, shows it its email using To:
 287              if ( count( $to ) == 1 ) {
 288                  $headers['To'] = $to[0]->toString();
 289              }
 290  
 291              # Split jobs since SMTP servers tends to limit the maximum
 292              # number of possible recipients.
 293              $chunks = array_chunk( $to, $wgEnotifMaxRecips );
 294              foreach ( $chunks as $chunk ) {
 295                  $status = self::sendWithPear( $mail_object, $chunk, $headers, $body );
 296                  # FIXME : some chunks might be sent while others are not!
 297                  if ( !$status->isOK() ) {
 298                      wfRestoreWarnings();
 299                      return $status;
 300                  }
 301              }
 302              wfRestoreWarnings();
 303              return Status::newGood();
 304          } else {
 305              #
 306              # PHP mail()
 307              #
 308              if ( count( $to ) > 1 ) {
 309                  $headers['To'] = 'undisclosed-recipients:;';
 310              }
 311              $headers = self::arrayToHeaderString( $headers, $endl );
 312  
 313              wfDebug( "Sending mail via internal mail() function\n" );
 314  
 315              self::$mErrorString = '';
 316              $html_errors = ini_get( 'html_errors' );
 317              ini_set( 'html_errors', '0' );
 318              set_error_handler( 'UserMailer::errorHandler' );
 319  
 320              try {
 321                  $safeMode = wfIniGetBool( 'safe_mode' );
 322  
 323                  foreach ( $to as $recip ) {
 324                      if ( $safeMode ) {
 325                          $sent = mail( $recip, self::quotedPrintable( $subject ), $body, $headers );
 326                      } else {
 327                          $sent = mail(
 328                              $recip,
 329                              self::quotedPrintable( $subject ),
 330                              $body,
 331                              $headers,
 332                              $extraParams
 333                          );
 334                      }
 335                  }
 336              } catch ( Exception $e ) {
 337                  restore_error_handler();
 338                  throw $e;
 339              }
 340  
 341              restore_error_handler();
 342              ini_set( 'html_errors', $html_errors );
 343  
 344              if ( self::$mErrorString ) {
 345                  wfDebug( "Error sending mail: " . self::$mErrorString . "\n" );
 346                  return Status::newFatal( 'php-mail-error', self::$mErrorString );
 347              } elseif ( !$sent ) {
 348                  // mail function only tells if there's an error
 349                  wfDebug( "Unknown error sending mail\n" );
 350                  return Status::newFatal( 'php-mail-error-unknown' );
 351              } else {
 352                  return Status::newGood();
 353              }
 354          }
 355      }
 356  
 357      /**
 358       * Set the mail error message in self::$mErrorString
 359       *
 360       * @param int $code Error number
 361       * @param string $string Error message
 362       */
 363  	static function errorHandler( $code, $string ) {
 364          self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string );
 365      }
 366  
 367      /**
 368       * Strips bad characters from a header value to prevent PHP mail header injection attacks
 369       * @param string $val String to be santizied
 370       * @return string
 371       */
 372  	public static function sanitizeHeaderValue( $val ) {
 373          return strtr( $val, array( "\r" => '', "\n" => '' ) );
 374      }
 375  
 376      /**
 377       * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name
 378       * @param string $phrase
 379       * @return string
 380       */
 381  	public static function rfc822Phrase( $phrase ) {
 382          // Remove line breaks
 383          $phrase = self::sanitizeHeaderValue( $phrase );
 384          // Remove quotes
 385          $phrase = str_replace( '"', '', $phrase );
 386          return '"' . $phrase . '"';
 387      }
 388  
 389      /**
 390       * Converts a string into quoted-printable format
 391       * @since 1.17
 392       *
 393       * From PHP5.3 there is a built in function quoted_printable_encode()
 394       * This method does not duplicate that.
 395       * This method is doing Q encoding inside encoded-words as defined by RFC 2047
 396       * This is for email headers.
 397       * The built in quoted_printable_encode() is for email bodies
 398       * @param string $string
 399       * @param string $charset
 400       * @return string
 401       */
 402  	public static function quotedPrintable( $string, $charset = '' ) {
 403          # Probably incomplete; see RFC 2045
 404          if ( empty( $charset ) ) {
 405              $charset = 'UTF-8';
 406          }
 407          $charset = strtoupper( $charset );
 408          $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ?
 409  
 410          $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff=';
 411          $replace = $illegal . '\t ?_';
 412          if ( !preg_match( "/[$illegal]/", $string ) ) {
 413              return $string;
 414          }
 415          $out = "=?$charset?Q?";
 416          $out .= preg_replace_callback( "/([$replace])/",
 417              array( __CLASS__, 'quotedPrintableCallback' ), $string );
 418          $out .= '?=';
 419          return $out;
 420      }
 421  
 422  	protected static function quotedPrintableCallback( $matches ) {
 423          return sprintf( "=%02X", ord( $matches[1] ) );
 424      }
 425  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1