Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ if($mail->hasAttachments()) {
// Print all information of $mail
print_r($mail);

// Access arbitrary headers without adding custom properties to the library
$originMessageId = $mail->getHeader('Origin-MessageID');
$receivedHeaders = $mail->getHeaders('Received');

// Print all attachements of $mail
echo "\n\nAttachments:\n";
print_r($mail->getAttachments());
Expand Down
137 changes: 137 additions & 0 deletions src/PhpImap/IncomingMailHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class IncomingMailHeader
/** @var string|null */
public $headersRaw;

/**
* @var string[][]
*
* @psalm-var array<string, list<string>>
*/
public $headersByName = [];

/** @var object|null */
public $headers;

Expand Down Expand Up @@ -152,4 +159,134 @@ class IncomingMailHeader

/** @var string|null */
public $references;

public function setHeadersRaw(string $headersRaw): void
{
$this->headersRaw = $headersRaw;
$this->headersByName = $this->parseHeadersRaw($headersRaw);
}

public function getHeader(string $headerName): ?string
{
$headers = $this->getHeaders($headerName);

if ([] === $headers) {
return null;
}

return $headers[0];
}

/**
* @return string[]
*
* @psalm-return list<string>
*/
public function getHeaders(string $headerName): array
{
$this->ensureParsedHeadersByName();

return $this->headersByName[$this->normalizeHeaderName($headerName)] ?? [];
}

/**
* @return string[][]
*
* @psalm-return array<string, list<string>>
*/
public function getAllHeaders(): array
{
$this->ensureParsedHeadersByName();

return $this->headersByName;
}

protected function ensureParsedHeadersByName(): void
{
if ([] !== $this->headersByName || null === $this->headersRaw) {
return;
}

$this->headersByName = $this->parseHeadersRaw($this->headersRaw);
}

/**
* @return string[][]
*
* @psalm-return array<string, list<string>>
*/
protected function parseHeadersRaw(string $headersRaw): array
{
$parsedHeaders = [];
$currentHeaderName = null;
$currentHeaderValue = '';

foreach ($this->splitHeaderLines($headersRaw) as $line) {
if ('' === $line) {
$this->storeParsedHeader($parsedHeaders, $currentHeaderName, $currentHeaderValue);
$currentHeaderName = null;
$currentHeaderValue = '';

continue;
}

if (null !== $currentHeaderName && 1 === \preg_match('/^[ \t]/', $line)) {
$currentHeaderValue .= ' '.\ltrim($line);

continue;
}

$this->storeParsedHeader($parsedHeaders, $currentHeaderName, $currentHeaderValue);

$separatorPosition = \strpos($line, ':');

if (false === $separatorPosition) {
$currentHeaderName = null;
$currentHeaderValue = '';

continue;
}

$currentHeaderName = \substr($line, 0, $separatorPosition);
$currentHeaderValue = \trim(\substr($line, $separatorPosition + 1));
}

$this->storeParsedHeader($parsedHeaders, $currentHeaderName, $currentHeaderValue);

return $parsedHeaders;
}

/**
* @return string[]
*
* @psalm-return list<string>
*/
protected function splitHeaderLines(string $headersRaw): array
{
/** @var list<string> */
return \explode("\n", \str_replace(["\r\n", "\r"], "\n", $headersRaw));
}

/**
* @param string[][] $parsedHeaders
* @param string|null $headerName
*
* @psalm-param array<string, list<string>> $parsedHeaders
*/
protected function storeParsedHeader(array &$parsedHeaders, ?string $headerName, string $headerValue): void
{
if (null === $headerName || '' === \trim($headerName)) {
return;
}

$headerName = $this->normalizeHeaderName($headerName);

$parsedHeaders[$headerName] ??= [];
$parsedHeaders[$headerName][] = \trim($headerValue);
}

protected function normalizeHeaderName(string $headerName): string
{
return \strtolower(\trim($headerName));
}
}
2 changes: 1 addition & 1 deletion src/PhpImap/Mailbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,7 @@ public function getMailHeader(int $mailId): IncomingMailHeader
}

$header = new IncomingMailHeader();
$header->headersRaw = $headersRaw;
$header->setHeadersRaw($headersRaw);
$header->headers = $head;
$header->id = $mailId;
$header->imapPath = $this->imapPath;
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/IncomingMailHeaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace PhpImap;

use PHPUnit\Framework\TestCase;

final class IncomingMailHeaderTest extends TestCase
{
public function testSetHeadersRawParsesFoldedRepeatedAndCaseInsensitiveHeaders(): void
{
$header = new IncomingMailHeader();
$header->setHeadersRaw(
"Origin-MessageID: <origin@example.com>\r\n".
"X-Trace: first\r\n".
"\tcontinued\r\n".
"x-trace: second\r\n".
"Subject: Example\r\n"
);

$this->assertSame('<origin@example.com>', $header->getHeader('origin-messageid'));
$this->assertSame(
['first continued', 'second'],
$header->getHeaders('X-TRACE')
);
$this->assertSame(
[
'origin-messageid' => ['<origin@example.com>'],
'x-trace' => ['first continued', 'second'],
'subject' => ['Example'],
],
$header->getAllHeaders()
);
}

public function testGetHeaderLazilyParsesDirectlyAssignedHeadersRaw(): void
{
$header = new IncomingMailHeader();
$header->headersRaw = "X-Custom-Header: custom value\r\n";

$this->assertSame('custom value', $header->getHeader('x-custom-header'));
$this->assertSame(
['custom value'],
$header->headersByName['x-custom-header']
);
$this->assertNull($header->getHeader('missing-header'));
$this->assertSame([], $header->getHeaders('missing-header'));
}

public function testIncomingMailRetainsParsedHeadersAfterSetHeader(): void
{
$header = new IncomingMailHeader();
$header->setHeadersRaw(
"X-Custom-Header: custom value\r\n".
"Received: mx1.example.test\r\n".
"Received: mx2.example.test\r\n"
);

$mail = new IncomingMail();
$mail->setHeader($header);

$this->assertSame('custom value', $mail->getHeader('X-Custom-Header'));
$this->assertSame(
['mx1.example.test', 'mx2.example.test'],
$mail->getHeaders('received')
);
}
}
Loading