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