Skip to content

Commit f0fa197

Browse files
Merge pull request #9730 from magento-gl/arrows_delivery_04152025
AC-14363: Investigate failure due to symfony/mailer in test AsyncBulkAccountManagementTest
2 parents 27217d0 + b2e4372 commit f0fa197

File tree

4 files changed

+147
-98
lines changed

4 files changed

+147
-98
lines changed

app/code/Magento/Email/Test/Fixture/FileTransport.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@
1818
class FileTransport implements RevertibleDataFixtureInterface
1919
{
2020
private const DEFAULT_DATA = [
21-
'directory' => DirectoryList::VAR_DIR,
22-
'path' => 'mail/%uniqid%',
23-
'data' => 'Bienvenue sur Le Site de Paris.'
21+
'directory' => DirectoryList::TMP,
22+
'path' => 'mail/%uniqid%'
2423
];
2524

2625
private const CONFIG_FILE = 'mail-transport-config.json';

dev/tests/api-functional/_files/Magento/TestModuleEmail/Model/Transport/File.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ public function send(SymfonyMessage|RawMessage $message, ?Envelope $envelope = n
6363
$config = $this->json->unserialize($directory->readFile(self::CONFIG_FILE));
6464
$directory = $this->filesystem->getDirectoryWrite($config['directory']);
6565
$mail = $message->toString();
66-
$addresses = $this->message->getHeaders()->get('To')?->getAddresses() ?? [];
66+
$addresses = $message->getHeaders()->get('To')?->getAddresses() ?? [];
6767
foreach ($addresses as $address) {
6868
$index = 1;
69-
$filename = preg_replace('/[^a-z0-9_]/', '__', strtolower($address->getEmail()));
69+
$filename = preg_replace('/[^a-z0-9_]/', '__', strtolower($address->getAddress()));
7070
$basePath = $config['path']. DIRECTORY_SEPARATOR . $filename;
7171
$path = $basePath . '.eml';
7272
while ($directory->isExist($path)) {

dev/tests/api-functional/testsuite/Magento/Customer/Api/AsyncBulkAccountManagementTest.php

+9-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Magento\Directory\Helper\Data as LocaleConfig;
1212
use Magento\Email\Test\Fixture\FileTransport as FileTransportFixture;
1313
use Magento\Framework\Filesystem;
14-
use Magento\Framework\App\Filesystem\DirectoryList;
1514
use Magento\Store\Test\Fixture\Group as StoreGroupFixture;
1615
use Magento\Store\Test\Fixture\Store as StoreFixture;
1716
use Magento\Store\Test\Fixture\Website as WebsiteFixture;
@@ -139,34 +138,31 @@ function (array $data) {
139138
$this->fail("Customer was not created");
140139
}
141140

141+
$mailConfig = $fixtures->get('mail_transport_config')->getData();
142142
$filesystem = $this->objectManager->get(Filesystem::class);
143-
$directory = $filesystem->getDirectoryRead(DirectoryList::VAR_DIR);
143+
$directory = $filesystem->getDirectoryRead($mailConfig['directory']);
144144

145145
// wait until a mail is sent
146146
try {
147147
$this->publisherConsumerController->waitForAsynchronousResult(
148-
function (\Magento\Framework\Filesystem\Directory\ReadInterface $directory) {
149-
return count($directory->read('')) > 0;
148+
function (\Magento\Framework\Filesystem\Directory\ReadInterface $directory, string $path) {
149+
return $directory->isExist($path) && count($directory->read($path)) > 0;
150150
},
151-
[$directory]
151+
[$directory, $mailConfig['path']]
152152
);
153153
} catch (PreconditionFailedException $e) {
154154
$this->fail("No mail was sent");
155155
}
156156

157-
$mailPaths = $directory->read(DirectoryList::VAR_DIR);
157+
$mailPaths = $directory->read($mailConfig['path']);
158158
$sentMails = count($mailPaths);
159159
$this->assertCount(1, $mailPaths, "Only 1 mail was expected to be sent, actually $sentMails were sent.");
160-
$mailContent = $directory->readFile('mail-transport-config.json');
160+
$mailContent = $directory->readFile($mailPaths[0]);
161161
$parser = $this->objectManager->get(Parser::class);
162-
$message = $parser->fromString((string)$mailContent)->getSymfonyMessage()->getBody();
163-
$decoded_data = base64_decode($message->bodyToString());
164-
$parts = explode("\r\n", $decoded_data);
165-
$count = count($parts);
166-
$mergedString = base64_decode($parts[$count - 2] . $parts[$count - 1]);
162+
$message = $parser->fromString($mailContent);
167163
$this->assertStringContainsString(
168164
'Bienvenue sur Le Site de Paris.',
169-
$mergedString
165+
quoted_printable_decode($message->getSymfonyMessage()->getBody()->bodyToString())
170166
);
171167
}
172168

dev/tests/integration/framework/Magento/TestFramework/Mail/Parser.php

+134-80
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77

88
namespace Magento\TestFramework\Mail;
99

10+
use Magento\Framework\Mail\AddressInterface;
1011
use Magento\Framework\Mail\AddressFactory;
12+
use Magento\Framework\Mail\EmailMessageInterface;
1113
use Magento\Framework\Mail\EmailMessageInterfaceFactory;
1214
use Magento\Framework\Mail\MimeMessageInterfaceFactory;
1315
use Magento\Framework\Mail\MimePartInterfaceFactory;
14-
use Symfony\Component\Mime\Part\DataPart;
15-
use Symfony\Component\Mime\Part\AbstractPart;
16-
use Symfony\Component\Mime\Message as SymfonyMessage;
17-
use Symfony\Component\Mime\Address as SymfonyAddress;
1816

1917
class Parser
2018
{
@@ -61,97 +59,153 @@ public function __construct(
6159
* Parses mail string into EmailMessage
6260
*
6361
* @param string $content
64-
* @return \Magento\Framework\Mail\EmailMessageInterface
62+
* @return EmailMessageInterface
6563
*/
66-
public function fromString(string $content): \Magento\Framework\Mail\EmailMessageInterface
64+
public function fromString(string $content): EmailMessageInterface
6765
{
68-
$part = new DataPart($content);
69-
$part->setDisposition('inline');
70-
$symfonyMimeMessage = new SymfonyMessage(null, $part);
71-
if ($symfonyMimeMessage->getBody() instanceof AbstractPart &&
72-
method_exists($symfonyMimeMessage->getBody(), 'getFilename')) {
73-
$filename = $symfonyMimeMessage->getBody()->getFilename();
74-
} else {
75-
$filename = '';
76-
}
77-
78-
$mimePart = [];
79-
80-
/** @var \Magento\Framework\Mail\MimePartInterface $mimePart */
81-
$mimePart = $this->mimePartInterfaceFactory->create(
82-
[
83-
'content' => $symfonyMimeMessage->getBody()->toString(),
84-
'type' => 'text/' . $symfonyMimeMessage->getBody()->getMediaSubtype(),
85-
'fileName' => $filename,
86-
'disposition' => $symfonyMimeMessage->getBody()->getDisposition(),
87-
'encoding' => $symfonyMimeMessage->getHeaders()->getHeaderBody('Content-Transfer-Encoding'),
88-
'description' => $symfonyMimeMessage->getHeaders()->getHeaderBody('Content-Description'),
89-
'filters' => [],
90-
'charset' => $symfonyMimeMessage->getHeaders()->get('Content-Type')?->getCharset(),
91-
'boundary' => $symfonyMimeMessage->getHeaders()->getHeaderParameter('Content-Type', 'boundary'),
92-
'location' => $symfonyMimeMessage->getHeaders()->getHeaderBody('Content-Location'),
93-
'language' => $symfonyMimeMessage->getHeaders()->getHeaderBody('Content-Language'),
94-
'isStream' => is_resource($symfonyMimeMessage->getBody())
95-
]
96-
);
97-
98-
$body = $this->mimeMessageInterfaceFactory->create([
99-
'parts' => [$mimePart]
66+
$parts = preg_split('/\r?\n\r?\n/', $content, 2);
67+
$headerText = $parts[0] ?? '';
68+
$bodyText = $parts[1] ?? '';
69+
$headers = $this->parseHeaders($headerText);
70+
$contentType = $headers['Content-Type'] ?? 'text/plain';
71+
$charset = $this->extractParameter($contentType, 'charset') ?? 'utf-8';
72+
$boundary = $this->extractParameter($contentType, 'boundary');
73+
$encoding = $headers['Content-Transfer-Encoding'] ?? 'quoted-printable';
74+
$disposition = $headers['Content-Disposition'] ?? 'inline';
75+
$decodedBody = match (strtolower($encoding)) {
76+
'base64' => base64_decode($bodyText),
77+
'quoted-printable' => quoted_printable_decode($bodyText),
78+
default => $bodyText,
79+
};
80+
81+
$mimePart = $this->mimePartInterfaceFactory->create([
82+
'content' => $decodedBody,
83+
'type' => strtok($contentType, ';'),
84+
'fileName' => '',
85+
'disposition' => $disposition,
86+
'encoding' => $encoding,
87+
'description' => $headers['Content-Description'] ?? '',
88+
'filters' => [],
89+
'charset' => $charset,
90+
'boundary' => $boundary,
91+
'location' => $headers['Content-Location'] ?? '',
92+
'language' => $headers['Content-Language'] ?? '',
93+
'isStream' => false
10094
]);
10195

102-
$sender = $symfonyMimeMessage->getHeaders()->get('Sender') ? $this->addressFactory->create([
103-
'email' => $symfonyMimeMessage->getHeaders()->get('Sender')->getAddress(),
104-
'name' => $symfonyMimeMessage->getHeaders()->get('Sender')->getName()
105-
]): $this->addressFactory->create([
106-
'email' => '[email protected]',
107-
'name' => 'Sender'
96+
$mimeMessage = $this->mimeMessageInterfaceFactory->create([
97+
'parts' => [$mimePart]
10898
]);
10999

110-
$address = [
111-
'email' => '[email protected]',
112-
'name' => 'John'
113-
];
100+
$to = $this->parseAddresses($headers['To'] ?? '');
101+
$from = $this->parseAddresses($headers['From'] ?? '');
102+
$cc = $this->parseAddresses($headers['Cc'] ?? '');
103+
$bcc = $this->parseAddresses($headers['Bcc'] ?? '');
104+
$replyTo = $this->parseAddresses($headers['Reply-To'] ?? '');
105+
106+
$sender = null;
107+
if (!empty($headers['Sender'])) {
108+
$senderAddresses = $this->parseAddresses($headers['Sender']);
109+
$sender = $senderAddresses[0] ?? null;
110+
} elseif (!empty($from)) {
111+
$sender = $from[0];
112+
}
114113

115114
return $this->emailMessageInterfaceFactory->create([
116-
'body' => $body,
117-
'subject' => $symfonyMimeMessage->getHeaders()->getHeaderBody('Subject'),
115+
'body' => $mimeMessage,
116+
'subject' => $headers['Subject'] ?? '',
118117
'sender' => $sender,
119-
'to' => $this->convertAddresses(
120-
$symfonyMimeMessage->getHeaders()->get('To')?->getAddresses()
121-
?? $address
122-
),
123-
'from' => $this->convertAddresses(
124-
$symfonyMimeMessage->getHeaders()->get('From')?->getAddresses()
125-
?? $address
126-
),
127-
'cc' => $this->convertAddresses(
128-
$symfonyMimeMessage->getHeaders()->get('Cc')?->getAddresses()
129-
?? $address
130-
),
131-
'bcc' => $this->convertAddresses(
132-
$symfonyMimeMessage->getHeaders()->get('Bcc')?->getAddresses()
133-
?? $address
134-
),
135-
'replyTo' => $this->convertAddresses(
136-
$symfonyMimeMessage->getHeaders()->get('Reply-To')?->getAddresses()
137-
?? $address
138-
),
118+
'to' => $to,
119+
'from' => $from,
120+
'cc' => $cc,
121+
'bcc' => $bcc,
122+
'replyTo' => $replyTo,
139123
]);
140124
}
141125

142126
/**
143-
* Convert addresses to internal mail addresses
127+
* Parse email headers from string more efficiently
144128
*
145-
* @param array $address
146-
* @return array
129+
* @param string $headerText
130+
* @return array<string, string>
147131
*/
148-
private function convertAddresses(array $address): array
132+
private function parseHeaders(string $headerText): array
149133
{
150-
return [
151-
$this->addressFactory->create([
152-
'email' => $address['email'],
153-
'name' => $address['name']
154-
])
155-
];
134+
if (empty($headerText)) {
135+
return [];
136+
}
137+
138+
$headers = [];
139+
$lines = preg_split('/\r?\n/', $headerText);
140+
$currentHeader = '';
141+
142+
foreach ($lines as $line) {
143+
if (preg_match('/^\s+(.+)$/', $line, $matches)) {
144+
if ($currentHeader !== '') {
145+
$headers[$currentHeader] .= ' ' . trim($matches[1]);
146+
}
147+
continue;
148+
}
149+
if (preg_match('/^([^:]+):\s*(.*)$/', $line, $matches)) {
150+
$currentHeader = $matches[1];
151+
$headers[$currentHeader] = trim($matches[2]);
152+
}
153+
}
154+
155+
return $headers;
156+
}
157+
158+
/**
159+
* Parse email addresses from string
160+
*
161+
* @param string $addressString
162+
* @return array<AddressInterface>
163+
*/
164+
private function parseAddresses(string $addressString): array
165+
{
166+
if (empty($addressString)) {
167+
return [];
168+
}
169+
170+
$addresses = [];
171+
$addressParts = explode(',', $addressString);
172+
173+
foreach ($addressParts as $addressPart) {
174+
$addressPart = trim($addressPart);
175+
if (preg_match('/^(?:"?([^"]*)"?\s*)?<?([^>]*)>?$/', $addressPart, $matches)) {
176+
$name = trim($matches[1]);
177+
$email = trim($matches[2]);
178+
179+
if (empty($email) && filter_var($matches[1], FILTER_VALIDATE_EMAIL)) {
180+
$email = $matches[1];
181+
$name = '';
182+
}
183+
184+
if (!empty($email)) {
185+
$addresses[] = $this->addressFactory->create([
186+
'email' => $email,
187+
'name' => $name
188+
]);
189+
}
190+
}
191+
}
192+
193+
return $addresses;
194+
}
195+
196+
/**
197+
* Extract parameter value from a header that contains parameters (like Content-Type)s
198+
*
199+
* @param string $header
200+
* @param string $paramName
201+
* @return string|null
202+
*/
203+
private function extractParameter(string $header, string $paramName): ?string
204+
{
205+
if (preg_match('/\b' . preg_quote($paramName) . '=(["\']?)([^"\';\s]+)\1/i', $header, $matches)) {
206+
return $matches[2];
207+
}
208+
209+
return null;
156210
}
157211
}

0 commit comments

Comments
 (0)