Issues (1686)

sources/subs/Mail.subs.php (3 issues)

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\Languages\Loader as LangLoader;
20
use ElkArte\Languages\Txt;
21
use ElkArte\Mail\BuildMail;
22
use ElkArte\Mail\QueueMail;
23
use ElkArte\User;
24
25
/**
26
 * This function sends an email to the specified recipient(s).
27
 *
28
 * It uses the mail_type settings and webmaster_email variable.
29
 *
30
 * @param string[]|string $to - the email(s) to send to
31
 * @param string $subject - email subject, expected to have entities, and slashes, but not be parsed
32
 * @param string $message - email body, expected to have slashes, no htmlentities
33
 * @param string|null $from = null - the address to use for replies
34
 * @param string|null $message_id = null - if specified, it will be used as local part of the Message-ID header.
35
 * @param bool $send_html = false, whether the message is HTML vs. plain text
36
 * @param int $priority = 3 Primarily used for queue priority.  0 = send now, >3 = no PBE
37
 * @param bool|null $hotmail_fix = null  ** No longer used, left only for old function calls **
38
 * @param bool $is_private - Hides to/from names when viewing the mail queue
39
 * @param string|null $from_wrapper - used to provide envelope from wrapper based on if we share users display name
40
 * @param int|null $reference - The parent topic id for use in a References header
41
 * @return bool whether the email was accepted properly.
42
 * @package Mail
43 3
 */
44
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)
0 ignored issues
show
The parameter $hotmail_fix is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

44
function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, /** @scrutinizer ignore-unused */ $hotmail_fix = null, $is_private = false, $from_wrapper = null, $reference = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
45
{
46 3
	// Pass this on to the buildEmail and sendMail functions
47
	return (new BuildMail())->buildEmail($to, $subject, $message, $from, $message_id, $send_html, $priority, $is_private, $from_wrapper, $reference);
48
}
49 3
50
/**
51
 * Add an email to the mail queue.
52 3
 *
53
 * @param bool $flush = false
54 3
 * @param string[] $to_array = array()
55
 * @param string $subject = ''
56
 * @param string $message = ''
57
 * @param string $headers = ''
58
 * @param bool $send_html = false
59
 * @param int $priority = 3
60
 * @param bool $is_private
61 3
 * @param string|null $message_id
62
 * @return bool
63
 * @package Mail
64
 */
65 3
function AddMailQueue($flush = false, $to_array = [], $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false, $message_id = '')
66
{
67
	global $context;
68 3
69
	$db = database();
70
71
	static $cur_insert = [];
72 3
	static $cur_insert_len = 0;
73
74 3
	if ($cur_insert_len === 0)
75 3
	{
76
		$cur_insert = [];
77 3
	}
78
79
	// If we're flushing, make the final inserts - also if we're near the MySQL length limit!
80 1
	if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
81
	{
82
		// Only do these once.
83
		$cur_insert_len = 0;
84
85 3
		// Dump the data...
86
		$db->insert('',
87
			'{db_prefix}mail_queue',
88
			[
89
				'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
90
				'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255',
91 3
			],
92
			$cur_insert,
93
			['id_mail']
94 3
		);
95
96
		$cur_insert = [];
97
		$context['flush_mail'] = false;
98
	}
99
100
	// If we're flushing we're done.
101 3
	if ($flush)
102
	{
103
		$nextSendTime = time() + 10;
104 3
105
		$db->query('', '
106
			UPDATE {db_prefix}settings
107 3
			SET 
108
				value = {string:nextSendTime}
109
			WHERE variable = {string:mail_next_send}
110
				AND value = {string:no_outstanding}',
111
			[
112
				'nextSendTime' => $nextSendTime,
113
				'mail_next_send' => 'mail_next_send',
114
				'no_outstanding' => '0',
115
			]
116 3
		);
117
118
		return true;
119
	}
120
121
	// Ensure we tell obExit to flush.
122
	$context['flush_mail'] = true;
123
124
	foreach ($to_array as $to)
125
	{
126
		// Will this insert go over MySQL's limit?
127
		$this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
128
129
		// Insert limit of 1M (just under the safety) is reached?
130
		if ($this_insert_len + $cur_insert_len > 1000000)
131
		{
132
			// Flush out what we have so far.
133
			$db->insert('',
134
				'{db_prefix}mail_queue',
135
				[
136
					'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string', 'subject' => 'string-255',
137
					'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255',
138
				],
139
				$cur_insert,
140 3
				['id_mail']
141 3
			);
142 3
143
			// Clear this out.
144 3
			$cur_insert = [];
145
			$cur_insert_len = 0;
146
		}
147
148 3
		// Now add the current insert to the array...
149
		$cur_insert[] = [time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private, (string) $message_id];
150
		$cur_insert_len += $this_insert_len;
151
	}
152
153
	// If they are using SSI there is a good chance obExit will never be called.  So lets be nice and flush it for them.
154
	if (ELK === 'SSI')
0 ignored issues
show
The condition ELK === 'SSI' is always true.
Loading history...
155
	{
156
		return AddMailQueue(true);
157
	}
158
159
	return true;
160
}
161
162 3
/**
163 3
 * Converts out of ascii range utf-8 characters in to HTML entities.  Primarily used
164
 * to maintain 7bit compliance for plain emails
165
 *
166
 * - Character codes <= 128 are left as is
167 3
 * - Character codes U+0080 <> U+00A0 range (control) are dropped
168
 * - Callback function of preg_replace_callback
169
 *
170 3
 * @param array $match
171 3
 *
172 3
 * @return string
173
 * @package Mail
174
 *
175 3
 */
176
function entityConvert($match)
177
{
178
	$c = $match[1];
179
	$c_strlen = strlen($c);
180
	$c_ord = ord($c[0]);
181
182
	// <= 127 are standard ASCII characters
183
	if ($c_strlen === 1 && $c_ord <= 0x7F)
184
	{
185
		return $c;
186
	}
187
188
	// Drop 2 byte control characters in the  U+0080 <> U+00A0 range
189 3
	if ($c_strlen === 2 && $c_ord === 0xC2 && ord($c[1]) <= 0xA0)
190
	{
191
		return '';
192
	}
193
194
	if ($c_strlen === 2 && $c_ord >= 0xC0 && $c_ord <= 0xDF)
195 3
	{
196
		return '&#' . ((($c_ord ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ';';
197
	}
198 3
199
	if ($c_strlen === 3 && $c_ord >= 0xE0 && $c_ord <= 0xEF)
200
	{
201 3
		return '&#' . ((($c_ord ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ';';
202 3
	}
203 3
204
	if ($c_strlen === 4 && $c_ord >= 0xF0 && $c_ord <= 0xF7)
205
	{
206 3
		return '&#' . ((($c_ord ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ';';
207
	}
208
209
	return '';
210
}
211
212
/**
213
 * Adds the unique security key in to an email
214
 *
215
 * - adds the key in to (each) message body section
216
 * - safety net for clients that strip out the message-id and in-reply-to headers
217
 *
218
 * @param string $message
219
 * @param string $unq_head
220
 * @param string $line_break
221
 *
222
 * @return string
223
 * @package Mail
224
 *
225
 */
226
function mail_insert_key($message, $unq_head, $line_break)
227
{
228
	$regex = [];
229
	$regex['plain'] = '~^(.*?)(' . $line_break . '--ELK-[a-z0-9]{28})~s';
230 3
	$regex['qp'] = '~(Content-Transfer-Encoding: Quoted-Printable' . $line_break . $line_break . ')(.*?)(' . $line_break . '--ELK-[a-z0-9]{28})~s';
231 3
	$regex['base64'] = '~(Content-Transfer-Encoding: base64' . $line_break . $line_break . ')(.*?)(' . $line_break . '--ELK-[a-z0-9]{28})~s';
232
233
	// Append the key to the bottom of the plain section, it is always the first one
234 3
	$message = preg_replace($regex['plain'], "$1{$line_break}{$line_break}[{$unq_head}]{$line_break}$2", $message);
235 3
236 3
	// Quoted Printable section, add the key in background color so the html message looks good
237 3
	if (preg_match($regex['qp'], $message, $match))
238
	{
239
		$qp_message = quoted_printable_decode($match[2]);
240
		$qp_message = str_replace('<span class="key-holder">[]</span>', '<span style="color: #F6F6F6">[' . $unq_head . ']</span>', $qp_message);
241 3
		$qp_message = quoted_printable_encode($qp_message);
242
		$message = str_replace($match[2], $qp_message, $message);
243
	}
244
245
	// base64 the harder one as it must match RFC 2045 semantics
246 3
	// Find the sections, decode, add in the new key, and encode the new message
247
	if (preg_match($regex['base64'], $message, $match))
248
	{
249
		// un-chunk, add in our encoded key header, and re chunk.  Done so we match RFC 2045 semantics.
250
		$encoded_message = base64_decode(str_replace($line_break, '', $match[2]));
251
		$encoded_message .= $line_break . $line_break . '[' . $unq_head . ']' . $line_break;
252
		$encoded_message = base64_encode($encoded_message);
253
		$encoded_message = chunk_split($encoded_message, 76, $line_break);
254
		$message = str_replace($match[2], $encoded_message, $message);
255
	}
256
257
	return $message;
258
}
259
260
/**
261
 * Load a template from EmailTemplates language file.
262 3
 *
263
 * @param string $template
264 3
 * @param array $replacements
265 3
 * @param string $lang = ''
266
 * @param bool $html = false - If to prepare the template for HTML output (newlines to BR, <a></a> links)
267
 * @param bool $loadLang = true
268
 * @param string[] $suffixes - Additional suffixes to find and return
269
 * @param string[] $additional_files - Additional language files to load
270 3
 *
271 3
 * @return array
272
 * @throws \ElkArte\Exceptions\Exception email_no_template
273 3
 * @package Mail
274
 */
275 3
function loadEmailTemplate($template, $replacements = [], $lang = '', $html = false, $loadLang = true, $suffixes = [], $additional_files = [])
276 3
{
277 3
	global $txt, $mbname, $scripturl, $settings, $boardurl, $modSettings;
278
279
	// First things first, load up the email templates language file, if we need to.
280 3
	if ($loadLang)
281
	{
282
		$lang_loader = new LangLoader($lang, $txt, database());
283
		$lang_loader->load('EmailTemplates+MaillistTemplates');
284
285
		if (!empty($additional_files))
286
		{
287
			foreach ($additional_files as $file)
288
			{
289
				$lang_loader->load($file);
290 3
			}
291
		}
292 3
	}
293
294
	$templateSubject = $template . '_subject';
295
	$templateBody = $template . '_body';
296
	if (!isset($txt[$templateSubject]))
297 3
	{
298 3
		throw new \ElkArte\Exceptions\Exception('email_no_template', 'template', [$templateSubject]);
299
	}
300 3
301 3
	if (!isset($txt[$templateBody]))
302
	{
303
		throw new \ElkArte\Exceptions\Exception('email_no_template', 'template', [$templateBody]);
304
	}
305
306
	$ret = [
307
		'subject' => $txt[$templateSubject],
308
		'body' => $txt[$templateBody],
309
	];
310
311
	if (!empty($suffixes))
312
	{
313
		foreach ($suffixes as $key)
314
		{
315
			$ret[$key] = $txt[$template . '_' . $key];
316
		}
317
	}
318
319
	// Add in the default replacements.
320
	$replacements += [
321 3
		'FORUMNAME' => $mbname,
322
		'FORUMNAMESHORT' => (!empty($modSettings['maillist_sitename']) ? $modSettings['maillist_sitename'] : $mbname),
323
		'EMAILREGARDS' => (!empty($modSettings['maillist_sitename_regards']) ? $modSettings['maillist_sitename_regards'] : ''),
324 3
		'FORUMURL' => $boardurl,
325
		'SCRIPTURL' => $scripturl,
326
		'THEMEURL' => $settings['theme_url'],
327
		'IMAGESURL' => $settings['images_url'],
328 3
		'DEFAULT_THEMEURL' => $settings['default_theme_url'],
329
		'REGARDS' => replaceBasicActionUrl($txt['regards_team']),
330
	];
331 3
332
	// Split the replacements up into two arrays, for use with str_replace
333
	$find = [];
334
	$replace = [];
335
336
	foreach ($replacements as $f => $r)
337
	{
338
		$find[] = '{' . $f . '}';
339
		$replace[] = $html && strpos($r, 'http') === 0 ? '<a href="' . $r . '">' . $r . '</a>' : $r;
340
	}
341 3
342
	// Do the variable replacements.
343
	foreach ($ret as $key => $val)
344 3
	{
345
		$val = str_replace($find, $replace, $val);
346
347
		// Now deal with the {USER.variable} items.
348
		$ret[$key] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $val);
349
	}
350
351
	// If we want this template to be used as HTML,
352
	$ret['body'] = $html ? templateToHtml($ret['body']) : $ret['body'];
353
354
	// Finally return the email to the caller, so they can send it out.
355
	return $ret;
356
}
357
358
/**
359
 * Used to preserve the Pre formatted look of txt template's when sending HTML
360
 *
361
 * @param $string
362
 * @return string
363
 */
364
function templateToHtml($string)
365
{
366
	$newString = preg_replace('~^-{3,40}$~m', '<hr />', $string);
367
368
	$newString = str_replace("\n", '<br />', $newString);
369
370
	return $newString ?? $string;
371
}
372
373
/**
374
 * Prepare subject and message of an email for the preview box
375
 *
376
 * Used in action_mailingcompose and RetrievePreview (Xml.controller.php)
377
 *
378
 * @package Mail
379
 */
380
function prepareMailingForPreview()
381
{
382
	global $context, $modSettings, $scripturl, $txt;
383
384
	Txt::load('Errors');
385
	require_once(SUBSDIR . '/Post.subs.php');
386
387
	$processing = [
388
		'preview_subject' => 'subject',
389
		'preview_message' => 'message'
390
	];
391
392
	// Use the default time format.
393
	User::$info->time_format = $modSettings['time_format'];
0 ignored issues
show
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
394
395
	$variables = [
396
		'{$board_url}',
397
		'{$current_time}',
398
		'{$latest_member.link}',
399
		'{$latest_member.id}',
400
		'{$latest_member.name}'
401
	];
402
403
	$html = $context['send_html'];
404
405
	// We might need this in a bit
406
	$cleanLatestMember = empty($context['send_html']) || $context['send_pm'] ? un_htmlspecialchars($modSettings['latestRealName']) : $modSettings['latestRealName'];
407
408
	$bbc_parser = ParserWrapper::instance();
409
410
	foreach ($processing as $key => $post)
411
	{
412
		$context[$key] = !empty($_REQUEST[$post]) ? $_REQUEST[$post] : '';
413
414
		if (empty($context[$key]) && empty($_REQUEST['xml']))
415
		{
416
			$context['post_error']['messages'][] = $txt['error_no_' . $post];
417
		}
418
		elseif (!empty($_REQUEST['xml']))
419
		{
420
			continue;
421
		}
422
423
		preparsecode($context[$key]);
424
425
		// Sending as html then we convert any bbc
426
		if ($html)
427
		{
428
			$enablePostHTML = $modSettings['enablePostHTML'];
429
			$modSettings['enablePostHTML'] = $context['send_html'];
430
			$context[$key] = $bbc_parser->parseEmail($context[$key]);
431
			$modSettings['enablePostHTML'] = $enablePostHTML;
432
		}
433
434
		// Replace in all the standard things.
435
		$context[$key] = str_replace($variables,
436
			[
437
				!empty($context['send_html']) ? '<a href="' . $scripturl . '">' . $scripturl . '</a>' : $scripturl,
438
				standardTime(forum_time(), false),
439
				!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),
440
				$modSettings['latestMember'],
441
				$cleanLatestMember
442
			], $context[$key]);
443
	}
444
}
445
446
/**
447
 * Callback function for load email template on subject and body
448
 * Uses capture group 1 in array
449
 *
450
 * @param array $matches
451
 * @return string
452
 * @package Mail
453
 */
454
function user_info_callback($matches)
455
{
456
	if (empty($matches[1]))
457
	{
458
		return '';
459
	}
460
461
	$use_ref = true;
462
	$ref = User::$info->toArray();
463
464
	foreach (explode('.', $matches[1]) as $index)
465
	{
466
		if ($use_ref && isset($ref[$index]))
467
		{
468
			$ref = &$ref[$index];
469
		}
470
		else
471
		{
472
			$use_ref = false;
473
			break;
474
		}
475
	}
476
477
	return $use_ref ? $ref : $matches[0];
478
}
479
480 3
/**
481
 * This function grabs the mail queue items from the database, according to the params given.
482
 *
483 3
 * @param int $start The item to start with (for pagination purposes)
484
 * @param int $items_per_page The number of items to show per page
485
 * @param string $sort A string indicating how to sort the results
486
 * @return array
487
 * @package Mail
488
 */
489
function list_getMailQueue($start, $items_per_page, $sort)
490
{
491
	global $txt;
492
493
	$db = database();
494
495
	return $db->fetchQuery('
496
		SELECT
497
			id_mail, time_sent, recipient, priority, private, subject
498
		FROM {db_prefix}mail_queue
499
		ORDER BY {raw:sort}
500
		LIMIT {int:start}, {int:items_per_page}',
501
		[
502
			'start' => $start,
503
			'sort' => $sort,
504
			'items_per_page' => $items_per_page,
505
		]
506
	)->fetch_callback(
507
		function ($row) use ($txt) {
508
			// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
509
			if (!empty($row['private']))
510
			{
511 3
				$row['subject'] = $txt['personal_message'];
512
			}
513
514 3
			return $row;
515
		}
516
	);
517 3
}
518
519
/**
520
 * Returns the total count of items in the mail queue.
521
 *
522
 * @return int
523
 * @package Mail
524
 */
525
function list_getMailQueueSize()
526
{
527
	$db = database();
528
529
	// How many items do we have?
530 3
	$request = $db->query('', '
531
		SELECT 
532
			COUNT(*) AS queue_size
533
		FROM {db_prefix}mail_queue',
534
		[]
535
	);
536
	list ($mailQueueSize) = $request->fetch_row();
537
	$request->free_result();
538
539
	return $mailQueueSize;
540
}
541
542
/**
543
 * Deletes items from the mail queue
544
 *
545
 * @param int[] $items
546
 * @package Mail
547
 */
548
function deleteMailQueueItems($items)
549
{
550
	$db = database();
551
552
	$db->query('', '
553
		DELETE FROM {db_prefix}mail_queue
554
		WHERE id_mail IN ({array_int:mail_ids})',
555
		[
556
			'mail_ids' => $items,
557
		]
558
	);
559
}
560
561
/**
562
 * Get the current mail queue status
563
 *
564
 * @package Mail
565
 */
566
function list_MailQueueStatus()
567
{
568
	$db = database();
569
570
	$items = [];
571
572
	// How many items do we have?
573
	$request = $db->query('', '
574
		SELECT 
575
		    COUNT(*) AS queue_size, MIN(time_sent) AS oldest
576
		FROM {db_prefix}mail_queue',
577
		[]
578
	);
579
	list ($items['mailQueueSize'], $items['mailOldest']) = $request->fetch_row();
580
	$request->free_result();
581
582
	return $items;
583
}
584
585
/**
586
 * This function handles updates to account for failed emails.
587
 *
588
 * - It is used to keep track of failed emails attempts and next try.
589
 *
590
 * @param array $failed_emails
591
 * @package Mail
592
 */
593
function updateFailedQueue($failed_emails)
594
{
595
	global $modSettings;
596
597
	$db = database();
598
599
	// Update the failed attempts check.
600
	$db->replace(
601
		'{db_prefix}settings',
602
		['variable' => 'string', 'value' => 'string'],
603
		['mail_failed_attempts', empty($modSettings['mail_failed_attempts']) ? 1 : ++$modSettings['mail_failed_attempts']],
604
		['variable']
605
	);
606
607
	// If we have failed to many times, tell mail to wait a bit and try again.
608
	if ($modSettings['mail_failed_attempts'] > 5)
609
	{
610
		$db->query('', '
611
			UPDATE {db_prefix}settings
612
			SET value = {string:next_mail_send}
613
			WHERE variable = {string:mail_next_send}
614
				AND value = {string:last_send}',
615
			[
616
				'next_mail_send' => time() + 60,
617
				'mail_next_send' => 'mail_next_send',
618
				'last_send' => $modSettings['mail_next_send'],
619
			]
620
		);
621
	}
622
623
	// Add our email back to the queue, manually.
624
	$db->insert('insert',
625
		'{db_prefix}mail_queue',
626
		['time_sent' => 'int', 'recipient' => 'string', 'body' => 'string', 'subject' => 'string', 'headers' => 'string', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int', 'message_id' => 'string-255'],
627
		$failed_emails,
628
		['id_mail']
629
	);
630
}
631
632
/**
633
 * Updates the failed attempts to email in the database.
634
 *
635
 * - It sets mail failed attempts value to 0.
636
 *
637
 * @package Mail
638
 */
639
function updateSuccessQueue()
640
{
641
	$db = database();
642
643
	$db->query('', '
644
		UPDATE {db_prefix}settings
645
		SET value = {string:zero}
646
		WHERE variable = {string:mail_failed_attempts}',
647
		[
648
			'zero' => '0',
649
			'mail_failed_attempts' => 'mail_failed_attempts',
650
		]
651
	);
652
}
653
654
/**
655
 * Reset to 0 the next send time for emails queue.
656
 */
657
function resetNextSendTime()
658
{
659
	global $modSettings;
660
661
	$db = database();
662
663
	// Update the setting to zero, yay
664
	// ...unless someone else did.
665
	$db->query('', '
666
		UPDATE {db_prefix}settings
667
		SET value = {string:no_send}
668
		WHERE variable = {string:mail_next_send}
669
			AND value = {string:last_mail_send}',
670
		[
671
			'no_send' => '0',
672
			'mail_next_send' => 'mail_next_send',
673
			'last_mail_send' => $modSettings['mail_next_send'],
674
		]
675
	);
676
}
677
678
/**
679
 * Update the next sending time for mail queue.
680
 *
681
 * - By default, move it 10 seconds for lower per mail_period_limits
682
 * and 5 seconds for larger mail_period_limits
683
 * - Requires an affected row
684
 *
685
 * @return int|bool
686
 * @package Mail
687
 */
688
function updateNextSendTime()
689
{
690
	global $modSettings;
691
692
	$db = database();
693
694
	// Set a delay based on the per minute limit (mail_period_limit)
695
	$delay = !empty($modSettings['mail_queue_delay'])
696
		? $modSettings['mail_queue_delay']
697
		: (!empty($modSettings['mail_period_limit']) && $modSettings['mail_period_limit'] <= 5 ? 10 : 5);
698
699
	$request = $db->query('', '
700
		UPDATE {db_prefix}settings
701
		SET value = {string:next_mail_send}
702
		WHERE variable = {string:mail_next_send}
703
			AND value = {string:last_send}',
704
		[
705
			'next_mail_send' => time() + $delay,
706
			'mail_next_send' => 'mail_next_send',
707
			'last_send' => $modSettings['mail_next_send'],
708
		]
709
	);
710
	if ($request->affected_rows() === 0)
711
	{
712
		return false;
713
	}
714
715
	return (int) $delay;
716
}
717
718
/**
719
 * Retrieve all details from the database on the next emails in the queue
720
 *
721
 * - Will fetch the next batch number of queued emails, sorted by priority
722
 *
723
 * @param int $number
724
 * @return array
725
 * @package Mail
726
 */
727
function emailsInfo($number)
728
{
729
	$db = database();
730
	$ids = [];
731
	$emails = [];
732
733
	// Get the next $number emails, with all that's to know about them and one more.
734
	$db->fetchQuery('
735
		SELECT /*!40001 SQL_NO_CACHE */ 
736
			id_mail, recipient, body, subject, headers, send_html, time_sent, priority, private, message_id
737
		FROM {db_prefix}mail_queue
738
		ORDER BY priority ASC, id_mail ASC
739
		LIMIT ' . $number,
740
		[]
741
	)->fetch_callback(
742
		function ($row) use (&$ids, &$emails) {
743
			// Just get the data and go.
744
			$ids[] = $row['id_mail'];
745
			$emails[] = [
746
				'to' => $row['recipient'],
747
				'body' => $row['body'],
748
				'subject' => $row['subject'],
749
				'headers' => $row['headers'],
750
				'send_html' => $row['send_html'],
751
				'time_sent' => $row['time_sent'],
752
				'priority' => $row['priority'],
753
				'private' => $row['private'],
754
				'message_id' => $row['message_id'],
755
			];
756
		}
757
	);
758
759
	return [$ids, $emails];
760
}
761
762
/**
763
 * Sends a group of emails from the mail queue.
764
 *
765
 * - Allows a batch of emails to be released every 5 to 10 seconds (based on per period limits)
766
 * - If batch size is not set, will determine a size such that it sends in 1/2 the period (buffer)
767
 *
768
 * @param int|bool $batch_size = false the number to send each loop
769
 * @param bool $override_limit = false bypassing our limit flaf
770
 * @param bool $force_send = false
771
 * @return bool
772
 * @package Mail
773
 */
774
function reduceMailQueue($batch_size = false, $override_limit = false, $force_send = false)
775
{
776
	return (new QueueMail())->reduceMailQueue($batch_size, $override_limit, $force_send);
777
}
778
779
/**
780
 * This function finds email address and few other details of the
781
 * poster of a certain message.
782
 *
783
 * @param int $id_msg the id of a message
784
 * @param int $topic_id the topic the message belongs to
785
 * @return array the poster's details
786
 * @todo very similar to mailFromMessage
787
 * @package Mail
788
 */
789
function posterDetails($id_msg, $topic_id)
790
{
791
	$db = database();
792
793
	$request = $db->query('', '
794
		SELECT 
795
			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
796
		FROM {db_prefix}messages AS m
797
			LEFT JOIN {db_prefix}members AS mem ON (m.id_member = mem.id_member)
798
		WHERE m.id_msg = {int:id_msg}
799
			AND m.id_topic = {int:current_topic}
800
		LIMIT 1',
801
		[
802
			'current_topic' => $topic_id,
803
			'id_msg' => $id_msg,
804
		]
805
	);
806
	$message = $request->fetch_assoc();
807
	$request->free_result();
808
809
	return $message;
810
}
811
812
/**
813
 * Little utility function to calculate how long ago a time was.
814
 *
815
 * @param int|double $time_diff
816
 * @return string
817
 * @package Mail
818
 */
819
function time_since($time_diff)
820
{
821
	global $txt;
822
823
	if ($time_diff < 0)
824
	{
825
		$time_diff = 0;
826
	}
827
828
	// Just do a bit of an if fest...
829
	if ($time_diff > 86400)
830
	{
831
		$days = round($time_diff / 86400, 1);
832
833
		return sprintf($days === 1.0 ? $txt['mq_day'] : $txt['mq_days'], $time_diff / 86400);
834
	}
835
836
	// Hours?
837
	if ($time_diff > 3600)
838
	{
839
		$hours = round($time_diff / 3600, 1);
840
841
		return sprintf($hours === 1.0 ? $txt['mq_hour'] : $txt['mq_hours'], $hours);
842
	}
843
844
	// Minutes?
845
	if ($time_diff > 60)
846
	{
847
		$minutes = (int) ($time_diff / 60);
848
849
		return sprintf($minutes === 1 ? $txt['mq_minute'] : $txt['mq_minutes'], $minutes);
850
	}
851
852
	// Otherwise must be second
853
	return sprintf($time_diff === 1 ? $txt['mq_second'] : $txt['mq_seconds'], $time_diff);
854
}
855