* * @todo see @todo of Imap::mail_compose() */ class LiveMailboxTest extends AbstractLiveMailboxTest { public const RANDOM_MAILBOX_SAMPLE_SIZE = 3; public const ISSUE_EXPECTED_ATTACHMENT_COUNT = [ 448 => 1, 391 => 2, ]; /** * @dataProvider MailBoxProvider * * @group live */ public function testGetImapStream(HiddenString $imapPath, HiddenString $login, HiddenString $password, string $attachmentsDir, string $serverEncoding = 'UTF-8'): void { [$mailbox, $remove_mailbox] = $this->getMailbox( $imapPath, $login, $password, $attachmentsDir, $serverEncoding ); /** @var Throwable|null */ $exception = null; try { $mailbox->getImapStream(); $this->assertTrue($mailbox->hasImapStream()); $mailboxes = $mailbox->getMailboxes(); \shuffle($mailboxes); $mailboxes = \array_values($mailboxes); $limit = \min(\count($mailboxes), self::RANDOM_MAILBOX_SAMPLE_SIZE); for ($i = 0; $i < $limit; ++$i) { $this->assertIsArray($mailboxes[$i]); $this->assertTrue(isset($mailboxes[$i]['shortpath'])); $this->assertIsString($mailboxes[$i]['shortpath']); $mailbox->switchMailbox($mailboxes[$i]['shortpath']); $check = $mailbox->checkMailbox(); foreach ([ 'Date', 'Driver', 'Mailbox', 'Nmsgs', 'Recent', ] as $expectedProperty) { $this->assertTrue(\property_exists($check, $expectedProperty)); } $this->assertIsString($check->Date, 'Date property of Mailbox::checkMailbox() result was not a string!'); $unix = \strtotime($check->Date); if (false === $unix && \preg_match('/[+-]\d{1,2}:?\d{2} \([^\)]+\)$/', $check->Date)) { /** @var int */ $pos = \strrpos($check->Date, '('); // Although the date property is likely RFC2822-compliant, it will not be parsed by strtotime() $unix = \strtotime(\substr($check->Date, 0, $pos)); } $this->assertIsInt($unix, 'Date property of Mailbox::checkMailbox() result was not a valid date!'); $this->assertTrue(\in_array($check->Driver, ['POP3', 'IMAP', 'NNTP', 'pop3', 'imap', 'nntp'], true), 'Driver property of Mailbox::checkMailbox() result was not of an expected value!'); $this->assertIsInt($check->Nmsgs, 'Nmsgs property of Mailbox::checkMailbox() result was not of an expected type!'); $this->assertIsInt($check->Recent, 'Recent property of Mailbox::checkMailbox() result was not of an expected type!'); $status = $mailbox->statusMailbox(); foreach ([ 'messages', 'recent', 'unseen', 'uidnext', 'uidvalidity', ] as $expectedProperty) { $this->assertTrue(\property_exists($status, $expectedProperty)); } $this->assertSame($check->Nmsgs, $mailbox->countMails(), 'Mailbox::checkMailbox()->Nmsgs did not match Mailbox::countMails()!'); } } catch (Throwable $ex) { $exception = $ex; } finally { $mailbox->switchMailbox($imapPath->getString()); $mailbox->deleteMailbox($remove_mailbox); $mailbox->disconnect(); } if (null !== $exception) { throw $exception; } } /** * @psalm-return Generator */ public function ComposeProvider(): Generator { $random_subject = 'test: '.\bin2hex(\random_bytes(16)); yield [ ['subject' => $random_subject], [ [ 'type' => TYPETEXT, 'contents.data' => 'test', ], ], ( 'Subject: '.$random_subject."\r\n". 'MIME-Version: 1.0'."\r\n". 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII'."\r\n". "\r\n". 'test'."\r\n" ), ]; $random_subject = 'barbushin/php-imap#448: dot first:'.\bin2hex(\random_bytes(16)); yield [ ['subject' => $random_subject], [ [ 'type' => TYPEAPPLICATION, 'encoding' => ENCBASE64, 'subtype' => 'octet-stream', 'description' => '.gitignore', 'disposition.type' => 'attachment', 'disposition' => ['filename' => '.gitignore'], 'type.parameters' => ['name' => '.gitignore'], 'contents.data' => \base64_encode( \file_get_contents(__DIR__.'/../../.gitignore') ), ], ], ( 'Subject: '.$random_subject."\r\n". 'MIME-Version: 1.0'."\r\n". 'Content-Type: APPLICATION/octet-stream; name=.gitignore'."\r\n". 'Content-Transfer-Encoding: BASE64'."\r\n". 'Content-Description: .gitignore'."\r\n". 'Content-Disposition: attachment; filename=.gitignore'."\r\n". "\r\n". \base64_encode( \file_get_contents(__DIR__.'/../../.gitignore') )."\r\n" ), ]; $random_subject = 'barbushin/php-imap#448: dot last: '.\bin2hex(\random_bytes(16)); yield [ ['subject' => $random_subject], [ [ 'type' => TYPEAPPLICATION, 'encoding' => ENCBASE64, 'subtype' => 'octet-stream', 'description' => 'gitignore.', 'disposition.type' => 'attachment', 'disposition' => ['filename' => 'gitignore.'], 'type.parameters' => ['name' => 'gitignore.'], 'contents.data' => \base64_encode( \file_get_contents(__DIR__.'/../../.gitignore') ), ], ], ( 'Subject: '.$random_subject."\r\n". 'MIME-Version: 1.0'."\r\n". 'Content-Type: APPLICATION/octet-stream; name=gitignore.'."\r\n". 'Content-Transfer-Encoding: BASE64'."\r\n". 'Content-Description: gitignore.'."\r\n". 'Content-Disposition: attachment; filename=gitignore.'."\r\n". "\r\n". \base64_encode( \file_get_contents(__DIR__.'/../../.gitignore') )."\r\n" ), ]; $random_subject = 'barbushin/php-imap#391: '.\bin2hex(\random_bytes(16)); $random_attachment_a = \base64_encode(\random_bytes(16)); $random_attachment_b = \base64_encode(\random_bytes(16)); yield [ ['subject' => $random_subject], [ [ 'type' => TYPEMULTIPART, ], [ 'type' => TYPETEXT, 'contents.data' => 'test', ], [ 'type' => TYPEAPPLICATION, 'encoding' => ENCBASE64, 'subtype' => 'octet-stream', 'description' => 'foo.bin', 'disposition.type' => 'attachment', 'disposition' => ['filename' => 'foo.bin'], 'type.parameters' => ['name' => 'foo.bin'], 'contents.data' => $random_attachment_a, ], [ 'type' => TYPEAPPLICATION, 'encoding' => ENCBASE64, 'subtype' => 'octet-stream', 'description' => 'foo.bin', 'disposition.type' => 'attachment', 'disposition' => ['filename' => 'foo.bin'], 'type.parameters' => ['name' => 'foo.bin'], 'contents.data' => $random_attachment_b, ], ], ( 'Subject: '.$random_subject."\r\n". 'MIME-Version: 1.0'."\r\n". 'Content-Type: MULTIPART/MIXED; BOUNDARY="{{REPLACE_BOUNDARY_HERE}}"'."\r\n". "\r\n". '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII'."\r\n". "\r\n". 'test'."\r\n". '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". 'Content-Type: APPLICATION/octet-stream; name=foo.bin'."\r\n". 'Content-Transfer-Encoding: BASE64'."\r\n". 'Content-Description: foo.bin'."\r\n". 'Content-Disposition: attachment; filename=foo.bin'."\r\n". "\r\n". $random_attachment_a."\r\n". '--{{REPLACE_BOUNDARY_HERE}}'."\r\n". 'Content-Type: APPLICATION/octet-stream; name=foo.bin'."\r\n". 'Content-Transfer-Encoding: BASE64'."\r\n". 'Content-Description: foo.bin'."\r\n". 'Content-Disposition: attachment; filename=foo.bin'."\r\n". "\r\n". $random_attachment_b."\r\n". '--{{REPLACE_BOUNDARY_HERE}}--'."\r\n" ), ]; } /** * @dataProvider ComposeProvider * * @group compose * * @psalm-param COMPOSE_ENVELOPE $envelope * @psalm-param COMPOSE_BODY $body */ public function testMailCompose(array $envelope, array $body, string $expected_result): void { $actual_result = Imap::mail_compose($envelope, $body); $expected_result = $this->ReplaceBoundaryHere( $expected_result, $actual_result ); $this->assertSame($expected_result, $actual_result); } /** * @dataProvider AppendProvider * * @group live * * @depends testAppend * * @psalm-param MAILBOX_ARGS $mailbox_args * @psalm-param COMPOSE_ENVELOPE $envelope * @psalm-param COMPOSE_BODY $body */ public function testAppendNudgesMailboxCount( array $mailbox_args, array $envelope, array $body, string $_expected_compose_result, bool $pre_compose ): void { if ($this->MaybeSkipAppendTest($envelope)) { return; } [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( $mailbox_args ); $count = $mailbox->countMails(); $message = [$envelope, $body]; if ($pre_compose) { $message = Imap::mail_compose($envelope, $body); } $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 0, $search, ( 'If a subject was found,'. ' then the message is insufficiently unique to assert that'. ' a newly-appended message was actually created.' ) ); $mailbox->appendMessageToMailbox($message); $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 1, $search, ( 'If a subject was not found, '. ' then Mailbox::appendMessageToMailbox() failed'. ' despite not throwing an exception.' ) ); $this->assertSame( $count + 1, $mailbox->countMails(), ( 'If the message count did not increase'. ' then either the message was not appended,'. ' or a mesage was removed while the test was running.' ) ); $mailbox->deleteMail($search[0]); $mailbox->expungeDeletedMails(); $mailbox->switchMailbox($path->getString()); $mailbox->deleteMailbox($remove_mailbox); $this->assertCount( 0, $mailbox->searchMailbox($search_criteria), ( 'If a subject was found,'. ' then the message is was not expunged as requested.' ) ); } /** * @dataProvider AppendProvider * * @group live * * @depends testAppend * * @psalm-param MAILBOX_ARGS $mailbox_args * @psalm-param COMPOSE_ENVELOPE $envelope * @psalm-param COMPOSE_BODY $body */ public function testAppendSingleSearchMatchesSort( array $mailbox_args, array $envelope, array $body, string $_expected_compose_result, bool $pre_compose ): void { if ($this->MaybeSkipAppendTest($envelope)) { return; } [$search_criteria] = $this->SubjectSearchCriteriaAndSubject($envelope); [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( $mailbox_args ); $message = [$envelope, $body]; if ($pre_compose) { $message = Imap::mail_compose($envelope, $body); } $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 0, $search, ( 'If a subject was found,'. ' then the message is insufficiently unique to assert that'. ' a newly-appended message was actually created.' ) ); $mailbox->appendMessageToMailbox($message); $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 1, $search, ( 'If a subject was not found, '. ' then Mailbox::appendMessageToMailbox() failed'. ' despite not throwing an exception.' ) ); $this->assertSame( $search, $mailbox->sortMails(SORTARRIVAL, true, $search_criteria) ); $this->assertSame( $search, $mailbox->sortMails(SORTARRIVAL, false, $search_criteria) ); $this->assertSame( $search, $mailbox->sortMails(SORTARRIVAL, false, $search_criteria, 'UTF-8') ); $this->assertTrue(\in_array( $search[0], $mailbox->sortMails(SORTARRIVAL, false, null), true )); $mailbox->deleteMail($search[0]); $mailbox->expungeDeletedMails(); $mailbox->switchMailbox($path->getString()); $mailbox->deleteMailbox($remove_mailbox); $this->assertCount( 0, $mailbox->searchMailbox($search_criteria), ( 'If a subject was found,'. ' then the message is was not expunged as requested.' ) ); } /** * @dataProvider AppendProvider * * @group live * * @depends testAppend * * @psalm-param MAILBOX_ARGS $mailbox_args * @psalm-param COMPOSE_ENVELOPE $envelope * @psalm-param COMPOSE_BODY $body */ public function testAppendRetrievalMatchesExpected( array $mailbox_args, array $envelope, array $body, string $expected_compose_result, bool $pre_compose ): void { if ($this->MaybeSkipAppendTest($envelope)) { return; } [$search_criteria, $search_subject] = $this->SubjectSearchCriteriaAndSubject($envelope); [$mailbox, $remove_mailbox, $path] = $this->getMailboxFromArgs( $mailbox_args ); $message = [$envelope, $body]; if ($pre_compose) { $message = Imap::mail_compose($envelope, $body); } $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 0, $search, ( 'If a subject was found,'. ' then the message is insufficiently unique to assert that'. ' a newly-appended message was actually created.' ) ); $mailbox->appendMessageToMailbox($message); $search = $mailbox->searchMailbox($search_criteria); $this->assertCount( 1, $search, ( 'If a subject was not found, '. ' then Mailbox::appendMessageToMailbox() failed'. ' despite not throwing an exception.' ) ); $actual_result = $mailbox->getMailMboxFormat($search[0]); $this->assertSame( $this->ReplaceBoundaryHere( $expected_compose_result, $actual_result ), $actual_result ); $actual_result = $mailbox->getRawMail($search[0]); $this->assertSame( $this->ReplaceBoundaryHere( $expected_compose_result, $actual_result ), $actual_result ); $mail = $mailbox->getMail($search[0], false); $this->assertSame( $search_subject, $mail->subject, ( 'If a retrieved mail did not have a matching subject'. ' despite being found via search,'. ' then something has gone wrong.' ) ); $info = $mailbox->getMailsInfo($search); $this->assertCount(1, $info); $this->assertSame( $search_subject, $info[0]->subject, ( 'If a retrieved mail did not have a matching subject'. ' despite being found via search,'. ' then something has gone wrong.' ) ); if (1 === \preg_match( '/^barbushin\/php-imap#(448|391):/', $envelope['subject'] ?? '', $matches )) { $this->assertTrue($mail->hasAttachments()); $attachments = $mail->getAttachments(); $this->assertCount(self::ISSUE_EXPECTED_ATTACHMENT_COUNT[ (int) $matches[1]], $attachments ); if ('448' === $matches[1]) { $this->assertSame( \file_get_contents(__DIR__.'/../../.gitignore'), \current($attachments)->getContents() ); } } $mailbox->deleteMail($search[0]); $mailbox->expungeDeletedMails(); $mailbox->switchMailbox($path->getString()); $mailbox->deleteMailbox($remove_mailbox); $this->assertCount( 0, $mailbox->searchMailbox($search_criteria), ( 'If a subject was found,'. ' then the message is was not expunged as requested.' ) ); } /** * @param string $expected_result * @param string $actual_result * * @return string * * @psalm-pure */ protected function ReplaceBoundaryHere( $expected_result, $actual_result ) { if ( 1 === \preg_match('/{{REPLACE_BOUNDARY_HERE}}/', $expected_result) && 1 === \preg_match( '/Content-Type: MULTIPART\/MIXED; BOUNDARY="([^"]+)"/', $actual_result, $matches ) ) { $expected_result = \str_replace( '{{REPLACE_BOUNDARY_HERE}}', $matches[1], $expected_result ); } return $expected_result; } }