6 | MIME-Version: 1.0
7 | Date: Thu, 24 Aug 2023 14:51:14 +0100
8 | Content-Type: multipart/mixed; boundary=lGiKDww4
9 |
10 | --lGiKDww4
11 | Content-Type: text/html; charset=utf-8
12 | Content-Transfer-Encoding: quoted-printable
13 |
14 |
15 |
16 |
17 |
18 |
19 | --lGiKDww4
20 | Content-Type: text/calendar; name=Appointment.ics
21 | Content-Transfer-Encoding: base64
22 | Content-Disposition: attachment; name=Appointment.ics;
23 | filename=Appointment.ics
24 |
25 | QkVHSU46VkNBTEVOREFSDQpWRVJTSU9OOjIuMA0KUFJPRElEOi0vL2hhY2tzdy9oYW5kY2FsLy9O
26 | T05TR01MIHYxLjAvL0VODQpCRUdJTjpWVElNRVpPTkUNClRaSUQ6RXVyb3BlL0xvbmRvbg0KWC1M
27 | SUMtTE9DQVRJT046RXVyb3BlL0xvbmRvbg0KQkVHSU46REFZTElHSFQNClRaT0ZGU0VURlJPTTor
28 | MDEwMA0KVFpPRkZTRVRUTzorMDIwMA0KVFpOQU1FOkNFU1QN
29 | --lGiKDww4--
30 |
--------------------------------------------------------------------------------
/src/Utils.php:
--------------------------------------------------------------------------------
1 | $value) {
68 | if (str_starts_with($value, '=?')) {
69 | $decodedHeaders[$key] = self::decodeHeader($value);
70 | } else {
71 | $decodedHeaders[$key] = $value;
72 | }
73 | }
74 |
75 | return $decodedHeaders;
76 | }
77 |
78 | public static function decodeHeader(string $header): string
79 | {
80 | if (preg_match_all('/=\?([^?]+)\?([BQ])\?([^?]+)\?=/i', $header, $matches, PREG_SET_ORDER)) {
81 | foreach ($matches as $match) {
82 | $encoding = $match[1];
83 | $type = $match[2];
84 | $data = $match[3];
85 |
86 | if ($type === 'B') {
87 | $decoded = base64_decode($data);
88 | } else {
89 | $decoded = quoted_printable_decode($data);
90 | }
91 |
92 | $header = str_replace($match[0], $decoded, $header);
93 | }
94 | }
95 |
96 | return $header;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/MessagePart.php:
--------------------------------------------------------------------------------
1 | rawMessage = $message;
16 |
17 | $this->parse();
18 | }
19 |
20 | protected function parse(): void
21 | {
22 | // Split part into headers and content
23 | if (strpos($this->rawMessage, "\r\n\r\n") !== false) {
24 | [$headers, $content] = explode("\r\n\r\n", $this->rawMessage, 2);
25 |
26 | // Parse part headers
27 | $this->headers = Utils::parseHeaders($headers);
28 | $this->headers = Utils::decodeHeaders($this->headers);
29 |
30 | $this->content = trim($content);
31 | } else {
32 | // No headers, just content
33 | $this->content = trim($this->rawMessage);
34 | }
35 | }
36 |
37 | public function getContentType(): string
38 | {
39 | return $this->getHeader('Content-Type', '');
40 | }
41 |
42 | public function getContent(): string
43 | {
44 | if (strtolower($this->getHeader('Content-Transfer-Encoding', '')) === 'base64') {
45 | return Utils::normaliseLineEndings(base64_decode($this->content));
46 | }
47 |
48 | return Utils::normaliseLineEndings($this->content);
49 | }
50 |
51 | public function isHtml(): bool
52 | {
53 | return str_starts_with(strtolower($this->getContentType()), 'text/html');
54 | }
55 |
56 | public function isText(): bool
57 | {
58 | return str_starts_with(strtolower($this->getContentType()), 'text/plain');
59 | }
60 |
61 | public function isImage(): bool
62 | {
63 | return str_starts_with(strtolower($this->getContentType()), 'image/');
64 | }
65 |
66 | public function isAttachment(): bool
67 | {
68 | return str_starts_with($this->getHeader('Content-Disposition', ''), 'attachment');
69 | }
70 |
71 | public function getFilename(): string
72 | {
73 | if (preg_match('/filename=([^;]+)/', $this->getHeader('Content-Disposition'), $matches)) {
74 | return trim($matches[1], '"');
75 | }
76 |
77 | if (preg_match('/name=([^;]+)/', $this->getContentType(), $matches)) {
78 | return trim($matches[1], '"');
79 | }
80 |
81 | return '';
82 | }
83 |
84 | public function getSize(): int
85 | {
86 | return strlen($this->rawMessage);
87 | }
88 |
89 | public function toArray(): array
90 | {
91 | return [
92 | 'headers' => $this->getHeaders(),
93 | 'content' => $this->getContent(),
94 | 'filename' => $this->getFilename(),
95 | 'size' => $this->getSize(),
96 | ];
97 | }
98 |
99 | public function jsonSerialize(): mixed
100 | {
101 | return $this->toArray();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Mail Parser for PHP
Simple, fast, no extensions required
4 |
5 |
6 |
7 |
8 | Features |
9 | Installation |
10 | Credits
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Features
20 |
21 | [OPcodes's](https://www.opcodes.io/) **Mail Parser** has a very simple API to parse emails and their MIME contents. Unlike many other parsers out there, this package does not require the [mailparse](https://www.php.net/manual/en/book.mailparse.php) PHP extension.
22 |
23 | Has not been fully tested against RFC 5322.
24 |
25 | ## Get Started
26 |
27 | ### Requirements
28 |
29 | - **PHP 8.0+**
30 |
31 | ### Installation
32 |
33 | To install the package via composer, Run:
34 |
35 | ```bash
36 | composer require opcodesio/mail-parser
37 | ```
38 |
39 | ### Usage
40 |
41 | ```php
42 | use Opcodes\MailParser\Message;
43 |
44 | // Parse a message from a string
45 | $message = Message::fromString('...');
46 | // Or from a file location (accessible with file_get_contents())
47 | $message = Message::fromFile('/path/to/email.eml');
48 |
49 | $message->getHeaders(); // get all headers
50 | $message->getHeader('Content-Type'); // 'multipart/mixed; boundary="----=_Part_1_1234567890"'
51 | $message->getFrom(); // 'Arunas
52 | $message->getTo(); // 'John Doe
53 | $message->getSubject(); // 'Subject line'
54 | $message->getDate(); // DateTime object when the email was sent
55 | $message->getSize(); // Email size in bytes
56 |
57 | $message->getParts(); // Returns an array of \Opcodes\MailParser\MessagePart, which can be html parts, text parts, attachments, etc.
58 | $message->getHtmlPart(); // Returns the \Opcodes\MailParser\MessagePart containing the HTML body
59 | $message->getTextPart(); // Returns the \Opcodes\MailParser\MessagePart containing the Text body
60 | $message->getAttachments(); // Returns an array of \Opcodes\MailParser\MessagePart that represent attachments
61 |
62 | $messagePart = $message->getParts()[0];
63 |
64 | $messagePart->getHeaders(); // array of all headers for this message part
65 | $messagePart->getHeader('Content-Type'); // value of a particular header
66 | $messagePart->getContentType(); // 'text/html; charset="utf-8"'
67 | $messagePart->getContent(); // '....'
68 | $messagePart->getSize(); // 312
69 | $messagePart->getFilename(); // name of the file, in case this is an attachment part
70 | ```
71 |
72 | ## Contributing
73 |
74 | A guide for contributing is in progress...
75 |
76 | ## Security Vulnerabilities
77 |
78 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
79 |
80 | ## Credits
81 |
82 | - [Arunas Skirius](https://github.com/arukompas)
83 | - [All Contributors](../../contributors)
84 |
85 | ## License
86 |
87 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
88 |
--------------------------------------------------------------------------------
/tests/Fixtures/complex_email_2.eml:
--------------------------------------------------------------------------------
1 | Delivered-To: receiver@example.com
2 | Received: by 2002:a05:6a20:6a27:b0:1ce:d986:86ca with SMTP id asdf;
3 | Tue, 12 Nov 2024 07:01:18 -0800 (PST)
4 | X-Google-Smtp-Source: asdfasdfasdfasdfasdfasdfasdfasdf
5 | X-Received: by 2002:a05:6512:4025:b0:533:3fc8:43ee with SMTP id asdf-asdf.13.1731423677759;
6 | Tue, 12 Nov 2024 07:01:17 -0800 (PST)
7 | ARC-Seal: i=1; a=rsa-sha256; t=1731423677; cv=none;
8 | d=google.com; s=arc-20240605;
9 | b=asdf/asdf/asdf
10 | asdf/asdf
11 | asdf
12 | asdf/asdf+asdf
13 | asdf+asdf/sadf/OP5
14 | 8b+g==
15 | ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
16 | h=list-id:list-unsubscribe-post:list-unsubscribe:from:subject
17 | :reply-to:mime-version:date:message-id:to:content-transfer-encoding
18 | :dkim-signature;
19 | bh=asdf=;
20 | fh=asdf=;
21 | b=asdf/f/asdf/asdf/oECL+asdf
22 | asdf+nfQu/asdf/+asdf
23 | asdf/asdf
24 | asdf+1ig+asdf
25 | asdf/asdf
26 | XGyw==;
27 | dara=google.com
28 | ARC-Authentication-Results: i=1; mx.google.com;
29 | dkim=pass header.i=@test34345345435.com header.s=scph0924 header.b="ChJ/3uaY";
30 | spf=pass (google.com: domain of msprvs1=asdf=bounces-12345@sparkpost.bounce.edrone.me designates 123.123.123.123 as permitted sender) smtp.mailfrom="asdf=asdf=bounces-12345@sparkpost.bounce.edrone.me";
31 | dmarc=pass (p=QUARANTINE sp=NONE dis=NONE) header.from=test34345345435.com
32 | Return-Path:
33 | Received: from mta-12-228-142.sparkpostmail.com (mta-12-228-142.sparkpostmail.com. [123.123.123.123])
34 | by mx.google.com with ESMTPS id asdf-asdf.299.2024.11.12.07.01.17
35 | for
36 | (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
37 | Tue, 12 Nov 2024 07:01:17 -0800 (PST)
38 | Received-SPF: pass (google.com: domain of msprvs1=asdf=bounces-12345@sparkpost.bounce.edrone.me designates 123.123.123.123 as permitted sender) client-ip=123.123.123.123;
39 | Authentication-Results: mx.google.com;
40 | dkim=pass header.i=@test34345345435.com header.s=scph0924 header.b="ChJ/3uaY";
41 | spf=pass (google.com: domain of msprvs1=asdf=bounces-12345@sparkpost.bounce.edrone.me designates 123.123.123.123 as permitted sender) smtp.mailfrom="msprvs1=asdf=bounces-12345@sparkpost.bounce.edrone.me";
42 | dmarc=pass (p=QUARANTINE sp=NONE dis=NONE) header.from=test34345345435.com
43 | X-MSFBL: asdf+asdf=|asdf
44 | asdf
45 | asdf
46 | asdf
47 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=test34345345435.com;
48 | s=scph0924; t=1731423675; i=@test34345345435.com;
49 | bh=asdf=;
50 | h=Content-Type:To:Message-ID:Date:Subject:From:List-Unsubscribe:
51 | List-Unsubscribe-Post:From:To:Cc:Subject;
52 | b=ChJ/asdf+asdf
53 | 3Y+asdf/asdf
54 | asdf/asdf/asdf/asdf=
55 | Content-Transfer-Encoding: quoted-printable
56 | Content-Type: text/html; charset="UTF-8"
57 | To: receiver@example.com
58 | Message-ID: <01.A1.00000.ABC000000@gt.mta3vrest.cc.prd.sparkpost>
59 | Date: Tue, 12 Nov 2024 15:01:15 +0000
60 | MIME-Version: 1.0
61 | Reply-To: test@test34345345435.com
62 | Subject: =?utf-8?B?VGVzdCBzdWJqZWN0?=
63 | From: "Test Center"
64 | List-Unsubscribe: ,
65 | List-Unsubscribe-Post: List-Unsubscribe=One-Click
66 | List-Id:
67 |
68 | =0A=0A=0A=0A =0A =0A =
77 | =0A =0A
81 |
--------------------------------------------------------------------------------
/src/Message.php:
--------------------------------------------------------------------------------
1 | rawMessage = Utils::cleanUntilFirstHeader($message);
21 | $this->rawMessage = Utils::normaliseLineEndings($this->rawMessage, true);
22 |
23 | $this->parse();
24 | }
25 |
26 | public static function fromString($message): self
27 | {
28 | return new self($message);
29 | }
30 |
31 | public static function fromFile($path): self
32 | {
33 | return new self(file_get_contents($path));
34 | }
35 |
36 | public function getBoundary(): ?string
37 | {
38 | return $this->boundary ?? null;
39 | }
40 |
41 | public function getContentType(): string
42 | {
43 | return $this->getHeader('Content-Type', '');
44 | }
45 |
46 | public function getId(): string
47 | {
48 | $header = $this->getHeader('Message-ID', '');
49 |
50 | return trim($header, '<>');
51 | }
52 |
53 | public function getSubject(): string
54 | {
55 | return $this->getHeader('Subject', '');
56 | }
57 |
58 | public function getFrom(): string
59 | {
60 | return $this->getHeader('From', '');
61 | }
62 |
63 | public function getTo(): string
64 | {
65 | return $this->getHeader('To', '');
66 | }
67 |
68 | public function getReplyTo(): string
69 | {
70 | return $this->getHeader('Reply-To', '');
71 | }
72 |
73 | public function getDate(): ?\DateTime
74 | {
75 | return \DateTime::createFromFormat(
76 | 'D, d M Y H:i:s O',
77 | $this->getHeader('Date')
78 | ) ?: null;
79 | }
80 |
81 | public function getParts(): array
82 | {
83 | return $this->parts;
84 | }
85 |
86 | public function getHtmlPart(): ?MessagePart
87 | {
88 | foreach ($this->getParts() as $part) {
89 | if ($part->isHtml()) {
90 | return $part;
91 | }
92 | }
93 |
94 | return null;
95 | }
96 |
97 | public function getTextPart(): ?MessagePart
98 | {
99 | foreach ($this->getParts() as $part) {
100 | if ($part->isText()) {
101 | return $part;
102 | }
103 | }
104 |
105 | return null;
106 | }
107 |
108 | /**
109 | * @return MessagePart[]
110 | */
111 | public function getAttachments(): array
112 | {
113 | return array_values(array_filter($this->parts, fn ($part) => $part->isAttachment()));
114 | }
115 |
116 | public function getSize(): int
117 | {
118 | return strlen($this->rawMessage);
119 | }
120 |
121 | public function toArray(): array
122 | {
123 | return [
124 | 'id' => $this->getId(),
125 | 'subject' => $this->getSubject(),
126 | 'from' => $this->getFrom(),
127 | 'to' => $this->getTo(),
128 | 'reply_to' => $this->getReplyTo(),
129 | 'date' => $this->getDate() ? $this->getDate()->format('c') : null,
130 | 'headers' => $this->getHeaders(),
131 | 'parts' => array_map(fn ($part) => $part->toArray(), $this->getParts()),
132 | ];
133 | }
134 |
135 | public function jsonSerialize(): mixed
136 | {
137 | return $this->toArray();
138 | }
139 |
140 | protected function parse(): void
141 | {
142 | // Split email into headers and body
143 | [$rawHeaders, $body] = explode("\r\n\r\n", $this->rawMessage, 2);
144 |
145 | // Parse top-level headers
146 | $this->headers = Utils::parseHeaders($rawHeaders);
147 | $this->headers = Utils::decodeHeaders($this->headers);
148 |
149 | // Get boundary if this is a multipart email
150 | $contentType = $this->getHeader('Content-Type');
151 | if ($contentType && preg_match('/boundary="?([^";\r\n]+)"?/', $contentType, $matches)) {
152 | $this->boundary = $matches[1];
153 | }
154 |
155 | if (!isset($this->boundary) && str_contains($contentType ?? '', 'multipart/')) {
156 | // multipart email, perhaps the boundary is corrupted in the header.
157 | // Let's attempt to find a boundary in the body.
158 | if (preg_match("~^--(?[0-9A-Za-z'()+_,-./:=?]{0,68}[0-9A-Za-z'()+_,-./=?])~", $body, $matches)) {
159 | $this->boundary = trim($matches['boundary']);
160 | }
161 | }
162 |
163 | // If no boundary, treat the entire body as a single part
164 | if (!isset($this->boundary)) {
165 | $part = $this->addPart($body ?? '');
166 | if ($contentType = $this->getHeader('Content-Type')) {
167 | $part->setHeader('Content-Type', $contentType);
168 | }
169 | if ($contentTransferEncoding = $this->getHeader('Content-Transfer-Encoding')) {
170 | $part->setHeader('Content-Transfer-Encoding', $contentTransferEncoding);
171 | $this->removeHeader('Content-Transfer-Encoding');
172 | }
173 | return;
174 | }
175 |
176 | // Split body into parts using boundary
177 | $parts = preg_split("/--" . preg_quote($this->boundary) . "(?:--|(?:\r\n|$))/", $body);
178 |
179 | // Process each part
180 | foreach ($parts as $rawPart) {
181 | if (empty(trim($rawPart))) continue;
182 |
183 | $this->addPart($rawPart);
184 | }
185 | }
186 |
187 | protected function addPart(string $rawMessage): MessagePart
188 | {
189 | $this->parts[] = $part = new MessagePart($rawMessage);
190 |
191 | return $part;
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/tests/Unit/MessageTest.php:
--------------------------------------------------------------------------------
1 |
10 | To: Receiver
11 | Subject: Test Subject
12 | Message-ID: <6e30b164904cf01158c7cc58f144b9ca@example.com>
13 | MIME-Version: 1.0
14 | Date: Fri, 25 Aug 2023 15:36:13 +0200
15 | Content-Type: text/html; charset=utf-8
16 | Content-Transfer-Encoding: quoted-printable
17 |
18 | Email content goes here.
19 | EOF;
20 |
21 | $message = Message::fromString($messageString);
22 |
23 | expect($message->getFrom())->toBe('Sender ')
24 | ->and($message->getTo())->toBe('Receiver ')
25 | ->and($message->getSubject())->toBe('Test Subject')
26 | ->and($message->getId())->toBe('6e30b164904cf01158c7cc58f144b9ca@example.com')
27 | ->and($message->getDate()?->format('Y-m-d H:i:s'))->toBe('2023-08-25 15:36:13')
28 | ->and($message->getContentType())->toBe('text/html; charset=utf-8')
29 | ->and($message->getHtmlPart()?->getContent())->toBe('Email content goes here.')
30 | ->and($message->getHtmlPart()?->getHeaders())->toBe([
31 | 'Content-Type' => 'text/html; charset=utf-8',
32 | 'Content-Transfer-Encoding' => 'quoted-printable',
33 | ]);
34 | });
35 |
36 | it('can parse lowercase headers', function () {
37 | $messageString = <<
39 | to: Receiver
40 | subject: Test Subject
41 | message-id: <6e30b164904cf01158c7cc58f144b9ca@example.com>
42 | mime-version: 1.0
43 | date: Fri, 25 Aug 2023 15:36:13 +0200
44 | content-type: text/html; charset=utf-8
45 | content-transfer-encoding: quoted-printable
46 |
47 | Email content goes here.
48 | EOF;
49 |
50 | $message = Message::fromString($messageString);
51 |
52 | expect($message->getHeaders())->toBe([
53 | 'from' => 'Sender ',
54 | 'to' => 'Receiver ',
55 | 'subject' => 'Test Subject',
56 | 'message-id' => '<6e30b164904cf01158c7cc58f144b9ca@example.com>',
57 | 'mime-version' => '1.0',
58 | 'date' => 'Fri, 25 Aug 2023 15:36:13 +0200',
59 | 'content-type' => 'text/html; charset=utf-8',
60 | ])
61 | ->and($message->getFrom())->toBe('Sender ')
62 | ->and($message->getHeader('Content-Type'))->toBe('text/html; charset=utf-8');
63 | });
64 |
65 | it('can parse a mail message with boundaries', function () {
66 | date_default_timezone_set('UTC');
67 | $messageString = <<
86 |
87 | This is an HTML email
88 |
89 |
90 | This is the HTML version of the email
91 |
92 |
93 |
94 | ------=_Part_1_1234567890--
95 | EOF;
96 |
97 | $message = new Message($messageString);
98 |
99 | expect($message->getHeaders())->toBe([
100 | 'From' => 'sender@example.com',
101 | 'To' => 'recipient@example.com',
102 | 'Cc' => 'cc@example.com',
103 | 'Bcc' => 'bcc@example.com',
104 | 'Subject' => 'This is an email with common headers',
105 | 'Date' => 'Thu, 24 Aug 2023 21:15:01 PST',
106 | 'MIME-Version' => '1.0',
107 | 'Content-Type' => 'multipart/mixed; boundary="----=_Part_1_1234567890"',
108 | ])
109 | ->and($message->getSubject())->toBe('This is an email with common headers')
110 | ->and($message->getFrom())->toBe('sender@example.com')
111 | ->and($message->getTo())->toBe('recipient@example.com')
112 | ->and($message->getDate())->toBeInstanceOf(\DateTime::class)
113 | ->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 21:15:01');
114 |
115 | $parts = $message->getParts();
116 |
117 | expect($parts)->toHaveCount(2)
118 | ->and($parts[0]->getContentType())->toBe('text/plain; charset="utf-8"')
119 | ->and($parts[0]->getContent())->toBe('This is the text version of the email.')
120 | ->and($parts[1]->getContentType())->toBe('text/html; charset="utf-8"')
121 | ->and($parts[1]->getContent())->toBe(<<
123 |
124 | This is an HTML email
125 |
126 |
127 | This is the HTML version of the email
128 |
129 |
130 | EOF);
131 |
132 | });
133 |
134 | it('can parse a complex mail message', function () {
135 | $message = Message::fromFile(__DIR__ . '/../Fixtures/complex_email.eml');
136 |
137 | expect($message->getFrom())->toBe('Arunas Practice ')
138 | ->and($message->getTo())->toBe('Arunas arukomp ')
139 | ->and($message->getReplyTo())->toBe('Arunas Practice ')
140 | ->and($message->getSubject())->toBe('Appointment confirmation')
141 | ->and($message->getId())->toBe('fddff4779513441c3f0c1811193f5b12@example.com')
142 | ->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 14:51:14')
143 | ->and($message->getBoundary())->toBe('lGiKDww4');
144 |
145 | $parts = $message->getParts();
146 |
147 | expect($parts)->toHaveCount(2)
148 | ->and($parts[0]->getContentType())->toBe('text/html; charset=utf-8')
149 | ->and($parts[0]->getHeaders())->toBe([
150 | 'Content-Type' => 'text/html; charset=utf-8',
151 | 'Content-Transfer-Encoding' => 'quoted-printable',
152 | ])
153 | ->and($parts[1]->getContentType())->toBe('text/calendar; name=Appointment.ics')
154 | ->and($parts[1]->getHeaders())->toBe([
155 | 'Content-Type' => 'text/calendar; name=Appointment.ics',
156 | 'Content-Transfer-Encoding' => 'base64',
157 | 'Content-Disposition' => 'attachment; name=Appointment.ics; filename=Appointment.ics',
158 | ]);
159 | });
160 |
161 | it('can parse a complex mail message 2', function () {
162 | $message = Message::fromFile(__DIR__ . '/../Fixtures/complex_email_2.eml');
163 |
164 | expect($message->getFrom())->toBe('"Test Center" ')
165 | ->and($message->getTo())->toBe('receiver@example.com')
166 | ->and($message->getReplyTo())->toBe('test@test34345345435.com')
167 | ->and($message->getSubject())->toBe('Test subject')
168 | ->and($message->getId())->toBe('01.A1.00000.ABC000000@gt.mta3vrest.cc.prd.sparkpost')
169 | ->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2024-11-12 15:01:15')
170 | ->and($message->getHeader('Delivered-To'))->toBe('receiver@example.com')
171 | ->and($message->getBoundary())->toBeNull();
172 |
173 | $parts = $message->getParts();
174 |
175 | expect($parts)->toHaveCount(1);
176 | $part = $parts[0];
177 |
178 | expect($part->isHtml())->toBeTrue()
179 | ->and($part->getContent())->toStartWith('');
181 | });
182 |
183 | it('can parse a multi-format mail message', function () {
184 | $message = Message::fromFile(__DIR__ . '/../Fixtures/multiformat_email.eml');
185 |
186 | expect($message->getFrom())->toBe('Arunas Practice ')
187 | ->and($message->getTo())->toBe('Arunas arukomp ')
188 | ->and($message->getReplyTo())->toBe('Arunas Practice ')
189 | ->and($message->getSubject())->toBe('Appointment confirmation')
190 | ->and($message->getId())->toBe('fddff4779513441c3f0c1811193f5b12@example.com')
191 | ->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 14:51:14')
192 | ->and($message->getBoundary())->toBe('s1NCDW_3');
193 |
194 | $parts = $message->getParts();
195 |
196 | expect($parts)->toHaveCount(2)
197 | ->and($parts[0]->getContentType())->toBe('text/plain; charset=utf-8')
198 | ->and($parts[0]->getHeaders())->toBe([
199 | 'Content-Type' => 'text/plain; charset=utf-8',
200 | 'Content-Transfer-Encoding' => 'quoted-printable',
201 | ])
202 | ->and($parts[1]->getContentType())->toBe('text/html; charset=utf-8')
203 | ->and($parts[1]->getHeaders())->toBe([
204 | 'Content-Type' => 'text/html; charset=utf-8',
205 | 'Content-Transfer-Encoding' => 'quoted-printable',
206 | ])->and($message->getTextPart()?->getContent())->toBe(<<
226 |
227 | This is an HTML email
228 |
229 |
230 | This is the HTML version of the email
231 |
232 |