pbe_emailError()   F
last analyzed

Complexity

Conditions 23
Paths 1008

Size

Total Lines 112
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 50
nc 1008
nop 2
dl 0
loc 112
rs 0
c 0
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
 * All the vital helper functions for use in email posting, formatting, and conversion,
5
 * and boy are there a bunch!
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
 * @version 2.0 Beta 1
12
 *
13
 */
14
15
use BBC\PreparseCode;
16
use ElkArte\Attachments\AttachmentsDirectory;
17
use ElkArte\Attachments\TemporaryAttachment;
18
use ElkArte\Attachments\TemporaryAttachmentsList;
19
use ElkArte\Cache\Cache;
20
use ElkArte\Converters\Html2BBC;
21
use ElkArte\Converters\Html2Md;
22
use ElkArte\Helper\Util;
23
use ElkArte\Languages\Txt;
24
use ElkArte\Mail\PreparseMail;
25
use ElkArte\Maillist\EmailFormat;
26
use ElkArte\Maillist\EmailParse;
27
use ElkArte\MembersList;
28
use ElkArte\Notifications\Notifications;
29
use ElkArte\Notifications\NotificationsTask;
30
use Michelf\MarkdownExtra;
31
32
/**
33
 * Converts text / HTML to BBC
34
 *
35
 * What it does:
36
 *
37
 * - Protects certain tags from conversion
38
 * - Strips original message from the reply if possible
39
 * - If the email is HTML-based, then this will convert basic HTML tags to BBC tags
40
 * - If the email is plain text, it will convert it to HTML based on Markdown text
41
 *  conventions, and then that will be converted to BBC.
42
 *
43
 * @param string $text plain or HTML text
44
 * @param bool $html
45
 *
46
 * @return string
47
 * @uses Html2BBC.class.php for the HTML to BBC conversion
48
 * @uses Markdown.php for text to HTML conversions
49
 * @package Maillist
50
 */
51
function pbe_email_to_bbc($text, $html)
52
{
53
	// Define some things that need to be converted/modified, outside normal HTML or Markdown
54
	$tags = [
55
		'~\*\*\s?(.*?)\*\*~is' => '**$1**', // set as markup bold
56
		'~<\*>~' => '&lt;*&gt;', // <*> as set in default Mailist Templates
57
		'~^-{3,}~' => '<hr>', // 3+ --- to hr
58
		'~#([0-9a-fA-F]{4,6}\b)~' => '&#35;$1', // HTML entities
59
	];
60
61
	// We are starting with HTML, our goal is to convert the best parts of it to BBC,
62
	$text = pbe_run_parsers($text);
63
64
	if ($html)
65
	{
66
		// upfront pre-process $tags, mostly for the email template strings
67
		$text = preg_replace(array_keys($tags), array_values($tags), $text);
68
	}
69
	// Starting with plain text, possibly even Markdown style ;)
70
	else
71
	{
72
		// Set a gmail flag for special quote processing since its quotes are strange
73
		$gmail = (bool) preg_match('~<div class="gmail_quote">~i', $text);
74
75
		// Attempt to fix textual ('>') quotes, so we also fix wrapping issues first!
76
		$text = pbe_fix_email_quotes($text, $gmail);
77
		$text = str_replace(['[quote]', '[/quote]'], ['&gt;blockquote>', '&gt;/blockquote>'], $text);
78
79
		// Convert this (Markdown) text to HTML
80
		$text = preg_replace(array_keys($tags), array_values($tags), $text);
81
		$parser = new MarkdownExtra;
82
		$parser->hashtag_protection = true;
83
		$text = $parser->transform($text);
84
		$text = str_replace(['&gt;blockquote>', '&gt;/blockquote>'], ['<blockquote>', '</blockquote>'], $text);
85
	}
86
87
	// Convert the resulting HTML to BBC
88
	$bbc_converter = new Html2BBC($text, $html);
89
	$bbc_converter->skip_tags(['font', 'span']);
90
	$bbc_converter->skip_styles(['font-family', 'font-size', 'color']);
91
92
	$text = $bbc_converter->get_bbc();
93
94
	// Some tags often end up as just empty tags - remove those.
95
	$emptytags = [
96
		'~\[[bisu]\]\s*\[/[bisu]\]~' => '',
97
		'~\[quote\]\s*\[/quote\]~' => '',
98
		'~\[center\]\s*\[/center\]~' => '',
99
		'~(\n){3,}~si' => "\n\n",
100
	];
101
102
	return preg_replace(array_keys($emptytags), array_values($emptytags), $text);
103
}
104
105
/**
106
 * Runs the ACP email parsers
107
 *   - Returns cut email or original if the cut results in a blank message
108
 *
109
 * @param string $text
110
 * @return string
111
 */
112
function pbe_run_parsers($text)
113
{
114
	if (empty($text))
115
	{
116
		return '';
117
	}
118
119
	// Run our parsers, as defined in the ACP, to remove the original "replied to" message
120
	$text_save = $text;
121
	$result = pbe_parse_email_message($text);
122
123
	// If we have no message left after running the parser, then they may have replied
124
	// below and/or inside the original message. People like this should not be allowed
125
	// to use the net or be forced to read their own messed up emails
126
	if (empty($result) || (trim(strip_tags(pbe_filter_email_message($text))) === ''))
127
	{
128
		return $text_save;
129
	}
130
131
	return $text;
132
}
133
134
/**
135
 * Prepares the email body so that it looks like a forum post
136
 *
137
 * What it does:
138
 *
139
 * - Removes extra content as defined in the ACP filters
140
 * - Fixes quotes and quote levels
141
 * - Re-flows (unfolds) an email using the EmailFormat.class
142
 * - Attempts to remove any exposed email address
143
 *
144
 * @param string $body
145
 * @param string $real_name
146
 * @param string $charset character set of the text
147
 *
148
 * @return null|string
149
 * @package Maillist
150
 *
151
 * @uses EmailFormat.class.php
152
 */
153
function pbe_fix_email_body($body, $real_name = '', $charset = 'UTF-8')
154
{
155
	global $txt;
156
157
	// Remove the \r's now so its done
158
	$body = trim(str_replace("\r", '', $body));
159
160
	// Remove any control characters
161
	$body = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $body);
162
163
	// Remove the riff-raff as defined by the ACP filters
164
	$body = pbe_filter_email_message($body);
165
166
	// Any old school email john smith wrote: etc. style quotes that we need to update
167
	$body = pbe_fix_client_quotes($body);
168
169
	// Attempt to remove any exposed email addresses that are in the reply
170
	$body = preg_replace('~>' . $txt['to'] . '(.*)@(.*?)(?:\n|\[br])~i', '', $body);
171
	$body = preg_replace('~\b\s?[a-z0-9._%+-]+@[a-zZ0-9.-]+\.[a-z]{2,4}\b.?' . $txt['email_wrote'] . ':\s?~i', '', $body);
172
	$body = preg_replace('~<(.*?)>(.*@.*?)(?:\n|\[br])~', '$1' . "\n", $body);
173
	$body = preg_replace('~' . $txt['email_quoting'] . ' (.*) (?:<|&lt;|\[email]).*?@.*?(?:>|&gt;|\[\/email]):~i', '', $body);
174
175
	// Remove multiple sequential blank lines, again
176
	$body = preg_replace('~(\n\s?){3,}~i', "\n\n", $body);
177
178
	// Check and remove any blank quotes
179
	$body = preg_replace('~(\[quote\s?([a-zA-Z0-9"=]*)?]\s*(\[br]s*)?\[\/quote])~', '', $body);
180
181
	// Reflow and Cleanup this message to something that looks normal-er
182
	return (new EmailFormat())->reflow($body, $real_name, $charset);
183
}
184
185
/**
186
 * Replaces a messages >'s with BBC [quote] [/quote] blocks
187
 *
188
 * - Uses quote depth function
189
 * - Works with nested quotes of many forms >, > >, >>, >asd
190
 * - Bypassed for gmail as it only block quotes the outer layer and then plain
191
 * text > quotes the inner, which is confusing to all
192
 *
193
 * @param string $body
194
 * @param bool $html
195
 *
196
 * @return string
197
 * @package Maillist
198
 */
199
function pbe_fix_email_quotes($body, $html)
200
{
201
	// Coming from HTML, then remove lines that start with > and are inside [quote] ... [/quote] blocks
202
	if ($html)
203
	{
204
		$quotes = [];
205
		if (preg_match_all('~\[quote](.*)\[/quote]~sU', $body, $quotes, PREG_SET_ORDER))
206
		{
207
			foreach ($quotes as $quote)
208
			{
209
				$quotenew = preg_replace('~^\s?> (.*)$~m', '$1', $quote[1] . "\n");
210
				$body = str_replace($quote[0], '[quote]' . $quotenew . '[/quote]', $body);
211
			}
212
		}
213
	}
214
215
	// Create a line-by-line array broken on the newlines
216
	$body_array = explode("\n", $body);
217
	$original = $body_array;
218
219
	// Init
220
	$current_quote = 0;
221
	$quote_done = '';
222
223
	// Go line by line and add the quote blocks where needed, fixing where needed
224
	for ($i = 0, $num = count($body_array); $i < $num; $i++)
225
	{
226
		$body_array[$i] = trim($body_array[$i]);
227
228
		// Get the quote "depth" level for this line
229
		$level = pbe_email_quote_depth($body_array[$i]);
230
231
		// No quote marker on this line, but we are in a quote
232
		if ($level === 0 && $current_quote > 0)
233
		{
234
			// Make sure we don't have an email wrap issue
235
			$level_prev = pbe_email_quote_depth($original[$i - 1], false);
236
			$level_next = pbe_email_quote_depth($original[$i + 1], false);
237
238
			// A line between two = quote or descending quote levels,
239
			// probably an email break, so join (wrap) it back up and continue
240
			if (($level_prev !== 0) && ($level_prev >= $level_next && $level_next !== 0))
241
			{
242
				$body_array[$i - 1] .= ' ' . $body_array[$i];
243
				unset($body_array[$i]);
244
				continue;
245
			}
246
		}
247
248
		// No quote or in the same quote just continue
249
		if ($level === $current_quote)
250
		{
251
			continue;
252
		}
253
254
		// Deeper than we were, so add a quote
255
		if ($level > $current_quote)
256
		{
257
			$qin_temp = '';
258
			while ($level > $current_quote)
259
			{
260
				$qin_temp .= '[quote]' . "\n";
261
				$current_quote++;
262
			}
263
264
			$body_array[$i] = $qin_temp . $body_array[$i];
265
		}
266
267
		// Less deep so back out
268
		if ($level < $current_quote)
269
		{
270
			$qout_temp = '';
271
			while ($level < $current_quote)
272
			{
273
				$qout_temp .= '[/quote]' . "\n";
274
				$current_quote--;
275
			}
276
277
			$body_array[$i] = $qout_temp . $body_array[$i];
278
		}
279
280
		// That's all I have to say about that
281
		if ($level === 0 && $current_quote !== 0)
282
		{
283
			$quote_done = '';
284
			while ($current_quote)
285
			{
286
				$quote_done .= '[/quote]' . "\n";
287
				$current_quote--;
288
			}
289
290
			$body_array[$i] = $quote_done . $body_array[$i];
291
		}
292
	}
293
294
	// No more lines, let's just make sure we did not leave ourselves any open quotes
295
	while (!empty($current_quote))
296
	{
297
		$quote_done .= '[/quote]' . "\n";
298
		$current_quote--;
299
	}
300
301
	$body_array[$i] = $quote_done;
302
303
	// Join the array back together while dropping null index's
304
	return implode("\n", array_values($body_array));
305
}
306
307
/**
308
 * Looks for text quotes in the form of > and returns the current level for the line
309
 *
310
 * - If update is true (default), will strip the >'s and return the numeric level found
311
 * - Called by pbe_fix_email_quotes
312
 *
313
 * @param string $string
314
 * @param bool $update
315
 *
316
 * @return int
317
 * @package Maillist
318
 *
319
 */
320
function pbe_email_quote_depth(&$string, $update = true)
321
{
322
	// Get the quote "depth" level for this line
323
	$level = 0;
324
	$check = true;
325
	$string_save = $string;
326
327
	while ($check)
328
	{
329
		// We have a quote marker, increase our depth and strip the line of that quote marker
330
		if ($string === null)
331
		{
332
			$check = false;
333
		}
334
		elseif ($string === '>' || str_starts_with($string, '> '))
335
		{
336
			$level++;
337
			$string = substr($string, 2);
338
		}
339
		// Maybe a poorly nested quote, with no spaces between the >'s or the > and the data with no space
340
		elseif ((str_starts_with($string, '>>')) || (preg_match('~^>[a-z0-9<-]+~Ui', $string) === 1))
341
		{
342
			$level++;
343
			$string = substr($string, 1);
344
		}
345
		// All done getting the depth
346
		else
347
		{
348
			$check = false;
349
		}
350
	}
351
352
	if (!$update)
353
	{
354
		$string = $string_save;
355
	}
356
357
	return $level;
358
}
359
360
/**
361
 * Splits a message at a given string, returning only the upper portion
362
 *
363
 * - Intended to split off the 'replied to' portion that often follows the reply
364
 * - Uses parsers as defined in the ACP to do its searching
365
 * - Stops after the first successful hit occurs
366
 * - Goes in the order defined in the table
367
 *
368
 * @param string $body
369
 * @return bool on find
370
 * @package Maillist
371
 */
372
function pbe_parse_email_message(&$body)
373
{
374
	$db = database();
375
376
	// Load up the parsers from the database
377
	$expressions = [];
378
	$db->fetchQuery('
379
		SELECT
380
			filter_from, filter_type
381
		FROM {db_prefix}postby_emails_filters
382
		WHERE filter_style = {string:filter_style}
383
		ORDER BY filter_order ASC',
384
		[
385
			'filter_style' => 'parser'
386
		]
387
	)->fetch_callback(
388
		static function ($row) use (&$expressions) {
389
			// Build an array of valid expressions
390
			$expressions[] = [
391
				'type' => $row['filter_type'] === 'regex' ? 'regex' : 'string',
392
				'parser' => $row['filter_from']];
393
		}
394
	);
395
396
	// Look for the markers, **stop** after the first successful one, good hunting!
397
	foreach ($expressions as $expression)
398
	{
399
		$split = $expression['type'] === 'regex'
400
			? preg_split($expression['parser'], $body)
401
			: explode($expression['parser'], $body, 2);
402
403
		// If an expression was matched, our fine work is done
404
		if (!empty($split[1]))
405
		{
406
			// If we had a hit, then we clip off the mail and return above the split text
407
			$body = $split[0];
408
			return true;
409
		}
410
	}
411
412
	return false;
413
}
414
415
/**
416
 * Searches for extraneous text and removes/replaces it
417
 *
418
 * - Uses filters as defined in the ACP to do the search / replace
419
 * - Will apply regex filters first, then string match filters
420
 * - Apply all filters to a message
421
 *
422
 * @param string $text
423
 *
424
 * @return string
425
 * @package Maillist
426
 *
427
 */
428
function pbe_filter_email_message($text)
429
{
430
	if (empty($text))
431
	{
432
		return '';
433
	}
434
435
	$db = database();
436
437
	// load up the text filters from the database, regex first and ordered by the filter order ...
438
	$db->fetchQuery('
439
		SELECT
440
			filter_from, filter_to, filter_type
441
		FROM {db_prefix}postby_emails_filters
442
		WHERE filter_style = {string:filter_style}
443
		ORDER BY filter_type ASC, filter_order ASC',
444
		[
445
			'filter_style' => 'filter'
446
		]
447
	)->fetch_callback(
448
		static function ($row) use (&$text) {
449
			if ($row['filter_type'] === 'regex')
450
			{
451
				// Newline madness
452
				if (!empty($row['filter_to']))
453
				{
454
					$row['filter_to'] = str_replace('\n', "\n", $row['filter_to']);
455
				}
456
457
				// Test the regex and if good use, else skip, don't want a bad regex to empty the message!
458
				$temp = preg_replace($row['filter_from'], $row['filter_to'], $text);
459
				if ($temp !== null)
460
				{
461
					$text = $temp;
462
				}
463
			}
464
			else
465
			{
466
				$text = str_replace($row['filter_from'], $row['filter_to'], $text);
467
			}
468
		}
469
	);
470
471
	return $text;
472
}
473
474
/**
475
 * Finds Re: Subject: FW: FWD or [$sitename] in the subject and strips it
476
 *
477
 * - Recursively calls itself till no more tags are found
478
 *
479
 * @param string $text
480
 * @param bool $check if true will return whether tags were found
481
 *
482
 * @return bool|string
483
 * @package Maillist
484
 *
485
 */
486
function pbe_clean_email_subject($text, $check = false)
487
{
488
	global $txt, $modSettings, $mbname;
489
490
	$sitename = empty($modSettings['maillist_sitename']) ? $mbname : $modSettings['maillist_sitename'];
491
492
	// Find Re: Subject: FW: FWD or [$sitename] in the subject and strip it
493
	$re = stripos($text, $txt['RE:']);
494
	if ($re !== false)
495
	{
496
		$text = substr($text, 0, $re) . substr($text, $re + strlen($txt['RE:']), strlen($text));
497
	}
498
499
	$su = stripos($text, $txt['SUBJECT:']);
500
	if ($su !== false)
501
	{
502
		$text = substr($text, 0, $su) . substr($text, $su + strlen($txt['SUBJECT:']), strlen($text));
503
	}
504
505
	$fw = stripos($text, $txt['FW:']);
506
	if ($fw !== false)
507
	{
508
		$text = substr($text, 0, $fw) . substr($text, $fw + strlen($txt['FW:']), strlen($text));
509
	}
510
511
	$gr = strpos($text, '[' . $sitename . ']');
512
	if ($gr !== false)
513
	{
514
		$text = substr($text, 0, $gr) . substr($text, $gr + strlen($sitename) + 2, strlen($text));
515
	}
516
517
	$fwd = stripos($text, $txt['FWD:']);
518
	if ($fwd !== false)
519
	{
520
		$text = substr($text, 0, $fwd) . substr($text, $fwd + strlen($txt['FWD:']), strlen($text));
521
	}
522
523
	// if not done, then call ourselves again; we like the sound of our name
524
	if (stripos($text, (string) $txt['RE:']) || stripos($text, $txt['FW:']) || stripos($text, $txt['FWD:']) || strpos($text, '[' . $sitename . ']'))
525
	{
526
		$text = pbe_clean_email_subject($text);
527
	}
528
529
	// clean or not?
530
	if ($check)
531
	{
532
		return ($re === false && $su === false && $gr === false && $fw === false && $fwd === false);
533
	}
534
535
	return trim($text);
536
}
537
538
/**
539
 * Used if the original email could not be removed from the message (top of post)
540
 *
541
 * - Tries to quote the original message instead of using a loose original message search
542
 * - Looks for email client original message tags and converts them to bbc quotes
543
 *
544
 * @param string $body
545
 *
546
 * @return null|string
547
 * @package Maillist
548
 *
549
 */
550
function pbe_fix_client_quotes($body)
551
{
552
	global $txt;
553
554
	// Define some common quote markers (from the original messages)
555
	// @todo ACP for this? ... not sure
556
	$regex = [];
557
558
	// On sun, jan 12, 2020 at 10:10 AM, John Smith wrote: [quote]
559
	$regex[] = '~(?:' . $txt['email_on'] . ')?\w{3}, \w{3} \d{1,2},\s?\d{4} ' . $txt['email_at'] . ' \d{1,2}:\d{1,2} [AP]M,(.*)?' . $txt['email_wrote'] . ':\s?\s{1,4}\[quote\]~i';
560
	// [quote] on: mon jan 12, 2004 John Smith wrote:
561
	$regex[] = '~\[quote\]\s?' . $txt['email_on'] . ': \w{3} \w{3} \d{1,2}, \d{4} (.*)?' . $txt['email_wrote'] . ':\s~i';
562
	// on jan 12, 2020 at 10:10 PM, John Smith wrote:   [quote]
563
	$regex[] = '~' . $txt['email_on'] . ' \w{3} \d{1,2}, \d{4}, ' . $txt['email_at'] . ' \d{1,2}:\d{1,2} [AP]M,(.*)?' . $txt['email_wrote'] . ':\s{1,4}\[quote\]~i';
564
	// on jan 12, 2020 at 10:10, John Smith wrote   [quote]
565
	$regex[] = '~' . $txt['email_on'] . ' \w{3} \d{1,2}, \d{4}, ' . $txt['email_at'] . ' \d{1,2}:\d{1,2}, (.*)?' . $txt['email_wrote'] . ':\s{1,4}\[quote\]~i';
566
	// quoting: John Smith on stuffz at 10:10:23 AM
567
	$regex[] = '~' . $txt['email_quotefrom'] . ': (.*) ' . $txt['email_on'] . ' .* ' . $txt['email_at'] . ' \d{1,2}:\d{1,2}:\d{1,2} [AP]M~';
568
	// quoting John Smith <[email protected]>
569
	$regex[] = '~' . $txt['email_quoting'] . ' (.*) (?:<|&lt;|\[email\]).*?@.*?(?:>|&gt;|\[/email\]):~i';
570
	// --- in some group name "John Smith" <[email protected]> wrote:
571
	$regex[] = '~---\s.*?"(.*)"\s+' . $txt['email_wrote'] . ':\s(\[quote\])?~i';
572
	// --- in [email protected] John Smith wrote
573
	$regex[] = '~---\s.*?\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6}\b,\s(.*?)\s' . $txt['email_wrote'] . ':?~iu';
574
	// --- In some@g..., "someone"  wrote:
575
	$regex[] = '~---\s.*?\b[A-Z0-9._%+-]+@[A-Z0-9][.]{3}, [A-Z0-9._%+\-"]+\b(.*?)\s' . $txt['email_wrote'] . ':?~iu';
576
	// --- In [email]something[/email] "someone" wrote:
577
	$regex[] = '~---\s.*?\[email=.*?/email\],?\s"?(.*?)"?\s' . $txt['email_wrote'] . ':?~iu';
578
579
	// For each one see if we can do nice [quote author=john smith]
580
	foreach ($regex as $reg)
581
	{
582
		if (preg_match_all($reg, $body, $matches, PREG_SET_ORDER))
583
		{
584
			foreach ($matches as $quote)
585
			{
586
				$quote[1] = preg_replace('~\[email].*\[/email]~', '', $quote[1]);
587
				$body = pbe_str_replace_once($quote[0], "\n" . '[quote author=' . trim($quote[1]) . "]\n", $body);
588
589
				$quote[1] = preg_quote($quote[1], '~');
590
591
				// Look for [quote author=][/quote][quote] issues
592
				$body = preg_replace('~\[quote author=' . trim($quote[1]) . '] ?(?:\n|\[br]?){2,4} ?\[/ote]?\[quote]~u', '[quote author=' . trim($quote[1]) . "]\n", $body, 1);
593
594
				// And [quote author=][quote] newlines [/quote] issues
595
				$body = preg_replace('~\[quote author=' . trim($quote[1]) . '] ?(?:\n|\[br]?){2,4}\[quote]~u', '[quote author=' . trim($quote[1]) . "]\n", $body);
596
			}
597
		}
598
	}
599
600
	return $body;
601
}
602
603
/**
604
 * Does a single replacement of the first found string in the haystack
605
 *
606
 * @param string $needle
607
 * @param string $replace
608
 * @param string $haystack
609
 * @return string
610
 * @package Maillist
611
 */
612
function pbe_str_replace_once($needle, $replace, $haystack)
613
{
614
	// Looks for the first occurrence of $needle in $haystack and replaces it with $replace
615
	$pos = strpos($haystack, $needle);
616
	if ($pos === false)
617
	{
618
		return $haystack;
619
	}
620
621
	return substr_replace($haystack, $replace, $pos, strlen($needle));
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr_replace($h... $pos, strlen($needle)) also could return the type array which is incompatible with the documented return type string.
Loading history...
622
}
623
624
/**
625
 * Does moderation check on a given user (global)
626
 *
627
 * - Removes permissions of PBE concern that a given moderated level denies
628
 *
629
 * @param array $pbe array of user values
630
 * @package Maillist
631
 */
632
function pbe_check_moderation(&$pbe)
633
{
634
	global $modSettings;
635
636
	if (empty($modSettings['postmod_active']))
637
	{
638
		return;
639
	}
640
641
	// Have they been muted for being naughty?
642
	if (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $pbe['user_info']['warning'])
643
	{
644
		// Remove anything that would allow them to do anything via PBE
645
		$denied_permissions = [
646
			'pm_send', 'postby_email',
647
			'admin_forum', 'moderate_forum',
648
			'post_new', 'post_reply_own', 'post_reply_any',
649
			'post_attachment', 'post_unapproved_attachments',
650
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
651
		];
652
		$pbe['user_info']['permissions'] = array_diff($pbe['user_info']['permissions'], $denied_permissions);
653
	}
654
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $pbe['user_info']['warning'])
655
	{
656
		// Work out what permissions should change if they are just being moderated
657
		$permission_change = [
658
			'post_new' => 'post_unapproved_topics',
659
			'post_reply_own' => 'post_unapproved_replies_own',
660
			'post_reply_any' => 'post_unapproved_replies_any',
661
			'post_attachment' => 'post_unapproved_attachments',
662
		];
663
664
		foreach ($permission_change as $old => $new)
665
		{
666
			if (!in_array($old, $pbe['user_info']['permissions']))
667
			{
668
				unset($permission_change[$old]);
669
			}
670
			else
671
			{
672
				$pbe['user_info']['permissions'][] = $new;
673
			}
674
		}
675
676
		$pbe['user_info']['permissions'] = array_diff($pbe['user_info']['permissions'], array_keys($permission_change));
677
	}
678
}
679
680
/**
681
 * Creates a failed email entry in the postby_emails_error table
682
 *
683
 * - Attempts autocorrect for common errors so the admin / moderator
684
 * - Can choose to approve the email with the corrections
685
 *
686
 * @param string $error
687
 * @param EmailParse $email_message
688
 *
689
 * @return bool
690
 * @package Maillist
691
 *
692
 */
693
function pbe_emailError($error, $email_message)
694
{
695
	global $txt;
696
697
	$db = database();
698
699
	Txt::load('EmailTemplates');
700
701
	// Some extra items we will need to remove from the message subject
702
	$pm_subject_leader = str_replace('{SUBJECT}', '', $txt['new_pm_subject']);
703
704
	// Clean the subject like we don't know where it has been
705
	$subject = trim(str_replace($pm_subject_leader, '', $email_message->subject));
706
	$subject = pbe_clean_email_subject($subject);
707
	$subject = ($subject === '' ? $txt['no_subject'] : $subject);
708
709
	// Start off with what we know about the security key, even if it's nothing
710
	$message_key = $email_message->message_key;
711
	$message_type = $email_message->message_type;
712
	$message_id = $email_message->message_id;
713
	$board_id = -1;
714
715
	// First up is the old, wrong email address, let's see who this should have come from if
716
	// it is not a new topic request
717
	if ($error === 'error_not_find_member' && $email_message->message_type !== 'x')
718
	{
719
		$key_owner = query_key_owner($email_message);
720
		if (!empty($key_owner))
721
		{
722
			// Valid key so show who should have sent this key in? email aggravaters :P often messes this up
723
			$email_message->email['from'] .= ' => ' . $key_owner;
724
		}
725
	}
726
727
	// A valid key but it was not sent to this user ... but we did get the email from a valid site user
728
	if ($error === 'error_key_sender_match')
729
	{
730
		$key_owner = query_key_owner($email_message);
731
		if (!empty($key_owner))
732
		{
733
			// Valid key so show who should have sent this key in
734
			$email_message->email['from'] = $key_owner . ' => ' . $email_message->email['from'];
735
		}
736
	}
737
738
	// No key? We should at a minimum have who it's from and a subject, so use that
739
	if ($email_message->message_type !== 'x' && (empty($message_key) || $error === 'error_pm_not_found'))
740
	{
741
		// We don't have the message type (since we don't have a key)
742
		// Attempt to see if it might be a PM, so we handle it correctly
743
		if (empty($message_type) && (str_contains($email_message->subject, (string) $pm_subject_leader)))
744
		{
745
			$message_type = 'p';
746
		}
747
748
		// Find all keys sent to this user, sorted by date
749
		$user_keys = query_user_keys($email_message->email['from']);
750
751
		// While we have keys to look at, see if we can match up this lost message on subjects
752
		foreach ($user_keys as $user_key)
753
		{
754
			$key = $user_key['message_key'];
755
			$type = $user_key['message_type'];
756
			$message = $user_key['message_id'];
757
758
			// If we know/suspect it's an "m,t or p", then use that to avoid a match on a wrong type; that would be bad ;)
759
			// Look up this message/topic/pm and see if the subjects match ... if they do, then tada!
760
			if (((!empty($message_type) && $message_type === $type) || (empty($message_type) && $type !== 'p'))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! empty($message_type) ...l['from']) === $subject, Probably Intended Meaning: ! empty($message_type) &...['from']) === $subject)
Loading history...
761
				&& query_load_subject($message, $type, $email_message->email['from']) === $subject)
762
			{
763
				// This email has a subject that matches the subject of a message that was sent to them
764
				$message_key = $key;
765
				$message_id = $message;
766
				$message_type = $type;
767
				break;
768
			}
769
		}
770
	}
771
772
	// Maybe we have enough to find the board id where this was going
773
	if (!empty($message_id) && $message_type !== 'p')
774
	{
775
		$board_id = query_load_board($message_id);
776
	}
777
778
	// Log the error so the moderators can take a look, helps keep them sharp
779
	$id = isset($_REQUEST['item']) ? (int) $_REQUEST['item'] : 0;
780
	$db->insert(empty($id) ? 'ignore' : 'replace',
781
		'{db_prefix}postby_emails_error',
782
		[
783
			'id_email' => 'int', 'error' => 'string', 'message_key' => 'string',
784
			'subject' => 'string', 'message_id' => 'int', 'id_board' => 'int',
785
			'email_from' => 'string', 'message_type' => 'string', 'message' => 'string'],
786
		[
787
			$id, $error, $message_key,
788
			$subject, $message_id, $board_id,
789
			$email_message->email['from'], $message_type, $email_message->raw_message],
790
		['id_email']
791
	);
792
793
	// Flush the moderator error number cache, if we are here, it likely just changed.
794
	Cache::instance()->remove('num_menu_errors');
795
796
	// If not running from the cli, then go back to the form
797
	if (isset($_POST['item']))
798
	{
799
		// Back to the form we go
800
		$_SESSION['email_error'] = $txt[$error];
801
		redirectexit('action=admin;area=maillist');
802
	}
803
804
	return false;
805
}
806
807
/**
808
 * Writes email attachments as temp names in the proper attachment directory
809
 *
810
 * What it does:
811
 *
812
 * - Populates TemporaryAttachmentsList with the email attachments
813
 * - Does all the checks to validate them
814
 * - Skips ones flagged with errors
815
 * - Adds valid ones to attachmentOptions
816
 * - Calls createAttachment to store them
817
 *
818
 * @param array $pbe
819
 * @param EmailParse $email_message
820
 *
821
 * @return array
822
 * @package Maillist
823
 *
824
 */
825
function pbe_email_attachments($pbe, $email_message)
826
{
827
	// Trying to attach a file with this post...
828
	global $modSettings, $context, $txt;
829
830
	// Init
831
	$attachIDs = [];
832
	$tmp_attachments = new TemporaryAttachmentsList();
833
834
	// Make sure we know where to upload
835
	$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
836
	try
837
	{
838
		$attachmentDirectory->automanageCheckDirectory(isset($_REQUEST['action']) && $_REQUEST['action'] === 'admin');
839
840
		$attach_current_dir = $attachmentDirectory->getCurrent();
841
842
		if (!is_dir($attach_current_dir))
843
		{
844
			$tmp_attachments->setSystemError('attach_folder_warning');
845
			\ElkArte\Errors\Errors::instance()->log_error(sprintf($txt['attach_folder_admin_warning'], $attach_current_dir), 'critical');
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
846
		}
847
	}
848
	catch (\Exception $exception)
849
	{
850
		// If the attachment folder is not there: error.
851
		$tmp_attachments->setSystemError($exception->getMessage());
852
	}
853
854
	// For attachmentChecks function
855
	require_once(SUBSDIR . '/Attachments.subs.php');
856
	$context['attachments'] = ['quantity' => 0, 'total_size' => 0];
857
858
	// Create the file(s) with a temp name, so we can validate its contents/type
859
	foreach ($email_message->attachments as $name => $attachment)
860
	{
861
		if ($tmp_attachments->hasSystemError())
862
		{
863
			continue;
864
		}
865
866
		$attachID = $tmp_attachments->getTplName($pbe['profile']['id_member'], bin2hex(random_bytes(16)));
867
868
		// Write the contents to an actual file
869
		$destName = $attach_current_dir . '/' . $attachID;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $attach_current_dir does not seem to be defined for all execution paths leading up to this point.
Loading history...
870
		if (file_put_contents($destName, $attachment) !== false)
871
		{
872
			@chmod($destName, 0644);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

872
			/** @scrutinizer ignore-unhandled */ @chmod($destName, 0644);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
873
874
			$temp_file = new TemporaryAttachment([
875
				'name' => basename($name),
876
				'tmp_name' => $destName,
877
				'attachid' => $attachID,
878
				'user_id' => $pbe['profile']['id_member'],
879
				'size' => strlen($attachment),
880
				'type' => null,
881
				'id_folder' => $attachmentDirectory->currentDirectoryId(),
882
			]);
883
884
			// Make sure it's valid
885
			$temp_file->doChecks($attachmentDirectory);
0 ignored issues
show
Bug introduced by
The method doChecks() does not exist on ElkArte\Attachments\TemporaryAttachment. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

885
			$temp_file->/** @scrutinizer ignore-call */ 
886
               doChecks($attachmentDirectory);
Loading history...
886
			$tmp_attachments->addAttachment($temp_file);
887
		}
888
	}
889
890
	$prefix = $tmp_attachments->getTplName($pbe['profile']['id_member']);
891
892
	// Space for improvement: move the removeAll to the end before ->unset
893
	if ($tmp_attachments->hasSystemError())
894
	{
895
		$tmp_attachments->removeAll();
896
	}
897
	else
898
	{
899
		// Get the results from attachmentChecks and see if it is suitable for posting
900
		foreach ($tmp_attachments as $attachID => $attachment)
901
		{
902
			// If there were any errors, we just skip that file
903
			if (!str_contains($attachID, (string) $prefix) || $attachment->hasErrors())
904
			{
905
				$attachment->remove(false);
906
				continue;
907
			}
908
909
			// Load the attachmentOptions array with the data needed to create an attachment
910
			$attachmentOptions = [
911
				'post' => 0,
912
				'poster' => $pbe['profile']['id_member'],
913
				'name' => $attachment['name'],
914
				'tmp_name' => $attachment['tmp_name'],
915
				'size' => (int) $attachment['size'],
916
				'mime_type' => (string) $attachment['type'],
917
				'id_folder' => (int) $attachment['id_folder'],
918
				'approved' => !$modSettings['postmod_active'] || in_array('post_unapproved_attachments', $pbe['user_info']['permissions']),
919
				'errors' => [],
920
			];
921
922
			// Make it available to the forum/post
923
			if (createAttachment($attachmentOptions))
924
			{
925
				$attachIDs[] = $attachmentOptions['id'];
926
				if (!empty($attachmentOptions['thumb']))
927
				{
928
					$attachIDs[] = $attachmentOptions['thumb'];
929
				}
930
			}
931
			// We had a problem, so simply remove it
932
			else
933
			{
934
				$tmp_attachments->removeById($attachID, false);
935
			}
936
		}
937
	}
938
939
	$tmp_attachments->unset();
940
941
	return $attachIDs;
942
}
943
944
/**
945
 * Used when an email attempts to start a new topic
946
 *
947
 * - Load the board id that a given email address is assigned to in the ACP
948
 * - Returns the board number in which the new topic must go
949
 *
950
 * @param EmailParse $email_address
951
 *
952
 * @return int
953
 * @package Maillist
954
 *
955
 */
956
function pbe_find_board_number($email_address)
957
{
958
	global $modSettings;
959
960
	$valid_address = [];
961
	$board_number = 0;
962
963
	// Load our valid email ids and the corresponding board ids
964
	$data = (empty($modSettings['maillist_receiving_address'])) ? [] : Util::unserialize($modSettings['maillist_receiving_address']);
965
	foreach ($data as $addr)
966
	{
967
		$valid_address[$addr[0]] = $addr[1];
968
	}
969
970
	// Who was this message sent to, may have been sent to multiple addresses,
971
	// so we must check each one to see if we have a valid entry
972
	foreach ($email_address->email['to'] as $to_email)
973
	{
974
		if (isset($valid_address[$to_email]))
975
		{
976
			$board_number = (int) $valid_address[$to_email];
977
			break;
978
		}
979
	}
980
981
	return $board_number;
982
}
983
984
/**
985
 * Converts a post/pm to text (Markdown) for sending in an email
986
 *
987
 * - Censors everything it will send
988
 * - Pre-converts select bbc tags to HTML, so they can be Markdown properly
989
 * - Uses parse-bbc to convert remaining BBC to HTML
990
 * - Uses html2markdown to convert HTML into Markdown text suitable for email
991
 * - If someone wants to write a direct BBC->Markdown conversion tool, I'm listening!
992
 *
993
 * @param string $message
994
 * @param string $subject
995
 * @param string $signature
996
 * @package Maillist
997
 */
998
function pbe_prepare_text(&$message, &$subject = '', &$signature = '')
999
{
1000
	$mailPreparse = new PreparseMail();
1001
	$message = $mailPreparse->preparseHtml($message);
1002
	$subject = $mailPreparse->preparseSubject($subject);
1003
	$signature = $mailPreparse->preparseSignature($signature);
1004
1005
	// Convert this to text (Markdown)
1006
	$mark_down = new Html2Md($message);
1007
	$message = $mark_down->get_markdown();
1008
}
1009
1010
/**
1011
 * When a DSN (bounce) is received and the feature is enabled, update the settings
1012
 * For the user in question to disable Board and Post notifications. Do not clear
1013
 * Notification subscriptions.
1014
 *
1015
 * When finished, fire off a site notification informing the user of the action and reason
1016
 *
1017
 * @param EmailParse $email_message
1018
 * @package Maillist
1019
 */
1020
function pbe_disable_user_notify($email_message)
1021
{
1022
	global $modSettings;
1023
	$db = database();
1024
1025
	$email = $email_message->get_failed_dest();
1026
1027
	$request = $db->query('', '
1028
		SELECT
1029
			id_member
1030
		FROM {db_prefix}members
1031
		WHERE email_address = {string:email}
1032
		LIMIT 1',
1033
		[
1034
			'email' => $email
1035
		]
1036
	);
1037
1038
	if ($request->num_rows() !== 0)
1039
	{
1040
		[$id_member] = $request->fetch_row();
1041
		$request->free_result();
1042
1043
		// Once we have the member's ID, we can turn off board/topic notifications
1044
		// by setting notify_regularity->99 ("Never")
1045
		$db->query('', '
1046
			UPDATE {db_prefix}members
1047
			SET
1048
				notify_regularity = 99
1049
			WHERE id_member = {int:id_member}',
1050
			[
1051
				'id_member' => $id_member
1052
			]
1053
		);
1054
1055
		// Now that other notifications have been added, we need to turn off email for those, too.
1056
		$db->query('', '
1057
			DELETE FROM {db_prefix}notifications_pref
1058
			WHERE id_member = {int:id_member}
1059
				AND notification_type = {string:email}',
1060
			[
1061
				'id_member' => $id_member,
1062
				'email' => 'email'
1063
			]
1064
		);
1065
1066
		// Add a "mention" of email notification being disabled
1067
		if (!empty($modSettings['mentions_enabled']))
1068
		{
1069
			$notifier = Notifications::instance();
1070
			$notifier->add(new NotificationsTask(
1071
				'mailfail',
1072
				0,
1073
				$id_member,
1074
				['id_members' => [$id_member]]
1075
			));
1076
			$notifier->send();
1077
		}
1078
	}
1079
}
1080
1081
/**
1082
 * Replace full BBC quote tags with HTML blockquote version where the cite line
1083
 * is used as the first line of the quote.
1084
 *
1085
 * - Callback for pbe_prepare_text
1086
 * - Only changes the leading [quote], the closing /quote is not changed but
1087
 * handled back in the main function
1088
 *
1089
 * @param string[] $matches array of matches from the regex in the preg_replace
1090
 *
1091
 * @return string
1092
 */
1093
function quote_callback($matches)
1094
{
1095
	global $txt;
1096
1097
	$date = '';
1098
	$author = '';
1099
1100
	if (preg_match('~date=(\d{8,10})~ui', $matches[0], $match) === 1)
1101
	{
1102
		$date = $txt['email_on'] . ': ' . date('D M j, Y', $match[1]);
1103
	}
1104
1105
	if (preg_match('~author=([^<>\n]+?)(?=(?:link=|date=|]))~ui', $matches[0], $match) === 1)
1106
	{
1107
		$author = $match[1] . $txt['email_wrote'] . ': ';
1108
	}
1109
1110
	return "\n" . '<blockquote>' . $date . ' ' . $author;
1111
}
1112
1113
/**
1114
 * Loads up the vital user information given an email address
1115
 *
1116
 * - Similar to \ElkArte\MembersList::load, loadPermissions, loadUserSettings, but only loads a
1117
 * subset of that data, enough to validate that a user can make a post to a given board.
1118
 * - Done this way to avoid overwriting user_info etc. for those who are running
1119
 * this function (on behalf of the email owner, similar to profile views etc.)
1120
 *
1121
 * Sets:
1122
 * - pbe['profile']
1123
 * - pbe['profile']['options']
1124
 * - pbe['user_info']
1125
 * - pbe['user_info']['permissions']
1126
 * - pbe['user_info']['groups']
1127
 *
1128
 * @param string $email
1129
 *
1130
 * @return array|bool
1131
 * @package Maillist
1132
 *
1133
 */
1134
function query_load_user_info($email)
1135
{
1136
	global $modSettings, $language;
1137
1138
	$db = database();
1139
1140
	if (empty($email))
1141
	{
1142
		return false;
1143
	}
1144
1145
	// Find the user who owns this email address
1146
	$request = $db->query('', '
1147
		SELECT
1148
			id_member
1149
		FROM {db_prefix}members
1150
		WHERE email_address = {string:email}
1151
		AND is_activated = {int:act}
1152
		LIMIT 1',
1153
		[
1154
			'email' => $email,
1155
			'act' => 1,
1156
		]
1157
	);
1158
	[$id_member] = $request->fetch_row();
1159
	$request->free_result();
1160
1161
	// No user found ... back we go
1162
	if (empty($id_member))
1163
	{
1164
		return false;
1165
	}
1166
1167
	// Load the users profile information
1168
	$pbe = [];
1169
	if (MembersList::load($id_member, false, 'profile'))
1170
	{
1171
		$pbe['profile'] = MembersList::get($id_member);
1172
1173
		// Load in *some* user_info data just like loadUserSettings would do
1174
		if (empty($pbe['profile']['additional_groups']))
1175
		{
1176
			$pbe['user_info']['groups'] = [
1177
				$pbe['profile']['id_group'], $pbe['profile']['id_post_group']];
1178
		}
1179
		else
1180
		{
1181
			$pbe['user_info']['groups'] = array_merge(
1182
				[$pbe['profile']['id_group'], $pbe['profile']['id_post_group']],
1183
				explode(',', $pbe['profile']['additional_groups'])
1184
			);
1185
		}
1186
1187
		// Clean up the groups
1188
		$pbe['user_info']['groups'] = array_map(static function ($v) {
1189
			return (int) $v;
1190
		}, $pbe['user_info']['groups']);
1191
1192
		$pbe['user_info']['groups'] = array_unique($pbe['user_info']['groups']);
1193
1194
		// Load the user's general permissions....
1195
		query_load_permissions('general', $pbe);
1196
1197
		// Set the moderation warning level
1198
		$pbe['user_info']['warning'] = $pbe['profile']['warning'] ?? 0;
1199
1200
		// Work out our query_see_board string for security
1201
		if (in_array(1, $pbe['user_info']['groups'], true))
1202
		{
1203
			$pbe['user_info']['query_see_board'] = '1=1';
1204
		}
1205
		else
1206
		{
1207
			$pbe['user_info']['query_see_board'] = '((FIND_IN_SET(' . implode(', b.member_groups) != 0 OR FIND_IN_SET(', $pbe['user_info']['groups']) . ', b.member_groups) != 0)' . (empty($modSettings['deny_boards_access']) ? '' : ' AND (FIND_IN_SET(' . implode(', b.deny_member_groups) = 0 AND FIND_IN_SET(', $pbe['user_info']['groups']) . ', b.deny_member_groups) = 0)') . ')';
1208
		}
1209
1210
		// Set some convenience items
1211
		$pbe['user_info']['is_admin'] = in_array(1, $pbe['user_info']['groups'], true) ? 1 : 0;
1212
		$pbe['user_info']['id'] = $id_member;
1213
		$pbe['user_info']['username'] = $pbe['profile']['member_name'] ?? '';
1214
		$pbe['user_info']['name'] = $pbe['profile']['real_name'] ?? '';
1215
		$pbe['user_info']['email'] = $pbe['profile']['email_address'] ?? '';
1216
		$pbe['user_info']['language'] = empty($pbe['profile']['lngfile']) || empty($modSettings['userLanguage']) ? $language : $pbe['profile']['lngfile'];
1217
	}
1218
1219
	return empty($pbe) ? false : $pbe;
1220
}
1221
1222
/**
1223
 * Load the users permissions either general or board-specific
1224
 *
1225
 * - Similar to the functions in loadPermissions()
1226
 *
1227
 * @param string $type board to load board permissions, otherwise general permissions
1228
 * @param array $pbe
1229
 * @param array $topic_info
1230
 * @package Maillist
1231
 */
1232
function query_load_permissions($type, &$pbe, $topic_info = [])
1233
{
1234
	global $modSettings;
1235
1236
	$db = database();
1237
1238
	$where_query = ($type === 'board' ? '({array_int:member_groups}) AND id_profile = {int:id_profile}' : '({array_int:member_groups})');
1239
1240
	// Load up the users board or general site permissions.
1241
	$removals = [];
1242
	$pbe['user_info']['permissions'] = [];
1243
	$db->fetchQuery('
1244
		SELECT
1245
			permission, add_deny
1246
		FROM {db_prefix}' . ($type === 'board' ? 'board_permissions' : 'permissions') . '
1247
		WHERE id_group IN ' . $where_query,
1248
		[
1249
			'member_groups' => $pbe['user_info']['groups'],
1250
			'id_profile' => ($type === 'board') ? $topic_info['id_profile'] : '',
1251
		]
1252
	)->fetch_callback(
1253
		static function ($row) use (&$removals, &$pbe) {
1254
			if (empty($row['add_deny']))
1255
			{
1256
				$removals[] = $row['permission'];
1257
			}
1258
			else
1259
			{
1260
				$pbe['user_info']['permissions'][] = $row['permission'];
1261
			}
1262
		}
1263
	);
1264
1265
	// Remove all the permissions they shouldn't have ;)
1266
	if (!empty($modSettings['permission_enable_deny']))
1267
	{
1268
		$pbe['user_info']['permissions'] = array_diff($pbe['user_info']['permissions'], $removals);
1269
	}
1270
}
1271
1272
/**
1273
 * Reads all the keys that have been sent to a given email id
1274
 *
1275
 * - Returns all keys sent to a user in date order
1276
 *
1277
 * @param string $email email address to lookup
1278
 *
1279
 * @return array
1280
 * @package Maillist
1281
 *
1282
 */
1283
function query_user_keys($email)
1284
{
1285
	$db = database();
1286
1287
	// Find all keys sent to this email, sorted by date
1288
	return $db->fetchQuery('
1289
		SELECT
1290
			message_key, message_type, message_id
1291
		FROM {db_prefix}postby_emails
1292
		WHERE email_to = {string:email}
1293
		ORDER BY time_sent DESC',
1294
		[
1295
			'email' => $email,
1296
		]
1297
	)->fetch_all();
1298
}
1299
1300
/**
1301
 * Return the email that a given key was sent to
1302
 *
1303
 * @param EmailParse $email_message
1304
 * @return string email address the key was sent to
1305
 * @package Maillist
1306
 */
1307
function query_key_owner($email_message)
1308
{
1309
	$db = database();
1310
1311
	if ($email_message->message_key === null && $email_message->message_type === null && $email_message->message_id === null)
1312
	{
1313
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
1314
	}
1315
1316
	// Check that this is a reply to an "actual" message by finding the key in the sent email table
1317
	$request = $db->query('', '
1318
		SELECT
1319
			email_to
1320
		FROM {db_prefix}postby_emails
1321
		WHERE message_key = {string:key}
1322
			AND message_type = {string:type}
1323
			AND message_id = {string:message}
1324
		LIMIT 1',
1325
		[
1326
			'key' => $email_message->message_key,
1327
			'type' => $email_message->message_type,
1328
			'message' => $email_message->message_id,
1329
		]
1330
	);
1331
	[$email_to] = $request->fetch_row();
1332
	$request->free_result();
1333
1334
	return $email_to;
1335
}
1336
1337
/**
1338
 * For a given type, t m or p, query the appropriate table for a given message id
1339
 *
1340
 * - If found, returns the message subject
1341
 *
1342
 * @param int $message_id
1343
 * @param string $message_type
1344
 * @param string $email
1345
 *
1346
 * @return bool|string
1347
 * @package Maillist
1348
 *
1349
 */
1350
function query_load_subject($message_id, $message_type, $email)
1351
{
1352
	$db = database();
1353
1354
	$subject = '';
1355
1356
	// Load up the core topic details,
1357
	if ($message_type === 't')
1358
	{
1359
		$request = $db->query('', '
1360
			SELECT
1361
				t.id_topic, m.subject
1362
			FROM {db_prefix}topics AS t
1363
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
1364
			WHERE t.id_topic = {int:id_topic}',
1365
			[
1366
				'id_topic' => $message_id
1367
			]
1368
		);
1369
	}
1370
	elseif ($message_type === 'm')
1371
	{
1372
		$request = $db->query('', '
1373
			SELECT
1374
				m.id_topic, m.subject
1375
			FROM {db_prefix}messages AS m
1376
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1377
			WHERE m.id_msg = {int:message_id}',
1378
			[
1379
				'message_id' => $message_id
1380
			]
1381
		);
1382
	}
1383
	elseif ($message_type === 'p')
1384
	{
1385
		// With PM's ... first get the member id based on the email
1386
		$request = $db->query('', '
1387
			SELECT
1388
				id_member
1389
			FROM {db_prefix}members
1390
			WHERE email_address = {string:email}
1391
				AND is_activated = {int:act}
1392
			LIMIT 1',
1393
			[
1394
				'email' => $email,
1395
				'act' => 1,
1396
			]
1397
		);
1398
1399
		// Found them, now we find the PM to them with this ID
1400
		if ($request->num_rows() !== 0)
1401
		{
1402
			[$id_member] = $request->fetch_row();
1403
			$request->free_result();
1404
1405
			// Now find this PM ID and make sure it was sent to this member
1406
			$request = $db->query('', '
1407
				SELECT
1408
					p.subject
1409
				FROM {db_prefix}pm_recipients AS pmr, {db_prefix}personal_messages AS p
1410
				WHERE pmr.id_pm = {int:id_pm}
1411
					AND pmr.id_member = {int:id_member}
1412
					AND p.id_pm = pmr.id_pm',
1413
				[
1414
					'id_member' => $id_member,
1415
					'id_pm' => $message_id,
1416
				]
1417
			);
1418
		}
1419
	}
1420
	else
1421
	{
1422
		return $subject;
1423
	}
1424
1425
	// If we found the message, topic or PM, return the subject
1426
	if ($request->num_rows() !== 0)
1427
	{
1428
		[$subject] = $request->fetch_row();
1429
		$subject = pbe_clean_email_subject($subject);
1430
	}
1431
1432
	$request->free_result();
1433
1434
	return $subject;
1435
}
1436
1437
/**
1438
 * Loads the important information for a given topic or pm ID
1439
 *
1440
 * - Returns array with the topic or PM details
1441
 *
1442
 * @param string $message_type
1443
 * @param int $message_id
1444
 * @param array $pbe
1445
 *
1446
 * @return array|bool
1447
 * @package Maillist
1448
 *
1449
 */
1450
function query_load_message($message_type, $message_id, $pbe)
1451
{
1452
	$db = database();
1453
1454
	// Load up the topic details
1455
	if ($message_type === 't')
1456
	{
1457
		$request = $db->query('', '
1458
			SELECT
1459
				t.id_topic, t.id_board, t.locked, t.id_member_started, t.id_last_msg,
1460
				m.subject,
1461
				b.count_posts, b.id_profile, b.member_groups, b.id_theme, b.override_theme
1462
			FROM {db_prefix}topics AS t
1463
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
1464
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
1465
			WHERE {raw:query_see_board}
1466
				AND t.id_topic = {int:message_id}',
1467
			[
1468
				'message_id' => $message_id,
1469
				'query_see_board' => $pbe['user_info']['query_see_board'],
1470
			]
1471
		);
1472
	}
1473
	elseif ($message_type === 'm')
1474
	{
1475
		$request = $db->query('', '
1476
			SELECT
1477
				m.id_topic, m.id_board, m.subject,
1478
				t.locked, t.id_member_started, t.approved, t.id_last_msg,
1479
				b.count_posts, b.id_profile, b.member_groups, b.id_theme, b.override_theme
1480
			FROM {db_prefix}messages AS m
1481
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
1482
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1483
			WHERE  {raw:query_see_board}
1484
				AND m.id_msg = {int:message_id}',
1485
			[
1486
				'message_id' => $message_id,
1487
				'query_see_board' => $pbe['user_info']['query_see_board'],
1488
			]
1489
		);
1490
	}
1491
	elseif ($message_type === 'p')
1492
	{
1493
		// Load up the personal message...
1494
		$request = $db->query('', '
1495
			SELECT
1496
				p.id_pm, p.subject, p.id_member_from, p.id_pm_head
1497
			FROM {db_prefix}pm_recipients AS pm, {db_prefix}personal_messages AS p, {db_prefix}members AS mem
1498
			WHERE pm.id_pm = {int:mess_id}
1499
				AND pm.id_member = {int:id_mem}
1500
				AND p.id_pm = pm.id_pm
1501
				AND mem.id_member = p.id_member_from',
1502
			[
1503
				'id_mem' => $pbe['profile']['id_member'],
1504
				'mess_id' => $message_id
1505
			]
1506
		);
1507
	}
1508
1509
	$topic_info = [];
1510
	if (isset($request))
1511
	{
1512
		// Found the information, load the topic_info array with the data for this topic and board
1513
		if ($request->num_rows() !== 0)
1514
		{
1515
			$topic_info = $request->fetch_assoc();
1516
		}
1517
1518
		$request->free_result();
1519
	}
1520
1521
	// Return the results or false
1522
	return empty($topic_info) ? false : $topic_info;
1523
}
1524
1525
/**
1526
 * Loads the board_id for where a given message resides
1527
 *
1528
 * @param int $message_id
1529
 *
1530
 * @return int
1531
 * @package Maillist
1532
 *
1533
 */
1534
function query_load_board($message_id)
1535
{
1536
	$db = database();
1537
1538
	$request = $db->query('', '
1539
		SELECT
1540
			id_board
1541
		FROM {db_prefix}messages
1542
		WHERE id_msg = {int:message_id}',
1543
		[
1544
			'message_id' => $message_id,
1545
		]
1546
	);
1547
	[$board_id] = $request->fetch_row();
1548
	$request->free_result();
1549
1550
	return empty($board_id) ? 0 : $board_id;
1551
}
1552
1553
/**
1554
 * Loads the basic board information for a given board id
1555
 *
1556
 * @param int $board_id
1557
 * @param array $pbe
1558
 * @return array
1559
 * @package Maillist
1560
 */
1561
function query_load_board_details($board_id, $pbe)
1562
{
1563
	$db = database();
1564
1565
	// To post a NEW Topic, we need certain board details
1566
	$request = $db->query('', '
1567
		SELECT
1568
			b.count_posts, b.id_profile, b.member_groups, b.id_theme, b.id_board
1569
		FROM {db_prefix}boards AS b
1570
		WHERE {raw:query_see_board} AND id_board = {int:id_board}',
1571
		[
1572
			'id_board' => $board_id,
1573
			'query_see_board' => $pbe['user_info']['query_see_board'],
1574
		]
1575
	);
1576
	$board_info = $request->fetch_assoc();
1577
	$request->free_result();
1578
1579
	return $board_info;
1580
}
1581
1582
/**
1583
 * Loads the theme settings for the theme this user is using
1584
 *
1585
 * - Mainly used to determine a users notify setting
1586
 *
1587
 * @param int $id_member
1588
 * @param int $id_theme
1589
 * @param array $board_info
1590
 *
1591
 * @return array
1592
 * @package Maillist
1593
 *
1594
 */
1595
function query_get_theme($id_member, $id_theme, $board_info)
1596
{
1597
	global $modSettings;
1598
1599
	$db = database();
1600
1601
	$id_theme = (int) $id_theme;
1602
1603
	// Verify the id_theme...
1604
	// Allow the board-specific theme if they are overriding.
1605
	if (!empty($board_info['id_theme']) && $board_info['override_theme'])
1606
	{
1607
		$id_theme = (int) $board_info['id_theme'];
1608
	}
1609
	elseif (!empty($modSettings['knownThemes']))
1610
	{
1611
		$themes = array_map('intval', explode(',', $modSettings['knownThemes']));
1612
1613
		$id_theme = in_array($id_theme, $themes, true) ? $id_theme : (int) $modSettings['theme_guests'];
1614
	}
1615
1616
	// With the theme and member, load the auto_notify variables
1617
	$theme_settings = [];
1618
	$db->fetchQuery('
1619
		SELECT
1620
			variable, value
1621
		FROM {db_prefix}themes
1622
		WHERE id_member = {int:id_member}
1623
			AND id_theme = {int:id_theme}',
1624
		[
1625
			'id_theme' => $id_theme,
1626
			'id_member' => $id_member,
1627
		]
1628
	)->fetch_callback(
1629
		static function ($row) use (&$theme_settings) {
1630
			// Put everything about this member/theme into a theme setting array
1631
			$theme_settings[$row['variable']] = $row['value'];
1632
		}
1633
	);
1634
1635
	return $theme_settings;
1636
}
1637
1638
/**
1639
 * Turn notifications on or off if the user has set auto notify 'when I reply'
1640
 *
1641
 * @param int $id_member
1642
 * @param int $id_board
1643
 * @param int $id_topic
1644
 * @param bool $auto_notify
1645
 * @param array $permissions
1646
 * @package Maillist
1647
 */
1648
function query_notifications($id_member, $id_board, $id_topic, $auto_notify, $permissions)
1649
{
1650
	$db = database();
1651
1652
	// First, see if they have a board notification on for this board,
1653
	// so we don't set both board and individual topic notifications
1654
	$board_notify = false;
1655
	$request = $db->query('', '
1656
		SELECT
1657
			id_member
1658
		FROM {db_prefix}log_notify
1659
		WHERE id_board = {int:board_list}
1660
			AND id_member = {int:current_member}',
1661
		[
1662
			'current_member' => $id_member,
1663
			'board_list' => $id_board,
1664
		]
1665
	);
1666
	if ($request->fetch_row())
1667
	{
1668
		$board_notify = true;
1669
	}
1670
1671
	$request->free_result();
1672
1673
	// If they have topic notification on and not board notification, then
1674
	// add this post to the notification log
1675
	if (!empty($auto_notify) && (in_array('mark_any_notify', $permissions)) && !$board_notify)
1676
	{
1677
		$db->insert('ignore',
1678
			'{db_prefix}log_notify',
1679
			['id_member' => 'int', 'id_topic' => 'int', 'id_board' => 'int'],
1680
			[$id_member, $id_topic, 0],
1681
			['id_member', 'id_topic', 'id_board']
1682
		);
1683
	}
1684
	else
1685
	{
1686
		// Make sure they don't get notified
1687
		$db->query('', '
1688
			DELETE FROM {db_prefix}log_notify
1689
			WHERE id_member = {int:current_member}
1690
				AND id_topic = {int:current_topic}',
1691
			[
1692
				'current_member' => $id_member,
1693
				'current_topic' => $id_topic,
1694
			]
1695
		);
1696
	}
1697
}
1698
1699
/**
1700
 * Called when a pm reply has been made
1701
 *
1702
 * - Marks the PM replied to as read
1703
 * - Marks the PM replied to as replied to
1704
 * - Updates the number of unread to reflect this
1705
 *
1706
 * @param EmailParse $email_message
1707
 * @param array $pbe
1708
 * @package Maillist
1709
 */
1710
function query_mark_pms($email_message, $pbe)
1711
{
1712
	$db = database();
1713
1714
	$request = $db->query('', '
1715
		UPDATE {db_prefix}pm_recipients
1716
		SET 
1717
			is_read = is_read | 1
1718
		WHERE id_member = {int:id_member}
1719
			AND NOT ((is_read & 1) >= 1)
1720
			AND id_pm = {int:personal_messages}',
1721
		[
1722
			'personal_messages' => $email_message->message_id,
1723
			'id_member' => $pbe['profile']['id_member'],
1724
		]
1725
	);
1726
1727
	// If something was marked as read, get the number of unread messages remaining.
1728
	if ($request->affected_rows() > 0)
1729
	{
1730
		$total_unread = 0;
1731
		$db->fetchQuery('
1732
			SELECT
1733
				labels, COUNT(*) AS num
1734
			FROM {db_prefix}pm_recipients
1735
			WHERE id_member = {int:id_member}
1736
				AND NOT ((is_read & 1) >= 1)
1737
				AND deleted = {int:is_not_deleted}
1738
			GROUP BY labels',
1739
			[
1740
				'id_member' => $pbe['profile']['id_member'],
1741
				'is_not_deleted' => 0,
1742
			]
1743
		)->fetch_callback(
1744
			static function ($row) use (&$total_unread) {
1745
				$total_unread += $row['num'];
1746
			}
1747
		);
1748
1749
		// Update things for when they do come to the site
1750
		require_once(SUBSDIR . '/Members.subs.php');
1751
		updateMemberData($pbe['profile']['id_member'], ['unread_messages' => $total_unread]);
1752
	}
1753
1754
	// Now mark the message as "replied to" since they just did
1755
	$db->query('', '
1756
		UPDATE {db_prefix}pm_recipients
1757
		SET 
1758
			is_read = is_read | 2
1759
		WHERE id_pm = {int:replied_to}
1760
			AND id_member = {int:current_member}',
1761
		[
1762
			'current_member' => $pbe['profile']['id_member'],
1763
			'replied_to' => $email_message->message_id,
1764
		]
1765
	);
1766
}
1767
1768
/**
1769
 * Once a key has been used, it is removed and cannot be used again
1770
 *
1771
 * - Also removes any old keys to minimize security issues
1772
 *
1773
 * @param EmailParse $email_message
1774
 * @package Maillist
1775
 */
1776
function query_key_maintenance($email_message)
1777
{
1778
	global $modSettings;
1779
1780
	$db = database();
1781
1782
	// Old keys simply expire
1783
	$days = (empty($modSettings['maillist_key_active'])) ? 21 : $modSettings['maillist_key_active'];
1784
	$delete_old = time() - ($days * 24 * 60 * 60);
1785
1786
	// Consume the database key that was just used ... one reply per key,
1787
	// but we let PM's slide, they often seem to be re re re replied to
1788
	if ($email_message->message_type !== 'p')
1789
	{
1790
		$db->query('', '
1791
			DELETE FROM {db_prefix}postby_emails
1792
			WHERE message_key = {string:key}
1793
				AND message_type = {string:type}
1794
				AND message_id = {string:message_id}',
1795
			[
1796
				'key' => $email_message->message_key,
1797
				'type' => $email_message->message_type,
1798
				'message_id' => $email_message->message_id,
1799
			]
1800
		);
1801
	}
1802
1803
	// Since we are here, let's delete any items older than delete_old days
1804
	// if they have not responded in that time tuff
1805
	$db->query('', '
1806
		DELETE FROM {db_prefix}postby_emails
1807
		WHERE time_sent < {int:delete_old}',
1808
		[
1809
			'delete_old' => $delete_old
1810
		]
1811
	);
1812
}
1813
1814
/**
1815
 * After an email post has been made, this updates the user information just like
1816
 * they are on the site to perform the given action.
1817
 *
1818
 * - Updates time online
1819
 * - Updates last active
1820
 * - Updates the who's online list with the member and action
1821
 *
1822
 * @param array $pbe
1823
 * @param EmailParse $email_message
1824
 * @param array $topic_info
1825
 * @package Maillist
1826
 */
1827
function query_update_member_stats($pbe, $email_message, $topic_info = [])
1828
{
1829
	$db = database();
1830
1831
	$last_login = time();
1832
	$do_delete = false;
1833
	$total_time_logged_in = empty($pbe['profile']['total_time_logged_in']) ? 0 : $pbe['profile']['total_time_logged_in'];
1834
1835
	// If they were active in the last 15 min, we don't want to run up their time
1836
	if (!empty($pbe['profile']['last_login']) && $pbe['profile']['last_login'] < (time() - (60 * 15)))
1837
	{
1838
		// Not recently active, so add some time to their login.
1839
		$do_delete = true;
1840
		$total_time_logged_in += 60 * 10;
1841
	}
1842
1843
	// Update the members' total time logged-in data
1844
	require_once(SUBSDIR . '/Members.subs.php');
1845
	updateMemberData($pbe['profile']['id_member'], ['total_time_logged_in' => $total_time_logged_in, 'last_login' => $last_login]);
1846
1847
	// Show they are active in the who's online list and what they have done
1848
	if ($email_message->message_type === 'm' || $email_message->message_type === 't')
1849
	{
1850
		$get_temp = [
1851
			'action' => 'postbyemail',
1852
			'topic' => $topic_info['id_topic'],
1853
			'last_msg' => $topic_info['id_last_msg'],
1854
			'board' => $topic_info['id_board']
1855
		];
1856
	}
1857
	elseif ($email_message->message_type === 'x')
1858
	{
1859
		$get_temp = [
1860
			'action' => 'topicbyemail',
1861
			'topic' => $topic_info['id'],
1862
			'board' => $topic_info['board'],
1863
		];
1864
	}
1865
	else
1866
	{
1867
		$get_temp = [
1868
			'action' => 'pm',
1869
			'sa' => 'byemail'
1870
		];
1871
	}
1872
1873
	// Place the entry in to the online log so who's online can use it
1874
	$serialized = serialize($get_temp);
1875
	$session_id = 'ip' . $pbe['profile']['member_ip'];
1876
	$member_ip = empty($pbe['profile']['member_ip']) ? 0 : $pbe['profile']['member_ip'];
1877
	$db->insert($do_delete ? 'ignore' : 'replace',
1878
		'{db_prefix}log_online',
1879
		['session' => 'string', 'id_member' => 'int', 'id_spider' => 'int', 'log_time' => 'int', 'ip' => 'string', 'url' => 'string'],
1880
		[$session_id, $pbe['profile']['id_member'], 0, $last_login, $member_ip, $serialized],
1881
		['session']
1882
	);
1883
}
1884
1885
/**
1886
 * Calls the necessary functions to extract and format the message for posting
1887
 *
1888
 * What it does:
1889
 *
1890
 * - Converts an email response (text or HTML) to a BBC equivalent via pbe_Email_to_bbc
1891
 * - Formats the email response such that it looks structured and not chopped up (via pbe_fix_email_body)
1892
 *
1893
 * @param EmailParse $email_message
1894
 * @param array $pbe
1895
 *
1896
 * @return string
1897
 * @package Maillist
1898
 */
1899
function pbe_load_text($email_message, $pbe)
1900
{
1901
	$html = $email_message->html_found;
1902
1903
	$text = $html ? pbe_load_html($email_message, $html) : $email_message->getPlainBody();
1904
1905
	// Convert to BBC and format it, so it looks like a post
1906
	$text = pbe_email_to_bbc($text, $html);
1907
1908
	$pbe_real_name = $pbe['profile']['real_name'] ?? '';
1909
	$text = pbe_fix_email_body($text, $pbe_real_name, (empty($email_message->_converted_utf8) ? $email_message->headers['x-parameters']['content-type']['charset'] : 'UTF-8'));
1910
1911
	// Do we even have a message left to post?
1912
	$text = Util::htmltrim($text);
1913
	if (empty($text))
1914
	{
1915
		return '';
1916
	}
1917
1918
	// PM's are handled by sendpm
1919
	if ($email_message->message_type !== 'p')
1920
	{
1921
		// Prepare it for the database
1922
		$text = Util::htmlspecialchars($text, ENT_QUOTES, 'UTF-8', true);
1923
		require_once(SUBSDIR . '/Post.subs.php');
1924
		preparsecode($text);
1925
	}
1926
1927
	return $text;
1928
}
1929
1930
/**
1931
 * Checks and removes role=presentation tables.  If too many tables remain, returns the plain text
1932
 * version of the email as converting too many HTML tables to BBC simply will not look good
1933
 *
1934
 * @param EmailParse $email_message
1935
 * @param bool $html
1936
 * @return string
1937
 */
1938
function pbe_load_html($email_message, &$html)
1939
{
1940
	// un_htmlspecialchars areas outside code blocks
1941
	$preparse = PreparseCode::instance('');
1942
	$text = $preparse->tokenizeCodeBlocks($email_message->body, true);
1943
	$text = un_htmlspecialchars($text);
1944
	$text = $preparse->restoreCodeBlocks($text);
1945
1946
	// If we are dealing with tables ....
1947
	if (preg_match('~<table.*?>~i', $text))
1948
	{
1949
		// Try and strip out purely presentational ones, for example, how we send HTML emails
1950
		$text = preg_replace_callback('~(<table[^>].*?role="presentation".*?>.*?</table>)~s',
1951
			static function ($matches) {
1952
				$result = preg_replace('~<table[^>].*?role="presentation".*?>~', '', $matches[0]);
1953
				$result = str_replace('</table>', '', $result);
1954
				return preg_replace('~<tr.*?>|</tr>|<td.*?>|</td>|<tbody.*?>|</tbody>~', '', $result);
1955
			},
1956
			$text);
1957
1958
		// Another check is in order, still to many tables?
1959
		if (preg_match_all('~<table.*?>~i', $text) > 2)
1960
		{
1961
			$text = $email_message->getPlainBody();
1962
			$html = false;
1963
		}
1964
	}
1965
1966
	return $text;
1967
}
1968
1969
/**
1970
 * Attempts to create a reply post on the forum
1971
 *
1972
 * What it does:
1973
 *
1974
 * - Checks if the user has permissions to post/reply/postby email
1975
 * - Calls pbe_load_text to prepare text for the post
1976
 * - returns true if successful or false for any number of failures
1977
 *
1978
 * @param array $pbe array of all pbe user_info values
1979
 * @param EmailParse $email_message
1980
 * @param array $topic_info
1981
 *
1982
 * @return bool
1983
 * @package Maillist
1984
 *
1985
 */
1986
function pbe_create_post($pbe, $email_message, $topic_info)
1987
{
1988
	global $modSettings, $txt;
1989
1990
	// Validate they have permission to reply
1991
	$becomesApproved = true;
1992
	if (!$pbe['user_info']['is_admin'] && !in_array('postby_email', $pbe['user_info']['permissions'], true))
1993
	{
1994
		return pbe_emailError('error_permission', $email_message);
1995
	}
1996
1997
	if ($topic_info['locked'] && !$pbe['user_info']['is_admin'] && !in_array('moderate_forum', $pbe['user_info']['permissions'], true))
1998
	{
1999
		return pbe_emailError('error_locked', $email_message);
2000
	}
2001
2002
	if ($topic_info['id_member_started'] === $pbe['profile']['id_member'] && !$pbe['user_info']['is_admin'])
2003
	{
2004
		if ($modSettings['postmod_active'] && in_array('post_unapproved_replies_any', $pbe['user_info']['permissions'], true) && (!in_array('post_reply_any', $pbe['user_info']['permissions'])))
2005
		{
2006
			$becomesApproved = false;
2007
		}
2008
		elseif (!in_array('post_reply_own', $pbe['user_info']['permissions'], true))
2009
		{
2010
			return pbe_emailError('error_cant_reply', $email_message);
2011
		}
2012
	}
2013
	elseif (!$pbe['user_info']['is_admin'])
2014
	{
2015
		if ($modSettings['postmod_active'] && in_array('post_unapproved_replies_any', $pbe['user_info']['permissions'], true) && (!in_array('post_reply_any', $pbe['user_info']['permissions'])))
2016
		{
2017
			$becomesApproved = false;
2018
		}
2019
		elseif (!in_array('post_reply_any', $pbe['user_info']['permissions'], true))
2020
		{
2021
			return pbe_emailError('error_cant_reply', $email_message);
2022
		}
2023
	}
2024
2025
	// Convert to BBC and Format the message
2026
	$text = pbe_load_text($email_message, $pbe);
2027
	if (empty($text))
2028
	{
2029
		return pbe_emailError('error_no_message', $email_message);
2030
	}
2031
2032
	// Seriously? Attachments?
2033
	if (!empty($email_message->attachments) && !empty($modSettings['maillist_allow_attachments']) && !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1)
2034
	{
2035
		if (($modSettings['postmod_active'] && in_array('post_unapproved_attachments', $pbe['user_info']['permissions'])) || in_array('post_attachment', $pbe['user_info']['permissions']))
2036
		{
2037
			$attachIDs = pbe_email_attachments($pbe, $email_message);
2038
		}
2039
		else
2040
		{
2041
			$text .= "\n\n" . $txt['error_no_attach'] . "\n";
2042
		}
2043
	}
2044
2045
	// Set up the post-variables.
2046
	$msgOptions = [
2047
		'id' => 0,
2048
		'subject' => str_starts_with($topic_info['subject'], trim($pbe['response_prefix'])) ? $topic_info['subject'] : $pbe['response_prefix'] . $topic_info['subject'],
2049
		'smileys_enabled' => true,
2050
		'body' => $text,
2051
		'attachments' => empty($attachIDs) ? [] : $attachIDs,
2052
		'approved' => $becomesApproved
2053
	];
2054
2055
	$topicOptions = [
2056
		'id' => $topic_info['id_topic'],
2057
		'board' => $topic_info['id_board'],
2058
		'mark_as_read' => true,
2059
		'is_approved' => !$modSettings['postmod_active'] || empty($topic_info['id_topic']) || !empty($topic_info['approved'])
2060
	];
2061
2062
	$posterOptions = [
2063
		'id' => $pbe['profile']['id_member'],
2064
		'name' => $pbe['profile']['real_name'],
2065
		'email' => $pbe['profile']['email_address'],
2066
		'update_post_count' => empty($topic_info['count_posts']),
2067
		'ip' => $email_message->load_ip() ? $email_message->ip : $pbe['profile']['member_ip']
2068
	];
2069
2070
	// Make the post.
2071
	createPost($msgOptions, $topicOptions, $posterOptions);
2072
2073
	// Bind any attachments that may be included in this new message
2074
	if (!empty($attachIDs) && !empty($msgOptions['id']))
2075
	{
2076
		bindMessageAttachments($msgOptions['id'], $attachIDs);
2077
	}
2078
2079
	// We need the auto_notify setting, it may be theme-based so pass the theme in use
2080
	$theme_settings = query_get_theme($pbe['profile']['id_member'], $pbe['profile']['id_theme'], $topic_info);
2081
	$auto_notify = $theme_settings['auto_notify'] ?? 0;
2082
2083
	// Turn notifications on or off
2084
	query_notifications($pbe['profile']['id_member'], $topic_info['id_board'], $topic_info['id_topic'], $auto_notify, $pbe['user_info']['permissions']);
0 ignored issues
show
Bug introduced by
It seems like $auto_notify can also be of type integer; however, parameter $auto_notify of query_notifications() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

2084
	query_notifications($pbe['profile']['id_member'], $topic_info['id_board'], $topic_info['id_topic'], /** @scrutinizer ignore-type */ $auto_notify, $pbe['user_info']['permissions']);
Loading history...
2085
2086
	// Notify members who have notification turned on for this,
2087
	// but only if it's going to be approved
2088
	if ($becomesApproved)
2089
	{
2090
		require_once(SUBSDIR . '/Notification.subs.php');
2091
		sendNotifications($topic_info['id_topic'], 'reply', [], [], $pbe);
2092
	}
2093
2094
	return true;
2095
}
2096
2097
/**
2098
 * Attempts to create a PM (reply) on the forum
2099
 *
2100
 * What it does
2101
 * - Checks if the user has permissions
2102
 * - Calls pbe_load_text to prepare text for the pm
2103
 * - Calls query_mark_pms to mark things as read
2104
 * - Returns true if successful or false for any number of failures
2105
 *
2106
 * @param array $pbe array of pbe 'user_info' values
2107
 * @param EmailParse $email_message
2108
 * @param array $pm_info
2109
 *
2110
 * @return bool
2111
 * @package Maillist
2112
 *
2113
 * @uses sendpm to do the actual "sending"
2114
 */
2115
function pbe_create_pm($pbe, $email_message, $pm_info)
2116
{
2117
	global $modSettings, $txt;
2118
2119
	// Can they send?
2120
	if (!$pbe['user_info']['is_admin'] && !in_array('pm_send', $pbe['user_info']['permissions'], true))
2121
	{
2122
		return pbe_emailError('error_pm_not_allowed', $email_message);
2123
	}
2124
2125
	// Convert the PM to BBC and Format the message
2126
	$text = pbe_load_text($email_message, $pbe);
2127
	if (empty($text))
2128
	{
2129
		return pbe_emailError('error_no_message', $email_message);
2130
	}
2131
2132
	// If they tried to attach a file, just say, sorry
2133
	if (!empty($email_message->attachments) && !empty($modSettings['maillist_allow_attachments']) && !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1)
2134
	{
2135
		$text .= "\n\n" . $txt['error_no_pm_attach'] . "\n";
2136
	}
2137
2138
	// For sending the message...
2139
	$from = [
2140
		'id' => $pbe['profile']['id_member'],
2141
		'name' => $pbe['profile']['real_name'],
2142
		'username' => $pbe['profile']['member_name']
2143
	];
2144
2145
	$pm_info['subject'] = str_starts_with($pm_info['subject'], trim($pbe['response_prefix'])) ? $pm_info['subject'] : $pbe['response_prefix'] . $pm_info['subject'];
2146
2147
	// send/save the actual PM.
2148
	require_once(SUBSDIR . '/PersonalMessage.subs.php');
2149
	$pm_result = sendpm(['to' => [$pm_info['id_member_from']], 'bcc' => []], $pm_info['subject'], $text, true, $from, $pm_info['id_pm_head']);
2150
2151
	// Assuming all went well, mark this as read, replied to and update the unread counter
2152
	if (!empty($pm_result))
2153
	{
2154
		query_mark_pms($email_message, $pbe);
2155
	}
2156
2157
	return !empty($pm_result);
2158
}
2159
2160
/**
2161
 * Create a new topic by email
2162
 *
2163
 * What it does:
2164
 *
2165
 * - Called by pbe_topic to create a new topic or by pbe_main to create a new topic via a subject change.
2166
 * - checks posting permissions, but requires all email validation checks are complete.
2167
 * - Calls pbe_load_text to prepare text for the post.
2168
 * - Calls sendNotifications to announce the new post.
2169
 * - Calls query_update_member_stats to show they did something.
2170
 * - Requires the pbe, email_message, and board_info arrays to be populated.
2171
 *
2172
 * @param array $pbe array of pbe 'user_info' values
2173
 * @param EmailParse $email_message
2174
 * @param array $board_info
2175
 *
2176
 * @return bool
2177
 * @package Maillist
2178
 *
2179
 * @uses createPost to do the actual "posting"
2180
 */
2181
function pbe_create_topic($pbe, $email_message, $board_info)
2182
{
2183
	global $txt, $modSettings;
2184
2185
	// It does not work like that
2186
	if (empty($pbe) || empty($email_message))
2187
	{
2188
		return false;
2189
	}
2190
2191
	// We have the board info and their permissions - do they have a right to start a new topic?
2192
	$becomesApproved = true;
2193
	if (!$pbe['user_info']['is_admin'])
2194
	{
2195
		if (!in_array('postby_email', $pbe['user_info']['permissions'], true))
2196
		{
2197
			return pbe_emailError('error_permission', $email_message);
2198
		}
2199
2200
		if ($modSettings['postmod_active'] && in_array('post_unapproved_topics', $pbe['user_info']['permissions']) && (!in_array('post_new', $pbe['user_info']['permissions'])))
2201
		{
2202
			$becomesApproved = false;
2203
		}
2204
		elseif (!in_array('post_new', $pbe['user_info']['permissions'], true))
2205
		{
2206
			return pbe_emailError('error_cant_start', $email_message);
2207
		}
2208
	}
2209
2210
	// Approving all new topics by email anyway, smart admin this one is ;)
2211
	if (!empty($modSettings['maillist_newtopic_needsapproval']))
2212
	{
2213
		$becomesApproved = false;
2214
	}
2215
2216
	// First on the agenda the subject
2217
	$subject = pbe_clean_email_subject($email_message->subject);
2218
	$subject = strtr(Util::htmlspecialchars($subject), ["\r" => '', "\n" => '', "\t" => '']);
2219
2220
	// Not too long, not too short
2221
	if (Util::strlen($subject) > 100)
2222
	{
2223
		$subject = Util::substr($subject, 0, 100);
2224
	}
2225
2226
	if ($subject === '')
2227
	{
2228
		return pbe_emailError('error_no_subject', $email_message);
2229
	}
2230
2231
	// The message itself will need a bit of work
2232
	$text = pbe_load_text($email_message, $pbe);
2233
	if (empty($text))
2234
	{
2235
		return pbe_emailError('error_no_message', $email_message);
2236
	}
2237
2238
	// Build the attachment array if needed
2239
	if (!empty($email_message->attachments) && !empty($modSettings['maillist_allow_attachments']) && !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1)
2240
	{
2241
		if (($modSettings['postmod_active'] && in_array('post_unapproved_attachments', $pbe['user_info']['permissions'])) || in_array('post_attachment', $pbe['user_info']['permissions']))
2242
		{
2243
			$attachIDs = pbe_email_attachments($pbe, $email_message);
2244
		}
2245
		else
2246
		{
2247
			$text .= "\n\n" . $txt['error_no_attach'] . "\n";
2248
		}
2249
	}
2250
2251
	// If we get to this point ... then it's time to play, let's start a topic!
2252
	require_once(SUBSDIR . '/Post.subs.php');
2253
2254
	// Set up the topic variables.
2255
	$msgOptions = [
2256
		'id' => 0,
2257
		'subject' => $subject,
2258
		'smileys_enabled' => true,
2259
		'body' => $text,
2260
		'attachments' => empty($attachIDs) ? [] : $attachIDs,
2261
		'approved' => $becomesApproved
2262
	];
2263
2264
	$topicOptions = [
2265
		'id' => 0,
2266
		'board' => $board_info['id_board'],
2267
		'mark_as_read' => false
2268
	];
2269
2270
	$posterOptions = [
2271
		'id' => $pbe['profile']['id_member'],
2272
		'name' => $pbe['profile']['real_name'],
2273
		'email' => $pbe['profile']['email_address'],
2274
		'update_post_count' => empty($board_info['count_posts']),
2275
		'ip' => $email_message->ip ?? $pbe['profile']['member_ip']
2276
	];
2277
2278
	// Attempt to make the new topic.
2279
	createPost($msgOptions, $topicOptions, $posterOptions);
2280
2281
	// Bind any attachments that may be included to this new topic
2282
	if (!empty($attachIDs) && !empty($msgOptions['id']))
2283
	{
2284
		bindMessageAttachments($msgOptions['id'], $attachIDs);
2285
	}
2286
2287
	// The auto_notify setting
2288
	$theme_settings = query_get_theme($pbe['profile']['id_member'], $pbe['profile']['id_theme'], $board_info);
2289
	$auto_notify = $theme_settings['auto_notify'] ?? 0;
2290
2291
	// Notifications on or off
2292
	query_notifications($pbe['profile']['id_member'], $board_info['id_board'], $topicOptions['id'], $auto_notify, $pbe['user_info']['permissions']);
0 ignored issues
show
Bug introduced by
It seems like $auto_notify can also be of type integer; however, parameter $auto_notify of query_notifications() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

2292
	query_notifications($pbe['profile']['id_member'], $board_info['id_board'], $topicOptions['id'], /** @scrutinizer ignore-type */ $auto_notify, $pbe['user_info']['permissions']);
Loading history...
2293
2294
	// Notify members who have notification turned on for this (if it's approved)
2295
	if ($becomesApproved)
2296
	{
2297
		require_once(SUBSDIR . '/Notification.subs.php');
2298
		sendNotifications($topicOptions['id'], 'reply', [], [], $pbe);
2299
	}
2300
2301
	// Update this users info so the log shows them as active
2302
	query_update_member_stats($pbe, $email_message, $topicOptions);
2303
2304
	return true;
2305
}
2306