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