Skip to content

Commit

Permalink
Attachment improvements (#40)
Browse files Browse the repository at this point in the history
 - Increased upload size limit to 10M
 - Added option for static attachments
 - Added base64_decode filter to twig to allow sending base64 encoded attachments, useful for binary contents
  • Loading branch information
adamquaile committed Jun 8, 2018
1 parent 6d7a2ad commit 79351cf
Show file tree
Hide file tree
Showing 21 changed files with 244 additions and 65 deletions.
3 changes: 3 additions & 0 deletions infrastructure/nginx/nginx.conf
Expand Up @@ -2,6 +2,9 @@ server {
root /app/web;
listen 80 default_server;

client_max_body_size 10M;
client_body_buffer_size 10M;

location / {
# try to serve file directly, fallback to app.php
try_files $uri /app.php$is_args$args;
Expand Down
7 changes: 3 additions & 4 deletions src/Outstack/Enveloper/Resolution/AttachmentResolver.php
Expand Up @@ -20,10 +20,9 @@ public function __construct(TemplateLanguage $language)
public function resolve(AttachmentTemplate $template, object $parameters)
{
return new Attachment(
$this->language->render(
$template->getContents(),
$parameters
),
$template->isStatic()
? $template->getContents()
: $this->language->render($template->getContents(), $parameters),
$this->language->render(
$template->getFilename(),
$parameters
Expand Down
14 changes: 14 additions & 0 deletions src/Outstack/Enveloper/Resolution/Twig/TwigEnveloperExtension.php
@@ -0,0 +1,14 @@
<?php

namespace Outstack\Enveloper\Resolution\Twig;

class TwigEnveloperExtension extends \Twig_Extension
{
public function getFilters()
{
return [
'base64_decode' => new \Twig_SimpleFilter('base64_decode', 'base64_decode')
];
}

}
Expand Up @@ -18,6 +18,7 @@ public function __construct(Twig_Environment $twig = null)
$twig = new Twig_Environment(new \Twig_Loader_Chain());
}

$twig->addExtension(new TwigEnveloperExtension());
$this->twig = $twig;
}

Expand Down
12 changes: 11 additions & 1 deletion src/Outstack/Enveloper/Templates/AttachmentTemplate.php
Expand Up @@ -16,12 +16,22 @@ class AttachmentTemplate
* @var null|string
*/
private $iterateOver;
/**
* @var bool
*/
private $static;

public function __construct(string $contents, string $filename, ?string $iterateOver = null)
public function __construct(bool $static, string $contents, string $filename, ?string $iterateOver = null)
{
$this->contents = $contents;
$this->filename = $filename;
$this->iterateOver = $iterateOver;
$this->static = $static;
}

public function isStatic(): bool
{
return $this->static;
}

public function getContents(): string
Expand Down
Expand Up @@ -86,7 +86,8 @@ public function getConfigTreeBuilder()
->arrayNode('attachments')
->prototype('array')
->children()
->scalarNode('contents')->isRequired()->end()
->scalarNode('source')->end()
->scalarNode('contents')->end()
->scalarNode('filename')->isRequired()->end()
->scalarNode('iterateOver')->defaultNull()->end()
->end()
Expand Down
19 changes: 14 additions & 5 deletions src/Outstack/Enveloper/Templates/Loader/FilesystemLoader.php
Expand Up @@ -68,20 +68,29 @@ public function find(string $name): Template
$textTemplate,
$config['content']['html'],
$htmlTemplate,
$this->parseAttachmentListTemplate($config['attachments'])
$this->parseAttachmentListTemplate($config['attachments'], $name)
);
}

private function parseAttachmentListTemplate(array $attachments)
private function parseAttachmentListTemplate(array $attachments, string $templateName)
{
return new AttachmentListTemplate(
array_map([$this, 'parseAttachmentTemplate'], $attachments)
array_map(
function($attachment) use ($templateName) {
return $this->parseAttachmentTemplate($attachment, $templateName);
},
$attachments)
);
}

private function parseAttachmentTemplate(array $template)
private function parseAttachmentTemplate(array $template, string $templateName)
{
return new AttachmentTemplate($template['contents'], $template['filename'], $template['iterateOver'] ?? null);
$static = false;
if (!array_key_exists('content', $template) && array_key_exists('source', $template)) {
$static = true;
$template['contents'] = $this->filesystem->read("$templateName/{$template['source']}");
}
return new AttachmentTemplate($static, $template['contents'], $template['filename'], $template['iterateOver'] ?? null);
}

private function parseRecipientListTemplate(array $recipients): ParticipantListTemplate
Expand Down
151 changes: 151 additions & 0 deletions tests/Functional/AttachmentHandlingFunctionalTest.php
@@ -0,0 +1,151 @@
<?php

namespace Outstack\Enveloper\Tests\Functional;

use Outstack\Enveloper\SwiftMailerBridge\SwiftMailerInterface;
use Http\Client\Exception\HttpException;
use Outstack\Components\SymfonySwiftMailerAssertionLibrary\SwiftMailerAssertionTrait;
use Zend\Diactoros\Request;
use Zend\Diactoros\Stream;
use Zend\Diactoros\Uri;

class AttachmentHandlingFunctionalTest extends AbstractApiTestCase
{
use SwiftMailerAssertionTrait;

protected $mailerSpy;

public function setUp()
{
parent::setUp();

$this->mailerSpy = self::$kernel->getContainer()->get(SwiftMailerInterface::class);
}

public function test_attachments_sent()
{
$request = new Request(
'/outbox',
'POST',
$this->convertToStream(json_encode([
'template' => 'message-with-attachments',
'parameters' => [
'email' => 'bob@example.com',
'attachments' => [
['contents' => base64_encode('This is a note'), 'filename' => 'note.txt']
]
]
]))
);

$response = $this->client->sendRequest($request);
$this->assertEquals(204, $response->getStatusCode());

$this->assertCountSentMessages(1);
$this->assertMessageSent(
function(\Swift_Message $message) {
$expectedContents = base64_encode('This is a note');
$expected =
'Content-Type: application/octet-stream; name=note.txt' . "\r\n" .
'Content-Transfer-Encoding: base64' . "\r\n" .
'Content-Disposition: attachment; filename=note.txt' . "\r\n" . "\r\n" .
$expectedContents . "\r\n" . "\r\n" .
'--'
;

foreach (explode($message->getBoundary(), (string) $message) as $part) {
if (trim($part) == trim($expected)) {
return true;
}
}

throw new \LogicException("No matching attachment found");
}
);
}

public function test_large_attachment_sent()
{
$largeAttachment = random_bytes(1048576 * 7);
$request = new Request(
'/outbox',
'POST',
$this->convertToStream(json_encode([
'template' => 'message-with-attachments',
'parameters' => [
'email' => 'bob@example.com',
'attachments' => [
['contents' => base64_encode($largeAttachment), 'filename' => 'random.txt']
]
]
]))
);

$response = $this->client->sendRequest($request);

$this->assertEquals(204, $response->getStatusCode());

$this->assertCountSentMessages(1);
$this->assertMessageSent(
function(\Swift_Message $message) use ($largeAttachment) {
$expectedContents = implode("\r\n", str_split(base64_encode($largeAttachment), 76));
$expected =
'Content-Type: application/octet-stream; name=random.txt' . "\r\n" .
'Content-Transfer-Encoding: base64' . "\r\n" .
'Content-Disposition: attachment; filename=random.txt' . "\r\n" . "\r\n" .
$expectedContents . "\r\n" . "\r\n" .
'--'
;

foreach (explode($message->getBoundary(), (string) $message) as $part) {
if (trim($part) == trim($expected)) {
return true;
}
}

throw new \LogicException("No matching attachment found");
}
);
}

public function test_static_attachment_sent()
{
$expectedAttachment = 'static attachment content';
$request = new Request(
'/outbox',
'POST',
$this->convertToStream(json_encode([
'template' => 'message-with-static-attachments',
'parameters' => [
'email' => 'bob@example.com'
]
]))
);

$response = $this->client->sendRequest($request);

$this->assertEquals(204, $response->getStatusCode());

$this->assertCountSentMessages(1);
$this->assertMessageSent(
function(\Swift_Message $message) use ($expectedAttachment) {
$expectedContents = implode("\r\n", str_split(base64_encode($expectedAttachment), 76));
$expected =
'Content-Type: application/octet-stream; name=attachment.txt' . "\r\n" .
'Content-Transfer-Encoding: base64' . "\r\n" .
'Content-Disposition: attachment; filename=attachment.txt' . "\r\n" . "\r\n" .
$expectedContents . "\r\n" . "\r\n" .
'--'
;

foreach (explode($message->getBoundary(), (string) $message) as $part) {
if (trim($part) == trim($expected)) {
return true;
}
}

throw new \LogicException("No matching attachment found");
}
);
}
}
41 changes: 0 additions & 41 deletions tests/Functional/EmailSendingFunctionalTest.php
Expand Up @@ -138,45 +138,4 @@ function(\Swift_Message $message) {
);
}

public function test_attachments_sent()
{
$request = new Request(
'/outbox',
'POST',
$this->convertToStream(json_encode([
'template' => 'message-with-attachments',
'parameters' => [
'email' => 'bob@example.com',
'attachments' => [
['contents' => 'This is a note', 'filename' => 'note.txt']
]
]
]))
);

$response = $this->client->sendRequest($request);
$this->assertEquals(204, $response->getStatusCode());

$this->assertCountSentMessages(1);
$this->assertMessageSent(
function(\Swift_Message $message) {
$expectedContents = base64_encode('This is a note');
$expected =
'Content-Type: application/octet-stream; name=note.txt' . "\r\n" .
'Content-Transfer-Encoding: base64' . "\r\n" .
'Content-Disposition: attachment; filename=note.txt' . "\r\n" . "\r\n" .
$expectedContents . "\r\n" . "\r\n" .
'--'
;

foreach (explode($message->getBoundary(), (string) $message) as $part) {
if (trim($part) == trim($expected)) {
return true;
}
}

throw new \LogicException("No matching attachment found");
}
);
}
}
Expand Up @@ -36,7 +36,7 @@ public function test_resolves_template_with_iterated_value()
$this->sut->resolveAttachmentList(
new AttachmentListTemplate(
[
new AttachmentTemplate('{{ item.data }}', '{{ item.filename }}', 'attachments')
new AttachmentTemplate(false, '{{ item.data }}', '{{ item.filename }}', 'attachments')
]
),
(object) [
Expand All @@ -61,8 +61,8 @@ public function test_resolves_multiple_attachments()
$this->sut->resolveAttachmentList(
new AttachmentListTemplate(
[
new AttachmentTemplate('{{ attachments[0].data }}', '{{ attachments[0].filename }}'),
new AttachmentTemplate('{{ attachments[1].data }}', '{{ attachments[1].filename }}')
new AttachmentTemplate(false, '{{ attachments[0].data }}', '{{ attachments[0].filename }}'),
new AttachmentTemplate(false, '{{ attachments[1].data }}', '{{ attachments[1].filename }}')
]
),
(object) [
Expand Down
Expand Up @@ -28,6 +28,7 @@ public function test_simple_txt_resolved()
),
$this->sut->resolve(
new AttachmentTemplate(
false,
'{{ string1 }} - {{ string2 }}',
'{{ string3 }}.txt',
null
Expand Down
Expand Up @@ -97,7 +97,7 @@ public function test_it_resolves_message_with_attachments()
'template.html.twig',
'<p>Welcome to app {{ user.name }}',
new AttachmentListTemplate([
new AttachmentTemplate('attachment {{ number }}', 'a{{ number }}.txt')
new AttachmentTemplate(false, 'attachment {{ number }}', 'a{{ number }}.txt')
])
),
(object) [
Expand Down
Expand Up @@ -122,7 +122,7 @@ public function test_finds_template_with_attachment()
'new-user-welcome.html.twig',
$html,
new AttachmentListTemplate([
new AttachmentTemplate('{{ contents }}', '{{ filename }}')
new AttachmentTemplate(false, '{{ contents }}', '{{ filename }}')
])
),
$this->sut->find('new-user-welcome')
Expand Down Expand Up @@ -165,7 +165,7 @@ public function test_finds_template_with_attachment_iterator()
'new-user-welcome.html.twig',
$html,
new AttachmentListTemplate([
new AttachmentTemplate('{{ item.contents }}', '{{ item.filename }}', 'attachments')
new AttachmentTemplate(false, '{{ item.contents }}', '{{ item.filename }}', 'attachments')
])
),
$this->sut->find('new-user-welcome')
Expand Down

This file was deleted.

Expand Up @@ -5,7 +5,7 @@ recipients:
to:
- "{{ email }}"
content:
html: "message-with-attachments.html.twig"
html: "message-with-attachments.mjml.twig"
text: "message-with-attachments.text.twig"
attachments:
- { contents: "{{ item.contents }}", filename: "{{ item.filename }}", iterateOver: "attachments" }
- { contents: "{% autoescape false %}{{ item.contents|base64_decode }}{% endautoescape %}", filename: "{{ item.filename }}", iterateOver: "attachments" }
@@ -0,0 +1,7 @@
<mjml>
<mj-body>
<mj-container>
<mj-text>Message with attachments</mj-text>
</mj-container>
</mj-body>
</mjml>

0 comments on commit 79351cf

Please sign in to comment.