1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This class deals with the actual sending of your sites emails |
||
5 | * |
||
6 | * @package ElkArte Forum |
||
7 | * @copyright ElkArte Forum contributors |
||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||
9 | * |
||
10 | * @version 2.0 dev |
||
11 | * |
||
12 | */ |
||
13 | |||
14 | namespace ElkArte\Mail; |
||
15 | |||
16 | use ElkArte\Errors\Errors; |
||
0 ignored issues
–
show
|
|||
17 | |||
18 | /** |
||
19 | * Deals with the sending of email via mail() or SMTP functions |
||
20 | */ |
||
21 | class Mail extends BaseMail |
||
22 | { |
||
23 | /** |
||
24 | * This function dispatches to PHP mail or SMTP mail to send email to the specified recipient(s). |
||
25 | * |
||
26 | * It uses the mail_type settings and webmaster_email variable. |
||
27 | * |
||
28 | * @param string[]|string $to - the email(s) to send to |
||
29 | * @param string $subject - email subject as prepared by buildEmail() |
||
30 | * @param string $message - email body as processed by buildEmail() |
||
31 | * @param string|null $message_id = null - if specified, it will be used as local part of the Message-ID header. |
||
32 | * @return bool whether the email was accepted properly. |
||
33 | * @package Mail |
||
34 | */ |
||
35 | public function sendMail($to, $subject, $headers, $message, $message_id = null) |
||
36 | { |
||
37 | $message_id = $this->setMessageType($message_id); |
||
38 | |||
39 | $to = is_array($to) ? $to : [$to]; |
||
40 | |||
41 | if ($this->useSendmail) |
||
42 | { |
||
43 | return $this->sendPHP($to, $subject, $message, $headers, $message_id); |
||
44 | } |
||
45 | |||
46 | return $this->SMTP($to, $subject, $message, $headers, $message_id); |
||
47 | } |
||
48 | |||
49 | /** |
||
50 | * Sends an email using PHP mail() function |
||
51 | * |
||
52 | * @param string[] $mail_to_array |
||
53 | * @param string $subject |
||
54 | * @param string $message |
||
55 | * @param string $headers |
||
56 | * @param string $message_id |
||
57 | * @return bool if the mail was accepted by the system |
||
58 | */ |
||
59 | public function sendPHP($mail_to_array, $subject, $message, $headers, $message_id) |
||
60 | { |
||
61 | global $webmaster_email, $modSettings, $txt; |
||
62 | |||
63 | $mail_result = true; |
||
64 | $subject = strtr($subject, ["\r" => '', "\n" => '']); |
||
65 | |||
66 | // Looks like another hidden beauty here |
||
67 | if (!empty($modSettings['mail_strip_carriage'])) |
||
68 | { |
||
69 | $message = strtr($message, ["\r" => '']); |
||
70 | $headers = strtr($headers, ["\r" => '']); |
||
71 | } |
||
72 | |||
73 | $mid = strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@'); |
||
74 | $this->setReturnPath(); |
||
75 | |||
76 | // This is frequently not set, or not set according to the needs of PBE and bounce detection |
||
77 | // We have to use ini_set, since "-f <address>" doesn't work on Windows systems, so we need both |
||
78 | $old_return = ini_set('sendmail_from', $this->returnPath); |
||
79 | |||
80 | $sent = []; |
||
81 | foreach ($mail_to_array as $sendTo) |
||
82 | { |
||
83 | // Every message sent gets a unique Message-ID header |
||
84 | $unq_head = $this->getUniqueMessageID($message_id); |
||
85 | $messageHeader = 'Message-ID: <' . $unq_head . $mid . '>'; |
||
86 | |||
87 | // Using PBE, we also insert keys in the message as a safety net of sorts |
||
88 | if ($this->mailList) |
||
89 | { |
||
90 | $message = mail_insert_key($message, $unq_head, $this->lineBreak); |
||
91 | } |
||
92 | |||
93 | $sendTo = strtr($sendTo, ["\r" => '', "\n" => '']); |
||
94 | if (!mail($sendTo, $subject, $message, $headers . $this->lineBreak . $messageHeader, '-f ' . $this->returnPath)) |
||
95 | { |
||
96 | Errors::instance()->log_error(sprintf($txt['mail_send_unable'], $sendTo)); |
||
97 | $mail_result = false; |
||
98 | } |
||
99 | else |
||
100 | { |
||
101 | // Keep our post via email log |
||
102 | if ($this->mailList) |
||
103 | { |
||
104 | $this->unqPBEHead[3] = time(); |
||
105 | $this->unqPBEHead[4] = $sendTo; |
||
106 | $sent[] = $this->unqPBEHead; |
||
107 | } |
||
108 | |||
109 | // Track total emails sent |
||
110 | if (!empty($modSettings['trackStats'])) |
||
111 | { |
||
112 | trackStats(['email' => '+']); |
||
113 | } |
||
114 | } |
||
115 | |||
116 | // Wait, wait, I'm still sending here! |
||
117 | detectServer()->setTimeLimit(300); |
||
118 | } |
||
119 | |||
120 | // Put it back |
||
121 | ini_set('sendmail_from', $old_return); |
||
122 | |||
123 | // Log each email that we sent, such that they can be replied to |
||
124 | if (!empty($sent)) |
||
125 | { |
||
126 | require_once(SUBSDIR . '/Maillist.subs.php'); |
||
127 | log_email($sent); |
||
128 | } |
||
129 | |||
130 | return $mail_result; |
||
131 | } |
||
132 | |||
133 | /** |
||
134 | * Sends mail, like mail() but using Simple Mail Transfer Protocol (SMTP). |
||
135 | * |
||
136 | * - It expects no slashes or entities. |
||
137 | * |
||
138 | * @param string[] $mail_to_array - array of strings (email addresses) |
||
139 | * @param string $subject email subject |
||
140 | * @param string $message email message |
||
141 | * @param string $headers |
||
142 | * @param string|null $message_id |
||
143 | * @return bool whether it sent or not. |
||
144 | * @package Mail |
||
145 | */ |
||
146 | public function SMTP($mail_to_array, $subject, $message, $headers, $message_id = null) |
||
147 | { |
||
148 | global $modSettings, $webmaster_email; |
||
149 | |||
150 | // This should already be set in the ACP |
||
151 | if (empty($modSettings['smtp_client'])) |
||
152 | { |
||
153 | $modSettings['smtp_client'] = detectServer()->getFQDN(empty($modSettings['smtp_host']) ? '' : $modSettings['smtp_host']); |
||
154 | updateSettings(['smtp_client' => $modSettings['smtp_client']]); |
||
155 | } |
||
156 | |||
157 | // Shortcuts |
||
158 | $smtp_client = $modSettings['smtp_client']; |
||
159 | $smtp_port = empty($modSettings['smtp_port']) ? 25 : (int) $modSettings['smtp_port']; |
||
160 | $smtp_host = trim($modSettings['smtp_host']); |
||
161 | |||
162 | // Try to connect to the SMTP server... |
||
163 | $socket = $this->_getSMTPSocket($smtp_host, $smtp_port); |
||
164 | if (!is_resource($socket)) |
||
165 | { |
||
166 | return false; |
||
167 | } |
||
168 | |||
169 | // The server responded, now login our client |
||
170 | $login = $this->_loginSMTPClient($socket, $smtp_client); |
||
171 | if ($login === false) |
||
172 | { |
||
173 | return false; |
||
174 | } |
||
175 | |||
176 | // Fix the message for any lines beginning with a period! (the first is ignored, you see.) |
||
177 | $message = strtr($message, ["\r\n" . '.' => "\r\n" . '..']); |
||
178 | |||
179 | $mid = strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@'); |
||
180 | $this->setReturnPath(); |
||
181 | $mail_to_array = array_values($mail_to_array); |
||
182 | $sent = []; |
||
183 | |||
184 | // Time to send these, so they can be trapped in a SPAM filter :P |
||
185 | foreach ($mail_to_array as $i => $mail_to) |
||
186 | { |
||
187 | $this_message = $message; |
||
188 | $unq_head = $this->getUniqueMessageID($message_id); |
||
189 | $messageHeader = 'Message-ID: <' . $unq_head . $mid . '>'; |
||
190 | |||
191 | // Reset the connection to send another email. |
||
192 | if (($i !== 0) && !$this->_server_parse('RSET', $socket, '250')) |
||
193 | { |
||
194 | return false; |
||
195 | } |
||
196 | |||
197 | // From, to, and then start the data... |
||
198 | if (!$this->_server_parse('MAIL FROM: <' . $this->returnPath . '>', $socket, '250')) |
||
199 | { |
||
200 | return false; |
||
201 | } |
||
202 | |||
203 | if (!$this->_server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250')) |
||
204 | { |
||
205 | return false; |
||
206 | } |
||
207 | |||
208 | if (!$this->_server_parse('DATA', $socket, '354')) |
||
209 | { |
||
210 | return false; |
||
211 | } |
||
212 | |||
213 | // Using PBE, we also insert keys in the message to overcome clients that act badly |
||
214 | if ($this->mailList) |
||
215 | { |
||
216 | $this_message = mail_insert_key($this_message, $unq_head, $this->lineBreak); |
||
217 | } |
||
218 | |||
219 | fwrite($socket, 'Subject: ' . $subject . $this->lineBreak); |
||
220 | if ($mail_to !== '') |
||
221 | { |
||
222 | fwrite($socket, 'To: <' . $mail_to . '>' . $this->lineBreak); |
||
223 | } |
||
224 | |||
225 | fwrite($socket, $headers . $this->lineBreak . $messageHeader . $this->lineBreak . $this->lineBreak); |
||
226 | fwrite($socket, $this_message . $this->lineBreak); |
||
227 | |||
228 | // Send a ., or in other words "end of data". |
||
229 | if (!$this->_server_parse('.', $socket, '250')) |
||
230 | { |
||
231 | return false; |
||
232 | } |
||
233 | |||
234 | // track the number of emails sent |
||
235 | if (!empty($modSettings['trackStats'])) |
||
236 | { |
||
237 | trackStats(['email' => '+']); |
||
238 | } |
||
239 | |||
240 | // Keep our post via email log |
||
241 | if ($this->mailList) |
||
242 | { |
||
243 | $this->unqPBEHead[3] = time(); |
||
244 | $this->unqPBEHead[4] = $mail_to; |
||
245 | $sent[] = $this->unqPBEHead; |
||
246 | } |
||
247 | |||
248 | // Almost done, almost done... don't stop me just yet! |
||
249 | detectServer()->setTimeLimit(300); |
||
250 | } |
||
251 | |||
252 | // say our goodbyes |
||
253 | fwrite($socket, 'QUIT' . $this->lineBreak); |
||
254 | fclose($socket); |
||
255 | |||
256 | // Log each email if using PBE |
||
257 | if (!empty($sent)) |
||
258 | { |
||
259 | require_once(SUBSDIR . '/Maillist.subs.php'); |
||
260 | log_email($sent); |
||
261 | } |
||
262 | |||
263 | return true; |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Make a connection to the SMTP server |
||
268 | * |
||
269 | * @param string $smtp_host |
||
270 | * @param int $smtp_port |
||
271 | * @return false|resource |
||
272 | */ |
||
273 | private function _getSMTPSocket($smtp_host, $smtp_port) |
||
274 | { |
||
275 | global $txt; |
||
276 | |||
277 | // Try to connect to the SMTP server... if it doesn't exist, only wait three seconds. |
||
278 | set_error_handler(static function () { /* ignore errors */ }); |
||
279 | try |
||
280 | { |
||
281 | $socket = fsockopen($smtp_host, $smtp_port, $errno, $errstr, 3); |
||
282 | } |
||
283 | catch (\Exception) |
||
284 | { |
||
285 | $socket = false; |
||
286 | } |
||
287 | finally |
||
288 | { |
||
289 | restore_error_handler(); |
||
290 | } |
||
291 | |||
292 | if (!is_resource($socket)) |
||
293 | { |
||
294 | // Maybe we can still save this? The port might be wrong. |
||
295 | if ($smtp_port === 25 && strpos($smtp_host, 'ssl:') === 0) |
||
296 | { |
||
297 | $socket = fsockopen($smtp_host, 465, $errno, $errstr, 3); |
||
298 | if (is_resource($socket)) |
||
299 | { |
||
300 | updateSettings(['smtp_port' => 465]); |
||
301 | Errors::instance()->log_error($txt['smtp_port_ssl']); |
||
302 | } |
||
303 | } |
||
304 | |||
305 | // Unable to connect! |
||
306 | if (!is_resource($socket)) |
||
307 | { |
||
308 | Errors::instance()->log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr); |
||
309 | } |
||
310 | } |
||
311 | |||
312 | // Wait for a response of 220, without "-" continue. |
||
313 | if (!is_resource($socket) || !$this->_server_parse(null, $socket, '220')) |
||
314 | { |
||
315 | return false; |
||
316 | } |
||
317 | |||
318 | return $socket; |
||
319 | } |
||
320 | |||
321 | /** |
||
322 | * Parse a message to the SMTP server. |
||
323 | * |
||
324 | * - Sends the specified message to the server, and checks for the expected response. |
||
325 | * |
||
326 | * @param string $message - the message to send |
||
327 | * @param resource $socket - socket to send on |
||
328 | * @param string $response - the expected response code |
||
329 | * @return string|bool it responded as such. |
||
330 | * @package Mail |
||
331 | */ |
||
332 | private function _server_parse($message, $socket, $response) |
||
333 | { |
||
334 | global $txt; |
||
335 | |||
336 | if ($message !== null) |
||
0 ignored issues
–
show
|
|||
337 | { |
||
338 | fwrite($socket, $message . "\r\n"); |
||
339 | } |
||
340 | |||
341 | // No response yet. |
||
342 | $server_response = ''; |
||
343 | |||
344 | while (substr($server_response, 3, 1) !== ' ') |
||
345 | { |
||
346 | if (!($server_response = fgets($socket, 256))) |
||
347 | { |
||
348 | // @todo Change this message to reflect that it may mean bad user/password/server issues/etc. |
||
349 | Errors::instance()->log_error($txt['smtp_bad_response']); |
||
350 | |||
351 | return false; |
||
352 | } |
||
353 | } |
||
354 | |||
355 | if ($response === null) |
||
0 ignored issues
–
show
|
|||
356 | { |
||
357 | return substr($server_response, 0, 3); |
||
358 | } |
||
359 | |||
360 | if (strpos($server_response, $response) !== 0) |
||
361 | { |
||
362 | Errors::instance()->log_error($txt['smtp_error'] . $server_response); |
||
363 | |||
364 | return false; |
||
365 | } |
||
366 | |||
367 | return true; |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Logs a 'user' on to the SMTP server |
||
372 | * |
||
373 | * If it fails and suspects TLS is required, will attempt that as well. |
||
374 | * |
||
375 | * @param resource $socket |
||
376 | * @param string $smtp_client |
||
377 | * @return bool |
||
378 | */ |
||
379 | private function _loginSMTPClient($socket, $smtp_client) |
||
380 | { |
||
381 | global $modSettings; |
||
382 | |||
383 | $smtp_username = trim($modSettings['smtp_username']); |
||
384 | $smtp_password = trim($modSettings['smtp_password']); |
||
385 | $smtp_starttls = !empty($modSettings['smtp_starttls']); |
||
386 | if ($smtp_username !== '' && $smtp_password !== '') |
||
387 | { |
||
388 | // EHLO could be understood to mean encrypted hello... |
||
389 | if ($this->_server_parse('EHLO ' . $smtp_client, $socket, null) === '250') |
||
390 | { |
||
391 | if ($smtp_starttls) |
||
392 | { |
||
393 | $this->_server_parse('STARTTLS', $socket, null); |
||
394 | stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); |
||
395 | $this->_server_parse('EHLO ' . $smtp_client, $socket, null); |
||
396 | } |
||
397 | if (!$this->_server_parse('AUTH LOGIN', $socket, '334')) |
||
398 | { |
||
399 | return false; |
||
400 | } |
||
401 | // Send the username and password, encoded. |
||
402 | if (!$this->_server_parse(base64_encode($smtp_username), $socket, '334')) |
||
403 | { |
||
404 | return false; |
||
405 | } |
||
406 | |||
407 | // The password is already encoded ;) |
||
408 | return (bool) $this->_server_parse($smtp_password, $socket, '235'); |
||
409 | } |
||
410 | |||
411 | return (bool) $this->_server_parse('HELO ' . $smtp_client, $socket, '250'); |
||
412 | } |
||
413 | |||
414 | // Just say "helo". |
||
415 | return (bool) $this->_server_parse('HELO ' . $smtp_client, $socket, '250'); |
||
416 | } |
||
417 | } |
||
418 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths