4 * Copyright © 2003-2024 The Galette Team
6 * This file is part of Galette (https://galette.eu).
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
22 namespace Galette\Core
;
26 use Galette\Entity\Adherent
;
28 use Laminas\Db\ResultSet\ResultSet
;
33 * @author Johan Cwiklinski <johan@x-tnd.be>
35 * @property string $subject
36 * @property string $message
37 * @property boolean $html
38 * @property integer $current_step
39 * @property-read integer $step
40 * @property integer|string $id
41 * @property-read string $alt_message
42 * @property-read string $wrapped_message
43 * @property-read PHPMailer\PHPMailer\PHPMailer $mail
44 * @property-read PHPMailer\PHPMailer\PHPMailer $_mail
45 * @property-read array $errors
46 * @property-read array $recipients
47 * @property-read array $unreachables
48 * @property-read string|false $tmp_path
49 * @property array $attachments
50 * @property-read string $sender_name
51 * @property-read string $sender_address
52 * @property integer $history_id
54 class Mailing
extends GaletteMail
56 public const STEP_START
= 0;
57 public const STEP_PREVIEW
= 1;
58 public const STEP_SEND
= 2;
59 public const STEP_SENT
= 3;
61 public const MIME_HTML
= 'text/html';
62 public const MIME_TEXT
= 'text/plain';
63 public const MIME_DEFAULT
= self
::MIME_TEXT
;
65 private string|
int $id;
67 /** @var array<int, Adherent> */
68 private array $unreachables = array();
69 /** @var array<int, Adherent> */
70 private array $mrecipients = array();
71 private int $current_step;
73 private string $mime_type;
75 private ?
string $tmp_path;
76 private int $history_id;
81 * @param Preferences $preferences Preferences instance
82 * @param array<int, Adherent> $members An array of members
83 * @param ?integer $id Identifier, defaults to null
85 public function __construct(Preferences
$preferences, array $members = [], int $id = null)
87 parent
::__construct($preferences);
88 $this->id
= $id ??
$this->generateNewId();
90 $this->current_step
= self
::STEP_START
;
91 $this->mime_type
= self
::MIME_DEFAULT
;
92 /** TODO: add a preference that propose default mime-type to use,
94 if (count($members)) {
95 //Check which members have a valid email address and which have not
96 $this->setRecipients($members);
98 $this->loadAttachments();
102 * Generate new mailing id and temporary path
106 private function generateNewId(): string
109 $chars = 'abcdefghjkmnpqrstuvwxyz0123456789';
112 while ($i <= $size - 1) {
113 $num = mt_rand(0, strlen($chars) - 1) %
strlen($chars);
114 $id .= substr($chars, $num, 1);
119 $this->generateTmpPath($this->id
);
124 * Generate temporary path
126 * @param ?string $id Random id, defaults to null
130 private function generateTmpPath(string $id = null): void
133 $id = $this->generateNewId();
135 $this->tmp_path
= GALETTE_ATTACHMENTS_PATH
. '/' . $id;
139 * Load mailing attachments
143 private function loadAttachments(): void
147 isset($this->tmp_path
)
148 && trim($this->tmp_path
) !== ''
150 $dir = $this->tmp_path
;
152 $dir = GALETTE_ATTACHMENTS_PATH
. $this->id
. '/';
155 $files = glob($dir . '*.*');
156 foreach ($files as $file) {
158 $f->setFileName(str_replace($dir, '', $file));
159 $this->attachments
[] = $f;
164 * Loads a mailing from history
166 * @param ArrayObject<string, mixed> $rs Mailing entry
167 * @param boolean $new True if we create a 'new' mailing,
168 * false otherwise (from preview for example)
172 public function loadFromHistory(ArrayObject
$rs, bool $new = true): bool
177 if (Galette
::isSerialized($rs->mailing_recipients
)) {
178 $orig_recipients = unserialize($rs->mailing_recipients
);
180 $orig_recipients = Galette
::jsonDecode($rs->mailing_recipients
);
182 } catch (\Throwable
$e) {
184 'Unable to retrieve recipients for mailing ' . $rs->mailing_id
,
187 $orig_recipients = [];
190 $_recipients = array();
191 $mdeps = ['parent' => true];
192 foreach ($orig_recipients as $k => $v) {
193 $m = new Adherent($zdb, $k, $mdeps);
196 $this->setRecipients($_recipients);
197 $this->subject
= $rs->mailing_subject
;
198 $this->message
= $rs->mailing_body
;
199 $this->html
= $this->message
!= strip_tags($this->message
) ?
true : false;
200 if ($rs->mailing_sender_name
!== null ||
$rs->mailing_sender_address
!== null) {
202 $rs->mailing_sender_name
,
203 $rs->mailing_sender_address
206 //if mailing has already been sent, generate a new id and copy attachments
207 if ($rs->mailing_sent
&& $new) {
208 $this->generateNewId();
209 $this->copyAttachments($rs->mailing_id
);
211 $this->tmp_path
= null;
212 $this->id
= $rs->mailing_id
;
213 if (!$this->attachments
) {
214 $this->loadAttachments();
216 $this->history_id
= $rs->mailing_id
;
222 * Copy attachments from another mailing
224 * @param int $id Original mailing id
228 private function copyAttachments(int $id): void
230 $source_dir = GALETTE_ATTACHMENTS_PATH
. $id . '/';
231 $dest_dir = GALETTE_ATTACHMENTS_PATH
. $this->id
. '/';
233 if (file_exists($source_dir)) {
234 if (file_exists($dest_dir)) {
235 throw new \
RuntimeException(
239 'Attachments directory already exists for mailing %s!'
245 //copy attachments from source mailing and populate attachments
246 $this->attachments
= array();
247 $files = glob($source_dir . '*.*');
248 foreach ($files as $file) {
249 $f = new File($source_dir);
250 $f->setFileName(str_replace($source_dir, '', $file));
251 $f->copyTo($dest_dir);
252 $this->attachments
[] = $f;
257 'No attachments in source directory',
264 * Apply final header to email and send it :-)
268 public function send(): int
271 foreach ($this->mrecipients
as $member) {
272 $email = $member->getEmail();
273 $m[$email] = $member->sname
;
275 parent
::setRecipients($m);
276 return parent
::send();
280 * Set mailing recipients
282 * @phpstan-ignore-next-line
283 * @param array<int, Adherent> $members Array of Adherent objects
287 public function setRecipients(array $members): bool
290 $this->mrecipients
= array();
291 $this->unreachables
= array();
293 foreach ($members as $member) {
294 $email = $member->getEmail();
296 if (trim($email) != '' && self
::isValidEmail($email)) {
297 if (!in_array($member, $this->mrecipients
)) {
298 $this->mrecipients
[] = $member;
300 $m[$email] = $member->sname
;
302 if (!in_array($member, $this->unreachables
)) {
303 $this->unreachables
[] = $member;
307 return parent
::setRecipients($m);
311 * Store maling attachments
313 * @param array<string, string|int> $files Array of uploaded files to store
315 * @return true|int error code
317 public function store(array $files): bool|
int
319 if ($this->tmp_path
=== null) {
320 $this->generateTmpPath();
323 if (!file_exists($this->tmp_path
)) {
324 //directory does not exist, create it
325 mkdir($this->tmp_path
);
328 if (!is_dir($this->tmp_path
)) {
329 throw new \
RuntimeException(
330 $this->tmp_path
. ' should be a directory!'
335 $attachment = new File($this->tmp_path
);
336 $res = $attachment->store($files);
340 $this->attachments
[] = $attachment;
347 * Move attachments with final id once mailing has been stored
349 * @param int $id Mailing history id
353 public function moveAttachments(int $id): void
356 isset($this->tmp_path
)
357 && trim($this->tmp_path
) !== ''
358 && count($this->attachments
) > 0
360 foreach ($this->attachments
as &$attachment) {
361 $old_path = $attachment->getDestDir() . $attachment->getFileName();
362 $new_path = GALETTE_ATTACHMENTS_PATH
. $id . '/' .
363 $attachment->getFileName();
364 if (!file_exists(GALETTE_ATTACHMENTS_PATH
. $id)) {
365 mkdir(GALETTE_ATTACHMENTS_PATH
. $id);
367 $moved = rename($old_path, $new_path);
369 $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH
);
372 rmdir($this->tmp_path
);
373 $this->tmp_path
= null;
378 * Remove specified attachment
380 * @param string $name Filename
384 public function removeAttachment(string $name): void
388 isset($this->tmp_path
)
389 && trim($this->tmp_path
) !== ''
390 && file_exists($this->tmp_path
)
392 $to_remove = $this->tmp_path
;
393 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH
. $this->id
)) {
394 $to_remove = GALETTE_ATTACHMENTS_PATH
. $this->id
;
397 if ($to_remove !== null) {
398 $to_remove .= '/' . $name;
400 if (!$this->attachments
) {
401 $this->loadAttachments();
404 if (file_exists($to_remove)) {
406 foreach ($this->attachments
as $att) {
407 if ($att->getFileName() == $name) {
408 unset($this->attachments
[$i]);
419 'File %file does not exists and cannot be removed!'
425 throw new \
RuntimeException(
426 'Unable to get attachments path!'
432 * Remove mailing attachments
434 * @param boolean $temp Remove only temporary attachments,
435 * to avoid history breaking
439 public function removeAttachments(bool $temp = false): bool
443 isset($this->tmp_path
)
444 && trim($this->tmp_path
) !== ''
445 && file_exists($this->tmp_path
)
447 $to_remove = $this->tmp_path
;
448 } elseif (file_exists(GALETTE_ATTACHMENTS_PATH
. $this->id
)) {
449 if ($temp === true) {
452 $to_remove = GALETTE_ATTACHMENTS_PATH
. $this->id
;
455 if ($to_remove !== null) {
456 $rdi = new \
RecursiveDirectoryIterator(
458 \FilesystemIterator
::SKIP_DOTS
460 $contents = new \
RecursiveIteratorIterator(
462 \RecursiveIteratorIterator
::CHILD_FIRST
464 foreach ($contents as $path) {
465 if ($path->isFile()) {
466 unlink($path->getPathname());
468 rmdir($path->getPathname());
477 * Return textual error message
479 * @param int $code The error code
481 * @return string Localized message
483 public function getAttachmentErrorMessage(int $code): string
485 $f = new File($this->tmp_path
);
486 return $f->getErrorMessage($code);
490 * Does mailing already exists in history?
494 public function existsInHistory(): bool
496 return isset($this->history_id
);
500 * Global getter method
502 * @param string $name name of the property we want to retrieve
504 * @return mixed the called property
506 public function __get(string $name)
508 $forbidden = array('ordered');
509 if (!in_array($name, $forbidden)) {
512 return $this->cleanedHtml();
514 return $this->current_step
;
516 return $this->getSubject();
518 return $this->getMessage();
519 case 'wrapped_message':
520 return $this->getWrappedMessage();
522 return $this->isHTML();
525 return $this->getPhpMailer();
527 return $this->getErrors();
529 return $this->mrecipients
;
531 if (isset($this->tmp_path
) && trim($this->tmp_path
) !== '') {
532 return $this->tmp_path
;
538 return $this->attachments
;
540 return $this->getSenderName();
541 case 'sender_address':
542 return $this->getSenderAddress();
547 '[' . get_class($this) . 'Trying to get ' . $name,
554 '[' . get_class($this) . 'Unable to get ' . $name,
562 * Global isset method
563 * Required for twig to access properties via __get
565 * @param string $name name of the property we want to retrieve
569 public function __isset(string $name): bool
571 $forbidden = array('ordered');
572 if (!in_array($name, $forbidden)) {
578 case 'wrapped_message':
587 case 'sender_address':
590 return isset($this->$name);
597 * Global setter method
599 * @param string $name name of the property we want to assign a value to
600 * @param mixed $value a relevant value for the property
604 public function __set(string $name, $value): void
608 $this->setSubject($value);
611 $this->setMessage($value);
614 if (is_bool($value)) {
615 $this->isHTML($value);
618 '[' . get_class($this) . '] Value for field `' . $name .
619 '` should be boolean - (' . gettype($value) . ')' .
628 && ($value == self
::STEP_START
629 ||
$value == self
::STEP_PREVIEW
630 ||
$value == self
::STEP_SEND
631 ||
$value == self
::STEP_SENT
)
633 $this->current_step
= (int)$value;
636 '[' . get_class($this) . '] Value for field `' . $name .
637 '` should be integer and know - (' . gettype($value) . ')' .
648 '[' . get_class($this) . '] Unable to set property `' . $name . '`',