query('//div[@id="messagebody"]'); $this->assertCount(1, $bodyElem, 'Message body'); return trim($bodyElem[0]->textContent); } /** * Get the subject from the document, stripped by the prefix "Subject: ", * the suffix "Open in new window", and trimmed from surrounding whitespace. */ protected function getScrubbedSubject(\DOMXPath $domxpath): string { $subjectElem = $domxpath->query('//h2[@class="subject"][1]'); $subject = preg_replace('/^\s*Subject:\s*(.*)\s*Open in new window$/', '$1', trim($subjectElem[0]->textContent)); return trim($subject); } /** * Execute run() to render the message with the given $msgId. * * This is useful to check how rcmail_action_mail_show() renders messages. * It requires a running dovecot to fetch the messages from. Messages need * to be placed as individual files in * `tests/src/emails/test@example/Mail/cur/`. */ protected function renderIframedBodyContent(array $params): \DOMXPath { return $this->runActionAndGetHtmlOutputDomxpath('get', function ($storage) use ($params) { // rcmail_action_mail_get calls rcmail_attachment_handler, which // closes one layer of output buffering, probably because index.php // always starts one and rcmail_attachment_handler wants to close // that. In order to avoid our tests being "risky" and thus marked // as problematic, we need to simulate the opened output buffer // layer. ob_start(); $action = new \rcmail_action_mail_get(); $this->assertTrue($action->checks()); $_GET = $params; $action->run(); }); } protected function renderMessage(string $msgId): \DOMXPath { return $this->runActionAndGetHtmlOutputDomxpath('preview', function ($storage) use ($msgId) { $action = new \rcmail_action_mail_show(); $this->assertTrue($action->checks()); $messagesList = $storage->list_messages(); // Find the UID of the wanted message. $messageUid = null; // TODO: find a method that scales better. foreach ($messagesList as $messageHeaders) { if ($messageHeaders->get('message-id') === "<{$msgId}>") { $messageUid = $messageHeaders->uid; break; } } if ($messageUid === null) { throw new \Exception("No message found in messages list with Message-Id '{$msgId}'"); } $_GET = ['_uid' => $messageUid]; $action->run(); }); } protected function runActionAndGetHtmlOutputDomxpath(string $urlActionParam, \Closure $callback): \DOMXPath { $imap_host = getenv('RC_CONFIG_IMAP_HOST') ?: 'tls://localhost:143'; $rcmail = \rcmail::get_instance(); // We need to overwrite the storage object, else storage_init() just // returns the cached one (which might be a StorageMock instance). $mockStorage = $rcmail->storage; $rcmail->storage = null; $rcmail->storage_init(); // Login our test user so we can fetch messages from the imap server. $rcmail->login('test-message-rendering@localhost', 'pass', $imap_host); $storage = $rcmail->get_storage(); $storage->set_options(['all_headers' => true]); // We need to set the folder, else no message can be fetched. $storage->set_folder('INBOX'); $this->initOutput(\rcmail_action::MODE_HTTP, 'mail', $urlActionParam); // TODO: Why do we need to set the skin manually? $rcmail->output->set_skin('elastic'); // Prepare and trigger the rendering. $html = ''; try { $callback($storage); } catch (ExitException $e) { // phpstan complains that the output classes of the production code // don't provide that method – which is true, but not relevant // here. // @phpstan-ignore-next-line $html = $rcmail->output->getOutput(); } // Reset the storage to the mocked one most other tests expect. $rcmail->storage = $mockStorage; // disabled_html_ns=true is a workaround for the performance issue // https://github.com/Masterminds/html5-php/issues/181 $html5 = new HTML5(['disable_html_ns' => true]); return new \DOMXPath($html5->loadHTML($html)); } protected function getSrcParams(\DOMNode $elem): array { $src = $elem->attributes->getNamedItem('data-src')->textContent; $url = parse_url($src, \PHP_URL_QUERY); parse_str($url, $params); return $params; } protected function getIframedContent(array $params): string { $domxpath_body = $this->renderIframedBodyContent($params); $bodyElem = $domxpath_body->query('//body'); $this->assertCount(1, $bodyElem, 'Message body'); return $bodyElem[0]->textContent; } protected function assertIframedContent(\DOMNode $elem, string $partId, string $content): void { $params = $this->getSrcParams($elem); $this->assertSrcUrlParams($params, $partId); $body = $this->getIframedContent($params); $this->assertSame($content, trim($body)); } protected function assertSrcUrlParams(array $params, string $partId): void { $this->assertSame('mail', $params['_task']); $this->assertSame('get', $params['_action']); $this->assertSame('INBOX', $params['_mbox']); $this->assertMatchesRegularExpression('/^\d+$/', $params['_uid']); $this->assertSame($partId, $params['_part']); } }