<?php
declare(strict_types = 1);

/**
 * Gestionnaire d'envoi de courriel.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Mail
{
	/**
	 * Longueur maximum des champs de formulaires.
	 *
	 * @var array
	 */
	CONST FIELDS_MAXLENGTH =
	[
		'name' => 64,
		'email' => 255,
		'subject' => 70,
		'message' => 5000
	];



	/**
	 * Paramètres (message et en-têtes) des courriels à envoyer.
	 *
	 * @var array
	 */
	public $messages = [];



	/**
	 * Notifications.
	 *
	 * @param string $n
	 *   Type de notification souhaitée.
	 * @param array $albums
	 *   Liste des albums concernés par la notification.
	 * @param int $user_exclude
	 *   Identifiant de l'utilisateur à exclure de la notification.
	 * @param array $infos
	 *   Informations complémentaires pour la notification.
	 * @param array $groups_exclude
	 *   Identifiants des groupes à exclure de la notification.
	 *
	 * @return mixed
	 *   Retourne un tableau des groupes qui seront notifiés,
	 *   ou FALSE en cas d'erreur.
	 */
	public function notify(string $notification, array $albums = [],
	int $user_exclude = 0, array $infos = [], array $groups_exclude = [])
	{
		// Récupération des informations utiles de tous les groupes.
		$sql = 'SELECT group_id, group_perms, group_admin FROM {groups} WHERE group_id != 2';
		if (!DB::execute($sql) || !$groups_all = DB::fetchAll())
		{
			return FALSE;
		}

		// Sélection des identifiants de groupes autorisés
		// à être notifiés en fonction de leurs permissions.
		$groups_notify = [];
		foreach ($groups_all as &$i)
		{
			// Groupes à exclure.
			if (in_array($i['group_id'], $groups_exclude))
			{
				continue;
			}

			// Il n'y a pas besoin de vérifier les droits pour le super-admin !
			if ($i['group_id'] == 1)
			{
				$groups_notify[] = 1;
				continue;
			}

			$group_perms = Utility::jsonDecode($i['group_perms']);

			// Notifications pour administrateurs.
			if ($i['group_admin'])
			{
				$groups_notify[] = $i['group_id'];
			}

			// Notifications pour membres.
			else if ($albums && in_array($notification, ['comment-follow', 'items', 'items-http']))
			{
				$sql = sprintf('SELECT 1 FROM {categories} AS cat WHERE %s AND %s LIMIT 1',
					SQL::catPerms((int) $i['group_id'],
						$group_perms['perm_list'], (int) $i['group_admin']),
					'cat_path IN (?' . str_repeat(', ?', count($albums) - 1) . ')'
				);
				if (DB::execute($sql, array_values($albums)) && DB::fetchVal())
				{
					$groups_notify[] = $i['group_id'];
				}
			}
		}

		// Type de notification.
		switch ($notification)
		{
			// Nouveau commentaire.
			case 'comment' :
				$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{ITEM_URL}', '{USER_NAME}'];
				$sql_user_alert = '_1____';
				break;

			// Suivi de commentaires.
			case 'comment-follow' :
				$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{ITEM_URL}', '{USER_NAME}'];
				$sql_user_alert = '_____1';
				break;

			// Nouveau commentaire en attente.
			case 'comment-pending' :
				$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{USER_NAME}'];
				$sql_user_alert = '__1___';
				break;

			// Nouveaux fichiers (FTP).
			case 'items' :
				$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{USER_NAME}', '{USER_URL}'];
				$sql_user_alert = '___1__';
				break;

			// Nouveaux fichiers (HTTP).
			case 'items-http' :
				$vars = ['{ALBUM_TITLE}', '{ALBUM_URL}', '{GALLERY_TITLE}', '{GALLERY_URL}',
					'{USER_NAME}', '{USER_URL}'];
				$sql_user_alert = '___1__';
				break;

			// Nouveaux fichiers en attente.
			case 'items-pending' :
				$vars = ['{ALBUM_TITLE}', '{ALBUM_URL}', '{GALLERY_TITLE}', '{GALLERY_URL}',
					'{USER_NAME}', '{USER_URL}'];
				$sql_user_alert = '____1_';
				break;

			// Inscription.
			case 'registration' :
				if (Config::$params['users_registration_valid_admin'])
				{
					$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{USER_NAME}'];
					$notification .= '_pending';
				}
				else
				{
					$vars = ['{GALLERY_TITLE}', '{GALLERY_URL}', '{USER_NAME}', '{USER_URL}'];
				}

				$sql_user_alert = '1_____';
				break;
		}

		$msg_subj = self::_notifyMessageSubject($notification, $vars, $infos);
		$sql_where = '1=1';

		// Suivi de commentaires : récupération de l'identifiant
		// des utilisateurs ayant commenté le fichier.
		if ($notification == 'comment-follow' && Config::$params['users'])
		{
			$sql = 'SELECT DISTINCT user_id
					  FROM {comments}
					 WHERE user_id NOT IN (2, ?)
					   AND item_id = ?';
			if (!DB::execute($sql, [$user_exclude, (int) $infos['item_id']])
			|| !$users_id = DB::fetchCol('user_id'))
			{
				return FALSE;
			}
			$sql_where = 'user_id IN (' . DB::inInt($users_id) . ')';
		}

		// On récupère l'adresse de courriel de tous
		// les utilisateurs autorisés à être notifié.
		$sql = "SELECT user_email
				  FROM {users}
				 WHERE $sql_where
				   AND user_id NOT IN (2, ?)
				   AND user_alert LIKE '$sql_user_alert'
				   AND user_email LIKE '%@%'
				   AND user_status = '1'
				   AND group_id IN (" . DB::inInt($groups_notify) . ")";
		if (!DB::execute($sql, $user_exclude)
		|| !$emails = DB::fetchCol('user_email'))
		{
			return FALSE;
		}
		sort($emails);

		$message = $msg_subj['message'];

		// Signature.
		if (Config::$params['mail_auto_signature'])
		{
			$message .= "\n\n-- \n";
			$message .= Config::$params['mail_auto_signature_text'];
		}

		$this->messages[] =
		[
			'to' => trim(Config::$params['mail_auto_primary_recipient_address']),
			'name' => trim(Config::$params['mail_auto_sender_name']),
			'from' => '',
			'subject' => $msg_subj['subject'],
			'message' => $message,
			'bcc' => implode(', ', $emails)
		];

		return $groups_notify;
	}

	/**
	 * Envoi un ou plusieurs courriel.
	 *
	 * @return bool
	 */
	public function send(): bool
	{
		if (!$this->messages)
		{
			return TRUE;
		}

		if (CONF_SMTP_MAIL)
		{
			return $this->_smtpMail();
		}

		$ok = TRUE;
		foreach ($this->messages as &$i)
		{
			if (!is_array($i))
			{
				continue;
			}
			if (!$this->_phpMail($i))
			{
				$ok = FALSE;
				break;
			}
		}

		return $ok;
	}



	/**
	 * Champ "from".
	 *
	 * @param string $email
	 *
	 * @return string
	 */
	private function _from(string $email): string
	{
		if (Utility::isEmpty($email) || strlen($email) > 250)
		{
			$email = Config::$params['mail_auto_sender_address'];
		}

		$this->_sanitize($email);

		return $email;
	}

	/**
	 * En-têtes du courriel.
	 *
	 * @param array $headers
	 * @param string $bcc
	 * @param string $to
	 *
	 * @return string
	 */
	private function _headers(array $headers, string $bcc = '', string $to = ''): string
	{
		if ($bcc)
		{
			$this->_sanitize($bcc);
			$headers[] = 'Bcc: ' . $bcc;
		}

		if ($to)
		{
			$this->_sanitize($to);
			$headers[] = 'To: ' . $to;
		}

		$headers[] = 'Date: ' . date('r');
		$headers[] = 'MIME-Version: 1.0';
		$headers[] = 'Content-Transfer-Encoding: 8bit';
		$headers[] = 'Content-Type: text/plain; charset=' . CONF_CHARSET;
		$headers[] = 'X-Mailer: ' . System::APP_NAME . ' - ' . $_SERVER['HTTP_HOST'];

		return implode(PHP_EOL, $headers);
	}

	/**
	 * Prépare le message du courriel.
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	private function _message(string $str): string
	{
		$str = str_replace(["\r\n", "\r", "\n"], ["\n", "\n", PHP_EOL], $str);
		$str = strip_tags($str);
		$str = wordwrap($str, 70);

		return $str;
	}

	/**
	 * Retourne le message et le sujet d'une notification
	 * avec remplacement des variables correspondantes.
	 *
	 * @param string $notification
	 *   Type de notification.
	 * @param array $vars
	 *   Variables à remplacer.
	 * @param array $i
	 *   Informations complémentaires pour la notification.
	 *
	 * @return array
	 */
	private function _notifyMessageSubject(string $notification, array &$vars, array &$i): array
	{
		$replace = [];
		foreach ($vars as &$var)
		{
			switch ($var)
			{
				// Album.
				case '{ALBUM_TITLE}' :
					$replace[] = array_key_exists('cat_name', $i)
						? htmlspecialchars($i['cat_name'])
						: $var;
					break;

				case '{ALBUM_URL}' :
					$replace[] = array_key_exists('cat_id', $i)
						? GALLERY_HOST . App::getURLGallery('album/' . (int) $i['cat_id'])
						: $var;
					break;

				// Galerie.
				case '{GALLERY_TITLE}' :
					$replace[] = htmlspecialchars(Config::$params['gallery_title']);
					break;

				case '{GALLERY_URL}' :
					$replace[] = GALLERY_HOST . App::getURLGallery();
					break;

				// Fichier.
				case '{ITEM_TITLE}' :
					$replace[] = array_key_exists('item_name', $i)
						? htmlspecialchars($i['item_name'])
						: $var;
					break;

				case '{ITEM_URL}' :
					$replace[] = array_key_exists('item_id', $i)
						? GALLERY_HOST . App::getURLGallery('item/' . (int) $i['item_id'])
						: $var;
					break;

				// Utilisateur.
				case '{USER_NAME}' :
					$replace[] = array_key_exists('user_name', $i)
						? htmlspecialchars($i['user_name'])
						: $var;
					break;

				case '{USER_URL}' :
					$replace[] = array_key_exists('user_id', $i)
						? GALLERY_HOST . App::getURLGallery('user/' . (int) $i['user_id'])
						: $var;
					break;
			}
		}

		$params = ['message' => '', 'subject' => ''];
		foreach ($params as $k => &$v)
		{
			$v = str_replace(
				$vars,
				$replace,
				Config::$params['mail_notify_'
					. str_replace('-', '_', $notification) . '_' . $k]
			);
		}

		return $params;
	}

	/**
	 * Envoi un courriel par la fonction mail() de PHP.
	 *
	 * @param array $i
	 *   Paramètres du mail.
	 *
	 * @return bool
	 */
	private function _phpMail(array &$i): bool
	{
		if (!function_exists('mail'))
		{
			trigger_error('Function mail() is not available.', E_USER_NOTICE);
			return FALSE;
		}

		$to = $i['to'] ?? '';
		$from = $i['from'] ?? '';
		$name = $i['name'] ?? '';
		$bcc = $i['bcc'] ?? '';

		// Doit-on ne pas utiliser le champs "Bcc" ?
		if (Config::$params['mail_auto_bcc'] != '1')
		{
			$to = $bcc ? $bcc : $to;
			$bcc = '';
		}

		// Destinataire(s).
		$to = $this->_to($to);

		// Sujet.
		$subject = $this->_subject($i['subject']);

		// Message.
		$message = $this->_message($i['message']);

		// En-têtes.
		$this->_sanitize($name);
		$name = Utility::isEmpty($name)
			? trim(Config::$params['mail_auto_sender_name'])
			: $name . ' ';
		$from = $name . '<' . $this->_from($from) . '>';
		$headers = $this->_headers(['From: ' . $from], $bcc);

		// Envoi.
		$send = mail($to, $subject, $message, $headers);

		return $send;
	}

	/**
	 * Nettoie une chaîne.
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	private function _sanitize(string &$str): string
	{
		return Utility::trimAll(
			preg_replace('`(?:to:|b?cc:|from:|content-type:|[\t\r\n\x5C]+)`i', '', $str)
		);
	}

	/**
	 * Envoi des courriels par un serveur SMTP.
	 *
	 * @return bool
	 */
	private function _smtpMail(): bool
	{
		if (!function_exists('fsockopen'))
		{
			trigger_error('Function fsockopen() is not available.', E_USER_NOTICE);
			return FALSE;
		}

		$data = '';

		// Connexion au serveur SMTP.
		$fp = NULL;

		// Effectue une commande SMTP.
		$cmd = function (string $str) use (&$data, &$fp): bool
		{
			fputs($fp, $str . PHP_EOL);
			if (($data = fgets($fp, 1024)) === FALSE)
			{
				return FALSE;
			}
			return substr($data, 0, 1) != 4 && substr($data, 0, 1) != 5;
		};

		// Ferme la connexion au serveur SMTP.
		$quit = function() use (&$fp, &$cmd): void
		{
			if (is_resource($fp))
			{
				$cmd('QUIT');
				fclose($fp);
				$fp = NULL;
			}
		};

		// Génère une erreur.
		$error = function(string $message = '') use (&$data, &$quit): bool
		{
			$quit();
			trigger_error($message ? $message : 'Unable to send email. '
				. 'Error message reported by the SMTP server: ' . $data,
				E_USER_WARNING);
			return FALSE;
		};

		// Ouverture de la connexion au serveur SMTP.
		$fp = fsockopen(CONF_SMTP_HOST, CONF_SMTP_PORT, $errno, $errstr, CONF_SMTP_TIME);
		if (!is_resource($fp))
		{
			return $error('Could not connect to SMTP host "'
				. CONF_SMTP_HOST . '" (' . $errno . ': ' . $errstr . ')');
		}
		stream_set_timeout($fp, CONF_SMTP_TIME);

		// Authentification.
		if ((CONF_SMTP_AUTH && (!$cmd('EHLO client') || !$cmd('AUTH LOGIN')
		|| !$cmd(base64_encode(CONF_SMTP_USER))
		|| !$cmd(base64_encode(CONF_SMTP_PASS))))
		|| (!CONF_SMTP_AUTH && !$cmd('HELO client')))
		{
			return $error();
		}

		// Envoi des courriels.
		foreach ($this->messages as &$i)
		{
			if (!is_array($i))
			{
				continue;
			}

			$mail = [];

			// Expéditeur.
			$from = $this->_from(isset($i['from']) ? $i['from'] : '');
			$mail[] = 'MAIL FROM: <' . $from . '>';

			// Ne pas utiliser le champs "Bcc" ?
			if (!Config::$params['mail_auto_bcc'])
			{
				$i['to'] = $i['bcc'];
				$i['bcc'] = '';
			}

			// Destinataire(s).
			if (!empty($i['bcc']))
			{
				$mail[] = 'RCPT TO: <'
					. str_replace(', ', '>' . PHP_EOL . 'RCPT TO: <', $i['bcc'])
					. '>';
			}
			else if (!empty($i['to']))
			{
				$mail[] = 'RCPT TO: <' . $i['to'] . '>';
			}

			// En-têtes et message.
			$mail[] = 'DATA';
			$name = isset($i['name']) ? $this->_sanitize($i['name']) . ' ' : '';
			$headers =
			[
				'From: ' . $name . '<' . $from . '>',
				'Subject: ' . $this->_subject($i['subject'])
			];
			$mail[] = $this->_headers($headers, '', $i['to']) . PHP_EOL
					. $this->_message($i['message']) . PHP_EOL . '.';

			// Envoi du courriel.
			foreach ($mail as &$c)
			{
				if (!$cmd($c))
				{
					return $error();
				}
			}
		}

		$quit();
		return TRUE;
	}

	/**
	 * Prépare le sujet du courriel.
	 *
	 * @param string $str
	 *
	 * @return string
	 */
	private function _subject(string $str): string
	{
		$this->_sanitize($str);

		if (preg_match('`[^\x00-\x3C\x3E-\x7E]`', $str))
		{
			$str = '=?' . CONF_CHARSET . '?B?' . base64_encode($str) . '?=';
		}

		return $str;
	}

	/**
	 * Champ "to".
	 *
	 * @param string $to
	 *
	 * @return string
	 */
	private function _to(string $str): string
	{
		if (Utility::isEmpty($str) || strlen($str) > 250)
		{
			$str = Config::$params['mail_auto_primary_recipient_address'];
		}

		$this->_sanitize($str);

		return $str;
	}
}
?>