Mail::SMTP()   F
last analyzed

Complexity

Conditions 19
Paths 176

Size

Total Lines 118
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 19
eloc 52
nc 176
nop 5
dl 0
loc 118
rs 3.8833
c 2
b 1
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
introduced by
The condition $message !== null is always true.
Loading history...
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
introduced by
The condition $response === null is always false.
Loading history...
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