diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85bd6966..576d5895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: # Keys: # - experimental: Whether the build is "allowed to fail". matrix: - php: ['7.3', '7.4', '8.0', '8.1'] + php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] experimental: [false] include: - - php: '8.2' + - php: '8.4' experimental: true name: "PHP: ${{ matrix.php }}" @@ -41,12 +41,12 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-composer-dependencies - - name: "Install Composer dependencies (PHP < 8.2)" - if: ${{ matrix.php < '8.2' }} + - name: "Install Composer dependencies (PHP < 8.4)" + if: ${{ matrix.php < '8.4' }} uses: "ramsey/composer-install@v1" - - name: "Install Composer dependencies (PHP 8.2)" - if: ${{ matrix.php >= '8.2' }} + - name: "Install Composer dependencies (PHP 8.4)" + if: ${{ matrix.php >= '8.4' }} uses: "ramsey/composer-install@v1" with: composer-options: --ignore-platform-reqs diff --git a/ChangeLog b/ChangeLog index aa17400d..e688d379 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +1.10.0 - [RELEASEDATE] +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Fixed #96: Add new option 'parseMultipartMixedTextAttachmentsAsFiles' to + ezcMailParserOptions to retrieve only text attachments, that are sub-parts of + multipart/mixed parts an ezcMailFile object, instead of the default ezcMailText + object. + 1.9.7 - Tuesday 20 August 2024 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/options/parser_options.php b/src/options/parser_options.php index ac0b6816..3d0de157 100644 --- a/src/options/parser_options.php +++ b/src/options/parser_options.php @@ -33,6 +33,7 @@ * $options->mailClass = 'myCustomMailClass'; // extends ezcMail * $options->fileClass = 'myCustomFileClass'; // extends ezcMailFile * $options->parseTextAttachmentsAsFiles = true; // to get the text attachments in ezcMailFile objects + * $options->parseMultipartMixedTextAttachmentsAsFiles = true; // to get all text attachments that are sub-parts of multipart/mixed parts as ezcMailFile objects * * $parser = new ezcMailParser( $options ); * @@ -43,6 +44,7 @@ * $parser->options->mailClass = 'myCustomMailClass'; // extends ezcMail * $parser->options->fileClass = 'myCustomFileClass'; // extends ezcMailFile * $parser->options->parseTextAttachmentsAsFiles = true; + * $parser->options->parseMultipartMixedTextAttachmentsAsFiles = true; * * * @property string $mailClass @@ -54,7 +56,11 @@ * by the parser to handle file attachments. The default value is * ezcMailFile. * @property string $parseTextAttachmentsAsFiles - * Specifies whether to parse the text attachments in an ezcMailTextPart + * Specifies whether to parse the text attachments in an ezcMailText part + * (default) or in an ezcMailFile (by setting the option to true). + * @property string $parseMultipartMixedTextAttachmentsAsFiles + * Specifies whether to parse the text attachments that are sub-parts + * of a multipart/mixed part as an ezcMailText part * (default) or in an ezcMailFile (by setting the option to true). * @package Mail * @version //autogen// @@ -74,7 +80,8 @@ public function __construct( array $options = array() ) { $this->mailClass = 'ezcMail'; // default value for mail class is 'ezcMail' $this->fileClass = 'ezcMailFile'; // default value for file attachment class is 'ezcMailFile' - $this->parseTextAttachmentsAsFiles = false; // default is to parse text attachments in ezcMailTextPart objects + $this->parseTextAttachmentsAsFiles = false; // default is to parse text attachments in ezcMailText objects + $this->parseMultipartMixedTextAttachmentsAsFiles = false; // default is to parse multiple/mixed text attachments in ezcMailText objects parent::__construct( $options ); } @@ -140,6 +147,16 @@ public function __set( $propertyName, $propertyValue ) ezcMailPartParser::$parseTextAttachmentsAsFiles = $propertyValue; break; + + case 'parseMultipartMixedTextAttachmentsAsFiles': + if ( !is_bool( $propertyValue ) ) + { + throw new ezcBaseValueException( $propertyName, $propertyValue, 'bool' ); + } + $this->properties[$propertyName] = $propertyValue; + ezcMailPartParser::$parseMultipartMixedTextAttachmentsAsFiles = $propertyValue; + break; + default: throw new ezcBasePropertyNotFoundException( $propertyName ); } diff --git a/src/parser/interfaces/part_parser.php b/src/parser/interfaces/part_parser.php index a81378b3..89fd4e96 100644 --- a/src/parser/interfaces/part_parser.php +++ b/src/parser/interfaces/part_parser.php @@ -66,7 +66,7 @@ abstract class ezcMailPartParser 'sender', 'subject', 'sender', 'to' ); /** - * The default is to parse text attachments into ezcMailTextPart objects. + * The default is to parse text attachments into ezcMailText objects. * * Setting this to true before calling the parser will parse text attachments * into ezcMailFile objects. Use the parser options for this: @@ -81,6 +81,23 @@ abstract class ezcMailPartParser */ public static $parseTextAttachmentsAsFiles = false; + /** + * The default is to parse text attachments into ezcMailText objects. + * + * Setting this to true before calling the parser will parse text attachments + * that are sub-parts of Multipart/Mixed parts into ezcMailFail objects. + * Use the parser options for this: + * + * + * $parser = new ezcMailParser(); + * $parser->options->parseMultipartMixedTextAttachmentsAsFiles = true; + * // call $parser->parseMail( $set ); + * + * + * @var bool + */ + public static $parseMultipartMixedTextAttachmentsAsFiles = false; + /** * The name of the last header parsed. * @@ -118,7 +135,7 @@ abstract public function finish(); * @param ezcMailHeadersHolder $headers * @return ezcMailPartParser */ - static public function createPartParserForHeaders( ezcMailHeadersHolder $headers ) + static public function createPartParserForHeaders( ezcMailHeadersHolder $headers, ?ezcMailPartParser $parentParser = null ) { // default as specified by RFC2045 - #5.2 $mainType = 'text'; @@ -168,7 +185,18 @@ static public function createPartParserForHeaders( ezcMailHeadersHolder $headers break; case 'text': - if ( ezcMailPartParser::$parseTextAttachmentsAsFiles === true ) + if ( ezcMailPartParser::$parseMultipartMixedTextAttachmentsAsFiles === true ) + { + if ( $parentParser instanceof ezcMailMultipartMixedParser ) + { + $bodyParser = new ezcMailFileParser( $mainType, $subType, $headers ); + } + else + { + $bodyParser = new ezcMailTextParser( $subType, $headers ); + } + } + else if ( ezcMailPartParser::$parseTextAttachmentsAsFiles === true ) { $bodyParser = new ezcMailFileParser( $mainType, $subType, $headers ); } diff --git a/src/parser/parser.php b/src/parser/parser.php index fef2a83e..1604d5de 100644 --- a/src/parser/parser.php +++ b/src/parser/parser.php @@ -83,6 +83,15 @@ * $parser->options->parseTextAttachmentsAsFiles = true; * * + * In some cases, you might only want to create {@link ezcMailFile} objects + * for text attachments that are sub-parts of multipart/mixed parts. + * In that case, you can use the parseMultipartMixedTextAttachmentsAsFiles + * option. Example: + * + * $parser = new ezcMailParser(); + * $parser->options->parseMultipartMixedTextAttachmentsAsFiles = true; + * + * * @property ezcMailParserOptions $options * Holds the options you can set to the mail parser. * diff --git a/src/parser/parts/multipart_parser.php b/src/parser/parts/multipart_parser.php index d31fb802..52ecbd50 100644 --- a/src/parser/parts/multipart_parser.php +++ b/src/parser/parts/multipart_parser.php @@ -181,7 +181,7 @@ public function parseBody( $origLine ) { if ( $this->parserState == self::PARSE_STATE_HEADERS && $line == '' ) { - $this->currentPartParser = self::createPartParserForHeaders( $this->currentPartHeaders ); + $this->currentPartParser = self::createPartParserForHeaders( $this->currentPartHeaders, $this ); $this->parserState = self::PARSE_STATE_BODY; } else if ( $this->parserState == self::PARSE_STATE_HEADERS ) diff --git a/src/parser/parts/rfc822_parser.php b/src/parser/parts/rfc822_parser.php index eed7c413..0a74c30d 100644 --- a/src/parser/parts/rfc822_parser.php +++ b/src/parser/parts/rfc822_parser.php @@ -107,7 +107,7 @@ public function parseBody( $origLine ) } // get the correct body type - $this->bodyParser = self::createPartParserForHeaders( $headers ); + $this->bodyParser = self::createPartParserForHeaders( $headers, $this ); } else if ( $this->parserState == self::PARSE_STATE_HEADERS ) { diff --git a/tests/parser/data/various/test-html-text-and-text-attachment b/tests/parser/data/various/test-html-text-and-text-attachment new file mode 100644 index 00000000..581e8159 --- /dev/null +++ b/tests/parser/data/various/test-html-text-and-text-attachment @@ -0,0 +1,64 @@ +MIME-Version: 1.0 +Date: Tue, 27 Aug 2024 09:28:55 +1000 +Message-ID: +Subject: test txt attachment +From: Sam Lee +To: samlee001@example.com +Content-Type: multipart/mixed; boundary="0000000000000f1e9806209e7d06" + +--0000000000000f1e9806209e7d06 +Content-Type: multipart/alternative; boundary="0000000000000f1e9606209e7d04" + +--0000000000000f1e9606209e7d04 +Content-Type: text/plain; charset="UTF-8" + +test + +-- +Sam Lee + + +Big Business + +He/Him/His + +--0000000000000f1e9606209e7d04 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + +--0000000000000f1e9606209e7d04-- +--0000000000000f1e9806209e7d06 +Content-Type: text/plain; charset="UTF-8"; name="2_load_xss.html.txt" +Content-Disposition: attachment; filename="2_load_xss.html.txt" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_m0bmqmih2 +Content-ID: + +77u/PGh0bWw+DQo8Zm9ybSBlbmN0eXBlPSJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29k +ZWQiIG1ldGhvZD0iUE9TVCIgYWN0aW9uPSJodHRwOi8vbG9jYWxob3N0L3dwLWFkbWluL2FkbWlu +LnBocD9wYWdlPUNpdmlDUk0mcT1jaXZpY3JtJTJGYWRtaW4lMkZja2VkaXRvciI+DQo8aW5wdXQg +dHlwZT0idGV4dCIgdmFsdWU9Im1vb25vIiBuYW1lPSJjb25maWdfc2tpbiI+IDxicj4NCjxpbnB1 +dCB0eXBlPSJ0ZXh0IiB2YWx1ZT0iQ0tFRElUT1IuZWRpdG9yQ29uZmlnID0gZnVuY3Rpb24oIGNv +bmZpZyApIHt9OyIgbmFtZT0iY29uZmlnIj4gPGJyPg0KPGlucHV0IHR5cGU9InRleHQiIHZhbHVl +PSIiIG5hbWU9ImNvbmZpZ19leHRyYVBsdWdpbnMiPiA8YnI+DQo8aW5wdXQgdHlwZT0idGV4dCIg +dmFsdWU9Ii4uLy4uLy4uLy4uLy4uL3VwbG9hZHMvY2l2aWNybS9wZXJzaXN0L2NybS1ja2VkaXRv +ci14c3MuanMiIG5hbWU9ImNvbmZpZ19jdXN0b21Db25maWciPiA8YnI+DQo8aW5wdXQgdHlwZT0i +c3VibWl0IiB2YWx1ZT0iUlVOIFBPQyI+DQo8L2Zvcm0+DQo8L2h0bWw+ +--0000000000000f1e9806209e7d06 +Content-Type: application/msword; name="form_03.doc" +Content-Disposition: attachment; filename="form_03.doc" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_m0bmqhqx1 +Content-ID: + + +--0000000000000f1e9806209e7d06 +Content-Type: application/pdf; name="2932_1 Ward Grouped Mayor-Lismore.pdf" +Content-Disposition: attachment; filename="2932_1 Ward Grouped Mayor-Lismore.pdf" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_m0bmqhpv0 +Content-ID: + + +--0000000000000f1e9806209e7d06-- diff --git a/tests/parser/parser_test.php b/tests/parser/parser_test.php index 8ef979be..8c324fae 100644 --- a/tests/parser/parser_test.php +++ b/tests/parser/parser_test.php @@ -1863,5 +1863,117 @@ public function testSpaceBeforeFileName() } } + public function testVarious14a() + { + $parser = new ezcMailParser(); + $parser->options->parseTextAttachmentsAsFiles = false; + + $set = new SingleFileSet( 'various/test-html-text-and-text-attachment' ); + $mail = $parser->parseMail( $set ); + $this->assertEquals( 1, count( $mail ) ); + $mail = $mail[0]; + $parts = $mail->body->getParts(); + + $this->assertEquals( 4, count( $parts ) ); + $this->assertEquals( 'ezcMailMultipartAlternative', get_class( $parts[0] ) ); + + $this->assertEquals( 'ezcMailText', get_class( $parts[1] ) ); + $this->assertEquals( 'plain', $parts[1]->subType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[2] ) ); + $this->assertEquals( 'form_03.doc', basename( $parts[2]->fileName ) ); + $this->assertEquals( 'application', $parts[2]->contentType ); + $this->assertEquals( 'msword', $parts[2]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[3] ) ); + $this->assertEquals( '2932_1 Ward Grouped Mayor-Lismore.pdf', basename( $parts[3]->fileName ) ); + $this->assertEquals( 'application', $parts[3]->contentType ); + $this->assertEquals( 'pdf', $parts[3]->mimeType ); + + $alternativeParts = $parts[0]->getParts(); + $this->assertEquals( 2, count( $alternativeParts ) ); + $this->assertEquals( 'ezcMailText', get_class( $alternativeParts[0] ) ); + $this->assertEquals( 'plain', $alternativeParts[0]->subType ); + $this->assertEquals( 'ezcMailText', get_class( $alternativeParts[1] ) ); + $this->assertEquals( 'html', $alternativeParts[1]->subType ); + } + + public function testVarious14b() + { + $parser = new ezcMailParser(); + $parser->options->parseTextAttachmentsAsFiles = true; + + $set = new SingleFileSet( 'various/test-html-text-and-text-attachment' ); + $mail = $parser->parseMail( $set ); + $this->assertEquals( 1, count( $mail ) ); + $mail = $mail[0]; + $parts = $mail->body->getParts(); + + $this->assertEquals( 4, count( $parts ) ); + $this->assertEquals( 'ezcMailMultipartAlternative', get_class( $parts[0] ) ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[1] ) ); + $this->assertEquals( '2_load_xss.html.txt', basename( $parts[1]->fileName ) ); + $this->assertEquals( 'text', $parts[1]->contentType ); + $this->assertEquals( 'plain', $parts[1]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[2] ) ); + $this->assertEquals( 'form_03.doc', basename( $parts[2]->fileName ) ); + $this->assertEquals( 'application', $parts[2]->contentType ); + $this->assertEquals( 'msword', $parts[2]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[3] ) ); + $this->assertEquals( '2932_1 Ward Grouped Mayor-Lismore.pdf', basename( $parts[3]->fileName ) ); + $this->assertEquals( 'application', $parts[3]->contentType ); + $this->assertEquals( 'pdf', $parts[3]->mimeType ); + + $alternativeParts = $parts[0]->getParts(); + $this->assertEquals( 2, count( $alternativeParts ) ); + $this->assertEquals( 'ezcMailFile', get_class( $alternativeParts[0] ) ); + $this->assertEquals( 'text', $alternativeParts[0]->contentType ); + $this->assertEquals( 'plain', $alternativeParts[0]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $alternativeParts[1] ) ); + $this->assertEquals( 'application', $alternativeParts[1]->contentType ); + $this->assertEquals( 'html', $alternativeParts[1]->mimeType ); + } + + + public function testVarious14c() + { + $parser = new ezcMailParser(); + $parser->options->parseMultipartMixedTextAttachmentsAsFiles = true; + + $set = new SingleFileSet( 'various/test-html-text-and-text-attachment' ); + $mail = $parser->parseMail( $set ); + $this->assertEquals( 1, count( $mail ) ); + $mail = $mail[0]; + $parts = $mail->body->getParts(); + + $this->assertEquals( 4, count( $parts ) ); + $this->assertEquals( 'ezcMailMultipartAlternative', get_class( $parts[0] ) ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[1] ) ); + $this->assertEquals( '2_load_xss.html.txt', basename( $parts[1]->fileName ) ); + $this->assertEquals( 'text', $parts[1]->contentType ); + $this->assertEquals( 'plain', $parts[1]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[2] ) ); + $this->assertEquals( 'form_03.doc', basename( $parts[2]->fileName ) ); + $this->assertEquals( 'application', $parts[2]->contentType ); + $this->assertEquals( 'msword', $parts[2]->mimeType ); + + $this->assertEquals( 'ezcMailFile', get_class( $parts[3] ) ); + $this->assertEquals( '2932_1 Ward Grouped Mayor-Lismore.pdf', basename( $parts[3]->fileName ) ); + $this->assertEquals( 'application', $parts[3]->contentType ); + $this->assertEquals( 'pdf', $parts[3]->mimeType ); + + $alternativeParts = $parts[0]->getParts(); + $this->assertEquals( 2, count( $alternativeParts ) ); + $this->assertEquals( 'ezcMailText', get_class( $alternativeParts[0] ) ); + $this->assertEquals( 'plain', $alternativeParts[0]->subType ); + $this->assertEquals( 'ezcMailText', get_class( $alternativeParts[1] ) ); + $this->assertEquals( 'html', $alternativeParts[1]->subType ); + } } ?>