]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Mailing.php
Fix mailing preview; fixes #1800
[galette.git] / galette / lib / Galette / Core / Mailing.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Mailing features
7 *
8 * PHP version 5
9 *
10 * Copyright © 2009-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Core
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2009-03-07
35 */
36
37 namespace Galette\Core;
38
39 use Analog\Analog;
40 use ArrayObject;
41 use Galette\Entity\Adherent;
42 use Galette\IO\File;
43 use Laminas\Db\ResultSet\ResultSet;
44
45 /**
46 * Mailing features
47 *
48 * @category Core
49 * @name Mailing
50 * @package Galette
51 * @author Johan Cwiklinski <johan@x-tnd.be>
52 * @copyright 2009-2023 The Galette Team
53 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
54 * @link http://galette.tuxfamily.org
55 * @since Available since 0.7dev - 2009-03-07
56 *
57 * @property string $subject
58 * @property string $message
59 * @property boolean $html
60 * @property integer $current_step
61 * @property-read integer $step
62 * @property integer|string $id
63 * @property-read string $alt_message
64 * @property-read string $wrapped_message
65 * @property-read PHPMailer\PHPMailer\PHPMailer $mail
66 * @property-read PHPMailer\PHPMailer\PHPMailer $_mail
67 * @property-read array $errors
68 * @property-read array $recipients
69 * @property-read string|false $tmp_path
70 * @property array $attachments
71 * @property-read string $sender_name
72 * @property-read string $sender_address
73 * @property integer $history_id
74 */
75 class Mailing extends GaletteMail
76 {
77 public const STEP_START = 0;
78 public const STEP_PREVIEW = 1;
79 public const STEP_SEND = 2;
80 public const STEP_SENT = 3;
81
82 public const MIME_HTML = 'text/html';
83 public const MIME_TEXT = 'text/plain';
84 public const MIME_DEFAULT = self::MIME_TEXT;
85
86 private $id;
87
88 private $unreachables = array();
89 private $mrecipients = array();
90 private $current_step;
91
92 private $mime_type;
93
94 private $tmp_path;
95 private $history_id;
96
97 /**
98 * Default constructor
99 *
100 * @param Preferences $preferences Preferences instance
101 * @param array $members An array of members
102 * @param int $id Identifier, defaults to null
103 */
104 public function __construct(Preferences $preferences, array $members = [], int $id = null)
105 {
106 parent::__construct($preferences);
107 $this->id = $id ?? $this->generateNewId();
108
109 $this->current_step = self::STEP_START;
110 $this->mime_type = self::MIME_DEFAULT;
111 /** TODO: add a preference that propose default mime-type to use,
112 then init it here */
113 if (count($members)) {
114 //Check which members have a valid email address and which have not
115 $this->setRecipients($members);
116 }
117 $this->loadAttachments();
118 }
119
120 /**
121 * Generate new mailing id and temporary path
122 *
123 * @return string
124 */
125 private function generateNewId(): string
126 {
127 $id = '';
128 $chars = 'abcdefghjkmnpqrstuvwxyz0123456789';
129 $i = 0;
130 $size = 30;
131 while ($i <= $size - 1) {
132 $num = mt_rand(0, strlen($chars) - 1) % strlen($chars);
133 $id .= substr($chars, $num, 1);
134 $i++;
135 }
136
137 $this->id = $id;
138 $this->generateTmpPath($this->id);
139 return $this->id;
140 }
141
142 /**
143 * Generate temporary path
144 *
145 * @param string $id Random id, defautls to null
146 *
147 * @return void
148 */
149 private function generateTmpPath($id = null)
150 {
151 if ($id === null) {
152 $id = $this->generateNewId();
153 }
154 $this->tmp_path = GALETTE_ATTACHMENTS_PATH . '/' . $id;
155 }
156
157 /**
158 * Load mailing attachments
159 *
160 * @return void
161 */
162 private function loadAttachments()
163 {
164 $dir = '';
165 if (
166 isset($this->tmp_path)
167 && trim($this->tmp_path) !== ''
168 ) {
169 $dir = $this->tmp_path;
170 } else {
171 $dir = GALETTE_ATTACHMENTS_PATH . $this->id . '/';
172 }
173
174 $files = glob($dir . '*.*');
175 foreach ($files as $file) {
176 $f = new File($dir);
177 $f->setFileName(str_replace($dir, '', $file));
178 $this->attachments[] = $f;
179 }
180 }
181
182 /**
183 * Loads a mailing from history
184 *
185 * @param ArrayObject $rs Mailing entry
186 * @param boolean $new True if we create a 'new' mailing,
187 * false otherwise (from preview for example)
188 *
189 * @return boolean
190 */
191 public function loadFromHistory(ArrayObject $rs, $new = true)
192 {
193 global $zdb;
194
195 try {
196 $orig_recipients = unserialize($rs->mailing_recipients);
197 } catch (\Throwable $e) {
198 Analog::log(
199 'Unable to unserialize recipients for mailing ' . $rs->mailing_id,
200 Analog::ERROR
201 );
202 $orig_recipients = [];
203 }
204
205 $_recipients = array();
206 $mdeps = ['parent' => true];
207 foreach ($orig_recipients as $k => $v) {
208 $m = new Adherent($zdb, $k, $mdeps);
209 $_recipients[] = $m;
210 }
211 $this->setRecipients($_recipients);
212 $this->subject = $rs->mailing_subject;
213 $this->message = $rs->mailing_body;
214 $this->html = $this->message != strip_tags($this->message) ? true : false;
215 if ($rs->mailing_sender_name !== null || $rs->mailing_sender_address !== null) {
216 $this->setSender(
217 $rs->mailing_sender_name,
218 $rs->mailing_sender_address
219 );
220 }
221 //if mailing has already been sent, generate a new id and copy attachments
222 if ($rs->mailing_sent && $new) {
223 $this->generateNewId();
224 $this->copyAttachments($rs->mailing_id);
225 } else {
226 $this->tmp_path = null;
227 $this->id = $rs->mailing_id;
228 if (!$this->attachments) {
229 $this->loadAttachments();
230 }
231 $this->history_id = $rs->mailing_id;
232 }
233 return true;
234 }
235
236 /**
237 * Copy attachments from another mailing
238 *
239 * @param int $id Original mailing id
240 *
241 * @return void
242 */
243 private function copyAttachments($id)
244 {
245 $source_dir = GALETTE_ATTACHMENTS_PATH . $id . '/';
246 $dest_dir = GALETTE_ATTACHMENTS_PATH . $this->id . '/';
247
248 if (file_exists($source_dir)) {
249 if (file_exists($dest_dir)) {
250 throw new \RuntimeException(
251 str_replace(
252 '%s',
253 $this->id,
254 'Attachments directory already exists for mailing %s!'
255 )
256 );
257 } else {
258 //create directory
259 mkdir($dest_dir);
260 //copy attachments from source mailing and populate attachments
261 $this->attachments = array();
262 $files = glob($source_dir . '*.*');
263 foreach ($files as $file) {
264 $f = new File($source_dir);
265 $f->setFileName(str_replace($source_dir, '', $file));
266 $f->copyTo($dest_dir);
267 $this->attachments[] = $f;
268 }
269 }
270 } else {
271 Analog::log(
272 'No attachments in source directory',
273 Analog::DEBUG
274 );
275 }
276 }
277
278 /**
279 * Apply final header to email and send it :-)
280 *
281 * @return int
282 */
283 public function send()
284 {
285 $m = array();
286 foreach ($this->mrecipients as $member) {
287 $email = $member->getEmail();
288 $m[$email] = $member->sname;
289 }
290 parent::setRecipients($m);
291 return parent::send();
292 }
293
294 /**
295 * Set mailing recipients
296 *
297 * @param array $members Array of Adherent objects
298 *
299 * @return bool
300 */
301 public function setRecipients($members)
302 {
303 $m = array();
304 $this->mrecipients = array();
305 $this->unreachables = array();
306
307 foreach ($members as $member) {
308 $email = $member->getEmail();
309
310 if (trim($email) != '' && self::isValidEmail($email)) {
311 if (!in_array($member, $this->mrecipients)) {
312 $this->mrecipients[] = $member;
313 }
314 $m[$email] = $member->sname;
315 } else {
316 if (!in_array($member, $this->unreachables)) {
317 $this->unreachables[] = $member;
318 }
319 }
320 }
321 return parent::setRecipients($m);
322 }
323
324 /**
325 * Store maling attachments
326 *
327 * @param array $files Array of uploaded files to store
328 *
329 * @return true|int error code
330 */
331 public function store($files)
332 {
333 if ($this->tmp_path === null) {
334 $this->generateTmpPath();
335 }
336
337 if (!file_exists($this->tmp_path)) {
338 //directory does not exist, create it
339 mkdir($this->tmp_path);
340 }
341
342 if (!is_dir($this->tmp_path)) {
343 throw new \RuntimeException(
344 $this->tmp_path . ' should be a directory!'
345 );
346 }
347
348 //store files
349 $attachment = new File($this->tmp_path);
350 $res = $attachment->store($files);
351 if ($res < 0) {
352 return $res;
353 } else {
354 $this->attachments[] = $attachment;
355 }
356
357 return true;
358 }
359
360 /**
361 * Move attachments with final id once mailing has been stored
362 *
363 * @param int $id Mailing history id
364 *
365 * @return void
366 */
367 public function moveAttachments($id)
368 {
369 if (
370 isset($this->tmp_path)
371 && trim($this->tmp_path) !== ''
372 && count($this->attachments) > 0
373 ) {
374 foreach ($this->attachments as &$attachment) {
375 $old_path = $attachment->getDestDir() . $attachment->getFileName();
376 $new_path = GALETTE_ATTACHMENTS_PATH . $id . '/' .
377 $attachment->getFileName();
378 if (!file_exists(GALETTE_ATTACHMENTS_PATH . $id)) {
379 mkdir(GALETTE_ATTACHMENTS_PATH . $id);
380 }
381 $moved = rename($old_path, $new_path);
382 if ($moved) {
383 $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH);
384 }
385 }
386 rmdir($this->tmp_path);
387 $this->tmp_path = null;
388 }
389 }
390
391 /**
392 * Remove specified attachment
393 *
394 * @param string $name Filename
395 *
396 * @return void
397 */
398 public function removeAttachment($name)
399 {
400 $to_remove = null;
401 if (
402 isset($this->tmp_path)
403 && trim($this->tmp_path) !== ''
404 && file_exists($this->tmp_path)
405 ) {
406 $to_remove = $this->tmp_path;
407 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
408 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
409 }
410
411 if ($to_remove !== null) {
412 $to_remove .= '/' . $name;
413
414 if (!$this->attachments) {
415 $this->loadAttachments();
416 }
417
418 if (file_exists($to_remove)) {
419 $i = 0;
420 foreach ($this->attachments as $att) {
421 if ($att->getFileName() == $name) {
422 unset($this->attachments[$i]);
423 unlink($to_remove);
424 break;
425 }
426 $i++;
427 }
428 } else {
429 Analog::log(
430 str_replace(
431 '%file',
432 $name,
433 'File %file does not exists and cannot be removed!'
434 ),
435 Analog::WARNING
436 );
437 }
438 } else {
439 throw new \RuntimeException(
440 'Unable to get attachments path!'
441 );
442 }
443 }
444
445 /**
446 * Remove mailing attachments
447 *
448 * @param boolean $temp Remove only tmporary attachments,
449 * to avoid history breaking
450 *
451 * @return void|false
452 */
453 public function removeAttachments($temp = false)
454 {
455 $to_remove = null;
456 if (
457 isset($this->tmp_path)
458 && trim($this->tmp_path) !== ''
459 && file_exists($this->tmp_path)
460 ) {
461 $to_remove = $this->tmp_path;
462 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
463 if ($temp === true) {
464 return false;
465 }
466 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
467 }
468
469 if ($to_remove !== null) {
470 $rdi = new \RecursiveDirectoryIterator(
471 $to_remove,
472 \FilesystemIterator::SKIP_DOTS
473 );
474 $contents = new \RecursiveIteratorIterator(
475 $rdi,
476 \RecursiveIteratorIterator::CHILD_FIRST
477 );
478 foreach ($contents as $path) {
479 if ($path->isFile()) {
480 unlink($path->getPathname());
481 } else {
482 rmdir($path->getPathname());
483 }
484 }
485 rmdir($to_remove);
486 }
487 }
488
489 /**
490 * Return textual error message
491 *
492 * @param int $code The error code
493 *
494 * @return string Localized message
495 */
496 public function getAttachmentErrorMessage($code)
497 {
498 $f = new File($this->tmp_path);
499 return $f->getErrorMessage($code);
500 }
501
502 /**
503 * Does mailing already exists in history?
504 *
505 * @return boolean
506 */
507 public function existsInHistory()
508 {
509 return isset($this->history_id);
510 }
511
512 /**
513 * Global getter method
514 *
515 * @param string $name name of the property we want to retrieve
516 *
517 * @return mixed the called property
518 */
519 public function __get($name)
520 {
521 $forbidden = array('ordered');
522 if (!in_array($name, $forbidden)) {
523 switch ($name) {
524 case 'alt_message':
525 return $this->cleanedHtml();
526 case 'step':
527 return $this->current_step;
528 case 'subject':
529 return $this->getSubject();
530 case 'message':
531 return $this->getMessage();
532 case 'wrapped_message':
533 return $this->getWrappedMessage();
534 case 'html':
535 return $this->isHTML();
536 case 'mail':
537 case '_mail':
538 return $this->getPhpMailer();
539 case 'errors':
540 return $this->getErrors();
541 case 'recipients':
542 return $this->mrecipients;
543 case 'tmp_path':
544 if (isset($this->tmp_path) && trim($this->tmp_path) !== '') {
545 return $this->tmp_path;
546 } else {
547 //no attachments
548 return false;
549 }
550 case 'attachments':
551 return $this->attachments;
552 case 'sender_name':
553 return $this->getSenderName();
554 case 'sender_address':
555 return $this->getSenderAddress();
556 case 'history_id':
557 return $this->$name;
558 default:
559 Analog::log(
560 '[' . get_class($this) . 'Trying to get ' . $name,
561 Analog::DEBUG
562 );
563 return $this->$name;
564 }
565 } else {
566 Analog::log(
567 '[' . get_class($this) . 'Unable to get ' . $name,
568 Analog::ERROR
569 );
570 return false;
571 }
572 }
573
574 /**
575 * Global isset method
576 * Required for twig to access properties via __get
577 *
578 * @param string $name name of the property we want to retrieve
579 *
580 * @return bool
581 */
582 public function __isset($name)
583 {
584 $forbidden = array('ordered');
585 if (!in_array($name, $forbidden)) {
586 switch ($name) {
587 case 'alt_message':
588 case 'step':
589 case 'subject':
590 case 'message':
591 case 'wrapped_message':
592 case 'html':
593 case 'mail':
594 case '_mail':
595 case 'errors':
596 case 'recipients':
597 case 'tmp_path':
598 case 'attachments':
599 case 'sender_name':
600 case 'sender_address':
601 return true;
602 }
603 return isset($this->$name);
604 }
605
606 return false;
607 }
608
609 /**
610 * Global setter method
611 *
612 * @param string $name name of the property we want to assign a value to
613 * @param mixed $value a relevant value for the property
614 *
615 * @return void
616 */
617 public function __set($name, $value)
618 {
619 switch ($name) {
620 case 'subject':
621 $this->setSubject($value);
622 break;
623 case 'message':
624 $this->setMessage($value);
625 break;
626 case 'html':
627 if (is_bool($value)) {
628 $this->isHTML($value);
629 } else {
630 Analog::log(
631 '[' . get_class($this) . '] Value for field `' . $name .
632 '` should be boolean - (' . gettype($value) . ')' .
633 $value . ' given',
634 Analog::WARNING
635 );
636 }
637 break;
638 case 'current_step':
639 if (
640 is_int($value)
641 && ($value == self::STEP_START
642 || $value == self::STEP_PREVIEW
643 || $value == self::STEP_SEND
644 || $value == self::STEP_SENT)
645 ) {
646 $this->current_step = (int)$value;
647 } else {
648 Analog::log(
649 '[' . get_class($this) . '] Value for field `' . $name .
650 '` should be integer and know - (' . gettype($value) . ')' .
651 $value . ' given',
652 Analog::WARNING
653 );
654 }
655 break;
656 case 'id':
657 $this->id = $value;
658 break;
659 default:
660 Analog::log(
661 '[' . get_class($this) . '] Unable to set property `' . $name . '`',
662 Analog::WARNING
663 );
664 }
665 }
666 }