Skip to content

Commit 32b4b7c

Browse files
authored
[9.x] Improve file attachment for mail and notifications (#42563)
* Improve file attachment for mail and notifications * style ci
1 parent 091e287 commit 32b4b7c

File tree

10 files changed

+621
-9
lines changed

10 files changed

+621
-9
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Mail;
4+
5+
interface Attachable
6+
{
7+
/**
8+
* Get an attachment instance for this entity.
9+
*
10+
* @return \Illuminate\Mail\Attachment
11+
*/
12+
public function toMailAttachment();
13+
}

src/Illuminate/Mail/Attachment.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
namespace Illuminate\Mail;
4+
5+
use Closure;
6+
use Illuminate\Container\Container;
7+
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
8+
use Illuminate\Support\Traits\Macroable;
9+
10+
class Attachment
11+
{
12+
use Macroable;
13+
14+
/**
15+
* The attached file's filename.
16+
*
17+
* @var string|null
18+
*/
19+
public $as;
20+
21+
/**
22+
* The attached file's mime type.
23+
*
24+
* @var string|null
25+
*/
26+
public $mime;
27+
28+
/**
29+
* A callback that attaches the attachment to the mail message.
30+
*
31+
* @var \Closure
32+
*/
33+
protected $resolver;
34+
35+
/**
36+
* Create a mail attachment.
37+
*
38+
* @param \Closure $resolver
39+
* @return void
40+
*/
41+
private function __construct(Closure $resolver)
42+
{
43+
$this->resolver = $resolver;
44+
}
45+
46+
/**
47+
* Create a mail attachment from a path.
48+
*
49+
* @param string $path
50+
* @return static
51+
*/
52+
public static function fromPath($path)
53+
{
54+
return new static(fn ($attachment, $pathStragegy) => $pathStragegy($path, $attachment));
55+
}
56+
57+
/**
58+
* Create a mail attachment from in-memory data.
59+
*
60+
* @param \Closure $data
61+
* @param string $name
62+
* @return static
63+
*/
64+
public static function fromData(Closure $data, $name)
65+
{
66+
return (new static(
67+
fn ($attachment, $pathStrategy, $dataStrategy) => $dataStrategy($data, $attachment)
68+
))->as($name);
69+
}
70+
71+
/**
72+
* Create a mail attachment from a file in the default storage disk.
73+
*
74+
* @param string $path
75+
* @return static
76+
*/
77+
public static function fromStorage($path)
78+
{
79+
return static::fromStorageDisk(null, $path);
80+
}
81+
82+
/**
83+
* Create a mail attachment from a file in the specified storage disk.
84+
*
85+
* @param string|null $disk
86+
* @param string $path
87+
* @return static
88+
*/
89+
public static function fromStorageDisk($disk, $path)
90+
{
91+
return new static(function ($attachment, $pathStrategy, $dataStrategy) use ($disk, $path) {
92+
$storage = Container::getInstance()->make(
93+
FilesystemFactory::class
94+
)->disk($disk);
95+
96+
$attachment
97+
->as($attachment->as ?? basename($path))
98+
->withMime($attachment->mime ?? $storage->mimeType($path));
99+
100+
$dataStrategy(fn () => $storage->get($path), $attachment);
101+
});
102+
}
103+
104+
/**
105+
* Set the attached file's filename.
106+
*
107+
* @param string $name
108+
* @return $this
109+
*/
110+
public function as($name)
111+
{
112+
$this->as = $name;
113+
114+
return $this;
115+
}
116+
117+
/**
118+
* Set the attached file's mime type.
119+
*
120+
* @param string $mime
121+
* @return $this
122+
*/
123+
public function withMime($mime)
124+
{
125+
$this->mime = $mime;
126+
127+
return $this;
128+
}
129+
130+
/**
131+
* Attach the attachment with the given strategies.
132+
*
133+
* @param \Closure $pathStrategy
134+
* @param \Closure $dataStrategy
135+
* @return mixed
136+
*/
137+
public function attachWith(Closure $pathStrategy, Closure $dataStrategy)
138+
{
139+
return ($this->resolver)($this, $pathStrategy, $dataStrategy);
140+
}
141+
142+
/**
143+
* Attach the attachment to a built-in mail type.
144+
*
145+
* @param \Illuminate\Mail\Mailable|\Illuminate\Mail\Message|\Illuminate\Notifications\Messages\MailMessage $mail
146+
* @return mixed
147+
*/
148+
public function attachTo($mail)
149+
{
150+
return $this->attachWith(
151+
fn ($path) => $mail->attach($path, ['as' => $this->as, 'mime' => $this->mime]),
152+
fn ($data) => $mail->attachData($data(), $this->as, ['mime' => $this->mime])
153+
);
154+
}
155+
}

src/Illuminate/Mail/Mailable.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Container\Container;
66
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
7+
use Illuminate\Contracts\Mail\Attachable;
78
use Illuminate\Contracts\Mail\Factory as MailFactory;
89
use Illuminate\Contracts\Mail\Mailable as MailableContract;
910
use Illuminate\Contracts\Queue\Factory as Queue;
@@ -867,12 +868,20 @@ public function with($key, $value = null)
867868
/**
868869
* Attach a file to the message.
869870
*
870-
* @param string $file
871+
* @param string|\Illuminate\Contracts\Mail\Attachable|\Illuminate\Mail\Attachment $file
871872
* @param array $options
872873
* @return $this
873874
*/
874875
public function attach($file, array $options = [])
875876
{
877+
if ($file instanceof Attachable) {
878+
$file = $file->toMailAttachment();
879+
}
880+
881+
if ($file instanceof Attachment) {
882+
return $file->attachTo($this);
883+
}
884+
876885
$this->attachments = collect($this->attachments)
877886
->push(compact('file', 'options'))
878887
->unique('file')

src/Illuminate/Mail/Message.php

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

33
namespace Illuminate\Mail;
44

5+
use Illuminate\Contracts\Mail\Attachable;
56
use Illuminate\Support\Str;
67
use Illuminate\Support\Traits\ForwardsCalls;
78
use Symfony\Component\Mime\Address;
@@ -24,6 +25,8 @@ class Message
2425
/**
2526
* CIDs of files embedded in the message.
2627
*
28+
* @deprecated Will be removed in a future Laravel version.
29+
*
2730
* @var array
2831
*/
2932
protected $embeddedFiles = [];
@@ -290,12 +293,20 @@ public function priority($level)
290293
/**
291294
* Attach a file to the message.
292295
*
293-
* @param string $file
296+
* @param string|\Illuminate\Contracts\Mail\Attachable|\Illuminate\Mail\Attachment $file
294297
* @param array $options
295298
* @return $this
296299
*/
297300
public function attach($file, array $options = [])
298301
{
302+
if ($file instanceof Attachable) {
303+
$file = $file->toMailAttachment();
304+
}
305+
306+
if ($file instanceof Attachment) {
307+
return $file->attachTo($this);
308+
}
309+
299310
$this->message->attachFromPath($file, $options['as'] ?? null, $options['mime'] ?? null);
300311

301312
return $this;
@@ -319,11 +330,32 @@ public function attachData($data, $name, array $options = [])
319330
/**
320331
* Embed a file in the message and get the CID.
321332
*
322-
* @param string $file
333+
* @param string|\Illuminate\Contracts\Mail\Attachable|\Illuminate\Mail\Attachment $file
323334
* @return string
324335
*/
325336
public function embed($file)
326337
{
338+
if ($file instanceof Attachable) {
339+
$file = $file->toMailAttachment();
340+
}
341+
342+
if ($file instanceof Attachment) {
343+
return $file->attachWith(
344+
function ($path) use ($file) {
345+
$cid = $file->as ?? Str::random();
346+
347+
$this->message->embedFromPath($path, $cid, $file->mime);
348+
349+
return "cid:{$cid}";
350+
},
351+
function ($data) use ($file) {
352+
$this->message->embed($data(), $file->as, $file->mime);
353+
354+
return "cid:{$file->as}";
355+
}
356+
);
357+
}
358+
327359
$cid = Str::random(10);
328360

329361
$this->message->embedFromPath($file, $cid);

src/Illuminate/Notifications/Messages/MailMessage.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
namespace Illuminate\Notifications\Messages;
44

55
use Illuminate\Container\Container;
6+
use Illuminate\Contracts\Mail\Attachable;
67
use Illuminate\Contracts\Support\Arrayable;
78
use Illuminate\Contracts\Support\Renderable;
9+
use Illuminate\Mail\Attachment;
810
use Illuminate\Mail\Markdown;
911
use Illuminate\Support\Traits\Conditionable;
1012

@@ -241,12 +243,20 @@ public function bcc($address, $name = null)
241243
/**
242244
* Attach a file to the message.
243245
*
244-
* @param string $file
246+
* @param string|\Illuminate\Contracts\Mail\Attachable|\Illuminate\Contracts\Mail\Attachable $file
245247
* @param array $options
246248
* @return $this
247249
*/
248250
public function attach($file, array $options = [])
249251
{
252+
if ($file instanceof Attachable) {
253+
$file = $file->toMailAttachment();
254+
}
255+
256+
if ($file instanceof Attachment) {
257+
return $file->attachTo($this);
258+
}
259+
250260
$this->attachments[] = compact('file', 'options');
251261

252262
return $this;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Mail;
4+
5+
use Illuminate\Mail\Attachment;
6+
use Illuminate\Notifications\Messages\MailMessage;
7+
use Illuminate\Support\Facades\Storage;
8+
use Orchestra\Testbench\TestCase;
9+
10+
class AttachingFromStorageTest extends TestCase
11+
{
12+
public function testItCanAttachFromStorage()
13+
{
14+
Storage::disk('local')->put('/dir/foo.png', 'expected body contents');
15+
$mail = new MailMessage();
16+
$attachment = Attachment::fromStorageDisk('local', '/dir/foo.png')
17+
->as('bar')
18+
->withMime('text/css');
19+
20+
$attachment->attachTo($mail);
21+
22+
$this->assertSame([
23+
'data' => 'expected body contents',
24+
'name' => 'bar',
25+
'options' => [
26+
'mime' => 'text/css',
27+
],
28+
], $mail->rawAttachments[0]);
29+
30+
Storage::disk('local')->delete('/dir/foo.png');
31+
}
32+
33+
public function testItCanAttachFromStorageAndFallbackToStorageNameAndMime()
34+
{
35+
Storage::disk()->put('/dir/foo.png', 'expected body contents');
36+
$mail = new MailMessage();
37+
$attachment = Attachment::fromStorageDisk('local', '/dir/foo.png');
38+
39+
$attachment->attachTo($mail);
40+
41+
$this->assertSame([
42+
'data' => 'expected body contents',
43+
'name' => 'foo.png',
44+
'options' => [
45+
// when using "prefer-lowest" the local filesystem driver will
46+
// not detect the mime type based on the extension and will
47+
// instead fallback to "text/plain".
48+
'mime' => class_exists(\League\Flysystem\Local\FallbackMimeTypeDetector::class)
49+
? 'image/png'
50+
: 'text/plain',
51+
],
52+
], $mail->rawAttachments[0]);
53+
54+
Storage::disk('local')->delete('/dir/foo.png');
55+
}
56+
}

0 commit comments

Comments
 (0)