🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,ts,vue}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
@@ -0,0 +1,4 @@
vendor
composer.lock
.idea
.DS_Store
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) arukompas <arukomp@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,87 @@
<div align="center">
<p>
<h1>Mail Parser for PHP<br/>Simple, fast, no extensions required</h1>
</p>
</div>
<p align="center">
<a href="#features">Features</a> |
<a href="#installation">Installation</a> |
<a href="#credits">Credits</a>
</p>
<p align="center">
<a href="https://packagist.org/packages/opcodesio/mail-parser"><img src="https://img.shields.io/packagist/v/opcodesio/mail-parser.svg?style=flat-square" alt="Packagist"></a>
<a href="https://packagist.org/packages/opcodesio/mail-parser"><img src="https://img.shields.io/packagist/dm/opcodesio/mail-parser.svg?style=flat-square" alt="Packagist"></a>
<a href="https://packagist.org/packages/opcodesio/mail-parser"><img src="https://img.shields.io/packagist/php-v/opcodesio/mail-parser.svg?style=flat-square" alt="PHP from Packagist"></a>
</p>
## Features
[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.
Has not been fully tested against RFC 5322.
## Get Started
### Requirements
- **PHP 8.0+**
### Installation
To install the package via composer, Run:
```bash
composer require opcodesio/mail-parser
```
### Usage
```php
use Opcodes\MailParser\Message;
// Parse a message from a string
$message = Message::fromString('...');
// Or from a file location (accessible with file_get_contents())
$message = Message::fromFile('/path/to/email.eml');
$message->getHeaders(); // get all headers
$message->getHeader('Content-Type'); // 'multipart/mixed; boundary="----=_Part_1_1234567890"'
$message->getFrom(); // 'Arunas <arunas@example.com>
$message->getTo(); // 'John Doe <johndoe@example.com>
$message->getSubject(); // 'Subject line'
$message->getDate(); // DateTime object when the email was sent
$message->getSize(); // Email size in bytes
$message->getParts(); // Returns an array of \Opcodes\MailParser\MessagePart, which can be html parts, text parts, attachments, etc.
$message->getHtmlPart(); // Returns the \Opcodes\MailParser\MessagePart containing the HTML body
$message->getTextPart(); // Returns the \Opcodes\MailParser\MessagePart containing the Text body
$message->getAttachments(); // Returns an array of \Opcodes\MailParser\MessagePart that represent attachments
$messagePart = $message->getParts()[0];
$messagePart->getHeaders(); // array of all headers for this message part
$messagePart->getHeader('Content-Type'); // value of a particular header
$messagePart->getContentType(); // 'text/html; charset="utf-8"'
$messagePart->getContent(); // '<html><body>....'
$messagePart->getSize(); // 312
$messagePart->getFilename(); // name of the file, in case this is an attachment part
```
## Contributing
A guide for contributing is in progress...
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [Arunas Skirius](https://github.com/arukompas)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
@@ -0,0 +1,46 @@
{
"name": "opcodesio/mail-parser",
"description": "Parse emails without the mailparse extension",
"keywords": [
"arukompas",
"opcodesio",
"php",
"mail",
"email",
"email parser"
],
"license": "MIT",
"authors": [
{
"name": "Arunas Skirius",
"email": "arukomp@gmail.com",
"role": "Developer"
}
],
"scripts": {
"test": "vendor/bin/pest"
},
"autoload": {
"psr-4": {
"Opcodes\\MailParser\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Opcodes\\MailParser\\Tests\\": "tests"
}
},
"require": {
"php": "^8.0"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"require-dev": {
"pestphp/pest": "^2.16",
"symfony/var-dumper": "^6.3"
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
@@ -0,0 +1,284 @@
<?php
namespace Opcodes\MailParser;
class Message implements \JsonSerializable
{
protected string $message;
protected string $boundary;
protected array $headers = [];
/**
* @var MessagePart[]
*/
protected array $parts = [];
public function __construct(string $message)
{
$this->message = $message;
$this->parse();
}
public static function fromString($message): self
{
return new self($message);
}
public static function fromFile($path): self
{
return new self(file_get_contents($path));
}
public function getBoundary(): string
{
return $this->boundary;
}
public function getHeaders(): array
{
return $this->headers;
}
public function getHeader(string $header, $default = null): ?string
{
$header = strtolower($header);
foreach ($this->headers as $key => $value) {
if (strtolower($key) === $header) {
return $value;
}
}
return $default;
}
public function getContentType(): string
{
return $this->getHeader('Content-Type', '');
}
public function getId(): string
{
$header = $this->getHeader('Message-ID', '');
return trim($header, '<>');
}
public function getSubject(): string
{
return $this->getHeader('Subject', '');
}
public function getFrom(): string
{
return $this->getHeader('From', '');
}
public function getTo(): string
{
return $this->getHeader('To', '');
}
public function getReplyTo(): string
{
return $this->getHeader('Reply-To', '');
}
public function getDate(): ?\DateTime
{
return \DateTime::createFromFormat(
'D, d M Y H:i:s O',
$this->getHeader('Date')
) ?: null;
}
public function getParts(): array
{
return $this->parts;
}
public function getHtmlPart(): ?MessagePart
{
foreach ($this->parts as $part) {
if ($part->isHtml()) {
return $part;
}
}
return null;
}
public function getTextPart(): ?MessagePart
{
foreach ($this->parts as $part) {
if ($part->isText()) {
return $part;
}
}
return null;
}
/**
* @return MessagePart[]
*/
public function getAttachments(): array
{
return array_values(array_filter($this->parts, fn ($part) => $part->isAttachment()));
}
public function getSize(): int
{
return strlen($this->message);
}
public function toArray(): array
{
return [
'id' => $this->getId(),
'subject' => $this->getSubject(),
'from' => $this->getFrom(),
'to' => $this->getTo(),
'reply_to' => $this->getReplyTo(),
'date' => $this->getDate() ? $this->getDate()->format('c') : null,
'headers' => $this->getHeaders(),
'parts' => array_map(fn ($part) => $part->toArray(), $this->getParts()),
];
}
public function jsonSerialize(): mixed
{
return $this->toArray();
}
/**
* Parse the email message into headers and body parts.
*/
protected function parse(): void
{
$lines = explode("\n", $this->message);
$headerInProgress = null;
$collectingBody = false;
$currentBody = '';
$currentBodyHeaders = [];
$currentBodyHeaderInProgress = null;
foreach ($lines as $line) {
$line = rtrim($line, "\r\n ");
if ($headerInProgress) {
$this->headers[$headerInProgress] .= PHP_EOL . $line;
$headerInProgress = str_ends_with($this->headers[$headerInProgress], ';');
continue;
}
if ($currentBodyHeaderInProgress) {
$currentBodyHeaders[$currentBodyHeaderInProgress] .= PHP_EOL . $line;
$currentBodyHeaderInProgress = str_ends_with($currentBodyHeaders[$currentBodyHeaderInProgress], ';');
continue;
}
if (isset($this->boundary) && str_ends_with($line, '--'.$this->boundary.'--')) {
$line = str_replace('--'.$this->boundary.'--', '', $line);
$currentBody .= $line;
// We've reached the end of the message
break;
}
if (isset($this->boundary) && str_ends_with($line, '--'.$this->boundary)) {
$line = str_replace('--'.$this->boundary, '', $line);
if ($collectingBody) {
// We've reached the end of a part, add it and reset the variables
$this->addPart($currentBody . $line, $currentBodyHeaders);
}
$collectingBody = true;
$currentBody = '';
$currentBodyHeaders = [];
continue;
}
if ($collectingBody && preg_match('/^(?<key>[A-Za-z\-0-9]+): (?<value>.*)$/', $line, $matches)) {
$currentBodyHeaders[$matches['key']] = $matches['value'];
// if the last character is a semicolon, then the header is continued on the next line
if (str_ends_with($currentBodyHeaders[$matches['key']], ';')) {
$currentBodyHeaderInProgress = $matches['key'];
}
continue;
}
if ($collectingBody) {
$currentBody .= $line . PHP_EOL;
continue;
}
if (preg_match("/^Content-Type: (?<contenttype>multipart\/.*); boundary=(?<boundary>.*)$/", $line, $matches)) {
$this->headers['Content-Type'] = $matches['contenttype']."; boundary=".$matches['boundary'];
$this->boundary = trim($matches['boundary'], '"');
continue;
}
if (preg_match('/^(?<key>[A-Za-z\-0-9]+): (?<value>.*)$/', $line, $matches)) {
if (strtolower($matches['key']) === 'content-type' && !isset($this->boundary) && !str_contains($matches['value'], 'multipart/mixed')) {
// this might be a single-part message. Let's start collecting the body.
$collectingBody = true;
$currentBody = '';
$currentBodyHeaders = [
$matches['key'] => $matches['value'],
];
if (str_ends_with($currentBodyHeaders[$matches['key']], ';')) {
$currentBodyHeaderInProgress = $matches['key'];
}
continue;
}
$this->headers[$matches['key']] = $matches['value'];
// if the last character is a semicolon, then the header is continued on the next line
if (str_ends_with($this->headers[$matches['key']], ';')) {
$headerInProgress = $matches['key'];
}
continue;
}
if (preg_match("~^--(?<boundary>[0-9A-Za-z'()+_,-./:=?]{0,68}[0-9A-Za-z'()+_,-./=?])~", $line, $matches)) {
$this->boundary = trim($matches['boundary']);
$collectingBody = true;
$currentBody = '';
$currentBodyHeaders = [];
continue;
}
// The line is not part of the email message. Let's remove it altogether.
$this->message = ltrim(substr($this->message, strlen($line)));
}
if (!empty($currentBody) || !empty($currentBodyHeaders)) {
$this->addPart($currentBody, $currentBodyHeaders);
}
if (! $this->getContentType() && ($part = $this->getParts()[0] ?? null)) {
foreach ($part->getHeaders() as $key => $value) {
if (strtolower($key) === 'content-type') {
$this->headers[$key] = $value;
break;
}
}
}
}
protected function addPart(string $currentBody, array $currentBodyHeaders): void
{
$this->parts[] = new MessagePart(trim($currentBody), $currentBodyHeaders);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Opcodes\MailParser;
class MessagePart implements \JsonSerializable
{
protected string $content;
protected array $headers;
public function __construct(string $content, array $headers = [])
{
$this->content = $content;
$this->headers = $headers;
}
public function getContentType(): string
{
return $this->headers['Content-Type'] ?? '';
}
public function getHeaders(): array
{
return $this->headers;
}
public function getHeader(string $name, $default = null): mixed
{
return $this->headers[$name] ?? $default;
}
public function getContent(): string
{
if (strtolower($this->getHeader('Content-Transfer-Encoding', '')) === 'base64') {
return base64_decode($this->content);
}
return $this->content;
}
public function isHtml(): bool
{
return str_starts_with(strtolower($this->getContentType()), 'text/html');
}
public function isText(): bool
{
return str_starts_with(strtolower($this->getContentType()), 'text/plain');
}
public function isImage(): bool
{
return str_starts_with(strtolower($this->getContentType()), 'image/');
}
public function isAttachment(): bool
{
return str_starts_with($this->getHeader('Content-Disposition', ''), 'attachment');
}
public function getFilename(): string
{
if (preg_match('/filename=([^;]+)/', $this->getHeader('Content-Disposition'), $matches)) {
return trim($matches[1], '"');
}
if (preg_match('/name=([^;]+)/', $this->getContentType(), $matches)) {
return trim($matches[1], '"');
}
return '';
}
public function getSize(): int
{
return strlen($this->getContent());
}
public function toArray(): array
{
return [
'headers' => $this->getHeaders(),
'content' => $this->getContent(),
'filename' => $this->getFilename(),
'size' => $this->getSize(),
];
}
public function jsonSerialize(): mixed
{
return $this->toArray();
}
}
@@ -0,0 +1,29 @@
From: Arunas Practice <no-reply@example.com>
To: Arunas arukomp <arukomp@example.com>
Reply-To: Arunas Practice <arunas@example.com>
Subject: Appointment confirmation
Message-ID: <fddff4779513441c3f0c1811193f5b12@example.com>
MIME-Version: 1.0
Date: Thu, 24 Aug 2023 14:51:14 +0100
Content-Type: multipart/mixed; boundary=lGiKDww4
--lGiKDww4
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!-- omitted content -->
</html>
--lGiKDww4
Content-Type: text/calendar; name=Appointment.ics
Content-Transfer-Encoding: base64
Content-Disposition: attachment; name=Appointment.ics;
filename=Appointment.ics
QkVHSU46VkNBTEVOREFSDQpWRVJTSU9OOjIuMA0KUFJPRElEOi0vL2hhY2tzdy9oYW5kY2FsLy9O
T05TR01MIHYxLjAvL0VODQpCRUdJTjpWVElNRVpPTkUNClRaSUQ6RXVyb3BlL0xvbmRvbg0KWC1M
SUMtTE9DQVRJT046RXVyb3BlL0xvbmRvbg0KQkVHSU46REFZTElHSFQNClRaT0ZGU0VURlJPTTor
MDEwMA0KVFpPRkZTRVRUTzorMDIwMA0KVFpOQU1FOkNFU1QN
--lGiKDww4--
@@ -0,0 +1,27 @@
From: Arunas Practice <no-reply@example.com>
To: Arunas arukomp <arukomp@example.com>
Reply-To: Arunas Practice <arunas@example.com>
Subject: Appointment confirmation
Message-ID: <fddff4779513441c3f0c1811193f5b12@example.com>
MIME-Version: 1.0
Date: Thu, 24 Aug 2023 14:51:14 +0100
Content-Type: multipart/alternative; boundary=s1NCDW_3
--s1NCDW_3
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Hi Arunas Skirius,
This is a confirmation of your appointment.
--s1NCDW_3
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Title</title>
</head>
</html>
--s1NCDW_3--
@@ -0,0 +1,3 @@
<?php
// uses(Tests\TestCase::class)->in('Feature');
@@ -0,0 +1,10 @@
<?php
namespace Opcodes\MailParser\Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
@@ -0,0 +1,365 @@
<?php
namespace Opcodes\MailParser\Tests\Unit;
use Opcodes\MailParser\Message;
it('can parse a simple mail message', function () {
$messageString = <<<EOF
From: Sender <no-reply@example.com>
To: Receiver <receiver@example.com>
Subject: Test Subject
Message-ID: <6e30b164904cf01158c7cc58f144b9ca@example.com>
MIME-Version: 1.0
Date: Fri, 25 Aug 2023 15:36:13 +0200
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Email content goes here.
EOF;
$message = Message::fromString($messageString);
expect($message->getFrom())->toBe('Sender <no-reply@example.com>')
->and($message->getTo())->toBe('Receiver <receiver@example.com>')
->and($message->getSubject())->toBe('Test Subject')
->and($message->getId())->toBe('6e30b164904cf01158c7cc58f144b9ca@example.com')
->and($message->getDate()?->format('Y-m-d H:i:s'))->toBe('2023-08-25 15:36:13')
->and($message->getContentType())->toBe('text/html; charset=utf-8')
->and($message->getHtmlPart()?->getContent())->toBe('Email content goes here.')
->and($message->getHtmlPart()?->getHeaders())->toBe([
'Content-Type' => 'text/html; charset=utf-8',
'Content-Transfer-Encoding' => 'quoted-printable',
]);
});
it('can parse lowercase headers', function () {
$messageString = <<<EOF
from: Sender <no-reply@example.com>
to: Receiver <receiver@example.com>
subject: Test Subject
message-id: <6e30b164904cf01158c7cc58f144b9ca@example.com>
mime-version: 1.0
date: Fri, 25 Aug 2023 15:36:13 +0200
content-type: text/html; charset=utf-8
content-transfer-encoding: quoted-printable
Email content goes here.
EOF;
$message = Message::fromString($messageString);
expect($message->getHeaders())->toBe([
'from' => 'Sender <no-reply@example.com>',
'to' => 'Receiver <receiver@example.com>',
'subject' => 'Test Subject',
'message-id' => '<6e30b164904cf01158c7cc58f144b9ca@example.com>',
'mime-version' => '1.0',
'date' => 'Fri, 25 Aug 2023 15:36:13 +0200',
'content-type' => 'text/html; charset=utf-8',
])
->and($message->getFrom())->toBe('Sender <no-reply@example.com>')
->and($message->getHeader('Content-Type'))->toBe('text/html; charset=utf-8');
});
it('can parse a mail message with boundaries', function () {
date_default_timezone_set('UTC');
$messageString = <<<EOF
From: sender@example.com
To: recipient@example.com
Cc: cc@example.com
Bcc: bcc@example.com
Subject: This is an email with common headers
Date: Thu, 24 Aug 2023 21:15:01 PST
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Part_1_1234567890"
------=_Part_1_1234567890
Content-Type: text/plain; charset="utf-8"
This is the text version of the email.
------=_Part_1_1234567890
Content-Type: text/html; charset="utf-8"
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
------=_Part_1_1234567890--
EOF;
$message = new Message($messageString);
expect($message->getHeaders())->toBe([
'From' => 'sender@example.com',
'To' => 'recipient@example.com',
'Cc' => 'cc@example.com',
'Bcc' => 'bcc@example.com',
'Subject' => 'This is an email with common headers',
'Date' => 'Thu, 24 Aug 2023 21:15:01 PST',
'MIME-Version' => '1.0',
'Content-Type' => 'multipart/mixed; boundary="----=_Part_1_1234567890"',
])
->and($message->getSubject())->toBe('This is an email with common headers')
->and($message->getFrom())->toBe('sender@example.com')
->and($message->getTo())->toBe('recipient@example.com')
->and($message->getDate())->toBeInstanceOf(\DateTime::class)
->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 21:15:01');
$parts = $message->getParts();
expect($parts)->toHaveCount(2)
->and($parts[0]->getContentType())->toBe('text/plain; charset="utf-8"')
->and($parts[0]->getContent())->toBe('This is the text version of the email.')
->and($parts[1]->getContentType())->toBe('text/html; charset="utf-8"')
->and($parts[1]->getContent())->toBe(<<<EOF
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
EOF);
});
it('can parse a complex mail message', function () {
$message = Message::fromFile(__DIR__ . '/../Fixtures/complex_email.eml');
expect($message->getFrom())->toBe('Arunas Practice <no-reply@example.com>')
->and($message->getTo())->toBe('Arunas arukomp <arukomp@example.com>')
->and($message->getReplyTo())->toBe('Arunas Practice <arunas@example.com>')
->and($message->getSubject())->toBe('Appointment confirmation')
->and($message->getId())->toBe('fddff4779513441c3f0c1811193f5b12@example.com')
->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 14:51:14')
->and($message->getBoundary())->toBe('lGiKDww4');
$parts = $message->getParts();
expect($parts)->toHaveCount(2)
->and($parts[0]->getContentType())->toBe('text/html; charset=utf-8')
->and($parts[0]->getHeaders())->toBe([
'Content-Type' => 'text/html; charset=utf-8',
'Content-Transfer-Encoding' => 'quoted-printable',
])
->and($parts[1]->getContentType())->toBe('text/calendar; name=Appointment.ics')
->and($parts[1]->getHeaders())->toBe([
'Content-Type' => 'text/calendar; name=Appointment.ics',
'Content-Transfer-Encoding' => 'base64',
'Content-Disposition' => 'attachment; name=Appointment.ics;
filename=Appointment.ics',
]);
});
it('can parse a multi-format mail message', function () {
$message = Message::fromFile(__DIR__ . '/../Fixtures/multiformat_email.eml');
expect($message->getFrom())->toBe('Arunas Practice <no-reply@example.com>')
->and($message->getTo())->toBe('Arunas arukomp <arukomp@example.com>')
->and($message->getReplyTo())->toBe('Arunas Practice <arunas@example.com>')
->and($message->getSubject())->toBe('Appointment confirmation')
->and($message->getId())->toBe('fddff4779513441c3f0c1811193f5b12@example.com')
->and($message->getDate()->format('Y-m-d H:i:s'))->toBe('2023-08-24 14:51:14')
->and($message->getBoundary())->toBe('s1NCDW_3');
$parts = $message->getParts();
expect($parts)->toHaveCount(2)
->and($parts[0]->getContentType())->toBe('text/plain; charset=utf-8')
->and($parts[0]->getHeaders())->toBe([
'Content-Type' => 'text/plain; charset=utf-8',
'Content-Transfer-Encoding' => 'quoted-printable',
])
->and($parts[1]->getContentType())->toBe('text/html; charset=utf-8')
->and($parts[1]->getHeaders())->toBe([
'Content-Type' => 'text/html; charset=utf-8',
'Content-Transfer-Encoding' => 'quoted-printable',
])->and($message->getTextPart()?->getContent())->toBe(<<<EOF
Hi Arunas Skirius,
This is a confirmation of your appointment.
EOF);
});
it('can get contents of an encoded part', function () {
$messageString = <<<EOF
From: sender@example.com
To: recipient@example.com
Subject: This is an email with common headers
Date: Thu, 24 Aug 2023 21:15:01 PST
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Part_1_1234567890"
------=_Part_1_1234567890
Content-Type: text/html; charset="utf-8"
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
------=_Part_1_1234567890
Content-Type: text/plain; name=test.txt
Content-Transfer-Encoding: base64
Content-Disposition: attachment; name=test.txt;
filename="test.txt"; name="test.txt"
VGhpcyBpcyBhIHRlc3Qgc3RyaW5n
------=_Part_1_1234567890--
EOF;
$message = new Message($messageString);
$parts = $message->getParts();
expect($parts)->toHaveCount(2);
$htmlPart = $parts[0];
expect($htmlPart->getContentType())->toBe('text/html; charset="utf-8"')
->and($htmlPart->isHtml())->toBe(true);
$attachmentPart = $parts[1];
expect($attachmentPart->getContent())->toBe('This is a test string')
->and($attachmentPart->isAttachment())->toBe(true)
->and($attachmentPart->getFilename())->toBe('test.txt');
$attachments = $message->getAttachments();
expect($attachments)->toHaveCount(1)
->and($attachments)->toHaveKey(0);
});
it('skips initial content that is not part of the message', function () {
$messageString = <<<EOF
This is some initial content that is not part of the message.
From: sender@example.com
To: recipient@example.com
Subject: This is an email with common headers
Date: Thu, 24 Aug 2023 21:15:01 PST
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Part_1_1234567890"
------=_Part_1_1234567890
Content-Type: text/html; charset="utf-8"
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
------=_Part_1_1234567890--
EOF;
$message = Message::fromString($messageString);
expect($message->getFrom())->toBe('sender@example.com')
->and($message->getHtmlPart()?->getContent())->toBe(<<<EOF
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
EOF);
});
it('catches boundaries on the same line', function () {
$messageString = <<<EOF
From: sender@example.com
To: recipient@example.com
Subject: This is an email with common headers
Date: Thu, 24 Aug 2023 21:15:01 PST
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="b552as-tfy"
--b552as-tfy
Content-Type: text/html; charset="utf-8"
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>--b552as-tfy
Content-Type: text/plain; name=test.txt
Content-Transfer-Encoding: base64
Content-Disposition: attachment; name=test.txt;
filename="test.txt"; name="test.txt"
VGhpcyBpcyBhIHRlc3Qgc3RyaW5n--b552as-tfy--
EOF;
$message = Message::fromString($messageString);
expect($message->getParts())->toHaveCount(2)
->and($message->getParts()[0]->getContent())->toBe(<<<EOF
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>
EOF)
->and($message->getPArts()[1]->getContent())->toBe('This is a test string');
});
it('still parses with a broken boundary', function () {
$messageString = <<<EOF
From: sender@example.com
To: recipient@example.com
Subject: This is an email with common headers
Date: Thu, 24 Aug 2023 21:15:01 PST
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary¨cQXEYh
--a8cQXEYh
Content-Type: text/html; charset="utf-8"
<html>
<head>
<title>This is an HTML email</title>
</head>
<body>
<h1>This is the HTML version of the email</h1>
</body>
</html>--a8cQXEYh
Content-Type: text/plain; name=test.txt
Content-Transfer-Encoding: base64
Content-Disposition: attachment; name=test.txt;
filename="test.txt"; name="test.txt"
--a8cQXEYh--
EOF;
$messageString = str_replace("\n", "\r\n", $messageString);
$message = Message::fromString($messageString);
expect($message->getParts())->toHaveCount(2)
->and($message->getParts()[1]->isAttachment())->toBe(true)
->and($message->getParts()[1]->getContent())->toBeEmpty();
});