Skip to content

Commit

Permalink
Adds embeddings support
Browse files Browse the repository at this point in the history
fixes #185
  • Loading branch information
butschster committed Jun 5, 2024
1 parent 8b612ba commit f8f11e7
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 11 deletions.
6 changes: 6 additions & 0 deletions app/modules/Smtp/Application/Mail/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function __construct(
private ?string $filename,
private string $content,
private string $type,
private ?string $contentId,
) {
$this->id = (string) Uuid::uuid4();
}
Expand All @@ -37,4 +38,9 @@ public function getId(): string
{
return $this->id;
}

public function getContentId(): ?string
{
return $this->contentId;
}
}
1 change: 1 addition & 0 deletions app/modules/Smtp/Application/Mail/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private function buildAttachmentFrom(array $attachments): array
$part->getFilename(),
$part->getContent(),
$part->getContentType(),
$part->getContentId(),
), $attachments);
}

Expand Down
16 changes: 13 additions & 3 deletions app/modules/Smtp/Application/Storage/AttachmentStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(),
);
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/modules/Smtp/Domain/AttachmentStorageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ interface AttachmentStorageInterface
{
/**
* @param MailAttachment[] $attachments
* @return iterable<string, string>
*/
public function store(Uuid $eventUuid, array $attachments): void;
public function store(Uuid $eventUuid, array $attachments): iterable;

public function deleteByEvent(Uuid $eventUuid): void;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Modules\Smtp\Interfaces\Http\Controllers\Attachments;

use App\Application\Commands\FindEventByUuid;
use App\Application\Commands\FindSmtpAttachmentByUuid;
use App\Application\Domain\ValueObjects\Uuid;
use App\Application\HTTP\Response\ErrorResource;
use Modules\Smtp\Domain\AttachmentStorageInterface;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\ResponseInterface;
use Spiral\Cqrs\QueryBusInterface;
use Spiral\Http\Exception\ClientException\ForbiddenException;
use Spiral\Http\ResponseWrapper;
use Spiral\Router\Annotation\Route;
use OpenApi\Attributes as OA;

#[OA\Get(
path: '/api/smtp/{eventUuid}/attachments/preview/{uuid}',
description: 'Preview an attachment by UUID',
tags: ['Smtp'],
parameters: [
new OA\PathParameter(
name: 'eventUuid',
description: 'Event UUID',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid'),
),
new OA\PathParameter(
name: 'uuid',
description: 'Attachment UUID',
required: true,
schema: new OA\Schema(type: 'string', format: 'uuid'),
),
],
responses: [
new OA\Response(
response: 200,
description: 'Success',
headers: [
new OA\Header(header: 'Content-Type', schema: new OA\Schema(type: 'string', format: 'binary')),
],
),
new OA\Response(
response: 404,
description: 'Not found',
content: new OA\JsonContent(
ref: ErrorResource::class,
),
),
new OA\Response(
response: 403,
description: 'Access denied.',
content: new OA\JsonContent(
ref: ErrorResource::class,
),
),
],
)]
final readonly class PreviewAction
{
public function __construct(
private AttachmentStorageInterface $storage,
) {}

#[Route(route: 'smtp/<eventUuid>/attachments/preview/<uuid>', 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);
}
}
5 changes: 4 additions & 1 deletion app/modules/Smtp/Interfaces/TCP/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
57 changes: 51 additions & 6 deletions tests/Feature/Interfaces/TCP/Smtp/EmailTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down Expand Up @@ -142,8 +174,12 @@ private function validateMessage(string $messageId, string $uuid): void
$this->assertSame([], $parsedMessage->getBccs());

$this->assertSame(
'Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.',
$parsedMessage->textBody,
<<<'HTML'
<img src="cid:test-cid@buggregator">
Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.
HTML
,
$parsedMessage->htmlBody,
);

$this->assertSame('', $parsedMessage->htmlBody);
Expand Down Expand Up @@ -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.<br>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'
<img src="cid:logo-embeddable">
Hello Alice.<br>This is a test message with 5 header fields and 4 lines in the message body.
TEXT
,
);
}
}

0 comments on commit f8f11e7

Please sign in to comment.