Skip to content

Commit e81d8d1

Browse files
committed
feat: improve attachments
1 parent 40c50bd commit e81d8d1

File tree

15 files changed

+456
-48
lines changed

15 files changed

+456
-48
lines changed

packages/mailer/composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@
1515
"psr-4": {
1616
"Tempest\\Mail\\": "src"
1717
}
18+
},
19+
"autoload-dev": {
20+
"psr-4": {
21+
"Tempest\\Mail\\Tests\\": "tests"
22+
}
1823
}
1924
}

packages/mailer/src/Attachment.php renamed to packages/mailer/src/Attachments/Attachment.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace Tempest\Mail;
3+
namespace Tempest\Mail\Attachments;
44

55
interface Attachment
66
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Tempest\Mail\Attachments;
4+
5+
use Closure;
6+
7+
/**
8+
* Represents an attachment that leaves in the local filesystem.
9+
*/
10+
final class DataAttachment implements Attachment
11+
{
12+
private function __construct(
13+
public readonly Closure $resolve,
14+
public readonly ?string $name,
15+
public readonly ?string $contentType,
16+
) {}
17+
18+
/**
19+
* Creates an attachment from the given closure.
20+
*/
21+
public static function fromClosure(Closure $closure, ?string $name = null, ?string $contentType = null): self
22+
{
23+
return new self($closure, $name, $contentType);
24+
}
25+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Tempest\Mail\Attachments;
4+
5+
use Closure;
6+
use Tempest\Mail\Exceptions\CouldNotFindFileAttachmentException;
7+
use Tempest\Support\Filesystem;
8+
use Tempest\Support\Path;
9+
10+
/**
11+
* Represents an attachment that lives in the local filesystem.
12+
*/
13+
final class FileAttachment implements Attachment
14+
{
15+
public Closure $resolve {
16+
get => fn () => Filesystem\read_file($this->path);
17+
}
18+
19+
private function __construct(
20+
private readonly string $path,
21+
public readonly ?string $name,
22+
public readonly ?string $contentType,
23+
) {}
24+
25+
/**
26+
* Creates an attachment from the local filesystem.
27+
*/
28+
public static function fromPath(string $path, ?string $name = null, ?string $contentType = null): self
29+
{
30+
$path = Path\normalize($path);
31+
32+
if (! Filesystem\is_file($path)) {
33+
throw new CouldNotFindFileAttachmentException($path);
34+
}
35+
36+
return new self(
37+
path: $path,
38+
name: $name ?? basename($path),
39+
contentType: $contentType ?? finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path),
40+
);
41+
}
42+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Tempest\Mail\Attachments;
4+
5+
use Closure;
6+
use Tempest\Container\GenericContainer;
7+
use Tempest\Storage\Storage;
8+
use Tempest\Support\Path;
9+
use UnitEnum;
10+
11+
/**
12+
* Represents an attachment that lives in the {@see Tempest\Storage\Storage}.
13+
*/
14+
final class StorageAttachment implements Attachment
15+
{
16+
public Closure $resolve {
17+
get => fn () => $this->storage->readStream($this->path);
18+
}
19+
20+
private function __construct(
21+
private readonly string $path,
22+
public readonly ?string $name,
23+
public readonly ?string $contentType,
24+
private readonly Storage $storage,
25+
) {}
26+
27+
/**
28+
* Creates an attachment from the storage.
29+
*/
30+
public static function fromPath(string $path, ?string $name = null, ?string $contentType = null, null|string|UnitEnum $tag = null): self
31+
{
32+
if (! ($storage = self::resolveStorage($tag))) {
33+
throw new \RuntimeException('No storage found.');
34+
}
35+
36+
$path = Path\normalize($path);
37+
38+
return new self(
39+
path: $path,
40+
name: $name ?? basename($path),
41+
contentType: $contentType ?? $storage->mimeType($path),
42+
storage: $storage,
43+
);
44+
}
45+
46+
private static function resolveStorage(null|string|UnitEnum $tag = null): ?Storage
47+
{
48+
if (! class_exists(GenericContainer::class)) {
49+
return null;
50+
}
51+
52+
if (is_null(GenericContainer::instance())) {
53+
return null;
54+
}
55+
56+
if (! GenericContainer::instance()->has(Storage::class, $tag)) {
57+
return null;
58+
}
59+
60+
return GenericContainer::instance()->get(Storage::class, $tag);
61+
}
62+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<?php
2+
3+
namespace Tempest\Mail\Builder;
4+
5+
use Stringable;
6+
use Tempest\Mail\Address;
7+
use Tempest\Mail\Attachments\Attachment;
8+
use Tempest\Mail\Attachments\FileAttachment;
9+
use Tempest\Mail\Attachments\StorageAttachment;
10+
use Tempest\Mail\Content;
11+
use Tempest\Mail\Email;
12+
use Tempest\Mail\Envelope;
13+
use Tempest\Mail\GenericEmail;
14+
use Tempest\Mail\Priority;
15+
use Tempest\Support\Arr;
16+
use Tempest\Support\Arr\ArrayInterface;
17+
use Tempest\View\View;
18+
use UnitEnum;
19+
20+
final class EmailBuilder
21+
{
22+
public function __construct(
23+
private(set) null|string|array|ArrayInterface|Address $to = null,
24+
private(set) null|string|array|ArrayInterface|Address $from = null,
25+
private(set) null|string|array|ArrayInterface|Address $replyTo = null,
26+
private(set) null|string|array|ArrayInterface|Address $cc = null,
27+
private(set) null|string|array|ArrayInterface|Address $bcc = null,
28+
private(set) ?string $subject = null,
29+
private(set) null|string|View $html = null,
30+
private(set) ?string $text = null,
31+
private(set) Priority|int $priority = Priority::NORMAL,
32+
private(set) array $headers = [],
33+
private(set) array $attachments = [],
34+
) {}
35+
36+
/**
37+
* Defines the recipients of the email.
38+
*/
39+
public function to(null|string|array|ArrayInterface|Address $to): self
40+
{
41+
$this->to = $to;
42+
43+
return $this;
44+
}
45+
46+
/**
47+
* Defines the sender of the email.
48+
*/
49+
public function from(null|string|array|ArrayInterface|Address $from): self
50+
{
51+
$this->from = $from;
52+
53+
return $this;
54+
}
55+
56+
/**
57+
* Defines the reply-to address of the email.
58+
*/
59+
public function replyTo(null|string|array|ArrayInterface|Address $replyTo): self
60+
{
61+
$this->replyTo = $replyTo;
62+
63+
return $this;
64+
}
65+
66+
/**
67+
* Defines the carbon-copy recipients of the email.
68+
*/
69+
public function cc(null|string|array|ArrayInterface|Address $cc): self
70+
{
71+
$this->cc = $cc;
72+
73+
return $this;
74+
}
75+
76+
/**
77+
* Defines the blind carbon-copy recipients of the email.
78+
*/
79+
public function bcc(null|string|array|ArrayInterface|Address $bcc): self
80+
{
81+
$this->bcc = $bcc;
82+
83+
return $this;
84+
}
85+
86+
/**
87+
* Defines the subject of the email.
88+
*/
89+
public function withSubject(string|Stringable $subject): self
90+
{
91+
$this->subject = (string) $subject;
92+
93+
return $this;
94+
}
95+
96+
/**
97+
* Defines the HTML body of the email.
98+
*/
99+
public function withHtml(string|View $html): self
100+
{
101+
$this->html = $html;
102+
103+
return $this;
104+
}
105+
106+
/**
107+
* Defines the text body of the email.
108+
*/
109+
public function withText(string $text): self
110+
{
111+
$this->text = $text;
112+
113+
return $this;
114+
}
115+
116+
/**
117+
* Defines the priority of the email.
118+
*/
119+
public function withPriority(Priority|int $priority): self
120+
{
121+
$this->priority = $priority;
122+
123+
return $this;
124+
}
125+
126+
/**
127+
* Defines the headers of the email.
128+
*/
129+
public function withHeaders(array $headers): self
130+
{
131+
$this->headers = $headers;
132+
133+
return $this;
134+
}
135+
136+
/**
137+
* Defines the attachments of the email.
138+
*
139+
* @param Attachment[] $attachments
140+
*/
141+
public function withAttachments(array $attachments): self
142+
{
143+
foreach ($attachments as $attachment) {
144+
if (! ($attachment instanceof Attachment)) {
145+
throw new \InvalidArgumentException(sprintf('All attachments must be instances of `%s`.', Attachment::class));
146+
}
147+
}
148+
149+
$this->attachments = $attachments;
150+
151+
return $this;
152+
}
153+
154+
/**
155+
* Adds an attachment to the email.
156+
*/
157+
public function withAttachment(Attachment $attachment): self
158+
{
159+
$this->attachments[] = $attachment;
160+
161+
return $this;
162+
}
163+
164+
/**
165+
* Adds an attachment from the filesystem.
166+
*/
167+
public function withFileAttachment(string $path, ?string $name = null, ?string $contentType = null): self
168+
{
169+
$this->attachments[] = FileAttachment::fromPath($path, $name, $contentType);
170+
171+
return $this;
172+
}
173+
174+
/**
175+
* Adds an attachment from the storage.
176+
*/
177+
public function withStorageAttachment(string $path, ?string $name = null, ?string $contentType = null, null|string|UnitEnum $tag = null): self
178+
{
179+
$this->attachments[] = StorageAttachment::fromPath($path, $name, $contentType, $tag);
180+
181+
return $this;
182+
}
183+
184+
/**
185+
* Builds the email.
186+
*/
187+
public function make(): Email
188+
{
189+
return new GenericEmail(
190+
envelope: new Envelope(
191+
subject: $this->subject,
192+
to: Arr\wrap($this->to),
193+
from: Arr\wrap($this->from),
194+
cc: Arr\wrap($this->cc),
195+
bcc: Arr\wrap($this->bcc),
196+
replyTo: Arr\wrap($this->replyTo),
197+
priority: is_int($this->priority)
198+
? Priority::from($this->priority)
199+
: $this->priority,
200+
headers: $this->headers,
201+
),
202+
content: new Content(
203+
html: $this->html,
204+
text: $this->text,
205+
attachments: $this->attachments,
206+
),
207+
);
208+
}
209+
}

packages/mailer/src/Content.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Tempest\Mail;
44

5+
use Tempest\Mail\Attachments\Attachment;
56
use Tempest\Mail\Exceptions\MissingContentException;
67
use Tempest\View\View;
78

@@ -13,7 +14,7 @@ final class Content
1314
public function __construct(
1415
public null|string|View $html = null,
1516
public ?string $text = null,
16-
/** @var Attachment[] */
17+
/** @var Tempest\Mail\Attachments\Attachment[] */
1718
public array $attachments = [],
1819
) {
1920
if (! $text && ! $html) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Mail\Exceptions;
4+
5+
use Exception;
6+
7+
final class CouldNotFindFileAttachmentException extends Exception implements MailerException
8+
{
9+
public function __construct(string $file)
10+
{
11+
parent::__construct(sprintf('File `%s` could not be found on the filesystem.', $file));
12+
}
13+
}

0 commit comments

Comments
 (0)