From f65b90fb7dec475db0ffc26a7e048a88241a0097 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 9 Feb 2026 14:27:59 +0400 Subject: [PATCH 1/6] AttachmentDownloadService --- .../AttachmentFileNotFoundException.php | 13 +++++ .../Model/Dto/DownloadableAttachment.php | 18 +++++++ .../Messaging/Service/AttachmentAdder.php | 3 +- .../Service/AttachmentDownloadService.php | 53 +++++++++++++++++++ .../Messaging/Service/AttachmentAdderTest.php | 2 +- 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php create mode 100644 src/Domain/Messaging/Model/Dto/DownloadableAttachment.php create mode 100644 src/Domain/Messaging/Service/AttachmentDownloadService.php diff --git a/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php b/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php new file mode 100644 index 00000000..b924b39a --- /dev/null +++ b/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php @@ -0,0 +1,13 @@ +getTo()[0]->getAddress(); - // todo: add endpoint in rest-api project - $viewUrl = $this->attachmentDownloadUrl . '/?id=' . $att->getId() . '&uid=' . $hash; + $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash; $email->text( $email->getTextBody() diff --git a/src/Domain/Messaging/Service/AttachmentDownloadService.php b/src/Domain/Messaging/Service/AttachmentDownloadService.php new file mode 100644 index 00000000..7a816b25 --- /dev/null +++ b/src/Domain/Messaging/Service/AttachmentDownloadService.php @@ -0,0 +1,53 @@ +getFilename(); + if ($filename === null || $filename === '') { + throw new AttachmentFileNotFoundException(); + } + + $path = rtrim($this->attachmentRepositoryPath, '/'); + $filePath = $path . '/' . $filename; + + if (!is_file($filePath) || !is_readable($filePath)) { + throw new AttachmentFileNotFoundException(); + } + + $mimeType = $attachment->getMimeType() + ?? MimeTypes::getDefault()->guessMimeType($filePath) + ?? 'application/octet-stream'; + + $size = filesize($filePath); + $size = $size === false ? null : $size; + + /** @var StreamInterface $stream */ + $stream = Utils::streamFor(Utils::tryFopen($filePath, 'rb')); + + return new DownloadableAttachment( + filename: $filename, + mimeType: $mimeType, + size: $size, + content: $stream, + ); + } +} diff --git a/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php b/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php index 9356eb3d..511833cb 100644 --- a/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php +++ b/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php @@ -91,7 +91,7 @@ public function testTextModePrependsNoticeAndLinks(): void ); $this->assertStringContainsString('Doc description', $body); $this->assertStringContainsString('Location', $body); - $this->assertStringContainsString('https://dl.example/?id=42&uid=user@example.com', $body); + $this->assertStringContainsString('https://dl.example/42/?uid=user@example.com', $body); } public function testHtmlUsesRepositoryFileIfExists(): void From 14c47a3b4d1441a80df77fe231a34708e4673616 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 9 Feb 2026 14:33:57 +0400 Subject: [PATCH 2/6] Add tests --- .../Service/AttachmentDownloadServiceTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php diff --git a/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php b/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php new file mode 100644 index 00000000..64bf2551 --- /dev/null +++ b/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php @@ -0,0 +1,101 @@ +tempDir = sys_get_temp_dir() . '/phplist-att-dl-' . bin2hex(random_bytes(5)); + if (!is_dir($this->tempDir)) { + mkdir($this->tempDir, 0777, true); + } + } + + protected function tearDown(): void + { + // cleanup temp directory + if (is_dir($this->tempDir)) { + $files = scandir($this->tempDir) ?: []; + foreach ($files as $f) { + if ($f === '.' || $f === '..') { + continue; + } + unlink($this->tempDir . '/' . $f); + } + rmdir($this->tempDir); + } + } + + public function testThrowsWhenFilenameIsEmpty(): void + { + $service = new AttachmentDownloadService($this->tempDir); + + $attachment = $this->createMock(Attachment::class); + $attachment->method('getFilename')->willReturn(''); + + $this->expectException(AttachmentFileNotFoundException::class); + $service->getDownloadable($attachment); + } + + public function testThrowsWhenFileDoesNotExist(): void + { + $service = new AttachmentDownloadService($this->tempDir); + + $attachment = $this->createMock(Attachment::class); + $attachment->method('getFilename')->willReturn('missing-file.pdf'); + + $this->expectException(AttachmentFileNotFoundException::class); + $service->getDownloadable($attachment); + } + + public function testReturnsDownloadableWithExplicitMimeType(): void + { + $service = new AttachmentDownloadService($this->tempDir); + + $filename = 'doc.pdf'; + $content = '%PDF-1.4\n'; + file_put_contents($this->tempDir . '/' . $filename, $content); + + $attachment = $this->createMock(Attachment::class); + $attachment->method('getFilename')->willReturn($filename); + $attachment->method('getMimeType')->willReturn('application/pdf'); + + $dl = $service->getDownloadable($attachment); + + $this->assertSame($filename, $dl->filename); + $this->assertSame('application/pdf', $dl->mimeType); + $this->assertSame(strlen($content), $dl->size); + $this->assertSame($content, (string)$dl->content); + } + + public function testGuessesMimeTypeAndProvidesStream(): void + { + $service = new AttachmentDownloadService($this->tempDir); + + $filename = 'note.txt'; + $content = "Hello, world!\n"; + file_put_contents($this->tempDir . '/' . $filename, $content); + + $attachment = $this->createMock(Attachment::class); + $attachment->method('getFilename')->willReturn($filename); + $attachment->method('getMimeType')->willReturn(null); + + $dl = $service->getDownloadable($attachment); + + $this->assertSame($filename, $dl->filename); + // Symfony MimeTypes should detect text/plain for .txt + $this->assertSame('text/plain', $dl->mimeType); + $this->assertSame(strlen($content), $dl->size); + $this->assertSame($content, (string)$dl->content); + } +} From 0838e277e05640bbeb2dde53c21699eecfa5187d Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 9 Feb 2026 14:47:33 +0400 Subject: [PATCH 3/6] After review 0 --- composer.json | 4 ++-- .../Model/Dto/DownloadableAttachment.php | 2 +- .../Service/AttachmentDownloadService.php | 21 ++++++++++++++----- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 0a39fd1b..f49193ab 100644 --- a/composer.json +++ b/composer.json @@ -86,11 +86,11 @@ "ext-curl": "*", "ext-fileinfo": "*", "setasign/fpdf": "^1.8", - "phpdocumentor/reflection-docblock": "^5.2" + "phpdocumentor/reflection-docblock": "^5.2", + "guzzlehttp/guzzle": "^6.3.0" }, "require-dev": { "phpunit/phpunit": "^9.5", - "guzzlehttp/guzzle": "^6.3.0", "squizlabs/php_codesniffer": "^3.2.0", "phpstan/phpstan": "^1.10", "nette/caching": "^3.0.0", diff --git a/src/Domain/Messaging/Model/Dto/DownloadableAttachment.php b/src/Domain/Messaging/Model/Dto/DownloadableAttachment.php index 93b258aa..f02265fb 100644 --- a/src/Domain/Messaging/Model/Dto/DownloadableAttachment.php +++ b/src/Domain/Messaging/Model/Dto/DownloadableAttachment.php @@ -11,7 +11,7 @@ final class DownloadableAttachment public function __construct( public readonly string $filename, public readonly string $mimeType, - public readonly int $size, + public readonly ?int $size, public readonly StreamInterface $content, ) { } diff --git a/src/Domain/Messaging/Service/AttachmentDownloadService.php b/src/Domain/Messaging/Service/AttachmentDownloadService.php index 7a816b25..97302284 100644 --- a/src/Domain/Messaging/Service/AttachmentDownloadService.php +++ b/src/Domain/Messaging/Service/AttachmentDownloadService.php @@ -21,15 +21,26 @@ public function __construct( public function getDownloadable(Attachment $attachment): DownloadableAttachment { - $filename = $attachment->getFilename(); - if ($filename === null || $filename === '') { + $original = $attachment->getFilename(); + $filename = basename($original); + + if ($filename === '' || $filename !== $original) { throw new AttachmentFileNotFoundException(); } - $path = rtrim($this->attachmentRepositoryPath, '/'); - $filePath = $path . '/' . $filename; + $baseDir = realpath($this->attachmentRepositoryPath); + if ($baseDir === false) { + throw new \RuntimeException('Attachment repository path does not exist.'); + } + + $filePath = $baseDir . DIRECTORY_SEPARATOR . $filename; + $realPath = realpath($filePath); - if (!is_file($filePath) || !is_readable($filePath)) { + if ($realPath === false || + !str_starts_with($realPath, $baseDir . DIRECTORY_SEPARATOR) || + !is_file($realPath) || + !is_readable($realPath) + ) { throw new AttachmentFileNotFoundException(); } From 05d2c1431a182afc2ee0314adcfde1cb8beb3b35 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 10 Feb 2026 10:58:28 +0400 Subject: [PATCH 4/6] Exception --- .../Exception/AttachmentFileNotFoundException.php | 8 +++----- .../Messaging/Service/AttachmentDownloadService.php | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php b/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php index b924b39a..42510085 100644 --- a/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php +++ b/src/Domain/Messaging/Exception/AttachmentFileNotFoundException.php @@ -4,10 +4,8 @@ namespace PhpList\Core\Domain\Messaging\Exception; -class AttachmentFileNotFoundException extends \RuntimeException +use RuntimeException; + +class AttachmentFileNotFoundException extends RuntimeException { - public function __construct() - { - parent::__construct('Attachment file not available'); - } } diff --git a/src/Domain/Messaging/Service/AttachmentDownloadService.php b/src/Domain/Messaging/Service/AttachmentDownloadService.php index 97302284..3f747bd7 100644 --- a/src/Domain/Messaging/Service/AttachmentDownloadService.php +++ b/src/Domain/Messaging/Service/AttachmentDownloadService.php @@ -25,12 +25,12 @@ public function getDownloadable(Attachment $attachment): DownloadableAttachment $filename = basename($original); if ($filename === '' || $filename !== $original) { - throw new AttachmentFileNotFoundException(); + throw new AttachmentFileNotFoundException('Invalid attachment filename: ' . $original); } $baseDir = realpath($this->attachmentRepositoryPath); if ($baseDir === false) { - throw new \RuntimeException('Attachment repository path does not exist.'); + throw new AttachmentFileNotFoundException('Attachment repository path does not exist.'); } $filePath = $baseDir . DIRECTORY_SEPARATOR . $filename; @@ -41,7 +41,7 @@ public function getDownloadable(Attachment $attachment): DownloadableAttachment !is_file($realPath) || !is_readable($realPath) ) { - throw new AttachmentFileNotFoundException(); + throw new AttachmentFileNotFoundException('Attachment file not available'); } $mimeType = $attachment->getMimeType() From 309f8a7a4be3e80c988bc5abb15163dbd7ed80df Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 10 Feb 2026 12:44:22 +0400 Subject: [PATCH 5/6] Add uid check --- src/Domain/Messaging/Model/Attachment.php | 2 + .../Messaging/Service/AttachmentAdder.php | 2 +- .../Service/AttachmentDownloadService.php | 55 +++++++++++++------ .../Service/AttachmentDownloadServiceTest.php | 24 +++++--- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/Domain/Messaging/Model/Attachment.php b/src/Domain/Messaging/Model/Attachment.php index 79fd5f95..d49cd386 100644 --- a/src/Domain/Messaging/Model/Attachment.php +++ b/src/Domain/Messaging/Model/Attachment.php @@ -13,6 +13,8 @@ #[ORM\Table(name: 'phplist_attachment')] class Attachment implements DomainModel, Identity { + public const FORWARD = 'forwarded'; + #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] diff --git a/src/Domain/Messaging/Service/AttachmentAdder.php b/src/Domain/Messaging/Service/AttachmentAdder.php index 940034d0..9c8ebe63 100644 --- a/src/Domain/Messaging/Service/AttachmentAdder.php +++ b/src/Domain/Messaging/Service/AttachmentAdder.php @@ -57,7 +57,7 @@ public function add(Email $email, int $campaignId, OutputFormat $format, bool $f break; case OutputFormat::Text: - $hash = $forwarded ? 'forwarded' : $email->getTo()[0]->getAddress(); + $hash = $forwarded ? Attachment::FORWARD : $email->getTo()[0]->getAddress(); $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash; $email->text( diff --git a/src/Domain/Messaging/Service/AttachmentDownloadService.php b/src/Domain/Messaging/Service/AttachmentDownloadService.php index 3f747bd7..3d69e23d 100644 --- a/src/Domain/Messaging/Service/AttachmentDownloadService.php +++ b/src/Domain/Messaging/Service/AttachmentDownloadService.php @@ -6,8 +6,10 @@ use GuzzleHttp\Psr7\Utils; use PhpList\Core\Domain\Messaging\Exception\AttachmentFileNotFoundException; +use PhpList\Core\Domain\Messaging\Exception\MessageNotReceivedException; use PhpList\Core\Domain\Messaging\Model\Attachment; use PhpList\Core\Domain\Messaging\Model\Dto\DownloadableAttachment; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use Psr\Http\Message\StreamInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\MimeTypes; @@ -15,15 +17,49 @@ class AttachmentDownloadService { public function __construct( + private readonly SubscriberRepository $subscriberRepository, #[Autowire('%phplist.attachment_repository_path%')] private readonly string $attachmentRepositoryPath = '/tmp', ) { } - public function getDownloadable(Attachment $attachment): DownloadableAttachment + public function getDownloadable(Attachment $attachment, string $uid): DownloadableAttachment { + $this->validateUid($uid); + $original = $attachment->getFilename(); $filename = basename($original); + $filePath = $this->validateFilePath($filename, $original); + + $mimeType = $attachment->getMimeType() + ?? MimeTypes::getDefault()->guessMimeType($filePath) + ?? 'application/octet-stream'; + + $size = filesize($filePath); + $size = $size === false ? null : $size; + + /** @var StreamInterface $stream */ + $stream = Utils::streamFor(Utils::tryFopen($filePath, 'rb')); + + return new DownloadableAttachment( + filename: $filename, + mimeType: $mimeType, + size: $size, + content: $stream, + ); + } + + private function validateUid(string $uid): void + { + if ($uid !== Attachment::FORWARD) { + $subscriber = $this->subscriberRepository->findOneByEmail($uid); + if ($subscriber === null) { + throw new MessageNotReceivedException(); + } + } + } + private function validateFilePath(string $filename, ?string $original): string + { if ($filename === '' || $filename !== $original) { throw new AttachmentFileNotFoundException('Invalid attachment filename: ' . $original); } @@ -44,21 +80,6 @@ public function getDownloadable(Attachment $attachment): DownloadableAttachment throw new AttachmentFileNotFoundException('Attachment file not available'); } - $mimeType = $attachment->getMimeType() - ?? MimeTypes::getDefault()->guessMimeType($filePath) - ?? 'application/octet-stream'; - - $size = filesize($filePath); - $size = $size === false ? null : $size; - - /** @var StreamInterface $stream */ - $stream = Utils::streamFor(Utils::tryFopen($filePath, 'rb')); - - return new DownloadableAttachment( - filename: $filename, - mimeType: $mimeType, - size: $size, - content: $stream, - ); + return $filePath; } } diff --git a/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php b/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php index 64bf2551..3d165f66 100644 --- a/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php +++ b/tests/Unit/Domain/Messaging/Service/AttachmentDownloadServiceTest.php @@ -7,6 +7,8 @@ use PhpList\Core\Domain\Messaging\Exception\AttachmentFileNotFoundException; use PhpList\Core\Domain\Messaging\Model\Attachment; use PhpList\Core\Domain\Messaging\Service\AttachmentDownloadService; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PHPUnit\Framework\TestCase; final class AttachmentDownloadServiceTest extends TestCase @@ -38,29 +40,33 @@ protected function tearDown(): void public function testThrowsWhenFilenameIsEmpty(): void { - $service = new AttachmentDownloadService($this->tempDir); + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $service = new AttachmentDownloadService($subscriberRepo, $this->tempDir); $attachment = $this->createMock(Attachment::class); $attachment->method('getFilename')->willReturn(''); $this->expectException(AttachmentFileNotFoundException::class); - $service->getDownloadable($attachment); + $service->getDownloadable($attachment, 'forwarded'); } public function testThrowsWhenFileDoesNotExist(): void { - $service = new AttachmentDownloadService($this->tempDir); + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $service = new AttachmentDownloadService($subscriberRepo, $this->tempDir); $attachment = $this->createMock(Attachment::class); $attachment->method('getFilename')->willReturn('missing-file.pdf'); $this->expectException(AttachmentFileNotFoundException::class); - $service->getDownloadable($attachment); + $service->getDownloadable($attachment, 'forwarded'); } public function testReturnsDownloadableWithExplicitMimeType(): void { - $service = new AttachmentDownloadService($this->tempDir); + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $subscriberRepo->method('findOneByEmail')->with('user@example.com')->willReturn(new Subscriber()); + $service = new AttachmentDownloadService($subscriberRepo, $this->tempDir); $filename = 'doc.pdf'; $content = '%PDF-1.4\n'; @@ -70,7 +76,7 @@ public function testReturnsDownloadableWithExplicitMimeType(): void $attachment->method('getFilename')->willReturn($filename); $attachment->method('getMimeType')->willReturn('application/pdf'); - $dl = $service->getDownloadable($attachment); + $dl = $service->getDownloadable($attachment, 'user@example.com'); $this->assertSame($filename, $dl->filename); $this->assertSame('application/pdf', $dl->mimeType); @@ -80,7 +86,9 @@ public function testReturnsDownloadableWithExplicitMimeType(): void public function testGuessesMimeTypeAndProvidesStream(): void { - $service = new AttachmentDownloadService($this->tempDir); + $subscriberRepo = $this->createMock(SubscriberRepository::class); + $subscriberRepo->method('findOneByEmail')->with('user@example.com')->willReturn(new Subscriber()); + $service = new AttachmentDownloadService($subscriberRepo, $this->tempDir); $filename = 'note.txt'; $content = "Hello, world!\n"; @@ -90,7 +98,7 @@ public function testGuessesMimeTypeAndProvidesStream(): void $attachment->method('getFilename')->willReturn($filename); $attachment->method('getMimeType')->willReturn(null); - $dl = $service->getDownloadable($attachment); + $dl = $service->getDownloadable($attachment, 'user@example.com'); $this->assertSame($filename, $dl->filename); // Symfony MimeTypes should detect text/plain for .txt From c3bcd4bb797a4a5c1d23170ece86ca2e4c7e3b20 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 10 Feb 2026 12:56:29 +0400 Subject: [PATCH 6/6] After review 1 --- config/services/services.yml | 4 ++++ .../Messaging/Service/AttachmentAdder.php | 2 +- .../Service/AttachmentDownloadService.php | 17 +++++++++++------ .../Messaging/Service/AttachmentAdderTest.php | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/services/services.yml b/config/services/services.yml index 93134c38..5e7db66b 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -87,6 +87,10 @@ services: autowire: true autoconfigure: true + PhpList\Core\Domain\Messaging\Service\AttachmentDownloadService: + autowire: true + autoconfigure: true + PhpList\Core\Domain\Configuration\Service\UserPersonalizer: autowire: true autoconfigure: true diff --git a/src/Domain/Messaging/Service/AttachmentAdder.php b/src/Domain/Messaging/Service/AttachmentAdder.php index 9c8ebe63..d6309992 100644 --- a/src/Domain/Messaging/Service/AttachmentAdder.php +++ b/src/Domain/Messaging/Service/AttachmentAdder.php @@ -58,7 +58,7 @@ public function add(Email $email, int $campaignId, OutputFormat $format, bool $f case OutputFormat::Text: $hash = $forwarded ? Attachment::FORWARD : $email->getTo()[0]->getAddress(); - $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . $hash; + $viewUrl = $this->attachmentDownloadUrl . '/' . $att->getId() . '/?uid=' . urlencode($hash); $email->text( $email->getTextBody() diff --git a/src/Domain/Messaging/Service/AttachmentDownloadService.php b/src/Domain/Messaging/Service/AttachmentDownloadService.php index 3d69e23d..00181df6 100644 --- a/src/Domain/Messaging/Service/AttachmentDownloadService.php +++ b/src/Domain/Messaging/Service/AttachmentDownloadService.php @@ -6,7 +6,7 @@ use GuzzleHttp\Psr7\Utils; use PhpList\Core\Domain\Messaging\Exception\AttachmentFileNotFoundException; -use PhpList\Core\Domain\Messaging\Exception\MessageNotReceivedException; +use PhpList\Core\Domain\Messaging\Exception\SubscriberNotFoundException; use PhpList\Core\Domain\Messaging\Model\Attachment; use PhpList\Core\Domain\Messaging\Model\Dto\DownloadableAttachment; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; @@ -27,6 +27,9 @@ public function getDownloadable(Attachment $attachment, string $uid): Downloadab $this->validateUid($uid); $original = $attachment->getFilename(); + if ($original === null || $original === '') { + throw new AttachmentFileNotFoundException('Attachment has no filename.'); + } $filename = basename($original); $filePath = $this->validateFilePath($filename, $original); @@ -50,11 +53,13 @@ public function getDownloadable(Attachment $attachment, string $uid): Downloadab private function validateUid(string $uid): void { - if ($uid !== Attachment::FORWARD) { - $subscriber = $this->subscriberRepository->findOneByEmail($uid); - if ($subscriber === null) { - throw new MessageNotReceivedException(); - } + if ($uid === Attachment::FORWARD) { + return; + } + + $subscriber = $this->subscriberRepository->findOneByEmail($uid); + if ($subscriber === null) { + throw new SubscriberNotFoundException(); } } diff --git a/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php b/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php index 511833cb..50a91a51 100644 --- a/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php +++ b/tests/Unit/Domain/Messaging/Service/AttachmentAdderTest.php @@ -91,7 +91,7 @@ public function testTextModePrependsNoticeAndLinks(): void ); $this->assertStringContainsString('Doc description', $body); $this->assertStringContainsString('Location', $body); - $this->assertStringContainsString('https://dl.example/42/?uid=user@example.com', $body); + $this->assertStringContainsString('https://dl.example/42/?uid=' . urlencode('user@example.com'), $body); } public function testHtmlUsesRepositoryFileIfExists(): void