Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.68% |
133 / 139 |
|
82.35% |
14 / 17 |
CRAP | |
0.00% |
0 / 1 |
Mailer | |
95.68% |
133 / 139 |
|
82.35% |
14 / 17 |
48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTransport | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNewMessage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
resetConnection | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
3.71 | |||
getNewBlankMessage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setTransport | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
stringToAddressList | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
buildMultipartBody | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
3 | |||
send | |
96.72% |
59 / 61 |
|
0.00% |
0 / 1 |
21 | |||
sendLink | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getDefaultLinkSubject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sendRecord | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
setMaxRecipients | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultRecordSubject | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFromAddressOverride | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setFromAddressOverride | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
convertToAddressList | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Mailer Class |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2009. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Mailer |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFind\Mailer; |
31 | |
32 | use Laminas\Mail\Address; |
33 | use Laminas\Mail\AddressList; |
34 | use Laminas\Mail\Header\ContentType; |
35 | use Laminas\Mail\Transport\TransportInterface; |
36 | use Laminas\Mime\Message as MimeMessage; |
37 | use Laminas\Mime\Mime; |
38 | use Laminas\Mime\Part as MimePart; |
39 | use VuFind\Exception\Mail as MailException; |
40 | |
41 | use function count; |
42 | use function is_callable; |
43 | |
44 | /** |
45 | * VuFind Mailer Class |
46 | * |
47 | * @category VuFind |
48 | * @package Mailer |
49 | * @author Demian Katz <demian.katz@villanova.edu> |
50 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
51 | * @link https://vufind.org/wiki/development Wiki |
52 | */ |
53 | class Mailer implements |
54 | \VuFind\I18n\Translator\TranslatorAwareInterface, |
55 | \Laminas\Log\LoggerAwareInterface |
56 | { |
57 | use \VuFind\I18n\Translator\TranslatorAwareTrait; |
58 | use \VuFind\Log\LoggerAwareTrait; |
59 | |
60 | /** |
61 | * Mail transport |
62 | * |
63 | * @var TransportInterface |
64 | */ |
65 | protected $transport; |
66 | |
67 | /** |
68 | * A clone of $transport above. This can be used to reset the connection state |
69 | * in case transport doesn't support the disconnect method or it throws an |
70 | * exception (this can happen if the connection is stale and the connector tries |
71 | * to issue a QUIT message for clean disconnect). |
72 | * |
73 | * @var TransportInterface |
74 | */ |
75 | protected $initialTransport; |
76 | |
77 | /** |
78 | * The maximum number of email recipients allowed (0 = no limit) |
79 | * |
80 | * @var int |
81 | */ |
82 | protected $maxRecipients = 1; |
83 | |
84 | /** |
85 | * "From" address override |
86 | * |
87 | * @var string |
88 | */ |
89 | protected $fromAddressOverride = ''; |
90 | |
91 | /** |
92 | * Constructor |
93 | * |
94 | * @param TransportInterface $transport Mail transport |
95 | * @param ?string $messageLog File to log messages into (null for no logging) |
96 | */ |
97 | public function __construct(TransportInterface $transport, protected ?string $messageLog = null) |
98 | { |
99 | $this->setTransport($transport); |
100 | } |
101 | |
102 | /** |
103 | * Get the mail transport object. |
104 | * |
105 | * @return TransportInterface |
106 | */ |
107 | public function getTransport() |
108 | { |
109 | return $this->transport; |
110 | } |
111 | |
112 | /** |
113 | * Get a text email message object. |
114 | * |
115 | * @return Message |
116 | */ |
117 | public function getNewMessage() |
118 | { |
119 | $message = $this->getNewBlankMessage(); |
120 | $headers = $message->getHeaders(); |
121 | $ctype = new ContentType(); |
122 | $ctype->setType(Mime::TYPE_TEXT); |
123 | $ctype->addParameter('charset', 'UTF-8'); |
124 | $headers->addHeader($ctype); |
125 | return $message; |
126 | } |
127 | |
128 | /** |
129 | * Reset the connection in the transport. Implements a fluent interface. |
130 | * |
131 | * @return Mailer |
132 | */ |
133 | public function resetConnection() |
134 | { |
135 | // If the transport has a disconnect method, call it. Otherwise, and in case |
136 | // disconnect fails, revert to the transport instance clone made before a |
137 | // connection was made. |
138 | $transport = $this->getTransport(); |
139 | if (is_callable([$transport, 'disconnect'])) { |
140 | try { |
141 | $transport->disconnect(); |
142 | } catch (\Exception $e) { |
143 | $this->setTransport($this->initialTransport); |
144 | } |
145 | } else { |
146 | $this->setTransport($this->initialTransport); |
147 | } |
148 | return $this; |
149 | } |
150 | |
151 | /** |
152 | * Get a blank email message object. |
153 | * |
154 | * @return Message |
155 | */ |
156 | public function getNewBlankMessage() |
157 | { |
158 | $message = new Message(); |
159 | $message->setEncoding('UTF-8'); |
160 | return $message; |
161 | } |
162 | |
163 | /** |
164 | * Set the mail transport object. |
165 | * |
166 | * @param TransportInterface $transport Mail transport object |
167 | * |
168 | * @return void |
169 | */ |
170 | public function setTransport($transport) |
171 | { |
172 | $this->transport = $transport; |
173 | // Store a clone of the given transport so that we can reset the connection |
174 | // as necessary. |
175 | $this->initialTransport = clone $this->transport; |
176 | } |
177 | |
178 | /** |
179 | * Convert a delimited string to an address list. |
180 | * |
181 | * @param string $input String to convert |
182 | * |
183 | * @return AddressList |
184 | */ |
185 | public function stringToAddressList($input) |
186 | { |
187 | // Create recipient list |
188 | $list = new AddressList(); |
189 | foreach (preg_split('/[\s,;]/', $input) as $current) { |
190 | $current = trim($current); |
191 | if (!empty($current)) { |
192 | $list->add($current); |
193 | } |
194 | } |
195 | return $list; |
196 | } |
197 | |
198 | /** |
199 | * Constructs a {@see MimeMessage} body from given text and html content. |
200 | * |
201 | * @param string|null $text Mail content used for plain text part |
202 | * @param string|null $html Mail content used for html part |
203 | * |
204 | * @return MimeMessage |
205 | */ |
206 | public function buildMultipartBody( |
207 | string $text = null, |
208 | string $html = null |
209 | ): MimeMessage { |
210 | $parts = new MimeMessage(); |
211 | |
212 | if ($text) { |
213 | $textPart = new MimePart($text); |
214 | $textPart->setType(Mime::TYPE_TEXT); |
215 | $textPart->setCharset('utf-8'); |
216 | $textPart->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE); |
217 | $parts->addPart($textPart); |
218 | } |
219 | |
220 | if ($html) { |
221 | $htmlPart = new MimePart($html); |
222 | $htmlPart->setType(Mime::TYPE_HTML); |
223 | $htmlPart->setCharset('utf-8'); |
224 | $htmlPart->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE); |
225 | $parts->addPart($htmlPart); |
226 | } |
227 | |
228 | $alternativePart = new MimePart($parts->generateMessage()); |
229 | $alternativePart->setType('multipart/alternative'); |
230 | $alternativePart->setBoundary($parts->getMime()->boundary()); |
231 | $alternativePart->setCharset('utf-8'); |
232 | |
233 | $body = new MimeMessage(); |
234 | $body->setParts([$alternativePart]); |
235 | |
236 | return $body; |
237 | } |
238 | |
239 | /** |
240 | * Send an email message. |
241 | * |
242 | * @param string|Address|AddressList $to Recipient email address (or |
243 | * delimited list) |
244 | * @param string|Address $from Sender name and email address |
245 | * @param string $subject Subject line for message |
246 | * @param string|MimeMessage $body Message body |
247 | * @param string $cc CC recipient (null for none) |
248 | * @param string|Address|AddressList $replyTo Reply-To address (or delimited |
249 | * list, null for none) |
250 | * |
251 | * @throws MailException |
252 | * @return void |
253 | */ |
254 | public function send($to, $from, $subject, $body, $cc = null, $replyTo = null) |
255 | { |
256 | $recipients = $this->convertToAddressList($to); |
257 | $replyTo = $this->convertToAddressList($replyTo); |
258 | |
259 | // Validate email addresses: |
260 | if ($this->maxRecipients > 0) { |
261 | if ($this->maxRecipients < count($recipients)) { |
262 | throw new MailException( |
263 | 'Too Many Email Recipients', |
264 | MailException::ERROR_TOO_MANY_RECIPIENTS |
265 | ); |
266 | } |
267 | } |
268 | $validator = new \Laminas\Validator\EmailAddress(); |
269 | if (count($recipients) == 0) { |
270 | throw new MailException( |
271 | 'Invalid Recipient Email Address', |
272 | MailException::ERROR_INVALID_RECIPIENT |
273 | ); |
274 | } |
275 | foreach ($recipients as $current) { |
276 | if (!$validator->isValid($current->getEmail())) { |
277 | throw new MailException( |
278 | 'Invalid Recipient Email Address', |
279 | MailException::ERROR_INVALID_RECIPIENT |
280 | ); |
281 | } |
282 | } |
283 | foreach ($replyTo as $current) { |
284 | if (!$validator->isValid($current->getEmail())) { |
285 | throw new MailException( |
286 | 'Invalid Reply-To Email Address', |
287 | MailException::ERROR_INVALID_REPLY_TO |
288 | ); |
289 | } |
290 | } |
291 | $fromEmail = ($from instanceof Address) |
292 | ? $from->getEmail() : $from; |
293 | if (!$validator->isValid($fromEmail)) { |
294 | throw new MailException( |
295 | 'Invalid Sender Email Address', |
296 | MailException::ERROR_INVALID_SENDER |
297 | ); |
298 | } |
299 | |
300 | if ( |
301 | !empty($this->fromAddressOverride) |
302 | && $this->fromAddressOverride != $fromEmail |
303 | ) { |
304 | // Add the original from address as the reply-to address unless |
305 | // a reply-to address has been specified |
306 | if (count($replyTo) === 0) { |
307 | $replyTo->add($fromEmail); |
308 | } |
309 | if (!($from instanceof Address)) { |
310 | $from = new Address($from); |
311 | } |
312 | $name = $from->getName(); |
313 | if (!$name) { |
314 | [$fromPre] = explode('@', $from->getEmail()); |
315 | $name = $fromPre ? $fromPre : null; |
316 | } |
317 | $from = new Address($this->fromAddressOverride, $name); |
318 | } |
319 | |
320 | // Convert all exceptions thrown by mailer into MailException objects: |
321 | try { |
322 | // Send message |
323 | $message = $body instanceof MimeMessage |
324 | ? $this->getNewBlankMessage() |
325 | : $this->getNewMessage(); |
326 | $message->addFrom($from) |
327 | ->addTo($recipients) |
328 | ->setBody($body) |
329 | ->setSubject($subject); |
330 | if ($cc !== null) { |
331 | $message->addCc($cc); |
332 | } |
333 | if ($replyTo) { |
334 | $message->addReplyTo($replyTo); |
335 | } |
336 | $this->getTransport()->send($message); |
337 | if ($this->messageLog) { |
338 | file_put_contents($this->messageLog, $message->toString() . "\n", FILE_APPEND); |
339 | } |
340 | } catch (\Exception $e) { |
341 | $this->logError($e->getMessage()); |
342 | throw new MailException($e->getMessage(), MailException::ERROR_UNKNOWN); |
343 | } |
344 | } |
345 | |
346 | /** |
347 | * Send an email message representing a link. |
348 | * |
349 | * @param string $to Recipient email address |
350 | * @param string|\Laminas\Mail\Address $from Sender name and email |
351 | * address |
352 | * @param string $msg User notes to include in |
353 | * message |
354 | * @param string $url URL to share |
355 | * @param \Laminas\View\Renderer\PhpRenderer $view View object (used to render |
356 | * email templates) |
357 | * @param string $subject Subject for email |
358 | * (optional) |
359 | * @param string $cc CC recipient (null for |
360 | * none) |
361 | * @param string|Address|AddressList $replyTo Reply-To address (or |
362 | * delimited list, null for none) |
363 | * |
364 | * @throws MailException |
365 | * @return void |
366 | */ |
367 | public function sendLink( |
368 | $to, |
369 | $from, |
370 | $msg, |
371 | $url, |
372 | $view, |
373 | $subject = null, |
374 | $cc = null, |
375 | $replyTo = null |
376 | ) { |
377 | if (null === $subject) { |
378 | $subject = $this->getDefaultLinkSubject(); |
379 | } |
380 | $body = $view->partial( |
381 | 'Email/share-link.phtml', |
382 | [ |
383 | 'msgUrl' => $url, 'to' => $to, 'from' => $from, 'message' => $msg, |
384 | ] |
385 | ); |
386 | $this->send($to, $from, $subject, $body, $cc, $replyTo); |
387 | } |
388 | |
389 | /** |
390 | * Get the default subject line for sendLink(). |
391 | * |
392 | * @return string |
393 | */ |
394 | public function getDefaultLinkSubject() |
395 | { |
396 | return $this->translate('Library Catalog Search Result'); |
397 | } |
398 | |
399 | /** |
400 | * Send an email message representing a record. |
401 | * |
402 | * @param string $to Recipient email address |
403 | * @param string|\Laminas\Mail\Address $from Sender name and email |
404 | * address |
405 | * @param string $msg User notes to include in |
406 | * message |
407 | * @param \VuFind\RecordDriver\AbstractBase $record Record being emailed |
408 | * @param \Laminas\View\Renderer\PhpRenderer $view View object (used to render |
409 | * email templates) |
410 | * @param string $subject Subject for email |
411 | * (optional) |
412 | * @param string $cc CC recipient (null for |
413 | * none) |
414 | * @param string|Address|AddressList $replyTo Reply-To address (or |
415 | * delimited list, null for none) |
416 | * |
417 | * @throws MailException |
418 | * @return void |
419 | */ |
420 | public function sendRecord( |
421 | $to, |
422 | $from, |
423 | $msg, |
424 | $record, |
425 | $view, |
426 | $subject = null, |
427 | $cc = null, |
428 | $replyTo = null |
429 | ) { |
430 | if (null === $subject) { |
431 | $subject = $this->getDefaultRecordSubject($record); |
432 | } |
433 | $body = $view->partial( |
434 | 'Email/record.phtml', |
435 | [ |
436 | 'driver' => $record, 'to' => $to, 'from' => $from, 'message' => $msg, |
437 | ] |
438 | ); |
439 | $this->send($to, $from, $subject, $body, $cc, $replyTo); |
440 | } |
441 | |
442 | /** |
443 | * Set the maximum number of email recipients |
444 | * |
445 | * @param int $max Maximum |
446 | * |
447 | * @return void |
448 | */ |
449 | public function setMaxRecipients($max) |
450 | { |
451 | $this->maxRecipients = $max; |
452 | } |
453 | |
454 | /** |
455 | * Get the default subject line for sendRecord() |
456 | * |
457 | * @param \VuFind\RecordDriver\AbstractBase $record Record being emailed |
458 | * |
459 | * @return string |
460 | */ |
461 | public function getDefaultRecordSubject($record) |
462 | { |
463 | return $this->translate('Library Catalog Record') . ': ' |
464 | . $record->getBreadcrumb(); |
465 | } |
466 | |
467 | /** |
468 | * Get the "From" address override value |
469 | * |
470 | * @return string |
471 | */ |
472 | public function getFromAddressOverride() |
473 | { |
474 | return $this->fromAddressOverride; |
475 | } |
476 | |
477 | /** |
478 | * Set the "From" address override |
479 | * |
480 | * @param string $address "From" address |
481 | * |
482 | * @return void |
483 | */ |
484 | public function setFromAddressOverride($address) |
485 | { |
486 | $this->fromAddressOverride = $address; |
487 | } |
488 | |
489 | /** |
490 | * Convert the given addresses to an AddressList object |
491 | * |
492 | * @param string|Address|AddressList $addresses Addresses |
493 | * |
494 | * @return AddressList |
495 | */ |
496 | protected function convertToAddressList($addresses) |
497 | { |
498 | if ($addresses instanceof AddressList) { |
499 | $result = $addresses; |
500 | } elseif ($addresses instanceof Address) { |
501 | $result = new AddressList(); |
502 | $result->add($addresses); |
503 | } else { |
504 | $result = $this->stringToAddressList($addresses ? $addresses : ''); |
505 | } |
506 | return $result; |
507 | } |
508 | } |