<?php
/**
 *
 * This file is part of the phpBB Forum Software package.
 *
 * @copyright (c) phpBB Limited <https://www.phpbb.com>
 * @license GNU General Public License, version 2 (GPL-2.0)
 *
 * For full copyright and license information, please see
 * the docs/CREDITS.txt file.
 *
 */

namespace phpbb\messenger\method;

use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email as symfony_email;
use Symfony\Component\Mime\Header\Headers;

/**
 * Messenger class
 */
class email extends base
{
	/** @var array */
	private const PRIORITY_MAP = [
		symfony_email::PRIORITY_HIGHEST => 'Highest',
		symfony_email::PRIORITY_HIGH => 'High',
		symfony_email::PRIORITY_NORMAL => 'Normal',
		symfony_email::PRIORITY_LOW => 'Low',
		symfony_email::PRIORITY_LOWEST => 'Lowest',
	];

	/**
	 * @var string
	 *
	 * Symfony Mailer transport DSN
	 */
	protected string $dsn = '';

	/** @var symfony_email */
	protected symfony_email $email;

	/** @var Address From address */
	protected Address $from;

	/** @var Headers Email headers */
	protected Headers $headers;

	/**
	 * @var int
	 *
	 * Possible values are:
	 * symfony_email::PRIORITY_HIGHEST
	 * symfony_email::PRIORITY_HIGH
	 * symfony_email::PRIORITY_NORMAL
	 * symfony_email::PRIORITY_LOW
	 * symfony_email::PRIORITY_LOWEST
	 */
	protected int $mail_priority = symfony_email::PRIORITY_NORMAL;

	/** @var \phpbb\messenger\queue */
	protected $queue;

	/** @var Address */
	protected Address $reply_to;

	/** @var AbstractTransport */
	protected AbstractTransport $transport;

	/**
	 * {@inheritDoc}
	 */
	public function get_queue_object_name(): string
	{
		return 'email';
	}

	/**
	 * {@inheritDoc}
	 */
	public function is_enabled(): bool
	{
		return (bool) $this->config['email_enable'];
	}

	/**
	 * {@inheritDoc}
	 */
	public function init(): void
	{
		$this->email = new symfony_email();
		$this->headers = $this->email->getHeaders();
		$this->subject =  $this->msg = '';
		$this->mail_priority = symfony_email::PRIORITY_NORMAL;

		$this->additional_headers = [];
		$this->set_use_queue();
		unset($this->template, $this->reply_to, $this->from);
	}

	/**
	 * {@inheritdoc}
	 */
	public function set_use_queue(bool $use_queue = true): void
	{
		$this->use_queue = !$this->config['email_package_size'] ? false : $use_queue;
	}

	/**
	 * {@inheritDoc}
	 */
	public function set_addresses(array $user_row): void
	{
		if (!empty($user_row['user_email']))
		{
			$this->to($user_row['user_email'], $user_row['username'] ?? '');
		}
	}

	/**
	 * Sets email address to send to
	 *
	 * @param string	$address	Email "To" recipient address
	 * @param string	$realname	Email "To" recipient name
	 * @return void
	 */
	public function to(string $address, string $realname = ''): void
	{
		if (!$address = trim($address))
		{
			return;
		}

		$to = new Address($address, trim($realname));
		$this->email->getTo() ? $this->email->addTo($to) : $this->email->to($to);
	}

	/**
	 * Sets cc address to send to
	 *
	 * @param string	$address	Email carbon copy recipient address
	 * @param string	$realname	Email carbon copy recipient name
	 * @return void
	 */
	public function cc(string $address, string $realname = ''): void
	{
		if (!$address = trim($address))
		{
			return;
		}

		$cc = new Address($address, trim($realname));
		$this->email->getCc() ? $this->email->addCc($cc) : $this->email->cc($cc);
	}

	/**
	 * Sets bcc address to send to
	 *
	 * @param string	$address	Email black carbon copy recipient address
	 * @param string	$realname	Email black carbon copy recipient name
	 * @return void
	 */
	public function bcc(string $address, string $realname = ''): void
	{
		if (!$address = trim($address))
		{
			return;
		}

		$bcc = new Address($address, trim($realname));
		$this->email->getBcc() ? $this->email->addBcc($bcc) : $this->email->bcc($bcc);
	}

	/**
	 * Set the reply to address
	 *
	 * @param string	$address	Email "Reply to" address
	 * @param string	$realname	Email "Reply to" recipient name
	 * @return void
	 */
	public function reply_to(string $address, string $realname = ''): void
	{
		if (!$address = trim($address))
		{
			return;
		}

		$this->reply_to = new Address($address, trim($realname));
		$this->email->getReplyTo() ? $this->email->addReplyTo($this->reply_to) : $this->email->replyTo($this->reply_to);
	}

	/**
	 * Set the from address
	 *
	 * @param string	$address	Email "from" address
	 * @param string	$realname	Email "from" recipient name
	 * @return void
	 */
	public function from(string $address, string $realname = ''): void
	{
		if (!$address = trim($address))
		{
			return;
		}

		$this->from = new Address($address, trim($realname));
		$this->email->getFrom() ? $this->email->addFrom($this->from) : $this->email->from($this->from);
	}

	/**
	 * Set up subject for mail
	 *
	 * @param string	$subject	Email subject
	 * @return void
	 */
	public function subject(string $subject = ''): void
	{
		parent::subject(trim($subject));
		$this->email->subject($this->subject);
	}

	/**
	 * Adds X-AntiAbuse headers
	 *
	 * @param \phpbb\config\config	$config		Config object
	 * @param \phpbb\user			$user		User object
	 * @return void
	 */
	public function anti_abuse_headers(\phpbb\config\config $config, \phpbb\user $user): void
	{
		$this->header('X-AntiAbuse', 'Board servername - ' . $config['server_name']);
		$this->header('X-AntiAbuse', 'User_id - ' . $user->data['user_id']);
		$this->header('X-AntiAbuse', 'Username - ' . $user->data['username']);
		$this->header('X-AntiAbuse', 'User IP - ' . $user->ip);
	}

	/**
	 * Set the email priority
	 *
	 * Possible values are:
	 * symfony_email::PRIORITY_HIGHEST = 1
	 * symfony_email::PRIORITY_HIGH = 2
	 * symfony_email::PRIORITY_NORMAL = 3
	 * symfony_email::PRIORITY_LOW = 4
	 * symfony_email::PRIORITY_LOWEST = 5
	 *
	 * @param int	$priority	Email priority level
	 * @return void
	 */
	public function set_mail_priority(int $priority = symfony_email::PRIORITY_NORMAL): void
	{
		$this->email->priority($priority);
	}

	/**
	 * Set email headers
	 *
	 * @return void
	 */
	protected function build_headers(): void
	{
		$board_contact = trim($this->config['board_contact']);
		$contact_name = html_entity_decode($this->config['board_contact_name'], ENT_COMPAT);

		if (empty($this->email->getReplyTo()))
		{
			$this->reply_to($board_contact, $contact_name);
		}

		if (empty($this->email->getFrom()))
		{
			$this->from($board_contact, $contact_name);
		}

		$this->email->priority($this->mail_priority);

		$headers = [
			'Return-Path'		=> new Address($this->config['board_email']),
			'Sender'			=> new Address($this->config['board_email']),
			'X-MSMail-Priority'	=> self::PRIORITY_MAP[$this->mail_priority],
			'X-Mailer'			=> 'phpBB',
			'X-MimeOLE'			=> 'phpBB',
			'X-phpBB-Origin'	=> 'phpbb://' . str_replace(['http://', 'https://'], ['', ''], generate_board_url()),
		];

		// Add additional headers
		$headers = array_merge($headers, $this->additional_headers);

		/**
		 * Event to modify email header entries
		 *
		 * @event core.modify_email_headers
		 * @var	array	headers	Array containing email header entries
		 * @since 3.1.11-RC1
		 */
		$vars = ['headers'];
		extract($this->dispatcher->trigger_event('core.modify_email_headers', compact($vars)));

		foreach ($headers as $header => $value)
		{
			$this->header($header, $value);
		}
	}

	/**
	 * Generates valid DSN for Symfony Mailer transport
	 *
	 * @param string $dsn Symfony Mailer transport DSN
	 * @return void
	 */
	public function set_dsn(string $dsn = ''): void
	{
		if (!empty($dsn))
		{
			$this->dsn = $dsn;
		}
		else if ($this->config['smtp_delivery'])
		{
			if (empty($this->config['smtp_host']))
			{
				$this->dsn = 'null://null';
			}
			else
			{
				$user = urlencode($this->config['smtp_username']);
				$password = urlencode($this->config['smtp_password']);
				$smtp_host = urlencode($this->config['smtp_host']);
				$smtp_port = $this->config['smtp_port'];

				$this->dsn = "smtp://$user:$password@$smtp_host:$smtp_port";
			}
		}
		else
		{
			$this->dsn = 'sendmail://default';
		}
	}

	/**
	 * Get Symfony Mailer transport DSN
	 *
	 * @return string
	 */
	public function get_dsn(): string
	{
		return $this->dsn;
	}

	/**
	 * Generates a valid transport to send email
	 *
	 * @return void
	 */
	public function set_transport(): void
	{
		if (empty($this->dsn))
		{
			$this->set_dsn();
		}

		$this->transport = Transport::fromDsn($this->dsn);

		if ($this->config['smtp_delivery'] && method_exists($this->transport, 'getStream'))
		{
			// Set ssl context options, see http://php.net/manual/en/context.ssl.php
			$options['ssl'] = [
				'verify_peer' => (bool) $this->config['smtp_verify_peer'],
				'verify_peer_name' => (bool) $this->config['smtp_verify_peer_name'],
				'allow_self_signed' => (bool) $this->config['smtp_allow_self_signed'],
			];
			$this->transport->getStream()->setStreamOptions($options);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function process_queue(array &$queue_data): void
	{
		$queue_object_name = $this->get_queue_object_name();
		$messages_count = count($queue_data[$queue_object_name]['data']);

		if (!$this->is_enabled() || !$messages_count)
		{
			unset($queue_data[$queue_object_name]);
			return;
		}

		@set_time_limit(0);

		$package_size = $queue_data[$queue_object_name]['package_size'] ?? 0;
		$num_items = (!$package_size || $messages_count < $package_size) ? $messages_count : $package_size;
		$mailer = $this->get_mailer();

		for ($i = 0; $i < $num_items; $i++)
		{
			// Make variables available...
			extract(array_shift($queue_data[$queue_object_name]['data']));

			$break = false;
			/**
			 * Event to send message via external transport
			 *
			 * @event core.notification_message_process
			 * @var	string		break	Flag indicating if the function return after hook
			 * @var	string		email	The Symfony Email object
			 * @since 3.2.4-RC1
			 * @changed 4.0.0-a1 Added vars: email. Removed vars: addresses, subject, msg.
			 */
			$vars = [
				'break',
				'email',
			];
			extract($this->dispatcher->trigger_event('core.notification_message_process', compact($vars)));

			if (!$break)
			{
				try
				{
					$mailer->send($email);
				}
				catch (TransportExceptionInterface $e)
				{
					$this->error($e->getDebug());
					continue;
				}
			}
		}

		// No more data for this object? Unset it
		if (!count($queue_data[$queue_object_name]['data']))
		{
			unset($queue_data[$queue_object_name]);
		}
	}

	/**
	 * Get mailer object
	 *
	 * @return MailerInterface Symfony Mailer object
	 */
	protected function get_mailer(): MailerInterface
	{
		return new Mailer($this->transport);
	}

	/**
	 * Get mailer transport object
	 *
	 * @return TransportInterface Symfony Mailer transport object
	 */
	public function get_transport(): TransportInterface
	{
		return $this->transport;
	}

	/**
	 * {@inheritDoc}
	 */
	public function send(): bool
	{
		$this->prepare_message();

		$this->email->subject($this->subject);
		$this->email->text($this->msg);

		$break = false;
		$subject = $this->subject;
		$msg = $this->msg;
		$email = $this->email;
		/**
		 * Event to send message via external transport
		 *
		 * @event core.notification_message_email
		 * @var	bool	break	Flag indicating if the function return after hook
		 * @var	string	subject	The message subject
		 * @var	string	msg		The message text
		 * @var	string	email	The Symfony Email object
		 * @since 3.2.4-RC1
		 * @changed 4.0.0-a1 Added vars: email. Removed vars: addresses
		 */
		$vars = [
			'break',
			'subject',
			'msg',
			'email',
		];
		extract($this->dispatcher->trigger_event('core.notification_message_email', compact($vars)));
		$this->email = $email;

		$this->build_headers();

		if ($break)
		{
			return true;
		}

		// Send message ...
		if (!$this->use_queue)
		{
			$mailer = $this->get_mailer();

			$subject = $this->subject;
			$msg = $this->msg;
			$headers = $this->headers;
			$email = $this->email;
			/**
			 * Modify data before sending out emails with PHP's mail function
			 *
			 * @event core.phpbb_mail_before
			 * @var	string	email		The Symfony Email object
			 * @var	string	subject		The message subject
			 * @var	string	msg			The message text
			 * @var string	headers		The email headers
			 * @since 3.3.6-RC1
			 * @changed 4.0.0-a1 Added vars: email. Removed vars: to, eol, additional_parameters.
			 */
			$vars = [
				'email',
				'subject',
				'msg',
				'headers',
			];
			extract($this->dispatcher->trigger_event('core.phpbb_mail_before', compact($vars)));

			$this->subject = $subject;
			$this->msg = $msg;
			$this->headers = $headers;
			$this->email = $email;

			try
			{
				$mailer->send($this->email);
			}
			catch (TransportExceptionInterface $e)
			{
				$this->error($e->getDebug());
				return false;
			}

			/**
			 * Execute code after sending out emails with PHP's mail function
			 *
			 * @event core.phpbb_mail_after
			 * @var	string	email		The Symfony Email object
			 * @var	string	subject		The message subject
			 * @var	string	msg			The message text
			 * @var string	headers		The email headers
			 * @since 3.3.6-RC1
			 * @changed 4.0.0-a1 Added vars: email. Removed vars: to, eol, additional_parameters, $result.
			 */
			$vars = [
				'email',
				'subject',
				'msg',
				'headers',
			];
			extract($this->dispatcher->trigger_event('core.phpbb_mail_after', compact($vars)));
		}
		else
		{
			$this->queue->init('email', $this->config['email_package_size']);
			$this->queue->put('email', [
				'email'	=> $this->email,
			]);
		}

		// Reset the object
		$this->init();

		return true;
	}

	/**
	 * {@inheritdoc}
	 */
	public function header(string $header_name, mixed $header_value): void
	{
		$this->headers->addHeader($header_name, $header_value);
	}
}
