Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
22 / 22
CRAP
100.00% covered (success)
100.00%
1 / 1
email
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
22 / 22
54
100.00% covered (success)
100.00%
1 / 1
 get_queue_object_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 set_use_queue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 set_addresses
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 to
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 cc
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 bcc
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 reply_to
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 from
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 subject
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 anti_abuse_headers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 set_mail_priority
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build_headers
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 set_dsn
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 get_dsn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_transport
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 process_queue
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
9
 get_mailer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_transport
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 send
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
1 / 1
4
 header
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\messenger\method;
15
16use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
17use Symfony\Component\Mailer\MailerInterface;
18use Symfony\Component\Mailer\Transport;
19use Symfony\Component\Mailer\Mailer;
20use Symfony\Component\Mailer\Transport\AbstractTransport;
21use Symfony\Component\Mailer\Transport\TransportInterface;
22use Symfony\Component\Mime\Address;
23use Symfony\Component\Mime\Email as symfony_email;
24use Symfony\Component\Mime\Header\Headers;
25
26/**
27 * Messenger class
28 */
29class email extends base
30{
31    /** @var array */
32    private const PRIORITY_MAP = [
33        symfony_email::PRIORITY_HIGHEST => 'Highest',
34        symfony_email::PRIORITY_HIGH => 'High',
35        symfony_email::PRIORITY_NORMAL => 'Normal',
36        symfony_email::PRIORITY_LOW => 'Low',
37        symfony_email::PRIORITY_LOWEST => 'Lowest',
38    ];
39
40    /**
41     * @var string
42     *
43     * Symfony Mailer transport DSN
44     */
45    protected string $dsn = '';
46
47    /** @var symfony_email */
48    protected symfony_email $email;
49
50    /** @var Address From address */
51    protected Address $from;
52
53    /** @var Headers Email headers */
54    protected Headers $headers;
55
56    /**
57     * @var int
58     *
59     * Possible values are:
60     * symfony_email::PRIORITY_HIGHEST
61     * symfony_email::PRIORITY_HIGH
62     * symfony_email::PRIORITY_NORMAL
63     * symfony_email::PRIORITY_LOW
64     * symfony_email::PRIORITY_LOWEST
65     */
66    protected int $mail_priority = symfony_email::PRIORITY_NORMAL;
67
68    /** @var \phpbb\messenger\queue */
69    protected $queue;
70
71    /** @var Address */
72    protected Address $reply_to;
73
74    /** @var AbstractTransport */
75    protected AbstractTransport $transport;
76
77    /**
78     * {@inheritDoc}
79     */
80    public function get_queue_object_name(): string
81    {
82        return 'email';
83    }
84
85    /**
86     * {@inheritDoc}
87     */
88    public function is_enabled(): bool
89    {
90        return (bool) $this->config['email_enable'];
91    }
92
93    /**
94     * {@inheritDoc}
95     */
96    public function init(): void
97    {
98        $this->email = new symfony_email();
99        $this->headers = $this->email->getHeaders();
100        $this->subject =  $this->msg = '';
101        $this->mail_priority = symfony_email::PRIORITY_NORMAL;
102
103        $this->additional_headers = [];
104        $this->set_use_queue();
105        unset($this->template, $this->reply_to, $this->from);
106    }
107
108    /**
109     * {@inheritdoc}
110     */
111    public function set_use_queue(bool $use_queue = true): void
112    {
113        $this->use_queue = !$this->config['email_package_size'] ? false : $use_queue;
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    public function set_addresses(array $user_row): void
120    {
121        if (!empty($user_row['user_email']))
122        {
123            $this->to($user_row['user_email'], $user_row['username'] ?? '');
124        }
125    }
126
127    /**
128     * Sets email address to send to
129     *
130     * @param string    $address    Email "To" recipient address
131     * @param string    $realname    Email "To" recipient name
132     * @return void
133     */
134    public function to(string $address, string $realname = ''): void
135    {
136        if (!$address = trim($address))
137        {
138            return;
139        }
140
141        $to = new Address($address, trim($realname));
142        $this->email->getTo() ? $this->email->addTo($to) : $this->email->to($to);
143    }
144
145    /**
146     * Sets cc address to send to
147     *
148     * @param string    $address    Email carbon copy recipient address
149     * @param string    $realname    Email carbon copy recipient name
150     * @return void
151     */
152    public function cc(string $address, string $realname = ''): void
153    {
154        if (!$address = trim($address))
155        {
156            return;
157        }
158
159        $cc = new Address($address, trim($realname));
160        $this->email->getCc() ? $this->email->addCc($cc) : $this->email->cc($cc);
161    }
162
163    /**
164     * Sets bcc address to send to
165     *
166     * @param string    $address    Email black carbon copy recipient address
167     * @param string    $realname    Email black carbon copy recipient name
168     * @return void
169     */
170    public function bcc(string $address, string $realname = ''): void
171    {
172        if (!$address = trim($address))
173        {
174            return;
175        }
176
177        $bcc = new Address($address, trim($realname));
178        $this->email->getBcc() ? $this->email->addBcc($bcc) : $this->email->bcc($bcc);
179    }
180
181    /**
182     * Set the reply to address
183     *
184     * @param string    $address    Email "Reply to" address
185     * @param string    $realname    Email "Reply to" recipient name
186     * @return void
187     */
188    public function reply_to(string $address, string $realname = ''): void
189    {
190        if (!$address = trim($address))
191        {
192            return;
193        }
194
195        $this->reply_to = new Address($address, trim($realname));
196        $this->email->getReplyTo() ? $this->email->addReplyTo($this->reply_to) : $this->email->replyTo($this->reply_to);
197    }
198
199    /**
200     * Set the from address
201     *
202     * @param string    $address    Email "from" address
203     * @param string    $realname    Email "from" recipient name
204     * @return void
205     */
206    public function from(string $address, string $realname = ''): void
207    {
208        if (!$address = trim($address))
209        {
210            return;
211        }
212
213        $this->from = new Address($address, trim($realname));
214        $this->email->getFrom() ? $this->email->addFrom($this->from) : $this->email->from($this->from);
215    }
216
217    /**
218     * Set up subject for mail
219     *
220     * @param string    $subject    Email subject
221     * @return void
222     */
223    public function subject(string $subject = ''): void
224    {
225        parent::subject(trim($subject));
226        $this->email->subject($this->subject);
227    }
228
229    /**
230     * Adds X-AntiAbuse headers
231     *
232     * @param \phpbb\config\config    $config        Config object
233     * @param \phpbb\user            $user        User object
234     * @return void
235     */
236    public function anti_abuse_headers(\phpbb\config\config $config, \phpbb\user $user): void
237    {
238        $this->header('X-AntiAbuse', 'Board servername - ' . $config['server_name']);
239        $this->header('X-AntiAbuse', 'User_id - ' . $user->data['user_id']);
240        $this->header('X-AntiAbuse', 'Username - ' . $user->data['username']);
241        $this->header('X-AntiAbuse', 'User IP - ' . $user->ip);
242    }
243
244    /**
245     * Set the email priority
246     *
247     * Possible values are:
248     * symfony_email::PRIORITY_HIGHEST = 1
249     * symfony_email::PRIORITY_HIGH = 2
250     * symfony_email::PRIORITY_NORMAL = 3
251     * symfony_email::PRIORITY_LOW = 4
252     * symfony_email::PRIORITY_LOWEST = 5
253     *
254     * @param int    $priority    Email priority level
255     * @return void
256     */
257    public function set_mail_priority(int $priority = symfony_email::PRIORITY_NORMAL): void
258    {
259        $this->email->priority($priority);
260    }
261
262    /**
263     * Set email headers
264     *
265     * @return void
266     */
267    protected function build_headers(): void
268    {
269        $board_contact = trim($this->config['board_contact']);
270        $contact_name = html_entity_decode($this->config['board_contact_name'], ENT_COMPAT);
271
272        if (empty($this->email->getReplyTo()))
273        {
274            $this->reply_to($board_contact, $contact_name);
275        }
276
277        if (empty($this->email->getFrom()))
278        {
279            $this->from($board_contact, $contact_name);
280        }
281
282        $this->email->priority($this->mail_priority);
283
284        $headers = [
285            'Return-Path'        => new Address($this->config['board_email']),
286            'Sender'            => new Address($this->config['board_email']),
287            'X-MSMail-Priority'    => self::PRIORITY_MAP[$this->mail_priority],
288            'X-Mailer'            => 'phpBB',
289            'X-MimeOLE'            => 'phpBB',
290            'X-phpBB-Origin'    => 'phpbb://' . str_replace(['http://', 'https://'], ['', ''], generate_board_url()),
291        ];
292
293        // Add additional headers
294        $headers = array_merge($headers, $this->additional_headers);
295
296        /**
297         * Event to modify email header entries
298         *
299         * @event core.modify_email_headers
300         * @var    array    headers    Array containing email header entries
301         * @since 3.1.11-RC1
302         */
303        $vars = ['headers'];
304        extract($this->dispatcher->trigger_event('core.modify_email_headers', compact($vars)));
305
306        foreach ($headers as $header => $value)
307        {
308            $this->header($header, $value);
309        }
310    }
311
312    /**
313     * Generates valid DSN for Symfony Mailer transport
314     *
315     * @param string $dsn Symfony Mailer transport DSN
316     * @return void
317     */
318    public function set_dsn(string $dsn = ''): void
319    {
320        if (!empty($dsn))
321        {
322            $this->dsn = $dsn;
323        }
324        else if ($this->config['smtp_delivery'])
325        {
326            if (empty($this->config['smtp_host']))
327            {
328                $this->dsn = 'null://null';
329            }
330            else
331            {
332                $user = urlencode($this->config['smtp_username']);
333                $password = urlencode($this->config['smtp_password']);
334                $smtp_host = urlencode($this->config['smtp_host']);
335                $smtp_port = $this->config['smtp_port'];
336
337                $this->dsn = "smtp://$user:$password@$smtp_host:$smtp_port";
338            }
339        }
340        else
341        {
342            $this->dsn = 'sendmail://default';
343        }
344    }
345
346    /**
347     * Get Symfony Mailer transport DSN
348     *
349     * @return string
350     */
351    public function get_dsn(): string
352    {
353        return $this->dsn;
354    }
355
356    /**
357     * Generates a valid transport to send email
358     *
359     * @return void
360     */
361    public function set_transport(): void
362    {
363        if (empty($this->dsn))
364        {
365            $this->set_dsn();
366        }
367
368        $this->transport = Transport::fromDsn($this->dsn);
369
370        if ($this->config['smtp_delivery'] && method_exists($this->transport, 'getStream'))
371        {
372            // Set ssl context options, see http://php.net/manual/en/context.ssl.php
373            $options['ssl'] = [
374                'verify_peer' => (bool) $this->config['smtp_verify_peer'],
375                'verify_peer_name' => (bool) $this->config['smtp_verify_peer_name'],
376                'allow_self_signed' => (bool) $this->config['smtp_allow_self_signed'],
377            ];
378            $this->transport->getStream()->setStreamOptions($options);
379        }
380    }
381
382    /**
383     * {@inheritDoc}
384     */
385    public function process_queue(array &$queue_data): void
386    {
387        $queue_object_name = $this->get_queue_object_name();
388        $messages_count = count($queue_data[$queue_object_name]['data']);
389
390        if (!$this->is_enabled() || !$messages_count)
391        {
392            unset($queue_data[$queue_object_name]);
393            return;
394        }
395
396        @set_time_limit(0);
397
398        $package_size = $queue_data[$queue_object_name]['package_size'] ?? 0;
399        $num_items = (!$package_size || $messages_count < $package_size) ? $messages_count : $package_size;
400        $mailer = $this->get_mailer();
401
402        for ($i = 0; $i < $num_items; $i++)
403        {
404            // Make variables available...
405            extract(array_shift($queue_data[$queue_object_name]['data']));
406
407            $break = false;
408            /**
409             * Event to send message via external transport
410             *
411             * @event core.notification_message_process
412             * @var    string        break    Flag indicating if the function return after hook
413             * @var    string        email    The Symfony Email object
414             * @since 3.2.4-RC1
415             * @changed 4.0.0-a1 Added vars: email. Removed vars: addresses, subject, msg.
416             */
417            $vars = [
418                'break',
419                'email',
420            ];
421            extract($this->dispatcher->trigger_event('core.notification_message_process', compact($vars)));
422
423            if (!$break)
424            {
425                try
426                {
427                    $mailer->send($email);
428                }
429                catch (TransportExceptionInterface $e)
430                {
431                    $this->error($e->getDebug());
432                    continue;
433                }
434            }
435        }
436
437        // No more data for this object? Unset it
438        if (!count($queue_data[$queue_object_name]['data']))
439        {
440            unset($queue_data[$queue_object_name]);
441        }
442    }
443
444    /**
445     * Get mailer object
446     *
447     * @return MailerInterface Symfony Mailer object
448     */
449    protected function get_mailer(): MailerInterface
450    {
451        return new Mailer($this->transport);
452    }
453
454    /**
455     * Get mailer transport object
456     *
457     * @return TransportInterface Symfony Mailer transport object
458     */
459    public function get_transport(): TransportInterface
460    {
461        return $this->transport;
462    }
463
464    /**
465     * {@inheritDoc}
466     */
467    public function send(): bool
468    {
469        $this->prepare_message();
470
471        $this->email->subject($this->subject);
472        $this->email->text($this->msg);
473
474        $break = false;
475        $subject = $this->subject;
476        $msg = $this->msg;
477        $email = $this->email;
478        /**
479         * Event to send message via external transport
480         *
481         * @event core.notification_message_email
482         * @var    bool    break    Flag indicating if the function return after hook
483         * @var    string    subject    The message subject
484         * @var    string    msg        The message text
485         * @var    string    email    The Symfony Email object
486         * @since 3.2.4-RC1
487         * @changed 4.0.0-a1 Added vars: email. Removed vars: addresses
488         */
489        $vars = [
490            'break',
491            'subject',
492            'msg',
493            'email',
494        ];
495        extract($this->dispatcher->trigger_event('core.notification_message_email', compact($vars)));
496        $this->email = $email;
497
498        $this->build_headers();
499
500        if ($break)
501        {
502            return true;
503        }
504
505        // Send message ...
506        if (!$this->use_queue)
507        {
508            $mailer = $this->get_mailer();
509
510            $subject = $this->subject;
511            $msg = $this->msg;
512            $headers = $this->headers;
513            $email = $this->email;
514            /**
515             * Modify data before sending out emails with PHP's mail function
516             *
517             * @event core.phpbb_mail_before
518             * @var    string    email        The Symfony Email object
519             * @var    string    subject        The message subject
520             * @var    string    msg            The message text
521             * @var string    headers        The email headers
522             * @since 3.3.6-RC1
523             * @changed 4.0.0-a1 Added vars: email. Removed vars: to, eol, additional_parameters.
524             */
525            $vars = [
526                'email',
527                'subject',
528                'msg',
529                'headers',
530            ];
531            extract($this->dispatcher->trigger_event('core.phpbb_mail_before', compact($vars)));
532
533            $this->subject = $subject;
534            $this->msg = $msg;
535            $this->headers = $headers;
536            $this->email = $email;
537
538            try
539            {
540                $mailer->send($this->email);
541            }
542            catch (TransportExceptionInterface $e)
543            {
544                $this->error($e->getDebug());
545                return false;
546            }
547
548            /**
549             * Execute code after sending out emails with PHP's mail function
550             *
551             * @event core.phpbb_mail_after
552             * @var    string    email        The Symfony Email object
553             * @var    string    subject        The message subject
554             * @var    string    msg            The message text
555             * @var string    headers        The email headers
556             * @since 3.3.6-RC1
557             * @changed 4.0.0-a1 Added vars: email. Removed vars: to, eol, additional_parameters, $result.
558             */
559            $vars = [
560                'email',
561                'subject',
562                'msg',
563                'headers',
564            ];
565            extract($this->dispatcher->trigger_event('core.phpbb_mail_after', compact($vars)));
566        }
567        else
568        {
569            $this->queue->init('email', $this->config['email_package_size']);
570            $this->queue->put('email', [
571                'email'    => $this->email,
572            ]);
573        }
574
575        // Reset the object
576        $this->init();
577
578        return true;
579    }
580
581    /**
582     * {@inheritdoc}
583     */
584    public function header(string $header_name, mixed $header_value): void
585    {
586        $this->headers->addHeader($header_name, $header_value);
587    }
588}