Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
125 / 125
Mailer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
7 / 7
47
100.00% covered (success)
100.00%
125 / 125
 __construct
100.00% covered (success)
100.00%
1 / 1
14
100.00% covered (success)
100.00%
32 / 32
 sendMail
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
9 / 9
 configureSmtp
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
17 / 17
 configureRecipients
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
15 / 15
 configureBody
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 configureAttachments
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
25 / 25
 configureOptions
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
22 / 22
1<?php
2
3namespace Miniframe\Mailer\Middleware;
4
5use Miniframe\Mailer\Model\Attachment;
6use Miniframe\Mailer\Model\Recipient;
7use Miniframe\Core\AbstractMiddleware;
8use Miniframe\Core\Config;
9use Miniframe\Core\Request;
10use Miniframe\Core\Response;
11use PHPMailer\PHPMailer\PHPMailer;
12use PHPMailer\PHPMailer\Exception;
13
14class Mailer extends AbstractMiddleware
15{
16    /**
17     * SMTP config
18     *
19     * @var array|null
20     */
21    protected $smtp = null;
22
23    /**
24     * Initiates the Mailer middleware
25     *
26     * @param Request $request Reference to the Request object.
27     * @param Config  $config  Reference to the Config object.
28     */
29    public function __construct(Request $request, Config $config)
30    {
31        parent::__construct($request, $config);
32
33        $transport = 'sendmail';
34        if ($config->has('mailer', 'transport')) {
35            $transport = $config->get('mailer', 'transport');
36            if (!in_array($transport, ['sendmail', 'smtp'])) {
37                throw new \RuntimeException('Invalid mailer transport: ' . $transport);
38            }
39        }
40
41        // Configure SMTP transport
42        if ($transport == 'smtp') {
43            $this->smtp = [
44                'hostname' => $config->get('mailer', 'smtp_hostname'),
45                'encryption' => 'PLAIN',
46            ];
47            if ($config->has('mailer', 'smtp_authentication')) {
48                $this->smtp['authentication'] = $config->get('mailer', 'smtp_authentication');
49                if (!is_bool($this->smtp['authentication'])) {
50                    throw new \RuntimeException('Mailer SMTP authentication value must be a boolean');
51                }
52            }
53            if ($this->smtp['authentication']) {
54                $this->smtp['username'] = $config->get('mailer', 'smtp_username');
55                $this->smtp['password'] = $config->get('mailer', 'smtp_password');
56            }
57            if ($config->has('mailer', 'smtp_encryption')) {
58                $this->smtp['encryption'] = strtoupper($config->get('mailer', 'smtp_encryption'));
59                if (!in_array($this->smtp['encryption'], ['PLAIN', 'TLS', 'SSL'])) {
60                    throw new \RuntimeException('Mailer SMTP encryption must be plain, tls or ssl');
61                }
62            }
63            if ($config->has('mailer', 'smtp_port')) {
64                $this->smtp['port'] = $config->get('mailer', 'smtp_port');
65                if (!is_numeric($this->smtp['port'])) {
66                    throw new \RuntimeException('Mailer SMTP port value must be a numeric value');
67                }
68            } elseif ($this->smtp['encryption'] == 'PLAIN') {
69                $this->smtp['port'] = 25;
70            } elseif ($this->smtp['encryption'] == 'TLS') {
71                $this->smtp['port'] = 587;
72            } elseif ($this->smtp['encryption'] == 'SSL') {
73                $this->smtp['port'] = 465;
74            }
75        }
76    }
77
78    /**
79     * Sends an email.
80     *
81     * Valid options are:
82     * - from_name
83     * - from_email
84     * - replyto_name
85     * - replyto_email
86     * They may also be defined in the ini file as default.
87     *
88     * @param Recipient|Recipient[]        $recipient      One or more recipients.
89     * @param string                       $subject        Subject of the mail.
90     * @param Response                     $mailBody       A response object that returns a mail body.
91     * @param boolean                      $mailBodyIsHtml Set to false if the mail is plain text.
92     * @param Attachment|Attachment[]|null $attachment     None, one or more attachments.
93     * @param array                        $options        Additional options.
94     *
95     * @return void
96     * @throws Exception Can throw PHPMailer exceptions.
97     */
98    public function sendMail(
99        $recipient,
100        string $subject,
101        Response $mailBody,
102        bool $mailBodyIsHtml = true,
103        $attachment = null,
104        array $options = array()
105    ): void {
106        $phpmailer = new PHPMailer(true);
107        $this->configureSmtp($phpmailer);
108        $this->configureRecipients($phpmailer, is_array($recipient) ? $recipient : [$recipient]);
109        $this->configureBody($phpmailer, $mailBody, $mailBodyIsHtml);
110        $this->configureAttachments($phpmailer, is_array($attachment) ? $attachment : [$attachment]);
111        $this->configureOptions($phpmailer, $options);
112        $phpmailer->Subject = $subject;
113
114        $phpmailer->send();
115    }
116
117    /**
118     * Configures the SMTP properties for a PHPMailer instance
119     *
120     * @param PHPMailer $phpmailer The PHPMailer instance.
121     *
122     * @return void
123     */
124    protected function configureSmtp(PHPMailer $phpmailer): void
125    {
126        if (!is_array($this->smtp)) {
127            return;
128        }
129        $phpmailer->isSMTP();
130        $phpmailer->Host = $this->smtp['hostname'];
131        $phpmailer->SMTPAuth = $this->smtp['authentication'];
132        if ($this->smtp['authentication']) {
133            $phpmailer->Username = $this->smtp['username'];
134            $phpmailer->Password = $this->smtp['password'];
135        }
136        switch ($this->smtp['encryption']) {
137            case 'SSL':
138                $phpmailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
139                break;
140            case 'TLS':
141                $phpmailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
142                break;
143        }
144        $phpmailer->Port = $this->smtp['port'];
145    }
146
147    /**
148     * Adds the recipients to PHPMailer
149     *
150     * @param PHPMailer   $phpmailer  The PHPMailer instance.
151     * @param Recipient[] $recipients List of recipients.
152     *
153     * @return void
154     */
155    protected function configureRecipients(PHPMailer $phpmailer, array $recipients): void
156    {
157        foreach ($recipients as $recipient) {
158            if (!($recipient instanceof Recipient)) {
159                throw new \InvalidArgumentException(
160                    'The recipient property must be one or more Recipient models, not ' . gettype($recipient)
161                );
162            }
163            switch ($recipient->getHeader()) {
164                case Recipient::HEADER_TO:
165                    $phpmailer->addAddress($recipient->getEmail(), $recipient->getName());
166                    break;
167                case Recipient::HEADER_CC:
168                    $phpmailer->addCC($recipient->getEmail(), $recipient->getName());
169                    break;
170                case Recipient::HEADER_BCC:
171                    $phpmailer->addBCC($recipient->getEmail(), $recipient->getName());
172                    break;
173            }
174        }
175    }
176
177    /**
178     * Configures the mail body
179     *
180     * @param PHPMailer $phpmailer The PHPMailer instance.
181     * @param Response  $mailBody  A response object that returns a mail body.
182     * @param boolean   $isHtml    Set to false if the mail is plain text.
183     *
184     * @return void
185     */
186    protected function configureBody(PHPMailer $phpmailer, Response $mailBody, bool $isHtml): void
187    {
188        $phpmailer->isHTML($isHtml);
189        if ($isHtml) {
190            $phpmailer->msgHTML($mailBody->render());
191        } else {
192            $phpmailer->Body = $mailBody->render();
193        }
194    }
195
196    /**
197     * Configures the attachments.
198     *
199     * @param PHPMailer    $phpmailer   The PHPMailer instance.
200     * @param Attachment[] $attachments List of attachments.
201     *
202     * @return void
203     */
204    protected function configureAttachments(PHPMailer $phpmailer, array $attachments): void
205    {
206        if ($attachments === [null]) {
207            return;
208        }
209        foreach ($attachments as $attachment) {
210            if (!($attachment instanceof Attachment)) {
211                throw new \InvalidArgumentException(
212                    'The attachment property must be zero, one or more Attachment models, not '
213                    . gettype($attachment)
214                );
215            }
216            switch ($attachment->getDisposition()) {
217                case Attachment::DISPOSITION_INLINE:
218                    $disposition = 'inline';
219                    break;
220                case Attachment::DISPOSITION_ATTACHMENT:
221                default:
222                    $disposition = 'attachment';
223                    break;
224            }
225
226            if ($attachment->getFullPath()) {
227                $phpmailer->addAttachment(
228                    $attachment->getFullPath(),
229                    $attachment->getFilename(),
230                    PHPMailer::ENCODING_BASE64,
231                    $attachment->getContentType(),
232                    $disposition
233                );
234            } else {
235                $phpmailer->addStringAttachment(
236                    $attachment->getAttachmentData(),
237                    $attachment->getFilename(),
238                    PHPMailer::ENCODING_BASE64,
239                    $attachment->getContentType(),
240                    $disposition
241                );
242            }
243        }
244    }
245
246    /**
247     * Parse additional options.
248     *
249     * @param PHPMailer $phpmailer The PHPMailer instance.
250     * @param array     $options   Additional options.
251     *
252     * @return void
253     */
254    protected function configureOptions(PHPMailer $phpmailer, array $options): void
255    {
256        // Validate options
257        foreach (array_keys($options) as $optionKey) {
258            if (!in_array($optionKey, ['from_name', 'from_email', 'replyto_name', 'replyto_email'])) {
259                throw new \InvalidArgumentException('Invalid option: ' . $optionKey);
260            }
261        }
262
263        // Default sender (from_email is required, in config or in options)
264        $fromEmail = $options['from_email'] ?? $this->config->get('mailer', 'from_email');
265        if (isset($options['from_name'])) {
266            $fromName = $options['from_name'];
267        } elseif ($this->config->has('mailer', 'from_name')) {
268            $fromName = $this->config->get('mailer', 'from_name');
269        } else {
270            $fromName = ucfirst(explode('@', $fromEmail, 2)[0]);
271        }
272        $phpmailer->setFrom($fromEmail, $fromName);
273
274        // Reply to address
275        if (isset($options['replyto_email'])) {
276            $replyToEmail = $options['replyto_email'];
277        } elseif ($this->config->has('mailer', 'replyto_email')) {
278            $replyToEmail = $this->config->get('mailer', 'replyto_email');
279        } else {
280            $replyToEmail = $fromEmail;
281        }
282        // Reply to name
283        if (isset($options['replyto_name'])) {
284            $replyToName = $options['replyto_name'];
285        } elseif ($this->config->has('mailer', 'replyto_name')) {
286            $replyToName = $this->config->get('mailer', 'replyto_name');
287        } else {
288            $replyToName = ucfirst(explode('@', $replyToEmail, 2)[0]);
289        }
290        $phpmailer->addReplyTo($replyToEmail, $replyToName);
291    }
292}