Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
arukompas committed Aug 25, 2023
0 parents commit f996b56
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor
composer.lock
.idea
46 changes: 46 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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"
}
}
15 changes: 15 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -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>
171 changes: 171 additions & 0 deletions src/Message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

namespace Opcodes\MailParser;

class Message
{
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
{
return $this->headers[$header] ?? $default;
}

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;
}

protected function parse()
{
// Parse the email message into headers and body
$lines = explode("\n", $this->message);
$headerInProgress = null;

$collectingBody = false;
$currentBody = '';
$currentBodyHeaders = [];
$currentBodyHeaderInProgress = null;

foreach ($lines as $line) {
if ($headerInProgress) {
$this->headers[$headerInProgress] .= "\n" . $line;
$headerInProgress = str_ends_with($line, ';');
continue;
}

if ($currentBodyHeaderInProgress) {
$currentBodyHeaders[$currentBodyHeaderInProgress] .= "\n" . $line;
$currentBodyHeaderInProgress = str_ends_with($line, ';');
continue;
}

if (isset($this->boundary) && $line === '--'.$this->boundary.'--') {
// We've reached the end of the message
$this->addPart($currentBody, $currentBodyHeaders);
continue;
}

if (isset($this->boundary) && $line === '--'.$this->boundary) {
if ($collectingBody) {
// We've reached the end of a part, add it and reset the variables
$this->addPart($currentBody, $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($matches['value'], ';')) {
$currentBodyHeaderInProgress = $matches['key'];
}

continue;
}

if ($collectingBody) {
$currentBody .= $line."\n";
continue;
}

if (preg_match('/^Content-Type: multipart\/mixed; boundary=(?<boundary>.*)$/', $line, $matches)) {
$this->headers['Content-Type'] = 'multipart/mixed; boundary='.$matches['boundary'];
$this->boundary = trim($matches['boundary'], '"');
continue;
}

if (preg_match('/^(?<key>[A-Za-z\-0-9]+): (?<value>.*)$/', $line, $matches)) {
$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($matches['value'], ';')) {
$headerInProgress = $matches['key'];
}

continue;
}
}
}

protected function addPart(string $currentBody, array $currentBodyHeaders): void
{
$this->parts[] = new MessagePart(trim($currentBody), $currentBodyHeaders);
}
}
58 changes: 58 additions & 0 deletions src/MessagePart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Opcodes\MailParser;

class MessagePart
{
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 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 '';
}
}
29 changes: 29 additions & 0 deletions tests/Fixtures/complex_email.eml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
From: Arunas Practice <[email protected]>
To: Arunas arukomp <[email protected]>
Reply-To: Arunas Practice <[email protected]>
Subject: Appointment confirmation
Message-ID: <[email protected]>
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--
3 changes: 3 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

// uses(Tests\TestCase::class)->in('Feature');
10 changes: 10 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Opcodes\MailParser\Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
//
}
Loading

0 comments on commit f996b56

Please sign in to comment.