diff --git a/components/ILIAS/Badge/classes/class.ilBadgeHandler.php b/components/ILIAS/Badge/classes/class.ilBadgeHandler.php index 1381a4e2c0f1..f4886786bcd1 100755 --- a/components/ILIAS/Badge/classes/class.ilBadgeHandler.php +++ b/components/ILIAS/Badge/classes/class.ilBadgeHandler.php @@ -21,6 +21,7 @@ use ILIAS\Notifications\Model\ilNotificationLink; use ILIAS\Notifications\Model\ilNotificationParameter; use ILIAS\Badge\GlobalScreen\BadgeNotificationProvider; +use ILIAS\Mail\Attachments\MailAttachments; /** * Class ilBadgeHandler @@ -537,7 +538,7 @@ public function sendNotification( '', $lng->txt('badge_notification_subject'), $ntf->composeAndGetMessage($user_id, null, 'read', true), - [] + MailAttachments::empty() ); diff --git a/components/ILIAS/Calendar/classes/class.ilCalendarMailNotification.php b/components/ILIAS/Calendar/classes/class.ilCalendarMailNotification.php index 41935ce7cd3c..e10d7e90bb82 100755 --- a/components/ILIAS/Calendar/classes/class.ilCalendarMailNotification.php +++ b/components/ILIAS/Calendar/classes/class.ilCalendarMailNotification.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * Distributes calendar mail notifications * @author Stefan Meyer @@ -448,8 +450,6 @@ public function send(): void $this->sendMail(array($user_id), true); break; } - - $this->deleteAttachments(); } protected function addAttachment(): void @@ -462,17 +462,15 @@ protected function addAttachment(): void $ics_filename = 'appointment_' . (new DateTimeImmutable('now'))->format('Ymd_His_u') . '.ics'; $attachment = new ilFileDataMail($this->getSender()); - $effective_ics_filename = $attachment->storeAsAttachment( + $rcid = $attachment->createCollectionFromContent( $ics_filename, $export->getExportString() ); - $this->setAttachments([$effective_ics_filename]); - } - - protected function deleteAttachments(): void - { - $attachment = new ilFileDataMail($this->getSender()); - $attachment->unlinkFiles($this->getAttachments()); + $this->setAttachments( + $rcid !== null + ? MailAttachments::fromIrss($rcid) + : MailAttachments::empty() + ); } } diff --git a/components/ILIAS/Course/classes/class.ilTimingsCronReminder.php b/components/ILIAS/Course/classes/class.ilTimingsCronReminder.php index 88a264feb927..d7eeea1b2602 100755 --- a/components/ILIAS/Course/classes/class.ilTimingsCronReminder.php +++ b/components/ILIAS/Course/classes/class.ilTimingsCronReminder.php @@ -21,6 +21,7 @@ use ILIAS\Cron\Job\Schedule\JobScheduleType; use ILIAS\Cron\Job\JobResult; use ILIAS\Cron\CronJob; +use ILIAS\Mail\Attachments\MailAttachments; class ilTimingsCronReminder extends CronJob { @@ -309,7 +310,7 @@ protected function sendExceededMail(int $user_id, array $ref_ids, string $mail_b '', $this->user_lang->txt('timings_cron_reminder_exceeded_subject'), $mail_body, - [], + MailAttachments::empty(), true ); $this->log->debug('...mail send for user ' . $user_id . ' to mail ' . $login . ' has exceeded timings for ' . $mail_body); @@ -334,7 +335,7 @@ protected function sendFreshlyStartedMail(int $user_id, array $ref_ids, string $ '', $this->user_lang->txt('timings_cron_reminder_started_subject'), $mail_body, - [], + MailAttachments::empty(), true ); $this->log->debug('...mail send for user ' . $user_id . ' to mail ' . $login . ' has freshly started timings for ' . $mail_body); diff --git a/components/ILIAS/EmployeeTalk/classes/Notification/Notification.php b/components/ILIAS/EmployeeTalk/classes/Notification/Notification.php index 4158b62b48a6..ab6ab46b8451 100755 --- a/components/ILIAS/EmployeeTalk/classes/Notification/Notification.php +++ b/components/ILIAS/EmployeeTalk/classes/Notification/Notification.php @@ -20,6 +20,8 @@ namespace ILIAS\EmployeeTalk\Notification; +use ILIAS\Mail\Attachments\MailAttachments; + class Notification { protected \ilObjUser $to; @@ -117,10 +119,10 @@ public function send(): void } $attachment_name = 'appointments.ics'; - $attachment->storeAsAttachment( - $attachment_name, - $this->attachment - ); + $rcid = $attachment->createCollectionFromContent($attachment_name, $this->attachment); + $attachments = $rcid !== null + ? MailAttachments::fromIrss($rcid) + : MailAttachments::empty(); $mail = new \ilMail(ANONYMOUS_USER_ID); $mail->enqueue( @@ -129,10 +131,8 @@ public function send(): void '', $subject, $notif->composeAndGetMessage($this->to->getId(), null, '', true), - [$attachment_name] + $attachments ); - - $attachment->unlinkFile($attachment_name); } protected function getTalkGoto(): string diff --git a/components/ILIAS/Exercise/Assignment/class.ilExAssignmentReminder.php b/components/ILIAS/Exercise/Assignment/class.ilExAssignmentReminder.php index 0581655d17d6..92be6900ce82 100755 --- a/components/ILIAS/Exercise/Assignment/class.ilExAssignmentReminder.php +++ b/components/ILIAS/Exercise/Assignment/class.ilExAssignmentReminder.php @@ -16,6 +16,8 @@ * *********************************************************************/ +use ILIAS\Mail\Attachments\MailAttachments; + /** * * TODO: import/export reminder data with the exercise/assignment. @@ -561,7 +563,7 @@ protected function sendReminders(array $reminders): int "", $subject, $message, - array() + MailAttachments::empty() ); } diff --git a/components/ILIAS/Exercise/classes/class.ilObjExercise.php b/components/ILIAS/Exercise/classes/class.ilObjExercise.php index 2f5dfdf9b923..a39cdaa0b731 100755 --- a/components/ILIAS/Exercise/classes/class.ilObjExercise.php +++ b/components/ILIAS/Exercise/classes/class.ilObjExercise.php @@ -23,6 +23,8 @@ use ILIAS\Filesystem\Exception\IOException; use ILIAS\Exercise\InternalService; use ILIAS\Exercise\Assignment\Mandatory\MandatoryAssignmentsManager; +use ILIAS\Mail\Attachments\MailAttachments; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; /** * Class ilObjExercise @@ -454,17 +456,11 @@ public function sendAssignment(ilExAssignment $a_ass, array $a_members): void // instruction files $if = $this->service->domain()->assignment()->instructionFiles($a_ass->getId()); - $files = $if->getFiles(); - $file_names = []; - if (count($files) > 0) { - $mfile_obj = new ilFileDataMail($GLOBALS['DIC']['ilUser']->getId()); - foreach ($if->getFiles() as $file) { - $file_names[] = $file["name"]; - $mfile_obj->storeAsAttachment( - $file["name"], - $if->getStream($file["rid"])->getContents() - ); - } + $attachments = MailAttachments::empty(); + if ($if->getFiles() !== []) { + $attachments = MailAttachments::fromIrss( + new ResourceCollectionIdentification($if->getCollectionIdString()) + ); } // recipients @@ -485,16 +481,10 @@ public function sendAssignment(ilExAssignment $a_ass, array $a_members): void "", $subject, $body, - $file_names + $attachments ); unset($tmp_mail_obj); - // remove tmp files - if (count($file_names) && $mfile_obj) { - $mfile_obj->unlinkFiles($file_names); - unset($mfile_obj); - } - // set recipients mail status foreach ($a_members as $member_id) { $member_status = $a_ass->getMemberStatus($member_id); diff --git a/components/ILIAS/Forum/classes/Notification/class.ilForumCronNotificationDataProvider.php b/components/ILIAS/Forum/classes/Notification/class.ilForumCronNotificationDataProvider.php index bdb50c48e0d5..47b900239245 100755 --- a/components/ILIAS/Forum/classes/Notification/class.ilForumCronNotificationDataProvider.php +++ b/components/ILIAS/Forum/classes/Notification/class.ilForumCronNotificationDataProvider.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; + /** * Class ilForumCronNotificationDataProvider * @author Nadia Matuschek @@ -46,6 +49,7 @@ class ilForumCronNotificationDataProvider implements ilForumNotificationMailData private readonly string $import_name; /** @var list */ private array $attachments = []; + private ?MailAttachments $mail_attachments = null; /** @var int[] */ private array $cron_recipients = []; private readonly int $post_update_user_id; @@ -105,14 +109,24 @@ protected function read(): void private function readAttachments(): void { - if (ilForumProperties::isSendAttachmentsByMailEnabled()) { - $fileDataForum = new ilFileDataForum($this->getObjId(), $this->getPostId()); - $filesOfPost = $fileDataForum->getFilesOfPost(); + if (!ilForumProperties::isSendAttachmentsByMailEnabled()) { + return; + } + + $fileDataForum = new ilFileDataForum($this->getObjId(), $this->getPostId()); + foreach ($fileDataForum->getFilesOfPost() as $attachment) { + $this->attachments[] = $attachment['name']; + } - foreach ($filesOfPost as $attachment) { - $this->attachments[] = $attachment['name']; - } + $post = new ilForumPost($this->getPostId()); + $rcid_string = $post->getRCID(); + if ($rcid_string === '' || $rcid_string === ilForumPost::NO_RCID) { + return; } + + $this->mail_attachments = MailAttachments::fromIrss( + new ResourceCollectionIdentification($rcid_string) + ); } public function addRecipient(int $user_id): void @@ -216,6 +230,11 @@ public function getAttachments(): array return $this->attachments; } + public function getMailAttachments(): MailAttachments + { + return $this->mail_attachments ?? MailAttachments::empty(); + } + public function getPosDisplayUserId(): int { return $this->pos_display_user_id; diff --git a/components/ILIAS/Forum/classes/Notification/class.ilForumMailEventNotificationSender.php b/components/ILIAS/Forum/classes/Notification/class.ilForumMailEventNotificationSender.php index 5849107f6763..3810b29c71a4 100755 --- a/components/ILIAS/Forum/classes/Notification/class.ilForumMailEventNotificationSender.php +++ b/components/ILIAS/Forum/classes/Notification/class.ilForumMailEventNotificationSender.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * @author Niels Theen */ @@ -360,7 +362,7 @@ private function createMailValueObjectsWithAttachments( '', ilStr::strLen($subjectText) > 255 ? ilStr::substr($subjectText, 0, 255) : $subjectText, $bodyText, - $this->provider->getAttachments(), + $this->provider->getMailAttachments(), false, false ); @@ -393,7 +395,7 @@ private function createMailValueObjectWithoutAttachments( '', ilStr::strLen($subjectText) > 255 ? ilStr::substr($subjectText, 0, 255) : $subjectText, $bodyText, - [], + MailAttachments::empty(), false, false ); diff --git a/components/ILIAS/Forum/classes/Notification/class.ilForumNotificationDataProvider.php b/components/ILIAS/Forum/classes/Notification/class.ilForumNotificationDataProvider.php index 3cd624cd6d11..092ef784639d 100755 --- a/components/ILIAS/Forum/classes/Notification/class.ilForumNotificationDataProvider.php +++ b/components/ILIAS/Forum/classes/Notification/class.ilForumNotificationDataProvider.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; + /** * Class ilForumNotificationDataProvider * @author Nadia Matuschek @@ -35,6 +38,7 @@ class ilForumNotificationDataProvider implements ilForumNotificationMailData protected string $thread_title = ''; /** @var array */ protected array $attachments = []; + private ?MailAttachments $mail_attachments = null; private readonly ilDBInterface $db; private readonly ilAccessHandler $access; private readonly ilObjUser $user; @@ -148,6 +152,11 @@ public function getAttachments(): array return $this->attachments; } + public function getMailAttachments(): MailAttachments + { + return $this->mail_attachments ?? MailAttachments::empty(); + } + public function getPosUserAlias(): string { return $this->objPost->getUserAlias(); @@ -314,17 +323,23 @@ public function determineClosestContainer(int $frm_ref_id): ?ilObject private function readAttachments(): void { - if (ilForumProperties::isSendAttachmentsByMailEnabled()) { - $fileDataForum = new ilFileDataForum($this->getObjId(), $this->objPost->getId()); - $filesOfPost = $fileDataForum->getFilesOfPost(); + if (!ilForumProperties::isSendAttachmentsByMailEnabled()) { + return; + } - $fileDataMail = new ilFileDataMail(ANONYMOUS_USER_ID); + $fileDataForum = new ilFileDataForum($this->getObjId(), $this->objPost->getId()); + foreach ($fileDataForum->getFilesOfPost() as $attachment) { + $this->attachments[$attachment['path']] = $attachment['name']; + } - foreach ($filesOfPost as $attachment) { - $this->attachments[$attachment['path']] = $attachment['name']; - $fileDataMail->copyAttachmentFile($attachment['path'], $attachment['name']); - } + $rcid_string = $this->objPost->getRCID(); + if ($rcid_string === '' || $rcid_string === ilForumPost::NO_RCID) { + return; } + + $this->mail_attachments = MailAttachments::fromIrss( + new ResourceCollectionIdentification($rcid_string) + ); } /** diff --git a/components/ILIAS/Forum/classes/class.ilForumMailNotification.php b/components/ILIAS/Forum/classes/class.ilForumMailNotification.php index 808c0575b6e0..aea3f5fa15db 100755 --- a/components/ILIAS/Forum/classes/class.ilForumMailNotification.php +++ b/components/ILIAS/Forum/classes/class.ilForumMailNotification.php @@ -77,7 +77,7 @@ protected function appendAttachments(): void $this->appendBody($this->getLanguageText('attachment') . ': ' . $attachment . "\n"); } $this->appendBody("\n------------------------------------------------------------\n"); - $this->setAttachments($this->provider->getAttachments()); + $this->setAttachments($this->provider->getMailAttachments()); } } diff --git a/components/ILIAS/Forum/interfaces/interface.ilForumNotificationMailData.php b/components/ILIAS/Forum/interfaces/interface.ilForumNotificationMailData.php index 191ad0b26bef..cd28220a4d8f 100755 --- a/components/ILIAS/Forum/interfaces/interface.ilForumNotificationMailData.php +++ b/components/ILIAS/Forum/interfaces/interface.ilForumNotificationMailData.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * Interface ilForumNotificationMailData * @author Nadia Matuschek @@ -71,5 +73,7 @@ public function getCensorshipComment(): string; public function getAttachments(): array; + public function getMailAttachments(): MailAttachments; + public function getDeletedBy(): string; } diff --git a/components/ILIAS/LearningModule/classes/class.ilLearningModuleNotification.php b/components/ILIAS/LearningModule/classes/class.ilLearningModuleNotification.php index ee135bcbeebd..737d93067a66 100755 --- a/components/ILIAS/LearningModule/classes/class.ilLearningModuleNotification.php +++ b/components/ILIAS/LearningModule/classes/class.ilLearningModuleNotification.php @@ -16,6 +16,8 @@ * *********************************************************************/ +use ILIAS\Mail\Attachments\MailAttachments; + /** * Class ilLearningModuleNotification class * @@ -109,7 +111,7 @@ public function send(): void "", $subject, $message, - [] + MailAttachments::empty() ); } } diff --git a/components/ILIAS/Mail/README.md b/components/ILIAS/Mail/README.md index 5d6a7d44a418..101b094c31e1 100755 --- a/components/ILIAS/Mail/README.md +++ b/components/ILIAS/Mail/README.md @@ -159,7 +159,7 @@ $cc = 'john.doe'; $bc = 'max.mustermann'; $subject = 'Make ILIAS great again!'; $message = "Lorem ipsum dolor sit amet,\nconsetetur sadipscing elitr,\nsed diam nonumy eirmod tempor."; -$attachments = []; +$attachments = MailAttachments::empty(); $mail = new \ilMail($senderUserId); $mail->enqueue( @@ -385,10 +385,10 @@ $mail->enqueue( $bc, $subject, $message, - [ + MailAttachments::fromLegacyFilenames([ $attachment_filename, 'HelloWorld.jpg' - ] + ]) ); // or $attachment->unlinkFiles(['HelloWorld.jpg', $attachment_filename]); diff --git a/components/ILIAS/Mail/classes/Attachments/FileDataRCHandling.php b/components/ILIAS/Mail/classes/Attachments/FileDataRCHandling.php index 8e5d242d0b73..4b5ea7748637 100644 --- a/components/ILIAS/Mail/classes/Attachments/FileDataRCHandling.php +++ b/components/ILIAS/Mail/classes/Attachments/FileDataRCHandling.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\Filesystem\Stream\Streams; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; -use ILIAS\ResourceStorage\Collection\ResourceCollection; +use ILIAS\ResourceStorage\Identification\ResourceIdentification; trait FileDataRCHandling { @@ -28,111 +28,122 @@ trait FileDataRCHandling * @param list $path_to_files */ protected function getCurrentCollection( - array $path_to_files, - ilMailAttachmentStakeholder $stakeholder + array $path_to_files ): \ILIAS\ResourceStorage\Collection\ResourceCollection { - set_error_handler(static function ($severity, $message, $file, $line): void { - throw new ErrorException($message, $severity, 0, $file, $line); - }); - - try { - global $DIC; - $file_system = $DIC->filesystem()->storage(); - $rcid = $this->storage->collection()->id(); - $collection = $this->storage->collection()->get($rcid); - foreach ($path_to_files as $path_to_file) { - $base_dir = (new \ILIAS\FileDelivery\Setup\BaseDirObjective())::get(); - $path_to_file = str_replace($base_dir, '/', $path_to_file); - $rid = $this->storage->manage()->stream( - $file_system->readStream($path_to_file), - $stakeholder, - md5(basename($path_to_file)) - ); - $collection->add($rid); - } - $this->storage->collection()->store($collection); - } catch (Exception $e) { - throw new Exception("Storing file into collection failed: " . $e->getMessage()); - } finally { - restore_error_handler(); + $rcid = $this->fdm->createCollectionFromPaths($path_to_files); + if ($rcid === null) { + throw new Exception('Storing file into collection failed: no files found'); } - return $collection; + return $this->fdm->getCollection($rcid); } + + /** - * @param array $mail_data * @return list */ - public function filesFromLegacyToIRSS(array $mail_data): array + public function FilesFromIRSSToLegacy(ResourceCollectionIdentification $identification): array { - $files = []; - $path_to_files = []; - foreach ($mail_data['attachments'] as $file) { - $path_to_files[] = $this->fdm->getAbsoluteAttachmentPoolPathByFilename($file); + return $this->fdm->getRidsFromCollection($identification); + } + + /** + * @param list $form_attachment_rids + */ + protected function attachmentsFromFormUpload( + array $form_attachment_rids, + ?MailAttachments $stage_attachments = null + ): MailAttachments { + if ($form_attachment_rids === []) { + return $stage_attachments ?? MailAttachments::empty(); } - $collection = $this->getCurrentCollection($path_to_files, new ilMailAttachmentStakeholder()); - foreach ($collection->getResourceIdentifications() as $rcid) { - $files[] = $rcid->serialize(); + + $resource_identifications = []; + foreach ($form_attachment_rids as $attachment) { + $found = $this->storage->manage()->find($attachment); + if ($found === null) { + continue; + } + $resource_identifications[] = $found; + } + + if ($resource_identifications === []) { + return $stage_attachments ?? MailAttachments::empty(); + } + + $stage_rcid = ($stage_attachments instanceof MailAttachments && $stage_attachments->isIrss()) + ? $stage_attachments->rcid() + : null; + + if ($stage_rcid !== null && $this->fdm->collectionContainsResources($stage_rcid, $resource_identifications)) { + return MailAttachments::fromIrss($stage_rcid); } - return $files; + return MailAttachments::fromIrss( + $this->fdm->createCollectionFromResourceIdentifications($resource_identifications) + ); } /** - * @param array $mail_data + * @param array $attachments */ - public function getIdforCollection(array $mail_data): ?ResourceCollectionIdentification + protected function handleAttachments(array $attachments): ResourceCollectionIdentification { - $files = []; - $path_to_files = []; - foreach ($mail_data as $attachment) { - $path_to_files[] = $this->fdm->getAbsoluteAttachmentPoolPathByFilename($attachment); + $resource_identifications = []; + foreach ($attachments as $attachment) { + $info = $this->upload_handler->getInfoResult($attachment); + if ($info->getFileIdentifier() === 'unknown') { + continue; + } + $found = $this->storage->manage()->find($attachment); + if ($found === null) { + throw new Exception("File '" . $info->getName() . "' could not be found in IRSS"); + } + $resource_identifications[] = $found; + } + + if ($resource_identifications === []) { + throw new Exception('No attachments could be stored'); } - $collection = $this->getCurrentCollection($path_to_files, new ilMailAttachmentStakeholder()); - $rcid = $collection->getIdentification(); - return $rcid; + return $this->fdm->createCollectionFromResourceIdentifications($resource_identifications); } - /** - * @return list - */ - public function FilesFromIRSSToLegacy(ResourceCollectionIdentification $identification): array + protected function stageAttachmentsFromMailAttachments(MailAttachments $attachments): MailAttachments { - $files = []; - $collection = $this->storage->collection()->get($identification); - $all_ids = $collection->getResourceIdentifications(); - foreach ($all_ids as $id) { - $files[] = $id->serialize(); + if ($attachments->isEmpty()) { + return MailAttachments::empty(); } - return $files; + if ($attachments->isIrss()) { + $foreign = iterator_to_array( + $this->fdm->getCollection($attachments->rcid())->getResourceIdentifications(), + false + ); + $cloned_rcid = $this->fdm->createCollectionFromForeignResources($foreign); + + return $cloned_rcid !== null + ? MailAttachments::fromIrss($cloned_rcid) + : MailAttachments::empty(); + } + + $rcid = $this->fdm->createCollectionFromPoolFilenames($attachments->legacyFilenames()); + + return $rcid !== null + ? MailAttachments::fromIrss($rcid) + : MailAttachments::empty(); } /** - * @param array $attachments * @return list */ - protected function handleAttachments(array $attachments): array + protected function formRidsFromMailAttachments(MailAttachments $attachments): array { - $files = []; - foreach ($attachments as $attachment) { - $info = $this->upload_handler->getInfoResult($attachment); - if ($info->getFileIdentifier() !== 'unknown') { - $src = $this->upload_handler->getStreamConsumer($attachment); - $stored = $this->fdm->storeAsAttachment( - $info->getName(), - (string) $src->getStream() - ); - if ($stored === false) { - throw new Exception("File '" . $info->getName() . "' could not be stored"); - } - $files[] = ilFileUtils::_sanitizeFilemame($info->getName()); - $this->upload_handler->removeFileForIdentifier($attachment); - } + if ($attachments->isIrss()) { + return $this->FilesFromIRSSToLegacy($attachments->rcid()); } - return $files; + return []; } } diff --git a/components/ILIAS/Mail/classes/Attachments/MailAttachmentTableGUI.php b/components/ILIAS/Mail/classes/Attachments/MailAttachmentTableGUI.php index 95513b69b037..65eaf292a892 100644 --- a/components/ILIAS/Mail/classes/Attachments/MailAttachmentTableGUI.php +++ b/components/ILIAS/Mail/classes/Attachments/MailAttachmentTableGUI.php @@ -31,7 +31,7 @@ class MailAttachmentTableGUI implements \ILIAS\UI\Component\Table\DataRetrieval, private readonly \ILIAS\Data\Factory $data_factory; /** - * @param list $records + * @param list $records */ public function __construct( private readonly \ilMailAttachmentGUI $parent_gui, @@ -141,7 +141,7 @@ private function getActions(): array } /** - * @return list + * @return list */ private function getRecords(\ILIAS\Data\Range $range, \ILIAS\Data\Order $order): array { @@ -185,7 +185,7 @@ public function getRows( ]; yield $row_builder - ->buildDataRow(urlencode($record['filename']), $record); + ->buildDataRow($item['rid'], $record); } } diff --git a/components/ILIAS/Mail/classes/Attachments/MailAttachments.php b/components/ILIAS/Mail/classes/Attachments/MailAttachments.php new file mode 100644 index 000000000000..8499fd933354 --- /dev/null +++ b/components/ILIAS/Mail/classes/Attachments/MailAttachments.php @@ -0,0 +1,157 @@ + */ + private readonly array $legacy_filenames, + ) { + } + + public static function empty(): self + { + return new self(null, []); + } + + public static function fromIrss(ResourceCollectionIdentification $rcid): self + { + return new self($rcid, []); + } + + /** + * @param list $filenames + */ + public static function fromLegacyFilenames(array $filenames): self + { + if ($filenames === []) { + return self::empty(); + } + + return new self(null, array_values($filenames)); + } + + public static function fromDb(?string $raw): ?self + { + if ($raw === null || $raw === '') { + return null; + } + + if (str_contains($raw, 'a:')) { + $unserialized = unserialize($raw, ['allowed_classes' => false]); + if (!is_array($unserialized)) { + return null; + } + + if (isset($unserialized[self::SERIALIZED_RCID_KEY])) { + return self::fromIrss( + new ResourceCollectionIdentification((string) $unserialized[self::SERIALIZED_RCID_KEY]) + ); + } + + return self::fromLegacyFilenames($unserialized); + } + + return self::fromIrss(new ResourceCollectionIdentification($raw)); + } + + public static function fromBackgroundTask(string $serialized): self + { + $parsed = unserialize($serialized, ['allowed_classes' => false]); + if (!is_array($parsed)) { + return self::empty(); + } + + if (isset($parsed[self::SERIALIZED_RCID_KEY])) { + return self::fromIrss( + new ResourceCollectionIdentification((string) $parsed[self::SERIALIZED_RCID_KEY]) + ); + } + + return self::fromLegacyFilenames($parsed); + } + + public function isEmpty(): bool + { + return $this->rcid === null && $this->legacy_filenames === []; + } + + public function isIrss(): bool + { + return $this->rcid !== null; + } + + public function isLegacy(): bool + { + return $this->rcid === null && $this->legacy_filenames !== []; + } + + public function rcid(): ResourceCollectionIdentification + { + if ($this->rcid === null) { + throw new InvalidArgumentException('Mail attachments are not stored in IRSS.'); + } + + return $this->rcid; + } + + /** + * @return list + */ + public function legacyFilenames(): array + { + if ($this->isIrss()) { + throw new InvalidArgumentException('Mail attachments are not legacy filenames.'); + } + + return $this->legacy_filenames; + } + + public function toDb(): string + { + if ($this->isIrss()) { + return $this->rcid->serialize(); + } + + return serialize($this->legacy_filenames); + } + + public function toBackgroundTask(): string + { + if ($this->isIrss()) { + return serialize([self::SERIALIZED_RCID_KEY => $this->rcid->serialize()]); + } + + return serialize($this->legacy_filenames); + } + + public function stageRcidOrNull(): ?ResourceCollectionIdentification + { + return $this->rcid; + } +} diff --git a/components/ILIAS/Mail/classes/BackgroundTask/class.ilMailDeliveryJob.php b/components/ILIAS/Mail/classes/BackgroundTask/class.ilMailDeliveryJob.php index 24f51d6f433d..a6c885983418 100755 --- a/components/ILIAS/Mail/classes/BackgroundTask/class.ilMailDeliveryJob.php +++ b/components/ILIAS/Mail/classes/BackgroundTask/class.ilMailDeliveryJob.php @@ -26,6 +26,7 @@ use ILIAS\BackgroundTasks\Types\SingleType; use ILIAS\BackgroundTasks\Types\Type; use ILIAS\BackgroundTasks\Value; +use ILIAS\Mail\Attachments\MailAttachments; class ilMailDeliveryJob extends AbstractJob { @@ -69,7 +70,7 @@ public function run(array $input, Observer $observer): Value (string) $input[3]->getValue(), // Bcc (string) $input[4]->getValue(), // Subject (string) $input[5]->getValue(), // Message - (array) unserialize($input[6]->getValue(), ['allowed_classes' => false]), // Attachments + MailAttachments::fromBackgroundTask($input[6]->getValue()), // Attachments (bool) $input[7]->getValue() // Use Placeholders ); $mail->sendMail($mail_data); diff --git a/components/ILIAS/Mail/classes/BackgroundTask/class.ilMassMailDeliveryJob.php b/components/ILIAS/Mail/classes/BackgroundTask/class.ilMassMailDeliveryJob.php index d97d5ddbe707..962d21f76f2f 100755 --- a/components/ILIAS/Mail/classes/BackgroundTask/class.ilMassMailDeliveryJob.php +++ b/components/ILIAS/Mail/classes/BackgroundTask/class.ilMassMailDeliveryJob.php @@ -48,6 +48,7 @@ public function run(array $input, Observer $observer): Value $mail = new ilMail((int) $input[0]->getValue()); $mail->setSaveInSentbox($value_object->shouldSaveInSentBox()); + $mail->setShareAttachments(true); $context_id = $input[2]->getValue(); $mail = $mail ->withContextId((string) $context_id) diff --git a/components/ILIAS/Mail/classes/Cron/ExpiredOrOrphanedMails/MailDeletionHandler.php b/components/ILIAS/Mail/classes/Cron/ExpiredOrOrphanedMails/MailDeletionHandler.php index 8faac45a6718..218ecc60ee62 100755 --- a/components/ILIAS/Mail/classes/Cron/ExpiredOrOrphanedMails/MailDeletionHandler.php +++ b/components/ILIAS/Mail/classes/Cron/ExpiredOrOrphanedMails/MailDeletionHandler.php @@ -31,7 +31,9 @@ use Throwable; use RecursiveIteratorIterator; use RecursiveDirectoryIterator; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; use SplFileInfo; +use ilFileDataMail; class MailDeletionHandler { @@ -41,6 +43,7 @@ class MailDeletionHandler private readonly ilSetting $settings; private readonly ilLogger $logger; private readonly ilDBStatement $mail_ids_for_path_stmt; + private readonly ilDBStatement $mail_ids_for_rcid_stmt; /** @var callable(string): void|null */ private $delete_directory_callback; @@ -53,7 +56,7 @@ public function __construct( ?ilDBInterface $db = null, ?ilSetting $setting = null, ?ilLogger $logger = null, - ?callable $delete_directory_callback = null + ?callable $delete_directory_callback = null, ) { global $DIC; @@ -66,6 +69,46 @@ public function __construct( 'SELECT COUNT(*) cnt FROM mail_attachment WHERE path = ?', [ilDBConstants::T_TEXT] ); + $this->mail_ids_for_rcid_stmt = $this->db->prepare( + 'SELECT COUNT(*) cnt FROM mail_attachment WHERE rcid = ?', + [ilDBConstants::T_TEXT] + ); + } + + /** + * @return list + */ + private function determineDeletableAttachmentRcids(): array + { + $rcids = []; + $res = $this->db->query( + ' + SELECT rcid, COUNT(mail_id) cnt_mail_ids + FROM mail_attachment + WHERE rcid IS NOT NULL AND rcid != "" AND rcid != "-" + AND ' . $this->db->in( + 'mail_id', + $this->collector->mailIdsToDelete(), + false, + ilDBConstants::T_INTEGER + ) . ' GROUP BY rcid' + ); + + while ($row = $this->db->fetchAssoc($res)) { + $num_usages_total = (int) $this->db->fetchAssoc( + $this->db->execute( + $this->mail_ids_for_rcid_stmt, + [$row['rcid']] + ) + )['cnt']; + $num_usages_within_deleted_mails = (int) $row['cnt_mail_ids']; + + if ($num_usages_within_deleted_mails >= $num_usages_total) { + $rcids[] = $row['rcid']; + } + } + + return $rcids; } /** @@ -122,6 +165,15 @@ private function deleteDirectory(string $directory): void private function deleteAttachments(): void { + $mail_file_data = new ilFileDataMail(); + foreach ($this->determineDeletableAttachmentRcids() as $rcid) { + try { + $mail_file_data->removeCollection(new ResourceCollectionIdentification($rcid)); + } catch (Throwable $e) { + $this->logger->warning($e->getMessage()); + } + } + $attachment_paths = $this->determineDeletableAttachmentPaths(); $i = 0; diff --git a/components/ILIAS/Mail/classes/Folder/OutboxDatabaseRepository.php b/components/ILIAS/Mail/classes/Folder/OutboxDatabaseRepository.php index 0fed31891775..64f5aca1cadf 100644 --- a/components/ILIAS/Mail/classes/Folder/OutboxDatabaseRepository.php +++ b/components/ILIAS/Mail/classes/Folder/OutboxDatabaseRepository.php @@ -20,6 +20,7 @@ namespace ILIAS\Mail\Folder; +use ILIAS\Mail\Attachments\MailAttachments; use Generator; use DateTimeZone; use ilDBConstants; @@ -78,7 +79,7 @@ public function getOutboxMails(): Generator $row['rcp_bcc'], $row['m_subject'], $row['m_message'], - $row['attachments'], + $row['attachments'] ?? MailAttachments::empty(), (bool) ($row['use_placeholders'] ?? false), isset($row['mail_id']) ? (int) $row['mail_id'] : null ); diff --git a/components/ILIAS/Mail/classes/MailDeliveryData.php b/components/ILIAS/Mail/classes/MailDeliveryData.php index 26c42c8eb2a1..bfe3e3a24fbb 100755 --- a/components/ILIAS/Mail/classes/MailDeliveryData.php +++ b/components/ILIAS/Mail/classes/MailDeliveryData.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + final class MailDeliveryData { public function __construct( @@ -26,7 +28,7 @@ public function __construct( private readonly string $bcc, private readonly string $subject, private readonly string $message, - private readonly array $attachments, + private MailAttachments $attachments, private readonly bool $use_placeholder, private ?int $internal_mail_id = null ) { @@ -57,7 +59,7 @@ public function getMessage(): string return $this->message; } - public function getAttachments(): array + public function getAttachments(): MailAttachments { return $this->attachments; } @@ -78,4 +80,11 @@ public function withInternalMailId(int $internal_mail_id): MailDeliveryData $clone->internal_mail_id = $internal_mail_id; return $clone; } + + public function withAttachments(MailAttachments $attachments): MailDeliveryData + { + $clone = clone $this; + $clone->attachments = $attachments; + return $clone; + } } diff --git a/components/ILIAS/Mail/classes/Message/MailBoxQuery.php b/components/ILIAS/Mail/classes/Message/MailBoxQuery.php index b8a8ba9feeb5..6f4244bb6f50 100755 --- a/components/ILIAS/Mail/classes/Message/MailBoxQuery.php +++ b/components/ILIAS/Mail/classes/Message/MailBoxQuery.php @@ -26,7 +26,7 @@ use DateTimeZone; use ILIAS\Data\Order; use ilUserSearchOptions; -use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; +use ILIAS\Mail\Attachments\MailAttachments; class MailBoxQuery { @@ -51,9 +51,8 @@ class MailBoxQuery private MailBoxOrderColumn $order_column = self::DEFAULT_ORDER_COLUMN; private string $order_direction = self::DEFAULT_ORDER_DIRECTION; - public function __construct( - private readonly int $user_id, - ) { + public function __construct(private readonly int $user_id) + { global $DIC; $this->db = $DIC->database(); } @@ -278,11 +277,8 @@ public function query($short): array $set = []; while ($row = $this->db->fetchAssoc($res)) { - if (isset($row['attachments']) && \is_string($row['attachments']) && str_contains($row['attachments'], '{')) { - $unserialized_attachments = unserialize($row['attachments'], ['allowed_classes' => false]); - $row['attachments'] = \is_array($unserialized_attachments) ? $unserialized_attachments : null; - } elseif (isset($row['attachments']) && \is_string($row['attachments']) && $row['attachments'] !== '') { - $row['attachments'] = new ResourceCollectionIdentification($row['attachments']); + if (isset($row['attachments']) && is_string($row['attachments'])) { + $row['attachments'] = MailAttachments::fromDb($row['attachments']); } else { $row['attachments'] = null; } diff --git a/components/ILIAS/Mail/classes/Message/MailRecordData.php b/components/ILIAS/Mail/classes/Message/MailRecordData.php index 53bc3feb3e64..3e27d05a7806 100644 --- a/components/ILIAS/Mail/classes/Message/MailRecordData.php +++ b/components/ILIAS/Mail/classes/Message/MailRecordData.php @@ -21,7 +21,7 @@ namespace ILIAS\Mail\Message; use DateTimeImmutable; -use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; +use ILIAS\Mail\Attachments\MailAttachments; class MailRecordData { @@ -29,7 +29,7 @@ class MailRecordData public const string STATUS_UNREAD = 'unread'; /** - * @param null|non-empty-list|ResourceCollectionIdentification $attachments + * @param null|MailAttachments $attachments */ public function __construct( private readonly int $mail_id, @@ -45,7 +45,7 @@ public function __construct( private readonly ?string $rcp_to = null, private readonly ?string $rcp_cc = null, private readonly ?string $rcp_bc = null, - private readonly null|array|ResourceCollectionIdentification $attachments = null, + private readonly ?MailAttachments $attachments = null, private readonly ?string $tpl_ctx_id = null, private readonly ?string $tpl_ctx_params = null ) { @@ -116,10 +116,7 @@ public function getRcpBc(): ?string return $this->rcp_bc; } - /** - * @return null|non-empty-list|ResourceCollectionIdentification - */ - public function getAttachments(): null|array|ResourceCollectionIdentification + public function getAttachments(): ?MailAttachments { return $this->attachments; } @@ -141,7 +138,7 @@ public function isRead(): bool public function hasAttachments(): bool { - return !empty($this->attachments); + return $this->attachments !== null && !$this->attachments->isEmpty(); } public function hasPersonalSender(): bool diff --git a/components/ILIAS/Mail/classes/Mime/MailMimeAttachment.php b/components/ILIAS/Mail/classes/Mime/MailMimeAttachment.php new file mode 100644 index 000000000000..768d8841f49e --- /dev/null +++ b/components/ILIAS/Mail/classes/Mime/MailMimeAttachment.php @@ -0,0 +1,95 @@ +resource_identification !== null; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getResourceIdentification(): ?ResourceIdentification + { + return $this->resource_identification; + } + + public function getMimeType(): string + { + return $this->mime_type; + } + + public function getDisposition(): string + { + return $this->disposition; + } + + public function getDisplayName(): string + { + return $this->display_name; + } +} diff --git a/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportBase.php b/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportBase.php index 97eab62834c2..a33d2f725dc1 100755 --- a/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportBase.php +++ b/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportBase.php @@ -19,13 +19,18 @@ declare(strict_types=1); use PHPMailer\PHPMailer\PHPMailer; +use ILIAS\Mail\Mime\MailMimeAttachment; +use ILIAS\ResourceStorage\Services as IRSS; abstract class ilMailMimeTransportBase implements ilMailMimeTransport { protected PHPMailer $mailer; - public function __construct(protected ilSetting $settings, private readonly ilAppEventHandler $event_handler) - { + public function __construct( + protected ilSetting $settings, + private readonly ilAppEventHandler $event_handler, + private readonly IRSS $irss + ) { $mail = new PHPMailer(); $this->setMailer($mail); } @@ -101,8 +106,8 @@ final public function send(ilMimeMail $mail): bool ilLoggerFactory::getLogger('mail')->warning('{error}', ['error' => $this->getMailer()->ErrorInfo]); } - foreach ($mail->getAttachments() as $attachment) { - if (!$this->getMailer()->addAttachment($attachment['path'], $attachment['name'])) { + foreach ($mail->getMimeAttachments() as $attachment) { + if (!$this->addMimeAttachment($attachment)) { ilLoggerFactory::getLogger('mail')->warning('{error}', ['error' => $this->getMailer()->ErrorInfo]); } } @@ -187,4 +192,44 @@ final public function send(ilMimeMail $mail): bool return $result; } + + private function addMimeAttachment(MailMimeAttachment $attachment): bool + { + if ($attachment->isResource()) { + return $this->addResourceAttachment($attachment); + } + + $path = $attachment->getPath(); + if ($path === null) { + return false; + } + + return $this->getMailer()->addAttachment( + $path, + $attachment->getDisplayName(), + PHPMailer::ENCODING_BASE64, + $attachment->getMimeType(), + $attachment->getDisposition() + ); + } + + private function addResourceAttachment(MailMimeAttachment $attachment): bool + { + $resource_identification = $attachment->getResourceIdentification(); + if ($resource_identification === null) { + return false; + } + + $stream = $this->irss->consume()->stream($resource_identification)->getStream(); + + return $this->getMailer()->addStringAttachment( + (string) $stream, + $attachment->getDisplayName() !== '' + ? $attachment->getDisplayName() + : $resource_identification->serialize(), + PHPMailer::ENCODING_BASE64, + $attachment->getMimeType(), + $attachment->getDisposition() + ); + } } diff --git a/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportFactory.php b/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportFactory.php index e9ea446223fc..f56d08a32c00 100755 --- a/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportFactory.php +++ b/components/ILIAS/Mail/classes/Mime/Transport/class.ilMailMimeTransportFactory.php @@ -18,10 +18,15 @@ declare(strict_types=1); +use ILIAS\ResourceStorage\Services as IRSS; + class ilMailMimeTransportFactory { - public function __construct(protected ilSetting $settings, private readonly ilAppEventHandler $event_handler) - { + public function __construct( + protected ilSetting $settings, + private readonly ilAppEventHandler $event_handler, + private readonly IRSS $irss + ) { } public function getTransport(): ilMailMimeTransport @@ -31,9 +36,9 @@ public function getTransport(): ilMailMimeTransport } if ($this->settings->get('mail_smtp_status', '0')) { - return new ilMailMimeTransportSmtp($this->settings, $this->event_handler); + return new ilMailMimeTransportSmtp($this->settings, $this->event_handler, $this->irss); } - return new ilMailMimeTransportSendmail($this->settings, $this->event_handler); + return new ilMailMimeTransportSendmail($this->settings, $this->event_handler, $this->irss); } } diff --git a/components/ILIAS/Mail/classes/Object/class.ilMailValueObject.php b/components/ILIAS/Mail/classes/Object/class.ilMailValueObject.php index e7dbc3e223bf..de1236a63fd7 100755 --- a/components/ILIAS/Mail/classes/Object/class.ilMailValueObject.php +++ b/components/ILIAS/Mail/classes/Object/class.ilMailValueObject.php @@ -18,13 +18,13 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + class ilMailValueObject { - /** @var list */ - private readonly array $attachments; + private readonly MailAttachments $attachments; /** - * @param list $attachments * @throws InvalidArgumentException */ public function __construct( @@ -34,14 +34,14 @@ public function __construct( private readonly string $recipients_bcc, private readonly string $subject, private readonly string $body, - array $attachments, + ?MailAttachments $attachments = null, private readonly bool $use_placeholders = false, private readonly bool $save_in_sent_box = false ) { if (ilStr::strLen($this->subject) > 255) { throw new InvalidArgumentException('Subject must not be longer than 255 characters'); } - $this->attachments = array_filter(array_map('trim', $attachments)); + $this->attachments = $attachments ?? MailAttachments::empty(); } public function getRecipients(): string @@ -69,10 +69,7 @@ public function getBody(): string return $this->body; } - /** - * @return list - */ - public function getAttachments(): array + public function getAttachments(): MailAttachments { return $this->attachments; } diff --git a/components/ILIAS/Mail/classes/Object/class.ilMailValueObjectJsonService.php b/components/ILIAS/Mail/classes/Object/class.ilMailValueObjectJsonService.php index 2e02d8dfd1d3..080455b3bfab 100755 --- a/components/ILIAS/Mail/classes/Object/class.ilMailValueObjectJsonService.php +++ b/components/ILIAS/Mail/classes/Object/class.ilMailValueObjectJsonService.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + class ilMailValueObjectJsonService { /** @@ -32,7 +34,7 @@ public function convertToJson(array $mail_value_objects): string $mail_data['recipients'] = $mail_value_object->getRecipients(); $mail_data['recipients_cc'] = $mail_value_object->getRecipientsCC(); $mail_data['recipients_bcc'] = $mail_value_object->getRecipientsBCC(); - $mail_data['attachments'] = $mail_value_object->getAttachments(); + $mail_data['attachments'] = $mail_value_object->getAttachments()->toBackgroundTask(); $mail_data['body'] = $mail_value_object->getBody(); $mail_data['subject'] = $mail_value_object->getSubject(); $mail_data['is_using_placholders'] = $mail_value_object->isUsingPlaceholders(); @@ -60,7 +62,7 @@ public function convertFromJson(string $json): array $object_values['recipients_bcc'], ilStr::strLen($object_values['subject']) > 255 ? ilStr::substr($object_values['subject'], 0, 255) : $object_values['subject'], $object_values['body'], - $object_values['attachments'], + $this->attachmentsFromJson($object_values['attachments'] ?? null), $object_values['is_using_placholders'], $object_values['should_save_in_sent_box'] ); @@ -68,4 +70,17 @@ public function convertFromJson(string $json): array return $result; } + + private function attachmentsFromJson(mixed $attachments): MailAttachments + { + if (is_string($attachments)) { + return MailAttachments::fromBackgroundTask($attachments); + } + + if (is_array($attachments)) { + return MailAttachments::fromLegacyFilenames($attachments); + } + + return MailAttachments::empty(); + } } diff --git a/components/ILIAS/Mail/classes/Service/MailService.php b/components/ILIAS/Mail/classes/Service/MailService.php index 44e029ffa7d7..17b09458c4a9 100755 --- a/components/ILIAS/Mail/classes/Service/MailService.php +++ b/components/ILIAS/Mail/classes/Service/MailService.php @@ -63,7 +63,7 @@ public static function init(Container $container): void }; $container['mail.mime.transport.factory'] = static function (Container $c): ilMailMimeTransportFactory { - return new ilMailMimeTransportFactory($c->settings(), $c->event()); + return new ilMailMimeTransportFactory($c->settings(), $c->event(), $c->resourceStorage()); }; $container['mail.mime.sender.factory'] = static function (Container $c): ilMailMimeSenderFactory { diff --git a/components/ILIAS/Mail/classes/Setup/MailDBUpdateSteps11.php b/components/ILIAS/Mail/classes/Setup/MailDBUpdateSteps11.php index 5a476ae33cc7..f9154cd088de 100644 --- a/components/ILIAS/Mail/classes/Setup/MailDBUpdateSteps11.php +++ b/components/ILIAS/Mail/classes/Setup/MailDBUpdateSteps11.php @@ -68,4 +68,21 @@ public function step_2(): void ); } } + + public function step_3(): void + { + if (!$this->db->tableColumnExists('mail_attachment', 'rcid')) { + $this->db->addTableColumn( + 'mail_attachment', + 'rcid', + [ + 'type' => ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => false, + 'default' => null, + ] + ); + } + } + } diff --git a/components/ILIAS/Mail/classes/Setup/Migration/MigrateMailAttachmentsToIRSS.php b/components/ILIAS/Mail/classes/Setup/Migration/MigrateMailAttachmentsToIRSS.php new file mode 100644 index 000000000000..7423fecee77e --- /dev/null +++ b/components/ILIAS/Mail/classes/Setup/Migration/MigrateMailAttachmentsToIRSS.php @@ -0,0 +1,294 @@ +helper = new ilResourceStorageMigrationHelper( + new ilMailAttachmentStakeholder(), + $environment + ); + } + + public function step(Environment $environment): void + { + $this->migrateSentAttachmentDirectories(); + $this->migrateSerializedMailAttachments(); + } + + private function migrateSentAttachmentDirectories(): void + { + $db = $this->helper->getDatabase(); + $res = $db->query( + 'SELECT path FROM mail_attachment + WHERE (rcid IS NULL OR rcid = "") + AND path IS NOT NULL AND path != "" + GROUP BY path + LIMIT ' . self::PATHS_PER_STEP + ); + + $mail_path = rtrim($this->helper->getClientDataDir(), '/') . '/mail'; + + while ($row = $db->fetchObject($res)) { + $relative_path = (string) $row->path; + $absolute_path = $mail_path . '/' . $relative_path; + + if (!is_dir($absolute_path)) { + $this->markPathAsSkipped($relative_path); + + continue; + } + + $owner_id = $this->resolveOwnerIdForPath($relative_path); + $rcid = $this->helper->moveFilesOfPathToCollection( + $absolute_path, + $owner_id, + $owner_id + ); + + if ($rcid === null) { + $this->markPathAsSkipped($relative_path); + + continue; + } + + $this->assignRcidToPath($relative_path, $rcid); + $this->updateMailAttachmentFields($relative_path, $rcid); + } + } + + private function migrateSerializedMailAttachments(): void + { + $db = $this->helper->getDatabase(); + $res = $db->query( + 'SELECT mail_id, user_id, attachments FROM mail + WHERE attachments LIKE ' . $db->quote('a:%', 'text') . ' + LIMIT ' . self::MAILS_PER_STEP + ); + + $mail_path = rtrim($this->helper->getClientDataDir(), '/') . '/mail'; + + while ($row = $db->fetchObject($res)) { + $attachments = MailAttachments::fromDb((string) $row->attachments); + if ($attachments === null || !$attachments->isLegacy()) { + $this->clearMailAttachmentsColumn((int) $row->mail_id); + + continue; + } + + $rcid = $this->migratePoolFilenamesToCollection( + $attachments->legacyFilenames(), + (int) $row->user_id, + $mail_path + ); + + if ($rcid === null) { + $this->clearMailAttachmentsColumn((int) $row->mail_id); + + continue; + } + + $db->update( + 'mail', + [ + 'attachments' => [ilDBConstants::T_CLOB, $rcid->serialize()], + ], + [ + 'mail_id' => [ilDBConstants::T_INTEGER, (int) $row->mail_id], + ] + ); + } + } + + /** + * @param list $filenames + */ + private function migratePoolFilenamesToCollection( + array $filenames, + int $user_id, + string $mail_path + ): ?ResourceCollectionIdentification { + $collection = $this->helper->getCollectionBuilder()->new($user_id); + + foreach ($filenames as $filename) { + $absolute_path = $mail_path . '/' . $user_id . '_' . $filename; + if (!is_file($absolute_path)) { + continue; + } + + $resource_id = $this->helper->movePathToStorage( + $absolute_path, + $user_id, + null, + static fn(): string => md5($filename) + ); + + if ($resource_id instanceof ResourceIdentification) { + $collection->add($resource_id); + } + } + + if ($collection->count() === 0) { + return null; + } + + if (!$this->helper->getCollectionBuilder()->store($collection)) { + return null; + } + + return $collection->getIdentification(); + } + + private function clearMailAttachmentsColumn(int $mail_id): void + { + $this->helper->getDatabase()->update( + 'mail', + [ + 'attachments' => [ilDBConstants::T_CLOB, ''], + ], + [ + 'mail_id' => [ilDBConstants::T_INTEGER, $mail_id], + ] + ); + } + + public function getRemainingAmountOfSteps(): int + { + $db = $this->helper->getDatabase(); + + $path_count = (int) $db->fetchObject( + $db->query( + 'SELECT COUNT(DISTINCT path) cnt FROM mail_attachment + WHERE (rcid IS NULL OR rcid = "") + AND path IS NOT NULL AND path != ""' + ) + )->cnt; + + $mail_count = (int) $db->fetchObject( + $db->query( + 'SELECT COUNT(mail_id) cnt FROM mail + WHERE attachments LIKE ' . $db->quote('a:%', 'text') + ) + )->cnt; + + return $path_count + $mail_count; + } + + private function resolveOwnerIdForPath(string $relative_path): int + { + $db = $this->helper->getDatabase(); + $res = $db->queryF( + 'SELECT m.sender_id FROM mail_attachment ma + INNER JOIN mail m ON m.mail_id = ma.mail_id + WHERE ma.path = %s + ORDER BY m.send_time ASC + LIMIT 1', + [ilDBConstants::T_TEXT], + [$relative_path] + ); + + $row = $db->fetchObject($res); + if ($row !== null && (int) $row->sender_id > 0) { + return (int) $row->sender_id; + } + + return defined('SYSTEM_USER_ID') ? (int) SYSTEM_USER_ID : 6; + } + + private function assignRcidToPath(string $relative_path, ResourceCollectionIdentification $rcid): void + { + $this->helper->getDatabase()->manipulateF( + 'UPDATE mail_attachment SET rcid = %s WHERE path = %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + [$rcid->serialize(), $relative_path] + ); + } + + private function updateMailAttachmentFields(string $relative_path, ResourceCollectionIdentification $rcid): void + { + $db = $this->helper->getDatabase(); + $res = $db->queryF( + 'SELECT m.mail_id, m.attachments FROM mail_attachment ma + INNER JOIN mail m ON m.mail_id = ma.mail_id + WHERE ma.path = %s', + [ilDBConstants::T_TEXT], + [$relative_path] + ); + + while ($row = $db->fetchObject($res)) { + if (!is_string($row->attachments) || $row->attachments === '') { + continue; + } + if (!str_contains($row->attachments, 'a:')) { + continue; + } + + $db->update( + 'mail', + [ + 'attachments' => [ilDBConstants::T_CLOB, $rcid->serialize()], + ], + [ + 'mail_id' => [ilDBConstants::T_INTEGER, (int) $row->mail_id], + ] + ); + } + } + + private function markPathAsSkipped(string $relative_path): void + { + $this->helper->getDatabase()->manipulateF( + 'UPDATE mail_attachment SET rcid = %s WHERE path = %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + ['-', $relative_path] + ); + } +} diff --git a/components/ILIAS/Mail/classes/Setup/class.ilMailSetupAgent.php b/components/ILIAS/Mail/classes/Setup/class.ilMailSetupAgent.php index c0c184280aa5..45e8337ea07e 100755 --- a/components/ILIAS/Mail/classes/Setup/class.ilMailSetupAgent.php +++ b/components/ILIAS/Mail/classes/Setup/class.ilMailSetupAgent.php @@ -23,6 +23,7 @@ use ILIAS\Setup\ObjectiveCollection; use ILIAS\Mail\Setup\MailDBUpdateSteps11; use ILIAS\Mail\Setup\Migration\MailOutboxMigration; +use ILIAS\Mail\Setup\Migration\MigrateMailAttachmentsToIRSS; class ilMailSetupAgent implements Setup\Agent { @@ -74,7 +75,8 @@ public function getStatusObjective(Setup\Metrics\Storage $storage): Setup\Object public function getMigrations(): array { return [ - new MailOutboxMigration() + new MailOutboxMigration(), + new MigrateMailAttachmentsToIRSS(), ]; } } diff --git a/components/ILIAS/Mail/classes/class.ilAccountMail.php b/components/ILIAS/Mail/classes/class.ilAccountMail.php index c0fc6f57141f..0b3eea69378c 100755 --- a/components/ILIAS/Mail/classes/class.ilAccountMail.php +++ b/components/ILIAS/Mail/classes/class.ilAccountMail.php @@ -23,6 +23,7 @@ use ILIAS\User\Settings\NewAccountMail\Repository as NewAccountMailRepository; use ILIAS\User\Settings\NewAccountMail\Mail as NewAccountMail; use ILIAS\ResourceStorage\Services as ResourceStorage; +use ILIAS\ResourceStorage\Identification\ResourceIdentification; class ilAccountMail { @@ -169,9 +170,19 @@ public function send(): bool $mail_body .= $tmp_lang->txt('reg_mail_body_text3') . "\n\r"; $mail_body .= $user->getProfileAsString($tmp_lang); } else { - $attachment = $amail->getAttachment($this->irss); - if ($attachment !== null) { - $mmail->Attach($attachment->getPath(), '', 'attachment', $attachment->getFilename()); + if ($amail->getAttachmentRid() !== null) { + $rid = $this->irss->manage()->find($amail->getAttachmentRid()); + if ($rid instanceof ResourceIdentification) { + $filename = $this->irss->manage()->getCurrentRevision($rid)->getInformation()->getTitle(); + $mmail->AttachResource($rid, $filename); + } + } else { + $attachment = $amail->getAttachment($this->irss); + if ($attachment !== null && is_file($attachment->getPath())) { + $file_data_mail = new ilFileDataMail(ANONYMOUS_USER_ID); + $rid = $file_data_mail->streamFromPath($attachment->getPath()); + $mmail->AttachResource($rid, $attachment->getFilename()); + } } // replace placeholders diff --git a/components/ILIAS/Mail/classes/class.ilFileDataMail.php b/components/ILIAS/Mail/classes/class.ilFileDataMail.php index 2b4f44873b79..3db4622652dc 100755 --- a/components/ILIAS/Mail/classes/class.ilFileDataMail.php +++ b/components/ILIAS/Mail/classes/class.ilFileDataMail.php @@ -18,17 +18,31 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\Filesystem\Filesystem; +use ILIAS\ResourceStorage\Services; +use ILIAS\Filesystem\Stream\Streams; use ILIAS\FileUpload\DTO\UploadResult; +use ILIAS\ResourceStorage\Collection\ResourceCollection; +use ILIAS\ResourceStorage\Identification\ResourceIdentification; +use ILIAS\ResourceStorage\Resource\Repository\CollectionDBRepository; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; +use ILIAS\FileDelivery\Delivery as FileDelivery; class ilFileDataMail extends ilFileData { + private const string POOL_RCID_PREF_KEY = 'mail_attachment_pool_rcid'; + + public const string LEGACY_POOL_ITEM_PREFIX = 'legacy:'; + public string $mail_path; protected int $mail_max_upload_file_size; protected Filesystem $tmp_directory; protected Filesystem $storage_directory; protected ilDBInterface $db; protected ILIAS $ilias; + private readonly Services $irss; + private readonly ilMailAttachmentStakeholder $stakeholder; public function __construct(public int $user_id = 0) { @@ -43,23 +57,14 @@ public function __construct(public int $user_id = 0) $this->db = $DIC->database(); $this->tmp_directory = $DIC->filesystem()->temp(); $this->storage_directory = $DIC->filesystem()->storage(); + $this->irss = $DIC->resourceStorage(); + $this->stakeholder = new ilMailAttachmentStakeholder(); + $this->stakeholder->setOwner($this->user_id); $this->checkReadWrite(); $this->initAttachmentMaxUploadSize(); } - public function initDirectory(): bool - { - if (is_writable($this->getPath()) - && mkdir($this->getPath() . '/' . MAILPATH) - && chmod($this->getPath() . '/' . MAILPATH, 0755)) { - $this->mail_path = $this->getPath() . '/' . MAILPATH; - return true; - } - - return false; - } - public function getUploadLimit(): int { return $this->mail_max_upload_file_size; @@ -91,6 +96,22 @@ public function getAbsoluteAttachmentPoolPathPrefix(): string */ public function getAttachmentPathAndFilenameByMd5Hash(string $md5FileHash, int $mail_id): array { + $rcid = $this->getRcidForMail($mail_id); + if ($rcid !== null) { + $resource_identification = $this->getResourceIdByHash($rcid, $md5FileHash); + if ($resource_identification === null) { + throw new OutOfBoundsException(); + } + $info = $this->irss->manage()->getCurrentRevision($resource_identification)->getInformation(); + + return [ + 'path' => '', + 'filename' => $info->getTitle(), + 'rcid' => $rcid, + 'md5' => $md5FileHash, + ]; + } + $res = $this->db->queryF( 'SELECT path FROM mail_attachment WHERE mail_id = %s', ['integer'], @@ -133,32 +154,6 @@ private function getAttachmentPathByMailId(int $mail_id): string return ''; } - public function getAttachmentPath(string $a_filename, int $a_mail_id): string - { - $path = $this->getMailPath() . '/' . $this->getAttachmentPathByMailId($a_mail_id) . '/' . $a_filename; - - if (is_readable($path)) { - return $path; - } - - return ''; - } - - /** - * @param list $a_attachments - */ - public function adoptAttachments(array $a_attachments, int $a_mail_id): string - { - foreach ($a_attachments as $file) { - $path = $this->getAttachmentPath($file, $a_mail_id); - if (!copy($path, $this->getMailPath() . '/' . $this->user_id . '_' . $file)) { - return 'ERROR: ' . $this->getMailPath() . '/' . $this->user_id . '_' . $file . ' cannot be created'; - } - } - - return ''; - } - public function checkReadWrite(): bool { if (is_writable($this->mail_path) && is_readable($this->mail_path)) { @@ -209,154 +204,275 @@ private function getUnsentFiles(): array return $files; } - public function storeAsAttachment(string $a_filename, string $a_content): string - { - if (strlen($a_content) >= $this->getUploadLimit()) { - throw new DomainException( - sprintf( - 'Mail upload limit reached for user with id %s', - $this->user_id - ) - ); - } - $name = ilFileUtils::_sanitizeFilemame($a_filename); - $this->rotateFiles($this->getMailPath() . '/' . $this->user_id . '_' . $name); + /** + * @deprecated Legacy pool write; use uploadToPool() instead. + */ + public function storeUploadedFile(UploadResult $result): string + { + return $this->uploadToPool($result)->serialize(); + } - $abs_path = $this->getMailPath() . '/' . $this->user_id . '_' . $name; + public function uploadToPool(UploadResult $result): ResourceIdentification + { + $rid = $this->uploadToIrss($result); + $pool_rcid = $this->resolveUserPoolRcid(); + $updated_rcid = $this->adoptPoolResourcesToCollection($pool_rcid, [$rid]); - $fp = fopen($abs_path, 'wb+'); - if (!is_resource($fp)) { - throw new RuntimeException( - sprintf( - 'Could not read file: %s', - $abs_path - ) - ); + if ($updated_rcid === null) { + throw new RuntimeException('Could not store mail attachment in pool.'); } - if (fwrite($fp, $a_content) === false) { - fclose($fp); - throw new RuntimeException( - sprintf( - 'Could not write file: %s', - $abs_path - ) - ); + if ($pool_rcid === null || $pool_rcid->serialize() !== $updated_rcid->serialize()) { + $this->persistUserPoolRcid($updated_rcid); } - fclose($fp); - - return $name; + return $rid; } - public function storeUploadedFile(UploadResult $result): string + public function getUserPoolRcid(): ResourceCollectionIdentification { - $filename = ilFileUtils::_sanitizeFilemame( - $result->getName() - ); - - $this->rotateFiles($this->getMailPath() . '/' . $this->user_id . '_' . $filename); - - ilFileUtils::moveUploadedFile( - $result->getPath(), - $filename, - $this->getMailPath() . '/' . $this->user_id . '_' . $filename - ); + $pool_rcid = $this->resolveUserPoolRcid(); + if ($pool_rcid === null) { + throw new RuntimeException('Mail attachment pool does not exist.'); + } - return $filename; + return $pool_rcid; } - public function copyAttachmentFile(string $a_abs_path, string $a_new_name): bool + private function resolveUserPoolRcid(): ?ResourceCollectionIdentification { - @copy($a_abs_path, $this->getMailPath() . '/' . $this->user_id . '_' . $a_new_name); + $pref = ilObjUser::_lookupPref($this->user_id, self::POOL_RCID_PREF_KEY); + if (!is_string($pref) || $pref === '') { + return null; + } - return true; + $rcid = new ResourceCollectionIdentification($pref); + if (!$this->collectionIsKnown($rcid)) { + return null; + } + + $this->repairCollectionHeaderIfNeeded($rcid); + + return $rcid; } - private function rotateFiles(string $a_path): bool + /** + * @return \Generator + */ + private function iteratePoolResourceIdentifications(ResourceCollectionIdentification $pool_rcid): \Generator { - if (is_file($a_path)) { - $this->rotateFiles($a_path . '.old'); - return ilFileUtils::rename($a_path, $a_path . '.old'); - } + $this->repairCollectionHeaderIfNeeded($pool_rcid); - return true; + foreach ($this->getAssignedRidStrings($pool_rcid) as $rid_string) { + $resource_identification = $this->irss->manage()->find($rid_string); + if ($resource_identification !== null) { + yield $resource_identification; + } + } } /** - * @param list $a_filenames + * @return list */ - public function unlinkFiles(array $a_filenames): string + public function getUserPoolFilesData(): array { - foreach ($a_filenames as $file) { - if (!$this->unlinkFile($file)) { - return $file; + $files = []; + + $pool_rcid = $this->resolveUserPoolRcid(); + if ($pool_rcid !== null) { + foreach ($this->iteratePoolResourceIdentifications($pool_rcid) as $resource_identification) { + $revision = $this->irss->manage()->getCurrentRevision($resource_identification); + $info = $revision->getInformation(); + $files[] = [ + 'rid' => $resource_identification->serialize(), + 'name' => $info->getTitle(), + 'size' => $info->getSize(), + 'ctime' => $info->getCreationDate()->getTimestamp(), + 'md5' => $revision->getTitle(), + ]; } } - return ''; + foreach ($this->getUnsentFiles() as $file) { + $files[] = [ + 'rid' => self::LEGACY_POOL_ITEM_PREFIX . $file['name'], + 'name' => $file['name'], + 'size' => (int) $file['size'], + 'ctime' => (int) $file['ctime'], + 'md5' => md5($file['name']), + ]; + } + + return $files; } - public function unlinkFile(string $a_filename): bool + public function isLegacyPoolItemIdentifier(string $identifier): bool { - if (is_file($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename))) { - return unlink($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename)); + return str_starts_with($identifier, self::LEGACY_POOL_ITEM_PREFIX); + } + + public function legacyPoolFilenameFromIdentifier(string $identifier): string + { + return substr($identifier, strlen(self::LEGACY_POOL_ITEM_PREFIX)); + } + + public function migrateLegacyPoolFilenameToResource(string $filename): ?ResourceIdentification + { + $path = $this->getAbsoluteAttachmentPoolPathByFilename($filename); + if (!is_file($path)) { + return null; } - return false; + $rid = $this->streamFromPath($path, md5($filename)); + $pool_rcid = $this->resolveUserPoolRcid(); + $updated_rcid = $this->adoptPoolResourcesToCollection($pool_rcid, [$rid]); + if ($updated_rcid === null) { + return null; + } + + if ($pool_rcid === null || $pool_rcid->serialize() !== $updated_rcid->serialize()) { + $this->persistUserPoolRcid($updated_rcid); + } + + $this->unlinkFile($filename); + + return $rid; } /** - * Resolves a path for a passed filename in regards of a user's mail attachment pool, - * meaning attachments not being sent + * @param list $identifiers + * @return list */ - public function getAbsoluteAttachmentPoolPathByFilename(string $filename): string + public function resolvePoolIdentifiersToResources(array $identifiers): array { - return $this->getAbsoluteAttachmentPoolPathPrefix() . $filename; + $resource_identifications = []; + + foreach ($identifiers as $identifier) { + if ($this->isLegacyPoolItemIdentifier($identifier)) { + $resource_identification = $this->migrateLegacyPoolFilenameToResource( + $this->legacyPoolFilenameFromIdentifier($identifier) + ); + } else { + $resource_identification = $this->irss->manage()->find($identifier); + } + + if ($resource_identification !== null) { + $resource_identifications[] = $resource_identification; + } + } + + return $resource_identifications; } /** - * Saves all attachment files in a specific mail directory .../mail//mail__/... - * @param list $a_attachments + * @param list $resource_identifications */ - public function saveFiles(int $a_mail_id, array $a_attachments): void - { - if (!is_numeric($a_mail_id) || $a_mail_id < 1) { - throw new InvalidArgumentException('The passed mail_id must be a valid integer!'); + public function adoptPoolResourcesToCollection( + ?ResourceCollectionIdentification $rcid, + array $resource_identifications + ): ?ResourceCollectionIdentification { + if ($resource_identifications === []) { + return null; } - foreach ($a_attachments as $attachment) { - $this->saveFile($a_mail_id, $attachment); + $collection = $this->loadPoolCollectionForMutation($rcid); + $added = false; + + foreach ($resource_identifications as $resource_identification) { + $hash = $this->irss->manage()->getCurrentRevision($resource_identification)->getTitle(); + if ($this->resourceIdByHashInCollection($collection, $hash) !== null) { + continue; + } + + $collection->add($resource_identification); + $added = true; } + + if (!$added) { + return null; + } + + $this->irss->collection()->store($collection); + + return $collection->getIdentification(); } - public static function getStorage(int $a_mail_id, int $a_usr_id): ilFSStorageMail + public function removeFromPool(ResourceIdentification $rid): void { - static $fsstorage_cache = []; + $pool_rcid = $this->resolveUserPoolRcid(); + if ($pool_rcid === null) { + return; + } + + $collection = $this->getCollection($pool_rcid); + if (!$collection->isIn($rid)) { + return; + } - $fsstorage_cache[$a_mail_id][$a_usr_id] = new ilFSStorageMail($a_mail_id, $a_usr_id); + $collection->remove($rid); + $this->irss->collection()->store($collection); - return $fsstorage_cache[$a_mail_id][$a_usr_id]; + if (!$this->isResourceReferencedOutsideCollection($rid, $pool_rcid)) { + $this->irss->manage()->remove($rid, $this->stakeholder); + } } - /** - * Save attachment file in a specific mail directory .../mail//mail__/... - */ - public function saveFile(int $a_mail_id, string $a_attachment): bool + public function poolResourceHash(ResourceIdentification $rid): string { - $storage = self::getStorage($a_mail_id, $this->user_id); - $storage->create(); - $storage_directory = $storage->getAbsolutePath(); + return $this->irss->manage()->getCurrentRevision($rid)->getTitle(); + } - if (!is_dir($storage_directory)) { - return false; - } - return copy( - $this->mail_path . '/' . $this->user_id . '_' . $a_attachment, - $storage_directory . '/' . $a_attachment + private function persistUserPoolRcid(ResourceCollectionIdentification $rcid): void + { + $this->db->replace( + 'usr_pref', + [ + 'usr_id' => [ilDBConstants::T_INTEGER, $this->user_id], + 'keyword' => [ilDBConstants::T_TEXT, self::POOL_RCID_PREF_KEY], + ], + [ + 'value' => [ilDBConstants::T_TEXT, $rcid->serialize()], + ] + ); + } + + private function isResourceReferencedOutsideCollection( + ResourceIdentification $rid, + ResourceCollectionIdentification $collection_identification + ): bool { + $res = $this->db->queryF( + 'SELECT rcid FROM il_resource_rca WHERE rid = %s', + [ilDBConstants::T_TEXT], + [$rid->serialize()] ); + + while ($row = $this->db->fetchObject($res)) { + if ($row->rcid !== $collection_identification->serialize()) { + return true; + } + } + + return false; + } + + public function unlinkFile(string $a_filename): bool + { + if (is_file($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename))) { + return unlink($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename)); + } + + return false; + } + + /** + * Resolves a path for a passed filename in regards of a user's mail attachment pool, + * meaning attachments not being sent + */ + public function getAbsoluteAttachmentPoolPathByFilename(string $filename): string + { + return $this->getAbsoluteAttachmentPoolPathPrefix() . $filename; } /** @@ -375,30 +491,67 @@ public function checkFilesExist(array $a_files): bool return true; } - public function assignAttachmentsToDirectory(int $a_mail_id, int $a_sent_mail_id): void - { - $storage = self::getStorage($a_sent_mail_id, $this->user_id); + public function assignAttachmentsToCollection( + int $mail_id, + ResourceCollectionIdentification $rcid + ): void { $this->db->manipulateF( - ' - INSERT INTO mail_attachment - ( mail_id, path) VALUES (%s, %s)', - ['integer', 'text'], - [$a_mail_id, $storage->getRelativePathExMailDirectory()] + 'INSERT INTO mail_attachment (mail_id, path, rcid) VALUES (%s, %s, %s)', + [ilDBConstants::T_INTEGER, ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + [$mail_id, '', $rcid->serialize()] ); } + public function getRcidForMail(int $mail_id): ?ResourceCollectionIdentification + { + $res = $this->db->queryF( + 'SELECT rcid FROM mail_attachment WHERE mail_id = %s', + ['integer'], + [$mail_id] + ); + + if ($this->db->numRows($res) !== 1) { + return null; + } + + $row = $this->db->fetchAssoc($res); + $rcid = (string) ($row['rcid'] ?? ''); + if ($rcid === '' || $rcid === '-') { + return null; + } + + return new ResourceCollectionIdentification($rcid); + } + + public function countMailsReferencingRcid(string $rcid): int + { + $res = $this->db->queryF( + 'SELECT COUNT(mail_id) cnt FROM mail_attachment WHERE rcid = %s', + ['text'], + [$rcid] + ); + + return (int) $this->db->fetchObject($res)->cnt; + } + public function deassignAttachmentFromDirectory(int $a_mail_id): bool { $res = $this->db->query( - 'SELECT path FROM mail_attachment WHERE mail_id = ' . $this->db->quote($a_mail_id, 'integer') + 'SELECT path, rcid FROM mail_attachment WHERE mail_id = ' . $this->db->quote($a_mail_id, 'integer') ); $path = ''; + $rcid = ''; while ($row = $this->db->fetchObject($res)) { $path = (string) $row->path; + $rcid = (string) ($row->rcid ?? ''); } - if ($path !== '') { + if ($rcid !== '' && $rcid !== '-') { + if ($this->countMailsReferencingRcid($rcid) === 1) { + $this->removeCollection(new ResourceCollectionIdentification($rcid)); + } + } elseif ($path !== '') { $res = $this->db->query( 'SELECT COUNT(mail_id) count_mail_id FROM mail_attachment WHERE path = ' . $this->db->quote($path, 'text') @@ -473,6 +626,14 @@ protected function initAttachmentMaxUploadSize(): void public function onUserDelete(): void { + $pool_rcid = $this->resolveUserPoolRcid(); + if ($pool_rcid !== null) { + try { + $this->removeCollection($pool_rcid); + } catch (Exception) { + } + } + // Delete uploaded mail files which are not attached to any message try { $iter = new RegexIterator( @@ -495,6 +656,7 @@ public function onUserDelete(): void INNER JOIN mail ON mail.mail_id = ma1.mail_id WHERE mail.user_id = %s + AND ma1.path IS NOT NULL AND ma1.path != "" AND (SELECT COUNT(tmp.path) FROM mail_attachment tmp WHERE tmp.path = ma1.path) = 1 '; $res = $this->db->queryF( @@ -522,6 +684,26 @@ public function onUserDelete(): void } } + $rcid_query = ' + SELECT DISTINCT(ma1.rcid) + FROM mail_attachment ma1 + INNER JOIN mail ON mail.mail_id = ma1.mail_id + WHERE mail.user_id = %s + AND ma1.rcid IS NOT NULL AND ma1.rcid != "" AND ma1.rcid != "-" + AND (SELECT COUNT(tmp.rcid) FROM mail_attachment tmp WHERE tmp.rcid = ma1.rcid) = 1 + '; + $rcid_res = $this->db->queryF( + $rcid_query, + [ilDBConstants::T_INTEGER], + [$this->user_id] + ); + while ($row = $this->db->fetchAssoc($rcid_res)) { + try { + $this->removeCollection(new ResourceCollectionIdentification($row['rcid'])); + } catch (Exception) { + } + } + // Delete each mail attachment rows assigned to a message of the deleted user. $this->db->manipulateF( ' @@ -547,6 +729,12 @@ public function deliverAttachmentsAsZip( array $files = [], bool $is_draft = false ): void { + $rcid = $this->getRcidForMail($mail_id); + if ($rcid !== null && !$is_draft) { + $this->deliverCollectionAsZip($rcid, $basename); + return; + } + $path = ''; if (!$is_draft) { $path = $this->getAttachmentPathByMailId($mail_id); @@ -600,4 +788,426 @@ public function deliverAttachmentsAsZip( ilFileUtils::getValidFilename($download_filename . '.zip') ); } + + public function getStakeholder(): ilMailAttachmentStakeholder + { + return $this->stakeholder; + } + + public function streamFromPath(string $absolute_path, ?string $revision_title = null): ResourceIdentification + { + $stream = Streams::ofResource(fopen($absolute_path, 'rb')); + + return $this->irss->manage()->stream( + $stream, + $this->stakeholder, + $revision_title ?? md5(basename($absolute_path)) + ); + } + + public function uploadToIrss(UploadResult $result): ResourceIdentification + { + return $this->irss->manage()->upload( + $result, + $this->stakeholder, + md5($result->getName()) + ); + } + + /** + * @param list $resource_identifications + */ + public function createCollectionFromResourceIdentifications( + array $resource_identifications + ): ResourceCollectionIdentification { + $rcid = $this->irss->collection()->id(null, $this->user_id); + $collection = $this->irss->collection()->get($rcid); + foreach ($resource_identifications as $resource_identification) { + $collection->add($resource_identification); + } + $this->irss->collection()->store($collection); + + return $collection->getIdentification(); + } + + /** + * @param list $filenames Pool filenames without user prefix + */ + public function createCollectionFromPoolFilenames(array $filenames): ?ResourceCollectionIdentification + { + return $this->adoptPoolFilenamesToCollection(null, $filenames); + } + + public function migrateLegacyPoolAttachments(MailAttachments $attachments): ?MailAttachments + { + if (!$attachments->isLegacy()) { + return $attachments; + } + + if (!$this->checkFilesExist($attachments->legacyFilenames())) { + return null; + } + + $rcid = $this->createCollectionFromPoolFilenames($attachments->legacyFilenames()); + + return $rcid !== null ? MailAttachments::fromIrss($rcid) : null; + } + + /** + * @param list $filenames Pool filenames without user prefix + */ + public function adoptPoolFilenamesToCollection( + ?ResourceCollectionIdentification $rcid, + array $filenames + ): ?ResourceCollectionIdentification { + if ($filenames === []) { + return null; + } + + if ($rcid !== null && $this->irss->collection()->exists($rcid->serialize())) { + $collection_id = $this->irss->collection()->id($rcid->serialize(), $this->user_id); + } else { + $collection_id = $this->irss->collection()->id(null, $this->user_id); + } + + $collection = $this->irss->collection()->get($collection_id); + $added = false; + + foreach ($filenames as $filename) { + $path = $this->getAbsoluteAttachmentPoolPathByFilename($filename); + if (!is_file($path)) { + continue; + } + + $hash = md5(basename($path)); + if ($this->resourceIdByHashInCollection($collection, $hash) !== null) { + continue; + } + + $collection->add($this->streamFromPath($path, $hash)); + $added = true; + } + + if (!$added) { + return null; + } + + $this->irss->collection()->store($collection); + + return $collection->getIdentification(); + } + + /** + * @param list $absolute_paths + */ + public function createCollectionFromPaths(array $absolute_paths): ?ResourceCollectionIdentification + { + $resource_identifications = []; + foreach ($absolute_paths as $absolute_path) { + if (!is_file($absolute_path)) { + continue; + } + $resource_identifications[] = $this->streamFromPath($absolute_path); + } + + if ($resource_identifications === []) { + return null; + } + + return $this->createCollectionFromResourceIdentifications($resource_identifications); + } + + public function createCollectionFromContent(string $name, string $content): ?ResourceCollectionIdentification + { + if ($name === '' || $content === '') { + return null; + } + + if (strlen($content) >= $this->getUploadLimit()) { + throw new DomainException( + sprintf( + 'Mail upload limit reached for user with id %s', + $this->user_id + ) + ); + } + + $sanitized_name = ilFileUtils::_sanitizeFilemame($name); + + return $this->createCollectionFromResourceIdentifications([ + $this->irss->manage()->stream( + Streams::ofString($content), + $this->stakeholder, + md5($sanitized_name) + ), + ]); + } + + public function copyCollectionForDelivery( + ResourceCollectionIdentification $source + ): ?ResourceCollectionIdentification { + if (!$this->collectionIsKnown($source)) { + return null; + } + + $resource_identifications = iterator_to_array( + $this->getCollection($source)->getResourceIdentifications(), + false + ); + + return $this->createCollectionFromForeignResources($resource_identifications); + } + + /** + * @param list $resource_identifications + */ + public function createCollectionFromForeignResources( + array $resource_identifications + ): ?ResourceCollectionIdentification { + if ($resource_identifications === []) { + return null; + } + + $mail_resources = []; + foreach ($resource_identifications as $source) { + $revision = $this->irss->manage()->getCurrentRevision($source); + $stream = $this->irss->consume()->stream($source); + $mail_resources[] = $this->irss->manage()->stream( + $stream->getStream(), + $this->stakeholder, + $revision->getTitle() + ); + } + + return $this->createCollectionFromResourceIdentifications($mail_resources); + } + + public function getCollection(ResourceCollectionIdentification $rcid): ResourceCollection + { + if (!$this->collectionIsKnown($rcid)) { + throw new OutOfBoundsException( + sprintf('Mail attachment collection "%s" does not exist.', $rcid->serialize()) + ); + } + + $this->repairCollectionHeaderIfNeeded($rcid); + $this->irss->preloadCollections([$rcid->serialize()]); + + return $this->irss->collection()->get($rcid, $this->user_id); + } + + private function loadPoolCollectionForMutation(?ResourceCollectionIdentification $pool_rcid): ResourceCollection + { + if ($pool_rcid !== null && $this->collectionIsKnown($pool_rcid)) { + $this->repairCollectionHeaderIfNeeded($pool_rcid); + $this->irss->preloadCollections([$pool_rcid->serialize()]); + + return $this->irss->collection()->get($pool_rcid, $this->user_id); + } + + $collection_id = $this->irss->collection()->id(null, $this->user_id); + + return $this->irss->collection()->get($collection_id, $this->user_id); + } + + /** + * @return list + */ + private function getAssignedRidStrings(ResourceCollectionIdentification $rcid): array + { + $res = $this->db->queryF( + 'SELECT ' . CollectionDBRepository::R_IDENTIFICATION . + ' FROM ' . CollectionDBRepository::COLLECTION_ASSIGNMENT_TABLE_NAME . + ' WHERE ' . CollectionDBRepository::C_IDENTIFICATION . ' = %s ORDER BY position ASC', + [ilDBConstants::T_TEXT], + [$rcid->serialize()] + ); + + $rids = []; + while ($row = $this->db->fetchAssoc($res)) { + $rids[] = (string) $row[CollectionDBRepository::R_IDENTIFICATION]; + } + + return $rids; + } + + private function collectionIsKnown(ResourceCollectionIdentification $rcid): bool + { + return $this->irss->collection()->exists($rcid->serialize()) + || $this->getAssignedRidStrings($rcid) !== []; + } + + private function repairCollectionHeaderIfNeeded(ResourceCollectionIdentification $rcid): void + { + if ($this->irss->collection()->exists($rcid->serialize()) + || $this->getAssignedRidStrings($rcid) === []) { + return; + } + + $this->db->replace( + CollectionDBRepository::COLLECTION_TABLE_NAME, + [ + CollectionDBRepository::C_IDENTIFICATION => [ilDBConstants::T_TEXT, $rcid->serialize()], + ], + [ + 'title' => [ilDBConstants::T_TEXT, ''], + 'owner_id' => [ilDBConstants::T_INTEGER, $this->user_id], + ] + ); + } + + public function getResourceIdByHash( + ResourceCollectionIdentification $rcid, + string $hash + ): ?ResourceIdentification { + if (!$this->collectionIsKnown($rcid)) { + return null; + } + + foreach ($this->getAssignedRidStrings($rcid) as $rid) { + $resource_identification = $this->irss->manage()->find($rid); + if ($resource_identification === null) { + continue; + } + + $revision = $this->irss->manage()->getCurrentRevision($resource_identification); + if ($revision->getTitle() === $hash) { + return $resource_identification; + } + } + + return null; + } + + private function resourceIdByHashInCollection(ResourceCollection $collection, string $hash): ?ResourceIdentification + { + foreach ($collection->getResourceIdentifications() as $resource_identification) { + $revision = $this->irss->manage()->getCurrentRevision($resource_identification); + if ($revision->getTitle() === $hash) { + return $resource_identification; + } + } + + return null; + } + + /** + * @return list + */ + public function getRidsFromCollection(ResourceCollectionIdentification $rcid): array + { + $rids = []; + foreach ($this->getAssignedRidStrings($rcid) as $rid) { + if ($this->irss->manage()->find($rid) !== null) { + $rids[] = $rid; + } + } + + return $rids; + } + + + /** + * @param list $resource_identifications + */ + public function collectionContainsResources( + ResourceCollectionIdentification $rcid, + array $resource_identifications + ): bool { + if ($resource_identifications === []) { + return true; + } + + $collection_serials = []; + foreach ($this->getCollection($rcid)->getResourceIdentifications() as $resource_identification) { + $collection_serials[] = $resource_identification->serialize(); + } + + $expected_serials = []; + foreach ($resource_identifications as $resource_identification) { + $expected_serials[] = $resource_identification->serialize(); + } + + sort($collection_serials); + sort($expected_serials); + + return $collection_serials === $expected_serials; + } + + /** + * @return array + */ + public function getAttachmentListing(ResourceCollectionIdentification $rcid): array + { + $files = []; + foreach ($this->getAssignedRidStrings($rcid) as $rid) { + $resource_identification = $this->irss->manage()->find($rid); + if ($resource_identification === null) { + continue; + } + + $revision = $this->irss->manage()->getCurrentRevision($resource_identification); + $info = $revision->getInformation(); + $file_title = $info->getTitle(); + $files[$file_title] = [ + 'md5' => $revision->getTitle(), + 'name' => $file_title, + 'size' => $info->getSize(), + 'ctime' => $info->getCreationDate()->format('Y-m-d H:i:s'), + ]; + } + + return $files; + } + + public function deliverFile(ResourceCollectionIdentification $rcid, string $md5_hash): void + { + $resource_identification = $this->getResourceIdByHash($rcid, $md5_hash); + if ($resource_identification === null) { + throw new OutOfBoundsException('mail_error_reading_attachment'); + } + + $this->irss->consume()->download($resource_identification)->run(); + } + + public function deliverCollectionAsZip(ResourceCollectionIdentification $rcid, string $zip_basename): void + { + $zip_filename = FileDelivery::returnASCIIFileName($zip_basename . '.zip'); + $this->irss + ->consume() + ->downloadCollection($rcid, $zip_filename) + ->useRevisionTitlesForFileNames(false) + ->run(); + } + + public function removeCollection(ResourceCollectionIdentification $rcid, bool $ignore_usage = true): void + { + $this->irss->collection()->remove( + $this->irss->collection()->id($rcid->serialize()), + $this->stakeholder, + $ignore_usage + ); + } + + /** + * @return list + */ + public function getIrssMimeAttachments(ResourceCollectionIdentification $rcid): array + { + $attachments = []; + foreach ($this->getAssignedRidStrings($rcid) as $rid) { + $resource_identification = $this->irss->manage()->find($rid); + if ($resource_identification === null) { + continue; + } + + $info = $this->irss->manage()->getCurrentRevision($resource_identification)->getInformation(); + $attachments[] = [ + 'rid' => $rid, + 'name' => $info->getTitle(), + ]; + } + + return $attachments; + } } diff --git a/components/ILIAS/Mail/classes/class.ilMail.php b/components/ILIAS/Mail/classes/class.ilMail.php index a3c04849cf89..2d2c39faa375 100755 --- a/components/ILIAS/Mail/classes/class.ilMail.php +++ b/components/ILIAS/Mail/classes/class.ilMail.php @@ -24,8 +24,9 @@ use ILIAS\Mail\Recipient; use ILIAS\Mail\Service\MailSignatureService; use ILIAS\Mail\Transformation\Utf8Mb4Sanitizer; -use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\Mail\Folder\MailScheduleData; +use ILIAS\ResourceStorage\Identification\ResourceIdentification; class ilMail { @@ -44,6 +45,9 @@ class ilMail private bool $append_installation_signature = false; private bool $append_user_signature = false; + private bool $share_attachments = false; + private ?MailAttachments $shared_delivery_attachments = null; + private ?string $context_id = null; private array $context_parameters = []; @@ -157,6 +161,35 @@ public function getSaveInSentbox(): bool return $this->save_in_sentbox; } + public function setShareAttachments(bool $share_attachments): void + { + $this->share_attachments = $share_attachments; + } + + private function getDeliveryAttachments(MailAttachments $source): MailAttachments + { + if ($source->isEmpty() || !$source->isIrss()) { + return $source; + } + + if ($this->share_attachments) { + if ($this->shared_delivery_attachments === null) { + $copied_rcid = $this->mail_file_data->copyCollectionForDelivery($source->rcid()); + $this->shared_delivery_attachments = $copied_rcid !== null + ? MailAttachments::fromIrss($copied_rcid) + : MailAttachments::empty(); + } + + return $this->shared_delivery_attachments; + } + + $copied_rcid = $this->mail_file_data->copyCollectionForDelivery($source->rcid()); + + return $copied_rcid !== null + ? MailAttachments::fromIrss($copied_rcid) + : MailAttachments::empty(); + } + private function readMailObjectReferenceId(): void { $this->mail_obj_ref_id = ilMailGlobalServices::getMailObjectRefId(); @@ -399,11 +432,17 @@ public function fetchMailData(?array $row): ?array return null; } - if (isset($row['attachments']) && is_string($row['attachments']) && str_contains($row['attachments'], '{')) { - $unserialized_attachments = unserialize($row['attachments'], ['allowed_classes' => false]); - $row['attachments'] = is_array($unserialized_attachments) ? $unserialized_attachments : null; - } elseif (isset($row['attachments']) && is_string($row['attachments']) && $row['attachments'] !== '') { - $row['attachments'] = new ResourceCollectionIdentification($row['attachments']); + if (isset($row['attachments']) && is_string($row['attachments'])) { + $row['attachments'] = MailAttachments::fromDb($row['attachments']); + if ($row['attachments'] instanceof MailAttachments && $row['attachments']->isLegacy()) { + $migrated = $this->mail_file_data->migrateLegacyPoolAttachments($row['attachments']); + if ($migrated !== null) { + $row['attachments'] = $migrated; + if (isset($row['mail_id'])) { + $this->persistMailAttachmentsColumn((int) $row['mail_id'], $migrated); + } + } + } } else { $row['attachments'] = null; } @@ -458,12 +497,9 @@ public function getNewDraftId(int $folder_id): int return $next_id; } - /** - * @param list $a_attachments - */ public function updateDraft( int $a_folder_id, - array $a_attachments, + MailAttachments $a_attachments, string $a_rcp_to, string $a_rcp_cc, string $a_rcp_bcc, @@ -479,7 +515,7 @@ public function updateDraft( $this->table_mail, [ 'folder_id' => ['integer', $a_folder_id], - 'attachments' => ['clob', serialize($a_attachments)], + 'attachments' => [ilDBConstants::T_CLOB, $a_attachments->toDb()], 'send_time' => ['timestamp', date('Y-m-d H:i:s')], 'rcp_to' => ['clob', $a_rcp_to], 'rcp_cc' => ['clob', $a_rcp_cc], @@ -520,7 +556,7 @@ public function scheduledMail( 'user_id' => [ilDBConstants::T_INTEGER, $sender_usr_id], 'folder_id' => [ilDBConstants::T_INTEGER, $folder_id], 'sender_id' => [ilDBConstants::T_INTEGER, $sender_usr_id], - 'attachments' => [ilDBConstants::T_CLOB, serialize($mail_data->getMailDeliveryData()->getAttachments())], + 'attachments' => [ilDBConstants::T_CLOB, $mail_data->getMailDeliveryData()->getAttachments()->toDb()], 'send_time' => [ilDBConstants::T_TIMESTAMP, date('Y-m-d H:i:s')], 'rcp_to' => [ilDBConstants::T_CLOB, $mail_data->getMailDeliveryData()->getTo()], 'rcp_cc' => [ilDBConstants::T_CLOB, $mail_data->getMailDeliveryData()->getCC()], @@ -551,7 +587,7 @@ public function scheduledMail( private function sendInternalMail( int $folder_id, int $sender_usr_id, - array $attachments, + MailAttachments $attachments, string $to, string $cc, string $bcc, @@ -577,7 +613,7 @@ private function sendInternalMail( 'user_id' => ['integer', $usr_id], 'folder_id' => ['integer', $folder_id], 'sender_id' => ['integer', $sender_usr_id], - 'attachments' => ['clob', serialize($attachments)], + 'attachments' => [ilDBConstants::T_BLOB, $attachments->toDb()], 'send_time' => ['timestamp', date('Y-m-d H:i:s')], 'rcp_to' => ['clob', $to], 'rcp_cc' => ['clob', $cc], @@ -801,10 +837,12 @@ private function sendChanneledMails( $mbox->setUsrId($recipient->getUserId()); $recipient_inbox_id = $mbox->getInboxFolder(); + $delivery_attachments = $this->getDeliveryAttachments($mail_data->getAttachments()); + $internal_mail_id = $this->sendInternalMail( $recipient_inbox_id, $this->user_id, - $mail_data->getAttachments(), + $delivery_attachments, $mail_data->getTo(), $mail_data->getCc(), '', @@ -820,26 +858,24 @@ private function sendChanneledMails( $this->getMailOptionsByUserId($this->user_id), ); - if ($mail_data->getAttachments() !== []) { - $this->mail_file_data->assignAttachmentsToDirectory($internal_mail_id, $mail_data->getInternalMailId()); - } + $this->assignMailAttachments( + $internal_mail_id, + $delivery_attachments, + $mail_data->getInternalMailId() + ); } $this->delegateExternalEmails( $mail_data->getSubject(), - $mail_data->getAttachments(), + $this->getDeliveryAttachments($mail_data->getAttachments()), $message, $usr_id_to_external_email_addresses_map ); } - /** - * @param list $attachments - * @param array $usr_id_to_external_email_addresses_map - */ private function delegateExternalEmails( string $subject, - array $attachments, + MailAttachments $attachments, string $message, array $usr_id_to_external_email_addresses_map ): void { @@ -973,9 +1009,6 @@ private function checkRecipients(string $recipients): array return array_merge(...$errors); } - /** - * @param list $a_attachments - */ public function persistToStage( int $a_user_id, string $a_rcp_to, @@ -983,13 +1016,14 @@ public function persistToStage( string $a_rcp_bcc, string $a_m_subject, string $a_m_message, - ?\ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification $a_attachments = null, + ?MailAttachments $a_attachments = null, bool $a_use_placeholders = false, ?string $a_tpl_context_id = null, ?array $a_tpl_ctx_params = [] ): bool { - if (!is_null($a_attachments)) { - $a_attachments = $a_attachments->serialize(); + $attachment_value = null; + if ($a_attachments !== null && $a_attachments->isIrss()) { + $attachment_value = $a_attachments->rcid()->serialize(); } $this->db->replace( $this->table_mail_saved, @@ -997,7 +1031,7 @@ public function persistToStage( 'user_id' => ['integer', $this->user_id], ], [ - 'attachments' => ['text', $a_attachments], + 'attachments' => [ilDBConstants::T_TEXT, $attachment_value], 'rcp_to' => ['clob', $a_rcp_to], 'rcp_cc' => ['clob', $a_rcp_cc], 'rcp_bcc' => ['clob', $a_rcp_bcc], @@ -1030,18 +1064,13 @@ public function retrieveFromStage(): array return $this->mail_data; } - /** - * Should be used to enqueue a 'mail'. A validation is executed before, errors are returned - * @param list $a_attachment - * @return list - */ public function enqueue( string $a_rcp_to, string $a_rcp_cc, string $a_rcp_bcc, string $a_m_subject, string $a_m_message, - array $a_attachment, + MailAttachments $a_attachment, bool $a_use_placeholders = false ): array { global $DIC; @@ -1056,12 +1085,18 @@ public function enqueue( ' | CC: ' . $a_rcp_cc . ' | BCC: ' . $a_rcp_bcc . ' | Subject: ' . $a_m_subject . - ' | Attachments: ' . print_r($a_attachment, true) + ' | Attachments: ' . ( + $a_attachment->isIrss() + ? $a_attachment->rcid()->serialize() + : print_r($a_attachment->legacyFilenames(), true) + ) ); - if ($a_attachment && !$this->mail_file_data->checkFilesExist($a_attachment)) { - return [new ilMailError('mail_attachment_file_not_exist', [implode(', ', $a_attachment)])]; + $normalized = $this->normalizeLegacyAttachments($a_attachment); + if ($normalized['errors'] !== []) { + return $normalized['errors']; } + $a_attachment = $normalized['attachments']; $errors = $this->checkMail($a_rcp_to, $a_rcp_cc, $a_rcp_bcc, $a_m_subject); if ($errors !== []) { @@ -1116,7 +1151,7 @@ public function enqueue( $rcp_bcc, $a_m_subject, $a_m_message, - serialize($a_attachment), + $a_attachment->toBackgroundTask(), $a_use_placeholders, $this->getSaveInSentbox(), (string) $this->context_id, @@ -1153,8 +1188,16 @@ public function enqueue( public function sendMail( MailDeliveryData $mail_data ): array { + $normalized = $this->normalizeLegacyAttachments($mail_data->getAttachments()); + if ($normalized['errors'] !== []) { + return $normalized['errors']; + } + $mail_data = $mail_data->withAttachments($normalized['attachments']); + $this->shared_delivery_attachments = null; + + $sentbox_attachments = $this->getDeliveryAttachments($mail_data->getAttachments()); $internal_message_id = $this->saveInSentbox( - $mail_data->getAttachments(), + $sentbox_attachments, $mail_data->getTo(), $mail_data->getCc(), $mail_data->getBcc(), @@ -1163,10 +1206,11 @@ public function sendMail( ); $mail_data = $mail_data->withInternalMailId($internal_message_id); - if ($mail_data->getAttachments() !== []) { - $this->mail_file_data->assignAttachmentsToDirectory($internal_message_id, $internal_message_id); - $this->mail_file_data->saveFiles($internal_message_id, $mail_data->getAttachments()); - } + $this->assignMailAttachments( + $internal_message_id, + $sentbox_attachments, + $internal_message_id + ); $num_external_email_addresses = $this->getCountRecipients( $mail_data->getTo(), @@ -1195,7 +1239,7 @@ public function sendMail( $mail_data->isUsePlaceholder() ? $this->replacePlaceholders($mail_data->getMessage(), 0) : $mail_data->getMessage(), - $mail_data->getAttachments() + $this->getDeliveryAttachments($mail_data->getAttachments()) ); } else { $this->logger->debug('No external email addresses given in recipient string'); @@ -1254,11 +1298,8 @@ private function getSubjectSentFolderId(): int return $send_folder_id; } - /** - * @param list $attachment - */ private function saveInSentbox( - array $attachment, + MailAttachments $attachment, string $to, string $cc, string $bcc, @@ -1279,16 +1320,13 @@ private function saveInSentbox( ); } - /** - * @param list $attachments - */ private function sendMimeMail( string $to, string $cc, string $bcc, string $subject, string $message, - array $attachments + MailAttachments $attachments ): void { $mailer = new ilMimeMail(); $mailer->From($this->sender_factory->getSenderByUsrId($this->user_id)); @@ -1317,35 +1355,88 @@ function (string $message): string { $mailer->Bcc($bcc); } - foreach ($attachments as $attachment) { - $mailer->Attach( - $this->mail_file_data->getAbsoluteAttachmentPoolPathByFilename($attachment), - '', - 'inline', - $attachment - ); + if ($attachments->isIrss()) { + foreach ($this->mail_file_data->getIrssMimeAttachments($attachments->rcid()) as $attachment) { + $mailer->AttachResource( + new ResourceIdentification($attachment['rid']), + $attachment['name'] + ); + } } $mailer->Send(); } - public function saveAttachments(?ResourceCollectionIdentification $attachments): void + /** + * @return array{attachments: MailAttachments, errors: list} + */ + private function normalizeLegacyAttachments(MailAttachments $attachments): array { - if (!is_null($attachments)) { - $attachments = $attachments->serialize(); + if (!$attachments->isLegacy()) { + return ['attachments' => $attachments, 'errors' => []]; + } + + if (!$this->mail_file_data->checkFilesExist($attachments->legacyFilenames())) { + return [ + 'attachments' => $attachments, + 'errors' => [new ilMailError('mail_error_reading_attachment')], + ]; + } + + $migrated = $this->mail_file_data->migrateLegacyPoolAttachments($attachments); + if ($migrated === null) { + return [ + 'attachments' => $attachments, + 'errors' => [new ilMailError('mail_error_reading_attachment')], + ]; } + return ['attachments' => $migrated, 'errors' => []]; + } + + private function persistMailAttachmentsColumn(int $mail_id, MailAttachments $attachments): void + { $this->db->update( - $this->table_mail_saved, + 'mail', [ - 'attachments' => ['text', $attachments], + 'attachments' => [ilDBConstants::T_CLOB, $attachments->toDb()], ], [ - 'user_id' => ['integer', $this->user_id], + 'mail_id' => [ilDBConstants::T_INTEGER, $mail_id], ] ); } + private function assignMailAttachments( + int $mail_id, + MailAttachments $attachments, + int $sent_mail_id + ): void { + if ($attachments->isEmpty() || !$attachments->isIrss()) { + return; + } + + $this->mail_file_data->assignAttachmentsToCollection($mail_id, $attachments->rcid()); + } + + public function saveAttachments(?MailAttachments $attachments): void + { + $stage = $this->retrieveFromStage(); + + $this->persistToStage( + $this->user_id, + (string) ($stage['rcp_to'] ?? ''), + (string) ($stage['rcp_cc'] ?? ''), + (string) ($stage['rcp_bcc'] ?? ''), + (string) ($stage['m_subject'] ?? ''), + (string) ($stage['m_message'] ?? ''), + $attachments, + (bool) ($stage['use_placeholders'] ?? false), + $stage['tpl_ctx_id'] ?? null, + (array) ($stage['tpl_ctx_params'] ?? []) + ); + } + /** * Explode recipient string, allowed separators are ',' ';' ' ' * @return list diff --git a/components/ILIAS/Mail/classes/class.ilMailAttachmentGUI.php b/components/ILIAS/Mail/classes/class.ilMailAttachmentGUI.php index fcecc2d65943..1da7ccf92ea8 100644 --- a/components/ILIAS/Mail/classes/class.ilMailAttachmentGUI.php +++ b/components/ILIAS/Mail/classes/class.ilMailAttachmentGUI.php @@ -19,11 +19,13 @@ declare(strict_types=1); use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\FileUpload\Handler\AbstractCtrlAwareUploadHandler; use ILIAS\FileUpload\Handler\FileInfoResult; use ILIAS\FileUpload\Handler\BasicHandlerResult; use ILIAS\FileUpload\DTO\UploadResult; use ILIAS\FileUpload\Handler\HandlerResult; +use ILIAS\FileUpload\Handler\BasicFileInfoResult; use ILIAS\Mail\Attachments\AttachmentManagement; use ILIAS\Mail\Attachments\MailAttachmentTableGUI; use ILIAS\Mail\Attachments\MailAttachmentCommands; @@ -112,12 +114,6 @@ public function executeCommand(): void private function saveAttachments(): void { - $files = []; - - // Important: Do not check for uploaded files here, - // otherwise it is no more possible to remove files (please ignore bug reports like 10137) - - $size_of_affected_files = 0; $files_of_request = $this->http->wrapper()->query()->retrieve( 'mail_attachments_filename', $this->refinery->byTrying([ @@ -127,22 +123,23 @@ private function saveAttachments(): void ); if ($files_of_request !== [] && $files_of_request[0] === 'ALL_OBJECTS') { - $files_of_request = array_map(static fn(array $file): string => $file['name'], $this->fdm->getUserFilesData()); + $files_of_request = array_map(static fn(array $file): string => $file['rid'], $this->fdm->getUserPoolFilesData()); } - foreach ($files_of_request as $file) { - if (is_file($this->fdm->getMailPath() . '/' . basename($this->user->getId() . '_' . urldecode((string) $file)))) { - $files[] = urldecode((string) $file); - $size_of_affected_files += filesize( - $this->fdm->getMailPath() . '/' . - basename($this->user->getId() . '_' . urldecode((string) $file)) - ); - } + $resource_identifications = $this->fdm->resolvePoolIdentifiersToResources($files_of_request); + $size_of_affected_files = 0; + foreach ($resource_identifications as $rid) { + $size_of_affected_files += $this->storage->manage()->getCurrentRevision($rid)->getInformation()->getSize(); + } + + if ($resource_identifications === []) { + $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_INFO, $this->lng->txt('select_one'), true); + $this->showAttachmentsCommand(); + return; } - if ($files !== [] && - $this->fdm->getAttachmentsTotalSizeLimit() !== null && - $size_of_affected_files > $this->fdm->getAttachmentsTotalSizeLimit()) { + if ($this->fdm->getAttachmentsTotalSizeLimit() !== null + && $size_of_affected_files > $this->fdm->getAttachmentsTotalSizeLimit()) { $this->tpl->setOnScreenMessage( $this->tpl::MESSAGE_TYPE_FAILURE, $this->lng->txt('mail_max_size_attachments_total_error') . ' ' . @@ -152,10 +149,26 @@ private function saveAttachments(): void return; } - $rcid_for_files = $this->getIdforCollection($files); - $this->umail->saveAttachments($rcid_for_files); + try { + $mail_data = $this->umail->retrieveFromStage(); + $stage_attachments = $mail_data['attachments'] ?? null; + $existing_rcid = ($stage_attachments instanceof MailAttachments && $stage_attachments->isIrss()) + ? $stage_attachments->rcid() + : null; + + $rcid = $this->fdm->adoptPoolResourcesToCollection($existing_rcid, $resource_identifications); + if ($rcid === null) { + throw new RuntimeException($this->lng->txt('mail_error_reading_attachment')); + } - $this->ctrl->returnToParent($this); + $this->umail->saveAttachments(MailAttachments::fromIrss($rcid)); + } catch (Throwable $e) { + $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_FAILURE, $e->getMessage(), true); + $this->showAttachmentsCommand(); + return; + } + + $this->ctrl->redirectByClass(ilMailFormGUI::class, 'returnFromAttachments'); } private function cancelSaveAttachmentsCommand(): void @@ -175,7 +188,7 @@ private function confirmDeleteAttachments(): void ); if ($files !== [] && $files[0] === 'ALL_OBJECTS') { - $files = array_map(static fn(array $file): string => $file['name'], $this->fdm->getUserFilesData()); + $files = array_map(static fn(array $file): string => $file['rid'], $this->fdm->getUserPoolFilesData()); } if ($files === []) { @@ -191,11 +204,17 @@ private function confirmDeleteAttachments(): void $confirmation->setCancel($this->lng->txt('cancel'), self::CMD_SHOW_ATTACHMENTS); $confirmation->setHeaderText($this->lng->txt('mail_sure_delete_file')); - foreach ($files as $filename) { + foreach ($files as $rid_string) { + $title = (string) $rid_string; + $rid = $this->storage->manage()->find($title); + if ($rid !== null) { + $title = $this->storage->manage()->getCurrentRevision($rid)->getInformation()->getTitle(); + } + $confirmation->addItem( 'filename[]', - ilUtil::stripSlashes($filename), - ilUtil::stripSlashes(urldecode((string) $filename)) + ilUtil::stripSlashes((string) $rid_string), + ilUtil::stripSlashes($title) ); } @@ -219,35 +238,21 @@ private function deleteAttachmentsCommand(): void return; } - $decoded_files = []; - foreach ($files as $value) { - $decoded_files[] = urldecode((string) $value); - } - - $error = $this->fdm->unlinkFiles($decoded_files); - if ($error !== '') { - $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_SUCCESS, $this->lng->txt('mail_error_delete_file') . ' ' . $error, true); - } else { - $mail_data = $this->umail->retrieveFromStage(); - if (!is_null($mail_data['attachments'])) { - $files_to_legacy = $this->FilesFromIRSSToLegacy($mail_data['attachments']); - $files = $this->handleAttachments($files_to_legacy); - $rcid = null; - if (is_array($files)) { - foreach ($files as $attachment) { - $tmp = []; - if (!in_array($attachment, $decoded_files, true)) { - $tmp[] = $attachment; - } - $rcid = $this->getIdforCollection($tmp); - } - $this->umail->saveAttachments($rcid); - } + foreach ($files as $rid_string) { + $rid_string = (string) $rid_string; + if ($this->fdm->isLegacyPoolItemIdentifier($rid_string)) { + $this->fdm->unlinkFile($this->fdm->legacyPoolFilenameFromIdentifier($rid_string)); + continue; } - $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_SUCCESS, $this->lng->txt('mail_files_deleted'), true); + $rid = $this->storage->manage()->find($rid_string); + if ($rid !== null) { + $this->fdm->removeFromPool($rid); + } } + $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_SUCCESS, $this->lng->txt('mail_files_deleted'), true); + $this->ctrl->redirect($this); } @@ -288,15 +293,24 @@ private function showAttachmentsCommand(): void } $mail_data = $this->umail->retrieveFromStage(); - $files = $this->fdm->getUserFilesData(); + $stage_attachments = $mail_data['attachments'] ?? null; + $adopted_pool_hashes = []; + if ($stage_attachments instanceof MailAttachments && $stage_attachments->isIrss()) { + foreach ($this->fdm->getAttachmentListing($stage_attachments->rcid()) as $file) { + $adopted_pool_hashes[$file['md5']] = true; + } + } + + $files = $this->fdm->getUserPoolFilesData(); $records = []; $checked_items = []; foreach ($files as $file) { - if (is_array($mail_data['attachments']) && in_array($file['name'], $mail_data['attachments'], true)) { - $checked_items[] = urlencode($file['name']); + if (isset($adopted_pool_hashes[$file['md5']])) { + $checked_items[] = $file['rid']; } $records[] = [ + 'rid' => $file['rid'], 'filename' => $file['name'], 'filesize' => (int) $file['size'], 'filecreatedate' => (int) $file['ctime'], @@ -340,6 +354,12 @@ private function handleTableActionsCommand(): void { $query = $this->http->wrapper()->query(); if (!$query->has('mail_attachments_table_action')) { + $this->tpl->setOnScreenMessage( + $this->tpl::MESSAGE_TYPE_FAILURE, + $this->lng->txt('select_one'), + true + ); + $this->showAttachmentsCommand(); return; } @@ -347,7 +367,7 @@ private function handleTableActionsCommand(): void match ($action) { self::TABLE_ACTION_SAVE_ATTACHMENTS => $this->saveAttachments(), self::TABLE_CONFIRM_DELETE_ATTACHMENTS => $this->confirmDeleteAttachments(), - default => $this->ctrl->redirect($this), + default => $this->showAttachmentsCommand(), }; } @@ -358,14 +378,15 @@ protected function getUploadResult(): HandlerResult $result = end($array); if ($result instanceof UploadResult && $result->isOK()) { - $identifier = $this->fdm->storeUploadedFile($result); + $rid = $this->fdm->uploadToPool($result); $status = HandlerResult::STATUS_OK; + $identifier = $rid->serialize(); $message = $this->lng->txt('saved_successfully'); $this->tpl->setOnScreenMessage($this->tpl::MESSAGE_TYPE_SUCCESS, $this->lng->txt('saved_successfully'), true); } else { $status = HandlerResult::STATUS_FAILED; $identifier = ''; - $message = $result->getStatus()->getMessage(); + $message = $result instanceof UploadResult ? $result->getStatus()->getMessage() : ''; } return new BasicHandlerResult($this->getFileIdentifierParameterName(), $status, $identifier, $message); @@ -373,17 +394,43 @@ protected function getUploadResult(): HandlerResult protected function getRemoveResult(string $identifier): HandlerResult { - throw new DomainException('Not necessary for this handler'); + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $identifier, + 'file deleted' + ); } public function getInfoResult(string $identifier): ?FileInfoResult { - throw new DomainException('Not necessary for this handler'); + $rid = $this->storage->manage()->find($identifier); + if ($rid === null) { + return new BasicFileInfoResult($this->getFileIdentifierParameterName(), 'unknown', 'unknown', 0, 'unknown'); + } + + $info = $this->storage->manage()->getCurrentRevision($rid)->getInformation(); + + return new BasicFileInfoResult( + $this->getFileIdentifierParameterName(), + $identifier, + $info->getTitle(), + $info->getSize(), + $info->getMimeType() + ); } public function getInfoForExistingFiles(array $file_ids): array { - throw new DomainException('Not necessary for this handler'); + $infos = []; + foreach ($file_ids as $file_id) { + $info = $this->getInfoResult($file_id); + if ($info !== null) { + $infos[] = $info; + } + } + + return $infos; } public function getFileIdentifierParameterName(): string diff --git a/components/ILIAS/Mail/classes/class.ilMailAttachmentStakeholder.php b/components/ILIAS/Mail/classes/class.ilMailAttachmentStakeholder.php index af9a116d3bab..cac1dd147e8a 100644 --- a/components/ILIAS/Mail/classes/class.ilMailAttachmentStakeholder.php +++ b/components/ILIAS/Mail/classes/class.ilMailAttachmentStakeholder.php @@ -18,14 +18,11 @@ declare(strict_types=1); +use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Stakeholder\AbstractResourceStakeholder; class ilMailAttachmentStakeholder extends AbstractResourceStakeholder { - public function __construct() - { - } - public function getId(): string { return 'mail_attachments'; @@ -33,6 +30,36 @@ public function getId(): string public function getOwnerOfNewResources(): int { - return 6; + return $this->default_owner; + } + + public function getOwnerOfResource(ResourceIdentification $identification): int + { + return $this->default_owner; + } + + public function canBeAccessedByCurrentUser(ResourceIdentification $identification): bool + { + global $DIC; + + if (!$DIC->isDependencyAvailable('user') || !$DIC->isDependencyAvailable('database')) { + return false; + } + + $user_id = $DIC->user()->getId(); + $db = $DIC->database(); + $rid = $identification->serialize(); + + $res = $db->queryF( + 'SELECT 1 FROM il_resource_rca rca + INNER JOIN mail_attachment ma ON ma.rcid = rca.rcid + INNER JOIN mail m ON m.mail_id = ma.mail_id + WHERE rca.rid = %s AND m.user_id = %s + LIMIT 1', + [ilDBConstants::T_TEXT, ilDBConstants::T_INTEGER], + [$rid, $user_id] + ); + + return $db->numRows($res) > 0; } } diff --git a/components/ILIAS/Mail/classes/class.ilMailFolderGUI.php b/components/ILIAS/Mail/classes/class.ilMailFolderGUI.php index 2eb886d7e37c..c887130ffd4a 100755 --- a/components/ILIAS/Mail/classes/class.ilMailFolderGUI.php +++ b/components/ILIAS/Mail/classes/class.ilMailFolderGUI.php @@ -28,8 +28,8 @@ use ILIAS\Mail\Folder\MailFolderSearch; use ILIAS\Mail\Folder\MailFolderTableUI; use ILIAS\Mail\Folder\MailFolderData; -use ILIAS\Filesystem\Stream\Streams; -use ILIAS\User\Profile\PublicProfileGUI; +use ILIAS\Mail\Attachments\MailAttachments; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; /** * @ilCtrl_Calls ilMailFolderGUI: ILIAS\User\Profile\PublicProfileGUI @@ -929,12 +929,20 @@ protected function showMail(): void $form->addItem($message); - if ($mail_data['attachments']) { + $mail_attachments = $mail_data['attachments'] ?? null; + if ($mail_attachments instanceof MailAttachments && !$mail_attachments->isEmpty()) { $att = new ilCustomInputGUI($this->lng->txt('attachments') . ':'); + $mail_file_data = new ilFileDataMail($this->user->getId()); $radiog = new ilRadioGroupInputGUI('', 'filename'); - foreach ($mail_data['attachments'] as $file) { - $radiog->addOption(new ilRadioOption($file, md5($file))); + if ($mail_attachments->isIrss()) { + foreach ($mail_file_data->getAttachmentListing($mail_attachments->rcid()) as $file) { + $radiog->addOption(new ilRadioOption($file['name'], $file['md5'])); + } + } else { + foreach ($mail_attachments->legacyFilenames() as $file) { + $radiog->addOption(new ilRadioOption($file, md5($file))); + } } $att->setHtml($radiog->render()); @@ -1167,7 +1175,11 @@ protected function deliverFile(): void $mail_file_data = new ilFileDataMail($this->user->getId()); try { $file = $mail_file_data->getAttachmentPathAndFilenameByMd5Hash($filename, (int) $mail_id); - ilFileDelivery::deliverFileLegacy($file['path'], $file['filename']); + if (isset($file['rcid']) && $file['rcid'] instanceof ResourceCollectionIdentification) { + $mail_file_data->deliverFile($file['rcid'], $file['md5']); + } else { + ilFileDelivery::deliverFileLegacy($file['path'], $file['filename']); + } } catch (OutOfBoundsException $e) { throw new ilMailException('mail_error_reading_attachment', $e->getCode(), $e); } @@ -1186,18 +1198,38 @@ protected function deliverAttachments(): void try { $mail_id = $this->getMailIdsFromRequest()[0] ?? 0; $mail_data = $this->umail->getMail($mail_id); - if ($mail_data === null || [] === (array) $mail_data['attachments']) { + if ($mail_data === null + || !($mail_data['attachments'] instanceof MailAttachments) + || $mail_data['attachments']->isEmpty()) { throw new ilMailException('mail_error_reading_attachment'); } + $attachments = $mail_data['attachments']; $type = $this->http->wrapper()->query()->retrieve( 'type', $this->refinery->byTrying([$this->refinery->kindlyTo()->string(), $this->refinery->always('')]) ); $mail_file_data = new ilFileDataMail($this->user->getId()); - if (count($mail_data['attachments']) === 1) { - $attachment = current($mail_data['attachments']); + + if ($attachments->isIrss()) { + $listing = $mail_file_data->getAttachmentListing($attachments->rcid()); + if (count($listing) === 1) { + $file = reset($listing); + $mail_file_data->deliverFile($attachments->rcid(), $file['md5']); + } else { + $mail_file_data->deliverAttachmentsAsZip( + $mail_data['m_subject'], + $mail_id, + [], + $type === 'draft' + ); + } + return; + } + + if ($attachments->isLegacy() && count($attachments->legacyFilenames()) === 1) { + $attachment = current($attachments->legacyFilenames()); try { if ($type === 'draft') { @@ -1206,25 +1238,30 @@ protected function deliverAttachments(): void } $path_to_file = $mail_file_data->getAbsoluteAttachmentPoolPathByFilename($attachment); $filename = $attachment; + ilFileDelivery::deliverFileLegacy($path_to_file, $filename); } else { $file = $mail_file_data->getAttachmentPathAndFilenameByMd5Hash( md5((string) $attachment), $mail_id ); - $path_to_file = $file['path']; - $filename = $file['filename']; + if (isset($file['rcid']) && $file['rcid'] instanceof ResourceCollectionIdentification) { + $mail_file_data->deliverFile($file['rcid'], $file['md5']); + } else { + ilFileDelivery::deliverFileLegacy($file['path'], $file['filename']); + } } - ilFileDelivery::deliverFileLegacy($path_to_file, $filename); } catch (OutOfBoundsException $e) { throw new ilMailException('mail_error_reading_attachment', $e->getCode(), $e); } - } else { + } elseif ($attachments->isLegacy()) { $mail_file_data->deliverAttachmentsAsZip( $mail_data['m_subject'], $mail_id, - $mail_data['attachments'], + $attachments->legacyFilenames(), $type === 'draft' ); + } else { + throw new ilMailException('mail_error_reading_attachment'); } } catch (Exception $e) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt($e->getMessage()), true); diff --git a/components/ILIAS/Mail/classes/class.ilMailFormGUI.php b/components/ILIAS/Mail/classes/class.ilMailFormGUI.php index 4f9c2878c016..8e70445be1db 100755 --- a/components/ILIAS/Mail/classes/class.ilMailFormGUI.php +++ b/components/ILIAS/Mail/classes/class.ilMailFormGUI.php @@ -34,6 +34,7 @@ use ILIAS\Mail\RecipientSearch\UserSearchEndpointConfigurator; use ILIAS\Data\Clock\ClockFactory; use ILIAS\Data\Factory as DataFactory; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\Mail\Folder\MailScheduleData; use ILIAS\UI\URLBuilder; use ILIAS\Data\URI; @@ -69,7 +70,7 @@ class ilMailFormGUI private readonly ilFileDataMail $mfile; private readonly GlobalHttpState $http; private readonly Refinery $refinery; - private ?array $request_attachments = null; + private ?MailAttachments $request_attachments = null; protected ilMailTemplateService $template_service; private readonly ilMailBodyPurifier $purifier; private string $mail_form_type = ''; @@ -211,28 +212,14 @@ public function executeCommand(): void } } - /** - * @param list $files - * @return list - */ - protected function decodeAttachmentFiles(array $files): array - { - $decoded_files = []; - foreach ($files as $value) { - if (is_file($this->mfile->getMailPath() . '/' . $this->user->getId() . '_' . urldecode($value))) { - $decoded_files[] = urldecode($value); - } - } - - return $decoded_files; - } public function saveMessageToOutbox(array $form_values, Form $form): void { - $files = []; - if (count($form_values['attachments']) > 0) { - $files = $this->handleAttachments($form_values['attachments']); - } + $stage = $this->umail->retrieveFromStage(); + $stage_attachments = ($stage['attachments'] ?? null) instanceof MailAttachments + ? $stage['attachments'] + : null; + $attachments = $this->attachmentsFromFormUpload($form_values['attachments'], $stage_attachments); $rcp_to = ''; $rcp_cc = ''; @@ -276,7 +263,7 @@ public function saveMessageToOutbox(array $form_values, Form $form): void $rcp_bcc, ilUtil::securePlainString($form_values['m_subject'] ?? $this->lng->txt('mail_no_subject')), $sanitized_message, - $files, + $attachments, $form_values['use_placeholders'], $outbox_id ?? null ), @@ -323,10 +310,11 @@ public function sendMessage(): void return; } - $files = []; - if (count($value['attachments']) > 0) { - $files = $this->handleAttachments($value['attachments']); - } + $stage = $this->umail->retrieveFromStage(); + $stage_attachments = ($stage['attachments'] ?? null) instanceof MailAttachments + ? $stage['attachments'] + : null; + $attachments = $this->attachmentsFromFormUpload($value['attachments'], $stage_attachments); $mailer = $this->umail ->withContextId(ilMailFormCall::getContextId() ?: '') @@ -355,7 +343,7 @@ public function sendMessage(): void $rcp_bcc, ilUtil::securePlainString($value['m_subject']), (new ilMailBody($value['m_message'], $this->purifier))->getContent(), - $files, + $attachments, $value['use_placeholders'] )) { $mailer->autoresponder()->disableAutoresponder(); @@ -419,10 +407,11 @@ public function saveDraft(): void if ($value['m_subject'] === '') { $value['m_subject'] = $this->lng->txt('mail_no_subject'); } - $files = []; - if (count($value['attachments']) > 0) { - $files = $this->handleAttachments($value['attachments']); - } + $stage = $this->umail->retrieveFromStage(); + $stage_attachments = ($stage['attachments'] ?? null) instanceof MailAttachments + ? $stage['attachments'] + : null; + $attachments = $this->attachmentsFromFormUpload($value['attachments'], $stage_attachments); $draft_folder_id = $this->mbox->getDraftsFolder(); @@ -431,7 +420,7 @@ public function saveDraft(): void $rcp_bcc = !empty($value['rcp_bcc']) ? implode(',', $value['rcp_bcc']) : ''; if ($errors = $this->umail->validateRecipients($rcp_to, $rcp_cc, $rcp_bcc)) { - $this->request_attachments = $files; + $this->request_attachments = $attachments; $this->showSubmissionErrors($errors); $this->showForm($form); return; @@ -446,7 +435,7 @@ public function saveDraft(): void $this->umail->updateDraft( $draft_folder_id, - $files, + $attachments, $rcp_to, $rcp_cc, $rcp_bcc, @@ -711,8 +700,23 @@ public function showForm(?Form $form = null): void ilSession::set('draft', $mail_id); $mail_data = $this->umail->getMail($mail_id); - if (!is_null($mail_data['attachments']) || !empty($mail_data['attachments'])) { - $mail_data['attachments'] = $this->filesFromLegacyToIRSS($mail_data); + if ($mail_data['attachments'] instanceof MailAttachments && !$mail_data['attachments']->isEmpty()) { + $stage_attachments = $this->stageAttachmentsFromMailAttachments($mail_data['attachments']); + $this->umail->persistToStage( + $this->user->getId(), + (string) ($mail_data['rcp_to'] ?? ''), + (string) ($mail_data['rcp_cc'] ?? ''), + (string) ($mail_data['rcp_bcc'] ?? ''), + (string) ($mail_data['m_subject'] ?? ''), + (string) ($mail_data['m_message'] ?? ''), + $stage_attachments, + (bool) ($mail_data['use_placeholders'] ?? false), + $mail_data['tpl_ctx_id'] ?? null, + (array) ($mail_data['tpl_ctx_params'] ?? []) + ); + $mail_data['attachments'] = $this->formRidsFromMailAttachments($stage_attachments); + } else { + $mail_data['attachments'] = []; } ilMailFormCall::setContextId($mail_data['tpl_ctx_id']); @@ -731,15 +735,21 @@ public function showForm(?Form $form = null): void $mail_data['rcp_to'] = $mail_data['rcp_cc'] = $mail_data['rcp_bcc'] = ''; $mail_data['m_subject'] = $this->umail->formatForwardSubject($mail_data['m_subject'] ?? ''); $mail_data['m_message'] = $this->umail->prependSignature($mail_data['m_message'] ?? ''); - if (is_array($mail_data['attachments']) && count($mail_data['attachments']) && $error = $this->mfile->adoptAttachments( - $mail_data['attachments'], - $mail_id - )) { - $this->tpl->setOnScreenMessage('info', $error); - } - - if (!is_null($mail_data['attachments']) || ($mail_data['attachments'] != '')) { - $mail_data['attachments'] = $this->filesFromLegacyToIRSS($mail_data); + if ($mail_data['attachments'] instanceof MailAttachments && !$mail_data['attachments']->isEmpty()) { + $stage_attachments = $this->stageAttachmentsFromMailAttachments($mail_data['attachments']); + $this->umail->persistToStage( + $this->user->getId(), + '', + '', + '', + $mail_data['m_subject'] ?? '', + $mail_data['m_message'] ?? '', + $stage_attachments, + false + ); + $mail_data['attachments'] = $this->formRidsFromMailAttachments($stage_attachments); + } else { + $mail_data['attachments'] = []; } break; @@ -841,8 +851,17 @@ public function showForm(?Form $form = null): void } } - if ($this->request_attachments) { - $mail_data['attachments'] = $this->request_attachments; + if ($this->request_attachments instanceof MailAttachments) { + if ($this->request_attachments->isIrss()) { + $mail_data['attachments'] = $this->FilesFromIRSSToLegacy($this->request_attachments->rcid()); + } elseif ($this->request_attachments->isLegacy()) { + $rcid = $this->fdm->createCollectionFromPoolFilenames( + $this->request_attachments->legacyFilenames() + ); + $mail_data['attachments'] = $rcid !== null + ? $this->FilesFromIRSSToLegacy($rcid) + : []; + } } break; } @@ -919,10 +938,11 @@ private function saveMailBeforeSearch(?array $input_results = null): void $result = $input_results; } - $resource_collection_id = null; + $stage_attachments = null; if (!empty($result['attachments']->getValue())) { - $files = $this->handleAttachments($result['attachments']->getValue()); - $resource_collection_id = $this->getIdforCollection($files); + $stage_attachments = MailAttachments::fromIrss( + $this->handleAttachments($result['attachments']->getValue()) + ); } $rcp_to = implode(',', $result['rcp_to']->getValue() ?? []); @@ -936,7 +956,7 @@ private function saveMailBeforeSearch(?array $input_results = null): void $rcp_bcc, ilUtil::securePlainString($result['m_subject']->getValue()), ilUtil::securePlainString($result['m_message']->getValue()), - $resource_collection_id, + $stage_attachments, (bool) $result['use_placeholders']->getValue(), ilMailFormCall::getContextId(), ilMailFormCall::getContextParameters() @@ -982,6 +1002,7 @@ private function getUserSearchConfigurator(): \ILIAS\User\Search\EndpointConfigu protected function buildFormElements(?array $mail_data): array { + $mail_data ??= []; $ff = $this->ui_factory->input()->field(); $rcp_to = $this->user_search->getInput( @@ -1014,17 +1035,26 @@ protected function buildFormElements(?array $mail_data): array } } - $has_files = !empty($mail_data['attachments']); $attachments = $ff->file( $this->upload_handler, $this->lng->txt('attachments') )->withMaxFiles(10); - if (isset($mail_data['attachments']) && $has_files) { - if ($mail_data['attachments'] instanceof \ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification) { - $mail_data['attachments'] = $this->FilesFromIRSSToLegacy($mail_data['attachments']); + $mail_attachments = $mail_data['attachments'] ?? null; + if ($mail_attachments instanceof MailAttachments && !$mail_attachments->isEmpty()) { + if ($mail_attachments->isIrss()) { + $mail_data['attachments'] = $this->FilesFromIRSSToLegacy($mail_attachments->rcid()); + } elseif ($mail_attachments->isLegacy()) { + $rcid = $this->fdm->createCollectionFromPoolFilenames($mail_attachments->legacyFilenames()); + $mail_data['attachments'] = $rcid !== null + ? $this->FilesFromIRSSToLegacy($rcid) + : []; + } else { + $mail_data['attachments'] = []; } - $attachments = $attachments->withValue($mail_data['attachments'] ?? []); + $attachments = $attachments->withValue($mail_data['attachments']); + } elseif (is_array($mail_attachments) && $mail_attachments !== []) { + $attachments = $attachments->withValue($mail_attachments); } $template_chb = null; diff --git a/components/ILIAS/Mail/classes/class.ilMailNotification.php b/components/ILIAS/Mail/classes/class.ilMailNotification.php index 0b79f5d3313d..b7fffae61980 100755 --- a/components/ILIAS/Mail/classes/class.ilMailNotification.php +++ b/components/ILIAS/Mail/classes/class.ilMailNotification.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + abstract class ilMailNotification { final public const int SUBJECT_TITLE_LENGTH = 60; @@ -27,7 +29,7 @@ abstract class ilMailNotification protected ?ilMail $mail = null; protected string $subject = ''; protected string $body = ''; - protected array $attachments = []; + protected MailAttachments $attachments; protected ilLanguage $language; protected array $lang_modules = []; protected array $recipients = []; @@ -41,6 +43,7 @@ abstract class ilMailNotification public function __construct(protected bool $is_in_wsp = false) { global $DIC; + $this->attachments = MailAttachments::empty(); $this->setSender(ANONYMOUS_USER_ID); $this->language = ilLanguageFactory::_getLanguage($DIC->language()->getDefaultLanguage()); @@ -109,12 +112,12 @@ public function getRecipients(): array return $this->recipients; } - public function setAttachments(array $a_att): void + public function setAttachments(MailAttachments $attachments): void { - $this->attachments = $a_att; + $this->attachments = $attachments; } - public function getAttachments(): array + public function getAttachments(): MailAttachments { return $this->attachments; } diff --git a/components/ILIAS/Mail/classes/class.ilMimeMail.php b/components/ILIAS/Mail/classes/class.ilMimeMail.php index 9e80aee45fb7..4c31635c85f0 100755 --- a/components/ILIAS/Mail/classes/class.ilMimeMail.php +++ b/components/ILIAS/Mail/classes/class.ilMimeMail.php @@ -19,7 +19,9 @@ declare(strict_types=1); use ILIAS\Data\Factory; +use ILIAS\Mail\Mime\MailMimeAttachment; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\ResourceStorage\Identification\ResourceIdentification; class ilMimeMail { @@ -47,14 +49,8 @@ class ilMimeMail protected array $abcc = []; /** @var array */ protected array $images = []; - /** @var string[] */ - protected array $aattach = []; - /** @var string[] */ - protected array $actype = []; - /** @var string[] */ - protected array $adispo = []; - /** @var string[] */ - protected array $adisplay = []; + /** @var list */ + protected array $mime_attachments = []; private readonly Refinery $refinery; /** @var Closure(string): string|null */ private ?Closure $to_html_transformation = null; @@ -195,35 +191,53 @@ public function Attach( string $disposition = 'inline', ?string $display_name = null ): void { - if ($file_type === '') { - $file_type = 'application/octet-stream'; - } + $this->mime_attachments[] = MailMimeAttachment::fromPath( + $filename, + $file_type, + $disposition, + $display_name + ); + } - $this->aattach[] = $filename; - $this->actype[] = $file_type; - $this->adispo[] = $disposition; - $this->adisplay[] = $display_name; + public function AttachResource( + ResourceIdentification $resource_identification, + string $display_name, + string $file_type = '', + string $disposition = 'inline' + ): void { + $this->mime_attachments[] = MailMimeAttachment::fromResource( + $resource_identification, + $display_name, + $file_type, + $disposition + ); + } + + /** + * @return list + */ + public function getMimeAttachments(): array + { + return $this->mime_attachments; } /** * @return array{path: string, name: string}[] + * @deprecated Use getMimeAttachments() for path and IRSS attachments. */ public function getAttachments(): array { $attachments = []; - $i = 0; - foreach ($this->aattach as $attachment) { - $name = ''; - if (isset($this->adisplay[$i]) && is_string($this->adisplay[$i]) && $this->adisplay[$i] !== '') { - $name = $this->adisplay[$i]; + foreach ($this->mime_attachments as $attachment) { + if ($attachment->isResource() || $attachment->getPath() === null) { + continue; } $attachments[] = [ - 'path' => $attachment, - 'name' => $name + 'path' => $attachment->getPath(), + 'name' => $attachment->getDisplayName(), ]; - ++$i; } return $attachments; diff --git a/components/ILIAS/Mail/classes/class.ilObjMailGUI.php b/components/ILIAS/Mail/classes/class.ilObjMailGUI.php index 20093d1c0aea..bd29e1722d56 100755 --- a/components/ILIAS/Mail/classes/class.ilObjMailGUI.php +++ b/components/ILIAS/Mail/classes/class.ilObjMailGUI.php @@ -19,6 +19,7 @@ declare(strict_types=1); use ILIAS\Mail\Autoresponder\AutoresponderService; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\Mail\Signature\MailUserSignature; use ILIAS\Mail\Signature\Signature; use ILIAS\Mail\Signature\MailInstallationSignature; @@ -388,7 +389,7 @@ protected function sendTestMail(bool $is_manual_mail = false): void '', $this->lng->txt('mail_email_' . $lng_variable_prefix . '_subject'), $this->lng->txt('mail_email_' . $lng_variable_prefix . '_body'), - [] + MailAttachments::empty() ); $this->tpl->setOnScreenMessage('success', $this->lng->txt('mail_external_test_sent')); diff --git a/components/ILIAS/Mail/tests/MailAttachmentsTest.php b/components/ILIAS/Mail/tests/MailAttachmentsTest.php new file mode 100644 index 000000000000..b6219a5f049c --- /dev/null +++ b/components/ILIAS/Mail/tests/MailAttachmentsTest.php @@ -0,0 +1,91 @@ +serialize()); + + $this->assertInstanceOf(MailAttachments::class, $parsed); + $this->assertTrue($parsed->isIrss()); + $this->assertSame('rcid-123', $parsed->rcid()->serialize()); + } + + public function testParseLegacyArray(): void + { + $parsed = MailAttachments::fromDb(serialize(['file.pdf', 'image.png'])); + + $this->assertTrue($parsed->isLegacy()); + $this->assertSame(['file.pdf', 'image.png'], $parsed->legacyFilenames()); + } + + public function testSerializeRcidForBackgroundTask(): void + { + $attachments = MailAttachments::fromIrss(new ResourceCollectionIdentification('rcid-456')); + $parsed = MailAttachments::fromBackgroundTask($attachments->toBackgroundTask()); + + $this->assertTrue($parsed->isIrss()); + $this->assertSame('rcid-456', $parsed->rcid()->serialize()); + } + + public function testParseEmptyLegacyArray(): void + { + $parsed = MailAttachments::fromDb(serialize([])); + + $this->assertNotNull($parsed); + $this->assertTrue($parsed->isEmpty()); + $this->assertFalse($parsed->isLegacy()); + } + + public function testParseEmptyStringReturnsNull(): void + { + $this->assertNull(MailAttachments::fromDb('')); + $this->assertNull(MailAttachments::fromDb(null)); + } + + public function testParseSerializedRcidWrapper(): void + { + $raw = serialize([MailAttachments::SERIALIZED_RCID_KEY => 'rcid-wrapper']); + $parsed = MailAttachments::fromDb($raw); + + $this->assertNotNull($parsed); + $this->assertTrue($parsed->isIrss()); + $this->assertSame('rcid-wrapper', $parsed->rcid()->serialize()); + } + + public function testLegacyFilenamesThrowForIrss(): void + { + $attachments = MailAttachments::fromIrss(new ResourceCollectionIdentification('rcid-123')); + $this->expectException(InvalidArgumentException::class); + $attachments->legacyFilenames(); + } + + public function testToDbLegacySerialization(): void + { + $attachments = MailAttachments::fromLegacyFilenames(['a.pdf', 'b.png']); + $this->assertSame(serialize(['a.pdf', 'b.png']), $attachments->toDb()); + } +} diff --git a/components/ILIAS/Mail/tests/MigrateMailAttachmentsToIRSSUnitTest.php b/components/ILIAS/Mail/tests/MigrateMailAttachmentsToIRSSUnitTest.php new file mode 100644 index 000000000000..fbda226002e3 --- /dev/null +++ b/components/ILIAS/Mail/tests/MigrateMailAttachmentsToIRSSUnitTest.php @@ -0,0 +1,194 @@ +migration = new MigrateMailAttachmentsToIRSS(); + $this->helper = $this->getMockBuilder(ilResourceStorageMigrationHelper::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'getDatabase', + 'getClientDataDir', + 'moveFilesOfPathToCollection', + 'movePathToStorage', + 'getCollectionBuilder', + ]) + ->getMock(); + $this->db = $this->createMock(ilDBInterface::class); + + $this->helper->method('getDatabase')->willReturn($this->db); + $this->helper->method('getClientDataDir')->willReturn('/tmp/client'); + + $reflection = new ReflectionClass($this->migration); + $property = $reflection->getProperty('helper'); + $property->setValue($this->migration, $this->helper); + } + + public function testResolveOwnerIdForPathUsesSenderId(): void + { + $statement = $this->createMock(ilDBStatement::class); + $this->db->expects($this->once()) + ->method('queryF') + ->willReturn($statement); + $this->db->expects($this->once()) + ->method('fetchObject') + ->with($statement) + ->willReturn((object) ['sender_id' => 42]); + + $owner_id = $this->invokePrivate('resolveOwnerIdForPath', ['migtest/s1']); + + $this->assertSame(42, $owner_id); + } + + public function testResolveOwnerIdForPathFallsBackToSystemUser(): void + { + $statement = $this->createMock(ilDBStatement::class); + $this->db->method('queryF')->willReturn($statement); + $this->db->method('fetchObject')->willReturn((object) ['sender_id' => 0]); + + $owner_id = $this->invokePrivate('resolveOwnerIdForPath', ['migtest/s5']); + + $this->assertSame(SYSTEM_USER_ID, $owner_id); + } + + public function testMigratePoolFilenamesToCollectionSkipsMissingFiles(): void + { + $dir = sys_get_temp_dir() . '/mail-migration-unit-' . uniqid('', true); + $mail_path = $dir . '/mail'; + mkdir($mail_path, 0775, true); + file_put_contents($mail_path . '/6_present.svg', ''); + + $this->helper->method('getClientDataDir')->willReturn($dir); + + $collection = $this->createMock(ResourceCollection::class); + $collection->method('count')->willReturn(1); + $collection->expects($this->once())->method('add'); + $collection->method('getIdentification')->willReturn( + new ResourceCollectionIdentification('migrated-rcid') + ); + + $collection_builder = $this->createMock(CollectionBuilder::class); + $collection_builder->method('new')->willReturn($collection); + $collection_builder->method('store')->willReturn(true); + + $this->helper->method('getCollectionBuilder')->willReturn($collection_builder); + $this->helper->expects($this->once()) + ->method('movePathToStorage') + ->willReturn(new ResourceIdentification('rid-present')); + + $rcid = $this->invokePrivate('migratePoolFilenamesToCollection', [ + ['present.svg', 'missing.svg'], + 6, + $mail_path, + ]); + + $this->assertInstanceOf(ResourceCollectionIdentification::class, $rcid); + $this->assertSame('migrated-rcid', $rcid->serialize()); + + unlink($mail_path . '/6_present.svg'); + rmdir($mail_path); + rmdir($dir); + } + + public function testMigratePoolFilenamesToCollectionReturnsNullWhenNoFilesExist(): void + { + $dir = sys_get_temp_dir() . '/mail-migration-unit-empty-' . uniqid('', true); + $mail_path = $dir . '/mail'; + mkdir($mail_path, 0775, true); + + $collection = $this->createMock(ResourceCollection::class); + $collection->method('count')->willReturn(0); + + $collection_builder = $this->createMock(CollectionBuilder::class); + $collection_builder->method('new')->willReturn($collection); + + $this->helper->method('getCollectionBuilder')->willReturn($collection_builder); + $this->helper->expects($this->never())->method('movePathToStorage'); + + $rcid = $this->invokePrivate('migratePoolFilenamesToCollection', [ + ['missing.svg'], + 6, + $mail_path, + ]); + + $this->assertNull($rcid); + + rmdir($mail_path); + rmdir($dir); + } + + public function testUpdateMailAttachmentFieldsSkipsNonSerializedAttachments(): void + { + $statement = $this->createMock(ilDBStatement::class); + $this->db->expects($this->once())->method('queryF')->willReturn($statement); + $this->db->method('fetchObject')->willReturnOnConsecutiveCalls( + (object) ['mail_id' => 900011, 'attachments' => '657497dc-5079-4f95-b19d-aecdaf81ff1a'], + null + ); + $this->db->expects($this->never())->method('update'); + + $this->invokePrivate('updateMailAttachmentFields', [ + 'migtest/s11', + new ResourceCollectionIdentification('new-rcid'), + ]); + } + + public function testMarkPathAsSkippedWritesDashMarker(): void + { + $this->db->expects($this->once()) + ->method('manipulateF') + ->with( + 'UPDATE mail_attachment SET rcid = %s WHERE path = %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + ['-', 'migtest/missing'] + ); + + $this->invokePrivate('markPathAsSkipped', ['migtest/missing']); + } + + /** + * @param list $arguments + */ + private function invokePrivate(string $method, array $arguments): mixed + { + $reflection = new ReflectionClass($this->migration); + + return $reflection->getMethod($method)->invoke($this->migration, ...$arguments); + } +} diff --git a/components/ILIAS/Mail/tests/ilFileDataMailPoolTest.php b/components/ILIAS/Mail/tests/ilFileDataMailPoolTest.php new file mode 100644 index 000000000000..bcc813b81ea6 --- /dev/null +++ b/components/ILIAS/Mail/tests/ilFileDataMailPoolTest.php @@ -0,0 +1,147 @@ +file_data_mail = $this->getMockBuilder(ilFileDataMail::class) + ->disableOriginalConstructor() + ->onlyMethods(['migrateLegacyPoolFilenameToResource']) + ->getMock(); + } + + public function testLegacyPoolItemIdentifier(): void + { + $this->assertTrue( + $this->file_data_mail->isLegacyPoolItemIdentifier('legacy:chef-man-cap-svgrepo-com.svg') + ); + $this->assertFalse( + $this->file_data_mail->isLegacyPoolItemIdentifier('657497dc-5079-4f95-b19d-aecdaf81ff1a') + ); + } + + public function testLegacyPoolFilenameFromIdentifier(): void + { + $this->assertSame( + 'pepper-svgrepo-com.svg', + $this->file_data_mail->legacyPoolFilenameFromIdentifier('legacy:pepper-svgrepo-com.svg') + ); + } + + public function testResolvePoolIdentifiersMigratesLegacyEntries(): void + { + $legacy_rid = new ResourceIdentification('legacy-rid'); + $irss_rid = new ResourceIdentification('irss-rid'); + + $this->file_data_mail + ->expects($this->once()) + ->method('migrateLegacyPoolFilenameToResource') + ->with('old.svg') + ->willReturn($legacy_rid); + + $irss = $this->createIrssMock([ + 'irss-rid' => $irss_rid, + ]); + $this->injectIrss($irss); + + $resolved = $this->file_data_mail->resolvePoolIdentifiersToResources([ + 'legacy:old.svg', + 'irss-rid', + 'unknown-rid', + ]); + + $this->assertCount(2, $resolved); + $this->assertSame('legacy-rid', $resolved[0]->serialize()); + $this->assertSame('irss-rid', $resolved[1]->serialize()); + } + + public function testMigrateLegacyPoolAttachmentsReturnsIrssWhenAlreadyMigrated(): void + { + $irss = MailAttachments::fromIrss( + new ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification('rcid-1') + ); + + $partial = $this->getMockBuilder(ilFileDataMail::class) + ->disableOriginalConstructor() + ->onlyMethods(['checkFilesExist', 'createCollectionFromPoolFilenames']) + ->getMock(); + + $partial->expects($this->never())->method('checkFilesExist'); + $partial->expects($this->never())->method('createCollectionFromPoolFilenames'); + + $result = $partial->migrateLegacyPoolAttachments($irss); + + $this->assertSame($irss, $result); + } + + public function testMigrateLegacyPoolAttachmentsReturnsNullWhenFilesMissing(): void + { + $legacy = MailAttachments::fromLegacyFilenames(['missing.svg']); + + $partial = $this->getMockBuilder(ilFileDataMail::class) + ->disableOriginalConstructor() + ->onlyMethods(['checkFilesExist', 'createCollectionFromPoolFilenames']) + ->getMock(); + + $partial->expects($this->once()) + ->method('checkFilesExist') + ->with(['missing.svg']) + ->willReturn(false); + $partial->expects($this->never())->method('createCollectionFromPoolFilenames'); + + $this->assertNull($partial->migrateLegacyPoolAttachments($legacy)); + } + + /** + * @param array $known_rids + */ + private function createIrssMock(array $known_rids): IRSS&MockObject + { + $manage = $this->createMock(Manager::class); + $manage->method('find')->willReturnCallback( + static function (string $rid) use ($known_rids): ?ResourceIdentification { + return $known_rids[$rid] ?? null; + } + ); + + $irss = $this->getMockBuilder(IRSS::class)->disableOriginalConstructor()->getMock(); + $irss->method('manage')->willReturn($manage); + + return $irss; + } + + private function injectIrss(IRSS $irss): void + { + $reflection = new ReflectionClass(ilFileDataMail::class); + $property = $reflection->getProperty('irss'); + $property->setValue($this->file_data_mail, $irss); + } +} diff --git a/components/ILIAS/Mail/tests/ilMailCopyOnSendTest.php b/components/ILIAS/Mail/tests/ilMailCopyOnSendTest.php new file mode 100644 index 000000000000..ec8f66e25f75 --- /dev/null +++ b/components/ILIAS/Mail/tests/ilMailCopyOnSendTest.php @@ -0,0 +1,128 @@ +createMailInstance(); + $this->mail_file_data->expects($this->never())->method('copyCollectionForDelivery'); + + $legacy = MailAttachments::fromLegacyFilenames(['file.pdf']); + $result = $this->invokeDeliveryAttachments($legacy, $mail); + + $this->assertSame($legacy, $result); + } + + public function testDeliveryAttachmentsReturnsEmptyUnchanged(): void + { + $mail = $this->createMailInstance(); + $this->mail_file_data->expects($this->never())->method('copyCollectionForDelivery'); + + $empty = MailAttachments::empty(); + $result = $this->invokeDeliveryAttachments($empty, $mail); + + $this->assertTrue($result->isEmpty()); + } + + public function testDeliveryAttachmentsCopiesIrssCollectionPerCall(): void + { + $mail = $this->createMailInstance(); + $source = new ResourceCollectionIdentification('source-rcid'); + $copy_a = new ResourceCollectionIdentification('copy-a'); + $copy_b = new ResourceCollectionIdentification('copy-b'); + + $this->mail_file_data + ->expects($this->exactly(2)) + ->method('copyCollectionForDelivery') + ->with($source) + ->willReturnOnConsecutiveCalls($copy_a, $copy_b); + + $first = $this->invokeDeliveryAttachments(MailAttachments::fromIrss($source), $mail); + $second = $this->invokeDeliveryAttachments(MailAttachments::fromIrss($source), $mail); + + $this->assertSame('copy-a', $first->rcid()->serialize()); + $this->assertSame('copy-b', $second->rcid()->serialize()); + } + + public function testDeliveryAttachmentsReturnsEmptyWhenCopyFails(): void + { + $mail = $this->createMailInstance(); + $source = new ResourceCollectionIdentification('source-rcid'); + $this->mail_file_data + ->expects($this->once()) + ->method('copyCollectionForDelivery') + ->willReturn(null); + + $result = $this->invokeDeliveryAttachments(MailAttachments::fromIrss($source), $mail); + + $this->assertTrue($result->isEmpty()); + } + + public function testSharedDeliveryAttachmentsReuseSingleCopy(): void + { + $mail = $this->createMailInstance(); + $source = new ResourceCollectionIdentification('source-rcid'); + $shared = new ResourceCollectionIdentification('shared-copy'); + + $this->mail_file_data + ->expects($this->once()) + ->method('copyCollectionForDelivery') + ->with($source) + ->willReturn($shared); + + $mail->setShareAttachments(true); + + $first = $this->invokeDeliveryAttachments(MailAttachments::fromIrss($source), $mail); + $second = $this->invokeDeliveryAttachments(MailAttachments::fromIrss($source), $mail); + + $this->assertSame('shared-copy', $first->rcid()->serialize()); + $this->assertSame($first, $second); + } + + private function createMailInstance(): ilMail + { + $this->mail_file_data = $this->getMockBuilder(ilFileDataMail::class) + ->disableOriginalConstructor() + ->onlyMethods(['copyCollectionForDelivery']) + ->getMock(); + + $mail = (new ReflectionClass(ilMail::class))->newInstanceWithoutConstructor(); + + $reflection = new ReflectionClass(ilMail::class); + $reflection->getProperty('mail_file_data')->setValue($mail, $this->mail_file_data); + + return $mail; + } + + private function invokeDeliveryAttachments(MailAttachments $source, ilMail $mail): MailAttachments + { + $reflection = new ReflectionClass($mail); + + return $reflection->getMethod('getDeliveryAttachments')->invoke($mail, $source); + } +} diff --git a/components/ILIAS/Mail/tests/ilMailMimeTest.php b/components/ILIAS/Mail/tests/ilMailMimeTest.php index 7b72c166ce00..310ed8549e8d 100755 --- a/components/ILIAS/Mail/tests/ilMailMimeTest.php +++ b/components/ILIAS/Mail/tests/ilMailMimeTest.php @@ -101,7 +101,9 @@ public function testTransportFactoryWillReturnNullTransportIfExternalEmailDelive 'raise', ])->getMock(); - $factory = new ilMailMimeTransportFactory($settings, $event_handler); + $irss = $this->getMockBuilder(ILIAS\ResourceStorage\Services::class)->disableOriginalConstructor()->getMock(); + + $factory = new ilMailMimeTransportFactory($settings, $event_handler, $irss); $this->assertInstanceOf(ilMailMimeTransportNull::class, $factory->getTransport()); } @@ -118,7 +120,9 @@ public function testTransportFactoryWillReturnSmtpTransportIfEnabled(): void 'raise', ])->getMock(); - $factory = new ilMailMimeTransportFactory($settings, $event_handler); + $irss = $this->getMockBuilder(ILIAS\ResourceStorage\Services::class)->disableOriginalConstructor()->getMock(); + + $factory = new ilMailMimeTransportFactory($settings, $event_handler, $irss); $this->assertInstanceOf(ilMailMimeTransportSmtp::class, $factory->getTransport()); } @@ -147,7 +151,9 @@ public function testTransportFactoryWillReturnSendmailTransportIfSmtpTransportIs 'raise', ])->getMock(); - $factory = new ilMailMimeTransportFactory($settings, $event_handler); + $irss = $this->getMockBuilder(ILIAS\ResourceStorage\Services::class)->disableOriginalConstructor()->getMock(); + + $factory = new ilMailMimeTransportFactory($settings, $event_handler, $irss); $this->assertInstanceOf(ilMailMimeTransportSendmail::class, $factory->getTransport()); } diff --git a/components/ILIAS/Mail/tests/ilMailTaskProcessorTest.php b/components/ILIAS/Mail/tests/ilMailTaskProcessorTest.php index 32a1b31d6efb..f397da5d97f9 100755 --- a/components/ILIAS/Mail/tests/ilMailTaskProcessorTest.php +++ b/components/ILIAS/Mail/tests/ilMailTaskProcessorTest.php @@ -21,6 +21,7 @@ use ILIAS\BackgroundTasks\Implementation\TaskManager\BasicTaskManager; use ILIAS\BackgroundTasks\Task\TaskFactory; use ILIAS\DI\Container; +use ILIAS\Mail\Attachments\MailAttachments; class ilMailTaskProcessorTest extends ilMailBaseTestCase { @@ -58,7 +59,7 @@ public function testMailValueObjectCannotBeCreatedWithUnsupportedSubjectLength() '', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce mollis posuere tincidunt. Phasellus et euismod ligula. Suspendisse dignissim eget dui nec imperdiet. Donec in pretium tellus. Maecenas lacinia eleifend erat ut euismod. Aenean eu malesuada est.', 'Dear Steve, great!', - [] + MailAttachments::empty() ); } @@ -108,7 +109,7 @@ public function testOneTask(): void '', 'That is awesome!', 'Dear Steve, great!', - [] + MailAttachments::empty() ); $value_objects = [ @@ -175,7 +176,7 @@ public function testRunTwoTasks(): void '', 'That is awesome!', 'Dear Steve, great!', - [] + MailAttachments::empty() ); $value_objects[] = new ilMailValueObject( @@ -185,7 +186,7 @@ public function testRunTwoTasks(): void '', 'Greate', 'Steve, Steve, Steve. Wait that is not Steve', - [] + MailAttachments::empty() ); $usr_id = 100; @@ -248,7 +249,7 @@ public function testRunThreeTasksInDifferentBuckets(): void '', 'That is awesome!', 'Dear Steve, great!', - [] + MailAttachments::empty() ); $value_objects[] = new ilMailValueObject( @@ -258,7 +259,7 @@ public function testRunThreeTasksInDifferentBuckets(): void '', 'Greate', 'Steve, Steve, Steve. Wait that is not Steve', - [] + MailAttachments::empty() ); $value_objects[] = new ilMailValueObject( @@ -268,7 +269,7 @@ public function testRunThreeTasksInDifferentBuckets(): void '', 'That is awesome!', 'Hey Alan! Alan! Alan!', - [] + MailAttachments::empty() ); $usr_id = 100; @@ -334,7 +335,7 @@ public function testRunHasWrongTypeAndWillResultInException(): void '', 'That is awesome!', 'Dear Steve, great!', - [] + MailAttachments::empty() ); $value_objects[] = new ilMailValueObject( @@ -344,7 +345,7 @@ public function testRunHasWrongTypeAndWillResultInException(): void '', 'Greate', 'Steve, Steve, Steve. Wait that is not Steve', - [] + MailAttachments::empty() ); $value_objects[] = 'This should fail'; diff --git a/components/ILIAS/Mail/tests/ilMailTest.php b/components/ILIAS/Mail/tests/ilMailTest.php index af1b17589623..1fd882f0d249 100755 --- a/components/ILIAS/Mail/tests/ilMailTest.php +++ b/components/ILIAS/Mail/tests/ilMailTest.php @@ -21,6 +21,7 @@ use ILIAS\Refinery\Factory; use PHPUnit\Framework\MockObject\MockObject; use ILIAS\Mail\Autoresponder\AutoresponderService; +use ILIAS\Mail\Attachments\MailAttachments; use ILIAS\LegalDocuments\Conductor; use ILIAS\Refinery\Transformation; use ILIAS\Data\Result\Ok; @@ -28,6 +29,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use ILIAS\Mail\TemplateEngine\TemplateEngineFactoryInterface; use ILIAS\Mail\TemplateEngine\Mustache\MustacheTemplateEngineFactory; +use ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification; class ilMailTest extends ilMailBaseTestCase { @@ -264,7 +266,7 @@ public function render(string $template, object|array $context): string implode(',', array_slice(array_keys($active_users_login_to_id_map), 5, 2)), 'Subject', 'Message', - [], + MailAttachments::empty(), false ); $mail_service->sendMail($mail_data); @@ -461,7 +463,7 @@ public function testUpdateDraft(): void $this->mock_database->expects($this->once())->method('update')->with('mail', [ 'folder_id' => ['integer', $folder_id], - 'attachments' => ['clob', serialize([])], + 'attachments' => [ilDBConstants::T_CLOB, serialize([])], 'send_time' => ['timestamp', date('Y-m-d H:i:s')], 'rcp_to' => ['clob', $to], 'rcp_cc' => ['clob', $cc], @@ -482,7 +484,7 @@ public function testUpdateDraft(): void $draft_id, $instance->updateDraft( $folder_id, - [], + MailAttachments::empty(), $to, $cc, $bcc, @@ -630,17 +632,34 @@ public function testGetIliasMailerName(): void public function testSaveAttachments(): void { $usr_id = 89; - $attachments = new \ILIAS\ResourceStorage\Identification\ResourceCollectionIdentification('657497dc-5079-4f95-b19d-aecdaf81ff1a'); + $attachments = MailAttachments::fromIrss( + new ResourceCollectionIdentification('657497dc-5079-4f95-b19d-aecdaf81ff1a') + ); $instance = $this->create(789, $usr_id); + $mock_statement = $this->getMockBuilder(ilDBStatement::class)->getMock(); - $this->mock_database->expects($this->once())->method('update')->with( + $this->mock_database->expects($this->exactly(2))->method('queryF')->willReturnCallback( + $this->queryCallback($mock_statement, ['integer'], [$usr_id]) + ); + $this->mock_database->expects($this->exactly(2))->method('fetchAssoc')->with($mock_statement)->willReturn([ + 'attachments' => null, + 'rcp_to' => '', + 'rcp_cc' => '', + 'rcp_bcc' => '', + 'm_subject' => '', + 'm_message' => '', + 'use_placeholders' => 0, + 'tpl_ctx_id' => null, + 'tpl_ctx_params' => '[]', + ]); + $this->mock_database->expects($this->once())->method('replace')->with( 'mail_saved', - [ - 'attachments' => ['text', $attachments->serialize()], - ], [ 'user_id' => ['integer', $usr_id], - ] + ], + $this->callback(static function (array $fields) use ($attachments): bool { + return ($fields['attachments'][1] ?? null) === $attachments->rcid()->serialize(); + }) ); $instance->saveAttachments($attachments); diff --git a/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronMinMembers.php b/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronMinMembers.php index b35b70407284..f49fe7db0e84 100755 --- a/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronMinMembers.php +++ b/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronMinMembers.php @@ -21,6 +21,7 @@ use ILIAS\Cron\Job\Schedule\JobScheduleType; use ILIAS\Cron\Job\JobResult; use ILIAS\Cron\CronJob; +use ILIAS\Mail\Attachments\MailAttachments; /** * Cron for course/group minimum members @@ -162,7 +163,7 @@ protected function sendMessage(int $a_reci_id, array $a_items): void '', $lng->txt("mem_cron_min_members_subject"), $ntf->composeAndGetMessage($a_reci_id, null, "read", true), - [] + MailAttachments::empty() ); } } diff --git a/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronNotifications.php b/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronNotifications.php index c0fea84531eb..501c8432f7d0 100755 --- a/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronNotifications.php +++ b/components/ILIAS/Membership/classes/Cron/class.ilMembershipCronNotifications.php @@ -21,6 +21,7 @@ use ILIAS\Cron\Job\Schedule\JobScheduleType; use ILIAS\Cron\Job\JobResult; use ILIAS\Cron\CronJob; +use ILIAS\Mail\Attachments\MailAttachments; /** * Course/group notifications @@ -475,7 +476,7 @@ protected function sendMail(int $a_user_id, array $a_objects, string $a_last_run (string) null, $subject, $mail_content, - [] + MailAttachments::empty() ); } diff --git a/components/ILIAS/Notes/Notifications/class.NotificationsManager.php b/components/ILIAS/Notes/Notifications/class.NotificationsManager.php index c54a86fb1f97..ebe13666ad2c 100755 --- a/components/ILIAS/Notes/Notifications/class.NotificationsManager.php +++ b/components/ILIAS/Notes/Notifications/class.NotificationsManager.php @@ -20,6 +20,8 @@ namespace ILIAS\Notes; +use ILIAS\Mail\Attachments\MailAttachments; + /** * @author Alexander Killing */ @@ -162,7 +164,7 @@ public function sendNotifications( "", $subject, $message, - array() + MailAttachments::empty() ); } } diff --git a/components/ILIAS/Notifications/classes/ilNotificationMailHandler.php b/components/ILIAS/Notifications/classes/ilNotificationMailHandler.php index 4d04e1a17238..628febdfe86b 100755 --- a/components/ILIAS/Notifications/classes/ilNotificationMailHandler.php +++ b/components/ILIAS/Notifications/classes/ilNotificationMailHandler.php @@ -22,6 +22,7 @@ use ILIAS\Notifications\Model\ilNotificationObject; use ilMail; +use ILIAS\Mail\Attachments\MailAttachments; /** * @author Jan Posselt @@ -39,7 +40,7 @@ public function notify(ilNotificationObject $notification): void '', $notification->title, $notification->longDescription, - [] + MailAttachments::empty() ); } } diff --git a/components/ILIAS/Skill/classes/class.ilSkillNotifications.php b/components/ILIAS/Skill/classes/class.ilSkillNotifications.php index 37faa43c7cf2..91c864a20d32 100755 --- a/components/ILIAS/Skill/classes/class.ilSkillNotifications.php +++ b/components/ILIAS/Skill/classes/class.ilSkillNotifications.php @@ -22,6 +22,7 @@ use ILIAS\Cron\Job\Schedule\JobScheduleType; use ILIAS\Cron\Job\JobResult; use ILIAS\Cron\CronJob; +use ILIAS\Mail\Attachments\MailAttachments; /** * Course/group skill notification @@ -233,7 +234,7 @@ protected function sendMail(int $a_user_id, array $a_achievements, string $a_las "", $subject, $ntf->composeAndGetMessage($a_user_id, null, "read", true), - [] + MailAttachments::empty() ); } } diff --git a/components/ILIAS/StudyProgramme/classes/Mail/class.ilPRGMail.php b/components/ILIAS/StudyProgramme/classes/Mail/class.ilPRGMail.php index 7bb1abd6b321..8bc7c6b2c123 100755 --- a/components/ILIAS/StudyProgramme/classes/Mail/class.ilPRGMail.php +++ b/components/ILIAS/StudyProgramme/classes/Mail/class.ilPRGMail.php @@ -19,6 +19,7 @@ declare(strict_types=1); use ILIAS\User\Settings\Settings as UserSettings; +use ILIAS\Mail\Attachments\MailAttachments; /** * Send mails to users (usually triggered by cron) @@ -96,7 +97,7 @@ protected function sendMail( $mail = new ilMail(ANONYMOUS_USER_ID); try { - $mail->enqueue($login, '', '', $subject, $body, []); + $mail->enqueue($login, '', '', $subject, $body, MailAttachments::empty()); return true; } catch (Exception $e) { $this->log->write($e->getMessage()); diff --git a/components/ILIAS/Survey/Participants/class.ilSurveyParticipantsGUI.php b/components/ILIAS/Survey/Participants/class.ilSurveyParticipantsGUI.php index 70fb9d0f124f..355260fab847 100755 --- a/components/ILIAS/Survey/Participants/class.ilSurveyParticipantsGUI.php +++ b/components/ILIAS/Survey/Participants/class.ilSurveyParticipantsGUI.php @@ -18,6 +18,7 @@ use ILIAS\Survey\CSVReader; use ILIAS\Survey\Participants; +use ILIAS\Mail\Attachments\MailAttachments; /** * Class ilSurveyParticipantsGUI @@ -1729,7 +1730,7 @@ public function mailRatersActionObject(): void "", // bcc $subj, // subject $mytxt, // message - array() // attachments + MailAttachments::empty() // attachments ); $this->object->set360RaterSent( diff --git a/components/ILIAS/Survey/Participants/class.ilSurveyRaterGUI.php b/components/ILIAS/Survey/Participants/class.ilSurveyRaterGUI.php index 608833e4f878..1b146f3a509c 100755 --- a/components/ILIAS/Survey/Participants/class.ilSurveyRaterGUI.php +++ b/components/ILIAS/Survey/Participants/class.ilSurveyRaterGUI.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * @author Alexander Killing * @ilCtrl_Calls ilSurveyRaterGUI: ilRepositorySearchGUI @@ -385,7 +387,7 @@ public function mailRatersAction(): void "", // bcc $subj, // subject $mytxt, // message - array() // attachments + MailAttachments::empty() // attachments ); $this->survey->set360RaterSent( diff --git a/components/ILIAS/Survey/classes/class.ilObjSurvey.php b/components/ILIAS/Survey/classes/class.ilObjSurvey.php index b46c6b549556..d9e63d0405cb 100755 --- a/components/ILIAS/Survey/classes/class.ilObjSurvey.php +++ b/components/ILIAS/Survey/classes/class.ilObjSurvey.php @@ -19,6 +19,7 @@ use ILIAS\Survey\Participants; use ILIAS\Survey\Mode; use ILIAS\Survey\InternalDomainService; +use ILIAS\Mail\Attachments\MailAttachments; /** * @author Helmut Schottmüller @@ -3817,7 +3818,7 @@ public function sendCodes( "", // bcc $subject, // subject $messagetext, // message - array() // attachments + MailAttachments::empty() // attachments ); } } @@ -4321,7 +4322,7 @@ public function sendAppraiseeNotification( "", $subject, $ntf->composeAndGetMessage($a_user_id, null, "read", true), - [] + MailAttachments::empty() ); } @@ -4350,7 +4351,7 @@ public function sendAppraiseeCloseNotification( "", $subject, $ntf->composeAndGetMessage($a_user_id, null, "read", true), - [] + MailAttachments::empty() ); } @@ -4381,7 +4382,7 @@ public function sendRaterNotification( "", $subject, $ntf->composeAndGetMessage($a_user_id, null, "read", true), - [] + MailAttachments::empty() ); } @@ -5244,7 +5245,7 @@ public function send360ReminderToUser( "", $subject, $ntf->composeAndGetMessage($a_user_id, null, "read", true), - [] + MailAttachments::empty() ); } @@ -5307,7 +5308,7 @@ protected function sendTutorNotification(): void "", $subject, $message, - array() + MailAttachments::empty() ); } } @@ -5461,7 +5462,7 @@ protected function sentReminder( "", $subject, $message, - array() + MailAttachments::empty() ); } } diff --git a/components/ILIAS/Test/classes/class.ilTestMailNotification.php b/components/ILIAS/Test/classes/class.ilTestMailNotification.php index 2a7b1b5cd174..94678e4e01a8 100755 --- a/components/ILIAS/Test/classes/class.ilTestMailNotification.php +++ b/components/ILIAS/Test/classes/class.ilTestMailNotification.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * Class ilTestMailNotification * @author Nadia Ahmad @@ -100,7 +102,15 @@ public function sendAdvancedNotification($owner_id, $title, $usr_data, $file_nam $this->appendBody($this->language->txt('tst_notification_explanation_admin')); $this->appendBody("\n"); - $this->setAttachments($file_names); + $attachments = MailAttachments::empty(); + if ($file_names !== []) { + $file_data_mail = new ilFileDataMail(ANONYMOUS_USER_ID); + $rcid = $file_data_mail->createCollectionFromPoolFilenames($file_names); + if ($rcid !== null) { + $attachments = MailAttachments::fromIrss($rcid); + } + } + $this->setAttachments($attachments); $this->getMail()->appendInstallationSignature(true); $this->sendMail([$owner_id]); diff --git a/components/ILIAS/User/classes/Cron/class.ilUserCronCheckAccounts.php b/components/ILIAS/User/classes/Cron/class.ilUserCronCheckAccounts.php index 5a65cf5f6bcf..53ccd7940ce8 100755 --- a/components/ILIAS/User/classes/Cron/class.ilUserCronCheckAccounts.php +++ b/components/ILIAS/User/classes/Cron/class.ilUserCronCheckAccounts.php @@ -21,6 +21,7 @@ use ILIAS\Cron\Job\Schedule\JobScheduleType; use ILIAS\Cron\Job\JobResult; use ILIAS\Cron\CronJob; +use ILIAS\Mail\Attachments\MailAttachments; /** * This cron send notifications about expiring user accounts @@ -133,7 +134,7 @@ public function run(): JobResult '', $lng->txt('account_expires_subject'), $body, - [] + MailAttachments::empty() ); // set status 'mail sent' diff --git a/components/ILIAS/User/src/Settings/NewAccountMail/Mail.php b/components/ILIAS/User/src/Settings/NewAccountMail/Mail.php index 7d5ab877d531..22199e7012b3 100644 --- a/components/ILIAS/User/src/Settings/NewAccountMail/Mail.php +++ b/components/ILIAS/User/src/Settings/NewAccountMail/Mail.php @@ -31,4 +31,5 @@ public function getSalutationNoneSpecific(): string; public function getSalutationMale(): string; public function getSalutationFemale(): string; public function getAttachment(ResourceStorage $irss): ?MailAttachment; + public function getAttachmentRid(): ?string; } diff --git a/components/ILIAS/WebServices/ECS/classes/class.ilECSAppEventListener.php b/components/ILIAS/WebServices/ECS/classes/class.ilECSAppEventListener.php index 0215acd1f65b..73f808e14281 100755 --- a/components/ILIAS/WebServices/ECS/classes/class.ilECSAppEventListener.php +++ b/components/ILIAS/WebServices/ECS/classes/class.ilECSAppEventListener.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * ECS Event Handler * @author Stefan Meyer @@ -220,7 +222,7 @@ private function sendNotification(ilECSSetting $server, ilObjUser $user_obj): vo $body .= $user_obj->getProfileAsString($lang) . "\n\n"; $body .= ilMail::_getAutoGeneratedMessageString($lang); - $mail->enqueue($server->getUserRecipientsAsString(), "", "", $subject, $body, array()); + $mail->enqueue($server->getUserRecipientsAsString(), "", "", $subject, $body, MailAttachments::empty()); // Store sub_id = 1 in ecs import which means mail is send $import->setSubId("1"); diff --git a/components/ILIAS/WebServices/ECS/classes/class.ilECSObjectSettings.php b/components/ILIAS/WebServices/ECS/classes/class.ilECSObjectSettings.php index 76a255dbe2a6..a400cb22fe4b 100755 --- a/components/ILIAS/WebServices/ECS/classes/class.ilECSObjectSettings.php +++ b/components/ILIAS/WebServices/ECS/classes/class.ilECSObjectSettings.php @@ -22,6 +22,7 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; +use ILIAS\Mail\Attachments\MailAttachments; /** * Handles object exports to ECS @@ -658,7 +659,7 @@ protected function sendNewContentNotification(ilECSSetting $a_server, $a_econten '', $lang->txt('ecs_new_approval_subject'), $message, - array() + MailAttachments::empty(), ); return true; diff --git a/components/ILIAS/WebServices/ECS/classes/class.ilRemoteObjectBase.php b/components/ILIAS/WebServices/ECS/classes/class.ilRemoteObjectBase.php index a9f62f51eac4..45da124352de 100755 --- a/components/ILIAS/WebServices/ECS/classes/class.ilRemoteObjectBase.php +++ b/components/ILIAS/WebServices/ECS/classes/class.ilRemoteObjectBase.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Mail\Attachments\MailAttachments; + /** * Remote object app base class * @@ -629,7 +631,7 @@ protected function sendNewContentNotification($a_server_id): void '', $lang->txt('ecs_new_econtent_subject'), $message, - array() + MailAttachments::empty() ); } diff --git a/components/ILIAS/Wiki/classes/class.ilWikiUtil.php b/components/ILIAS/Wiki/classes/class.ilWikiUtil.php index 7d1130d1aa30..d3aa1a5ad0d7 100755 --- a/components/ILIAS/Wiki/classes/class.ilWikiUtil.php +++ b/components/ILIAS/Wiki/classes/class.ilWikiUtil.php @@ -30,6 +30,8 @@ * */ // From include/Unicode/UtfNormal.php +use ILIAS\Mail\Attachments\MailAttachments; + if (!defined('UTF8_REPLACEMENT')) { define('UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/); } @@ -626,7 +628,7 @@ public static function sendNotification( "", $subject, $message, - array() + MailAttachments::empty() );*/ $message .= ilMail::_getInstallationSignature(); $mails[] = new ilMailValueObject( @@ -636,7 +638,7 @@ public static function sendNotification( '', $subject, $message, - [], + MailAttachments::empty(), false, false );