40 private static $mErrorString;
52 protected static function sendWithPear( $mailer, $dest, $headers, $body ) {
53 $mailResult = $mailer->send( $dest, $headers, $body );
56 if ( PEAR::isError( $mailResult ) ) {
57 wfDebug(
"PEAR::Mail failed: " . $mailResult->getMessage() );
58 return Status::newFatal(
'pear-mail-error', $mailResult->getMessage() );
60 return Status::newGood();
69 private static function makeMsgId() {
74 $domainId = WikiMap::getCurrentWikiDbDomain()->getId();
75 $msgid = uniqid( $domainId .
".",
true );
77 if ( is_array( $smtp ) && isset( $smtp[
'IDHost'] ) && $smtp[
'IDHost'] ) {
78 $domain = $smtp[
'IDHost'];
80 $domain = parse_url( $server, PHP_URL_HOST ) ??
'';
82 return "<$msgid@$domain>";
106 public static function send( $to, $from, $subject, $body, $options = [] ) {
108 $allowHTMLEmail = $services->getMainConfig()->get(
111 if ( !isset( $options[
'contentType'] ) ) {
112 $options[
'contentType'] =
'text/plain; charset=UTF-8';
115 if ( !is_array( $to ) ) {
126 !is_array( $body ) &&
127 strlen( $body ) >= $minBodyLen
132 isset( $body[
'text'] ) &&
133 isset( $body[
'html'] ) &&
134 strlen( $body[
'text'] ) >= $minBodyLen &&
135 strlen( $body[
'html'] ) >= $minBodyLen
139 return Status::newFatal(
'user-mail-no-body' );
142 if ( !$allowHTMLEmail && is_array( $body ) ) {
144 $body = $body[
'text'];
147 wfDebug( __METHOD__ .
': sending mail to ' . implode(
', ', $to ) );
150 $has_address =
false;
151 foreach ( $to as $u ) {
157 if ( !$has_address ) {
158 return Status::newFatal(
'user-mail-no-addy' );
163 if ( count( $to ) > 1 ) {
165 (
new HookRunner( $services->getHookContainer() ) )->onUserMailerSplitTo( $to );
166 if ( $oldTo != $to ) {
167 $splitTo = array_diff( $oldTo, $to );
168 $to = array_diff( $oldTo, $splitTo );
170 $status = Status::newGood();
172 $status->merge( self::sendInternal(
173 $to, $from, $subject, $body, $options ) );
175 foreach ( $splitTo as $newTo ) {
176 $status->merge( self::sendInternal(
177 [ $newTo ], $from, $subject, $body, $options ) );
207 $mainConfig = $services->getMainConfig();
211 $replyto = $options[
'replyTo'] ??
null;
212 $contentType = $options[
'contentType'] ??
'text/plain; charset=UTF-8';
213 $headers = $options[
'headers'] ?? [];
215 $hookRunner =
new HookRunner( $services->getHookContainer() );
219 if ( !$hookRunner->onUserMailerTransformContent( $to, $from, $body, $error ) ) {
221 return Status::newFatal(
'php-mail-error', $error );
223 return Status::newFatal(
'php-mail-error-unknown' );
256 $headers[
'From'] = $from->
toString();
257 $returnPath = $from->address;
258 $extraParams = $additionalMailParams;
261 $hookRunner->onUserMailerChangeReturnPath( $to, $returnPath );
271 $returnPathCLI =
'"' . str_replace(
'"',
'', $returnPath ) .
'"';
272 $extraParams .=
' -f ' . $returnPathCLI;
274 $headers[
'Return-Path'] = $returnPath;
277 $headers[
'Reply-To'] = $replyto->
toString();
280 $headers[
'Date'] = MWTimestamp::getLocalInstance()->format(
'r' );
281 $headers[
'Message-ID'] = self::makeMsgId();
282 $headers[
'X-Mailer'] =
'MediaWiki mailer';
290 if ( is_array( $body ) ) {
292 wfDebug(
"Assembling multipart mime email" );
294 $body[
'text'] = str_replace(
"\n",
"\r\n", $body[
'text'] );
295 $body[
'html'] = str_replace(
"\n",
"\r\n", $body[
'html'] );
297 $mime =
new Mail_mime( [
299 'text_charset' =>
'UTF-8',
300 'html_charset' =>
'UTF-8'
302 $mime->setTXTBody( $body[
'text'] );
303 $mime->setHTMLBody( $body[
'html'] );
304 $body = $mime->get();
305 $headers = $mime->headers( $headers );
309 $body = str_replace(
"\n",
"\r\n", $body );
311 $headers[
'MIME-Version'] =
'1.0';
312 $headers[
'Content-type'] = $contentType;
313 $headers[
'Content-transfer-encoding'] =
'8bit';
317 if ( !$hookRunner->onUserMailerTransformMessage(
318 $to, $from, $subject, $headers, $body, $error )
321 return Status::newFatal(
'php-mail-error', $error );
323 return Status::newFatal(
'php-mail-error-unknown' );
327 $ret = $hookRunner->onAlternateUserMailer( $headers, $to, $from, $subject, $body );
328 if ( $ret ===
false ) {
330 LoggerFactory::getInstance(
'usermailer' )->info(
331 "Email to {to} from {from} with subject {subject} handled by AlternateUserMailer",
333 'to' => $to[0]->toString(),
334 'allto' => implode(
', ', array_map(
'strval', $to ) ),
336 'subject' => $subject,
339 return Status::newGood();
340 } elseif ( $ret !==
true ) {
342 return Status::newFatal(
'php-mail-error', $ret );
345 if ( is_array( $smtp ) ) {
346 $receips = array_map(
'strval', $to );
348 if ( count( $receips ) !== 1 ) {
349 throw new RuntimeException(
350 __METHOD__ .
'somehow called for multiple recipients, no longer supported.'
353 $recipient = $receips[0];
357 if ( PEAR::isError( $mail_object ) ) {
358 wfDebug(
"PEAR::Mail factory failed: " . $mail_object->getMessage() );
359 return Status::newFatal(
'pear-mail-error', $mail_object->getMessage() );
361 '@phan-var Mail_smtp $mail_object';
363 wfDebug(
"Sending mail via PEAR::Mail" );
368 $headers[
'To'] = $recipient;
371 if ( !$status->isOK() ) {
374 return Status::newGood();
377 if ( count( $to ) > 1 ) {
378 $headers[
'To'] =
'undisclosed-recipients:;';
381 wfDebug(
"Sending mail via internal mail() function" );
383 self::$mErrorString =
'';
384 $html_errors = ini_get(
'html_errors' );
385 ini_set(
'html_errors',
'0' );
386 set_error_handler( self::errorHandler( ... ) );
389 foreach ( $to as $recip ) {
392 self::quotedPrintable( $subject ),
398 }
catch ( Exception $e ) {
399 restore_error_handler();
403 restore_error_handler();
404 ini_set(
'html_errors', $html_errors );
406 if ( self::$mErrorString ) {
407 wfDebug(
"Error sending mail: " . self::$mErrorString );
408 return Status::newFatal(
'php-mail-error', self::$mErrorString );
409 } elseif ( !$sent ) {
412 wfDebug(
"Unknown error sending mail" );
413 return Status::newFatal(
'php-mail-error-unknown' );
415 LoggerFactory::getInstance(
'usermailer' )->info(
416 "Email sent to {to} from {from} with subject {subject}",
418 'to' => $to[0]->toString(),
419 'allto' => implode(
', ', array_map(
'strval', $to ) ),
421 'subject' => $subject,
424 return Status::newGood();
435 private static function errorHandler( $code, $string ): bool {
436 if ( self::$mErrorString !==
'' ) {
437 self::$mErrorString .=
"\n";
439 self::$mErrorString .= preg_replace(
'/^mail\(\)(\s*\[.*?\])?: /',
'', $string );
461 $charset = strtoupper( $charset );
462 $charset = str_replace(
'ISO-8859',
'ISO8859', $charset );
464 $illegal =
'\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff=';
465 if ( !preg_match(
"/[$illegal]/", $string ) ) {
471 $replace = $illegal .
'.,\t ?_';
473 $out =
"=?$charset?Q?";
474 $out .= preg_replace_callback(
"/[$replace]/",
475 static fn ( $m ) => sprintf(
"=%02X", ord( $m[0] ) ),
484class_alias( UserMailer::class,
'UserMailer' );
wfIsWindows()
Check if the operating system is Windows.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
if(!defined('MW_SETUP_CALLBACK'))
A class containing constants representing the names of configuration variables.
const Server
Name constant for the Server setting, for use with Config::get()
const AllowHTMLEmail
Name constant for the AllowHTMLEmail setting, for use with Config::get()
const AdditionalMailParams
Name constant for the AdditionalMailParams setting, for use with Config::get()
const SMTP
Name constant for the SMTP setting, for use with Config::get()
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...