]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Mailing.php
2b3edea7fdc7026cc214ab0a55e0697b4f642884
[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 if ($rs->mailing_sender_name !== null || $rs->mailing_sender_address !== null) {
215 $this->setSender(
216 $rs->mailing_sender_name,
217 $rs->mailing_sender_address
218 );
219 }
220 //if mailing has already been sent, generate a new id and copy attachments
221 if ($rs->mailing_sent && $new) {
222 $this->generateNewId();
223 $this->copyAttachments($rs->mailing_id);
224 } else {
225 $this->tmp_path = null;
226 $this->id = $rs->mailing_id;
227 if (!$this->attachments) {
228 $this->loadAttachments();
229 }
230 $this->history_id = $rs->mailing_id;
231 }
232 return true;
233 }
234
235 /**
236 * Copy attachments from another mailing
237 *
238 * @param int $id Original mailing id
239 *
240 * @return void
241 */
242 private function copyAttachments($id)
243 {
244 $source_dir = GALETTE_ATTACHMENTS_PATH . $id . '/';
245 $dest_dir = GALETTE_ATTACHMENTS_PATH . $this->id . '/';
246
247 if (file_exists($source_dir)) {
248 if (file_exists($dest_dir)) {
249 throw new \RuntimeException(
250 str_replace(
251 '%s',
252 $this->id,
253 'Attachments directory already exists for mailing %s!'
254 )
255 );
256 } else {
257 //create directory
258 mkdir($dest_dir);
259 //copy attachments from source mailing and populate attachments
260 $this->attachments = array();
261 $files = glob($source_dir . '*.*');
262 foreach ($files as $file) {
263 $f = new File($source_dir);
264 $f->setFileName(str_replace($source_dir, '', $file));
265 $f->copyTo($dest_dir);
266 $this->attachments[] = $f;
267 }
268 }
269 } else {
270 Analog::log(
271 'No attachments in source directory',
272 Analog::DEBUG
273 );
274 }
275 }
276
277 /**
278 * Apply final header to email and send it :-)
279 *
280 * @return int
281 */
282 public function send()
283 {
284 $m = array();
285 foreach ($this->mrecipients as $member) {
286 $email = $member->getEmail();
287 $m[$email] = $member->sname;
288 }
289 parent::setRecipients($m);
290 return parent::send();
291 }
292
293 /**
294 * Set mailing recipients
295 *
296 * @param array $members Array of Adherent objects
297 *
298 * @return bool
299 */
300 public function setRecipients($members)
301 {
302 $m = array();
303 $this->mrecipients = array();
304 $this->unreachables = array();
305
306 foreach ($members as $member) {
307 $email = $member->getEmail();
308
309 if (trim($email) != '' && self::isValidEmail($email)) {
310 if (!in_array($member, $this->mrecipients)) {
311 $this->mrecipients[] = $member;
312 }
313 $m[$email] = $member->sname;
314 } else {
315 if (!in_array($member, $this->unreachables)) {
316 $this->unreachables[] = $member;
317 }
318 }
319 }
320 return parent::setRecipients($m);
321 }
322
323 /**
324 * Store maling attachments
325 *
326 * @param array $files Array of uploaded files to store
327 *
328 * @return true|int error code
329 */
330 public function store($files)
331 {
332 if ($this->tmp_path === null) {
333 $this->generateTmpPath();
334 }
335
336 if (!file_exists($this->tmp_path)) {
337 //directory does not exist, create it
338 mkdir($this->tmp_path);
339 }
340
341 if (!is_dir($this->tmp_path)) {
342 throw new \RuntimeException(
343 $this->tmp_path . ' should be a directory!'
344 );
345 }
346
347 //store files
348 $attachment = new File($this->tmp_path);
349 $res = $attachment->store($files);
350 if ($res < 0) {
351 return $res;
352 } else {
353 $this->attachments[] = $attachment;
354 }
355
356 return true;
357 }
358
359 /**
360 * Move attachments with final id once mailing has been stored
361 *
362 * @param int $id Mailing history id
363 *
364 * @return void
365 */
366 public function moveAttachments($id)
367 {
368 if (
369 isset($this->tmp_path)
370 && trim($this->tmp_path) !== ''
371 && count($this->attachments) > 0
372 ) {
373 foreach ($this->attachments as &$attachment) {
374 $old_path = $attachment->getDestDir() . $attachment->getFileName();
375 $new_path = GALETTE_ATTACHMENTS_PATH . $id . '/' .
376 $attachment->getFileName();
377 if (!file_exists(GALETTE_ATTACHMENTS_PATH . $id)) {
378 mkdir(GALETTE_ATTACHMENTS_PATH . $id);
379 }
380 $moved = rename($old_path, $new_path);
381 if ($moved) {
382 $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH);
383 }
384 }
385 rmdir($this->tmp_path);
386 $this->tmp_path = null;
387 }
388 }
389
390 /**
391 * Remove specified attachment
392 *
393 * @param string $name Filename
394 *
395 * @return void
396 */
397 public function removeAttachment($name)
398 {
399 $to_remove = null;
400 if (
401 isset($this->tmp_path)
402 && trim($this->tmp_path) !== ''
403 && file_exists($this->tmp_path)
404 ) {
405 $to_remove = $this->tmp_path;
406 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
407 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
408 }
409
410 if ($to_remove !== null) {
411 $to_remove .= '/' . $name;
412
413 if (!$this->attachments) {
414 $this->loadAttachments();
415 }
416
417 if (file_exists($to_remove)) {
418 $i = 0;
419 foreach ($this->attachments as $att) {
420 if ($att->getFileName() == $name) {
421 unset($this->attachments[$i]);
422 unlink($to_remove);
423 break;
424 }
425 $i++;
426 }
427 } else {
428 Analog::log(
429 str_replace(
430 '%file',
431 $name,
432 'File %file does not exists and cannot be removed!'
433 ),
434 Analog::WARNING
435 );
436 }
437 } else {
438 throw new \RuntimeException(
439 'Unable to get attachments path!'
440 );
441 }
442 }
443
444 /**
445 * Remove mailing attachments
446 *
447 * @param boolean $temp Remove only tmporary attachments,
448 * to avoid history breaking
449 *
450 * @return void|false
451 */
452 public function removeAttachments($temp = false)
453 {
454 $to_remove = null;
455 if (
456 isset($this->tmp_path)
457 && trim($this->tmp_path) !== ''
458 && file_exists($this->tmp_path)
459 ) {
460 $to_remove = $this->tmp_path;
461 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
462 if ($temp === true) {
463 return false;
464 }
465 $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
466 }
467
468 if ($to_remove !== null) {
469 $rdi = new \RecursiveDirectoryIterator(
470 $to_remove,
471 \FilesystemIterator::SKIP_DOTS
472 );
473 $contents = new \RecursiveIteratorIterator(
474 $rdi,
475 \RecursiveIteratorIterator::CHILD_FIRST
476 );
477 foreach ($contents as $path) {
478 if ($path->isFile()) {
479 unlink($path->getPathname());
480 } else {
481 rmdir($path->getPathname());
482 }
483 }
484 rmdir($to_remove);
485 }
486 }
487
488 /**
489 * Return textual error message
490 *
491 * @param int $code The error code
492 *
493 * @return string Localized message
494 */
495 public function getAttachmentErrorMessage($code)
496 {
497 $f = new File($this->tmp_path);
498 return $f->getErrorMessage($code);
499 }
500
501 /**
502 * Does mailing already exists in history?
503 *
504 * @return boolean
505 */
506 public function existsInHistory()
507 {
508 return isset($this->history_id);
509 }
510
511 /**
512 * Global getter method
513 *
514 * @param string $name name of the property we want to retrive
515 *
516 * @return mixed the called property
517 */
518 public function __get($name)
519 {
520 $forbidden = array('ordered');
521 if (!in_array($name, $forbidden)) {
522 switch ($name) {
523 case 'alt_message':
524 return $this->cleanedHtml();
525 case 'step':
526 return $this->current_step;
527 case 'subject':
528 return $this->getSubject();
529 case 'message':
530 return $this->getMessage();
531 case 'wrapped_message':
532 return $this->getWrappedMessage();
533 case 'html':
534 return $this->isHTML();
535 case 'mail':
536 case '_mail':
537 return $this->getPhpMailer();
538 case 'errors':
539 return $this->getErrors();
540 case 'recipients':
541 return $this->mrecipients;
542 case 'tmp_path':
543 if (isset($this->tmp_path) && trim($this->tmp_path) !== '') {
544 return $this->tmp_path;
545 } else {
546 //no attachments
547 return false;
548 }
549 case 'attachments':
550 return $this->attachments;
551 case 'sender_name':
552 return $this->getSenderName();
553 case 'sender_address':
554 return $this->getSenderAddress();
555 case 'history_id':
556 return $this->$name;
557 default:
558 Analog::log(
559 '[' . get_class($this) . 'Trying to get ' . $name,
560 Analog::DEBUG
561 );
562 return $this->$name;
563 }
564 } else {
565 Analog::log(
566 '[' . get_class($this) . 'Unable to get ' . $name,
567 Analog::ERROR
568 );
569 return false;
570 }
571 }
572
573 /**
574 * Global isset method
575 * Required for twig to access properties via __get
576 *
577 * @param string $name name of the property we want to retrive
578 *
579 * @return bool
580 */
581 public function __isset($name)
582 {
583 $forbidden = array('ordered');
584 if (!in_array($name, $forbidden)) {
585 switch ($name) {
586 case 'alt_message':
587 case 'step':
588 case 'subject':
589 case 'message':
590 case 'wrapped_message':
591 case 'html':
592 case 'mail':
593 case '_mail':
594 case 'errors':
595 case 'recipients':
596 case 'tmp_path':
597 case 'attachments':
598 case 'sender_name':
599 case 'sender_address':
600 return true;
601 }
602 return isset($this->$name);
603 }
604
605 return false;
606 }
607
608 /**
609 * Global setter method
610 *
611 * @param string $name name of the property we want to assign a value to
612 * @param mixed $value a relevant value for the property
613 *
614 * @return void
615 */
616 public function __set($name, $value)
617 {
618 switch ($name) {
619 case 'subject':
620 $this->setSubject($value);
621 break;
622 case 'message':
623 $this->setMessage($value);
624 break;
625 case 'html':
626 if (is_bool($value)) {
627 $this->isHTML($value);
628 } else {
629 Analog::log(
630 '[' . get_class($this) . '] Value for field `' . $name .
631 '` should be boolean - (' . gettype($value) . ')' .
632 $value . ' given',
633 Analog::WARNING
634 );
635 }
636 break;
637 case 'current_step':
638 if (
639 is_int($value)
640 && ($value == self::STEP_START
641 || $value == self::STEP_PREVIEW
642 || $value == self::STEP_SEND
643 || $value == self::STEP_SENT)
644 ) {
645 $this->current_step = (int)$value;
646 } else {
647 Analog::log(
648 '[' . get_class($this) . '] Value for field `' . $name .
649 '` should be integer and know - (' . gettype($value) . ')' .
650 $value . ' given',
651 Analog::WARNING
652 );
653 }
654 break;
655 case 'id':
656 $this->id = $value;
657 break;
658 default:
659 Analog::log(
660 '[' . get_class($this) . '] Unable to set property `' . $name . '`',
661 Analog::WARNING
662 );
663 }
664 }
665 }