From df5ab5b5d842659ad681e86fd519dcc74b352d0f Mon Sep 17 00:00:00 2001 From: rxu Date: Fri, 5 May 2023 19:59:45 +0700 Subject: [PATCH] [ticket/17135] Introduce Symfony Mailer for emails backend PHPBB3-17135 --- phpBB/includes/functions_messenger.php | 1660 ++++++------------------ phpBB/phpbb/message/message.php | 8 +- tests/email/email_parsing_test.php | 13 +- tests/email/headers_encoding_test.php | 48 - 4 files changed, 432 insertions(+), 1297 deletions(-) delete mode 100644 tests/email/headers_encoding_test.php diff --git a/phpBB/includes/functions_messenger.php b/phpBB/includes/functions_messenger.php index c94a2a5c42..390463fe3a 100644 --- a/phpBB/includes/functions_messenger.php +++ b/phpBB/includes/functions_messenger.php @@ -11,6 +11,12 @@ * */ +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\Headers; + /** * @ignore */ @@ -24,85 +30,115 @@ if (!defined('IN_PHPBB')) */ class messenger { - var $msg, $replyto, $from, $subject; - var $addresses = array(); - var $extra_headers = array(); + private const PRIORITY_MAP = [ + Email::PRIORITY_HIGHEST => 'Highest', + Email::PRIORITY_HIGH => 'High', + Email::PRIORITY_NORMAL => 'Normal', + Email::PRIORITY_LOW => 'Low', + Email::PRIORITY_LOWEST => 'Lowest', + ]; + private const HEADER_CLASS_MAP = [ + 'date' => DateHeader::class, + 'from' => MailboxListHeader::class, + 'sender' => MailboxHeader::class, + 'reply-to' => MailboxListHeader::class, + 'to' => MailboxListHeader::class, + 'cc' => MailboxListHeader::class, + 'bcc' => MailboxListHeader::class, + 'message-id' => IdentificationHeader::class, + 'in-reply-to' => UnstructuredHeader::class, + 'references' => UnstructuredHeader::class, + 'return-path' => PathHeader::class, + ]; - var $mail_priority = MAIL_NORMAL_PRIORITY; + var $msg, $replyto, $from, $subject; + var $addresses = []; + var $extra_headers = []; + + /** + * Possible values are: + * Email::PRIORITY_HIGHEST + * Email::PRIORITY_HIGH + * Email::PRIORITY_NORMAL + * Email::PRIORITY_LOW + * Email::PRIORITY_LOWEST + */ + var $mail_priority = Email::PRIORITY_NORMAL; var $use_queue = true; /** @var \phpbb\template\template */ protected $template; /** - * Constructor - */ + * Constructor + */ function __construct($use_queue = true) { - global $config; + global $phpbb_container, $phpbb_root_path, $phpEx; - $this->use_queue = (!$config['email_package_size']) ? false : $use_queue; + $this->phpbb_container = $phpbb_container; + $this->config = $this->phpbb_container->get('config'); + $this->dispatcher = $this->phpbb_container->get('dispatcher'); + $this->language = $this->phpbb_container->get('language'); + $this->log = $this->phpbb_container->get('log'); + $this->request = $this->phpbb_container->get('request'); + $this->user = $this->phpbb_container->get('user'); + $this->email = new Email(); + $this->headers = $this->email->getHeaders(); + $this->use_queue = (!$this->config['email_package_size']) ? false : $use_queue; $this->subject = ''; + $this->root_path = $phpbb_root_path; + $this->php_ext = $phpEx; } /** - * Resets all the data (address, template file, etc etc) to default - */ + * Resets all the data (address, template file, etc etc) to default + */ function reset() { - $this->addresses = $this->extra_headers = array(); + $this->addresses = $this->extra_headers = []; $this->msg = $this->replyto = $this->from = ''; - $this->mail_priority = MAIL_NORMAL_PRIORITY; + $this->mail_priority = Email::PRIORITY_NORMAL; } /** - * Set addresses for to/im as available - * - * @param array $user User row - */ + * Set addresses for to/im as available + * + * @param array $user User row + */ function set_addresses($user) { if (isset($user['user_email']) && $user['user_email']) { - $this->to($user['user_email'], (isset($user['username']) ? $user['username'] : '')); + $this->email->to(new Address($user['user_email'], $user['username'] ?: '')); } if (isset($user['user_jabber']) && $user['user_jabber']) { - $this->im($user['user_jabber'], (isset($user['username']) ? $user['username'] : '')); + $this->im($user['user_jabber'], $user['username'] ?: ''); } } /** - * Sets an email address to send to - */ + * Sets an email address to send to + */ function to($address, $realname = '') { - global $config; - if (!trim($address)) { return; } - $pos = isset($this->addresses['to']) ? count($this->addresses['to']) : 0; - - $this->addresses['to'][$pos]['email'] = trim($address); - // If empty sendmail_path on windows, PHP changes the to line - if (!$config['smtp_delivery'] && DIRECTORY_SEPARATOR == '\\') - { - $this->addresses['to'][$pos]['name'] = ''; - } - else - { - $this->addresses['to'][$pos]['name'] = trim($realname); - } + $windows_empty_sendmail_path = !$this->config['smtp_delivery'] && DIRECTORY_SEPARATOR == '\\'; + + $to = new Address(trim($address), $windows_empty_sendmail_path ? '' : trim($realname)); + $this->email->getTo() ? $this->email->addTo($to) : $this->email->to($to); } /** - * Sets an cc address to send to - */ + * Sets an cc address to send to + */ function cc($address, $realname = '') { if (!trim($address)) @@ -110,14 +146,13 @@ class messenger return; } - $pos = isset($this->addresses['cc']) ? count($this->addresses['cc']) : 0; - $this->addresses['cc'][$pos]['email'] = trim($address); - $this->addresses['cc'][$pos]['name'] = trim($realname); + $cc = new Address(trim($address), trim($realname)); + $this->email->getCc() ? $this->email->addCc($to) : $this->email->cc($to); } /** - * Sets an bcc address to send to - */ + * Sets an bcc address to send to + */ function bcc($address, $realname = '') { if (!trim($address)) @@ -125,14 +160,13 @@ class messenger return; } - $pos = isset($this->addresses['bcc']) ? count($this->addresses['bcc']) : 0; - $this->addresses['bcc'][$pos]['email'] = trim($address); - $this->addresses['bcc'][$pos]['name'] = trim($realname); + $bcc = new Address(trim($address), trim($realname)); + $this->email->getBcc() ? $this->email->addBcc($to) : $this->email->bcc($to); } /** - * Sets a im contact to send to - */ + * Sets a im contact to send to + */ function im($address, $realname = '') { // IM-Addresses could be empty @@ -147,67 +181,73 @@ class messenger } /** - * Set the reply to address - */ + * Set the reply to address + */ function replyto($address) { - $this->replyto = trim($address); + $this->replyto = new Address(trim($address)); + $this->email->getReplyTo() ? $this->email->addReplyTo($this->replyto) : $this->email->replyTo($this->replyto); } /** - * Set the from address - */ + * Set the from address + */ function from($address) { - $this->from = trim($address); + $this->from = new Address(trim($address)); + $this->email->getFrom() ? $this->email->addFrom($this->from) : $this->email->from($this->from); } /** - * set up subject for mail - */ + * set up subject for mail + */ function subject($subject = '') { - $this->subject = trim($subject); + $this->email->subject(trim($subject)); } /** - * set up extra mail headers - */ - function headers($headers) + * set up extra mail headers + */ + function headers($header_name, $header_value) { - $this->extra_headers[] = trim($headers); + $this->headers->addTextHeader(trim($header_name), trim($header_value)); } /** - * Adds X-AntiAbuse headers - * - * @param \phpbb\config\config $config Config object - * @param \phpbb\user $user User object - * @return void - */ + * Adds X-AntiAbuse headers + * + * @param \phpbb\config\config $config Config object + * @param \phpbb\user $user User object + * @return void + */ function anti_abuse_headers($config, $user) { - $this->headers('X-AntiAbuse: Board servername - ' . mail_encode($config['server_name'])); - $this->headers('X-AntiAbuse: User_id - ' . $user->data['user_id']); - $this->headers('X-AntiAbuse: Username - ' . mail_encode($user->data['username'])); - $this->headers('X-AntiAbuse: User IP - ' . $user->ip); + $this->headers->addTextHeader('X-AntiAbuse', 'Board servername - ' . $config['server_name']); + $this->headers->addTextHeader('X-AntiAbuse', 'User_id - ' . $user->data['user_id']); + $this->headers->addTextHeader('X-AntiAbuse', 'Username - ' . $user->data['username']); + $this->headers->addTextHeader('X-AntiAbuse', 'User IP - ' . $user->ip); } /** - * Set the email priority - */ - function set_mail_priority($priority = MAIL_NORMAL_PRIORITY) + * Set the email priority + * Possible values are: + * Email::PRIORITY_HIGHEST + * Email::PRIORITY_HIGH + * Email::PRIORITY_NORMAL + * Email::PRIORITY_LOW + * Email::PRIORITY_LOWEST + */ + function set_mail_priority($priority = Email::PRIORITY_NORMAL) { - $this->mail_priority = $priority; + $this->email->priority($priority); } /** - * Set email template to use - */ + * Set email template to use + */ function template($template_file, $template_lang = '', $template_path = '', $template_dir_prefix = '') { - global $config, $phpbb_root_path, $user; - $template_dir_prefix = (!$template_dir_prefix || $template_dir_prefix[0] === '/') ? $template_dir_prefix : '/' . $template_dir_prefix; $this->setup_template(); @@ -222,7 +262,7 @@ class messenger // fall back to board default language if the user's language is // missing $template_file. If this does not exist either, // $this->template->set_filenames will do a trigger_error - $template_lang = basename($config['default_lang']); + $template_lang = basename($this->config['default_lang']); } $ext_template_paths = array( @@ -240,20 +280,20 @@ class messenger } else { - $template_path = (!empty($user->lang_path)) ? $user->lang_path : $phpbb_root_path . 'language/'; + $template_path = (!empty($this->user->lang_path)) ? $this->user->lang_path : $this->root_path . 'language/'; $template_path .= $template_lang . '/email'; $template_paths = array( $template_path . $template_dir_prefix, ); - $board_language = basename($config['default_lang']); + $board_language = basename($this->config['default_lang']); // we can only specify default language fallback when the path is not a custom one for which we // do not know the default language alternative if ($template_lang !== $board_language) { - $fallback_template_path = (!empty($user->lang_path)) ? $user->lang_path : $phpbb_root_path . 'language/'; + $fallback_template_path = (!empty($this->user->lang_path)) ? $this->user->lang_path : $this->root_path . 'language/'; $fallback_template_path .= $board_language . '/email'; $template_paths[] = $fallback_template_path . $template_dir_prefix; @@ -266,7 +306,7 @@ class messenger // If everything fails just fall back to en template if ($template_lang !== 'en' && $board_language !== 'en') { - $fallback_template_path = (!empty($user->lang_path)) ? $user->lang_path : $phpbb_root_path . 'language/'; + $fallback_template_path = (!empty($this->user->lang_path)) ? $this->user->lang_path : $this->root_path . 'language/'; $fallback_template_path .= 'en/email'; $template_paths[] = $fallback_template_path . $template_dir_prefix; @@ -288,8 +328,8 @@ class messenger } /** - * assign variables to email template - */ + * assign variables to email template + */ function assign_vars($vars) { $this->setup_template(); @@ -305,61 +345,65 @@ class messenger } /** - * Send the mail out to the recipients set previously in var $this->addresses - * - * @param int $method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH - * @param bool $break Flag indicating if the function only formats the subject - * and the message without sending it - * - * @return bool - */ + * Send the mail out to the recipients set previously in var $this->addresses + * + * @param int $method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH + * @param bool $break Flag indicating if the function only formats the subject + * and the message without sending it + * + * @return bool + */ function send($method = NOTIFY_EMAIL, $break = false) { - global $config, $user, $phpbb_dispatcher; - // We add some standard variables we always use, no need to specify them always - $this->assign_vars(array( + $this->assign_vars([ 'U_BOARD' => generate_board_url(), - 'EMAIL_SIG' => str_replace('
', "\n", "-- \n" . html_entity_decode($config['board_email_sig'], ENT_COMPAT)), - 'SITENAME' => html_entity_decode($config['sitename'], ENT_COMPAT), - )); + 'EMAIL_SIG' => str_replace('
', "\n", "-- \n" . html_entity_decode($this->config['board_email_sig'], ENT_COMPAT)), + 'SITENAME' => html_entity_decode($this->config['sitename'], ENT_COMPAT), + ]); - $subject = $this->subject; + $email = $this->email; + $subject = $this->email->getSubject(); $template = $this->template; /** - * Event to modify the template before parsing - * - * @event core.modify_notification_template - * @var int method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH - * @var bool break Flag indicating if the function only formats the subject - * and the message without sending it - * @var string subject The message subject - * @var \phpbb\template\template template The (readonly) template object - * @since 3.2.4-RC1 - */ - $vars = array('method', 'break', 'subject', 'template'); - extract($phpbb_dispatcher->trigger_event('core.modify_notification_template', compact($vars))); + * Event to modify the template before parsing + * + * @event core.modify_notification_template + * @var bool break Flag indicating if the function only formats the subject + * and the message without sending it + * @var Symfony\Component\Mime\Email email The Symfony Email object + * @var int method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH + * @var string subject The message subject + * @var \phpbb\template\template template The (readonly) template object + * @since 3.2.4-RC1 + * @changed 4.0.0-a1 Added vars: email. + */ + $vars = ['break', 'email', 'method', 'subject', 'template']; + extract($this->dispatcher->trigger_event('core.modify_notification_template', compact($vars))); // Parse message through template $message = trim($this->template->assign_display('body')); /** - * Event to modify notification message text after parsing - * - * @event core.modify_notification_message - * @var int method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH - * @var bool break Flag indicating if the function only formats the subject - * and the message without sending it - * @var string subject The message subject - * @var string message The message text - * @since 3.1.11-RC1 - */ - $vars = array('method', 'break', 'subject', 'message'); - extract($phpbb_dispatcher->trigger_event('core.modify_notification_message', compact($vars))); + * Event to modify notification message text after parsing + * + * @event core.modify_notification_message + * @var bool break Flag indicating if the function only formats the subject + * and the message without sending it + * @var Symfony\Component\Mime\Email email The Symfony Email object + * @var string message The message text + * @var int method User notification method NOTIFY_EMAIL|NOTIFY_IM|NOTIFY_BOTH + * @var string subject The message subject + * @since 3.1.11-RC1 + * @changed 4.0.0-a1 Added vars: email. + */ + $vars = ['break', 'email', 'message', 'method', 'subject']; + extract($this->dispatcher->trigger_event('core.modify_notification_message', compact($vars))); + $this->email = $email; $this->subject = $subject; $this->msg = $message; - unset($subject, $message, $template); + unset($email, $subject, $message, $template); // Because we use \n for newlines in the body message we need to fix line encoding errors for those admins who uploaded email template files in the wrong encoding $this->msg = str_replace("\r\n", "\n", $this->msg); @@ -370,17 +414,17 @@ class messenger $match = array(); if (preg_match('#^(Subject:(.*?))$#m', $this->msg, $match)) { - $this->subject = (trim($match[2]) != '') ? trim($match[2]) : (($this->subject != '') ? $this->subject : $user->lang['NO_EMAIL_SUBJECT']); + $this->subject = (trim($match[2]) != '') ? trim($match[2]) : (($this->subject != '') ? $this->subject : $this->user->lang['NO_EMAIL_SUBJECT']); $drop_header .= '[\r\n]*?' . preg_quote($match[1], '#'); } else { - $this->subject = (($this->subject != '') ? $this->subject : $user->lang['NO_EMAIL_SUBJECT']); + $this->subject = (($this->subject != '') ? $this->subject : $this->user->lang['NO_EMAIL_SUBJECT']); } if (preg_match('#^(List-Unsubscribe:(.*?))$#m', $this->msg, $match)) { - $this->extra_headers[] = $match[1]; + $this->headers->addTextHeader('List-Unsubscribe', trim($match[2])); $drop_header .= '[\r\n]*?' . preg_quote($match[1], '#'); } @@ -389,6 +433,9 @@ class messenger $this->msg = trim(preg_replace('#' . $drop_header . '#s', '', $this->msg)); } + $this->email->subject($this->subject); + $this->email->text($this->msg); + if ($break) { return true; @@ -415,24 +462,22 @@ class messenger } /** - * Add error message to log - */ + * Add error message to log + */ function error($type, $msg) { - global $user, $config, $request, $phpbb_log; - // Session doesn't exist, create it - if (!isset($user->session_id) || $user->session_id === '') + if (!isset($this->user->session_id) || $this->user->session_id === '') { - $user->session_begin(); + $this->user->session_begin(); } - $calling_page = html_entity_decode($request->server('REQUEST_URI'), ENT_COMPAT); + $calling_page = html_entity_decode($this->request->server('REQUEST_URI'), ENT_COMPAT); switch ($type) { case 'EMAIL': - $message = 'EMAIL/' . (($config['smtp_delivery']) ? 'SMTP' : 'PHP/mail()') . ''; + $message = 'EMAIL/' . (($this->config['smtp_delivery']) ? 'SMTP' : 'PHP/mail()') . ''; break; default: @@ -441,17 +486,15 @@ class messenger } $message .= '
' . htmlspecialchars($calling_page, ENT_COMPAT) . '

' . $msg . '
'; - $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_ERROR_' . $type, false, array($message)); + $this->log->add('critical', $this->user->data['user_id'], $this->user->ip, 'LOG_ERROR_' . $type, false, [$message]); } /** - * Save to queue - */ + * Save to queue + */ function save_queue() { - global $config; - - if ($config['email_package_size'] && $this->use_queue && !empty($this->queue)) + if ($this->config['email_package_size'] && $this->use_queue && !empty($this->queue)) { $this->queue->save(); return; @@ -459,128 +502,176 @@ class messenger } /** - * Generates a valid message id to be used in emails - * - * @return string message id - */ - function generate_message_id() + * Detect proper method to add header + * + * @return string + */ + public function get_header_method(string $name) { - global $config, $request; + $parts = explode('\\', self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class); + $method = 'add'.ucfirst(array_pop($parts)); + if ('addUnstructuredHeader' === $method) + { + $method = 'addTextHeader'; + } + else if ('addIdentificationHeader' === $method) + { + $method = 'addIdHeader'; + } - $domain = ($config['server_name']) ?: $request->server('SERVER_NAME', 'phpbb.generated'); - - return md5(unique_id()) . '@' . $domain; + return $method; } /** - * Return email header - */ - function build_header($to, $cc, $bcc) + * Set email headers + */ + function build_header() { - global $config, $phpbb_dispatcher; + $headers = []; - // We could use keys here, but we won't do this for 3.0.x to retain backwards compatibility - $headers = array(); - - $headers[] = 'From: ' . $this->from; - - if ($cc) + $board_contact = $this->config['board_contact']; + if (empty($this->email->getReplyTo())) { - $headers[] = 'Cc: ' . $cc; + $this->replyto = $board_contact; + $headers['Reply-To'] = $this->replyto; } - if ($bcc) + if (empty($this->email->getFrom())) { - $headers[] = 'Bcc: ' . $bcc; + $this->from = $board_contact; + $headers['From'] = $this->from; } - $headers[] = 'Reply-To: ' . $this->replyto; - $headers[] = 'Return-Path: <' . $config['board_email'] . '>'; - $headers[] = 'Sender: <' . $config['board_email'] . '>'; - $headers[] = 'MIME-Version: 1.0'; - $headers[] = 'Message-ID: <' . $this->generate_message_id() . '>'; - $headers[] = 'Date: ' . date('r', time()); - $headers[] = 'Content-Type: text/plain; charset=UTF-8'; // format=flowed - $headers[] = 'Content-Transfer-Encoding: 8bit'; // 7bit - - $headers[] = 'X-Priority: ' . $this->mail_priority; - $headers[] = 'X-MSMail-Priority: ' . (($this->mail_priority == MAIL_LOW_PRIORITY) ? 'Low' : (($this->mail_priority == MAIL_NORMAL_PRIORITY) ? 'Normal' : 'High')); - $headers[] = 'X-Mailer: phpBB3'; - $headers[] = 'X-MimeOLE: phpBB3'; - $headers[] = 'X-phpBB-Origin: phpbb://' . str_replace(array('http://', 'https://'), array('', ''), generate_board_url()); + $headers['Return-Path'] = new Address($this->config['board_email']); + $headers['Sender'] = new Address($this->config['board_email']); + $headers['X-Priority'] = sprintf('%d (%s)', $this->mail_priority, self::PRIORITY_MAP[$this->mail_priority]); + $headers['X-MSMail-Priority'] = self::PRIORITY_MAP[$this->mail_priority]; + $headers['X-Mailer'] = 'phpBB3'; + $headers['X-MimeOLE'] = 'phpBB3'; + $headers['X-phpBB-Origin'] = 'phpbb://' . str_replace(['http://', 'https://'], ['', ''], generate_board_url()); /** - * Event to modify email header entries - * - * @event core.modify_email_headers - * @var array headers Array containing email header entries - * @since 3.1.11-RC1 - */ + * 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 = array('headers'); - extract($phpbb_dispatcher->trigger_event('core.modify_email_headers', compact($vars))); + extract($this->dispatcher->trigger_event('core.modify_email_headers', compact($vars))); - if (count($this->extra_headers)) + foreach ($headers as $header => $value) { - $headers = array_merge($headers, $this->extra_headers); + // addMailboxListHeader() requires value to be array + if ($this->get_header_method($header) == 'addMailboxListHeader') + { + $value = [$value]; + } + $this->headers->addHeader($header, $value); } - return $headers; + return true; } /** - * Send out emails - */ + * Generates a valid transport to send email + * + * @param bool $dummy Flag to disable mail delivery + * @return Symfony\Component\Mailer\Transport Symfony Transport object + */ + function get_transport($dummy = false) + { + if ($dummy) + { + $dsn = 'null://null'; + } + else if ($this->config['smtp_delivery']) + { + $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']; + $dsn = "smtp://$user:$password@$smtp_host:$smtp_port"; + } + else + { + $dsn = 'sendmail://default'; + } + + $transport = Transport::fromDsn($dsn); + + if ($this->config['smtp_delivery']) + { + // Set ssl context options, see http://php.net/manual/en/context.ssl.php + $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']; + $options = [ + 'ssl' => [ + 'verify_peer' => $verify_peer, + 'verify_peer_name' => $verify_peer_name, + 'allow_self_signed' => $allow_self_signed, + ] + ]; + $transport->getStream()->setStreamOptions($options); + } + + return $transport; + } + + /** + * Send out emails + */ function msg_email() { - global $config, $phpbb_dispatcher; - - if (empty($config['email_enable'])) + if (empty($this->config['email_enable'])) { return false; } // Addresses to send to? - if (empty($this->addresses) || (empty($this->addresses['to']) && empty($this->addresses['cc']) && empty($this->addresses['bcc']))) + if (empty($this->email->getTo())) { // Send was successful. ;) return true; } $use_queue = false; - if ($config['email_package_size'] && $this->use_queue) + if ($this->config['email_package_size'] && $this->use_queue) { if (empty($this->queue)) { $this->queue = new queue(); - $this->queue->init('email', $config['email_package_size']); + $this->queue->init('email', $this->config['email_package_size']); } $use_queue = true; } - $contact_name = html_entity_decode($config['board_contact_name'], ENT_COMPAT); - $board_contact = (($contact_name !== '') ? '"' . mail_encode($contact_name) . '" ' : '') . '<' . $config['board_contact'] . '>'; + $contact_name = html_entity_decode($this->config['board_contact_name'], ENT_COMPAT); + $board_contact = $this->config['board_contact']; $break = false; - $addresses = $this->addresses; $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 array addresses The message recipients - * @var string subject The message subject - * @var string msg The message text - * @since 3.2.4-RC1 - */ + * 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 Symfony\Component\Mime\Email email The Symfony Email object + * @since 3.2.4-RC1 + * @changed 4.0.0-a1 Added vars: email. Removed vars: addresses + */ $vars = array( 'break', - 'addresses', 'subject', 'msg', + 'email', ); - extract($phpbb_dispatcher->trigger_event('core.notification_message_email', compact($vars))); + extract($this->dispatcher->trigger_event('core.notification_message_email', compact($vars))); $this->addresses = $addresses; $this->subject = $subject; @@ -592,66 +683,87 @@ class messenger return true; } - if (empty($this->replyto)) + if (empty($this->email->getReplyto())) { - $this->replyto = $board_contact; + $this->email->replyTo(new Address($board_contact)); } - if (empty($this->from)) + if (empty($this->email->getFrom())) { - $this->from = $board_contact; - } - - $encode_eol = $config['smtp_delivery'] || PHP_VERSION_ID >= 80000 ? "\r\n" : PHP_EOL; - - // Build to, cc and bcc strings - $to = $cc = $bcc = ''; - foreach ($this->addresses as $type => $address_ary) - { - if ($type == 'im') - { - continue; - } - - foreach ($address_ary as $which_ary) - { - ${$type} .= ((${$type} != '') ? ', ' : '') . (($which_ary['name'] != '') ? mail_encode($which_ary['name'], $encode_eol) . ' <' . $which_ary['email'] . '>' : $which_ary['email']); - } + $this->email->from(new Address($board_contact)); } // Build header - $headers = $this->build_header($to, $cc, $bcc); + $this->build_header(); // Send message ... if (!$use_queue) { - $mail_to = ($to == '') ? 'undisclosed-recipients:;' : $to; - $err_msg = ''; + $transport = $this->get_transport(); + $mailer = new Mailer($transport); - if ($config['smtp_delivery']) - { - $result = smtpmail($this->addresses, mail_encode($this->subject), wordwrap(utf8_wordwrap($this->msg), 997, "\n", true), $err_msg, $headers); - } - else - { - $result = phpbb_mail($mail_to, $this->subject, $this->msg, $headers, $encode_eol, $err_msg); - } + $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 Symfony\Component\Mime\Email 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))); - if (!$result) + $this->subject = $subject; + $this->msg = $msg; + $this->headers = $headers; + $this->email = $email; + + try { - $this->error('EMAIL', $err_msg); + $mailer->send($this->email); + } + catch (TransportExceptionInterface $e) + { + $this->error('EMAIL', $e->getDebug()); return false; } + + /** + * Execute code after sending out emails with PHP's mail function + * + * @event core.phpbb_mail_after + * @var Symfony\Component\Mime\Email 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->put('email', array( - 'to' => $to, - 'addresses' => $this->addresses, - 'subject' => $this->subject, - 'msg' => $this->msg, - 'headers' => $headers) - ); + $this->queue->put('email', [ + 'email' => $this->email, + ]); } return true; @@ -662,9 +774,7 @@ class messenger */ function msg_jabber() { - global $config, $user, $phpbb_root_path, $phpEx; - - if (empty($config['jab_enable']) || empty($config['jab_host']) || empty($config['jab_username']) || empty($config['jab_password'])) + if (empty($this->config['jab_enable']) || empty($this->config['jab_host']) || empty($this->config['jab_username']) || empty($this->config['jab_password'])) { return false; } @@ -676,12 +786,12 @@ class messenger } $use_queue = false; - if ($config['jab_package_size'] && $this->use_queue) + if ($this->config['jab_package_size'] && $this->use_queue) { if (empty($this->queue)) { $this->queue = new queue(); - $this->queue->init('jabber', $config['jab_package_size']); + $this->queue->init('jabber', $this->config['jab_package_size']); } $use_queue = true; } @@ -695,18 +805,18 @@ class messenger if (!$use_queue) { - include_once($phpbb_root_path . 'includes/functions_jabber.' . $phpEx); - $this->jabber = new jabber($config['jab_host'], $config['jab_port'], $config['jab_username'], html_entity_decode($config['jab_password'], ENT_COMPAT), $config['jab_use_ssl'], $config['jab_verify_peer'], $config['jab_verify_peer_name'], $config['jab_allow_self_signed']); + include_once($this->root_path . 'includes/functions_jabber.' . $this->php_ext); + $this->jabber = new jabber($this->config['jab_host'], $this->config['jab_port'], $this->config['jab_username'], html_entity_decode($this->config['jab_password'], ENT_COMPAT), $this->config['jab_use_ssl'], $this->config['jab_verify_peer'], $this->config['jab_verify_peer_name'], $this->config['jab_allow_self_signed']); if (!$this->jabber->connect()) { - $this->error('JABBER', $user->lang['ERR_JAB_CONNECT'] . '
' . $this->jabber->get_log()); + $this->error('JABBER', $this->user->lang['ERR_JAB_CONNECT'] . '
' . $this->jabber->get_log()); return false; } if (!$this->jabber->login()) { - $this->error('JABBER', $user->lang['ERR_JAB_AUTH'] . '
' . $this->jabber->get_log()); + $this->error('JABBER', $this->user->lang['ERR_JAB_AUTH'] . '
' . $this->jabber->get_log()); return false; } @@ -734,8 +844,6 @@ class messenger */ protected function setup_template() { - global $phpbb_container, $phpbb_dispatcher; - if ($this->template instanceof \phpbb\template\template) { return; @@ -749,20 +857,20 @@ class messenger $phpbb_container->getParameter('core.template.cache_path'), $phpbb_container->get('ext.manager'), new \phpbb\template\twig\loader(), - $phpbb_dispatcher, - array() + $this->dispatcher, + [] ); - $template_environment->setLexer($phpbb_container->get('template.twig.lexer')); + $template_environment->setLexer($this->phpbb_container->get('template.twig.lexer')); $this->template = new \phpbb\template\twig\twig( - $phpbb_container->get('path_helper'), - $phpbb_container->get('config'), + $this->phpbb_container->get('path_helper'), + $this->config, new \phpbb\template\context(), $template_environment, - $phpbb_container->getParameter('core.template.cache_path'), - $phpbb_container->get('user'), - $phpbb_container->get('template.twig.extensions.collection'), - $phpbb_container->get('ext.manager') + $this->phpbb_container->getParameter('core.template.cache_path'), + $this->user, + $this->phpbb_container->get('template.twig.extensions.collection'), + $this->phpbb_container->get('ext.manager') ); } @@ -782,8 +890,8 @@ class messenger */ class queue { - var $data = array(); - var $queue_data = array(); + var $data = []; + var $queue_data = []; var $package_size = 0; var $cache_file = ''; var $eol = "\n"; @@ -798,11 +906,15 @@ class queue */ function __construct() { - global $phpEx, $phpbb_root_path, $phpbb_filesystem, $phpbb_container; + global $phpbb_container; - $this->data = array(); - $this->cache_file = $phpbb_container->getParameter('core.cache_dir') . "queue.$phpEx"; - $this->filesystem = $phpbb_filesystem; + $this->phpbb_container = $phpbb_container; + $this->php_ext = $this->phpbb_container->getParameter('core.php_ext'); + $this->root_path = $this->phpbb_container->getParameter('core.root_path'); + $this->cache_file = $this->phpbb_container->getParameter('core.cache_dir') . "queue.{$this->php_ext}"; + $this->filesystem = $this->phpbb_container->get('filesystem'); + $this->config = $this->phpbb_container->get('config'); + $this->dispatcher = $this->phpbb_container->get('dispatcher'); } /** @@ -829,25 +941,23 @@ class queue */ function process() { - global $config, $phpEx, $phpbb_root_path, $user, $phpbb_dispatcher; - $lock = new \phpbb\lock\flock($this->cache_file); $lock->acquire(); // avoid races, check file existence once $have_cache_file = file_exists($this->cache_file); - if (!$have_cache_file || $config['last_queue_run'] > time() - $config['queue_interval']) + if (!$have_cache_file || $this->config['last_queue_run'] > time() - $this->config['queue_interval']) { if (!$have_cache_file) { - $config->set('last_queue_run', time(), false); + $this->config->set('last_queue_run', time(), false); } $lock->release(); return; } - $config->set('last_queue_run', time(), false); + $this->config->set('last_queue_run', time(), false); include($this->cache_file); @@ -880,7 +990,7 @@ class queue { case 'email': // Delete the email queued objects if mailing is disabled - if (!$config['email_enable']) + if (!$this->config['email_enable']) { unset($this->queue_data['email']); continue 2; @@ -888,26 +998,26 @@ class queue break; case 'jabber': - if (!$config['jab_enable']) + if (!$this->config['jab_enable']) { unset($this->queue_data['jabber']); continue 2; } - include_once($phpbb_root_path . 'includes/functions_jabber.' . $phpEx); - $this->jabber = new jabber($config['jab_host'], $config['jab_port'], $config['jab_username'], html_entity_decode($config['jab_password'], ENT_COMPAT), $config['jab_use_ssl'], $config['jab_verify_peer'], $config['jab_verify_peer_name'], $config['jab_allow_self_signed']); + include_once($this->root_path . 'includes/functions_jabber.' . $this->php_ext); + $this->jabber = new jabber($this->config['jab_host'], $this->config['jab_port'], $this->config['jab_username'], html_entity_decode($this->config['jab_password'], ENT_COMPAT), $this->config['jab_use_ssl'], $this->config['jab_verify_peer'], $this->config['jab_verify_peer_name'], $this->config['jab_allow_self_signed']); if (!$this->jabber->connect()) { $messenger = new messenger(); - $messenger->error('JABBER', $user->lang['ERR_JAB_CONNECT']); + $messenger->error('JABBER', $this->user->lang['ERR_JAB_CONNECT']); continue 2; } if (!$this->jabber->login()) { $messenger = new messenger(); - $messenger->error('JABBER', $user->lang['ERR_JAB_AUTH']); + $messenger->error('JABBER', $this->user->lang['ERR_JAB_AUTH']); continue 2; } @@ -928,42 +1038,33 @@ class queue case 'email': $break = false; /** - * Event to send message via external transport - * - * @event core.notification_message_process - * @var bool break Flag indicating if the function return after hook - * @var array addresses The message recipients - * @var string subject The message subject - * @var string msg The message text - * @since 3.2.4-RC1 - */ - $vars = array( + * Event to send message via external transport + * + * @event core.notification_message_process + * @var bool break Flag indicating if the function return after hook + * @var Symfony\Component\Mime\Email 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', - 'addresses', - 'subject', - 'msg', - ); - extract($phpbb_dispatcher->trigger_event('core.notification_message_process', compact($vars))); + 'email', + ]; + extract($this->dispatcher->trigger_event('core.notification_message_process', compact($vars))); if (!$break) { - $err_msg = ''; - $to = (!$to) ? 'undisclosed-recipients:;' : $to; + $messenger = new messenger(); + $transport = $messenger->get_transport(); + $mailer = new Mailer($transport); - if ($config['smtp_delivery']) + try { - $result = smtpmail($addresses, mail_encode($subject), wordwrap(utf8_wordwrap($msg), 997, "\n", true), $err_msg, $headers); + $mailer->send($email); } - else + catch (TransportExceptionInterface $e) { - $encode_eol = $config['smtp_delivery'] || PHP_VERSION_ID >= 80000 ? "\r\n" : PHP_EOL; - $result = phpbb_mail($to, $subject, $msg, $headers, $encode_eol, $err_msg); - } - - if (!$result) - { - $messenger = new messenger(); - $messenger->error('EMAIL', $err_msg); + $messenger->error('EMAIL', $e->getDebug()); continue 2; } } @@ -1085,932 +1186,3 @@ class queue $lock->release(); } } - -/** -* Replacement or substitute for PHP's mail command -*/ -function smtpmail($addresses, $subject, $message, &$err_msg, $headers = false) -{ - global $config, $user; - - // Fix any bare linefeeds in the message to make it RFC821 Compliant. - $message = preg_replace("#(?lang['NO_EMAIL_SUBJECT'])) ? $user->lang['NO_EMAIL_SUBJECT'] : 'No email subject specified'; - return false; - } - - if (trim($message) == '') - { - $err_msg = (isset($user->lang['NO_EMAIL_MESSAGE'])) ? $user->lang['NO_EMAIL_MESSAGE'] : 'Email message was blank'; - return false; - } - - $mail_rcpt = $mail_to = $mail_cc = array(); - - // Build correct addresses for RCPT TO command and the client side display (TO, CC) - if (isset($addresses['to']) && count($addresses['to'])) - { - foreach ($addresses['to'] as $which_ary) - { - $mail_to[] = ($which_ary['name'] != '') ? mail_encode(trim($which_ary['name'])) . ' <' . trim($which_ary['email']) . '>' : '<' . trim($which_ary['email']) . '>'; - $mail_rcpt['to'][] = '<' . trim($which_ary['email']) . '>'; - } - } - - if (isset($addresses['bcc']) && count($addresses['bcc'])) - { - foreach ($addresses['bcc'] as $which_ary) - { - $mail_rcpt['bcc'][] = '<' . trim($which_ary['email']) . '>'; - } - } - - if (isset($addresses['cc']) && count($addresses['cc'])) - { - foreach ($addresses['cc'] as $which_ary) - { - $mail_cc[] = ($which_ary['name'] != '') ? mail_encode(trim($which_ary['name'])) . ' <' . trim($which_ary['email']) . '>' : '<' . trim($which_ary['email']) . '>'; - $mail_rcpt['cc'][] = '<' . trim($which_ary['email']) . '>'; - } - } - - $smtp = new smtp_class(); - - $errno = 0; - $errstr = ''; - - $smtp->add_backtrace('Connecting to ' . $config['smtp_host'] . ':' . $config['smtp_port']); - - // Ok we have error checked as much as we can to this point let's get on it already. - if (!class_exists('\phpbb\error_collector')) - { - global $phpbb_root_path, $phpEx; - include($phpbb_root_path . 'includes/error_collector.' . $phpEx); - } - $collector = new \phpbb\error_collector; - $collector->install(); - - $options = array(); - $verify_peer = (bool) $config['smtp_verify_peer']; - $verify_peer_name = (bool) $config['smtp_verify_peer_name']; - $allow_self_signed = (bool) $config['smtp_allow_self_signed']; - $remote_socket = $config['smtp_host'] . ':' . $config['smtp_port']; - - // Set ssl context options, see http://php.net/manual/en/context.ssl.php - $options['ssl'] = array('verify_peer' => $verify_peer, 'verify_peer_name' => $verify_peer_name, 'allow_self_signed' => $allow_self_signed); - $socket_context = stream_context_create($options); - - $smtp->socket = @stream_socket_client($remote_socket, $errno, $errstr, 20, STREAM_CLIENT_CONNECT, $socket_context); - $collector->uninstall(); - $error_contents = $collector->format_errors(); - - if (!$smtp->socket) - { - if ($errstr) - { - $errstr = utf8_convert_message($errstr); - } - - $err_msg = (isset($user->lang['NO_CONNECT_TO_SMTP_HOST'])) ? sprintf($user->lang['NO_CONNECT_TO_SMTP_HOST'], $errno, $errstr) : "Could not connect to smtp host : $errno : $errstr"; - $err_msg .= ($error_contents) ? '

' . htmlspecialchars($error_contents, ENT_COMPAT) : ''; - return false; - } - - // Wait for reply - if ($err_msg = $smtp->server_parse('220', __LINE__)) - { - $smtp->close_session($err_msg); - return false; - } - - // Let me in. This function handles the complete authentication process - if ($err_msg = $smtp->log_into_server($config['smtp_host'], $config['smtp_username'], html_entity_decode($config['smtp_password'], ENT_COMPAT), $config['smtp_auth_method'])) - { - $smtp->close_session($err_msg); - return false; - } - - // From this point onward most server response codes should be 250 - // Specify who the mail is from.... - $smtp->server_send('MAIL FROM:<' . $config['board_email'] . '>'); - if ($err_msg = $smtp->server_parse('250', __LINE__)) - { - $smtp->close_session($err_msg); - return false; - } - - // Specify each user to send to and build to header. - $to_header = implode(', ', $mail_to); - $cc_header = implode(', ', $mail_cc); - - // Now tell the MTA to send the Message to the following people... [TO, BCC, CC] - $rcpt = false; - foreach ($mail_rcpt as $type => $mail_to_addresses) - { - foreach ($mail_to_addresses as $mail_to_address) - { - // Add an additional bit of error checking to the To field. - if (preg_match('#[^ ]+\@[^ ]+#', $mail_to_address)) - { - $smtp->server_send("RCPT TO:$mail_to_address"); - if ($err_msg = $smtp->server_parse('250', __LINE__)) - { - // We continue... if users are not resolved we do not care - if ($smtp->numeric_response_code != 550) - { - $smtp->close_session($err_msg); - return false; - } - } - else - { - $rcpt = true; - } - } - } - } - - // We try to send messages even if a few people do not seem to have valid email addresses, but if no one has, we have to exit here. - if (!$rcpt) - { - $user->session_begin(); - $err_msg .= '

'; - $err_msg .= (isset($user->lang['INVALID_EMAIL_LOG'])) ? sprintf($user->lang['INVALID_EMAIL_LOG'], htmlspecialchars($mail_to_address, ENT_COMPAT)) : '' . htmlspecialchars($mail_to_address, ENT_COMPAT) . ' possibly an invalid email address?'; - $smtp->close_session($err_msg); - return false; - } - - // Ok now we tell the server we are ready to start sending data - $smtp->server_send('DATA'); - - // This is the last response code we look for until the end of the message. - if ($err_msg = $smtp->server_parse('354', __LINE__)) - { - $smtp->close_session($err_msg); - return false; - } - - // Send the Subject Line... - $smtp->server_send("Subject: $subject"); - - // Now the To Header. - $to_header = ($to_header == '') ? 'undisclosed-recipients:;' : $to_header; - $smtp->server_send("To: $to_header"); - - // Now the CC Header. - if ($cc_header != '') - { - $smtp->server_send("CC: $cc_header"); - } - - // Now any custom headers.... - if ($headers !== false) - { - $smtp->server_send("$headers\r\n"); - } - - // Ok now we are ready for the message... - $smtp->server_send($message); - - // Ok the all the ingredients are mixed in let's cook this puppy... - $smtp->server_send('.'); - if ($err_msg = $smtp->server_parse('250', __LINE__)) - { - $smtp->close_session($err_msg); - return false; - } - - // Now tell the server we are done and close the socket... - $smtp->server_send('QUIT'); - $smtp->close_session($err_msg); - - return true; -} - -/** -* SMTP Class -* Auth Mechanisms originally taken from the AUTH Modules found within the PHP Extension and Application Repository (PEAR) -* See docs/AUTHORS for more details -*/ -class smtp_class -{ - var $server_response = ''; - var $socket = 0; - protected $socket_tls = false; - var $responses = array(); - var $commands = array(); - var $numeric_response_code = 0; - - var $backtrace = false; - var $backtrace_log = array(); - - function __construct() - { - // Always create a backtrace for admins to identify SMTP problems - $this->backtrace = true; - $this->backtrace_log = array(); - } - - /** - * Add backtrace message for debugging - */ - function add_backtrace($message) - { - if ($this->backtrace) - { - $this->backtrace_log[] = utf8_htmlspecialchars($message, ENT_COMPAT); - } - } - - /** - * Send command to smtp server - */ - function server_send($command, $private_info = false) - { - fputs($this->socket, $command . "\r\n"); - - (!$private_info) ? $this->add_backtrace("# $command") : $this->add_backtrace('# Omitting sensitive information'); - - // We could put additional code here - } - - /** - * We use the line to give the support people an indication at which command the error occurred - */ - function server_parse($response, $line) - { - global $user; - - $this->server_response = ''; - $this->responses = array(); - $this->numeric_response_code = 0; - - while (substr($this->server_response, 3, 1) != ' ') - { - if (!($this->server_response = fgets($this->socket, 256))) - { - return (isset($user->lang['NO_EMAIL_RESPONSE_CODE'])) ? $user->lang['NO_EMAIL_RESPONSE_CODE'] : 'Could not get mail server response codes'; - } - $this->responses[] = substr(rtrim($this->server_response), 4); - $this->numeric_response_code = (int) substr($this->server_response, 0, 3); - - $this->add_backtrace("LINE: $line <- {$this->server_response}"); - } - - if (!(substr($this->server_response, 0, 3) == $response)) - { - $this->numeric_response_code = (int) substr($this->server_response, 0, 3); - return (isset($user->lang['EMAIL_SMTP_ERROR_RESPONSE'])) ? sprintf($user->lang['EMAIL_SMTP_ERROR_RESPONSE'], $line, $this->server_response) : "Ran into problems sending Mail at Line $line. Response: $this->server_response"; - } - - return 0; - } - - /** - * Close session - */ - function close_session(&$err_msg) - { - fclose($this->socket); - - if ($this->backtrace) - { - $message = '

Backtrace

' . implode('
', $this->backtrace_log) . '

'; - $err_msg .= $message; - } - } - - /** - * Log into server and get possible auth codes if neccessary - */ - function log_into_server($hostname, $username, $password, $default_auth_method) - { - global $user; - - // Here we try to determine the *real* hostname (reverse DNS entry preferrably) - if (function_exists('php_uname') && !empty($local_host = php_uname('n'))) - { - // Able to resolve name to IP - if (($addr = @gethostbyname($local_host)) !== $local_host) - { - // Able to resolve IP back to name - if (!empty($name = @gethostbyaddr($addr)) && $name !== $addr) - { - $local_host = $name; - } - } - } - else - { - $local_host = $user->host; - } - - // If we are authenticating through pop-before-smtp, we - // have to login ones before we get authenticated - // NOTE: on some configurations the time between an update of the auth database takes so - // long that the first email send does not work. This is not a biggie on a live board (only - // the install mail will most likely fail) - but on a dynamic ip connection this might produce - // severe problems and is not fixable! - if ($default_auth_method == 'POP-BEFORE-SMTP' && $username && $password) - { - global $config; - - $errno = 0; - $errstr = ''; - - $this->server_send("QUIT"); - fclose($this->socket); - - $this->pop_before_smtp($hostname, $username, $password); - $username = $password = $default_auth_method = ''; - - // We need to close the previous session, else the server is not - // able to get our ip for matching... - if (!$this->socket = @fsockopen($config['smtp_host'], $config['smtp_port'], $errno, $errstr, 10)) - { - if ($errstr) - { - $errstr = utf8_convert_message($errstr); - } - - $err_msg = (isset($user->lang['NO_CONNECT_TO_SMTP_HOST'])) ? sprintf($user->lang['NO_CONNECT_TO_SMTP_HOST'], $errno, $errstr) : "Could not connect to smtp host : $errno : $errstr"; - return $err_msg; - } - - // Wait for reply - if ($err_msg = $this->server_parse('220', __LINE__)) - { - $this->close_session($err_msg); - return $err_msg; - } - } - - $hello_result = $this->hello($local_host); - if (!is_null($hello_result)) - { - return $hello_result; - } - - // SMTP STARTTLS (RFC 3207) - if (!$this->socket_tls) - { - $this->socket_tls = $this->starttls(); - - if ($this->socket_tls) - { - // Switched to TLS - // RFC 3207: "The client MUST discard any knowledge obtained from the server, [...]" - // So say hello again - $hello_result = $this->hello($local_host); - - if (!is_null($hello_result)) - { - return $hello_result; - } - } - } - - // If we are not authenticated yet, something might be wrong if no username and passwd passed - if (!$username || !$password) - { - return false; - } - - if (!isset($this->commands['AUTH'])) - { - return (isset($user->lang['SMTP_NO_AUTH_SUPPORT'])) ? $user->lang['SMTP_NO_AUTH_SUPPORT'] : 'SMTP server does not support authentication'; - } - - // Get best authentication method - $available_methods = explode(' ', $this->commands['AUTH']); - - // Define the auth ordering if the default auth method was not found - $auth_methods = array('PLAIN', 'LOGIN', 'CRAM-MD5', 'DIGEST-MD5'); - $method = ''; - - if (in_array($default_auth_method, $available_methods)) - { - $method = $default_auth_method; - } - else - { - foreach ($auth_methods as $_method) - { - if (in_array($_method, $available_methods)) - { - $method = $_method; - break; - } - } - } - - if (!$method) - { - return (isset($user->lang['NO_SUPPORTED_AUTH_METHODS'])) ? $user->lang['NO_SUPPORTED_AUTH_METHODS'] : 'No supported authentication methods'; - } - - $method = strtolower(str_replace('-', '_', $method)); - return $this->$method($username, $password); - } - - /** - * SMTP EHLO/HELO - * - * @return mixed Null if the authentication process is supposed to continue - * False if already authenticated - * Error message (string) otherwise - */ - protected function hello($hostname) - { - // Try EHLO first - $this->server_send("EHLO $hostname"); - if ($err_msg = $this->server_parse('250', __LINE__)) - { - // a 503 response code means that we're already authenticated - if ($this->numeric_response_code == 503) - { - return false; - } - - // If EHLO fails, we try HELO - $this->server_send("HELO $hostname"); - if ($err_msg = $this->server_parse('250', __LINE__)) - { - return ($this->numeric_response_code == 503) ? false : $err_msg; - } - } - - foreach ($this->responses as $response) - { - $response = explode(' ', $response); - $response_code = $response[0]; - unset($response[0]); - $this->commands[$response_code] = implode(' ', $response); - } - - return null; - } - - /** - * SMTP STARTTLS (RFC 3207) - * - * @return bool Returns true if TLS was started - * Otherwise false - */ - protected function starttls() - { - global $config; - - // allow SMTPS (what was used by phpBB 3.0) if hostname is prefixed with tls:// or ssl:// - if (strpos($config['smtp_host'], 'tls://') === 0 || strpos($config['smtp_host'], 'ssl://') === 0) - { - return true; - } - - if (!function_exists('stream_socket_enable_crypto')) - { - return false; - } - - if (!isset($this->commands['STARTTLS'])) - { - return false; - } - - $this->server_send('STARTTLS'); - - if ($err_msg = $this->server_parse('220', __LINE__)) - { - return false; - } - - $result = false; - $stream_meta = stream_get_meta_data($this->socket); - - if (socket_set_blocking($this->socket, 1)) - { - // https://secure.php.net/manual/en/function.stream-socket-enable-crypto.php#119122 - $crypto = (phpbb_version_compare(PHP_VERSION, '5.6.7', '<')) ? STREAM_CRYPTO_METHOD_TLS_CLIENT : STREAM_CRYPTO_METHOD_SSLv23_CLIENT; - $result = stream_socket_enable_crypto($this->socket, true, $crypto); - socket_set_blocking($this->socket, (int) $stream_meta['blocked']); - } - - return $result; - } - - /** - * Pop before smtp authentication - */ - function pop_before_smtp($hostname, $username, $password) - { - global $user; - - if (!$this->socket = @fsockopen($hostname, 110, $errno, $errstr, 10)) - { - if ($errstr) - { - $errstr = utf8_convert_message($errstr); - } - - return (isset($user->lang['NO_CONNECT_TO_SMTP_HOST'])) ? sprintf($user->lang['NO_CONNECT_TO_SMTP_HOST'], $errno, $errstr) : "Could not connect to smtp host : $errno : $errstr"; - } - - $this->server_send("USER $username", true); - if ($err_msg = $this->server_parse('+OK', __LINE__)) - { - return $err_msg; - } - - $this->server_send("PASS $password", true); - if ($err_msg = $this->server_parse('+OK', __LINE__)) - { - return $err_msg; - } - - $this->server_send('QUIT'); - fclose($this->socket); - - return false; - } - - /** - * Plain authentication method - */ - function plain($username, $password) - { - $this->server_send('AUTH PLAIN'); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return ($this->numeric_response_code == 503) ? false : $err_msg; - } - - $base64_method_plain = base64_encode("\0" . $username . "\0" . $password); - $this->server_send($base64_method_plain, true); - if ($err_msg = $this->server_parse('235', __LINE__)) - { - return $err_msg; - } - - return false; - } - - /** - * Login authentication method - */ - function login($username, $password) - { - $this->server_send('AUTH LOGIN'); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return ($this->numeric_response_code == 503) ? false : $err_msg; - } - - $this->server_send(base64_encode($username), true); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return $err_msg; - } - - $this->server_send(base64_encode($password), true); - if ($err_msg = $this->server_parse('235', __LINE__)) - { - return $err_msg; - } - - return false; - } - - /** - * cram_md5 authentication method - */ - function cram_md5($username, $password) - { - $this->server_send('AUTH CRAM-MD5'); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return ($this->numeric_response_code == 503) ? false : $err_msg; - } - - $md5_challenge = base64_decode($this->responses[0]); - $password = (strlen($password) > 64) ? pack('H32', md5($password)) : ((strlen($password) < 64) ? str_pad($password, 64, chr(0)) : $password); - $md5_digest = md5((substr($password, 0, 64) ^ str_repeat(chr(0x5C), 64)) . (pack('H32', md5((substr($password, 0, 64) ^ str_repeat(chr(0x36), 64)) . $md5_challenge)))); - - $base64_method_cram_md5 = base64_encode($username . ' ' . $md5_digest); - - $this->server_send($base64_method_cram_md5, true); - if ($err_msg = $this->server_parse('235', __LINE__)) - { - return $err_msg; - } - - return false; - } - - /** - * digest_md5 authentication method - * A real pain in the *** - */ - function digest_md5($username, $password) - { - global $config, $user; - - $this->server_send('AUTH DIGEST-MD5'); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return ($this->numeric_response_code == 503) ? false : $err_msg; - } - - $md5_challenge = base64_decode($this->responses[0]); - - // Parse the md5 challenge - from AUTH_SASL (PEAR) - $tokens = array(); - while (preg_match('/^([a-z-]+)=("[^"]+(?host; - } - - // Maxbuf - if (empty($tokens['maxbuf'])) - { - $tokens['maxbuf'] = 65536; - } - - // Required: nonce, algorithm - if (empty($tokens['nonce']) || empty($tokens['algorithm'])) - { - $tokens = array(); - } - $md5_challenge = $tokens; - - if (!empty($md5_challenge)) - { - $str = ''; - for ($i = 0; $i < 32; $i++) - { - $str .= chr(mt_rand(0, 255)); - } - $cnonce = base64_encode($str); - - $digest_uri = 'smtp/' . $config['smtp_host']; - - $auth_1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $md5_challenge['realm'], $password))), $md5_challenge['nonce'], $cnonce); - $auth_2 = 'AUTHENTICATE:' . $digest_uri; - $response_value = md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($auth_1), $md5_challenge['nonce'], $cnonce, md5($auth_2))); - - $input_string = sprintf('username="%s",realm="%s",nonce="%s",cnonce="%s",nc="00000001",qop=auth,digest-uri="%s",response=%s,%d', $username, $md5_challenge['realm'], $md5_challenge['nonce'], $cnonce, $digest_uri, $response_value, $md5_challenge['maxbuf']); - } - else - { - return (isset($user->lang['INVALID_DIGEST_CHALLENGE'])) ? $user->lang['INVALID_DIGEST_CHALLENGE'] : 'Invalid digest challenge'; - } - - $base64_method_digest_md5 = base64_encode($input_string); - $this->server_send($base64_method_digest_md5, true); - if ($err_msg = $this->server_parse('334', __LINE__)) - { - return $err_msg; - } - - $this->server_send(' '); - if ($err_msg = $this->server_parse('235', __LINE__)) - { - return $err_msg; - } - - return false; - } -} - -/** - * Encodes the given string for proper display in UTF-8 or US-ASCII. - * - * This version is based on iconv_mime_encode() implementation - * from symfomy/polyfill-iconv - * https://github.com/symfony/polyfill-iconv/blob/fd324208ec59a39ebe776e6e9ec5540ad4f40aaa/Iconv.php#L355 - * - * @param string $str - * @param string $eol Lines delimiter (optional to be backwards compatible) - * - * @return string - */ -function mail_encode($str, $eol = "\r\n") -{ - // Check if string contains ASCII only characters - $is_ascii = strlen($str) === utf8_strlen($str); - - $scheme = $is_ascii ? 'Q' : 'B'; - - // Define start delimiter, end delimiter - // Use the Quoted-Printable encoding for ASCII strings to avoid unnecessary encoding in Base64 - $start = '=?' . ($is_ascii ? 'US-ASCII' : 'UTF-8') . '?' . $scheme . '?'; - $end = '?='; - - // Maximum encoded-word length is 75 as per RFC 2047 section 2. - // $split_length *must* be a multiple of 4, but <= 75 - strlen($start . $eol . $end)!!! - $split_length = 75 - strlen($start . $eol . $end); - $split_length = $split_length - $split_length % 4; - - $line_length = strlen($start) + strlen($end); - $line_offset = strlen($start) + 1; - $line_data = ''; - - $is_quoted_printable = 'Q' === $scheme; - - preg_match_all('/./us', $str, $chars); - $chars = $chars[0] ?? []; - - $str = []; - foreach ($chars as $char) - { - $encoded_char = $is_quoted_printable - ? $char = preg_replace_callback( - '/[()<>@,;:\\\\".\[\]=_?\x20\x00-\x1F\x80-\xFF]/', - function ($matches) - { - $hex = dechex(ord($matches[0])); - $hex = strlen($hex) == 1 ? "0$hex" : $hex; - return '=' . strtoupper($hex); - }, - $char - ) - : base64_encode($line_data . $char); - - if (isset($encoded_char[$split_length - $line_length])) - { - if (!$is_quoted_printable) - { - $line_data = base64_encode($line_data); - } - $str[] = $start . $line_data . $end; - $line_length = $line_offset; - $line_data = ''; - } - - $line_data .= $char; - $is_quoted_printable && $line_length += strlen($char); - } - - if ($line_data !== '') - { - if (!$is_quoted_printable) - { - $line_data = base64_encode($line_data); - } - $str[] = $start . $line_data . $end; - } - - return implode($eol . ' ', $str); -} - -/** - * Wrapper for sending out emails with the PHP's mail function - */ -function phpbb_mail($to, $subject, $msg, $headers, $eol, &$err_msg) -{ - global $config, $phpbb_root_path, $phpEx, $phpbb_dispatcher; - - // Convert Numeric Character References to UTF-8 chars (ie. Emojis) - $subject = utf8_decode_ncr($subject); - $msg = utf8_decode_ncr($msg); - - /** - * We use the EOL character for the OS here because the PHP mail function does not correctly transform line endings. - * On Windows SMTP is used (SMTP is \r\n), on UNIX a command is used... - * Reference: http://bugs.php.net/bug.php?id=15841 - */ - $headers = implode($eol, $headers); - - if (!class_exists('\phpbb\error_collector')) - { - include($phpbb_root_path . 'includes/error_collector.' . $phpEx); - } - - $collector = new \phpbb\error_collector; - $collector->install(); - - /** - * On some PHP Versions mail() *may* fail if there are newlines within the subject. - * Newlines are used as a delimiter for lines in mail_encode() according to RFC 2045 section 6.8. - * Because PHP can't decide what is wanted we revert back to the non-RFC-compliant way of separating by one space - * (Use '' as parameter to mail_encode() results in SPACE used) - */ - $additional_parameters = $config['email_force_sender'] ? '-f' . $config['board_email'] : ''; - - /** - * Modify data before sending out emails with PHP's mail function - * - * @event core.phpbb_mail_before - * @var string to The message recipient - * @var string subject The message subject - * @var string msg The message text - * @var string headers The email headers - * @var string eol The endline character - * @var string additional_parameters The additional parameters - * @since 3.3.6-RC1 - */ - $vars = [ - 'to', - 'subject', - 'msg', - 'headers', - 'eol', - 'additional_parameters', - ]; - extract($phpbb_dispatcher->trigger_event('core.phpbb_mail_before', compact($vars))); - - $result = mail($to, mail_encode($subject, ''), wordwrap(utf8_wordwrap($msg), 997, "\n", true), $headers, $additional_parameters); - - /** - * Execute code after sending out emails with PHP's mail function - * - * @event core.phpbb_mail_after - * @var string to The message recipient - * @var string subject The message subject - * @var string msg The message text - * @var string headers The email headers - * @var string eol The endline character - * @var string additional_parameters The additional parameters - * @var bool result True if the email was sent, false otherwise - * @since 3.3.6-RC1 - */ - $vars = [ - 'to', - 'subject', - 'msg', - 'headers', - 'eol', - 'additional_parameters', - 'result', - ]; - extract($phpbb_dispatcher->trigger_event('core.phpbb_mail_after', compact($vars))); - - $collector->uninstall(); - $err_msg = $collector->format_errors(); - - return $result; -} diff --git a/phpBB/phpbb/message/message.php b/phpBB/phpbb/message/message.php index d77f56c73f..554fe48a12 100644 --- a/phpBB/phpbb/message/message.php +++ b/phpBB/phpbb/message/message.php @@ -250,16 +250,16 @@ class message $messenger->to($recipient['address'], $recipient['name']); $messenger->im($recipient['jabber'], $recipient['username']); - $messenger->headers('X-AntiAbuse: Board servername - ' . $this->server_name); - $messenger->headers('X-AntiAbuse: User IP - ' . $this->sender_ip); + $messenger->headers('X-AntiAbuse', 'Board servername - ' . $this->server_name); + $messenger->headers('X-AntiAbuse', 'User IP - ' . $this->sender_ip); if ($this->sender_id) { - $messenger->headers('X-AntiAbuse: User_id - ' . $this->sender_id); + $messenger->headers('X-AntiAbuse', 'User_id - ' . $this->sender_id); } if ($this->sender_username) { - $messenger->headers('X-AntiAbuse: Username - ' . $this->sender_username); + $messenger->headers('X-AntiAbuse', 'Username - ' . $this->sender_username); } $messenger->subject(html_entity_decode($this->subject, ENT_COMPAT)); diff --git a/tests/email/email_parsing_test.php b/tests/email/email_parsing_test.php index a346508624..e287bea407 100644 --- a/tests/email/email_parsing_test.php +++ b/tests/email/email_parsing_test.php @@ -70,6 +70,7 @@ class phpbb_email_parsing_test extends phpbb_test_case $phpbb_container->set('assets.bag', $assets_bag); $context = new \phpbb\template\context(); + $dispatcher = new \phpbb\event\dispatcher(); $twig = new \phpbb\template\twig\environment( $assets_bag, $config, @@ -78,7 +79,7 @@ class phpbb_email_parsing_test extends phpbb_test_case $cache_path, null, new \phpbb\template\twig\loader(''), - new \phpbb\event\dispatcher(), + $dispatcher, array( 'cache' => false, 'debug' => false, @@ -95,6 +96,16 @@ class phpbb_email_parsing_test extends phpbb_test_case $twig->addExtension($twig_extension); $phpbb_container->set('template.twig.lexer', new \phpbb\template\twig\lexer($twig)); + $phpbb_container->set('dispatcher', $dispatcher); + $phpbb_container->set('language', $lang); + $phpbb_container->set('request', $request); + + $db = $this->getMockBuilder('\phpbb\db\driver\mysqli') + ->disableOriginalConstructor() + ->getMock(); + $auth = $this->createMock('\phpbb\auth\auth'); + $log = new \phpbb\log\log($db, $user, $auth, $dispatcher, $phpbb_root_path, 'adm/', $phpEx, LOG_TABLE); + $phpbb_container->set('log', $log); if (!class_exists('messenger')) { diff --git a/tests/email/headers_encoding_test.php b/tests/email/headers_encoding_test.php deleted file mode 100644 index a884a4cc21..0000000000 --- a/tests/email/headers_encoding_test.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @license GNU General Public License, version 2 (GPL-2.0) - * - * For full copyright and license information, please see - * the docs/CREDITS.txt file. - * - */ - -class phpbb_headers_encoding_test extends phpbb_test_case -{ - protected function setUp(): void - { - global $phpbb_root_path, $phpEx; - - if (!function_exists('mail_encode')) - { - include($phpbb_root_path . 'includes/functions_messenger.' . $phpEx); - } - } - - public function headers_encoding_data() - { - return [ - ['test@yourdomain.com ', 'Q', 'US-ASCII'], - ['test@yourdomain.com <Несуществующий почтовый адрес phpBB>', 'B', 'UTF-8'], - ["\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88", 'B', 'UTF-8'], - ]; - } - - /** - * @dataProvider headers_encoding_data - */ - public function test_headers_encoding($header, $scheme, $encoding) - { - $encoded_string = mail_encode($header); - $this->assertStringStartsWith("=?$encoding?$scheme?", $encoded_string); - $this->assertStringEndsWith('?=', $encoded_string); - - // Result of iconv_mime_decode() on decoded header should be equal to initial header - $decoded_string = iconv_mime_decode($encoded_string, 0, $encoding); - $this->assertEquals(0, strcmp($header, $decoded_string)); - } -}