<parent>wcf.acp.menu.link.log</parent>
<permissions>admin.management.canManageCronjob</permissions>
</acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.log.email">
+ <controller>wcf\acp\page\EmailLogListPage</controller>
+ <parent>wcf.acp.menu.link.log</parent>
+ <permissions>admin.management.canViewLog</permissions>
+ </acpmenuitem>
<acpmenuitem name="wcf.acp.menu.link.log.exception">
<controller>wcf\acp\page\ExceptionLogViewPage</controller>
<parent>wcf.acp.menu.link.log</parent>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.email.log'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.email.log{/lang}{if $items} <span class="badge badgeInverse">{#$items}</span>{/if}</h1>
+ </div>
+
+ {hascontent}
+ <nav class="contentHeaderNavigation">
+ <ul>
+ {content}
+ {event name='contentHeaderNavigation'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</header>
+
+{hascontent}
+ <div class="paginationTop">
+ {content}{pages print=true assign=pagesLinks controller="EmailLogList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section tabularBox">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnEntryID{if $sortField == 'entryID'} active {@$sortOrder}{/if}"><a href="{link controller='EmailLogList'}pageNo={@$pageNo}&sortField=entryID&sortOrder={if $sortField == 'entryID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnTitle columnMessageID{if $sortField == 'messageID'} active {@$sortOrder}{/if}">{lang}wcf.acp.email.log.messageID{/lang}</th>
+ <th class="columnText columnRecipient{if $sortField == 'recipient'} active {@$sortOrder}{/if}">{lang}wcf.user.email{/lang}</th>
+ <th class="columnDate columnTime{if $sortField == 'time'} active {@$sortOrder}{/if}"><a href="{link controller='EmailLogList'}pageNo={@$pageNo}&sortField=time&sortOrder={if $sortField == 'execTime' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.email.log.time{/lang}</a></th>
+ <th class="columnText columnStatusMessage{if $sortField == 'status'} active {@$sortOrder}{/if}"><a href="{link controller='EmailLogList'}pageNo={@$pageNo}&sortField=status&sortOrder={if $sortField == 'success' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.email.log.status{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item=entry}
+ <tr class="jsEmailLogEntry">
+ <td class="columnID columnEntryID">{@$entry->entryID}</td>
+ <td class="columnTitle columnMessageID">
+ <kbd title="{$entry->messageID}">{$entry->getFormattedMessageId()|truncate:50}</kbd>
+ </td>
+ <td class="columnText columnRecipient">
+ {if $__wcf->session->getPermission('admin.user.canEditMailAddress')}
+ {$entry->recipient}
+ {else}
+ {$entry->getRedactedRecipientAddress()}
+ {/if}
+ {if $entry->getRecipient()}
+ (<a href="{link controller='UserEdit' id=$entry->getRecipient()->getObjectID()}{/link}">{$entry->getRecipient()->getTitle()}</a>)
+ {/if}
+ </td>
+ <td class="columnDate columnTime">{@$entry->time|time}</td>
+
+ <td class="columnText columnStatusMessage">
+ <span class="
+ badge
+ {if $entry->status === 'success'}green
+ {elseif $entry->status === 'transient_failure'}yellow
+ {elseif $entry->status === 'permanent_failure'}red
+ {/if}
+ {if $entry->message}pointer jsStaticDialog{/if}
+ "{if $entry->message} data-dialog-id="statusMessage{$entry->entryID}"{/if}>{lang}wcf.acp.email.log.status.{$entry->status}{/lang}</span>
+ {if $entry->message}
+ <div id="statusMessage{$entry->entryID}" data-title="{lang}wcf.acp.email.log.statusMessage.title{/lang}" style="display: none">
+ {$entry->message}
+ </div>
+ {/if}
+ </td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}
+ {event name='contentFooterNavigation'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+ </footer>
+{else}
+ <p class="info" role="status">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
--- /dev/null
+<?php
+
+namespace wcf\acp\page;
+
+use wcf\data\email\log\entry\EmailLogEntryList;
+use wcf\page\SortablePage;
+use wcf\system\cache\runtime\UserRuntimeCache;
+
+/**
+ * Shows email logs.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ *
+ * @property EmailLogEntryList $objectList
+ */
+class EmailLogListPage extends SortablePage
+{
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.log.email';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.management.canViewLog'];
+
+ /**
+ * @inheritDoc
+ */
+ public $itemsPerPage = 100;
+
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortField = 'time';
+
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortOrder = 'DESC';
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = ['entryID', 'time', 'status'];
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = EmailLogEntryList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public function readData()
+ {
+ parent::readData();
+
+ $userIDs = \array_filter(\array_column($this->objectList->getObjects(), 'recipientID'));
+ UserRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
+ }
+}
namespace wcf\data\email\log\entry;
use wcf\data\DatabaseObject;
+use wcf\data\user\User;
+use wcf\system\cache\runtime\UserRuntimeCache;
+use wcf\system\email\Email;
/**
* Represents an email log entry.
public const STATUS_TRANSIENT_FAILURE = 'transient_failure';
public const STATUS_PERMANENT_FAILURE = 'permanent_failure';
+
+ /**
+ * Returns the formatted 'Message-ID', stripping useless information.
+ */
+ public function getFormattedMessageId(): string
+ {
+ return \preg_replace_callback(
+ '/^\<((.*)@(.*))\>$/',
+ static function ($matches) {
+ if ($matches[3] === Email::getHost()) {
+ return $matches[2] . '@';
+ } else {
+ return $matches[1];
+ }
+ },
+ $this->messageID
+ );
+ }
+
+ /**
+ * Returns the recipient.
+ *
+ * @see EmailLogEntry::$recipient
+ */
+ public function getRecipient(): ?User
+ {
+ if (!$this->recipientID) {
+ return null;
+ }
+
+ return UserRuntimeCache::getInstance()->getObject($this->recipientID);
+ }
+
+ /**
+ * Returns the redacted recipient address.
+ */
+ public function getRedactedRecipientAddress(): string
+ {
+ $atSign = \strrpos($this->recipient, '@');
+ $localpart = \substr($this->recipient, 0, $atSign);
+ $domain = \substr($this->recipient, $atSign + 1);
+
+ return \substr($localpart, 0, 1) . "\u{2022}\u{2022}\u{2022}\u{2022}@{$domain}";
+ }
}
<item name="wcf.acp.email.smtp.test.error.hostUnknown"><![CDATA[Der Server antwortet nicht.]]></item>
<item name="wcf.acp.email.smtp.test.error.notTlsSupport"><![CDATA[Der Server unterstützt keine Verschlüsselung.]]></item>
<item name="wcf.acp.email.smtp.test.error.tlsFailed"><![CDATA[Der Aufbau einer verschlüsselten Verbindung war nicht möglich.]]></item>
+ <item name="wcf.acp.email.log"><![CDATA[Versendete E-Mails]]></item>
+ <item name="wcf.acp.email.log.messageID"><![CDATA[Message-ID]]></item>
+ <item name="wcf.acp.email.log.time"><![CDATA[Erzeugt]]></item>
+ <item name="wcf.acp.email.log.status"><![CDATA[Status]]></item>
+ <item name="wcf.acp.email.log.status.success"><![CDATA[Erfolgreich versendet]]></item>
+ <item name="wcf.acp.email.log.status.transient_failure"><![CDATA[Vorübergehendes Problem]]></item>
+ <item name="wcf.acp.email.log.status.permanent_failure"><![CDATA[Endgültig fehlgeschlagen]]></item>
+ <item name="wcf.acp.email.log.status.new"><![CDATA[Wartend]]></item>
+ <item name="wcf.acp.email.log.statusMessage.title"><![CDATA[Status-Nachricht]]></item>
</category>
<category name="wcf.acp.exceptionLog">
<item name="wcf.acp.exceptionLog"><![CDATA[Protokollierte Fehler]]></item>
<item name="wcf.acp.menu.link.language.item.add"><![CDATA[Text hinzufügen]]></item>
<item name="wcf.acp.menu.link.systemCheck"><![CDATA[Systemüberprüfung]]></item>
<item name="wcf.acp.menu.link.devtools.missingLanguageItem.list"><![CDATA[Fehlende Texte]]></item>
+ <item name="wcf.acp.menu.link.log.email"><![CDATA[E-Mails]]></item>
</category>
<category name="wcf.acp.modificationLog">
<item name="wcf.acp.modificationLog.list"><![CDATA[Globales Änderungsprotokoll]]></item>
<item name="wcf.acp.email.smtp.test.error.hostUnknown"><![CDATA[The server is not responding.]]></item>
<item name="wcf.acp.email.smtp.test.error.notTlsSupport"><![CDATA[The server does not support encryption.]]></item>
<item name="wcf.acp.email.smtp.test.error.tlsFailed"><![CDATA[Unable to establish a secure connection.]]></item>
+ <item name="wcf.acp.email.log"><![CDATA[Emails Sent]]></item>
+ <item name="wcf.acp.email.log.messageID"><![CDATA[Message-ID]]></item>
+ <item name="wcf.acp.email.log.time"><![CDATA[Created]]></item>
+ <item name="wcf.acp.email.log.status"><![CDATA[Status]]></item>
+ <item name="wcf.acp.email.log.status.success"><![CDATA[Successfully Sent]]></item>
+ <item name="wcf.acp.email.log.status.transient_failure"><![CDATA[Transient Failure]]></item>
+ <item name="wcf.acp.email.log.status.permanent_failure"><![CDATA[Ultimately Failed]]></item>
+ <item name="wcf.acp.email.log.status.new"><![CDATA[Pending]]></item>
+ <item name="wcf.acp.email.log.statusMessage.title"><![CDATA[Status Message]]></item>
</category>
<category name="wcf.acp.exceptionLog">
<item name="wcf.acp.exceptionLog"><![CDATA[Logged errors]]></item>
<item name="wcf.acp.menu.link.language.item.add"><![CDATA[Add Phrase]]></item>
<item name="wcf.acp.menu.link.systemCheck"><![CDATA[System Check]]></item>
<item name="wcf.acp.menu.link.devtools.missingLanguageItem.list"><![CDATA[Missing Phrases]]></item>
+ <item name="wcf.acp.menu.link.log.email"><![CDATA[Emails]]></item>
</category>
<category name="wcf.acp.modificationLog">
<item name="wcf.acp.modificationLog.list"><![CDATA[Global Modification Log]]></item>