MediaWiki
REL1_24
|
00001 <?php 00030 class UserMailer { 00031 private static $mErrorString; 00032 00043 protected static function sendWithPear( $mailer, $dest, $headers, $body ) { 00044 $mailResult = $mailer->send( $dest, $headers, $body ); 00045 00046 # Based on the result return an error string, 00047 if ( PEAR::isError( $mailResult ) ) { 00048 wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" ); 00049 return Status::newFatal( 'pear-mail-error', $mailResult->getMessage() ); 00050 } else { 00051 return Status::newGood(); 00052 } 00053 } 00054 00067 static function arrayToHeaderString( $headers, $endl = "\n" ) { 00068 $strings = array(); 00069 foreach ( $headers as $name => $value ) { 00070 // Prevent header injection by stripping newlines from value 00071 $value = self::sanitizeHeaderValue( $value ); 00072 $strings[] = "$name: $value"; 00073 } 00074 return implode( $endl, $strings ); 00075 } 00076 00082 static function makeMsgId() { 00083 global $wgSMTP, $wgServer; 00084 00085 $msgid = uniqid( wfWikiID() . ".", true ); /* true required for cygwin */ 00086 if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) { 00087 $domain = $wgSMTP['IDHost']; 00088 } else { 00089 $url = wfParseUrl( $wgServer ); 00090 $domain = $url['host']; 00091 } 00092 return "<$msgid@$domain>"; 00093 } 00094 00111 public static function send( $to, $from, $subject, $body, $replyto = null, 00112 $contentType = 'text/plain; charset=UTF-8' 00113 ) { 00114 global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail; 00115 $mime = null; 00116 if ( !is_array( $to ) ) { 00117 $to = array( $to ); 00118 } 00119 00120 // mail body must have some content 00121 $minBodyLen = 10; 00122 // arbitrary but longer than Array or Object to detect casting error 00123 00124 // body must either be a string or an array with text and body 00125 if ( 00126 !( 00127 !is_array( $body ) && 00128 strlen( $body ) >= $minBodyLen 00129 ) 00130 && 00131 !( 00132 is_array( $body ) && 00133 isset( $body['text'] ) && 00134 isset( $body['html'] ) && 00135 strlen( $body['text'] ) >= $minBodyLen && 00136 strlen( $body['html'] ) >= $minBodyLen 00137 ) 00138 ) { 00139 // if it is neither we have a problem 00140 return Status::newFatal( 'user-mail-no-body' ); 00141 } 00142 00143 if ( !$wgAllowHTMLEmail && is_array( $body ) ) { 00144 // HTML not wanted. Dump it. 00145 $body = $body['text']; 00146 } 00147 00148 wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" ); 00149 00150 # Make sure we have at least one address 00151 $has_address = false; 00152 foreach ( $to as $u ) { 00153 if ( $u->address ) { 00154 $has_address = true; 00155 break; 00156 } 00157 } 00158 if ( !$has_address ) { 00159 return Status::newFatal( 'user-mail-no-addy' ); 00160 } 00161 00162 # Forge email headers 00163 # ------------------- 00164 # 00165 # WARNING 00166 # 00167 # DO NOT add To: or Subject: headers at this step. They need to be 00168 # handled differently depending upon the mailer we are going to use. 00169 # 00170 # To: 00171 # PHP mail() first argument is the mail receiver. The argument is 00172 # used as a recipient destination and as a To header. 00173 # 00174 # PEAR mailer has a recipient argument which is only used to 00175 # send the mail. If no To header is given, PEAR will set it to 00176 # to 'undisclosed-recipients:'. 00177 # 00178 # NOTE: To: is for presentation, the actual recipient is specified 00179 # by the mailer using the Rcpt-To: header. 00180 # 00181 # Subject: 00182 # PHP mail() second argument to pass the subject, passing a Subject 00183 # as an additional header will result in a duplicate header. 00184 # 00185 # PEAR mailer should be passed a Subject header. 00186 # 00187 # -- hashar 20120218 00188 00189 $headers['From'] = $from->toString(); 00190 $returnPath = $from->address; 00191 $extraParams = $wgAdditionalMailParams; 00192 00193 // Hook to generate custom VERP address for 'Return-Path' 00194 wfRunHooks( 'UserMailerChangeReturnPath', array( $to, &$returnPath ) ); 00195 # Add the envelope sender address using the -f command line option when PHP mail() is used. 00196 # Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the 00197 # generated VERP address when the hook runs effectively. 00198 $extraParams .= ' -f ' . $returnPath; 00199 00200 $headers['Return-Path'] = $returnPath; 00201 00202 if ( $replyto ) { 00203 $headers['Reply-To'] = $replyto->toString(); 00204 } 00205 00206 $headers['Date'] = MWTimestamp::getLocalInstance()->format( 'r' ); 00207 $headers['Message-ID'] = self::makeMsgId(); 00208 $headers['X-Mailer'] = 'MediaWiki mailer'; 00209 00210 # Line endings need to be different on Unix and Windows due to 00211 # the bug described at http://trac.wordpress.org/ticket/2603 00212 if ( wfIsWindows() ) { 00213 $endl = "\r\n"; 00214 } else { 00215 $endl = "\n"; 00216 } 00217 00218 if ( is_array( $body ) ) { 00219 // we are sending a multipart message 00220 wfDebug( "Assembling multipart mime email\n" ); 00221 if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) { 00222 wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" ); 00223 // remove the html body for text email fall back 00224 $body = $body['text']; 00225 } else { 00226 require_once 'Mail/mime.php'; 00227 if ( wfIsWindows() ) { 00228 $body['text'] = str_replace( "\n", "\r\n", $body['text'] ); 00229 $body['html'] = str_replace( "\n", "\r\n", $body['html'] ); 00230 } 00231 $mime = new Mail_mime( array( 00232 'eol' => $endl, 00233 'text_charset' => 'UTF-8', 00234 'html_charset' => 'UTF-8' 00235 ) ); 00236 $mime->setTXTBody( $body['text'] ); 00237 $mime->setHTMLBody( $body['html'] ); 00238 $body = $mime->get(); // must call get() before headers() 00239 $headers = $mime->headers( $headers ); 00240 } 00241 } 00242 if ( $mime === null ) { 00243 // sending text only, either deliberately or as a fallback 00244 if ( wfIsWindows() ) { 00245 $body = str_replace( "\n", "\r\n", $body ); 00246 } 00247 $headers['MIME-Version'] = '1.0'; 00248 $headers['Content-type'] = ( is_null( $contentType ) ? 00249 'text/plain; charset=UTF-8' : $contentType ); 00250 $headers['Content-transfer-encoding'] = '8bit'; 00251 } 00252 00253 $ret = wfRunHooks( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) ); 00254 if ( $ret === false ) { 00255 // the hook implementation will return false to skip regular mail sending 00256 return Status::newGood(); 00257 } elseif ( $ret !== true ) { 00258 // the hook implementation will return a string to pass an error message 00259 return Status::newFatal( 'php-mail-error', $ret ); 00260 } 00261 00262 if ( is_array( $wgSMTP ) ) { 00263 # 00264 # PEAR MAILER 00265 # 00266 00267 if ( !stream_resolve_include_path( 'Mail.php' ) ) { 00268 throw new MWException( 'PEAR mail package is not installed' ); 00269 } 00270 require_once 'Mail.php'; 00271 00272 wfSuppressWarnings(); 00273 00274 // Create the mail object using the Mail::factory method 00275 $mail_object =& Mail::factory( 'smtp', $wgSMTP ); 00276 if ( PEAR::isError( $mail_object ) ) { 00277 wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" ); 00278 wfRestoreWarnings(); 00279 return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() ); 00280 } 00281 00282 wfDebug( "Sending mail via PEAR::Mail\n" ); 00283 00284 $headers['Subject'] = self::quotedPrintable( $subject ); 00285 00286 # When sending only to one recipient, shows it its email using To: 00287 if ( count( $to ) == 1 ) { 00288 $headers['To'] = $to[0]->toString(); 00289 } 00290 00291 # Split jobs since SMTP servers tends to limit the maximum 00292 # number of possible recipients. 00293 $chunks = array_chunk( $to, $wgEnotifMaxRecips ); 00294 foreach ( $chunks as $chunk ) { 00295 $status = self::sendWithPear( $mail_object, $chunk, $headers, $body ); 00296 # FIXME : some chunks might be sent while others are not! 00297 if ( !$status->isOK() ) { 00298 wfRestoreWarnings(); 00299 return $status; 00300 } 00301 } 00302 wfRestoreWarnings(); 00303 return Status::newGood(); 00304 } else { 00305 # 00306 # PHP mail() 00307 # 00308 if ( count( $to ) > 1 ) { 00309 $headers['To'] = 'undisclosed-recipients:;'; 00310 } 00311 $headers = self::arrayToHeaderString( $headers, $endl ); 00312 00313 wfDebug( "Sending mail via internal mail() function\n" ); 00314 00315 self::$mErrorString = ''; 00316 $html_errors = ini_get( 'html_errors' ); 00317 ini_set( 'html_errors', '0' ); 00318 set_error_handler( 'UserMailer::errorHandler' ); 00319 00320 try { 00321 $safeMode = wfIniGetBool( 'safe_mode' ); 00322 00323 foreach ( $to as $recip ) { 00324 if ( $safeMode ) { 00325 $sent = mail( $recip, self::quotedPrintable( $subject ), $body, $headers ); 00326 } else { 00327 $sent = mail( 00328 $recip, 00329 self::quotedPrintable( $subject ), 00330 $body, 00331 $headers, 00332 $extraParams 00333 ); 00334 } 00335 } 00336 } catch ( Exception $e ) { 00337 restore_error_handler(); 00338 throw $e; 00339 } 00340 00341 restore_error_handler(); 00342 ini_set( 'html_errors', $html_errors ); 00343 00344 if ( self::$mErrorString ) { 00345 wfDebug( "Error sending mail: " . self::$mErrorString . "\n" ); 00346 return Status::newFatal( 'php-mail-error', self::$mErrorString ); 00347 } elseif ( !$sent ) { 00348 // mail function only tells if there's an error 00349 wfDebug( "Unknown error sending mail\n" ); 00350 return Status::newFatal( 'php-mail-error-unknown' ); 00351 } else { 00352 return Status::newGood(); 00353 } 00354 } 00355 } 00356 00363 static function errorHandler( $code, $string ) { 00364 self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); 00365 } 00366 00372 public static function sanitizeHeaderValue( $val ) { 00373 return strtr( $val, array( "\r" => '', "\n" => '' ) ); 00374 } 00375 00381 public static function rfc822Phrase( $phrase ) { 00382 // Remove line breaks 00383 $phrase = self::sanitizeHeaderValue( $phrase ); 00384 // Remove quotes 00385 $phrase = str_replace( '"', '', $phrase ); 00386 return '"' . $phrase . '"'; 00387 } 00388 00402 public static function quotedPrintable( $string, $charset = '' ) { 00403 # Probably incomplete; see RFC 2045 00404 if ( empty( $charset ) ) { 00405 $charset = 'UTF-8'; 00406 } 00407 $charset = strtoupper( $charset ); 00408 $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ? 00409 00410 $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff='; 00411 $replace = $illegal . '\t ?_'; 00412 if ( !preg_match( "/[$illegal]/", $string ) ) { 00413 return $string; 00414 } 00415 $out = "=?$charset?Q?"; 00416 $out .= preg_replace_callback( "/([$replace])/", 00417 array( __CLASS__, 'quotedPrintableCallback' ), $string ); 00418 $out .= '?='; 00419 return $out; 00420 } 00421 00422 protected static function quotedPrintableCallback( $matches ) { 00423 return sprintf( "=%02X", ord( $matches[1] ) ); 00424 } 00425 }