mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-10 15:08:28 +03:00
Resolve issue #139
This commit is contained in:
parent
a15e7f6d1e
commit
cca8ce065c
14 changed files with 135 additions and 89 deletions
|
|
@ -48,6 +48,7 @@ export class MessageModel extends AbstractModel {
|
|||
spamScore: 0,
|
||||
spamResult: '',
|
||||
isSpam: false,
|
||||
hasVirus: null, // or boolean when scanned
|
||||
dateTimeStampInUTC: 0,
|
||||
priority: MessagePriority.Normal,
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ export class MessageModel extends AbstractModel {
|
|||
this.spamScore(0);
|
||||
this.spamResult('');
|
||||
this.isSpam(false);
|
||||
this.hasVirus(null);
|
||||
this.dateTimeStampInUTC(0);
|
||||
this.priority(MessagePriority.Normal);
|
||||
|
||||
|
|
@ -419,6 +421,7 @@ export class MessageModel extends AbstractModel {
|
|||
this.spamScore(message.spamScore());
|
||||
this.spamResult(message.spamResult());
|
||||
this.isSpam(message.isSpam());
|
||||
this.hasVirus(message.hasVirus());
|
||||
this.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
||||
this.priority(message.priority());
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,12 @@
|
|||
&.emptySubject .emptySubjectText {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.hasVirus {
|
||||
background: #f00;
|
||||
color: #fff;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageItem {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ class MailClient
|
|||
\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS,
|
||||
\MailSo\Mime\Enumerations\Header::X_SPAMD_RESULT,
|
||||
\MailSo\Mime\Enumerations\Header::X_BOGOSITY
|
||||
// Virus
|
||||
,\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED,
|
||||
,\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS
|
||||
), true);
|
||||
//
|
||||
// return \MailSo\Imap\Enumerations\FetchType::ENVELOPE;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class Message implements \JsonSerializable
|
|||
$iSpamScore = 0,
|
||||
$sSpamResult = '',
|
||||
$bIsSpam = false,
|
||||
$bHasVirus = null,
|
||||
$iInternalTimeStampInUTC = 0,
|
||||
$iHeaderTimeStampInUTC = 0,
|
||||
$sHeaderDate = '',
|
||||
|
|
@ -168,6 +169,16 @@ class Message implements \JsonSerializable
|
|||
return $this->bIsSpam;
|
||||
}
|
||||
|
||||
/**
|
||||
* null = not scanned
|
||||
* true = scanned and infected
|
||||
* false = scanned and no infection found
|
||||
*/
|
||||
public function HasVirus() : ?bool
|
||||
{
|
||||
return $this->bHasVirus;
|
||||
}
|
||||
|
||||
public function InternalTimeStampInUTC() : int
|
||||
{
|
||||
return $this->iInternalTimeStampInUTC;
|
||||
|
|
@ -456,6 +467,7 @@ class Message implements \JsonSerializable
|
|||
$this->iSpamScore = \max(0, \min(100, \floatval($spamicity[1])));
|
||||
}
|
||||
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS)) {
|
||||
$this->sSpamResult = $spam;
|
||||
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
|
||||
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
|
||||
if ($threshold = \floatval($required[1])) {
|
||||
|
|
@ -467,6 +479,17 @@ class Message implements \JsonSerializable
|
|||
$this->bIsSpam = false !== \stripos($spam, 'YES');
|
||||
}
|
||||
|
||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) {
|
||||
if (false !== \stripos($spam, 'infected')) {
|
||||
$this->bHasVirus = true;
|
||||
} else if (false !== \stripos($spam, 'clean')) {
|
||||
$this->bHasVirus = false;
|
||||
}
|
||||
}
|
||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED)) {
|
||||
$this->sVirusScanned = $virus;
|
||||
}
|
||||
|
||||
$sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO);
|
||||
if (0 < \strlen($sDraftInfo)) {
|
||||
$sType = '';
|
||||
|
|
@ -632,44 +655,43 @@ class Message implements \JsonSerializable
|
|||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$oAttachments = $this->Attachments();
|
||||
$aFlags = $this->FlagsLowerCase();
|
||||
return array(
|
||||
'@Object' => 'Object/Message',
|
||||
'Folder' => $this->Folder(),
|
||||
'Uid' => (string) $this->Uid(),
|
||||
'Subject' => \trim(\MailSo\Base\Utils::Utf8Clear($this->Subject())),
|
||||
'MessageId' => $this->MessageId(),
|
||||
'Size' => $this->Size(),
|
||||
'SpamScore' => $this->SpamScore(),
|
||||
'SpamResult' => $this->SpamResult(),
|
||||
'IsSpam' => $this->IsSpam(),
|
||||
'DateTimeStampInUTC' => $this->InternalTimeStampInUTC(),
|
||||
'Folder' => $this->sFolder,
|
||||
'Uid' => (string) $this->iUid,
|
||||
'Subject' => \trim(\MailSo\Base\Utils::Utf8Clear($this->sSubject)),
|
||||
'MessageId' => $this->sMessageId,
|
||||
'Size' => $this->iSize,
|
||||
'SpamScore' => $this->iSpamScore,
|
||||
'SpamResult' => $this->sSpamResult,
|
||||
'IsSpam' => $this->bIsSpam,
|
||||
'HasVirus' => $this->bHasVirus,
|
||||
'DateTimeStampInUTC' => $this->iInternalTimeStampInUTC,
|
||||
|
||||
// \MailSo\Mime\EmailCollection
|
||||
'From' => $this->From(),
|
||||
'ReplyTo' => $this->ReplyTo(),
|
||||
'To' => $this->To(),
|
||||
'Cc' => $this->Cc(),
|
||||
'Bcc' => $this->Bcc(),
|
||||
'Sender' => $this->Sender(),
|
||||
'DeliveredTo' => $this->DeliveredTo(),
|
||||
'From' => $this->oFrom,
|
||||
'ReplyTo' => $this->oReplyTo,
|
||||
'To' => $this->oTo,
|
||||
'Cc' => $this->oCc,
|
||||
'Bcc' => $this->oBcc,
|
||||
'Sender' => $this->oSender,
|
||||
'DeliveredTo' => $this->oDeliveredTo,
|
||||
|
||||
'Priority' => $this->Priority(),
|
||||
'Threads' => $this->Threads(),
|
||||
'Sensitivity' => $this->Sensitivity(),
|
||||
'UnsubsribeLinks' => $this->UnsubsribeLinks(),
|
||||
'Priority' => $this->iPriority,
|
||||
'Threads' => $this->aThreads,
|
||||
'Sensitivity' => $this->iSensitivity,
|
||||
'UnsubsribeLinks' => $this->aUnsubsribeLinks,
|
||||
'ReadReceipt' => '',
|
||||
|
||||
'HasAttachments' => $oAttachments ? 0 < $oAttachments->Count() : false,
|
||||
'AttachmentsSpecData' => $oAttachments ? $oAttachments->SpecData() : array(),
|
||||
'HasAttachments' => $this->oAttachments && 0 < $this->oAttachments->Count(),
|
||||
'AttachmentsSpecData' => $this->oAttachments ? $this->oAttachments->SpecData() : array(),
|
||||
|
||||
// Flags
|
||||
'IsUnseen' => \in_array('\\unseen', $aFlags) || !\in_array('\\seen', $aFlags),
|
||||
'IsSeen' => \in_array('\\seen', $aFlags),
|
||||
'IsFlagged' => \in_array('\\flagged', $aFlags),
|
||||
'IsAnswered' => \in_array('\\answered', $aFlags),
|
||||
'IsDeleted' => \in_array('\\deleted', $aFlags)
|
||||
'IsUnseen' => \in_array('\\unseen', $this->aFlagsLowerCase) || !\in_array('\\seen', $this->aFlagsLowerCase),
|
||||
'IsSeen' => \in_array('\\seen', $this->aFlagsLowerCase),
|
||||
'IsFlagged' => \in_array('\\flagged', $this->aFlagsLowerCase),
|
||||
'IsAnswered' => \in_array('\\answered', $this->aFlagsLowerCase),
|
||||
'IsDeleted' => \in_array('\\deleted', $this->aFlagsLowerCase)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,69 +18,74 @@ namespace MailSo\Mime\Enumerations;
|
|||
*/
|
||||
abstract class Header
|
||||
{
|
||||
const DATE = 'Date';
|
||||
const RECEIVED = 'Received';
|
||||
const
|
||||
DATE = 'Date',
|
||||
RECEIVED = 'Received',
|
||||
|
||||
const SUBJECT = 'Subject';
|
||||
SUBJECT = 'Subject',
|
||||
|
||||
const TO_ = 'To';
|
||||
const FROM_ = 'From';
|
||||
const CC = 'Cc';
|
||||
const BCC = 'Bcc';
|
||||
const REPLY_TO = 'Reply-To';
|
||||
const SENDER = 'Sender';
|
||||
const RETURN_PATH = 'Return-Path';
|
||||
const DELIVERED_TO = 'Delivered-To';
|
||||
TO_ = 'To',
|
||||
FROM_ = 'From',
|
||||
CC = 'Cc',
|
||||
BCC = 'Bcc',
|
||||
REPLY_TO = 'Reply-To',
|
||||
SENDER = 'Sender',
|
||||
RETURN_PATH = 'Return-Path',
|
||||
DELIVERED_TO = 'Delivered-To',
|
||||
|
||||
const MESSAGE_ID = 'Message-ID';
|
||||
const IN_REPLY_TO = 'In-Reply-To';
|
||||
const REFERENCES = 'References';
|
||||
const X_DRAFT_INFO = 'X-Draft-Info';
|
||||
const X_ORIGINATING_IP = 'X-Originating-IP';
|
||||
MESSAGE_ID = 'Message-ID',
|
||||
IN_REPLY_TO = 'In-Reply-To',
|
||||
REFERENCES = 'References',
|
||||
X_DRAFT_INFO = 'X-Draft-Info',
|
||||
X_ORIGINATING_IP = 'X-Originating-IP',
|
||||
|
||||
const CONTENT_TYPE = 'Content-Type';
|
||||
const CONTENT_TRANSFER_ENCODING = 'Content-Transfer-Encoding';
|
||||
const CONTENT_DISPOSITION = 'Content-Disposition';
|
||||
const CONTENT_DESCRIPTION = 'Content-Description';
|
||||
const CONTENT_ID = 'Content-ID';
|
||||
const CONTENT_LOCATION = 'Content-Location';
|
||||
CONTENT_TYPE = 'Content-Type',
|
||||
CONTENT_TRANSFER_ENCODING = 'Content-Transfer-Encoding',
|
||||
CONTENT_DISPOSITION = 'Content-Disposition',
|
||||
CONTENT_DESCRIPTION = 'Content-Description',
|
||||
CONTENT_ID = 'Content-ID',
|
||||
CONTENT_LOCATION = 'Content-Location',
|
||||
|
||||
const SENSITIVITY = 'Sensitivity';
|
||||
SENSITIVITY = 'Sensitivity',
|
||||
|
||||
const RECEIVED_SPF = 'Received-SPF';
|
||||
const AUTHENTICATION_RESULTS = 'Authentication-Results';
|
||||
const X_DKIM_AUTHENTICATION_RESULTS = 'X-DKIM-Authentication-Results';
|
||||
RECEIVED_SPF = 'Received-SPF',
|
||||
AUTHENTICATION_RESULTS = 'Authentication-Results',
|
||||
X_DKIM_AUTHENTICATION_RESULTS = 'X-DKIM-Authentication-Results',
|
||||
|
||||
const DKIM_SIGNATURE = 'DKIM-Signature';
|
||||
const DOMAINKEY_SIGNATURE = 'DomainKey-Signature';
|
||||
DKIM_SIGNATURE = 'DKIM-Signature',
|
||||
DOMAINKEY_SIGNATURE = 'DomainKey-Signature',
|
||||
|
||||
// SpamAssassin
|
||||
const X_SPAM_FLAG = 'X-Spam-Flag'; // YES/NO
|
||||
const X_SPAM_LEVEL = 'X-Spam-Level'; // *******
|
||||
const X_SPAM_STATUS = 'X-Spam-Status'; // Yes|No
|
||||
X_SPAM_FLAG = 'X-Spam-Flag', // YES/NO
|
||||
X_SPAM_LEVEL = 'X-Spam-Level', // *******
|
||||
X_SPAM_STATUS = 'X-Spam-Status', // Yes|No
|
||||
// Rspamd
|
||||
const X_SPAMD_RESULT = 'X-Spamd-Result'; // default: False [7.13 / 9.00];
|
||||
const X_SPAMD_BAR = 'X-Spamd-Bar'; // +++++++
|
||||
X_SPAMD_RESULT = 'X-Spamd-Result', // default: False [7.13 / 9.00],
|
||||
X_SPAMD_BAR = 'X-Spamd-Bar', // +++++++
|
||||
// Bogofilter
|
||||
const X_BOGOSITY = 'X-Bogosity';
|
||||
X_BOGOSITY = 'X-Bogosity',
|
||||
// Unknown
|
||||
const X_SPAM_CATEGORY = 'X-Spam-Category'; // SPAM|LEGIT
|
||||
const X_SPAM_SCORE = 'X-Spam-Score'; // 0
|
||||
const X_HAM_REPORT = 'X-Ham-Report';
|
||||
const X_MICROSOFT_ANTISPAM = 'x-microsoft-antispam:';
|
||||
X_SPAM_CATEGORY = 'X-Spam-Category', // SPAM|LEGIT
|
||||
X_SPAM_SCORE = 'X-Spam-Score', // 0
|
||||
X_HAM_REPORT = 'X-Ham-Report',
|
||||
X_MICROSOFT_ANTISPAM = 'x-microsoft-antispam',
|
||||
|
||||
const X_VIRUS = 'X-Virus';
|
||||
// X-Quarantine-ID
|
||||
X_VIRUS = 'X-Virus',
|
||||
// ClamAV
|
||||
X_VIRUS_SCANNED = 'X-Virus-Scanned',
|
||||
X_VIRUS_STATUS = 'X-Virus-Status', // clean/infected/not-scanned
|
||||
|
||||
const RETURN_RECEIPT_TO = 'Return-Receipt-To';
|
||||
const DISPOSITION_NOTIFICATION_TO = 'Disposition-Notification-To';
|
||||
const X_CONFIRM_READING_TO = 'X-Confirm-Reading-To';
|
||||
RETURN_RECEIPT_TO = 'Return-Receipt-To',
|
||||
DISPOSITION_NOTIFICATION_TO = 'Disposition-Notification-To',
|
||||
X_CONFIRM_READING_TO = 'X-Confirm-Reading-To',
|
||||
|
||||
const MIME_VERSION = 'MIME-Version';
|
||||
const X_MAILER = 'X-Mailer';
|
||||
MIME_VERSION = 'MIME-Version',
|
||||
X_MAILER = 'X-Mailer',
|
||||
|
||||
const X_MSMAIL_PRIORITY = 'X-MSMail-Priority';
|
||||
const IMPORTANCE = 'Importance';
|
||||
const X_PRIORITY = 'X-Priority';
|
||||
X_MSMAIL_PRIORITY = 'X-MSMail-Priority',
|
||||
IMPORTANCE = 'Importance',
|
||||
X_PRIORITY = 'X-Priority',
|
||||
|
||||
const LIST_UNSUBSCRIBE = 'List-Unsubscribe';
|
||||
LIST_UNSUBSCRIBE = 'List-Unsubscribe';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "OpenPGP-signierte Nachricht (klicken, um zu überprüfen)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "OpenPGP-verschlüsselte Nachricht (klicken, um zu entschlüsseln)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Als ZIP-Datei herunterladen",
|
||||
"SPAM_SCORE": "Spam-Score"
|
||||
"SPAM_SCORE": "Spam-Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Empfangsbestätigung (angezeigt) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "OpenPGP signed message (click to verify)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "OpenPGP encrypted message (click to decrypt)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Download as zip",
|
||||
"SPAM_SCORE": "Spam Score"
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Return Receipt (displayed) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "Mensaje firmado mediante OpenPGP (click para verificar)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "Mensaje cifrado mediante OpenPGP (click para desencriptar)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Descargar todo (archivo ZIP)",
|
||||
"SPAM_SCORE": "Puntuación de spam"
|
||||
"SPAM_SCORE": "Puntuación de spam",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Acuse de recibo (se visualiza) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "Message signé par OpenPGP (cliquer pour vérifier)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "Message chiffré par OpenPGP (cliquer pour déchiffrer)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Télécharger le zip",
|
||||
"SPAM_SCORE": "Score de spam"
|
||||
"SPAM_SCORE": "Score de spam",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Accusé de réception (affiché) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "OpenPGP aláírt üzenet (kattints az ellenőrzéshez)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "OpenPGP kódolt üzenet (kattints a visszafejtéshez)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Letöltés zip fájlként",
|
||||
"SPAM_SCORE": "Spam Score"
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Visszaigazolás (megjelenítve) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "OpenPGP ondertekend bericht (klik om te verifiëren)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "OpenPGP versleuteld bericht (klik om te ontsleutelen)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Download als zip",
|
||||
"SPAM_SCORE": "Spamscore"
|
||||
"SPAM_SCORE": "Spamscore",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Ontvangstbevestiging (weergegeven) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "OpenPGP-signerat meddelande (klicka för att verifiera)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "OpenPGP-krypterat meddelande (klicka för att dekryptera)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "Ladda ner som ZIP-fil",
|
||||
"SPAM_SCORE": "Spam Score"
|
||||
"SPAM_SCORE": "Spam Score",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "Mottagar kvitto (visad) - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@
|
|||
"PGP_SIGNED_MESSAGE_DESC": "由 OpenPGP 签名 (点击验证)",
|
||||
"PGP_ENCRYPTED_MESSAGE_DESC": "由 OpenPGP 加密 (点击解密)",
|
||||
"LINK_DOWNLOAD_AS_ZIP": "下载为zip压缩包",
|
||||
"SPAM_SCORE": "垃圾邮件分数"
|
||||
"SPAM_SCORE": "垃圾邮件分数",
|
||||
"HAS_VIRUS_WARNING": "WARNING: virus detected"
|
||||
},
|
||||
"READ_RECEIPT": {
|
||||
"SUBJECT": "送达回执 - %SUBJECT%",
|
||||
|
|
|
|||
|
|
@ -193,12 +193,10 @@
|
|||
<span data-bind="text: viewCc"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="informationShort">
|
||||
<div class="informationShort" data-bind="visible: viewSpamStatus()">
|
||||
<span data-i18n="MESSAGE/SPAM_SCORE"></span>:
|
||||
<meter min="0" max="100" optimum="0" low="33" high="66" data-bind="value: viewSpamScore, title: viewSpamStatus"></meter>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<div class="informationFull" data-bind="visible: showFullInfo()">
|
||||
<table>
|
||||
|
|
@ -245,8 +243,9 @@
|
|||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hasVirus" data-bind="visible: hasVirus()" data-i18n="MESSAGE/HAS_VIRUS_WARNING"></div>
|
||||
</div>
|
||||
<div class="messageItem fixIndex" data-bind="css: viewLineAsCss(), attr: {'style': 'top:' + viewBodyTopValue() + 'px' }">
|
||||
<div class="messageItem" data-bind="css: viewLineAsCss(), attr: {'style': 'top:' + viewBodyTopValue() + 'px' }">
|
||||
<div tabindex="0" data-bind="hasfocus: messageDomFocused">
|
||||
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue