diff --git a/src/Header/ContentType.php b/src/Header/ContentType.php index 46f1d3ef..6dcd00b9 100644 --- a/src/Header/ContentType.php +++ b/src/Header/ContentType.php @@ -61,7 +61,7 @@ public function getValue(): HeaderValue /** * @return ContentType */ - public static function unknown() + public static function unknown(): ContentType { return new self('application/octet-stream', ''); } diff --git a/src/Header/HeaderValue.php b/src/Header/HeaderValue.php index 72247d0c..91c74116 100644 --- a/src/Header/HeaderValue.php +++ b/src/Header/HeaderValue.php @@ -9,6 +9,14 @@ */ final class HeaderValue { + /** + * + */ + private CONST PARSE_START = 1; + /** + * + */ + private CONST PARSE_QUOTE = 2; /** * @var string */ @@ -129,4 +137,72 @@ public static function fromEncodedString(string $value): HeaderValue $headerValue->needsEncoding = false; return $headerValue; } + + /** + * @param string $headerValueAsString + * @return HeaderValue + */ + public static function fromString(string $headerValueAsString): HeaderValue + { + $values = []; + + $headerValueAsString = trim($headerValueAsString); + + $length = strlen($headerValueAsString) - 1; + $n = -1; + $state = self::PARSE_START; + $escapeNext = false; + $sequence = ''; + + while ($n < $length) { + $n++; + + $char = $headerValueAsString[$n]; + + $sequence .= $char; + + if ($char === '\\') { + $escapeNext = true; + continue; + } + + if ($escapeNext) { + $escapeNext = false; + continue; + } + + switch ($state) { + case self::PARSE_QUOTE: + if ($char === '"') { + $state = self::PARSE_START; + } + + break; + default: + if ($char === '"') { + $state = self::PARSE_QUOTE; + } + + if ($char === ';') { + $values[] = trim(substr($sequence, 0, -1)); + $sequence = ''; + $state = self::PARSE_START; + } + break; + } + } + + $values[] = trim($sequence); + + $headerValue = new self($values[0]); + + $parameters = []; + foreach (array_slice($values, 1) as $parameterString) { + $parameter = HeaderValueParameter::fromString($parameterString); + $parameters[$parameter->getName()] = $parameter; + } + + $headerValue->parameters = $parameters; + return $headerValue; + } } \ No newline at end of file diff --git a/src/Header/HeaderValueParameter.php b/src/Header/HeaderValueParameter.php index c028c15e..b17eb7f5 100644 --- a/src/Header/HeaderValueParameter.php +++ b/src/Header/HeaderValueParameter.php @@ -66,4 +66,26 @@ public function __toString(): string { return sprintf('%s="%s"', $this->name, $this->value); } + + /** + * @param string $parameterString + * @return HeaderValueParameter + */ + public static function fromString(string $parameterString): HeaderValueParameter + { + $nameValue = explode('=', $parameterString); + if (count($nameValue) !== 2) { + throw new \InvalidArgumentException( + sprintf('Invalid parameter string value %s', $parameterString) + ); + } + + [$name, $value] = $nameValue; + + if ($value[0] === '"' && $value[-1] === '"') { + $value = substr($value, 1, -1); + } + + return new self($name, $value); + } } \ No newline at end of file diff --git a/src/Mime/FileAttachment.php b/src/Mime/FileAttachment.php index 86b800ac..f3f306a8 100644 --- a/src/Mime/FileAttachment.php +++ b/src/Mime/FileAttachment.php @@ -6,6 +6,7 @@ use Genkgo\Mail\Header\ContentDisposition; use Genkgo\Mail\Header\ContentTransferEncoding; use Genkgo\Mail\Header\ContentType; +use Genkgo\Mail\Header\HeaderValue; use Genkgo\Mail\HeaderInterface; use Genkgo\Mail\Stream\Base64EncodedStream; use Genkgo\Mail\StreamInterface; @@ -41,6 +42,36 @@ public function __construct(string $filename, ContentType $contentType, string $ ->withHeader(new ContentTransferEncoding('base64')); } + /** + * @param string $filename + * @param string $attachmentName + * @return FileAttachment + */ + public static function fromUnknownFileType(string $filename, string $attachmentName = ''): FileAttachment + { + $fileInfo = new \finfo(FILEINFO_MIME); + $mime = $fileInfo->file($filename); + + $headerValue = HeaderValue::fromString($mime); + + try { + $charset = $headerValue->getParameter('charset')->getValue(); + if ($charset === 'binary') { + $contentType = new ContentType($headerValue->getRaw()); + } else { + $contentType = new ContentType($headerValue->getRaw(), $charset); + } + } catch (\UnexpectedValueException $e) { + $contentType = new ContentType($headerValue->getRaw()); + } + + return new self( + $filename, + $contentType, + $attachmentName + ); + } + /** * @return iterable */ diff --git a/test/Stub/minimal.pdf b/test/Stub/minimal.pdf new file mode 100644 index 00000000..1c641810 Binary files /dev/null and b/test/Stub/minimal.pdf differ diff --git a/test/Unit/Header/HeaderValueParameterTest.php b/test/Unit/Header/HeaderValueParameterTest.php new file mode 100644 index 00000000..2d6f8c8d --- /dev/null +++ b/test/Unit/Header/HeaderValueParameterTest.php @@ -0,0 +1,45 @@ +assertEquals((string)$parameter, 'charset="utf-8"'); + $this->assertEquals($parameter->getName(), 'charset'); + $this->assertEquals($parameter->getValue(), 'utf-8'); + } + + /** + * @test + */ + public function it_can_parse_an_unquoted_string() + { + $parameter = HeaderValueParameter::fromString('charset=utf-8'); + + $this->assertEquals((string)$parameter, 'charset="utf-8"'); + $this->assertEquals($parameter->getName(), 'charset'); + $this->assertEquals($parameter->getValue(), 'utf-8'); + } + + /** + * @test + */ + public function it_does_not_parse_invalid_values() + { + $this->expectException(\InvalidArgumentException::class); + HeaderValueParameter::fromString('charset,utf-8'); + } + +} \ No newline at end of file diff --git a/test/Unit/Header/HeaderValueTest.php b/test/Unit/Header/HeaderValueTest.php index b640a1ed..455c876f 100644 --- a/test/Unit/Header/HeaderValueTest.php +++ b/test/Unit/Header/HeaderValueTest.php @@ -74,4 +74,27 @@ public function it_overwrites_parameters() $this->assertEquals('value; name1="value2"', (string)$header); } + + /** + * @test + */ + public function it_can_parse_a_string() + { + $header = HeaderValue::fromString('application/pdf; charset="utf-8"'); + + $this->assertEquals('application/pdf; charset="utf-8"', (string)$header); + $this->assertEquals('charset="utf-8"', (string)$header->getParameter('charset')); + } + + /** + * @test + */ + public function it_can_parse_a_string_with_more_than_one_parameter() + { + $header = HeaderValue::fromString('application/pdf; charset="utf-8"; foo="bar"'); + + $this->assertEquals('application/pdf; charset="utf-8"; foo="bar"', (string)$header); + $this->assertEquals('charset="utf-8"', (string)$header->getParameter('charset')); + $this->assertEquals('foo="bar"', (string)$header->getParameter('foo')); + } } \ No newline at end of file diff --git a/test/Unit/Mime/FileAttachmentTest.php b/test/Unit/Mime/FileAttachmentTest.php index 08a0c722..9168bce9 100644 --- a/test/Unit/Mime/FileAttachmentTest.php +++ b/test/Unit/Mime/FileAttachmentTest.php @@ -110,4 +110,19 @@ public function it_encodes_body_with_base64() $this->assertInstanceOf(Base64EncodedStream::class, $attachment->getBody()); } + + /** + * @test + */ + public function it_is_able_to_detect_mime_type() + { + $attachment = FileAttachment::fromUnknownFileType( + __DIR__ .'/../../Stub/minimal.pdf' + ); + + $this->assertEquals( + 'application/pdf; charset="utf-8"', + (string) $attachment->getHeader('Content-Type')->getValue() + ); + } } \ No newline at end of file