Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
125 / 125 |
| Mailer | |
100.00% |
1 / 1 |
|
100.00% |
7 / 7 |
47 | |
100.00% |
125 / 125 |
| __construct | |
100.00% |
1 / 1 |
14 | |
100.00% |
32 / 32 |
|||
| sendMail | |
100.00% |
1 / 1 |
3 | |
100.00% |
9 / 9 |
|||
| configureSmtp | |
100.00% |
1 / 1 |
5 | |
100.00% |
17 / 17 |
|||
| configureRecipients | |
100.00% |
1 / 1 |
6 | |
100.00% |
15 / 15 |
|||
| configureBody | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
| configureAttachments | |
100.00% |
1 / 1 |
8 | |
100.00% |
25 / 25 |
|||
| configureOptions | |
100.00% |
1 / 1 |
9 | |
100.00% |
22 / 22 |
|||
| 1 | <?php |
| 2 | |
| 3 | namespace Miniframe\Mailer\Middleware; |
| 4 | |
| 5 | use Miniframe\Mailer\Model\Attachment; |
| 6 | use Miniframe\Mailer\Model\Recipient; |
| 7 | use Miniframe\Core\AbstractMiddleware; |
| 8 | use Miniframe\Core\Config; |
| 9 | use Miniframe\Core\Request; |
| 10 | use Miniframe\Core\Response; |
| 11 | use PHPMailer\PHPMailer\PHPMailer; |
| 12 | use PHPMailer\PHPMailer\Exception; |
| 13 | |
| 14 | class 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 | } |