diff --git a/phpBB/config/default/container/services.yml b/phpBB/config/default/container/services.yml
index 8a4336e832..c09f21b5c6 100644
--- a/phpBB/config/default/container/services.yml
+++ b/phpBB/config/default/container/services.yml
@@ -19,6 +19,7 @@ imports:
- { resource: services_http.yml }
- { resource: services_language.yml }
- { resource: services_mention.yml }
+ - { resource: services_messenger.yml }
- { resource: services_migrator.yml }
- { resource: services_mimetype_guesser.yml }
- { resource: services_module.yml }
diff --git a/phpBB/config/default/container/services_messenger.yml b/phpBB/config/default/container/services_messenger.yml
new file mode 100644
index 0000000000..659a4fa558
--- /dev/null
+++ b/phpBB/config/default/container/services_messenger.yml
@@ -0,0 +1,46 @@
+parameters:
+ core.messenger_queue_file: '%core.cache_dir%queue.%core.php_ext%'
+
+services:
+ messenger.method_collection:
+ class: phpbb\di\service_collection
+ arguments:
+ - '@service_container'
+ tags:
+ - { name: service_collection, tag: messenger.method, class_name_aware: true }
+
+ messenger.method_base:
+ class: phpbb\messenger\method\base
+ shared: false
+ arguments:
+ - '@config'
+ - '@dispatcher'
+ - '@language'
+ - '@log'
+ - '@request'
+ - '@user'
+ - '@messenger.queue'
+
+ messenger.method_email:
+ class: phpbb\messenger\method\email
+ shared: false
+ parent: messenger.method_base
+ calls:
+ - [init, []]
+ - [set_transport, []]
+
+ messenger.method_jabber:
+ class: phpbb\messenger\method\jabber
+ shared: false
+ parent: messenger.method_base
+ calls:
+ - [init, []]
+
+ messenger.queue:
+ class: phpbb\messenger\queue
+ shared: false
+ arguments:
+ - '@config'
+ - '@dispatcher'
+ - '@messenger.method_collection'
+ - '%core.messenger_queue_file%'
diff --git a/phpBB/phpbb/messenger/method/base.php b/phpBB/phpbb/messenger/method/base.php
new file mode 100644
index 0000000000..507b5beea8
--- /dev/null
+++ b/phpBB/phpbb/messenger/method/base.php
@@ -0,0 +1,408 @@
+
+ * @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 phpbb\config\config;
+use phpbb\event\dispatcher;
+use phpbb\language\language;
+use phpbb\log\log_interface;
+use phpbb\request\request;
+use phpbb\messenger\queue;
+use phpbb\template\template;
+use phpbb\user;
+
+/**
+ * Messenger base class
+ */
+class base
+{
+ /** @var array */
+ protected $additional_headers = [];
+
+ /** @var array */
+ protected $addresses = [];
+
+ /** @var config */
+ protected $config;
+
+ /** @var dispatcher */
+ protected $dispatcher;
+
+ /** @var language */
+ protected $language;
+
+ /** @var log_interface */
+ protected $log;
+
+ /** @var string */
+ protected $msg = '';
+
+ /** @var queue */
+ protected $queue;
+
+ /** @var request */
+ protected $request;
+
+ /** @var string */
+ protected $subject = '';
+
+ /** @var template */
+ protected $template;
+
+ /** @var bool */
+ protected $use_queue = true;
+
+ /** @var user */
+ protected $user;
+
+ /**
+ * Messenger base class constructor
+ *
+ * @param config $config
+ * @param dispatcher $dispatcher
+ * @param language $language
+ * @param log_interface $log
+ * @param request $request
+ * @param user $user
+ * @param queue $queue
+ */
+ function __construct(config $config, dispatcher $dispatcher, language $language, log_interface $log, request $request, user $user, queue $queue)
+ {
+ $this->config = $config;
+ $this->dispatcher = $dispatcher;
+ $this->language = $language;
+ $this->log = $log;
+ $this->request = $request;
+ $this->user = $user;
+ $this->queue = $queue;
+
+ $this->set_use_queue();
+ }
+
+ /**
+ * Sets the use of messenger queue flag
+ *
+ * @return void
+ */
+ public function set_use_queue($use_queue = true)
+ {
+ $this->use_queue = $use_queue;
+ }
+
+ /**
+ * Resets all the data (address, template file, etc etc) to default
+ *
+ * @return void
+ */
+ public function reset()
+ {
+ $this->addresses = [];
+ $this->msg = '';
+ }
+
+ /**
+ * Set addresses for to/im as available
+ *
+ * @param array $user User row
+ * @return void
+ */
+ public function set_addresses($user)
+ {
+ }
+
+ /**
+ * Set up subject for mail
+ *
+ * @param string $subject Email subject
+ * @return void
+ */
+ public function subject($subject = '')
+ {
+ $this->subject = $subject;
+ }
+
+ /**
+ * Set email template to use
+ *
+ * @param string $template_file Email template file name
+ * @param string $template_lang Email template language
+ * @param string $template_path Email template path
+ * @param string $template_dir_prefix Email template directory prefix
+ *
+ * @return bool
+ */
+ public function template($template_file, $template_lang = '', $template_path = '', $template_dir_prefix = '')
+ {
+ $template_dir_prefix = (!$template_dir_prefix || $template_dir_prefix[0] === '/') ? $template_dir_prefix : '/' . $template_dir_prefix;
+
+ $this->setup_template();
+
+ if (!trim($template_file))
+ {
+ trigger_error('No template file for emailing set.', E_USER_ERROR);
+ }
+
+ if (!trim($template_lang))
+ {
+ // 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($this->config['default_lang']);
+ }
+
+ $ext_template_paths = [
+ [
+ 'name' => $template_lang . '_email',
+ 'ext_path' => 'language/' . $template_lang . '/email' . $template_dir_prefix,
+ ],
+ ];
+
+ if ($template_path)
+ {
+ $template_paths = [
+ $template_path . $template_dir_prefix,
+ ];
+ }
+ else
+ {
+ $template_path = (!empty($this->user->lang_path)) ? $this->user->lang_path : $this->root_path . 'language/';
+ $template_path .= $template_lang . '/email';
+
+ $template_paths = [
+ $template_path . $template_dir_prefix,
+ ];
+
+ $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($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;
+
+ $ext_template_paths[] = [
+ 'name' => $board_language . '_email',
+ 'ext_path' => 'language/' . $board_language . '/email' . $template_dir_prefix,
+ ];
+ }
+ // If everything fails just fall back to en template
+ if ($template_lang !== 'en' && $board_language !== 'en')
+ {
+ $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;
+
+ $ext_template_paths[] = [
+ 'name' => 'en_email',
+ 'ext_path' => 'language/en/email' . $template_dir_prefix,
+ ];
+ }
+ }
+
+ $this->set_template_paths($ext_template_paths, $template_paths);
+
+ $this->template->set_filenames([
+ 'body' => $template_file . '.txt',
+ ]);
+
+ return true;
+ }
+
+ /**
+ * Assign variables to email template
+ *
+ * @param array $vars Array of VAR => VALUE to assign to email template
+ * @return void
+ */
+ public function assign_vars($vars)
+ {
+ $this->setup_template();
+
+ $this->template->assign_vars($vars);
+ }
+
+ /**
+ * Assign block of variables to email template
+ *
+ * @param string $blockname Template block name
+ * @param array $vars Array of VAR => VALUE to assign to email template block
+ * @return void
+ */
+ public function assign_block_vars($blockname, $vars)
+ {
+ $this->setup_template();
+
+ $this->template->assign_block_vars($blockname, $vars);
+ }
+
+ /**
+ * Prepare message before sending out to the recipients
+ *
+ * @return void
+ */
+ public function prepare_message()
+ {
+ // We add some standard variables we always use, no need to specify them always
+ $this->assign_vars([
+ 'U_BOARD' => generate_board_url(),
+ '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->email->getSubject();
+ $template = $this->template;
+ /**
+ * Event to modify the template before parsing
+ *
+ * @event core.modify_notification_template
+ * @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. Removed vars: method, break.
+ */
+ $vars = ['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 string message The message text
+ * @var string subject The message subject
+ * @since 3.1.11-RC1
+ * @changed 4.0.0-a1 Removed vars: method, break.
+ */
+ $vars = ['message', 'subject'];
+ extract($this->dispatcher->trigger_event('core.modify_notification_message', compact($vars)));
+
+ $this->subject = $subject;
+ $this->msg = $message;
+ unset($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);
+
+ // We now try and pull a subject from the email body ... if it exists,
+ // do this here because the subject may contain a variable
+ $drop_header = '';
+ $match = [];
+ if (preg_match('#^(Subject):(.*?)$#m', $this->msg, $match))
+ {
+ $this->subject = (trim($match[2]) != '') ? trim($match[2]) : (($this->subject != '') ? $this->subject : $this->language->lang('NO_EMAIL_SUBJECT'));
+ $drop_header .= '[\r\n]*?' . preg_quote($match[0], '#');
+ }
+ else
+ {
+ $this->subject = (($this->subject != '') ? $this->subject : $this->language->lang('NO_EMAIL_SUBJECT'));
+ }
+
+ if (preg_match('#^(List-Unsubscribe):(.*?)$#m', $this->msg, $match))
+ {
+ $drop_header .= '[\r\n]*?' . preg_quote($match[0], '#');
+ $this->additional_headers[$match[1]] = trim($match[2]);
+ }
+
+ if ($drop_header)
+ {
+ $this->msg = trim(preg_replace('#' . $drop_header . '#s', '', $this->msg));
+ }
+ }
+
+ /**
+ * Add error message to log
+ *
+ * @param string $type Error type: EMAIL / etc
+ * @param string $msg Error message text
+ * @return void
+ */
+ public function error($type, $msg)
+ {
+ // Session doesn't exist, create it
+ if (!isset($this->user->session_id) || $this->user->session_id === '')
+ {
+ $this->user->session_begin();
+ }
+
+ $calling_page = html_entity_decode($this->request->server('PHP_SELF'), ENT_COMPAT);
+ $message = '' . $type . '
' . htmlspecialchars($calling_page, ENT_COMPAT) . '
' . $msg . '
';
+ $this->log->add('critical', $this->user->data['user_id'], $this->user->ip, 'LOG_ERROR_' . $type, false, [$message]);
+ }
+
+ /**
+ * Save message data to the messemger file queue
+ * @return void
+ */
+ public function save_queue()
+ {
+ $this->queue->save();
+ return;
+ }
+
+ /**
+ * Setup template engine
+ *
+ * @return void
+ */
+ protected function setup_template()
+ {
+ if ($this->template instanceof \phpbb\template\template)
+ {
+ return;
+ }
+
+ $template_environment = new \phpbb\template\twig\environment(
+ $this->config,
+ $this->phpbb_container->get('filesystem'),
+ $this->phpbb_container->get('path_helper'),
+ $this->phpbb_container->getParameter('core.template.cache_path'),
+ $this->phpbb_container->get('ext.manager'),
+ new \phpbb\template\twig\loader(),
+ $this->dispatcher,
+ []
+ );
+ $template_environment->setLexer($this->phpbb_container->get('template.twig.lexer'));
+
+ $this->template = new \phpbb\template\twig\twig(
+ $this->phpbb_container->get('path_helper'),
+ $this->config,
+ new \phpbb\template\context(),
+ $template_environment,
+ $this->phpbb_container->getParameter('core.template.cache_path'),
+ $this->user,
+ $this->phpbb_container->get('template.twig.extensions.collection'),
+ $this->phpbb_container->get('ext.manager')
+ );
+ }
+
+ /**
+ * Set template paths to load
+ *
+ * @param string $path_name Email template path name
+ * @param string $paths Email template paths
+ * @return void
+ */
+ protected function set_template_paths($path_name, $paths)
+ {
+ $this->setup_template();
+
+ $this->template->set_custom_style($path_name, $paths);
+ }
+}
diff --git a/phpBB/phpbb/messenger/method/email.php b/phpBB/phpbb/messenger/method/email.php
new file mode 100644
index 0000000000..039591feb7
--- /dev/null
+++ b/phpBB/phpbb/messenger/method/email.php
@@ -0,0 +1,576 @@
+
+ * @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\Transport;
+use Symfony\Component\Mailer\Mailer;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Header\Headers;
+
+/**
+ * Messenger class
+ */
+class email extends base
+{
+ /** @var 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',
+ ];
+
+ /** @var array */
+ 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 string
+ *
+ * Symfony Mailer transport DSN
+ */
+ protected $dsn = '';
+
+ /** @var string */
+ protected $from;
+
+ /** @var Symfony\Component\Mime\Header\Headers */
+ protected $headers;
+
+ /**
+ * @var int
+ *
+ * Possible values are:
+ * Email::PRIORITY_HIGHEST
+ * Email::PRIORITY_HIGH
+ * Email::PRIORITY_NORMAL
+ * Email::PRIORITY_LOW
+ * Email::PRIORITY_LOWEST
+ */
+ protected $mail_priority = Email::PRIORITY_NORMAL;
+
+ /** @var queue */
+ protected $queue;
+
+ /** @var string */
+ protected $replyto = '';
+
+ /** @var Symfony\Component\Mailer\Transport */
+ protected $transport;
+
+ /**
+ * Check if the messenger method is enabled
+ * @return void
+ */
+ public function is_enabled()
+ {
+ return (bool) $this->config['email_enable'];
+ }
+
+ /**
+ * Inits/resets the data to default
+ *
+ * @return void
+ */
+ public function init()
+ {
+ $this->email = new Email();
+ $this->headers = $this->email->getHeaders();
+ $this->msg = $this->replyto = $this->from = '';
+ $this->mail_priority = Email::PRIORITY_NORMAL;
+ }
+
+ /**
+ * Sets the use of messenger queue flag
+ *
+ * @return void
+ */
+ public function set_use_queue($use_queue = true)
+ {
+ $this->use_queue = !$this->config['email_package_size'] ? false : $use_queue;
+ }
+
+ /**
+ * Set address as available
+ *
+ * @param array $user User row
+ * @return void
+ */
+ public function set_addresses($user)
+ {
+ if (isset($user['user_email']) && $user['user_email'])
+ {
+ $this->to($user['user_email'], $user['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($address, $realname = '')
+ {
+ if (!$address = trim($address))
+ {
+ return;
+ }
+
+ // If empty sendmail_path on windows, PHP changes the to line
+ $windows_empty_sendmail_path = !$this->config['smtp_delivery'] && DIRECTORY_SEPARATOR == '\\';
+
+ $to = new Address($address, $windows_empty_sendmail_path ? '' : 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($address, $realname = '')
+ {
+ if (!$address = trim($address))
+ {
+ return;
+ }
+
+ $cc = new Address($address, trim($realname));
+ $this->email->getCc() ? $this->email->addCc($to) : $this->email->cc($to);
+ }
+
+ /**
+ * 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($address, $realname = '')
+ {
+ if (!$address = trim($address))
+ {
+ return;
+ }
+
+ $bcc = new Address($address, trim($realname));
+ $this->email->getBcc() ? $this->email->addBcc($to) : $this->email->bcc($to);
+ }
+
+ /**
+ * Set the reply to address
+ *
+ * @param string $address Email "Reply to" address
+ * @return void
+ */
+ public function replyto($address)
+ {
+ $this->replyto = new Address(trim($address));
+ $this->email->getReplyTo() ? $this->email->addReplyTo($this->replyto) : $this->email->replyTo($this->replyto);
+ }
+
+ /**
+ * Set the from address
+ *
+ * @param string $address Email "from" address
+ * @return void
+ */
+ public function from($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
+ *
+ * @param string $subject Email subject
+ * @return void
+ */
+ public function subject($subject = '')
+ {
+ parent::subject(trim($subject));
+ $this->email->subject($this->subject);
+ }
+
+ /**
+ * Set up extra mail headers
+ *
+ * @param string $header_name Email header name
+ * @param string $header_value Email header body
+ * @return void
+ */
+ public function header($header_name, $header_value)
+ {
+ $header_name = trim($header_name);
+ $header_value = trim($header_value);
+
+ // addMailboxListHeader() requires value to be array
+ if ($this->get_header_method($header_name) == 'addMailboxListHeader')
+ {
+ $header_value = [$header_value];
+ }
+ $this->headers->addHeader($header_name, $header_value);
+ }
+
+ /**
+ * Adds X-AntiAbuse headers
+ *
+ * @param \phpbb\config\config $config Config object
+ * @param \phpbb\user $user User object
+ * @return void
+ */
+ public function anti_abuse_headers($config, $user)
+ {
+ $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:
+ * Email::PRIORITY_HIGHEST = 1
+ * Email::PRIORITY_HIGH = 2
+ * Email::PRIORITY_NORMAL = 3
+ * Email::PRIORITY_LOW = 4
+ * Email::PRIORITY_LOWEST = 5
+ *
+ * @param int $priority Email priority level
+ * @return void
+ */
+ public function set_mail_priority($priority = Email::PRIORITY_NORMAL)
+ {
+ $this->email->priority($priority);
+ }
+
+ /**
+ * Add error message to log
+ *
+ * @param string $msg Error message text
+ * @return void
+ */
+ public function error($msg)
+ {
+ $type = 'EMAIL/' . ($this->config['smtp_delivery']) ? 'SMTP' : 'PHP/mail()';
+ parent::error($type, $msg);
+ }
+
+ /**
+ * Save message data to the messenger file queue
+ * @return void
+ */
+ public function save_queue()
+ {
+ if ($this->config['email_package_size'] && $this->use_queue && !empty($this->queue))
+ {
+ $this->queue->save();
+ return;
+ }
+ }
+
+ /**
+ * Detect proper Header class method to add header
+ *
+ * @param string $name Email header name
+ * @return string
+ */
+ protected function get_header_method(string $name)
+ {
+ $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';
+ }
+
+ return $method;
+ }
+
+ /**
+ * Set email headers
+ *
+ * @return bool
+ */
+ protected function build_header()
+ {
+ $headers = [];
+
+ $board_contact = $this->config['board_contact'];
+ if (empty($this->email->getReplyTo()))
+ {
+ $this->replyto($board_contact);
+ $headers['Reply-To'] = $this->replyto;
+ }
+
+ if (empty($this->email->getFrom()))
+ {
+ $this->from($board_contact);
+ $headers['From'] = $this->from;
+ }
+
+ $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
+ */
+ $vars = ['headers'];
+ extract($this->dispatcher->trigger_event('core.modify_email_headers', compact($vars)));
+
+ foreach ($headers as $header => $value)
+ {
+ $this->header($header, $value);
+ }
+
+ return true;
+ }
+
+ /**
+ * Generates valid DSN for Symfony Mailer transport
+ *
+ * @param string $dsn Symfony Mailer transport DSN
+ * @return void
+ */
+ public function set_dsn($dsn = '')
+ {
+ 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 void
+ */
+ public function get_dsn()
+ {
+ return $this->dsn;
+ }
+
+ /**
+ * Generates a valid transport to send email
+ *
+ * @return void
+ */
+ public function set_transport()
+ {
+ if (empty($this->dsn))
+ {
+ $this->set_dsn();
+ }
+
+ $this->transport = Transport::fromDsn($this->dsn);
+
+ if ($this->config['smtp_delivery'] && !in_array($this->dsn, ['null://null', 'sendmail://default']))
+ {
+ // 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);
+ }
+ }
+
+ /**
+ * Get mailer transport object
+ *
+ * @return Symfony\Component\Mailer\Transport Symfony Mailer transport object
+ */
+ public function get_transport()
+ {
+ return $this->transport;
+ }
+
+ /**
+ * Send out emails
+ *
+ * @return bool
+ */
+ protected function send()
+ {
+ $this->prepare_message();
+
+ $contact_name = html_entity_decode($this->config['board_contact_name'], ENT_COMPAT);
+ $board_contact = trim($this->config['board_contact']);
+
+ $this->email->subject($this->subject);
+ $this->email->text($this->msg);
+
+ $subject = $this->subject;
+ $msg = $this->msg;
+ $email = $this->email;
+ /**
+ * Event to send message via external transport
+ *
+ * @event core.notification_message_email
+ * @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, break
+ */
+ $vars = [
+ 'subject',
+ 'msg',
+ 'email',
+ ];
+ extract($this->dispatcher->trigger_event('core.notification_message_email', compact($vars)));
+
+ if (empty($this->email->getReplyto()))
+ {
+ $this->replyto($board_contact);
+ }
+
+ if (empty($this->email->getFrom()))
+ {
+ $this->from($board_contact);
+ }
+
+ // Build headers
+ foreach ($this->additional_headers as $header_name => $header_value)
+ {
+ $this->header($header_name, $header_value);
+ }
+ $this->build_header();
+
+ // Send message ...
+ if (!$this->use_queue)
+ {
+ $mailer = new Mailer($this->transport);
+
+ $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)));
+
+ $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 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->init('email', $this->config['email_package_size']);
+ $this->queue->put('email', [
+ 'email' => $this->email,
+ ]);
+ }
+
+ return true;
+ }
+}
diff --git a/phpBB/includes/functions_jabber.php b/phpBB/phpbb/messenger/method/jabber.php
similarity index 79%
rename from phpBB/includes/functions_jabber.php
rename to phpBB/phpbb/messenger/method/jabber.php
index 282e6105a2..2e7f2040da 100644
--- a/phpBB/includes/functions_jabber.php
+++ b/phpBB/phpbb/messenger/method/jabber.php
@@ -1,35 +1,30 @@
-* @license GNU General Public License, version 2 (GPL-2.0)
-*
-* For full copyright and license information, please see
-* the docs/CREDITS.txt file.
-*
-*/
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited
+ * @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;
/**
-* @ignore
-*/
-if (!defined('IN_PHPBB'))
-{
- exit;
-}
-
-/**
-*
-* Jabber class from Flyspray project
-*
-* @version class.jabber2.php 1595 2008-09-19 (0.9.9)
-* @copyright 2006 Flyspray.org
-* @author Florian Schmitz (floele)
-*
-* Only slightly modified by Acyd Burn
-*/
-class jabber
+ *
+ * Based on Jabber class from Flyspray project
+ *
+ * @version class.jabber2.php 1595 2008-09-19 (0.9.9)
+ * @copyright 2006 Flyspray.org
+ * @author Florian Schmitz (floele)
+ *
+ * Slightly modified by Acyd Burn (2006)
+ * Refactored to a service (2023)
+ */
+class jabber extends base
{
/** @var string */
protected $connect_server;
@@ -70,6 +65,9 @@ class jabber
/** @var int */
protected $timeout = 10;
+ /** @var array */
+ protected $to = [];
+
/** @var bool */
protected $use_ssl = false;
@@ -80,25 +78,49 @@ class jabber
private const STREAM_CLOSE_HANDSHAKE = '';
/**
- * Jabber class constructor
+ * Set initial parameter values
+ * To init correctly, username() call should go before server()
+ * and ssl() call should go before port() and stream_options() calls.
*
- * Use (username() call should go before server()
- * and ssl() call should go before port() and stream_options()):
- *
- * new jabber()
- * -> username($username)
- * -> password($password)
- * -> ssl($use_ssl)
- * -> server($server)
- * -> port($port)
- * -> stream_options(
+ * Example:
+ * $this->username($username)
+ * ->password($password)
+ * ->ssl($use_ssl)
+ * ->server($server)
+ * ->port($port)
+ * ->stream_options(
* 'verify_peer' => true,
* 'verify_peer_name' => true,
* 'allow_self_signed' => false,
* );
+ *
+ * @return void
*/
- function __construct()
+ public function init()
{
+ $this->username($this->config['jab_username'])
+ ->password($this->config['jab_password'])
+ ->ssl((bool) $this->config['jab_use_ssl'])
+ ->server($this->config['jab_host'])
+ ->port($this->config['jab_port'])
+ ->stream_options['ssl'] = [
+ 'verify_peer' => $this->config['jab_verify_peer'],
+ 'verify_peer_name' => $this->config['jab_verify_peer_name'],
+ 'allow_self_signed' => $this->config['jab_allow_self_signed'],
+ ];
+ }
+
+ /**
+ * Check if the messenger method is enabled
+ * @return void
+ */
+ public function is_enabled()
+ {
+ return
+ empty($this->config['jab_enable']) ||
+ empty($this->config['jab_host']) ||
+ empty($this->config['jab_username']) ||
+ empty($this->config['jab_password']);
}
/**
@@ -112,12 +134,8 @@ class jabber
{
if ($this->use_ssl)
{
- // Set default stream options and change it if needed
- $this->stream_options['ssl'] = array_merge([
- 'verify_peer' => true,
- 'verify_peer_name' => true,
- 'allow_self_signed' => false,
- ], $options);
+ // Change default stream options if needed
+ $this->stream_options['ssl'] = array_merge($this->stream_options['ssl'], $options);
}
return $this;
@@ -131,7 +149,7 @@ class jabber
*/
public function password($password = '')
{
- $this->password = $password;
+ $this->password = html_entity_decode($password, ENT_COMPAT);
return $this;
}
@@ -263,8 +281,8 @@ class jabber
if ($this->open_socket($this->connect_server, $this->port))
{
- $this->send("\n");
- $this->send("\n");
+ $this->send_xml("\n");
+ $this->send_xml("\n");
}
else
{
@@ -331,6 +349,105 @@ class jabber
return $this->response($this->features);
}
+ /**
+ * Set address as available
+ *
+ * @param array $user User row
+ * @return void
+ */
+ public function set_addresses($user)
+ {
+ if (isset($user['user_jabber']) && $user['user_jabber'])
+ {
+ $this->to($user['user_jabber'], (isset($user['username']) ? $user['username'] : ''));
+ }
+ }
+
+ /**
+ * Sets jabber contact to send message to
+ *
+ * @param string $address Jabber "To" recipient address
+ * @param string $realname Jabber "To" recipient name
+ * @return void
+ */
+ public function to($address, $realname = '')
+ {
+ // IM-Addresses could be empty
+ if (!trim($address))
+ {
+ return;
+ }
+
+ $pos = !empty($this->to) ? count($this->to) : 0;
+ $this->to[$pos]['uid'] = trim($address);
+ $this->to[$pos]['name'] = trim($realname);
+ }
+
+ /**
+ * Sets the use of messenger queue flag
+ *
+ * @return void
+ */
+ public function set_use_queue($use_queue = true)
+ {
+ $this->use_queue = !$this->config['jab_package_size'] ? false : $use_queue;
+ }
+
+ /**
+ * Send jabber message out
+ */
+ public function send()
+ {
+ $this->prepare_message();
+
+ if (empty($this->to))
+ {
+ $this->add_to_log('Error: Could not send, recepient addresses undefined.');
+ return false;
+ }
+
+ $addresses = [];
+ foreach ($this->to as $uid_ary)
+ {
+ $addresses[] = $uid_ary['uid'];
+ }
+ $addresses = array_unique($addresses);
+
+ if (!$this->use_queue)
+ {
+ if (!$this->connect())
+ {
+ $this->error('JABBER', $this->user->lang['ERR_JAB_CONNECT'] . '
' . $this->get_log());
+ return false;
+ }
+
+ if (!$this->login())
+ {
+ $this->error('JABBER', $this->user->lang['ERR_JAB_AUTH'] . '
' . $this->get_log());
+ return false;
+ }
+
+ foreach ($addresses as $address)
+ {
+ $this->send_message($address, $this->msg, $this->subject);
+ }
+
+ $this->disconnect();
+ }
+ else
+ {
+ $this->queue->init('jabber', $this->config['jab_package_size']);
+ $this->queue->put('jabber', array(
+ 'addresses' => $addresses,
+ 'subject' => $this->subject,
+ 'msg' => $this->msg)
+ );
+ }
+ unset($addresses);
+
+ return true;
+ }
+
/**
* Send data to the Jabber server
*
@@ -338,7 +455,7 @@ class jabber
*
* @return bool
*/
- public function send($xml)
+ public function send_xml($xml)
{
if ($this->connected())
{
@@ -459,7 +576,7 @@ class jabber
return false;
}
- $this->send("");
+ $this->send_xml("");
return $this->response($this->listen());
}
@@ -488,7 +605,7 @@ class jabber
$this->session['sent_presence'] = !$unavailable;
- return $this->send("" . $type . $message . '');
+ return $this->send_xml("" . $type . $message . '');
}
/**
@@ -567,7 +684,7 @@ class jabber
// session required?
$this->session['sess_required'] = isset($xml['stream:features'][0]['#']['session']);
- $this->send("
+ $this->send_xml("
" . utf8_htmlspecialchars($this->resource) . '
@@ -579,7 +696,7 @@ class jabber
if (!$this->session['ssl'] && self::can_use_tls() && self::can_use_ssl() && isset($xml['stream:features'][0]['#']['starttls']))
{
$this->add_to_log('Switching to TLS.');
- $this->send("\n");
+ $this->send_xml("\n");
return $this->response($this->listen());
}
@@ -605,18 +722,18 @@ class jabber
if (in_array('DIGEST-MD5', $methods))
{
- $this->send("");
+ $this->send_xml("");
}
else if (in_array('PLAIN', $methods) && ($this->session['ssl'] || !empty($this->session['tls'])))
{
// http://www.ietf.org/rfc/rfc4616.txt (PLAIN SASL Mechanism)
- $this->send(""
+ $this->send_xml(""
. base64_encode($this->username . '@' . $this->server . chr(0) . $this->username . chr(0) . $this->password) .
'');
}
else if (in_array('ANONYMOUS', $methods))
{
- $this->send("");
+ $this->send_xml("");
}
else
{
@@ -653,7 +770,7 @@ class jabber
// second challenge?
if (isset($decoded['rspauth']))
{
- $this->send("");
+ $this->send_xml("");
}
else
{
@@ -680,7 +797,7 @@ class jabber
}
}
- $this->send("" . base64_encode($this->implode_data($response)) . '');
+ $this->send_xml("" . base64_encode($this->implode_data($response)) . '');
}
return $this->response($this->listen());
@@ -707,15 +824,15 @@ class jabber
$this->session['tls'] = true;
// new stream
- $this->send("\n");
- $this->send("\n");
+ $this->send_xml("\n");
+ $this->send_xml("\n");
return $this->response($this->listen());
break;
case 'success':
// Yay, authentication successful.
- $this->send("\n");
+ $this->send_xml("\n");
$this->session['authenticated'] = true;
// we have to wait for another response
@@ -738,7 +855,7 @@ class jabber
// and (maybe) yet another request to be able to send messages *finally*
if ($this->session['sess_required'])
{
- $this->send("
+ $this->send_xml("
");
return $this->response($this->listen());
@@ -752,7 +869,7 @@ class jabber
break;
case 'reg_1':
- $this->send("
+ $this->send_xml("
" . utf8_htmlspecialchars($this->username) . "
" . utf8_htmlspecialchars($this->password) . "
@@ -829,7 +946,7 @@ class jabber
$type = 'normal';
}
- return $this->send("
+ return $this->send_xml("
" . utf8_htmlspecialchars($subject) . "
" . utf8_htmlspecialchars($text) . "
"