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 dev
12
 *
13
 */
14
15
use BBC\PreparseCode;
16
use ElkArte\Cache\Cache;
17
use ElkArte\Converters\Html2BBC;
18
use ElkArte\Converters\Html2Md;
19
use ElkArte\Helper\Util;
20
use ElkArte\Languages\Txt;
21
use ElkArte\Mail\PreparseMail;
22
use ElkArte\Maillist\EmailFormat;
23
use ElkArte\Maillist\EmailParse;
24
use ElkArte\MembersList;
25
use ElkArte\Notifications\Notifications;
26
use ElkArte\Notifications\NotificationsTask;
27
28
/**
29
 * Converts text / HTML to BBC
30
 *
31
 * What it does:
32
 *
33
 * - protects certain tags from conversion
34
 * - strips original message from the reply if possible
35
 * - If the email is html based, this will convert basic html tags to bbc tags
36
 * - If the email is plain text it will convert it to html based on markdown text
37
 * conventions and then that will be converted to bbc.
38
 *
39
 * @param string $text plain or html text
40
 * @param bool $html
41
 *
42
 * @return string
43
 * @uses Html2BBC.class.php for the html to bbc conversion
44
 * @uses markdown.php for text to html conversions
45
 * @package Maillist
46
 */
47
function pbe_email_to_bbc($text, $html)
48
{
49
	// Define some things that need to be converted/modified, outside normal html or markup
50
	$tags = [
51
		'~\*\*\s?(.*?)\*\*~is' => '**$1**', // set as markup bold
52
		'~<\*>~' => '&lt;*&gt;', // <*> as set in default Mailist Templates
53
		'~^-{3,}~' => '<hr>', // 3+ --- to hr
54
		'~#([0-9a-fA-F]{4,6}\b)~' => '&#35;$1', // HTML entities
55
	];
56
57
	// We are starting with HTML, our goal is to convert the best parts of it to BBC,
58
	$text = pbe_run_parsers($text);
59
60
	if ($html)
61
	{
62
		// upfront pre-process $tags, mostly for the email template strings
63
		$text = preg_replace(array_keys($tags), array_values($tags), $text);
64
	}
65
	// Starting with plain text, possibly even markdown style ;)
66
	else
67
	{
68
		// Set a gmail flag for special quote processing since its quotes are strange
69
		$gmail = (bool) preg_match('~<div class="gmail_quote">~i', $text);
70
71
		// Attempt to fix textual ('>') quotes so we also fix wrapping issues first!
72
		$text = pbe_fix_email_quotes($text, $gmail);
73
		$text = str_replace(['[quote]', '[/quote]'], ['&gt;blockquote>', '&gt;/blockquote>'], $text);
74
75
		// Convert this (markup) text to html
76
		require_once(EXTDIR . '/markdown/markdown.php');
77
78
		$text = preg_replace(array_keys($tags), array_values($tags), $text);
79
		$text = Markdown($text);
0 ignored issues
show
Bug introduced by
The function Markdown was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

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

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

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

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