Skip to content
This repository has been archived by the owner on Sep 19, 2023. It is now read-only.

Commit

Permalink
Merge pull request #20 from xima-media/release-11.1.0
Browse files Browse the repository at this point in the history
release 11.1.0
  • Loading branch information
maikschneider authored Feb 12, 2023
2 parents 435b448 + f98cd3e commit b35334b
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 8,972 deletions.
2 changes: 1 addition & 1 deletion Classes/Domain/Model/Dto/JsonDateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class JsonDateTime extends \DateTime implements \JsonSerializable
{
public function jsonSerialize()
public function jsonSerialize(): string
{
return $this->format('c');
}
Expand Down
12 changes: 12 additions & 0 deletions Classes/Domain/Model/Dto/MailAttachment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Xima\XmMailCatcher\Domain\Model\Dto;

class MailAttachment
{
public string $filename = '';

public int $filesize = 0;

public string $publicPath = '';
}
3 changes: 3 additions & 0 deletions Classes/Domain/Model/Dto/MailMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class MailMessage

public string $bodyHtml = '';

/** @var MailAttachment[] */
public array $attachments = [];

public function getFileName(): string
{
$name = hash('md5', $this->messageId);
Expand Down
125 changes: 74 additions & 51 deletions Classes/Utility/LogParserUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

namespace Xima\XmMailCatcher\Utility;

use PhpMimeMailParser\Parser;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Xima\XmMailCatcher\Domain\Model\Dto\JsonDateTime;
use Xima\XmMailCatcher\Domain\Model\Dto\MailAttachment;
use Xima\XmMailCatcher\Domain\Model\Dto\MailMessage;

class LogParserUtility
Expand All @@ -32,8 +36,8 @@ protected function loadLogFile(): void
}

/**
* @throws \TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException
* @throws \TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException
* @throws ExtensionConfigurationExtensionNotConfiguredException
* @throws ExtensionConfigurationPathDoesNotExistException
*/
protected function emptyLogFile(): void
{
Expand Down Expand Up @@ -61,26 +65,31 @@ protected function extractMessages(): void
$boundaries
);

// decode whole file
$this->fileContent = quoted_printable_decode($this->fileContent);

if (!isset($boundaries[1])) {
return;
}

// decode whole file
$this->fileContent = quoted_printable_decode($this->fileContent);

foreach ($boundaries[1] as $boundary) {
$separator = '--' . $boundary . '--';
$messageParts = explode($separator, $this->fileContent);

if (!str_contains($messageParts[0], 'boundary=')) {
continue;
}

$messageString = $messageParts[0];
$this->fileContent = $messageParts[1];
$this->fileContent = $messageParts[1] ?? '';
$this->messages[] = self::convertToDto((string)$messageString);
}
}

protected function writeMessagesToFile(): void
{
foreach ($this->messages as $message) {
$fileContent = (string)json_encode($message);
$fileContent = (string)json_encode($message, JSON_THROW_ON_ERROR);
$fileName = $message->getFileName();
$filePath = self::getTempPath() . $fileName;
GeneralUtility::writeFileToTypo3tempDir($filePath, $fileContent);
Expand All @@ -89,73 +98,87 @@ protected function writeMessagesToFile(): void

protected static function convertToDto(string $msg): MailMessage
{
$parser = new Parser();
$parser->setText($msg);
$dto = new MailMessage();

preg_match('/(?:^From:\s)(.*)(?:\s\<)/m', $msg, $fromName);
if (isset($fromName[1])) {
$dto->fromName = $fromName[1];
$fromAddresses = $parser->getAddresses('from');
if (isset($fromAddresses[0])) {
$dto->fromName = $fromAddresses[0]['display'] ?? '';
$dto->from = $fromAddresses[0]['address'] ?? '';
}

preg_match('/(?:(?:^From:\s)(?:.*\<)(.*)(?:\>\n))|(?:(?:^From:\s)(.*)(?:\r\n))/m', $msg, $from);
if (isset($from[1])) {
$dto->from = array_values(array_filter($from))[1];
$toAddresses = $parser->getAddresses('to');
if (isset($toAddresses[0])) {
$dto->toName = $toAddresses[0]['display'] ?? '';
$dto->to = $toAddresses[0]['address'] ?? '';
}

preg_match('/(?:^To:\s)(.*)(?:\s\<)/m', $msg, $toName);
if (isset($toName[1])) {
$dto->toName = $toName[1];
$headers = $parser->getHeaders();
$dto->subject = $headers['subject'] ?? '';
$dto->messageId = md5($headers['message-id'] ?? '');
try {
$dto->date = new JsonDateTime($headers['date']);
} catch (\Exception $e) {
}

preg_match('/(?:(?:^To:\s)(?:.*\<)(.*)(?:\>\n))|(?:(?:^To:\s)(.*)(?:\r\n))/m', $msg, $to);
if (isset($to[1])) {
$dto->to = array_values(array_filter($to))[1];
}
$dto->bodyPlain = @mb_convert_encoding($parser->getMessageBody('text'), 'UTF-8', 'auto');
$dto->bodyHtml = @mb_convert_encoding($parser->getMessageBody('html'), 'UTF-8', 'auto');

preg_match('/(?:^Subject:\s)(.*)(?:\r\n)/m', $msg, $subject);
if (isset($subject[1])) {
$dto->subject = $subject[1];
$folder = self::getTempPath() . $dto->messageId;
if (!file_exists($folder)) {
mkdir($folder);
}

preg_match('/(?:^Message-ID:\s\<)(.*)(?:\>\r\n)/m', $msg, $messageId);
if (isset($messageId[1])) {
$dto->messageId = $messageId[1];
}
$attachments = $parser->getAttachments();

preg_match('/(?:^Date:\s)(.*)(?:\r\n)/m', $msg, $date);
if (isset($date[1])) {
try {
$date = new JsonDateTime($date[1]);
$dto->date = $date;
} catch (\Exception $e) {
}
$folder = self::getTempPath() . $dto->messageId;
if (count($attachments) && !file_exists($folder)) {
mkdir($folder);
}

preg_match('/(?:boundary\=)(.*)(?:\r\n)/m', $msg, $boundary);
if (!isset($boundary[1])) {
return $dto;
}
foreach ($attachments as $attachment) {
$attachmentDto = new MailAttachment();

$messageParts = explode('--' . $boundary[1], $msg);
foreach ($messageParts as $part) {
if (strpos($part, 'Content-Type: text/plain')) {
$dto->bodyPlain = self::removeFirstThreeLines($part);
}
if (strpos($part, 'Content-Type: text/html')) {
$dto->bodyHtml = self::removeFirstThreeLines($part);
// get filename from content disposition
$filename = $attachment->getFilename();
if (str_starts_with($filename, 'noname')) {
$headers = $attachment->getHeaders();
$disposition = $headers['content-disposition'] ?? '';
preg_match('/(?:; filename )(.+)/', $disposition, $filenameParts);
$filename = $filenameParts[1];
}
$attachmentDto->filename = $filename;

// calculate public path
$fullFilePath = $attachment->save($folder, Parser::ATTACHMENT_RANDOM_FILENAME);
$publicPath = str_replace(Environment::getPublicPath(), '', $fullFilePath);
$attachmentDto->publicPath = $publicPath;

// get file size
$fileSize = filesize($fullFilePath) ?: 0;
$attachmentDto->filesize = $fileSize;

$dto->attachments[] = $attachmentDto;
}

return $dto;
}

public static function removeFirstThreeLines(string $string): string
public static function getPublicPath(): string
{
return implode(PHP_EOL, array_slice(explode(PHP_EOL, $string), 4));
return '/typo3temp/xm_mail_catcher/';
}

public static function getTempPath(): string
{
return Environment::getPublicPath() . '/typo3temp/xm_mail_catcher/';
$tempPath = Environment::getPublicPath() . self::getPublicPath();

if (!is_dir($tempPath)) {
mkdir($tempPath);
}

return $tempPath;
}

public function run(): void
Expand All @@ -168,9 +191,9 @@ public function run(): void

public function loadMessages(): void
{
$messageFiles = array_filter((array)scandir(self::getTempPath()), function ($filename) {
$messageFiles = array_reverse(array_filter((array)scandir(self::getTempPath()), function ($filename) {
return strpos((string)$filename, '.json');
});
}));

$this->messages = [];

Expand All @@ -182,7 +205,7 @@ public function loadMessages(): void
}

/**
* @return \Xima\XmMailCatcher\Domain\Model\Dto\MailMessage[]
* @return MailMessage[]
*/
public function getMessages(): array
{
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This TYPO3 extension adds a module to view emails that were printed to file.

**Requirements**: Python & 1 free Port
**Requirements**: PHP8 & PHP [Mailparse extension](https://www.php.net/manual/en/book.mailparse.php)

![backend_module](Documentation/example_backend_module.jpg)

Expand All @@ -17,8 +17,8 @@ composer require xima/xm-mail-catcher
To prevent TYPO3 from sending emails, change the transport to `mbox` ([Mail-API](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Mail/Index.html)). This way TYPO3 writes the outgoing emails to a log file that you can specify via `transport_mbox_file`. The path musst be absolute.

```
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'mbox'
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file'] = \TYPO3\CMS\Core\Core\Environment::getProjectPath() . '/var/log/mail.log'
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'mbox';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_mbox_file'] = \TYPO3\CMS\Core\Core\Environment::getProjectPath() . '/var/log/mail.log';
```

In the configuration of this extension, adjust the path to the one, you just selected. This path musst be relative.
Expand Down
83 changes: 56 additions & 27 deletions Resources/Private/Templates/Backend/Index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ <h1>Mail Log</h1>
<div class="row justify-content-md-center">
<div class="col">
<ul class="list-group">
<li class="list-group-item">Inbox (<span class="message-count">{mails->f:count()}</span>)
<li class="list-group-item">Inbox (
<span class="message-count">{mails->f:count()}</span>
)
</li>
</ul>

Expand All @@ -33,19 +35,23 @@ <h1>Mail Log</h1>
<div class="panel-heading" id="h{i.cycle}">
<div class="form-irre-header">
<button class="form-irre-header-cell form-irre-header-button" aria-expanded="false">
<div class="form-irre-header-body"
data-bs-toggle="collapse"
data-bs-target="#m{i.cycle}">
<strong>{mail.displayFromAddress}</strong><br/>
<div class="form-irre-header-body" data-bs-toggle="collapse" data-bs-target="#m{i.cycle}">
<strong>{mail.displayFromAddress}</strong>
<f:if condition="{mail.attachments->f:count()}">
<svg viewBox="0 0 82 92" width="12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="m75.7 6.75-1-1c-7.34-7.67-19.34-7.67-27 0l-40 40.67C3.02 51.42.02 58.08.02 65.08a24.14 24.14 0 0 0 7.33 18.67l1.33 1.33c4.34 4.34 10.34 6.34 16.34 6.34 8 0 16.33-3.34 21.66-9l17-17a3.22 3.22 0 0 0 0-4.67 3.22 3.22 0 0 0-4.66 0l-17 17c-7.34 7.33-21.34 10-28.67 2.67l-1.67-1.34c-3.66-3.66-5.33-8.33-5.33-13.66 0-5.34 2.33-10.34 5.67-14l40-40.67c5-5 12.66-5 17.66 0l1 1c5 5 5 12.67.34 17.67l-.34.33-.33.33L31.7 69.42c-1.33 1.66-3.33 2.33-5 2.66a6.58 6.58 0 0 1-4.66-2c-1.34-1.33-2-3-2-5s1-3.66 2.33-5.33L49.7 32.08a3.22 3.22 0 0 0 0-4.66 3.22 3.22 0 0 0-4.66 0L17.69 55.08c-2.66 2.67-4.33 6-4.33 9.67-.33 3.67 1 7.33 3.67 10a11.9 11.9 0 0 0 9 4h.66a15.16 15.16 0 0 0 9.34-4.33l39-39.67.33-.33s.33 0 .33-.34c7.67-7.66 7.34-19.66 0-27.33Z"
fill="#000"/>
</svg>
</f:if>
<br/>
<span>{mail.subject}</span>
</div>
</button>
<div class="form-irre-header-cell form-irre-header-control">
<div class="btn-group btn-group-sm" role="group">
<button type="button"
class="btn btn-default"
title="Delete mail"
data-delete>
<button type="button" class="btn btn-default" title="Delete mail" data-delete>
<core:icon identifier="actions-delete" size="small"/>
</span>
</button>
Expand All @@ -58,33 +64,56 @@ <h1>Mail Log</h1>
<div class="tab-header">
<div class="row">
<div class="col">
<span><strong>To:</strong>
{mail.displayToAddress}<br/><strong>Date:</strong>
<span><strong>To:</strong> {mail.displayToAddress}<br/><strong>Date:</strong>
<f:format.date format="H:i, d.m.Y">{mail.date}</f:format.date>
</span>
</div>
<div class="col text-right">
<div class="btn-group content-type-switches">
<a class="btn btn-primary"
data-content-type="plain"
data-m-id="{i.cycle}">Plain
</a>
<a class="btn btn-outline-primary"
data-content-type="html"
data-m-id="{i.cycle}">HTML
</a>
<f:if condition="{mail.bodyPlain}">
<a class="btn btn-primary" data-content-type="plain" data-m-id="{i.cycle}">Plain
</a>
</f:if>
<f:if condition="{mail.bodyHtml}">
<a class="btn {f:if(condition: mail.bodyPlain, then: 'btn-outline-primary', else: 'btn-primary')}"
data-content-type="html"
data-m-id="{i.cycle}">HTML
</a>
</f:if>
<f:if condition="{mail.attachments->f:count()}">
<a class="btn btn-outline-primary" data-content-type="files" data-m-id="{i.cycle}">Files
</a>
</f:if>
</div>
</div>
</div>
</div>
<div class="form-section" data-content-type="plain" data-m-id="{i.cycle}">
<p>{mail.bodyPlain->f:format.nl2br()}</p>
</div>
<div class="form-section hidden" data-content-type="html" data-m-id="{i.cycle}">
<div class="loading">
<core:icon identifier="spinner-circle-dark" size="default"/>
<f:if condition="{mail.bodyPlain}">
<div class="form-section" data-content-type="plain" data-m-id="{i.cycle}">
<p>{mail.bodyPlain->f:format.nl2br()}</p>
</div>
</div>
</f:if>
<f:if condition="{mail.bodyHtml}">
<div class="form-section {f:if(condition: mail.bodyPlain, then: 'hidden')}"
data-content-type="html"
data-m-id="{i.cycle}">
<div class="loading">
<core:icon identifier="spinner-circle-dark" size="default"/>
</div>
</div>
</f:if>
<f:if condition="{mail.attachments}">
<div class="form-section hidden" data-content-type="files" data-m-id="{i.cycle}">
<f:for each="{mail.attachments}" as="file">{mail.publicPath}
<p>
<a href="{file.publicPath}" class="btn btn-outline-primary" download="{file.filename}">
<core:icon identifier="actions-download" size="small"/>
{file.filename} ({file.filesize->f:format.bytes()})
</a>
</p>
</f:for>
</div>
</f:if>
</div>
</div>
</div>
Expand Down Expand Up @@ -119,4 +148,4 @@ <h1>Mail Log</h1>
#delete-all-messages {
margin-top: 30px;
}
</style>
</style>
Loading

0 comments on commit b35334b

Please sign in to comment.