<?php
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 6.2.3 - Licence Number L3138846
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2026 MH Sub I, LLC dba vBulletin. All Rights Reserved.  # ||
|| # This file may not be redistributed in whole or significant part.   # ||
|| # ----------------- VBULLETIN IS NOT FREE SOFTWARE ----------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html   # ||
|| ###################################################################### ||
\*========================================================================*/


// #################### PRE-CACHE TEMPLATES AND DATA ######################
global $phrasegroups, $specialtemplates, $vbphrase, $vbulletin;
$phrasegroups = ['user', 'cphome', 'cpuser', 'messaging', 'cprofilefield', 'profilefield', 'subscription'];
$specialtemplates = [];

// ########################## REQUIRE BACK-END ############################
require_once(dirname(__FILE__) . '/global.php');
require_once(DIR . '/includes/adminfunctions_profilefield.php');
require_once(DIR . '/includes/adminfunctions_user.php');
$assertor = vB::getDbAssertor();

// ######################## CHECK ADMIN PERMISSIONS #######################
if (!vB::getUserContext()->hasAdminPermission('canadminusers'))
{
	print_cp_no_permission();
}

// ############################# LOG ACTION ###############################
vB_Utilities::logAdminAction('email.php', $_REQUEST['do']);

// ########################################################################
// ######################### START MAIN SCRIPT ############################
// ########################################################################

print_cp_header($vbphrase['send_email_to_users']);

if (empty($_REQUEST['do']))
{
	$_REQUEST['do'] = 'start';
}

// *************************** Send a page of emails **********************
if ($_POST['do'] == 'dosend' OR $_POST['do'] == 'makelist')
{
	$vbulletin->input->clean_array_gpc('p', [
		'type'              => vB_Cleaner::TYPE_STR,
		'user'              => vB_Cleaner::TYPE_ARRAY,
		'profile'           => vB_Cleaner::TYPE_ARRAY,
		'septext'           => vB_Cleaner::TYPE_NOTRIM,
		'perpage'           => vB_Cleaner::TYPE_UINT,
		'startat'           => vB_Cleaner::TYPE_UINT,
		'test'              => vB_Cleaner::TYPE_BOOL,
		'replyto'           => vB_Cleaner::TYPE_STR,
		'subject'           => vB_Cleaner::TYPE_STR,
		'message'           => vB_Cleaner::TYPE_STR,
		'pmmessage'         => vB_Cleaner::TYPE_STR,
		'serializeduser'    => vB_Cleaner::TYPE_STR,
		'serializedprofile' => vB_Cleaner::TYPE_STR,
	]);

	$type = $vbulletin->GPC['type'];
	$septext = nl2br(htmlspecialchars_uni($vbulletin->GPC['septext']));

	$perpage = $vbulletin->GPC['perpage'] ?: 500;
	$startat = $vbulletin->GPC['startat'];

	// We should either have the array values or the serialized values but not both.
	$user = ($vbulletin->GPC['serializeduser'] ? vb_unserialize(verify_client_string($vbulletin->GPC['serializeduser'])) : $vbulletin->GPC['user']);
	$profile = ($vbulletin->GPC['serializedprofile'] ? vb_unserialize(verify_client_string($vbulletin->GPC['serializedprofile'])) : $vbulletin->GPC['profile']);

	$replyto = $vbulletin->GPC['replyto'];
	$subject = $vbulletin->GPC['subject'];

	// Only one of these should be set.  It might be better to consolidate them into one value but it was
	// easier on the UI to show/hide the entire control rather than try to update the text surrounding it.
	$message = $vbulletin->GPC['message'];
	$pmmessage = $vbulletin->GPC['pmmessage'];

	$istest = $vbulletin->GPC['test'];
	$issend = ($_POST['do'] == 'dosend');

	$options = [];
	if ($issend)
	{
		if ($type == 'email')
		{
			// This validation could but pushed to the action functions, since we don't use these variables until then
			// but if we're going to fail it's better to do that before we run the query.
			if ($replyto AND !vB_String::isValidEmail($replyto))
			{
				print_stop_message('invalid_email_address');
			}

			if ($subject == '' OR $message == '')
			{
				print_stop_message('please_complete_required_fields');
			}
		}
		else if ($type == 'pm')
		{
			if ($subject == '' OR $pmmessage == '')
			{
				print_stop_message('please_complete_required_fields');
			}
		}
		else
		{
			print_stop_message(['unexpected_error', 'Invalid message type']);
		}

		// This is all kinds of strange.  We can only set the batch info if we pass activation = 1
		// This joins in the activation table (though a left join so it doesn't implicitly filter out
		// users that don't have it) and returns some extra fields (though the comments indicate
		// they aren't actually used.  All I can think is that we started with sending activation codes and generalized
		// to any message and the code hasn't quite caught up to that change. This should probably get cleaned up but not now.
		$options = [
			'activation' => 1,
			vB_dB_Query::PARAM_LIMITPAGE => $startat,
			vB_dB_Query::PARAM_LIMIT => $perpage,
		];
	}

	/** @var vB_Api_User */
	$userApi = vB_Api::instanceInternal('user');
	$users = $userApi->generateMailingList($user, $profile, $options);
	if ($users['totalcount'] == 0)
	{
		print_stop_message('no_users_matched_your_query');
	}

	// Batched processes either sending or testing the send.
	if ($issend)
	{
		vB_Utility_Functions::setPhpTimeout(0);

		if (!count($users['list']))
		{
			print_stop_message(($type == 'email' ? 'emails_sent_successfully' : 'pms_sent_successfully'), get_admincp_url('email', ['type' => $type]));
		}

		$endcount = $startat + $perpage;
		if ($endcount > $users['totalcount'])
		{
			$endcount = $users['totalcount'];
		}

		$stepmessage = construct_phrase(
			$vbphrase['showing_users_x_to_y_of_z'],
			vb_number_format($startat + 1),
			vb_number_format($endcount),
			vb_number_format($users['totalcount'])
		);

		if ($istest)
		{
			send_batched_email_test($vbphrase, $users, $stepmessage);
		}
		else
		{
			if ($type == 'email')
			{
				send_batched_email($vbphrase, $users, $stepmessage, $subject, $message, $replyto);
			}
			else if ($type == 'pm')
			{
				send_batched_pm($vbphrase, $users, $stepmessage, $subject, $pmmessage);
			}
		}

		$fields = [
			'type' => $type,
			'test' => $istest,
			'serializeduser' => sign_client_string(serialize($user)),
			'serializedprofile' => sign_client_string(serialize($profile)),
			'replyto' => $replyto,
			'subject' => $subject,
			'message' => $message,
			'pmmessage' => $pmmessage,
			'startat' => $startat + $perpage,
			'perpage' => $perpage,
		];
		print_form_redirect('admincp/email', 'dosend', $vbphrase['next_page'], ($istest ? 3 : 0), $fields);
	}
	else
	{
		// just print the list of emails if we aren't sending.
		$emails = array_unique(array_column($users['list'], 'email'));
		foreach ($emails AS $email)
		{
			echo $email, $septext;
			vbflush();
		}
	}
}

function send_batched_email_test(array $vbphrase, array $users, string $stepmessage) : void
{
	// I'm not sure how useful this really is but keeping it as it was.
	// It definitely needs to be cleaned up substantially or just removed.
	echo '<p><b>' . $vbphrase['emailing'] . '<br />' . $stepmessage . '</b></p>';
	foreach ($users['list'] AS $user)
	{
		echo "$user[userid] - $user[username] .... \n";
		echo $vbphrase['test'] . " ... \n";
		echo $vbphrase['okay'] . "<br />\n";
		vbflush();
	}
}

function send_batched_email(array $vbphrase, array $users, string $stepmessage, string $subject, string $message, string $replyto) : void
{
	echo '<p><b>' . $vbphrase['emailing'] . '<br />' . $stepmessage . '</b></p>';

	$hasactivateid = (
		strpos($message, '$activateid') !== false OR
		strpos($message, '$activatelink') !== false
	);

	// This isn't user specific so we only have to do it once.
	$vboptions = vB::getDatastore()->getValue('options');
	$sendmessage = str_replace(
		['$bburl', '$bbtitle'],
		[$vboptions['bburl'], $vboptions['bbtitle']],
		$message
	);

	$userLib = vB_Library_User::instance();
	foreach ($users['list'] AS $user)
	{
		echo "$user[userid] - $user[username] .... \n";
		vbflush();

		$replace = [
			'$email' => $user['email'],
			'$username' => $user['username'],
			'$userid' => $user['userid'],
			'$activateid' => '',
			'$activatelink' => '',
		];

		if ($hasactivateid AND $user['usergroupid'] == 3)
		{
			$activate = $userLib->buildActivationInfo($user['userid'], 2);
			$replace['$activateid'] = $activate['id'];
			$replace['$activatelink'] = $activate['url'];
		}

		echo $vbphrase['emailing'] . " \n";
		// Skipping addUnsubscribeHtml() & isUserOptedOutOfEmail() here since this should be a one-off admin email.
		// May change this in the future based on feedback however.
		// Should this allow HTML or escape as to be WYSIWYG? For now, allowing HTML as this is an adminCP script.
		$mail = vB_Mail::create2($user['email'], $subject, str_replace(array_keys($replace), array_values($replace), $sendmessage));
		if ($replyto)
		{
			$mail->addReplyTo($replyto);
		}
		$mail->send(true);

		echo $vbphrase['okay'] . "<br />\n";
		vbflush();
	}
}

function send_batched_pm(array $vbphrase, array $users, string $stepmessage, string $subject, string $message) : void
{
	echo '<p><b>' . $vbphrase['sending'] . '<br />' . $stepmessage . '</b></p>';

	// This isn't user specific so we only have to do it once.
	$vboptions = vB::getDatastore()->getValue('options');
	$sendmessage = str_replace(
		['$bburl', '$bbtitle'],
		[$vboptions['bburl'], $vboptions['bbtitle']],
		$message
	);

	$currentUserId = vB::getCurrentSession()->get('userid');
	$pmLib = vB_Library_Content_Privatemessage::instance();
	foreach ($users['list'] AS $user)
	{
		echo "$user[userid] - $user[username] .... \n";
		vbflush();

		$replace = [
			'$email' => $user['email'],
			'$username' => $user['username'],
			'$userid' => $user['userid'],
		];

		echo $vbphrase['sending'] . " \n";

		// We use the library here because the API doesn't allow skipFloodCheck (which shouldn't apply to
		// admins regardless, but better to be specific).  This means we need to set the sender explicitly.
		$data = [
			'sentto' => $user['userid'],
			'title' => $subject,
			'rawtext' => str_replace(array_keys($replace), array_values($replace), $sendmessage),
			'sender' => $currentUserId,
		];

		$pmLib->add($data, ['skipFloodCheck' => true, 'skipDupCheck' => true]);
		echo $vbphrase['okay'] . "<br />\n";
		vbflush();
	}
}

// *************************** Send invite emails **********************
if ($_POST['do'] == 'doinvite')
{
	$vbulletin->input->clean_array_gpc('p', [
		'replyto'    => vB_Cleaner::TYPE_STR,
		'to'      => vB_Cleaner::TYPE_STR,
		'subject' => vB_Cleaner::TYPE_STR,
		'message' => vB_Cleaner::TYPE_STR,
	]);

	$replyto = $vbulletin->GPC['replyto'];
	if ($replyto AND !vB_String::isValidEmail($replyto))
	{
		print_stop_message('invalid_email_address');
	}

	// verify basic fields
	if ($vbulletin->GPC['to'] == '' OR $vbulletin->GPC['subject'] == '' OR $vbulletin->GPC['message'] == '')
	{
		print_stop_message('please_complete_required_fields');
	}

	// verify the "to" email addresses
	$email_addresses = [];
	$raw_email_addresses = explode("\n", $vbulletin->GPC['to']);
	foreach ($raw_email_addresses AS $raw_email_address)
	{
		$raw_email_address = trim($raw_email_address);
		if (!empty($raw_email_address) AND vB_String::isValidEmail($raw_email_address))
		{
			$email_addresses[] = $raw_email_address;
		}
	}
	$email_addresses = array_unique($email_addresses);
	if (empty($email_addresses))
	{
		print_stop_message('please_complete_required_fields');
	}
	$email_address_count = count($email_addresses);

	// send emails
	vB_Utility_Functions::setPhpTimeout(0);
	echo '<p><b>' . construct_phrase($vbphrase['sending_invitation_email_to_x_addresses'], $email_address_count) . '</b></p>';
	foreach ($email_addresses AS $email_address)
	{
		echo $email_address . " ... \n";
		vbflush();

		// There rest of the {shortcode} substitutions are the standard phrase short codes
		// and are made as part of sending the email. This is the only one that's appropriate
		// to do here, since it's not available elsewhere.
		$sendmessage = str_replace('{email}', $email_address, $vbulletin->GPC['message']);

		echo $vbphrase['emailing'] . " \n";
		// Should this allow HTML or escape as to be WYSIWYG? For now, allowing HTML as this is an adminCP script.
		// TODO: This might need some kind of unsubscribe, but that would require that we keep a list of non-registered
		// email addresses that have opted out. Might be best to convert all the mailoption features to hinge on email
		// instead of userid (even if there's a chance of legacy forums with dupe emails), but not sure what the
		// registration-less emails in the DB would mean for GDPR
		$mail = vB_Mail::create2($email_address, $vbulletin->GPC['subject'], $sendmessage);
		if ($replyto)
		{
			$mail->addReplyTo($replyto);
		}
		$mail->send(true);

		vbflush();

		echo $vbphrase['okay'] . "<br />\n";
		vbflush();
	}
}

// *************************** Invite users form **********************
if ($_REQUEST['do'] == 'invite')
{
	$vboptions = vB::getDatastore()->getValue('options');

	print_form_header2('admincp/email', 'doinvite');
	print_table_start2();
	print_table_header($vbphrase['invite_members_to_forum']);

	print_input_row($vbphrase['replyto_gmessaging'], 'replyto', '');
	print_textarea_row2($vbphrase['to_invite_email'], 'to', '', ['rows' => 15, 'cols' => 80]);
	print_input_row($vbphrase['subject'], 'subject');
	$default_message = construct_phrase($vbphrase['default_invite_email_content_at_x_y'], $vboptions['bbtitle'], $vboptions['frontendurl']);
	print_textarea_row2($vbphrase['message_invite_email'], 'message', $default_message, ['rows' => 15, 'cols' => 80]);
	print_table_default_footer($vbphrase['send']);
}

// *************************** Main email form **********************
if ($_REQUEST['do'] == 'start' OR $_REQUEST['do'] == 'genlist')
{
	if ($_REQUEST['do'] == 'start')
	{
		$vbulletin->input->clean_array_gpc('r', [
			'type' => vB_Cleaner::TYPE_NOHTML,
		]);

		$type = $vbulletin->GPC['type'] ?: 'email';
		$emailclasses = ['js-dependant-child', 'js-dependant-child-email'];
		$pmclasses = ['js-dependant-child', 'js-dependant-child-pm'];

		if ($type == 'email')
		{
			$pmclasses[] = 'hide-row';
		}
		else
		{
			$emailclasses[] = 'hide-row';
		}

		print_form_header2('admincp/email', 'dosend');
		print_table_start2(['js-dependant-container'], []);
		print_column_style_code(['width:50%', 'width:50%']);
		print_table_header($vbphrase['send_email_to_users'], autostriping:true);

		print_radio_row($vbphrase['message_type'], 'type', ['email' => $vbphrase['email'], 'pm' => $vbphrase['private_message']], $type, 'js-dependant-trigger-group', false, true);

		print_yes_no_row($vbphrase['test_email_only'], 'test', 0);
		print_input_row($vbphrase['email_to_send_at_once_gcpuser'], 'perpage', 500);
		print_input_row($vbphrase['replyto_gmessaging'], 'replyto', '', inputclass: implode(' ', $emailclasses));
		print_input_row($vbphrase['subject'], 'subject');
		print_textarea_row2($vbphrase['message_email'], 'message', '', ['rows' => 15, 'cols' => 80], $emailclasses);
		print_textarea_row2($vbphrase['message_pm'], 'pmmessage', '', ['rows' => 15, 'cols' => 80], $pmclasses);
		$text = $vbphrase['send'];
	}
	else
	{
		print_form_header2('admincp/email', 'makelist');
		print_table_start2();
		print_table_header($vbphrase['generate_mailing_list']);
		print_textarea_row2($vbphrase['text_to_separate_addresses_by'], 'septext', ' ');
		$text = $vbphrase['submit'];
	}

	print_table_break();
	print_table_header($vbphrase['search_criteria']);
	print_user_search_rows(true, $text);
	print_table_footer();
}

print_cp_footer();

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 13:27, Sun Aug 30th 2026
|| # CVS: $RCSfile$ - $Revision: 122769 $
|| #######################################################################
\*=========================================================================*/
