Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 07a54d2

Browse filesBrowse files
committed
[Mime] Simplify adding Parts to an Email
1 parent 0ca5051 commit 07a54d2
Copy full SHA for 07a54d2

File tree

13 files changed

+85
-133
lines changed
Filter options

13 files changed

+85
-133
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php
+8-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/composer.json
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/composer.json
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Email.php
+14-75Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/MessageConverter.php
+1-4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Part/DataPart.php
+16-37Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Part/TextPart.php
+20-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Tests/EmailTest.php
+13-5Lines 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
}
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
content

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.