snappymail/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php
2022-04-22 14:51:18 +02:00

663 lines
18 KiB
PHP

<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Mail;
use \MailSo\Base\Utils;
use \MailSo\Imap\Enumerations\FetchType;
/**
* @category MailSo
* @package Mail
*/
class Message implements \JsonSerializable
{
private
$sFolder = '',
$iUid = 0,
$sSubject = '',
$sMessageId = '',
$sContentType = '',
$iSize = 0,
$iSpamScore = 0,
$sSpamResult = '',
$bIsSpam = false,
$bHasVirus = null,
$iInternalTimeStampInUTC = 0,
$iHeaderTimeStampInUTC = 0,
$sHeaderDate = '',
$aFlagsLowerCase = [],
/**
* @var \MailSo\Mime\EmailCollection
*/
$oFrom = null,
$oSender = null,
$oReplyTo = null,
$oDeliveredTo = null,
$oTo = null,
$oCc = null,
$oBcc = null,
$sInReplyTo = '',
$sPlain = '',
$sHtml = '',
/**
* @var AttachmentCollection
*/
$oAttachments = null,
/**
* @var array
*/
$aDraftInfo = null,
$sReferences = '',
/**
* @var int
*/
$iPriority,
$sDeliveryReceipt = '',
$sReadReceipt = '',
// https://autocrypt.org/level1.html#the-autocrypt-header
$sAutocrypt = '',
$aUnsubsribeLinks = array(),
$aThreads = array(),
$aPgpSigned = null,
$aPgpEncrypted = null;
function __construct()
{
$this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
}
public function Plain() : string
{
return $this->sPlain;
}
public function Html() : string
{
return $this->sHtml;
}
public function PgpSigned() : ?array
{
return $this->aPgpSigned;
}
public function PgpEncrypted() : ?array
{
return $this->aPgpEncrypted;
}
public function Folder() : string
{
return $this->sFolder;
}
public function Uid() : int
{
return $this->iUid;
}
public function MessageId() : string
{
return $this->sMessageId;
}
public function Subject() : string
{
return $this->sSubject;
}
public function ContentType() : string
{
return $this->sContentType;
}
public function Size() : int
{
return $this->iSize;
}
public function SpamScore() : int
{
return $this->iSpamScore;
}
private function setSpamScore($value) : void
{
$this->iSpamScore = \intval(\max(0, \min(100, $value)));
}
public function SpamResult() : string
{
return $this->sSpamResult;
}
public function IsSpam() : bool
{
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;
}
public function HeaderTimeStampInUTC() : int
{
return $this->iHeaderTimeStampInUTC;
}
public function HeaderDate() : string
{
return $this->sHeaderDate;
}
public function FlagsLowerCase() : array
{
return $this->aFlagsLowerCase;
}
public function From() : ?\MailSo\Mime\EmailCollection
{
return $this->oFrom;
}
public function Priority() : int
{
return $this->iPriority;
}
public function Sender() : ?\MailSo\Mime\EmailCollection
{
return $this->oSender;
}
public function ReplyTo() : ?\MailSo\Mime\EmailCollection
{
return $this->oReplyTo;
}
public function DeliveredTo() : ?\MailSo\Mime\EmailCollection
{
return $this->oDeliveredTo;
}
public function To() : ?\MailSo\Mime\EmailCollection
{
return $this->oTo;
}
public function Cc() : ?\MailSo\Mime\EmailCollection
{
return $this->oCc;
}
public function Bcc() : ?\MailSo\Mime\EmailCollection
{
return $this->oBcc;
}
public function Attachments() : ?AttachmentCollection
{
return $this->oAttachments;
}
public function InReplyTo() : string
{
return $this->sInReplyTo;
}
public function References() : string
{
return $this->sReferences;
}
public function DeliveryReceipt() : string
{
return $this->sDeliveryReceipt;
}
public function ReadReceipt() : string
{
return $this->sReadReceipt;
}
public function UnsubsribeLinks() : array
{
return $this->aUnsubsribeLinks;
}
public function ReadingConfirmation() : string
{
return $this->ReadReceipt();
}
public function DraftInfo() : ?array
{
return $this->aDraftInfo;
}
public function Threads() : array
{
return $this->aThreads;
}
public function SetThreads(array $aThreads)
{
$this->aThreads = $aThreads;
}
public static function NewFetchResponseInstance(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self
{
$oMessage = new self;
if (!$oBodyStructure)
{
$oBodyStructure = $oFetchResponse->GetFetchBodyStructure();
}
$sInternalDate = $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE);
$aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS);
$oMessage->sFolder = $sFolder;
$oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE);
$oMessage->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []);
$oMessage->iInternalTimeStampInUTC =
\MailSo\Base\DateTimeHelper::ParseInternalDateString($sInternalDate);
$sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : '';
$sHeaders = $oFetchResponse->GetHeaderFieldsValue();
if (\strlen($sHeaders))
{
$oHeaders = new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset);
$sContentTypeCharset = $oHeaders->ParameterValue(
\MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
\MailSo\Mime\Enumerations\Parameter::CHARSET
);
if (\strlen($sContentTypeCharset))
{
$sCharset = Utils::NormalizeCharset($sContentTypeCharset);
}
if (\strlen($sCharset))
{
$oHeaders->SetParentCharset($sCharset);
}
$bCharsetAutoDetect = !\strlen($sCharset);
$oMessage->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect);
$oMessage->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID);
$oMessage->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect);
$oMessage->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect);
$oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
if ($oMessage->oFrom) {
$oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom);
}
$oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
$oMessage->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
$oMessage->sReferences = Utils::StripSpaces(
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
$sHeaderDate = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE);
$oMessage->sHeaderDate = $sHeaderDate;
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate);
// Priority
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY);
if (!\strlen($sPriority))
{
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE);
}
if (!\strlen($sPriority))
{
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY);
}
if (\strlen($sPriority))
{
switch (\str_replace(' ', '', \strtolower($sPriority)))
{
case 'high':
case '1(highest)':
case '2(high)':
case '1':
case '2':
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH;
break;
case 'low':
case '4(low)':
case '5(lowest)':
case '4':
case '5':
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW;
break;
}
}
// Delivery Receipt
$oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO));
// Read Receipt
$oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
if (empty($oMessage->sReadReceipt))
{
$oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO));
}
// Unsubscribe links
$oMessage->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
if (empty($oMessage->aUnsubsribeLinks))
{
$oMessage->aUnsubsribeLinks = array();
}
else
{
$oMessage->aUnsubsribeLinks = explode(',', $oMessage->aUnsubsribeLinks);
$oMessage->aUnsubsribeLinks = array_map(
function ($link) {
return trim($link, ' <>');
},
$oMessage->aUnsubsribeLinks
);
}
if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAMD_RESULT)) {
if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) {
if ($threshold = \floatval($match[2])) {
$oMessage->setSpamScore(100 * \floatval($match[1]) / $threshold);
$oMessage->sSpamResult = "{$match[1]} / {$match[2]}";
}
}
$oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***');
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_BOGOSITY)) {
$oMessage->sSpamResult = $spam;
$oMessage->bIsSpam = !!\preg_match('/yes|spam/', $spam);
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
$oMessage->setSpamScore(\floatval($spamicity[1]));
}
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS)) {
$oMessage->sSpamResult = $spam;
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
if ($threshold = \floatval($required[1])) {
$oMessage->setSpamScore(100 * \floatval($value[1]) / $threshold);
$oMessage->sSpamResult = "{$value[1]} / {$required[1]}";
}
}
$oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3);
// $spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_FLAG);
// $oMessage->bIsSpam = false !== \stripos($spam, 'YES');
}
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS)) {
$oMessage->bHasVirus = true;
}
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) {
if (false !== \stripos($spam, 'infected')) {
$oMessage->bHasVirus = true;
} else if (false !== \stripos($spam, 'clean')) {
$oMessage->bHasVirus = false;
}
}
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED)) {
$oMessage->sVirusScanned = $virus;
}
$sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO);
if (\strlen($sDraftInfo)) {
$sType = '';
$sFolder = '';
$iUid = 0;
$oParameters = new \MailSo\Mime\ParameterCollection($sDraftInfo);
foreach ($oParameters as $oParameter) {
switch (\strtolower($oParameter->Name()))
{
case 'type':
$sType = $oParameter->Value();
break;
case 'uid':
$iUid = (int) $oParameter->Value();
break;
case 'folder':
$sFolder = \base64_decode($oParameter->Value());
break;
}
}
if (\strlen($sType) && \strlen($sFolder) && $iUid) {
$oMessage->aDraftInfo = array($sType, $iUid, $sFolder);
}
}
$oMessage->sAutocrypt = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::AUTOCRYPT);
}
else if ($oFetchResponse->GetEnvelope())
{
$sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::ISO_8859_1;
// date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id
$oMessage->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, '');
$oMessage->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset);
$oMessage->oFrom = $oFetchResponse->GetFetchEnvelopeEmailCollection(2, $sCharset);
$oMessage->oSender = $oFetchResponse->GetFetchEnvelopeEmailCollection(3, $sCharset);
$oMessage->oReplyTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(4, $sCharset);
$oMessage->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset);
$oMessage->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset);
$oMessage->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset);
$oMessage->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, '');
}
if ($oBodyStructure)
{
$gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted');
foreach ($gEncryptedParts as $oPart) {
if ($oPart->IsPgpEncrypted()) {
$oMessage->aPgpEncrypted = [
'PartId' => $oPart->SubParts()[1]->PartID()
];
}
}
$gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed');
foreach ($gSignatureParts as $oPart) {
if (!$oPart->IsPgpSigned()) {
continue;
}
$oPgpSignaturePart = $oPart->SubParts()[1];
$oMessage->aPgpSigned = [
// /?/Raw/&q[]=/0/Download/&q[]=/...
// /?/Raw/&q[]=/0/View/&q[]=/...
'BodyPartId' => $oPart->SubParts()[0]->PartID(),
'SigPartId' => $oPgpSignaturePart->PartID(),
'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg')
];
/*
// An empty section specification refers to the entire message, including the header.
// But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME].
$sPgpText = \trim(
\trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].'.MIME]'))
. "\r\n\r\n"
. \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].']'))
);
if ($sPgpText) {
$oMessage->aPgpSigned['Body'] = $sPgpText;
}
$sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['SigPartId'].']');
if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) {
$oMessage->aPgpSigned['Signature'] = $oPart->SubParts()[0]->PartID();
}
*/
break;
}
$aTextParts = $oBodyStructure->GetHtmlAndPlainParts();
if ($aTextParts)
{
$sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::UTF_8;
$aHtmlParts = array();
$aPlainParts = array();
foreach ($aTextParts as $oPart)
{
$sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']');
if (null === $sText)
{
// TextPartIsTrimmed ?
$sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>');
}
if (\is_string($sText) && \strlen($sText))
{
$sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName());
$sText = Utils::ConvertEncoding($sText,
Utils::NormalizeCharset($oPart->Charset() ?: $sCharset, true),
\MailSo\Base\Enumerations\Charset::UTF_8
);
$sText = Utils::Utf8Clear($sText);
// https://datatracker.ietf.org/doc/html/rfc4880#section-7
// Cleartext Signature
if (!$oMessage->aPgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----'))
{
$oMessage->aPgpSigned = [
'BodyPartId' => $oPart->PartID()
];
}
if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----'))
{
$keyIds = [];
if (\SnappyMail\PGP\GPG::isSupported()) {
$GPG = new \SnappyMail\PGP\GPG('');
$keyIds = $GPG->getEncryptedMessageKeys($sText);
}
$oMessage->aPgpEncrypted = [
'PartId' => $oPart->PartID(),
'KeyIds' => $keyIds
];
}
if ('text/html' === $oPart->ContentType())
{
$aHtmlParts[] = $sText;
}
else
{
if ($oPart->IsFlowedFormat())
{
$sText = Utils::DecodeFlowedFormat($sText);
}
$aPlainParts[] = $sText;
}
}
}
$oMessage->sHtml = \implode('<br>', $aHtmlParts);
$oMessage->sPlain = \trim(\implode("\n", $aPlainParts));
unset($aHtmlParts, $aPlainParts);
}
$gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts();
if ($gAttachmentsParts->valid())
{
$oMessage->oAttachments = new AttachmentCollection;
foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem)
{
// if ('application/pgp-keys' === $oAttachmentItem->ContentType()) import ???
$oMessage->oAttachments->append(
Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem)
);
}
}
}
return $oMessage;
}
public function jsonSerialize()
{
return array(
'@Object' => 'Object/Message',
'Folder' => $this->sFolder,
'Uid' => $this->iUid,
'Subject' => \trim(Utils::Utf8Clear($this->sSubject)),
'MessageId' => $this->sMessageId,
'Size' => $this->iSize,
'SpamScore' => $this->bIsSpam ? 100 : $this->iSpamScore,
'SpamResult' => $this->sSpamResult,
'IsSpam' => $this->bIsSpam,
'HasVirus' => $this->bHasVirus,
'DateTimeStampInUTC' => $this->iInternalTimeStampInUTC,
// \MailSo\Mime\EmailCollection
'From' => $this->oFrom,
'ReplyTo' => $this->oReplyTo,
'To' => $this->oTo,
'Cc' => $this->oCc,
'Bcc' => $this->oBcc,
'Sender' => $this->oSender,
'DeliveredTo' => $this->oDeliveredTo,
'Priority' => $this->iPriority,
'Threads' => $this->aThreads,
'UnsubsribeLinks' => $this->aUnsubsribeLinks,
'ReadReceipt' => '',
'Autocrypt' => $this->sAutocrypt,
'Attachments' => $this->oAttachments ? $this->oAttachments->SpecData() : null,
'Flags' => $this->aFlagsLowerCase
);
}
}