Files
fvsjs/vendor/php-imap/php-imap/tests/unit/LiveMailboxTest.php
2023-06-16 13:08:22 +02:00

679 lines
21 KiB
PHP

<?php
/**
* Live Mailbox - PHPUnit tests.
*
* Runs tests on a live mailbox
*
* @author BAPCLTD-Marv
*/
declare(strict_types=1);
namespace PhpImap;
use function date;
use const ENCBASE64;
use Generator;
use ParagonIE\HiddenString\HiddenString;
use const SORTARRIVAL;
use Throwable;
use const TYPEAPPLICATION;
use const TYPEMULTIPART;
use const TYPETEXT;
/**
* @psalm-type MAILBOX_ARGS = array{
* 0:HiddenString,
* 1:HiddenString,
* 2:HiddenString,
* 3:string,
* 4?:string
* }
* @psalm-type COMPOSE_ENVELOPE = array{
* subject?:string
* }
* @psalm-type COMPOSE_BODY = list<array{
* type?:int,
* encoding?:int,
* charset?:string,
* subtype?:string,
* description?:string,
* disposition?:array{filename:string}
* }>
*
* @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<int, array{0: array{subject: string}, 1: array{0: array{type: 0|1|3, 'contents.data'?: string, encoding?: 3, subtype?: 'octet-stream', description?: '.gitignore'|'gitignore.', 'disposition.type'?: 'attachment', disposition?: array{filename: '.gitignore'|'gitignore.'}, 'type.parameters'?: array{name: '.gitignore'|'gitignore.'}}, 1?: array{type: 0, 'contents.data': 'test'}, 2?: array{type: 3, encoding: 3, subtype: 'octet-stream', description: 'foo.bin', 'disposition.type': 'attachment', disposition: array{filename: 'foo.bin'}, 'type.parameters': array{name: 'foo.bin'}, 'contents.data': string}, 3?: array{type: 3, encoding: 3, subtype: 'octet-stream', description: 'foo.bin', 'disposition.type': 'attachment', disposition: array{filename: 'foo.bin'}, 'type.parameters': array{name: 'foo.bin'}, 'contents.data': string}}, 2: string}, mixed, void>
*/
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;
}
}