[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |