These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 Mail_smtp $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 | View Code Duplication | 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 = PHP_EOL ) { |
||
68 | $strings = []; |
||
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 array $options: |
||
106 | * 'replyTo' MailAddress |
||
107 | * 'contentType' string default 'text/plain; charset=UTF-8' |
||
108 | * 'headers' array Extra headers to set |
||
109 | * |
||
110 | * @throws MWException |
||
111 | * @throws Exception |
||
112 | * @return Status |
||
113 | */ |
||
114 | public static function send( $to, $from, $subject, $body, $options = [] ) { |
||
115 | global $wgAllowHTMLEmail; |
||
116 | |||
117 | if ( !isset( $options['contentType'] ) ) { |
||
118 | $options['contentType'] = 'text/plain; charset=UTF-8'; |
||
119 | } |
||
120 | |||
121 | if ( !is_array( $to ) ) { |
||
122 | $to = [ $to ]; |
||
123 | } |
||
124 | |||
125 | // mail body must have some content |
||
126 | $minBodyLen = 10; |
||
127 | // arbitrary but longer than Array or Object to detect casting error |
||
128 | |||
129 | // body must either be a string or an array with text and body |
||
130 | if ( |
||
131 | !( |
||
132 | !is_array( $body ) && |
||
133 | strlen( $body ) >= $minBodyLen |
||
134 | ) |
||
135 | && |
||
136 | !( |
||
137 | is_array( $body ) && |
||
138 | isset( $body['text'] ) && |
||
139 | isset( $body['html'] ) && |
||
140 | strlen( $body['text'] ) >= $minBodyLen && |
||
141 | strlen( $body['html'] ) >= $minBodyLen |
||
142 | ) |
||
143 | ) { |
||
144 | // if it is neither we have a problem |
||
145 | return Status::newFatal( 'user-mail-no-body' ); |
||
146 | } |
||
147 | |||
148 | if ( !$wgAllowHTMLEmail && is_array( $body ) ) { |
||
149 | // HTML not wanted. Dump it. |
||
150 | $body = $body['text']; |
||
151 | } |
||
152 | |||
153 | wfDebug( __METHOD__ . ': sending mail to ' . implode( ', ', $to ) . "\n" ); |
||
154 | |||
155 | // Make sure we have at least one address |
||
156 | $has_address = false; |
||
157 | foreach ( $to as $u ) { |
||
158 | if ( $u->address ) { |
||
159 | $has_address = true; |
||
160 | break; |
||
161 | } |
||
162 | } |
||
163 | if ( !$has_address ) { |
||
164 | return Status::newFatal( 'user-mail-no-addy' ); |
||
165 | } |
||
166 | |||
167 | // give a chance to UserMailerTransformContents subscribers who need to deal with each |
||
168 | // target differently to split up the address list |
||
169 | if ( count( $to ) > 1 ) { |
||
170 | $oldTo = $to; |
||
171 | Hooks::run( 'UserMailerSplitTo', [ &$to ] ); |
||
172 | if ( $oldTo != $to ) { |
||
173 | $splitTo = array_diff( $oldTo, $to ); |
||
174 | $to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook |
||
175 | // first send to non-split address list, then to split addresses one by one |
||
176 | $status = Status::newGood(); |
||
177 | if ( $to ) { |
||
178 | $status->merge( UserMailer::sendInternal( |
||
179 | $to, $from, $subject, $body, $options ) ); |
||
180 | } |
||
181 | foreach ( $splitTo as $newTo ) { |
||
182 | $status->merge( UserMailer::sendInternal( |
||
183 | [ $newTo ], $from, $subject, $body, $options ) ); |
||
184 | } |
||
185 | return $status; |
||
186 | } |
||
187 | } |
||
188 | |||
189 | return UserMailer::sendInternal( $to, $from, $subject, $body, $options ); |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * Helper function fo UserMailer::send() which does the actual sending. It expects a $to |
||
194 | * list which the UserMailerSplitTo hook would not split further. |
||
195 | * @param MailAddress[] $to Array of recipients' email addresses |
||
196 | * @param MailAddress $from Sender's email |
||
197 | * @param string $subject Email's subject. |
||
198 | * @param string $body Email's text or Array of two strings to be the text and html bodies |
||
199 | * @param array $options: |
||
0 ignored issues
–
show
|
|||
200 | * 'replyTo' MailAddress |
||
201 | * 'contentType' string default 'text/plain; charset=UTF-8' |
||
202 | * 'headers' array Extra headers to set |
||
203 | * |
||
204 | * @throws MWException |
||
205 | * @throws Exception |
||
206 | * @return Status |
||
207 | */ |
||
208 | protected static function sendInternal( |
||
209 | array $to, |
||
210 | MailAddress $from, |
||
211 | $subject, |
||
212 | $body, |
||
213 | $options = [] |
||
214 | ) { |
||
215 | global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams; |
||
216 | $mime = null; |
||
217 | |||
218 | $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null; |
||
219 | $contentType = isset( $options['contentType'] ) ? |
||
220 | $options['contentType'] : 'text/plain; charset=UTF-8'; |
||
221 | $headers = isset( $options['headers'] ) ? $options['headers'] : []; |
||
222 | |||
223 | // Allow transformation of content, such as encrypting/signing |
||
224 | $error = false; |
||
225 | View Code Duplication | if ( !Hooks::run( 'UserMailerTransformContent', [ $to, $from, &$body, &$error ] ) ) { |
|
226 | if ( $error ) { |
||
227 | return Status::newFatal( 'php-mail-error', $error ); |
||
228 | } else { |
||
229 | return Status::newFatal( 'php-mail-error-unknown' ); |
||
230 | } |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Forge email headers |
||
235 | * ------------------- |
||
236 | * |
||
237 | * WARNING |
||
238 | * |
||
239 | * DO NOT add To: or Subject: headers at this step. They need to be |
||
240 | * handled differently depending upon the mailer we are going to use. |
||
241 | * |
||
242 | * To: |
||
243 | * PHP mail() first argument is the mail receiver. The argument is |
||
244 | * used as a recipient destination and as a To header. |
||
245 | * |
||
246 | * PEAR mailer has a recipient argument which is only used to |
||
247 | * send the mail. If no To header is given, PEAR will set it to |
||
248 | * to 'undisclosed-recipients:'. |
||
249 | * |
||
250 | * NOTE: To: is for presentation, the actual recipient is specified |
||
251 | * by the mailer using the Rcpt-To: header. |
||
252 | * |
||
253 | * Subject: |
||
254 | * PHP mail() second argument to pass the subject, passing a Subject |
||
255 | * as an additional header will result in a duplicate header. |
||
256 | * |
||
257 | * PEAR mailer should be passed a Subject header. |
||
258 | * |
||
259 | * -- hashar 20120218 |
||
260 | */ |
||
261 | |||
262 | $headers['From'] = $from->toString(); |
||
263 | $returnPath = $from->address; |
||
264 | $extraParams = $wgAdditionalMailParams; |
||
265 | |||
266 | // Hook to generate custom VERP address for 'Return-Path' |
||
267 | Hooks::run( 'UserMailerChangeReturnPath', [ $to, &$returnPath ] ); |
||
268 | // Add the envelope sender address using the -f command line option when PHP mail() is used. |
||
269 | // Will default to the $from->address when the UserMailerChangeReturnPath hook fails and the |
||
270 | // generated VERP address when the hook runs effectively. |
||
271 | $extraParams .= ' -f ' . $returnPath; |
||
272 | |||
273 | $headers['Return-Path'] = $returnPath; |
||
274 | |||
275 | if ( $replyto ) { |
||
276 | $headers['Reply-To'] = $replyto->toString(); |
||
277 | } |
||
278 | |||
279 | $headers['Date'] = MWTimestamp::getLocalInstance()->format( 'r' ); |
||
280 | $headers['Message-ID'] = self::makeMsgId(); |
||
281 | $headers['X-Mailer'] = 'MediaWiki mailer'; |
||
282 | $headers['List-Unsubscribe'] = '<' . SpecialPage::getTitleFor( 'Preferences' ) |
||
283 | ->getFullURL( '', false, PROTO_CANONICAL ) . '>'; |
||
284 | |||
285 | // Line endings need to be different on Unix and Windows due to |
||
286 | // the bug described at http://trac.wordpress.org/ticket/2603 |
||
287 | $endl = PHP_EOL; |
||
288 | |||
289 | if ( is_array( $body ) ) { |
||
290 | // we are sending a multipart message |
||
291 | wfDebug( "Assembling multipart mime email\n" ); |
||
292 | if ( !stream_resolve_include_path( 'Mail/mime.php' ) ) { |
||
293 | wfDebug( "PEAR Mail_Mime package is not installed. Falling back to text email.\n" ); |
||
294 | // remove the html body for text email fall back |
||
295 | $body = $body['text']; |
||
296 | } else { |
||
297 | // Check if pear/mail_mime is already loaded (via composer) |
||
298 | if ( !class_exists( 'Mail_mime' ) ) { |
||
299 | require_once 'Mail/mime.php'; |
||
300 | } |
||
301 | if ( wfIsWindows() ) { |
||
302 | $body['text'] = str_replace( "\n", "\r\n", $body['text'] ); |
||
303 | $body['html'] = str_replace( "\n", "\r\n", $body['html'] ); |
||
304 | } |
||
305 | $mime = new Mail_mime( [ |
||
306 | 'eol' => $endl, |
||
307 | 'text_charset' => 'UTF-8', |
||
308 | 'html_charset' => 'UTF-8' |
||
309 | ] ); |
||
310 | $mime->setTXTBody( $body['text'] ); |
||
311 | $mime->setHTMLBody( $body['html'] ); |
||
312 | $body = $mime->get(); // must call get() before headers() |
||
313 | $headers = $mime->headers( $headers ); |
||
314 | } |
||
315 | } |
||
316 | if ( $mime === null ) { |
||
317 | // sending text only, either deliberately or as a fallback |
||
318 | if ( wfIsWindows() ) { |
||
319 | $body = str_replace( "\n", "\r\n", $body ); |
||
320 | } |
||
321 | $headers['MIME-Version'] = '1.0'; |
||
322 | $headers['Content-type'] = $contentType; |
||
323 | $headers['Content-transfer-encoding'] = '8bit'; |
||
324 | } |
||
325 | |||
326 | // allow transformation of MIME-encoded message |
||
327 | View Code Duplication | if ( !Hooks::run( 'UserMailerTransformMessage', |
|
328 | [ $to, $from, &$subject, &$headers, &$body, &$error ] ) |
||
329 | ) { |
||
330 | if ( $error ) { |
||
331 | return Status::newFatal( 'php-mail-error', $error ); |
||
332 | } else { |
||
333 | return Status::newFatal( 'php-mail-error-unknown' ); |
||
334 | } |
||
335 | } |
||
336 | |||
337 | $ret = Hooks::run( 'AlternateUserMailer', [ $headers, $to, $from, $subject, $body ] ); |
||
338 | if ( $ret === false ) { |
||
339 | // the hook implementation will return false to skip regular mail sending |
||
340 | return Status::newGood(); |
||
341 | } elseif ( $ret !== true ) { |
||
342 | // the hook implementation will return a string to pass an error message |
||
343 | return Status::newFatal( 'php-mail-error', $ret ); |
||
344 | } |
||
345 | |||
346 | if ( is_array( $wgSMTP ) ) { |
||
347 | // Check if pear/mail is already loaded (via composer) |
||
348 | if ( !class_exists( 'Mail' ) ) { |
||
349 | // PEAR MAILER |
||
350 | if ( !stream_resolve_include_path( 'Mail.php' ) ) { |
||
351 | throw new MWException( 'PEAR mail package is not installed' ); |
||
352 | } |
||
353 | require_once 'Mail.php'; |
||
354 | } |
||
355 | |||
356 | MediaWiki\suppressWarnings(); |
||
357 | |||
358 | // Create the mail object using the Mail::factory method |
||
359 | $mail_object =& Mail::factory( 'smtp', $wgSMTP ); |
||
360 | View Code Duplication | if ( PEAR::isError( $mail_object ) ) { |
|
361 | wfDebug( "PEAR::Mail factory failed: " . $mail_object->getMessage() . "\n" ); |
||
362 | MediaWiki\restoreWarnings(); |
||
363 | return Status::newFatal( 'pear-mail-error', $mail_object->getMessage() ); |
||
364 | } |
||
365 | |||
366 | wfDebug( "Sending mail via PEAR::Mail\n" ); |
||
367 | |||
368 | $headers['Subject'] = self::quotedPrintable( $subject ); |
||
369 | |||
370 | // When sending only to one recipient, shows it its email using To: |
||
371 | if ( count( $to ) == 1 ) { |
||
372 | $headers['To'] = $to[0]->toString(); |
||
373 | } |
||
374 | |||
375 | // Split jobs since SMTP servers tends to limit the maximum |
||
376 | // number of possible recipients. |
||
377 | $chunks = array_chunk( $to, $wgEnotifMaxRecips ); |
||
378 | foreach ( $chunks as $chunk ) { |
||
379 | $status = self::sendWithPear( $mail_object, $chunk, $headers, $body ); |
||
380 | // FIXME : some chunks might be sent while others are not! |
||
381 | if ( !$status->isOK() ) { |
||
382 | MediaWiki\restoreWarnings(); |
||
383 | return $status; |
||
384 | } |
||
385 | } |
||
386 | MediaWiki\restoreWarnings(); |
||
387 | return Status::newGood(); |
||
388 | } else { |
||
389 | // PHP mail() |
||
390 | if ( count( $to ) > 1 ) { |
||
391 | $headers['To'] = 'undisclosed-recipients:;'; |
||
392 | } |
||
393 | $headers = self::arrayToHeaderString( $headers, $endl ); |
||
394 | |||
395 | wfDebug( "Sending mail via internal mail() function\n" ); |
||
396 | |||
397 | self::$mErrorString = ''; |
||
398 | $html_errors = ini_get( 'html_errors' ); |
||
399 | ini_set( 'html_errors', '0' ); |
||
400 | set_error_handler( 'UserMailer::errorHandler' ); |
||
401 | |||
402 | try { |
||
403 | foreach ( $to as $recip ) { |
||
404 | $sent = mail( |
||
405 | $recip, |
||
406 | self::quotedPrintable( $subject ), |
||
407 | $body, |
||
408 | $headers, |
||
409 | $extraParams |
||
410 | ); |
||
411 | } |
||
412 | } catch ( Exception $e ) { |
||
413 | restore_error_handler(); |
||
414 | throw $e; |
||
415 | } |
||
416 | |||
417 | restore_error_handler(); |
||
418 | ini_set( 'html_errors', $html_errors ); |
||
419 | |||
420 | if ( self::$mErrorString ) { |
||
421 | wfDebug( "Error sending mail: " . self::$mErrorString . "\n" ); |
||
422 | return Status::newFatal( 'php-mail-error', self::$mErrorString ); |
||
423 | } elseif ( !$sent ) { |
||
424 | // mail function only tells if there's an error |
||
425 | wfDebug( "Unknown error sending mail\n" ); |
||
426 | return Status::newFatal( 'php-mail-error-unknown' ); |
||
427 | } else { |
||
428 | return Status::newGood(); |
||
429 | } |
||
430 | } |
||
431 | } |
||
432 | |||
433 | /** |
||
434 | * Set the mail error message in self::$mErrorString |
||
435 | * |
||
436 | * @param int $code Error number |
||
437 | * @param string $string Error message |
||
438 | */ |
||
439 | static function errorHandler( $code, $string ) { |
||
440 | self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); |
||
441 | } |
||
442 | |||
443 | /** |
||
444 | * Strips bad characters from a header value to prevent PHP mail header injection attacks |
||
445 | * @param string $val String to be santizied |
||
446 | * @return string |
||
447 | */ |
||
448 | public static function sanitizeHeaderValue( $val ) { |
||
449 | return strtr( $val, [ "\r" => '', "\n" => '' ] ); |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * Converts a string into a valid RFC 822 "phrase", such as is used for the sender name |
||
454 | * @param string $phrase |
||
455 | * @return string |
||
456 | */ |
||
457 | public static function rfc822Phrase( $phrase ) { |
||
458 | // Remove line breaks |
||
459 | $phrase = self::sanitizeHeaderValue( $phrase ); |
||
460 | // Remove quotes |
||
461 | $phrase = str_replace( '"', '', $phrase ); |
||
462 | return '"' . $phrase . '"'; |
||
463 | } |
||
464 | |||
465 | /** |
||
466 | * Converts a string into quoted-printable format |
||
467 | * @since 1.17 |
||
468 | * |
||
469 | * From PHP5.3 there is a built in function quoted_printable_encode() |
||
470 | * This method does not duplicate that. |
||
471 | * This method is doing Q encoding inside encoded-words as defined by RFC 2047 |
||
472 | * This is for email headers. |
||
473 | * The built in quoted_printable_encode() is for email bodies |
||
474 | * @param string $string |
||
475 | * @param string $charset |
||
476 | * @return string |
||
477 | */ |
||
478 | public static function quotedPrintable( $string, $charset = '' ) { |
||
479 | // Probably incomplete; see RFC 2045 |
||
480 | if ( empty( $charset ) ) { |
||
481 | $charset = 'UTF-8'; |
||
482 | } |
||
483 | $charset = strtoupper( $charset ); |
||
484 | $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ? |
||
485 | |||
486 | $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff='; |
||
487 | $replace = $illegal . '\t ?_'; |
||
488 | if ( !preg_match( "/[$illegal]/", $string ) ) { |
||
489 | return $string; |
||
490 | } |
||
491 | $out = "=?$charset?Q?"; |
||
492 | $out .= preg_replace_callback( "/([$replace])/", |
||
493 | function ( $matches ) { |
||
494 | return sprintf( "=%02X", ord( $matches[1] ) ); |
||
495 | }, |
||
496 | $string |
||
497 | ); |
||
498 | $out .= '?='; |
||
499 | return $out; |
||
500 | } |
||
501 | } |
||
502 |
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.
Consider the following example. The parameter
$ireland
is not defined by the methodfinale(...)
.The most likely cause is that the parameter was changed, but the annotation was not.