PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/main/lib/mail/smtp/mailer.php

https://gitlab.com/alexprowars/bitrix
PHP | 374 lines | 265 code | 57 blank | 52 comment | 31 complexity | 432bbc43f552922050e0dd16fa825d12 MD5 | raw file
  1. <?php
  2. /**
  3. * Bitrix Framework
  4. * @package bitrix
  5. * @subpackage main
  6. * @copyright 2001-2021 Bitrix
  7. */
  8. namespace Bitrix\Main\Mail\Smtp;
  9. use Bitrix\Main\Error;
  10. use Bitrix\Main\HttpApplication;
  11. use Bitrix\Main\Mail\Context;
  12. use Bitrix\Main\Localization\Loc;
  13. use Bitrix\Main\Mail\Sender;
  14. use Bitrix\Main\Mail\SenderSendCounter;
  15. use PHPMailer\PHPMailer\PHPMailer;
  16. use Bitrix\Main\Diag\FileLogger;
  17. class Mailer extends PHPMailer
  18. {
  19. /**
  20. * @var Mailer[] $instances
  21. */
  22. private static $instances = [];
  23. public const KEEP_ALIVE_ALWAYS = 'keep_alive_always';
  24. public const KEEP_ALIVE_NONE = 'keep_alive_none';
  25. public const KEEP_ALIVE_OPTIONAL = 'keep_alive_optional';
  26. private const HEADER_FROM_REGEX = '/(?<=From:).*?(?=\\n)/';
  27. private const HEADER_CC_REGEX = '/(?<=Cc:).*?(?=\\n)/';
  28. private const HEADER_BCC_REGEX = '/(?<=Bcc:).*?(?=\\n)/';
  29. private $configuration;
  30. protected function getActualConfiguration(Context $context): array
  31. {
  32. $configuration = \Bitrix\Main\Config\Configuration::getValue('smtp');
  33. if ($context->getSmtp())
  34. {
  35. return [
  36. 'host' => $context->getSmtp()->getHost(),
  37. 'port' => $context->getSmtp()->getPort(),
  38. 'encryption_type' => $context->getSmtp()->getProtocol() ?? 'smtp',
  39. 'login' => $context->getSmtp()->getLogin(),
  40. 'password' => $context->getSmtp()->getPassword(),
  41. 'from' => $context->getSmtp()->getFrom(),
  42. 'debug' => $configuration['debug'] ?? false,
  43. 'force_from' => $configuration['force_from'] ?? false,
  44. 'logFile' => $configuration['log_file'] ?? null,
  45. ];
  46. }
  47. return $configuration ?? [];
  48. }
  49. /**
  50. * Prepare PHPMailer configuration from bitrix/.settings.php
  51. * @param array|null $configuration
  52. * @return bool
  53. */
  54. public function prepareConfiguration(Context $context): bool
  55. {
  56. $this->configuration = $this->getActualConfiguration($context);
  57. $this->SMTPDebug = $this->configuration['debug'] ?? false;
  58. if ($this->SMTPDebug)
  59. {
  60. $configuration = $this->configuration;
  61. $this->Debugoutput = function ($logMessage) use ($configuration) {
  62. $logger = new FileLogger(
  63. $configuration['logFile'] ?? (($_SERVER['DOCUMENT_ROOT'] ?? __DIR__) . '/mailer.log')
  64. );
  65. $logger->info($logMessage);
  66. };
  67. }
  68. $this->isSMTP();
  69. $this->SMTPAuth = (bool)$this->configuration['password'];
  70. if (
  71. !$this->configuration['host']
  72. || !$this->configuration['login']
  73. )
  74. {
  75. return false;
  76. }
  77. $this->From = $this->configuration['from'];
  78. $this->Host = $this->configuration['host'];
  79. $this->Username = $this->configuration['login'];
  80. $this->Password = $this->configuration['password'] ?? '';
  81. $this->Port = $this->configuration['port'] ?? 465;
  82. if (
  83. 'smtps' === $this->configuration['encryption_type']
  84. || ('smtp' !== $this->configuration['encryption_type'] && 465 === $this->port))
  85. {
  86. $this->SMTPSecure = $this->Port == 465 ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
  87. }
  88. $this->Timeout = $this->configuration['connection_timeout'] ?? 30;
  89. return true;
  90. }
  91. /**
  92. * Set prepared MIME body data
  93. * @param $body
  94. */
  95. public function setMIMEBody($body)
  96. {
  97. $this->MIMEBody = $body;
  98. }
  99. /**
  100. * Set prepared MIME header
  101. * @param $headers
  102. */
  103. public function setMIMEHeader($headers)
  104. {
  105. $this->MIMEHeader = $headers;
  106. }
  107. /**
  108. * Send mail via smtp connection
  109. * @param string $to
  110. * @param string $subject
  111. * @param string $message
  112. * @param string $additional_headers
  113. * @param string $additional_parameters
  114. * @return bool
  115. * @throws \PHPMailer\PHPMailer\Exception
  116. */
  117. public function sendMailBySmtp(
  118. string $sourceTo,
  119. string $subject,
  120. string $message,
  121. string $additional_headers,
  122. string $additional_parameters
  123. ): bool
  124. {
  125. $addresses = Mailer::parseAddresses($sourceTo)??[];
  126. $eol = \Bitrix\Main\Mail\Mail::getMailEol();
  127. if ($subject && $additional_headers)
  128. {
  129. $this->Subject = $subject;
  130. $additional_headers .= $eol . 'Subject: ' . $subject;
  131. }
  132. preg_match(self::HEADER_FROM_REGEX, $additional_headers, $headerFrom);;
  133. if ($this->configuration['force_from'] && $headerFrom)
  134. {
  135. $additional_headers = preg_replace(
  136. self::HEADER_FROM_REGEX,
  137. $this->configuration['from'],
  138. $additional_headers
  139. );
  140. }
  141. if (!$headerFrom)
  142. {
  143. $additional_headers .= $eol . 'From: ' . $this->configuration['from'];
  144. }
  145. $this->clearAllRecipients();
  146. foreach ($addresses as $to)
  147. {
  148. if (!$to['address'])
  149. {
  150. continue;
  151. }
  152. $this->addAddress($to['address'], $to['name']);
  153. }
  154. $additional_headers .= $eol . 'To: ' . $sourceTo;
  155. $this->prepareBCCRecipients($additional_headers);
  156. $this->prepareCCRecipients($additional_headers);
  157. $this->setMIMEBody($message);
  158. $this->setMIMEHeader($additional_headers);
  159. $canSend = $this->checkLimit();
  160. if (!$canSend)
  161. {
  162. return false;
  163. }
  164. $sendResult = $this->postSend();
  165. if ($sendResult)
  166. {
  167. $this->increaseLimit();
  168. }
  169. return $sendResult;
  170. }
  171. private function prepareCCRecipients($additional_headers)
  172. {
  173. preg_match(self::HEADER_CC_REGEX, $additional_headers, $matches);
  174. if ($matches)
  175. {
  176. $recipients = explode(',', $matches[0]);
  177. foreach ($recipients as $to)
  178. {
  179. $to = self::parseAddresses($to) ?? [];
  180. if (!$to)
  181. {
  182. continue;
  183. }
  184. $this->addCC($to[0]['address'], $to[0]['name']);
  185. }
  186. }
  187. }
  188. private function prepareBCCRecipients($additional_headers)
  189. {
  190. preg_match(self::HEADER_BCC_REGEX, $additional_headers, $matches);
  191. if ($matches)
  192. {
  193. $recipients = explode(',', $matches[0]);
  194. foreach ($recipients as $to)
  195. {
  196. $to = self::parseAddresses($to) ?? [];
  197. if (!$to)
  198. {
  199. continue;
  200. }
  201. $this->addBCC($to[0]['address'], $to[0]['name']);
  202. }
  203. }
  204. }
  205. /**
  206. * Returns instance of current class
  207. * @param Context $context
  208. * @return Mailer|null
  209. * @throws \Bitrix\Main\ArgumentException
  210. * @throws \PHPMailer\PHPMailer\Exception
  211. */
  212. public static function getInstance(Context $context): ?Mailer
  213. {
  214. $key = hash('sha256', serialize($context));
  215. if (!static::$instances[$key])
  216. {
  217. $mail = new Mailer();
  218. if (!$mail->prepareConfiguration($context))
  219. {
  220. return null;
  221. }
  222. if ($context->getSmtp())
  223. {
  224. $mail->setFrom($context->getSmtp()->getFrom());
  225. }
  226. switch ($context->getKeepAlive())
  227. {
  228. default:
  229. case self::KEEP_ALIVE_NONE:
  230. $mail->SMTPKeepAlive = false;
  231. break;
  232. case self::KEEP_ALIVE_ALWAYS:
  233. $mail->SMTPKeepAlive = true;
  234. HttpApplication::getInstance()->addBackgroundJob(
  235. function () use ($mail)
  236. {
  237. $mail->smtpClose();
  238. });
  239. break;
  240. case self::KEEP_ALIVE_OPTIONAL:
  241. $mail->SMTPKeepAlive = true;
  242. break;
  243. }
  244. static::$instances[$key] = $mail;
  245. }
  246. return static::$instances[$key];
  247. }
  248. /**
  249. * Returns true if Login, Password and Server parameters is right
  250. * Returns false if any errors in connection was detected
  251. * @param Context $context
  252. * @param \Bitrix\Main\ErrorCollection $errors
  253. * @return bool
  254. * @throws \PHPMailer\PHPMailer\Exception
  255. */
  256. public static function checkConnect(Context $context, \Bitrix\Main\ErrorCollection $errors): bool
  257. {
  258. $mail = new Mailer();
  259. if (\Bitrix\Main\ModuleManager::isModuleInstalled('bitrix24'))
  260. {
  261. // Private addresses can't be used in the cloud
  262. $ip = \Bitrix\Main\Web\IpAddress::createByName($context->getSmtp()->getHost());
  263. if ($ip->isPrivate())
  264. {
  265. $errors->setError(new Error('SMTP server address is invalid'));
  266. return false;
  267. }
  268. }
  269. if (!$mail->prepareConfiguration($context))
  270. {
  271. return false;
  272. }
  273. if ($mail->smtpConnect())
  274. {
  275. $mail->smtpClose();
  276. return true;
  277. }
  278. $errors->setError(new Error(Loc::getMessage('main_mail_smtp_connection_failed')));
  279. return false;
  280. }
  281. private function checkLimit(): bool
  282. {
  283. $from = self::parseAddresses($this->From)[0]['address'];
  284. $count = count($this->getAllRecipientAddresses());
  285. $emailCounter = new SenderSendCounter();
  286. $emailDailyLimit = Sender::getEmailLimit($from);
  287. if($emailDailyLimit
  288. && ($emailCounter->get($from) + $count) > $emailDailyLimit)
  289. {
  290. //daily limit exceeded
  291. return false;
  292. }
  293. return true;
  294. }
  295. private function increaseLimit()
  296. {
  297. $from = self::parseAddresses($this->From)[0]['address'];
  298. $emailDailyLimit = Sender::getEmailLimit($from);
  299. if (!$emailDailyLimit)
  300. {
  301. return;
  302. }
  303. $emailCounter = new SenderSendCounter();
  304. $count = count($this->getAllRecipientAddresses());
  305. $emailCounter->increment($from, $count);
  306. }
  307. /**
  308. * Closes all instances connections
  309. */
  310. public static function closeConnections(): void
  311. {
  312. foreach (static::$instances as $instance)
  313. {
  314. $instance->smtpClose();
  315. }
  316. }
  317. }