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

AddMailQueue()   B

Complexity

Conditions 10
Paths 44

Size

Total Lines 95
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 43
nc 44
nop 9
dl 0
loc 95
ccs 0
cts 38
cp 0
crap 110
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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