diff --git a/app/modules/Smtp/Application/Mail/Attachment.php b/app/modules/Smtp/Application/Mail/Attachment.php index 61de022..6a0b4f9 100644 --- a/app/modules/Smtp/Application/Mail/Attachment.php +++ b/app/modules/Smtp/Application/Mail/Attachment.php @@ -14,6 +14,7 @@ public function __construct( private ?string $filename, private string $content, private string $type, + private ?string $contentId, ) { $this->id = (string) Uuid::uuid4(); } @@ -37,4 +38,9 @@ public function getId(): string { return $this->id; } + + public function getContentId(): ?string + { + return $this->contentId; + } } diff --git a/app/modules/Smtp/Application/Mail/Parser.php b/app/modules/Smtp/Application/Mail/Parser.php index b33473d..b64343f 100644 --- a/app/modules/Smtp/Application/Mail/Parser.php +++ b/app/modules/Smtp/Application/Mail/Parser.php @@ -66,6 +66,7 @@ private function buildAttachmentFrom(array $attachments): array $part->getFilename(), $part->getContent(), $part->getContentType(), + $part->getContentId(), ), $attachments); } diff --git a/app/modules/Smtp/Application/Storage/AttachmentStorage.php b/app/modules/Smtp/Application/Storage/AttachmentStorage.php index 3a8c97f..8642703 100644 --- a/app/modules/Smtp/Application/Storage/AttachmentStorage.php +++ b/app/modules/Smtp/Application/Storage/AttachmentStorage.php @@ -18,7 +18,7 @@ public function __construct( private AttachmentFactoryInterface $factory, ) {} - public function store(Uuid $eventUuid, array $attachments): void + public function store(Uuid $eventUuid, array $attachments): iterable { foreach ($attachments as $attachment) { $file = $this->bucket->write( @@ -27,15 +27,25 @@ public function store(Uuid $eventUuid, array $attachments): void ); $this->attachments->store( - $this->factory->create( + $a = $this->factory->create( eventUuid: $eventUuid, name: $attachment->getFilename(), path: $file->getPathname(), size: $file->getSize(), mime: $file->getMimeType(), - id: $attachment->getId(), + id: $attachment->getContentId() ?? $attachment->getId(), ), ); + + if ($attachment->getContentId() === null) { + continue; + } + + yield $attachment->getContentId() => \sprintf( + '/api/smtp/%s/attachments/preview/%s', + $eventUuid, + $a->getUuid(), + ); } } diff --git a/app/modules/Smtp/Domain/AttachmentStorageInterface.php b/app/modules/Smtp/Domain/AttachmentStorageInterface.php index bf509f5..c841818 100644 --- a/app/modules/Smtp/Domain/AttachmentStorageInterface.php +++ b/app/modules/Smtp/Domain/AttachmentStorageInterface.php @@ -11,8 +11,9 @@ interface AttachmentStorageInterface { /** * @param MailAttachment[] $attachments + * @return iterable */ - public function store(Uuid $eventUuid, array $attachments): void; + public function store(Uuid $eventUuid, array $attachments): iterable; public function deleteByEvent(Uuid $eventUuid): void; diff --git a/app/modules/Smtp/Interfaces/Http/Controllers/Attachments/PreviewAction.php b/app/modules/Smtp/Interfaces/Http/Controllers/Attachments/PreviewAction.php new file mode 100644 index 0000000..a92c71f --- /dev/null +++ b/app/modules/Smtp/Interfaces/Http/Controllers/Attachments/PreviewAction.php @@ -0,0 +1,88 @@ +/attachments/preview/', name: 'smtp.attachments.preview', group: 'api_guest')] + public function __invoke( + QueryBusInterface $bus, + ResponseWrapper $responseWrapper, + Uuid $eventUuid, + Uuid $uuid, + ): ResponseInterface { + $event = $bus->ask(new FindEventByUuid($eventUuid)); + $attachment = $bus->ask(new FindSmtpAttachmentByUuid($uuid)); + + if (!$attachment->getEventUuid()->equals($event->getUuid())) { + throw new ForbiddenException('Access denied.'); + } + + $stream = Stream::create($this->storage->getContent($attachment->getPath())); + + return $responseWrapper->create(200) + ->withHeader('Content-Type', $attachment->getMime()) + ->withBody($stream); + } +} diff --git a/app/modules/Smtp/Interfaces/TCP/Service.php b/app/modules/Smtp/Interfaces/TCP/Service.php index beb6c2a..1f9d602 100644 --- a/app/modules/Smtp/Interfaces/TCP/Service.php +++ b/app/modules/Smtp/Interfaces/TCP/Service.php @@ -94,7 +94,10 @@ private function dispatchMessage(Message $message, ?string $project = null): voi $uuid = Uuid::generate(); $data = $message->jsonSerialize(); - $this->attachments->store(eventUuid: $uuid, attachments: $message->attachments); + // TODO: Refactor this + foreach ($this->attachments->store(eventUuid: $uuid, attachments: $message->attachments) as $cid => $url) { + $data['html'] = \str_replace("cid:$cid", $url, $data['html']); + } $this->bus->dispatch( new HandleReceivedEvent( diff --git a/tests/Feature/Interfaces/TCP/Smtp/EmailTest.php b/tests/Feature/Interfaces/TCP/Smtp/EmailTest.php index c70224a..86e2047 100644 --- a/tests/Feature/Interfaces/TCP/Smtp/EmailTest.php +++ b/tests/Feature/Interfaces/TCP/Smtp/EmailTest.php @@ -14,6 +14,8 @@ use Spiral\RoadRunnerBridge\Tcp\Response\CloseConnection; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\File; use Tests\Feature\Interfaces\TCP\TCPTestCase; final class EmailTest extends TCPTestCase @@ -41,6 +43,21 @@ public function testSendEmail(): void uuid: $connectionUuid = Uuid::uuid7(), ); + // Assert logo-embeddable is persisted to a database + $this->accounts->shouldReceive('store') + ->once() + ->with( + \Mockery::on(function (Attachment $attachment) { + $this->assertSame('logo-embeddable', $attachment->getFilename()); + $this->assertSame(1206, $attachment->getSize()); + $this->assertSame('image/svg+xml', $attachment->getMime()); + + // Check attachments storage + $this->bucket->assertCreated($attachment->getPath()); + return true; + }), + ); + // Assert hello.txt is persisted to a database $this->accounts->shouldReceive('store') ->once() @@ -56,8 +73,23 @@ public function testSendEmail(): void }), ); + // Assert hello.txt is persisted to a database + $this->accounts->shouldReceive('store') + ->once() + ->with( + \Mockery::on(function (Attachment $attachment) { + $this->assertSame('logo.svg', $attachment->getFilename()); + $this->assertSame(1206, $attachment->getSize()); + $this->assertSame('image/svg+xml', $attachment->getMime()); + + // Check attachments storage + $this->bucket->assertCreated($attachment->getPath()); + return true; + }), + ); + - // Assert world.txt is persisted to a database + // Assert logo.svg is persisted to a database $this->accounts->shouldReceive('store') ->once() ->with( @@ -142,8 +174,12 @@ private function validateMessage(string $messageId, string $uuid): void $this->assertSame([], $parsedMessage->getBccs()); $this->assertSame( - 'Hello Alice.
This is a test message with 5 header fields and 4 lines in the message body.', - $parsedMessage->textBody, + <<<'HTML' + +Hello Alice.
This is a test message with 5 header fields and 4 lines in the message body. +HTML + , + $parsedMessage->htmlBody, ); $this->assertSame('', $parsedMessage->htmlBody); @@ -213,9 +249,18 @@ public function buildEmail(): Email ) ->addFrom(new Address('no-reply@site.com', 'Bob Example'),) ->attachFromPath(path: __DIR__ . '/hello.txt',) - ->attachFromPath(path: __DIR__ . '/logo.svg',) - ->text( - body: 'Hello Alice.
This is a test message with 5 header fields and 4 lines in the message body.', + ->attachFromPath(path: __DIR__ . '/logo.svg') + ->addPart( + (new DataPart(new File(__DIR__ . '/logo.svg'), 'logo-embeddable'))->asInline()->setContentId( + 'test-cid@buggregator', + ), + ) + ->html( + body: <<<'TEXT' + +Hello Alice.
This is a test message with 5 header fields and 4 lines in the message body. +TEXT + , ); } }