Skip to content

Commit 07a54d2

Browse files
committed
[Mime] Simplify adding Parts to an Email
1 parent 0ca5051 commit 07a54d2

File tree

13 files changed

+85
-133
lines changed

13 files changed

+85
-133
lines changed

src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,16 @@ public function testSymfonySerialize()
7474
"htmlCharset": null,
7575
"attachments": [
7676
{
77+
"filename": "test.txt",
78+
"mediaType": "application",
7779
"body": "Some Text file",
80+
"charset": null,
81+
"subtype": "octet-stream",
82+
"disposition": "attachment",
7883
"name": "test.txt",
79-
"content-type": null,
80-
"inline": false
84+
"encoding": "base64",
85+
"headers": [],
86+
"class": "Symfony\\\Component\\\Mime\\\Part\\\DataPart"
8187
}
8288
],
8389
"headers": {

src/Symfony/Bridge/Twig/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"symfony/security-core": "^5.4|^6.0",
4444
"symfony/security-csrf": "^5.4|^6.0",
4545
"symfony/security-http": "^5.4|^6.0",
46-
"symfony/serializer": "^5.4|^6.0",
46+
"symfony/serializer": "^6.2",
4747
"symfony/stopwatch": "^5.4|^6.0",
4848
"symfony/console": "^5.4|^6.0",
4949
"symfony/expression-language": "^5.4|^6.0",

src/Symfony/Component/Mailer/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"psr/event-dispatcher": "^1",
2222
"psr/log": "^1|^2|^3",
2323
"symfony/event-dispatcher": "^5.4|^6.0",
24-
"symfony/mime": "^5.4|^6.0",
24+
"symfony/mime": "^6.2",
2525
"symfony/service-contracts": "^1.1|^2|^3"
2626
},
2727
"require-dev": {

src/Symfony/Component/Mime/Email.php

Lines changed: 14 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -326,25 +326,15 @@ public function getHtmlCharset(): ?string
326326
*/
327327
public function attach($body, string $name = null, string $contentType = null): static
328328
{
329-
if (!\is_string($body) && !\is_resource($body)) {
330-
throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body)));
331-
}
332-
333-
$this->cachedBody = null;
334-
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
335-
336-
return $this;
329+
return $this->attachPart(new DataPart($body, $name, $contentType));
337330
}
338331

339332
/**
340333
* @return $this
341334
*/
342335
public function attachFromPath(string $path, string $name = null, string $contentType = null): static
343336
{
344-
$this->cachedBody = null;
345-
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
346-
347-
return $this;
337+
return $this->attachPart(new DataPart('file://'.$path, $name, $contentType));
348338
}
349339

350340
/**
@@ -354,25 +344,15 @@ public function attachFromPath(string $path, string $name = null, string $conten
354344
*/
355345
public function embed($body, string $name = null, string $contentType = null): static
356346
{
357-
if (!\is_string($body) && !\is_resource($body)) {
358-
throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body)));
359-
}
360-
361-
$this->cachedBody = null;
362-
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
363-
364-
return $this;
347+
return $this->attachPart((new DataPart($body, $name, $contentType))->asInline());
365348
}
366349

367350
/**
368351
* @return $this
369352
*/
370353
public function embedFromPath(string $path, string $name = null, string $contentType = null): static
371354
{
372-
$this->cachedBody = null;
373-
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
374-
375-
return $this;
355+
return $this->attachPart((new DataPart('file://'.$path, $name, $contentType))->asInline());
376356
}
377357

378358
/**
@@ -381,22 +361,17 @@ public function embedFromPath(string $path, string $name = null, string $content
381361
public function attachPart(DataPart $part): static
382362
{
383363
$this->cachedBody = null;
384-
$this->attachments[] = ['part' => $part];
364+
$this->attachments[] = $part;
385365

386366
return $this;
387367
}
388368

389369
/**
390-
* @return array|DataPart[]
370+
* @return DataPart[]
391371
*/
392372
public function getAttachments(): array
393373
{
394-
$parts = [];
395-
foreach ($this->attachments as $attachment) {
396-
$parts[] = $this->createDataPart($attachment);
397-
}
398-
399-
return $parts;
374+
return $this->attachments;
400375
}
401376

402377
public function getBody(): AbstractPart
@@ -502,35 +477,23 @@ private function prepareParts(): ?array
502477
}
503478

504479
$otherParts = $relatedParts = [];
505-
foreach ($this->attachments as $attachment) {
506-
$part = $this->createDataPart($attachment);
507-
if (isset($attachment['part'])) {
508-
$attachment['name'] = $part->getName();
509-
}
510-
511-
$related = false;
480+
foreach ($this->attachments as $part) {
512481
foreach ($names as $name) {
513-
if ($name !== $attachment['name']) {
482+
if ($name !== $part->getName()) {
514483
continue;
515484
}
516485
if (isset($relatedParts[$name])) {
517486
continue 2;
518487
}
519-
$part->setDisposition('inline');
488+
520489
$html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count);
521-
if ($count) {
522-
$related = true;
523-
}
524-
$part->setName($part->getContentId());
490+
$relatedParts[$name] = $part;
491+
$part->setName($part->getContentId())->asInline();
525492

526-
break;
493+
continue 2;
527494
}
528495

529-
if ($related) {
530-
$relatedParts[$attachment['name']] = $part;
531-
} else {
532-
$otherParts[] = $part;
533-
}
496+
$otherParts[] = $part;
534497
}
535498
if (null !== $htmlPart) {
536499
$htmlPart = new TextPart($html, $this->htmlCharset, 'html');
@@ -539,24 +502,6 @@ private function prepareParts(): ?array
539502
return [$htmlPart, $otherParts, array_values($relatedParts)];
540503
}
541504

542-
private function createDataPart(array $attachment): DataPart
543-
{
544-
if (isset($attachment['part'])) {
545-
return $attachment['part'];
546-
}
547-
548-
if (isset($attachment['body'])) {
549-
$part = new DataPart($attachment['body'], $attachment['name'] ?? null, $attachment['content-type'] ?? null);
550-
} else {
551-
$part = DataPart::fromPath($attachment['path'] ?? '', $attachment['name'] ?? null, $attachment['content-type'] ?? null);
552-
}
553-
if ($attachment['inline']) {
554-
$part->asInline();
555-
}
556-
557-
return $part;
558-
}
559-
560505
/**
561506
* @return $this
562507
*/
@@ -606,12 +551,6 @@ public function __serialize(): array
606551
$this->html = (new TextPart($this->html))->getBody();
607552
}
608553

609-
foreach ($this->attachments as $i => $attachment) {
610-
if (isset($attachment['body']) && \is_resource($attachment['body'])) {
611-
$this->attachments[$i]['body'] = (new TextPart($attachment['body']))->getBody();
612-
}
613-
}
614-
615554
return [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, parent::__serialize()];
616555
}
617556

src/Symfony/Component/Mime/MessageConverter.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,7 @@ private static function attachParts(Email $email, array $parts): Email
114114
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($email)));
115115
}
116116

117-
$headers = $part->getPreparedHeaders();
118-
$method = 'inline' === $headers->getHeaderBody('Content-Disposition') ? 'embed' : 'attach';
119-
$name = $headers->getHeaderParameter('Content-Disposition', 'filename');
120-
$email->$method($part->getBody(), $name, $part->getMediaType().'/'.$part->getMediaSubtype());
117+
$email->attachPart($part);
121118
}
122119

123120
return $email;

src/Symfony/Component/Mime/Part/DataPart.php

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,28 @@ class DataPart extends TextPart
2828
private $filename;
2929
private $mediaType;
3030
private $cid;
31-
private $handle;
3231

33-
/**
34-
* @param resource|string $body
35-
*/
3632
public function __construct($body, string $filename = null, string $contentType = null, string $encoding = null)
3733
{
3834
unset($this->_parent);
3935

36+
$path = null;
37+
if (\is_string($body) && str_starts_with($body, 'file://')) {
38+
$path = substr($body, \strlen('file://'));
39+
if (!$filename) {
40+
$filename = basename($path);
41+
}
42+
}
43+
4044
if (null === $contentType) {
4145
$contentType = 'application/octet-stream';
46+
if ($path) {
47+
$ext = strtolower(substr($path, strrpos($path, '.') + 1));
48+
if (null === self::$mimeTypes) {
49+
self::$mimeTypes = new MimeTypes();
50+
}
51+
$contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream';
52+
}
4253
}
4354
[$this->mediaType, $subtype] = explode('/', $contentType);
4455

@@ -53,32 +64,7 @@ public function __construct($body, string $filename = null, string $contentType
5364

5465
public static function fromPath(string $path, string $name = null, string $contentType = null): self
5566
{
56-
if (null === $contentType) {
57-
$ext = strtolower(substr($path, strrpos($path, '.') + 1));
58-
if (null === self::$mimeTypes) {
59-
self::$mimeTypes = new MimeTypes();
60-
}
61-
$contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream';
62-
}
63-
64-
if ((is_file($path) && !is_readable($path)) || is_dir($path)) {
65-
throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path));
66-
}
67-
68-
if (false === $handle = @fopen($path, 'r', false)) {
69-
throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path));
70-
}
71-
72-
if (!is_file($path)) {
73-
$cache = fopen('php://temp', 'r+');
74-
stream_copy_to_stream($handle, $cache);
75-
$handle = $cache;
76-
}
77-
78-
$p = new self($handle, $name ?: basename($path), $contentType);
79-
$p->handle = $handle;
80-
81-
return $p;
67+
return new self('file://'.$path, $name, $contentType);
8268
}
8369

8470
/**
@@ -158,13 +144,6 @@ private function generateContentId(): string
158144
return bin2hex(random_bytes(16)).'@symfony';
159145
}
160146

161-
public function __destruct()
162-
{
163-
if (null !== $this->handle && \is_resource($this->handle)) {
164-
fclose($this->handle);
165-
}
166-
}
167-
168147
public function __sleep(): array
169148
{
170149
// converts the body to a string

src/Symfony/Component/Mime/Part/TextPart.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class TextPart extends AbstractPart
4040
private $seekable;
4141

4242
/**
43-
* @param resource|string $body
43+
* @param resource|string $body Use file:// to defer loading the file until rendering
4444
*/
4545
public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', string $encoding = null)
4646
{
@@ -52,6 +52,13 @@ public function __construct($body, ?string $charset = 'utf-8', string $subtype =
5252
throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, get_debug_type($body)));
5353
}
5454

55+
if (\is_string($body) && str_starts_with($body, 'file://')) {
56+
$path = substr($body, \strlen('file://'));
57+
if ((is_file($path) && !is_readable($path)) || is_dir($path)) {
58+
throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path));
59+
}
60+
}
61+
5562
$this->body = $body;
5663
$this->charset = $charset;
5764
$this->subtype = $subtype;
@@ -116,6 +123,10 @@ public function getName(): ?string
116123

117124
public function getBody(): string
118125
{
126+
if (\is_string($this->body) && str_starts_with($this->body, 'file://')) {
127+
return file_get_contents(substr($this->body, \strlen('file://')));
128+
}
129+
119130
if (null === $this->seekable) {
120131
return $this->body;
121132
}
@@ -134,7 +145,14 @@ public function bodyToString(): string
134145

135146
public function bodyToIterable(): iterable
136147
{
137-
if (null !== $this->seekable) {
148+
if (\is_string($this->body) && str_starts_with($this->body, 'file://')) {
149+
$path = substr($this->body, \strlen('file://'));
150+
if (false === $handle = @fopen($path, 'r', false)) {
151+
throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path));
152+
}
153+
154+
yield from $this->getEncoder()->encodeByteStream($handle);
155+
} elseif (null !== $this->seekable) {
138156
if ($this->seekable) {
139157
rewind($this->body);
140158
}

src/Symfony/Component/Mime/Tests/EmailTest.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,9 @@ public function testSerialize()
504504
$expected = clone $e;
505505
$n = unserialize(serialize($e));
506506
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
507-
$this->assertEquals($e->getBody(), $n->getBody());
507+
$a = preg_replace(["{boundary=.+\r\n}", "{^\-\-.+\r\n}m"], ['boundary=x', '--x'], $e->getBody()->toString());
508+
$b = preg_replace(["{boundary=.+\r\n}", "{^\-\-.+\r\n}m"], ['boundary=x', '--x'], $n->getBody()->toString());
509+
$this->assertEquals($a, $b);
508510
}
509511

510512
public function testSymfonySerialize()
@@ -525,10 +527,16 @@ public function testSymfonySerialize()
525527
"htmlCharset": "utf-8",
526528
"attachments": [
527529
{
530+
"filename": "test.txt",
531+
"mediaType": "application",
528532
"body": "Some Text file",
533+
"charset": null,
534+
"subtype": "octet-stream",
535+
"disposition": "attachment",
529536
"name": "test.txt",
530-
"content-type": null,
531-
"inline": false
537+
"encoding": "base64",
538+
"headers": [],
539+
"class": "Symfony\\\Component\\\Mime\\\Part\\\DataPart"
532540
}
533541
],
534542
"headers": {
@@ -587,15 +595,15 @@ public function testMissingHeaderDoesNotThrowError()
587595
public function testAttachBodyExpectStringOrResource()
588596
{
589597
$this->expectException(\TypeError::class);
590-
$this->expectExceptionMessage('The body must be a string or a resource (got "bool").');
598+
$this->expectExceptionMessage('The body of "Symfony\Component\Mime\Part\TextPart" must be a string or a resource (got "bool").');
591599

592600
(new Email())->attach(false);
593601
}
594602

595603
public function testEmbedBodyExpectStringOrResource()
596604
{
597605
$this->expectException(\TypeError::class);
598-
$this->expectExceptionMessage('The body must be a string or a resource (got "bool").');
606+
$this->expectExceptionMessage('The body of "Symfony\Component\Mime\Part\TextPart" must be a string or a resource (got "bool").');
599607

600608
(new Email())->embed(false);
601609
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
content

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy