Cancelled
Pull Request — development (#3540)
by Emanuele
05:13
created

reduceMailQueue()   F

Complexity

Conditions 55
Paths > 20000

Size

Total Lines 227
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 3080

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 55
eloc 92
nc 78021861
nop 3
dl 0
loc 227
ccs 0
cts 86
cp 0
crap 3080
rs 0
c 1
b 0
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 file handles tasks related to mail.
5
 * The functions in this file do NOT check permissions.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
use BBC\ParserWrapper;
19
use ElkArte\Themes\ThemeLoader;
20
use ElkArte\User;
21
use ElkArte\Languages\Loader and LangLoader;
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_LOGICAL_AND, expecting ',' or ';' on line 21 at column 29
Loading history...
22
23
/**
24
 * This function sends an 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, expected to have entities, and slashes, but not be parsed
30
 * @param string $message - email body, expected to have slashes, no htmlentities
31
 * @param string|null $from = null - the address to use for replies
32
 * @param string|null $message_id = null - if specified, it will be used as local part of the Message-ID header.
33
 * @param bool $send_html = false, whether or not the message is HTML vs. plain text
34
 * @param int $priority = 3
35
 * @param bool|null $hotmail_fix = null
36
 * @param bool $is_private
37
 * @param string|null $from_wrapper - used to provide envelope from wrapper based on if we sharing a users display name
38
 * @param int|null $reference - The parent topic id for use in a References header
39
 * @return bool whether or not the email was accepted properly.
40
 * @throws \ElkArte\Exceptions\Exception
41
 * @package Mail
42
 */
43 3
function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false, $from_wrapper = null, $reference = null)
44
{
45
	global $webmaster_email, $context, $modSettings, $txt, $scripturl, $boardurl;
46 3
47
	// Use sendmail if it's set or if no SMTP server is set.
48
	$use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
49 3
50
	// Using maillist styles and this message qualifies (priority 3 and below only (4 = digest, 5 = newsletter))
51
	$maillist = !empty($modSettings['maillist_enabled']) && $from_wrapper !== null && $message_id !== null && $priority < 4 && empty($modSettings['mail_no_message_id']);
52 3
53
	// Line breaks need to be \r\n only in windows or for SMTP.
54 3
	$line_break = detectServer()->is('windows') || !$use_sendmail ? "\r\n" : "\n";
55
56
	if ($message_id !== null && isset($message_id[0]) && in_array($message_id[0], array('m', 'p', 't')))
57
	{
58
		$message_type = $message_id[0];
59
		$message_id = substr($message_id, 1);
60
	}
61 3
	else
62
	{
63
		$message_type = 'm';
64
	}
65 3
66
	// So far so good.
67
	$mail_result = true;
68 3
69
	// If the recipient list isn't an array, make it one.
70
	$to_array = is_array($to) ? $to : array($to);
71
72 3
	// Once upon a time, Hotmail could not interpret non-ASCII mails.
73
	// In honour of those days, it's still called the 'hotmail fix'.
74 3
	if ($hotmail_fix === null)
75 3
	{
76
		$hotmail_to = array();
77 3
		foreach ($to_array as $i => $to_address)
78
		{
79
			if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
80 1
			{
81
				$hotmail_to[] = $to_address;
82
				$to_array = array_diff($to_array, array($to_address));
83
			}
84
		}
85 3
86
		// Call this function recursively for the hotmail addresses.
87
		if (!empty($hotmail_to))
88
		{
89
			$mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_type . $message_id, $send_html, $priority, true, $is_private, $from_wrapper, $reference);
90
		}
91 3
92
		// The remaining addresses no longer need the fix.
93
		$hotmail_fix = false;
94 3
95
		// No other addresses left? Return instantly.
96
		if (empty($to_array))
97
		{
98
			return $mail_result;
99
		}
100
	}
101 3
102
	// Get rid of entities.
103
	$subject = un_htmlspecialchars($subject);
104 3
105
	// Make the message use the proper line breaks.
106
	$message = str_replace(array("\r", "\n"), array('', $line_break), $message);
107 3
108
	// Make sure hotmail mails are sent as HTML so that HTML entities work.
109
	if ($hotmail_fix && !$send_html)
110
	{
111
		$send_html = true;
112
		$message = strtr($message, array($line_break => '<br />' . $line_break));
113
		$message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
114
	}
115
116 3
	// Requirements (draft) for MLM to Support Basic DMARC Compliance
117
	// http://www.dmarc.org/supplemental/mailman-project-mlm-dmarc-reqs.html
118
	if ($maillist && $from !== null && $from_wrapper !== null)
119
	{
120
		// Be sure there is never an email in the from name if using maillist styles
121
		$dmarc_from = $from;
122
		if (filter_var($dmarc_from, FILTER_VALIDATE_EMAIL))
123
		{
124
			$dmarc_from = str_replace(strstr($dmarc_from, '@'), '', $dmarc_from);
125
		}
126
127
		// Add in the 'via' if desired, helps prevent email clients from learning/replacing legit names/emails
128
		if (!empty($modSettings['maillist_sitename']) && empty($modSettings['dmarc_spec_standard']))
129
			// @memo (2014) "via" is still a draft, and it's not yet clear if it will be localized or not.
130
			// To play safe, we are keeping it hard-coded, but the string is available for translation.
131
		{
132
			$from = $dmarc_from . ' ' . /* $txt['via'] */
133
				'via' . ' ' . $modSettings['maillist_sitename'];
134
		}
135
		else
136
		{
137
			$from = $dmarc_from;
138
		}
139
	}
140 3
141 3
	// Take care of from / subject encodings
142 3
	list (, $from_name, $from_encoding) = mimespecialchars(addcslashes($from !== null ? $from : (!empty($modSettings['maillist_sitename']) ? $modSettings['maillist_sitename'] : $context['forum_name']), '<>()\'\\"'), true, $hotmail_fix, $line_break);
143
	list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
144 3
	if ($from_encoding !== 'base64')
145
	{
146
		$from_name = '"' . $from_name . '"';
147
	}
148 3
149
	// Construct the from / replyTo mail headers, based on if we showing a users name
150
	if ($from_wrapper !== null)
151
	{
152
		$headers = 'From: ' . $from_name . ' <' . $from_wrapper . '>' . $line_break;
153
154
		// If they reply where is it going to be sent?
155
		$headers .= 'Reply-To: "' . (!empty($modSettings['maillist_sitename']) ? $modSettings['maillist_sitename'] : $context['forum_name']) . '" <' . (!empty($modSettings['maillist_sitename_address']) ? $modSettings['maillist_sitename_address'] : (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'])) . '>' . $line_break;
156
		if ($reference !== null)
157
		{
158
			$headers .= 'References: <' . $reference . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>' . $line_break;
159
		}
160
	}
161
	else
162 3
	{
163 3
		// Standard ElkArte headers
164
		$headers = 'From: ' . $from_name . ' <' . (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from']) . '>' . $line_break;
165
		$headers .= ($from !== null && strpos($from, '@') !== false) ? 'Reply-To: <' . $from . '>' . $line_break : '';
166
	}
167 3
168
	// We'll need this later for the envelope fix, too, so keep it
169
	$return_path = (!empty($modSettings['maillist_sitename_address']) ? $modSettings['maillist_sitename_address'] : (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from']));
170 3
171 3
	// Return path, date, mailer
172 3
	$headers .= 'Return-Path: ' . $return_path . $line_break;
173
	$headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
174
	$headers .= 'X-Mailer: ELK' . $line_break;
175 3
176
	// For maillist, digests or newsletters we include a few more headers for compliance
177
	if ($maillist || $priority > 3)
178
	{
179
		// Lets try to avoid auto replies
180
		$headers .= 'X-Auto-Response-Suppress: All' . $line_break;
181
		$headers .= 'Auto-Submitted: auto-generated' . $line_break;
182
183
		// Indicate its a list server to avoid spam tagging and to help client filters
184
		// http://www.ietf.org/rfc/rfc2369.txt
185
		$headers .= 'List-Id: <' . (!empty($modSettings['maillist_sitename_address']) ? $modSettings['maillist_sitename_address'] : (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'])) . '>' . $line_break;
186
		$headers .= 'List-Unsubscribe: <' . $boardurl . '/index.php?action=profile;area=notification>' . $line_break;
187
		$headers .= 'List-Owner: <mailto:' . (!empty($modSettings['maillist_sitename_help']) ? $modSettings['maillist_sitename_help'] : (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'])) . '> (' . (!empty($modSettings['maillist_sitename']) ? $modSettings['maillist_sitename'] : $context['forum_name']) . ')' . $line_break;
188
	}
189 3
190
	// Pass this to the integration before we start modifying the output -- it'll make it easier later.
191
	if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers)), true))
192
	{
193
		return false;
194
	}
195 3
196
	// Save the original message...
197
	$orig_message = $message;
198 3
199
	// The mime boundary separates the different alternative versions.
200
	$mime_boundary = 'ELK-' . md5($message . time());
201 3
202 3
	// Using mime, as it allows to send a plain unencoded alternative.
203 3
	$headers .= 'Mime-Version: 1.0' . $line_break;
204
	$headers .= 'Content-Type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
205
	$headers .= 'Content-Transfer-Encoding: 7bit' . $line_break;
206 3
207
	// Sending HTML?  Let's plop in some basic stuff, then.
208
	if ($send_html)
209
	{
210
		$no_html_message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $orig_message);
211
		$no_html_message = un_htmlspecialchars(strip_tags(strtr($no_html_message, array('</title>' => $line_break))));
212
213
		// But, then, dump it and use a plain one for dinosaur clients.
214
		list (, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
215
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
216
217
		// This is the plain text version.  Even if no one sees it, we need it for spam checkers.
218
		list ($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
219
		$message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
220
		$message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
221
		$message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
222
223
		// This is the actual HTML message, prim and proper.  If we wanted images, they could be inlined here (with multipart/related, etc.)
224
		list ($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
225
		$message .= 'Content-Type: text/html; charset=' . $charset . $line_break;
226
		$message .= 'Content-Transfer-Encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
227
		$message .= $html_message . $line_break . '--' . $mime_boundary . '--';
228
	}
229
	// Text is good too.
230 3
	else
231 3
	{
232
		// Send a plain message first, for the older web clients.
233
		$plain_message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $orig_message);
234 3
		list (, $plain_message) = mimespecialchars($plain_message, false, true, $line_break);
235 3
236 3
		$message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
237 3
238
		// Now add an encoded message using the forum's character set.
239
		list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
240
		$message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
241 3
		$message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
242
		$message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
243
	}
244
245
	// Are we using the mail queue, if so this is where we butt in...
246 3
	if (!empty($modSettings['mail_queue']) && $priority != 0)
247
	{
248
		return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private, $message_type . $message_id);
249
	}
250
	// If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
251
	elseif (!empty($modSettings['mail_queue']) && !empty($modSettings['mail_period_limit']))
252
	{
253
		list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
254
		if (empty($mails_this_minute) || time() > $last_mail_time + 60)
255
		{
256
			$new_queue_stat = time() . '|' . 1;
257
		}
258
		else
259
		{
260
			$new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
261
		}
262 3
263
		updateSettings(array('mail_recent' => $new_queue_stat));
264 3
	}
265 3
266
	// SMTP or sendmail?
267
	if ($use_sendmail)
268
	{
269
		$subject = strtr($subject, array("\r" => '', "\n" => ''));
270 3
		if (!empty($modSettings['mail_strip_carriage']))
271 3
		{
272
			$message = strtr($message, array("\r" => ''));
273 3
			$headers = strtr($headers, array("\r" => ''));
274
		}
275 3
		$sent = array();
276 3
		$need_break = substr($headers, -1) === "\n" || substr($headers, -1) === "\r" ? false : true;
277 3
278
		foreach ($to_array as $key => $to)
279
		{
280 3
			$unq_id = '';
281
			$unq_head = '';
282
			$unq_head_array = array();
283
284
			// If we are using the post by email functions, then we generate "reply to mail" security keys
285
			if ($maillist && !empty($message_id) && $priority != 4)
286
			{
287
				$unq_head_array[0] = md5($boardurl . microtime() . rand());
288
				$unq_head_array[1] = $message_type;
289
				$unq_head_array[2] = $message_id;
290 3
				$unq_head = $unq_head_array[0] . '-' . $unq_head_array[1] . $unq_head_array[2];
291
				$unq_id = ($need_break ? $line_break : '') . 'Message-ID: <' . $unq_head . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>';
292 3
				$message = mail_insert_key($message, $unq_head, $line_break);
293
			}
294
			elseif (empty($modSettings['mail_no_message_id']))
295
			{
296
				$unq_id = ($need_break ? $line_break : '') . 'Message-ID: <' . md5($boardurl . microtime()) . '-' . $message_id . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>';
297 3
			}
298 3
299
			// This is frequently not set, or not set according to the needs of PBE and bounce detection
300 3
			// We have to use ini_set, since "-f <address>" doesn't work on windows systems, so we need both
301 3
			$old_return = ini_set('sendmail_from', $return_path);
302
			if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers . $unq_id, '-f ' . $return_path))
303
			{
304
				\ElkArte\Errors\Errors::instance()->log_error(sprintf($txt['mail_send_unable'], $to));
305
				$mail_result = false;
306
			}
307
			else
308
			{
309
				// Keep our post via email log
310
				if (!empty($unq_head))
311
				{
312
					$unq_head_array[] = time();
313
					$unq_head_array[] = $to;
314
					$sent[] = $unq_head_array;
315
				}
316
317
				// Track total emails sent
318
				if (!empty($modSettings['trackStats']))
319
				{
320
					trackStats(array('email' => '+'));
321 3
				}
322
			}
323
324 3
			// Put it back
325
			ini_set('sendmail_from', $old_return);
326
327
			// Wait, wait, I'm still sending here!
328 3
			detectServer()->setTimeLimit(300);
329
		}
330
331 3
		// Log each email that we sent so they can be replied to
332
		if (!empty($sent))
333
		{
334
			require_once(SUBSDIR . '/Maillist.subs.php');
335
			log_email($sent);
336
		}
337
	}
338
	else
339
		// SMTP protocol it is
340
	{
341 3
		$mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers, $priority, $message_type . $message_id);
342
	}
343
344 3
	// Clear out the stat cache.
345
	trackStats();
346
347
	// Everything go smoothly?
348
	return $mail_result;
349
}
350
351
/**
352
 * Add an email to the mail queue.
353
 *
354
 * @param bool $flush = false
355
 * @param string[] $to_array = array()
356
 * @param string $subject = ''
357
 * @param string $message = ''
358
 * @param string $headers = ''
359
 * @param bool $send_html = false
360
 * @param int $priority = 3
361
 * @param bool $is_private
362
 * @param string|null $message_id
363
 * @return bool
364
 * @package Mail
365
 */
366
function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false, $message_id = '')
367
{
368
	global $context;
369
370
	$db = database();
371
372
	static $cur_insert = array();
373
	static $cur_insert_len = 0;
374
375
	if ($cur_insert_len == 0)
376
	{
377
		$cur_insert = array();
378
	}
379
380
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
381
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
382
	{
383
		// Only do these once.
384
		$cur_insert_len = 0;
385
386
		// Dump the data...
387
		$db->insert('',
388
			'{db_prefix}mail_queue',
389
			array(
390
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
391
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255',
392
			),
393
			$cur_insert,
394
			array('id_mail')
395
		);
396
397
		$cur_insert = array();
398
		$context['flush_mail'] = false;
399
	}
400
401
	// If we're flushing we're done.
402
	if ($flush)
403
	{
404
		$nextSendTime = time() + 10;
405
406
		$db->query('', '
407
			UPDATE {db_prefix}settings
408
			SET 
409
				value = {string:nextSendTime}
410
			WHERE variable = {string:mail_next_send}
411
				AND value = {string:no_outstanding}',
412
			array(
413
				'nextSendTime' => $nextSendTime,
414
				'mail_next_send' => 'mail_next_send',
415
				'no_outstanding' => '0',
416
			)
417
		);
418
419
		return true;
420
	}
421
422
	// Ensure we tell obExit to flush.
423
	$context['flush_mail'] = true;
424
425
	foreach ($to_array as $to)
426
	{
427
		// Will this insert go over MySQL's limit?
428
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
429
430
		// Insert limit of 1M (just under the safety) is reached?
431
		if ($this_insert_len + $cur_insert_len > 1000000)
432
		{
433
			// Flush out what we have so far.
434
			$db->insert('',
435
				'{db_prefix}mail_queue',
436
				array(
437
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
438
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255',
439
				),
440
				$cur_insert,
441
				array('id_mail')
442
			);
443
444
			// Clear this out.
445
			$cur_insert = array();
446
			$cur_insert_len = 0;
447
		}
448
449
		// Now add the current insert to the array...
450
		$cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private, (string) $message_id);
451
		$cur_insert_len += $this_insert_len;
452
	}
453
454
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
455
	if (ELK === 'SSI')
456
	{
457
		return AddMailQueue(true);
458
	}
459
460
	return true;
461
}
462
463
/**
464
 * Prepare text strings for sending as email body or header.
465
 *
466
 * What it does:
467
 *
468
 * - In case there are higher ASCII characters in the given string, this
469
 * function will attempt the transport method 'quoted-printable'.
470
 * - Otherwise the transport method '7bit' is used.
471
 *
472
 * @param string $string
473
 * @param bool $with_charset = true
474
 * @param bool $hotmail_fix = false, with hotmail_fix set all higher ASCII
475
 * characters are converted to HTML entities to assure proper display of the mail
476
 * @param string $line_break
477
 * @param string|null $custom_charset = null, if set, it uses this character set
478
 * @return string[] an array containing the character set, the converted string and the transport method.
479
 * @package Mail
480 3
 */
481
function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
482
{
483 3
	$charset = $custom_charset !== null ? $custom_charset : 'UTF-8';
484
485
	// This is the fun part....
486
	if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
487
	{
488
		// Let's, for now, assume there are only &#021;'ish characters.
489
		$simple = true;
490
491
		foreach ($matches[1] as $entity)
492
		{
493
			if ($entity > 128)
494
			{
495
				$simple = false;
496
			}
497
		}
498
		unset($matches);
499
500
		if ($simple)
501
		{
502
			$string = preg_replace_callback('~&#(\d{3,7});~', 'mimespecialchars_callback', $string);
503
		}
504
		else
505
		{
506
			$string = preg_replace_callback('~&#(\d{3,7});~', 'fixchar__callback', $string);
507
508
			// Unicode, baby.
509
			$charset = 'UTF-8';
510
		}
511 3
	}
512
513
	// Convert all special characters to HTML entities...just for Hotmail :-\
514 3
	if ($hotmail_fix)
515
	{
516
		// Convert all 'special' characters to HTML entities.
517 3
		return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', 'entityConvert', $string), '7bit');
518
	}
519
	// We don't need to mess with the line if no special characters were in it..
520
	elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
521
	{
522
		// Base64 encode.
523
		$string = str_replace("\x00", '', $string);
524
		$string = base64_encode($string);
525
526
		$string = $with_charset
527
			? '=?' . $charset . '?B?' . $string . '?='
528
			: chunk_split($string, 76, $line_break);
529
530 3
		return array($charset, $string, 'base64');
531
	}
532
	else
533
	{
534
		return array($charset, $string, '7bit');
535
	}
536
}
537
538
/**
539
 * Converts out of ascii range characters in to HTML entities
540
 *
541
 * - Character codes <= 128 are left as is
542
 * - Callback function of preg_replace_callback, used just for hotmail address
543
 *
544
 * @param mixed[] $match
545
 *
546
 * @return mixed|string
547
 * @package Mail
548
 *
549
 */
550
function entityConvert($match)
551
{
552
	$c = $match[1];
553
	$c_strlen = strlen($c);
554
	$c_ord = ord($c[0]);
555
556
	if ($c_strlen === 1 && $c_ord <= 0x7F)
557
	{
558
		return $c;
559
	}
560
561
	if ($c_strlen === 2 && $c_ord >= 0xC0 && $c_ord <= 0xDF)
562
	{
563
		return '&#' . ((($c_ord ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ';';
564
	}
565
566
	if ($c_strlen === 3 && $c_ord >= 0xE0 && $c_ord <= 0xEF)
567
	{
568
		return '&#' . ((($c_ord ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ';';
569
	}
570
571
	if ($c_strlen === 4 && $c_ord >= 0xF0 && $c_ord <= 0xF7)
572
	{
573
		return '&#' . ((($c_ord ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ';';
574
	}
575
576
	return '';
577
}
578
579
/**
580
 * Callback for the preg_replace in mimespecialchars
581
 *
582
 * @param mixed[] $match
583
 *
584
 * @return string
585
 * @package Mail
586
 *
587
 */
588
function mimespecialchars_callback($match)
589
{
590
	return chr($match[1]);
591
}
592
593
/**
594
 * Sends mail, like mail() but over SMTP.
595
 *
596
 * - It expects no slashes or entities.
597
 *
598
 * @param string[] $mail_to_array - array of strings (email addresses)
599
 * @param string $subject email subject
600
 * @param string $message email message
601
 * @param string $headers
602
 * @param int $priority
603
 * @param string|null $message_id
604
 * @return bool whether it sent or not.
605
 * @throws \Exception
606
 * @internal
607
 * @package Mail
608
 */
609
function smtp_mail($mail_to_array, $subject, $message, $headers, $priority, $message_id = null)
610
{
611
	global $modSettings, $webmaster_email, $txt, $scripturl;
612
613
	$modSettings['smtp_host'] = trim($modSettings['smtp_host']);
614
615
	if ($message_id !== null && isset($message_id[0]) && in_array($message_id[0], array('m', 'p', 't')))
616
	{
617
		$message_type = $message_id[0];
618
		$message_id = substr($message_id, 1);
619
	}
620
	else
621
	{
622
		$message_type = 'm';
623
	}
624
625
	// Try POP3 before SMTP?
626
	// @todo There's no interface for this yet.
627
	if ($modSettings['mail_type'] == 2 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
628
	{
629
		$socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
630
		if (!$socket && (substr($modSettings['smtp_host'], 0, 5) === 'smtp.' || substr($modSettings['smtp_host'], 0, 11) === 'ssl://smtp.'))
631
		{
632
			$socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
633
		}
634
635
		if ($socket)
636
		{
637
			fgets($socket, 256);
638
			fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
639
			fgets($socket, 256);
640
			fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
641
			fgets($socket, 256);
642
			fputs($socket, 'QUIT' . "\r\n");
643
644
			fclose($socket);
645
		}
646
	}
647
648
	// Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
649
	if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
650
	{
651
		// Maybe we can still save this?  The port might be wrong.
652
		if (substr($modSettings['smtp_host'], 0, 4) === 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
653
		{
654
			if (($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3)))
655
			{
656
				\ElkArte\Errors\Errors::instance()->log_error($txt['smtp_port_ssl']);
657
			}
658
		}
659
660
		// Unable to connect!  Don't show any error message, but just log one and try to continue anyway.
661
		if (!$socket)
662
		{
663
			\ElkArte\Errors\Errors::instance()->log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
664
665
			return false;
666
		}
667
	}
668
669
	// Wait for a response of 220, without "-" continue.
670
	if (!server_parse(null, $socket, '220'))
671
	{
672
		return false;
673
	}
674
675
	// This should be set in the ACP
676
	if (empty($modSettings['smtp_client']))
677
	{
678
		$modSettings['smtp_client'] = detectServer()->getFQDN(empty($modSettings['smtp_host']) ? '' : $modSettings['smtp_host']);
679
		updateSettings(array('smtp_client' => $modSettings['smtp_client']));
680
	}
681
682
	if ($modSettings['mail_type'] == 1 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
683
	{
684
		// EHLO could be understood to mean encrypted hello...
685
		if (server_parse('EHLO ' . $modSettings['smtp_client'], $socket, null) == '250')
686
		{
687
			if (!empty($modSettings['smtp_starttls']))
688
			{
689
				server_parse('STARTTLS', $socket, null);
690
				stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
691
				server_parse('EHLO ' . $modSettings['smtp_client'], $socket, null);
692
			}
693
694
			if (!server_parse('AUTH LOGIN', $socket, '334'))
695
			{
696
				return false;
697
			}
698
			// Send the username and password, encoded.
699
			if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
700
			{
701
				return false;
702
			}
703
			// The password is already encoded ;)
704
			if (!server_parse($modSettings['smtp_password'], $socket, '235'))
705
			{
706
				return false;
707
			}
708
		}
709
		elseif (!server_parse('HELO ' . $modSettings['smtp_client'], $socket, '250'))
710
		{
711
			return false;
712
		}
713
	}
714
	else
715
	{
716
		// Just say "helo".
717
		if (!server_parse('HELO ' . $modSettings['smtp_client'], $socket, '250'))
718
		{
719
			return false;
720
		}
721
	}
722
723
	// Fix the message for any lines beginning with a period! (the first is ignored, you see.)
724
	$message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
725
726
	$sent = array();
727
	$need_break = substr($headers, -1) === "\n" || substr($headers, -1) === "\r" ? false : true;
728
	$real_headers = $headers;
729
	$line_break = "\r\n";
730
731
	// !! Theoretically, we should be able to just loop the RCPT TO.
732
	$mail_to_array = array_values($mail_to_array);
733
	foreach ($mail_to_array as $i => $mail_to)
734
	{
735
		// the keys are must unique for every mail you see
736
		$unq_id = '';
737
		$unq_head = '';
738
		$unq_head_array = array();
739
740
		// Using the post by email functions, and not a digest (priority 4)
741
		// then generate "reply to mail" keys and place them in the message
742
		if (!empty($modSettings['maillist_enabled']) && !empty($message_id) && $priority != 4)
743
		{
744
			$unq_head_array[0] = md5($scripturl . microtime() . rand());
745
			$unq_head_array[1] = $message_type;
746
			$unq_head_array[2] = $message_id;
747
			$unq_head = $unq_head_array[0] . '-' . $unq_head_array[1] . $unq_head_array[2];
748
			$unq_id = ($need_break ? $line_break : '') . 'Message-ID: <' . $unq_head . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>';
749
			$message = mail_insert_key($message, $unq_head, $line_break);
750
		}
751
752
		// Fix up the headers for this email!
753
		$headers = $real_headers . $unq_id;
754
755
		// Reset the connection to send another email.
756
		if ($i !== 0)
757
		{
758
			if (!server_parse('RSET', $socket, '250'))
759
			{
760
				return false;
761
			}
762
		}
763
764
		// From, to, and then start the data...
765
		if (!server_parse('MAIL FROM: <' . (empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from']) . '>', $socket, '250'))
766
		{
767
			return false;
768
		}
769
770
		if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
771
		{
772
			return false;
773
		}
774
775
		if (!server_parse('DATA', $socket, '354'))
776
		{
777
			return false;
778
		}
779
780
		fputs($socket, 'Subject: ' . $subject . $line_break);
781
		if (strlen($mail_to) > 0)
782
		{
783
			fputs($socket, 'To: <' . $mail_to . '>' . $line_break);
784
		}
785
		fputs($socket, $headers . $line_break . $line_break);
786
		fputs($socket, $message . $line_break);
787
788
		// Send a ., or in other words "end of data".
789
		if (!server_parse('.', $socket, '250'))
790
		{
791
			return false;
792
		}
793
794
		// track the number of emails sent
795
		if (!empty($modSettings['trackStats']))
796
		{
797
			trackStats(array('email' => '+'));
798
		}
799
800
		// Keep our post via email log
801
		if (!empty($unq_head))
802
		{
803
			$unq_head_array[] = time();
804
			$unq_head_array[] = $mail_to;
805
			$sent[] = $unq_head_array;
806
		}
807
808
		// Almost done, almost done... don't stop me just yet!
809
		detectServer()->setTimeLimit(300);
810
	}
811
812
	// say our goodbyes
813
	fputs($socket, 'QUIT' . $line_break);
814
	fclose($socket);
815
816
	// Log each email
817
	if (!empty($sent))
818
	{
819
		require_once(SUBSDIR . '/Maillist.subs.php');
820
		log_email($sent);
821
	}
822
823
	return true;
824
}
825
826
/**
827
 * Parse a message to the SMTP server.
828
 *
829
 * - Sends the specified message to the server, and checks for the expected response.
830
 *
831
 * @param string $message - the message to send
832
 * @param resource $socket - socket to send on
833
 * @param string $response - the expected response code
834
 * @return string|bool it responded as such.
835
 * @throws \Exception
836
 * @internal
837
 * @package Mail
838
 */
839
function server_parse($message, $socket, $response)
840
{
841
	global $txt;
842
843
	if ($message !== null)
844
	{
845
		fputs($socket, $message . "\r\n");
846
	}
847
848
	// No response yet.
849
	$server_response = '';
850
851
	while (substr($server_response, 3, 1) !== ' ')
852
	{
853
		if (!($server_response = fgets($socket, 256)))
854
		{
855
			// @todo Change this message to reflect that it may mean bad user/password/server issues/etc.
856
			\ElkArte\Errors\Errors::instance()->log_error($txt['smtp_bad_response']);
857
858
			return false;
859
		}
860
	}
861
862
	if ($response === null)
863
	{
864
		return substr($server_response, 0, 3);
865
	}
866
867
	if (substr($server_response, 0, 3) !== $response)
868
	{
869
		\ElkArte\Errors\Errors::instance()->log_error($txt['smtp_error'] . $server_response);
870
871
		return false;
872
	}
873
874
	return true;
875
}
876
877
/**
878
 * Adds the unique security key in to an email
879
 *
880
 * - adds the key in to (each) message body section
881
 * - safety net for clients that strip out the message-id and in-reply-to headers
882
 *
883
 * @param string $message
884
 * @param string $unq_head
885
 * @param string $line_break
886
 *
887
 * @return mixed|null|string|string[]
888
 * @package Mail
889
 *
890
 */
891
function mail_insert_key($message, $unq_head, $line_break)
892
{
893
	// Append the key to the bottom of each message section, plain, html, encoded, etc
894
	$message = preg_replace('~^(.*?)(' . $line_break . '--ELK-[a-z0-9]{32})~s', "$1{$line_break}{$line_break}[{$unq_head}]{$line_break}$2", $message);
895
	$message = preg_replace('~(Content-Type: text/plain;.*?Content-Transfer-Encoding: 7bit' . $line_break . $line_break . ')(.*?)(' . $line_break . '--ELK-[a-z0-9]{32})~s', "$1$2{$line_break}{$line_break}[{$unq_head}]{$line_break}$3", $message);
896
	$message = preg_replace('~(Content-Type: text/html;.*?Content-Transfer-Encoding: 7bit' . $line_break . $line_break . ')(.*?)(' . $line_break . '--ELK-[a-z0-9]{32})~s', "$1$2<br /><br />[{$unq_head}]<br />$3", $message);
897
898
	// base64 the harder one to insert our key
899
	// Find the sections, un-do the chunk_split, add in the new key, and re chunky it
900
	if (preg_match('~(Content-Transfer-Encoding: base64' . $line_break . $line_break . ')(.*?)(' . $line_break . '--ELK-[a-z0-9]{32})~s', $message, $match))
901
	{
902
		// un-chunk, add in our encoded key header, and re chunk, all so we match RFC 2045 semantics.
903
		$encoded_message = base64_decode(str_replace($line_break, '', $match[2]));
904
		$encoded_message .= $line_break . $line_break . '[' . $unq_head . ']' . $line_break;
905
		$encoded_message = base64_encode($encoded_message);
906
		$encoded_message = chunk_split($encoded_message, 76, $line_break);
907
		$message = str_replace($match[2], $encoded_message, $message);
908
	}
909
910
	return $message;
911
}
912
913
/**
914
 * Load a template from EmailTemplates language file.
915
 *
916
 * @param string $template
917
 * @param mixed[] $replacements
918
 * @param string $lang = ''
919 3
 * @param bool $loadLang = true
920
 * @param string[] $suffixes - Additional suffixes to find and return
921
 * @param string[] $additional_files - Additional language files to load
922 3
 *
923
 * @return array
924 3
 * @throws \ElkArte\Exceptions\Exception email_no_template
925 3
 * @package Mail
926
 *
927
 */
928
function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true, $suffixes = array(), $additional_files = array())
929
{
930 3
	global $txt, $mbname, $scripturl, $settings, $boardurl, $modSettings;
931
932
	// First things first, load up the email templates language file, if we need to.
933
	if ($loadLang)
934
	{
935
		$lang_loader = new LangLoader($lang);
936
		$lang_loader->load('EmailTemplates');
937
		if (!empty($modSettings['maillist_enabled']))
938
		{
939 3
			$lang_loader->load('MaillistTemplates');
940
		}
941
942
		if (!empty($additional_files))
943
		{
944
			foreach ($additional_files as $file)
945 3
			{
946 3
				$lang_loader->load($file);
947
			}
948 3
		}
949
	}
950
951
	if (!isset($txt[$template . '_subject']) || !isset($txt[$template . '_body']))
952
	{
953
		throw new \ElkArte\Exceptions\Exception('email_no_template', 'template', array($template));
954
	}
955
956
	$ret = array(
957
		'subject' => $txt[$template . '_subject'],
958 3
		'body' => $txt[$template . '_body'],
959 3
	);
960 3
	if (!empty($suffixes))
961 3
	{
962 3
		foreach ($suffixes as $key)
963 3
		{
964 3
			$ret[$key] = $txt[$template . '_' . $key];
965 3
		}
966 3
	}
967
968
	// Add in the default replacements.
969
	$replacements += array(
970 3
		'FORUMNAME' => $mbname,
971 3
		'FORUMNAMESHORT' => (!empty($modSettings['maillist_sitename']) ? $modSettings['maillist_sitename'] : $mbname),
972
		'EMAILREGARDS' => (!empty($modSettings['maillist_sitename_regards']) ? $modSettings['maillist_sitename_regards'] : ''),
973 3
		'FORUMURL' => $boardurl,
974
		'SCRIPTURL' => $scripturl,
975 3
		'THEMEURL' => $settings['theme_url'],
976 3
		'IMAGESURL' => $settings['images_url'],
977
		'DEFAULT_THEMEURL' => $settings['default_theme_url'],
978
		'REGARDS' => replaceBasicActionUrl($txt['regards_team']),
979
	);
980 3
981
	// Split the replacements up into two arrays, for use with str_replace
982 3
	$find = array();
983
	$replace = array();
984 3
985
	foreach ($replacements as $f => $r)
986
	{
987
		$find[] = '{' . $f . '}';
988 3
		$replace[] = $r;
989
	}
990
991
	// Do the variable replacements.
992
	foreach ($ret as $key => $val)
993
	{
994
		$val = str_replace($find, $replace, $val);
995
		// Now deal with the {USER.variable} items.
996
		$ret[$key] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $val);
997
	}
998
999
	// Finally return the email to the caller so they can send it out.
1000
	return $ret;
1001
}
1002
1003
/**
1004
 * Prepare subject and message of an email for the preview box
1005
 *
1006
 * Used in action_mailingcompose and RetrievePreview (Xml.controller.php)
1007
 *
1008
 * @package Mail
1009
 */
1010
function prepareMailingForPreview()
1011
{
1012
	global $context, $modSettings, $scripturl, $txt;
1013
1014
	ThemeLoader::loadLanguageFile('Errors');
1015
	require_once(SUBSDIR . '/Post.subs.php');
1016
1017
	$processing = array(
1018
		'preview_subject' => 'subject',
1019
		'preview_message' => 'message'
1020
	);
1021
1022
	// Use the default time format.
1023
	User::$info->time_format = $modSettings['time_format'];
1024
1025
	$variables = array(
1026
		'{$board_url}',
1027
		'{$current_time}',
1028
		'{$latest_member.link}',
1029
		'{$latest_member.id}',
1030
		'{$latest_member.name}'
1031
	);
1032
1033
	$html = $context['send_html'];
1034
1035
	// We might need this in a bit
1036
	$cleanLatestMember = empty($context['send_html']) || $context['send_pm'] ? un_htmlspecialchars($modSettings['latestRealName']) : $modSettings['latestRealName'];
1037
1038
	$bbc_parser = ParserWrapper::instance();
1039
1040
	foreach ($processing as $key => $post)
1041
	{
1042
		$context[$key] = !empty($_REQUEST[$post]) ? $_REQUEST[$post] : '';
1043
1044
		if (empty($context[$key]) && empty($_REQUEST['xml']))
1045
		{
1046
			$context['post_error']['messages'][] = $txt['error_no_' . $post];
1047
		}
1048
		elseif (!empty($_REQUEST['xml']))
1049
		{
1050
			continue;
1051
		}
1052
1053
		preparsecode($context[$key]);
1054
1055
		// Sending as html then we convert any bbc
1056
		if ($html)
1057
		{
1058
			$enablePostHTML = $modSettings['enablePostHTML'];
1059
			$modSettings['enablePostHTML'] = $context['send_html'];
1060
			$context[$key] = $bbc_parser->parseEmail($context[$key]);
1061
			$modSettings['enablePostHTML'] = $enablePostHTML;
1062
		}
1063
1064
		// Replace in all the standard things.
1065
		$context[$key] = str_replace($variables,
1066
			array(
1067
				!empty($context['send_html']) ? '<a href="' . $scripturl . '">' . $scripturl . '</a>' : $scripturl,
1068
				standardTime(forum_time(), false),
1069
				!empty($context['send_html']) ? '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $cleanLatestMember . '</a>' : ($context['send_pm'] ? '[url=' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . ']' . $cleanLatestMember . '[/url]' : $cleanLatestMember),
1070
				$modSettings['latestMember'],
1071
				$cleanLatestMember
1072
			), $context[$key]);
1073
	}
1074
}
1075
1076
/**
1077
 * Callback function for load email template on subject and body
1078
 * Uses capture group 1 in array
1079
 *
1080
 * @param mixed[] $matches
1081
 * @return string
1082
 * @package Mail
1083
 */
1084
function user_info_callback($matches)
1085
{
1086
	if (empty($matches[1]))
1087
	{
1088
		return '';
1089
	}
1090
1091
	$use_ref = true;
1092
	$ref = User::$info->toArray();
1093
1094
	foreach (explode('.', $matches[1]) as $index)
1095
	{
1096
		if ($use_ref && isset($ref[$index]))
1097
		{
1098
			$ref = &$ref[$index];
1099
		}
1100
		else
1101
		{
1102
			$use_ref = false;
1103
			break;
1104
		}
1105
	}
1106
1107
	return $use_ref ? $ref : $matches[0];
1108
}
1109
1110
/**
1111
 * This function grabs the mail queue items from the database, according to the params given.
1112
 *
1113
 * @param int $start The item to start with (for pagination purposes)
1114
 * @param int $items_per_page The number of items to show per page
1115
 * @param string $sort A string indicating how to sort the results
1116
 * @return array
1117
 * @throws \Exception
1118
 * @package Mail
1119
 */
1120
function list_getMailQueue($start, $items_per_page, $sort)
1121
{
1122
	global $txt;
1123
1124
	$db = database();
1125
1126
	return $db->fetchQuery('
1127
		SELECT
1128
			id_mail, time_sent, recipient, priority, private, subject
1129
		FROM {db_prefix}mail_queue
1130
		ORDER BY {raw:sort}
1131
		LIMIT {int:start}, {int:items_per_page}',
1132
		array(
1133
			'start' => $start,
1134
			'sort' => $sort,
1135
			'items_per_page' => $items_per_page,
1136
		)
1137
	)->fetch_callback(
1138
		function ($row) use ($txt) {
1139
			// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
1140
			if (!empty($row['private']))
1141
			{
1142
				$row['subject'] = $txt['personal_message'];
1143
			}
1144
1145
			return $row;
1146
		}
1147
	);
1148
}
1149
1150
/**
1151
 * Returns the total count of items in the mail queue.
1152
 *
1153
 * @return int
1154
 * @throws \ElkArte\Exceptions\Exception
1155
 * @package Mail
1156
 */
1157
function list_getMailQueueSize()
1158
{
1159
	$db = database();
1160
1161
	// How many items do we have?
1162
	$request = $db->query('', '
1163
		SELECT COUNT(*) AS queue_size
1164
		FROM {db_prefix}mail_queue',
1165
		array()
1166
	);
1167
	list ($mailQueueSize) = $request->fetch_row();
1168
	$request->free_result();
1169
1170
	return $mailQueueSize;
1171
}
1172
1173
/**
1174
 * Deletes items from the mail queue
1175
 *
1176
 * @param int[] $items
1177
 * @throws \ElkArte\Exceptions\Exception
1178
 * @package Mail
1179
 */
1180
function deleteMailQueueItems($items)
1181
{
1182
	$db = database();
1183
1184
	$db->query('', '
1185
		DELETE FROM {db_prefix}mail_queue
1186
		WHERE id_mail IN ({array_int:mail_ids})',
1187
		array(
1188
			'mail_ids' => $items,
1189
		)
1190
	);
1191
}
1192
1193
/**
1194
 * Get the current mail queue status
1195
 *
1196
 * @package Mail
1197
 */
1198
function list_MailQueueStatus()
1199
{
1200
	$db = database();
1201
1202
	$items = array();
1203
1204
	// How many items do we have?
1205
	$request = $db->query('', '
1206
		SELECT COUNT(*) AS queue_size, MIN(time_sent) AS oldest
1207
		FROM {db_prefix}mail_queue',
1208
		array()
1209
	);
1210
	list ($items['mailQueueSize'], $items['mailOldest']) = $request->fetch_row();
1211
	$request->free_result();
1212
1213
	return $items;
1214
}
1215
1216
/**
1217
 * This function handles updates to account for failed emails.
1218
 *
1219
 * - It is used to keep track of failed emails attempts and next try.
1220
 *
1221
 * @param mixed[] $failed_emails
1222
 * @throws \ElkArte\Exceptions\Exception
1223
 * @package Mail
1224
 */
1225
function updateFailedQueue($failed_emails)
1226
{
1227
	global $modSettings;
1228
1229
	$db = database();
1230
1231
	// Update the failed attempts check.
1232
	$db->replace(
1233
		'{db_prefix}settings',
1234
		array('variable' => 'string', 'value' => 'string'),
1235
		array('mail_failed_attempts', empty($modSettings['mail_failed_attempts']) ? 1 : ++$modSettings['mail_failed_attempts']),
1236
		array('variable')
1237
	);
1238
1239
	// If we have failed to many times, tell mail to wait a bit and try again.
1240
	if ($modSettings['mail_failed_attempts'] > 5)
1241
	{
1242
		$db->query('', '
1243
			UPDATE {db_prefix}settings
1244
			SET value = {string:next_mail_send}
1245
			WHERE variable = {string:mail_next_send}
1246
				AND value = {string:last_send}',
1247
			array(
1248
				'next_mail_send' => time() + 60,
1249
				'mail_next_send' => 'mail_next_send',
1250
				'last_send' => $modSettings['mail_next_send'],
1251
			)
1252
		);
1253
	}
1254
1255
	// Add our email back to the queue, manually.
1256
	$db->insert('insert',
1257
		'{db_prefix}mail_queue',
1258
		array('time_sent' => 'int', 'recipient' => 'string', 'body' => 'string', 'subject' => 'string', 'headers' => 'string', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255'),
1259
		$failed_emails,
1260
		array('id_mail')
1261
	);
1262
}
1263
1264
/**
1265
 * Updates the failed attempts to email in the database.
1266
 *
1267
 * - It sets mail failed attempts value to 0.
1268
 *
1269
 * @package Mail
1270
 */
1271
function updateSuccessQueue()
1272
{
1273
	$db = database();
1274
1275
	$db->query('', '
1276
		UPDATE {db_prefix}settings
1277
		SET value = {string:zero}
1278
		WHERE variable = {string:mail_failed_attempts}',
1279
		array(
1280
			'zero' => '0',
1281
			'mail_failed_attempts' => 'mail_failed_attempts',
1282
		)
1283
	);
1284
}
1285
1286
/**
1287
 * Reset to 0 the next send time for emails queue.
1288
 */
1289
function resetNextSendTime()
1290
{
1291
	global $modSettings;
1292
1293
	$db = database();
1294
1295
	// Update the setting to zero, yay
1296
	// ...unless someone else did.
1297
	$db->query('', '
1298
		UPDATE {db_prefix}settings
1299
		SET value = {string:no_send}
1300
		WHERE variable = {string:mail_next_send}
1301
			AND value = {string:last_mail_send}',
1302
		array(
1303
			'no_send' => '0',
1304
			'mail_next_send' => 'mail_next_send',
1305
			'last_mail_send' => $modSettings['mail_next_send'],
1306
		)
1307
	);
1308
}
1309
1310
/**
1311
 * Update the next sending time for mail queue.
1312
 *
1313
 * - By default, move it 10 seconds for lower per mail_period_limits and 5 seconds for larger mail_period_limits
1314
 * - Requires an affected row
1315
 *
1316
 * @return int|bool
1317
 * @throws \ElkArte\Exceptions\Exception
1318
 * @package Mail
1319
 */
1320
function updateNextSendTime()
1321
{
1322
	global $modSettings;
1323
1324
	$db = database();
1325
1326
	// Set a delay based on the per minute limit (mail_period_limit)
1327
	$delay = !empty($modSettings['mail_queue_delay']) ? $modSettings['mail_queue_delay'] : (!empty($modSettings['mail_period_limit']) && $modSettings['mail_period_limit'] <= 5 ? 10 : 5);
1328
1329
	$request = $db->query('', '
1330
		UPDATE {db_prefix}settings
1331
		SET value = {string:next_mail_send}
1332
		WHERE variable = {string:mail_next_send}
1333
			AND value = {string:last_send}',
1334
		array(
1335
			'next_mail_send' => time() + $delay,
1336
			'mail_next_send' => 'mail_next_send',
1337
			'last_send' => $modSettings['mail_next_send'],
1338
		)
1339
	);
1340
	if ($request->affected_rows() === 0)
1341
	{
1342
		return false;
1343
	}
1344
1345
	return (int) $delay;
1346
}
1347
1348
/**
1349
 * Retrieve all details from the database on the next emails.
1350
 *
1351
 * @param int $number
1352
 * @return array
1353
 * @throws \Exception
1354
 * @package Mail
1355
 */
1356
function emailsInfo($number)
1357
{
1358
	$db = database();
1359
	$ids = array();
1360
	$emails = array();
1361
1362
	// Get the next $number emails, with all that's to know about them and one more.
1363
	$db->fetchQuery('
1364
		SELECT /*!40001 SQL_NO_CACHE */ id_mail, recipient, body, subject, headers, send_html, time_sent, priority, private, message_id
1365
		FROM {db_prefix}mail_queue
1366
		ORDER BY priority ASC, id_mail ASC
1367
		LIMIT ' . $number,
1368
		array()
1369
	)->fetch_callback(
1370
		function ($row) use (&$ids, &$emails) {
1371
			// Just get the data and go.
1372
			$ids[] = $row['id_mail'];
1373
			$emails[] = array(
1374
				'to' => $row['recipient'],
1375
				'body' => $row['body'],
1376
				'subject' => $row['subject'],
1377
				'headers' => $row['headers'],
1378
				'send_html' => $row['send_html'],
1379
				'time_sent' => $row['time_sent'],
1380
				'priority' => $row['priority'],
1381
				'private' => $row['private'],
1382
				'message_id' => $row['message_id'],
1383
			);
1384
		}
1385
	);
1386
1387
	return array($ids, $emails);
1388
}
1389
1390
/**
1391
 * Sends a group of emails from the mail queue.
1392
 *
1393
 * - Allows a batch of emails to be released every 5 to 10 seconds (based on per period limits)
1394
 * - If batch size is not set, will determine a size such that it sends in 1/2 the period (buffer)
1395
 *
1396
 * @param int|bool $batch_size = false the number to send each loop
1397
 * @param bool $override_limit = false bypassing our limit flaf
1398
 * @param bool $force_send = false
1399
 * @return bool
1400
 * @package Mail
1401
 */
1402
function reduceMailQueue($batch_size = false, $override_limit = false, $force_send = false)
1403
{
1404
	global $modSettings, $webmaster_email, $scripturl;
1405
1406
	// Do we have another script to send out the queue?
1407
	if (!empty($modSettings['mail_queue_use_cron']) && empty($force_send))
1408
	{
1409
		return false;
1410
	}
1411
1412
	// How many emails can we send each time we are called in a period
1413
	if (!$batch_size)
1414
	{
1415
		// Batch size has been set in the ACP, use it
1416
		if (!empty($modSettings['mail_batch_size']))
1417
		{
1418
			$batch_size = $modSettings['mail_batch_size'];
1419
		}
1420
		// No per period setting or batch size, set to send 5 every 5 seconds, or 60 per minute
1421
		elseif (empty($modSettings['mail_period_limit']))
1422
		{
1423
			$batch_size = 5;
1424
		}
1425
		// A per period limit but no defined batch size, set a batch size
1426
		else
1427
		{
1428
			// Based on the number of times we will potentially be called each minute
1429
			$delay = !empty($modSettings['mail_queue_delay']) ? $modSettings['mail_queue_delay'] : (!empty($modSettings['mail_period_limit']) && $modSettings['mail_period_limit'] <= 5 ? 10 : 5);
1430
			$batch_size = ceil($modSettings['mail_period_limit'] / ceil(60 / $delay));
1431
			$batch_size = ($batch_size == 1 && $modSettings['mail_period_limit'] > 1) ? 2 : $batch_size;
1432
		}
1433
	}
1434
1435
	// If we came with a timestamp, and that doesn't match the next event, then someone else has beaten us.
1436
	if (isset($_GET['ts']) && $_GET['ts'] != $modSettings['mail_next_send'] && empty($force_send))
1437
	{
1438
		return false;
1439
	}
1440
1441
	// Prepare to send each email, and log that for future proof.
1442
	require_once(SUBSDIR . '/Maillist.subs.php');
1443
1444
	// Set the delay for the next sending
1445
	$delay = 0;
1446
	if (!$override_limit)
1447
	{
1448
		// Update next send time for our mail queue, if there was something to update. Otherwise bail out :P
1449
		$delay = updateNextSendTime();
1450
		if ($delay === false)
1451
		{
1452
			return false;
1453
		}
1454
1455
		$modSettings['mail_next_send'] = time() + $delay;
1456
	}
1457
1458
	// If we're not overriding, do we have quota left in this mail period limit?
1459
	if (!$override_limit && !empty($modSettings['mail_period_limit']))
1460
	{
1461
		// See if we have quota left to send another batch_size this minute or if we have to wait
1462
		list ($mail_time, $mail_number) = isset($modSettings['mail_recent']) ? explode('|', $modSettings['mail_recent']) : array(0, 0);
1463
1464
		// Nothing worth noting...
1465
		if (empty($mail_number) || $mail_time < time() - 60)
1466
		{
1467
			$mail_time = time();
1468
			$mail_number = $batch_size;
1469
		}
1470
		// Otherwise we may still have quota to send a few more?
1471
		elseif ($mail_number < $modSettings['mail_period_limit'])
1472
		{
1473
			// If this is likely one of the last cycles for this period, then send any remaining quota
1474
			if (($mail_time - (time() - 60)) < $delay * 2)
1475
			{
1476
				$batch_size = $modSettings['mail_period_limit'] - $mail_number;
1477
			}
1478
			// Some batch sizes may need to be adjusted to fit as we approach the end
1479
			elseif ($mail_number + $batch_size > $modSettings['mail_period_limit'])
1480
			{
1481
				$batch_size = $modSettings['mail_period_limit'] - $mail_number;
1482
			}
1483
1484
			$mail_number += $batch_size;
1485
		}
1486
		// No more I'm afraid, return!
1487
		else
1488
		{
1489
			return false;
1490
		}
1491
1492
		// Reflect that we're about to send some, do it now to be safe.
1493
		updateSettings(array('mail_recent' => $mail_time . '|' . $mail_number));
1494
	}
1495
1496
	// Now we know how many we're sending, let's send them.
1497
	list ($ids, $emails) = emailsInfo($batch_size);
1498
1499
	// Delete, delete, delete!!!
1500
	if (!empty($ids))
1501
	{
1502
		deleteMailQueueItems($ids);
1503
	}
1504
1505
	// Don't believe we have any left after this batch?
1506
	if (count($ids) < $batch_size)
1507
	{
1508
		resetNextSendTime();
1509
	}
1510
1511
	if (empty($ids))
1512
	{
1513
		return false;
1514
	}
1515
1516
	// We have some to send, lets send them!
1517
	$sent = array();
1518
	$failed_emails = array();
1519
1520
	// Use sendmail or SMTP
1521
	$use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
1522
1523
	// Line breaks need to be \r\n only in windows or for SMTP.
1524
	$line_break = detectServer()->is('windows') || !$use_sendmail ? "\r\n" : "\n";
1525
1526
	foreach ($emails as $key => $email)
1527
	{
1528
		// So that we have it, just in case it's really needed - see #2245
1529
		$email['body_fail'] = $email['body'];
1530
		if ($email['message_id'] !== null && isset($email['message_id'][0]) && in_array($email['message_id'][0], array('m', 'p', 't')))
1531
		{
1532
			$email['message_type'] = $email['message_id'][0];
1533
			$email['message_id'] = substr($email['message_id'], 1);
1534
		}
1535
		else
1536
		{
1537
			$email['message_type'] = 'm';
1538
		}
1539
1540
		// Use the right mail resource
1541
		if ($use_sendmail)
1542
		{
1543
			$email['subject'] = strtr($email['subject'], array("\r" => '', "\n" => ''));
1544
			$email['body_fail'] = $email['body'];
1545
1546
			if (!empty($modSettings['mail_strip_carriage']))
1547
			{
1548
				$email['body'] = strtr($email['body'], array("\r" => ''));
1549
				$email['headers'] = strtr($email['headers'], array("\r" => ''));
1550
			}
1551
1552
			// See the assignment 10 lines before this and #2245 - repeated here for the strtr
1553
			$email['body_fail'] = $email['body'];
1554
			$need_break = substr($email['headers'], -1) === "\n" || substr($email['headers'], -1) === "\r" ? false : true;
1555
1556
			// Create our unique reply to email header if this message needs one
1557
			$unq_id = '';
1558
			$unq_head = '';
1559
			$unq_head_array = array();
1560
			if (!empty($modSettings['maillist_enabled']) && $email['message_id'] !== null && strpos($email['headers'], 'List-Id: <') !== false)
1561
			{
1562
				$unq_head_array[0] = md5($scripturl . microtime() . rand());
1563
				$unq_head_array[1] = $email['message_type'];
1564
				$unq_head_array[2] = $email['message_id'];
1565
				$unq_head = $unq_head_array[0] . '-' . $unq_head_array[1] . $unq_head_array[2];
1566
				$unq_id = ($need_break ? $line_break : '') . 'Message-ID: <' . $unq_head . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>';
1567
				$email['body'] = mail_insert_key($email['body'], $unq_head, $line_break);
1568
			}
1569
			elseif ($email['message_id'] !== null && empty($modSettings['mail_no_message_id']))
1570
			{
1571
				$unq_id = ($need_break ? $line_break : '') . 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $email['message_id'] . strstr(empty($modSettings['maillist_mail_from']) ? $webmaster_email : $modSettings['maillist_mail_from'], '@') . '>';
1572
			}
1573
1574
			// No point logging a specific error here, as we have no language. PHP error is helpful anyway...
1575
			$result = mail(strtr($email['to'], array("\r" => '', "\n" => '')), $email['subject'], $email['body'], $email['headers'] . $unq_id);
1576
1577
			// If it sent, keep a record so we can save it in our allowed to reply log
1578
			if (!empty($unq_head) && $result)
1579
			{
1580
				$sent[] = array_merge($unq_head_array, array(time(), $email['to']));
1581
			}
1582
1583
			// Track total emails sent
1584
			if ($result && !empty($modSettings['trackStats']))
1585
			{
1586
				trackStats(array('email' => '+'));
1587
			}
1588
1589
			// Try to stop a timeout, this would be bad...
1590
			detectServer()->setTimeLimit(300);
1591
		}
1592
		else
1593
		{
1594
			$result = smtp_mail(array($email['to']), $email['subject'], $email['body'], $email['headers'], $email['priority'], $email['message_type'] . $email['message_id']);
1595
		}
1596
1597
		// Hopefully it sent?
1598
		if (!$result)
1599
		{
1600
			$failed_emails[] = array(time(), $email['to'], $email['body_fail'], $email['subject'], $email['headers'], $email['send_html'], $email['priority'], $email['private'], $email['message_id']);
1601
		}
1602
	}
1603
1604
	// Clear out the stat cache.
1605
	trackStats();
1606
1607
	// Log each of the sent emails.
1608
	if (!empty($sent))
1609
	{
1610
		log_email($sent);
1611
	}
1612
1613
	// Any emails that didn't send?
1614
	if (!empty($failed_emails))
1615
	{
1616
		// If it failed, add it back to the queue
1617
		updateFailedQueue($failed_emails);
1618
1619
		return false;
1620
	}
1621
	// We were able to send the email, clear our failed attempts.
1622
	elseif (!empty($modSettings['mail_failed_attempts']))
1623
	{
1624
		updateSuccessQueue();
1625
	}
1626
1627
	// Had something to send...
1628
	return true;
1629
}
1630
1631
/**
1632
 * This function finds email address and few other details of the
1633
 * poster of a certain message.
1634 2
 *
1635
 * @param int $id_msg the id of a message
1636 2
 * @param int $topic_id the topic the message belongs to
1637
 * @return mixed[] the poster's details
1638
 * @throws \ElkArte\Exceptions\Exception
1639
 * @todo very similar to mailFromMessage
1640
 * @package Mail
1641
 */
1642
function posterDetails($id_msg, $topic_id)
1643
{
1644
	$db = database();
1645 2
1646 2
	$request = $db->query('', '
1647
		SELECT 
1648
			m.id_msg, m.id_topic, m.id_board, m.subject, m.body, m.id_member AS id_poster, m.poster_name, mem.real_name
1649 2
		FROM {db_prefix}messages AS m
1650 2
			LEFT JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member)
1651
		WHERE m.id_msg = {int:id_msg}
1652 2
			AND m.id_topic = {int:current_topic}
1653
		LIMIT 1',
1654
		array(
1655
			'current_topic' => $topic_id,
1656
			'id_msg' => $id_msg,
1657
		)
1658
	);
1659
	$message = $request->fetch_assoc();
1660
	$request->free_result();
1661
1662
	return $message;
1663
}
1664
1665
/**
1666
 * Little utility function to calculate how long ago a time was.
1667
 *
1668
 * @param int|double $time_diff
1669
 * @return string
1670
 * @package Mail
1671
 */
1672
function time_since($time_diff)
1673
{
1674
	global $txt;
1675
1676
	if ($time_diff < 0)
1677
	{
1678
		$time_diff = 0;
1679
	}
1680
1681
	// Just do a bit of an if fest...
1682
	if ($time_diff > 86400)
1683
	{
1684
		$days = round($time_diff / 86400, 1);
1685
1686
		return sprintf($days == 1 ? $txt['mq_day'] : $txt['mq_days'], $time_diff / 86400);
1687
	}
1688
	// Hours?
1689
	elseif ($time_diff > 3600)
1690
	{
1691
		$hours = round($time_diff / 3600, 1);
1692
1693
		return sprintf($hours == 1 ? $txt['mq_hour'] : $txt['mq_hours'], $hours);
1694
	}
1695
	// Minutes?
1696
	elseif ($time_diff > 60)
1697
	{
1698
		$minutes = (int) ($time_diff / 60);
1699
1700
		return sprintf($minutes === 1 ? $txt['mq_minute'] : $txt['mq_minutes'], $minutes);
1701
	}
1702
	// Otherwise must be second
1703
	else
1704
	{
1705
		return sprintf($time_diff == 1 ? $txt['mq_second'] : $txt['mq_seconds'], $time_diff);
1706
	}
1707
}
1708