diff --git a/dev/App/User.js b/dev/App/User.js index 8c7e15b01..0036860da 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -347,7 +347,7 @@ class AppUser extends AbstractApp { } reloadOpenPgpKeys() { - if (PgpUserStore.capaOpenPGP()) { + if (PgpUserStore.openpgp) { const keys = [], email = new EmailModel(), openpgpKeyring = PgpUserStore.openpgpKeyring, @@ -784,7 +784,6 @@ class AppUser extends AbstractApp { } } PgpUserStore.openpgpKeyring = new openpgp.Keyring(); - PgpUserStore.capaOpenPGP(true); this.reloadOpenPgpKeys(); }; script.onerror = () => console.error(script.src); diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 4a58e4f3b..9c02f0d1b 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -20,8 +20,6 @@ import { AbstractModel } from 'Knoin/AbstractModel'; import PreviewHTML from 'Html/PreviewMessage.html'; -import { PgpUserStore } from 'Stores/User/Pgp'; - const /*eslint-disable max-len*/ url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi, @@ -102,7 +100,7 @@ export class MessageModel extends AbstractModel { hasImages: false, hasExternals: false, - isPgpSigned: false, + pgpSigned: null, // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" } isPgpEncrypted: false, pgpSignedVerifyStatus: SignedVerifyStatus.None, pgpSignedVerifyUser: '', @@ -181,7 +179,7 @@ export class MessageModel extends AbstractModel { this.hasExternals(false); this.attachments(new AttachmentCollectionModel); - this.isPgpSigned(false); + this.pgpSigned(null); this.isPgpEncrypted(false); this.pgpSignedVerifyStatus(SignedVerifyStatus.None); this.pgpSignedVerifyUser(''); @@ -404,7 +402,7 @@ export class MessageModel extends AbstractModel { viewHtml() { const body = this.body; if (body && this.html()) { - let html = this.html().toString() + let html = this.html() .replace(/font-size:\s*[0-9]px/g, 'font-size:11px') // Strip utm_* tracking .replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1'); @@ -465,7 +463,7 @@ export class MessageModel extends AbstractModel { if (body && this.plain()) { body.classList.toggle('html', 0); body.classList.toggle('plain', 1); - body.innerHTML = plainToHtml(this.plain().toString()) + body.innerHTML = plainToHtml(this.plain()) // Strip utm_* tracking .replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1') .replace(url, '$1$2') @@ -479,8 +477,6 @@ export class MessageModel extends AbstractModel { } initView() { - PgpUserStore.initMessageBodyControls(this.body, this); - // init BlockquoteSwitcher this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => { if (node.textContent.trim() && !node.parentNode.closest('blockquote')) { diff --git a/dev/Stores/User/Message.js b/dev/Stores/User/Message.js index 7c10b7880..209b24300 100644 --- a/dev/Stores/User/Message.js +++ b/dev/Stores/User/Message.js @@ -370,7 +370,7 @@ export const MessageUserStore = new class { message.hasImages(body.rlHasImages); } else { body = Element.fromHTML(''); diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index 4ab12ea8b..c7578284b 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -1,6 +1,5 @@ import ko from 'ko'; -import { i18n } from 'Common/Translator'; import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils'; import { AccountUserStore } from 'Stores/User/Account'; @@ -9,132 +8,8 @@ import { showScreenPopup } from 'Knoin/Knoin'; import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp'; -function controlsHelper(dom, verControl, success, title, text) -{ - dom.classList.toggle('error', !success); - dom.classList.toggle('success', success); - verControl.classList.toggle('error', !success); - verControl.classList.toggle('success', success); - dom.title = verControl.title = title; - - if (undefined !== text) { - dom.textContent = text.trim(); - } -} - -function domControlEncryptedClickHelper(store, dom, armoredMessage, recipients) { - return function() { - let message = null; - - if (this.classList.contains('success')) { - return false; - } - - try { - message = store.openpgp.message.readArmored(armoredMessage); - } catch (e) { - console.log(e); - } - - if (message && message.getText && message.verify && message.decrypt) { - store.decryptMessage( - message, - recipients, - (validPrivateKey, decryptedMessage, validPublicKey, signingKeyIds) => { - if (decryptedMessage) { - if (validPublicKey) { - controlsHelper( - dom, - this, - true, - i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', { - USER: validPublicKey.user + ' (' + validPublicKey.id + ')' - }), - decryptedMessage.getText() - ); - } else if (validPrivateKey) { - const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, - additional = keyIds - ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') - : ''; - - controlsHelper( - dom, - this, - false, - i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : ''), - decryptedMessage.getText() - ); - } else { - controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); - } - } else { - controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); - } - } - ); - - return false; - } - - controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); - return false; - }; -} - -function domControlSignedClickHelper(store, dom, armoredMessage) { - return function() { - let message = null; - - if (this.classList.contains('success') || this.classList.contains('error')) { - return false; - } - - try { - message = store.openpgp.cleartext.readArmored(armoredMessage); - } catch (e) { - console.log(e); - } - - if (message && message.getText && message.verify) { - store.verifyMessage(message, (validKey, signingKeyIds) => { - if (validKey) { - controlsHelper( - dom, - this, - true, - i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', { - USER: validKey.user + ' (' + validKey.id + ')' - }), - message.getText() - ); - } else { - const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, - additional = keyIds - ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') - : ''; - - controlsHelper( - dom, - this, - false, - i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '') - ); - } - }); - - return false; - } - - controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); - return false; - }; -} - export const PgpUserStore = new class { constructor() { - this.capaOpenPGP = ko.observable(false); - this.openpgp = null; this.openpgpkeys = ko.observableArray(); @@ -335,28 +210,4 @@ export const PgpUserStore = new class { return false; } - /** - * @param {*} dom - * @param {MessageModel} rainLoopMessage - */ - initMessageBodyControls(dom, rainLoopMessage) { - const cl = dom.classList, - signed = cl.contains('openpgp-signed'), - encrypted = cl.contains('openpgp-encrypted'); - if (encrypted || signed) { - const - domText = dom.textContent, - recipients = rainLoopMessage ? rainLoopMessage.getEmails(['from', 'to', 'cc']) : [], - verControl = Element.fromHTML('
🔒
'); - if (encrypted) { - verControl.title = i18n('MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC'); - verControl.addEventListener('click', domControlEncryptedClickHelper(this, dom, domText, recipients)); - } else { - verControl.title = i18n('MESSAGE/PGP_SIGNED_MESSAGE_DESC'); - verControl.addEventListener('click', domControlSignedClickHelper(this, dom, domText)); - } - - dom.prepend(verControl); - } - } }; diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less index 06955d4f4..02970bfbf 100644 --- a/dev/Styles/User/MessageView.less +++ b/dev/Styles/User/MessageView.less @@ -429,27 +429,28 @@ html.rl-no-preview-pane { } } */ - .b-openpgp-control { + } - color: #FA0; - cursor: pointer; - display: block; - opacity: 0.5; - margin: 15px; + .b-openpgp-control { - &:hover { - opacity: 1; - } + color: #FA0; + cursor: pointer; + display: block; + opacity: 0.5; + margin: 15px; - &.success { - color: green; - opacity: 1; - } + &:hover { + opacity: 1; + } - &.error { - color: red; - opacity: 1; - } + &.success { + color: green; + opacity: 1; + } + + &.error { + color: red; + opacity: 1; } } } diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index c4226a991..b519b4999 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -131,7 +131,7 @@ class ComposePopupView extends AbstractViewPopup { this.bSkipNextHide = false; - this.capaOpenPGP = PgpUserStore.capaOpenPGP; + this.capaOpenPGP = !!PgpUserStore.openpgp; this.identities = IdentityUserStore; @@ -552,7 +552,7 @@ class ComposePopupView extends AbstractViewPopup { } openOpenPgpPopup() { - if (PgpUserStore.capaOpenPGP() && !this.oEditor.isHtml()) { + if (PgpUserStore.openpgp && !this.oEditor.isHtml()) { showScreenPopup(ComposeOpenPgpPopupView, [ result => this.editor(editor => editor.setPlain(result)), this.oEditor.getData(false), diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 28a6e3edb..f4c360e3a 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -40,6 +40,67 @@ import Remote from 'Remote/User/Fetch'; import { decorateKoCommands, createCommand } from 'Knoin/Knoin'; import { AbstractViewRight } from 'Knoin/AbstractViews'; +import { PgpUserStore } from 'Stores/User/Pgp'; + +function controlsHelper(dom, verControl, success, title, text) +{ + dom.classList.toggle('error', !success); + dom.classList.toggle('success', success); +// verControl.classList.toggle('error', !success); +// verControl.classList.toggle('success', success); + dom.title = verControl.title = title; + + if (undefined !== text) { + dom.textContent = text.trim(); + } +} + +function pgpClickHelper(dom, armoredMessage) { + if (dom.classList.contains('success') || dom.classList.contains('error')) { + return; + } + + let message = null; + try { + message = PgpUserStore.openpgp.cleartext.readArmored(armoredMessage); + } catch (e) { + console.log(e); + } + + if (message && message.getText && message.verify) { + PgpUserStore.verifyMessage(message, (validKey, signingKeyIds) => { + console.dir([validKey, signingKeyIds]); +/* + if (validKey) { + controlsHelper( + dom, + this, + true, + i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', { + USER: validKey.user + ' (' + validKey.id + ')' + }), + message.getText() + ); + } else { + const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null, + additional = keyIds + ? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ') + : ''; + + controlsHelper( + dom, + this, + false, + i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '') + ); + } +*/ + }); + } else { + controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); + } +} + export class MailMessageView extends AbstractViewRight { constructor() { super('MailMessageView'); @@ -176,6 +237,12 @@ export class MailMessageView extends AbstractViewRight { return ''; }, + pgpSigned: () => PgpUserStore.openpgp + && MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(), + + pgpEncrypted: () => PgpUserStore.openpgp + && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), + messageListOrViewLoading: () => MessageUserStore.listIsLoading() | MessageUserStore.messageLoading() }); @@ -614,4 +681,14 @@ export class MailMessageView extends AbstractViewRight { rl.app.reloadFlagsCurrentMessageListAndMessageFromCache(); } } + + pgpDecrypt(self/*, event*/) { + const message = self.message(); + message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc'])); + } + + pgpVerify(self/*, event*/) { + const message = self.message(); + message && pgpClickHelper(message.body, message.plain()); + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php index 95eaef2a5..31ec3dd47 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php @@ -138,6 +138,11 @@ class BodyStructure return $this->sLocation; } + public function SubParts() : array + { + return $this->aSubParts; + } + public function IsInline() : bool { return 'inline' === $this->sDisposition || \strlen($this->sContentID); @@ -163,6 +168,17 @@ class BodyStructure return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); } + public function IsPgpSigned() : bool + { + // https://datatracker.ietf.org/doc/html/rfc3156#section-5 + return 'multipart/signed' === $this->sContentType + && !empty($this->aBodyParams['protocol']) + && 'application/pgp-signature' === \strtolower(\trim($this->aBodyParams['protocol'])) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->aSubParts) + && $this->aSubParts[1]->IsPgpSignature(); + } + public function IsPgpSignature() : bool { return \in_array($this->sContentType, diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php index 01cdff93f..395644833 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php @@ -15,6 +15,8 @@ namespace MailSo\Imap\Enumerations; * @category MailSo * @package Imap * @subpackage Enumerations + * + * https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.5 */ abstract class FetchType { @@ -25,18 +27,25 @@ abstract class FetchType // Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) const FULL = 'FULL'; + const HEADER = 'HEADER'; // ([RFC-2822] header of the message) + const TEXT = 'TEXT'; // ([RFC-2822] text body of the message) + const MIME = 'MIME'; // ([MIME-IMB] header) + + // Non-extensible form of BODYSTRUCTURE const BODY = 'BODY'; + // An alternate form of BODY[
] that does not implicitly set the \Seen flag. const BODY_PEEK = 'BODY.PEEK'; + // The text of a particular body section. const BODY_HEADER = 'BODY[HEADER]'; const BODY_HEADER_PEEK = 'BODY.PEEK[HEADER]'; const BODYSTRUCTURE = 'BODYSTRUCTURE'; const ENVELOPE = 'ENVELOPE'; const FLAGS = 'FLAGS'; const INTERNALDATE = 'INTERNALDATE'; - const RFC822 = 'RFC822'; - const RFC822_HEADER = 'RFC822.HEADER'; +// const RFC822 = 'RFC822'; // Functionally equivalent to BODY[] +// const RFC822_HEADER = 'RFC822.HEADER'; // Functionally equivalent to BODY.PEEK[HEADER] const RFC822_SIZE = 'RFC822.SIZE'; - const RFC822_TEXT = 'RFC822.TEXT'; +// const RFC822_TEXT = 'RFC822.TEXT'; // Functionally equivalent to BODY[TEXT] const UID = 'UID'; // RFC 3516 const BINARY = 'BINARY'; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index cf92b029d..e07557764 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -158,6 +158,7 @@ class MailClient $aFetchItems = array( FetchType::UID, +// FetchType::FAST, FetchType::RFC822_SIZE, FetchType::INTERNALDATE, FetchType::FLAGS, @@ -180,12 +181,18 @@ class MailClient $aFetchItems[] = $sLine; } - - $gSignatureParts = $oBodyStructure->SearchByContentType('application/pgp-signature'); - foreach ($gSignatureParts as $oPart) - { - $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->PartID().']'; +/* + $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); + foreach ($gSignatureParts as $oPart) { + if ($oPart->IsPgpSigned()) { + // 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]. + $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[0]->PartID().'.MIME]'; + $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[0]->PartID().']'; + $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[1]->PartID().']'; + } } +*/ } } @@ -197,8 +204,7 @@ class MailClient $aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid); if (\count($aFetchResponse)) { - $oMessage = Message::NewFetchResponseInstance( - $sFolderName, $aFetchResponse[0], $oBodyStructure); + $oMessage = Message::NewFetchResponseInstance($sFolderName, $aFetchResponse[0], $oBodyStructure); } return $oMessage; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 8c3c1aeb4..5a65d33fb 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -79,9 +79,7 @@ class Message implements \JsonSerializable $bTextPartIsTrimmed = false, - $sPgpSignature = '', - $sPgpSignatureMicAlg = '', - $bPgpSigned = false, + $aPgpSigned = null, $bPgpEncrypted = false; function __construct() @@ -99,19 +97,9 @@ class Message implements \JsonSerializable return $this->sHtml; } - public function PgpSignature() : string + public function PgpSigned() : ?array { - return $this->sPgpSignature; - } - - public function PgpSignatureMicAlg() : string - { - return $this->sPgpSignatureMicAlg; - } - - public function isPgpSigned() : bool - { - return $this->bPgpSigned; + return $this->aPgpSigned; } public function isPgpEncrypted() : bool @@ -286,11 +274,8 @@ class Message implements \JsonSerializable public static function NewFetchResponseInstance(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self { - return (new self)->InitByFetchResponse($sFolder, $oFetchResponse, $oBodyStructure); - } + $oMessage = new self; - public function InitByFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self - { if (!$oBodyStructure) { $oBodyStructure = $oFetchResponse->GetFetchBodyStructure(); @@ -299,12 +284,12 @@ class Message implements \JsonSerializable $sInternalDate = $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE); $aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS); - $this->sFolder = $sFolder; - $this->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); - $this->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE); - $this->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []); + $oMessage->sFolder = $sFolder; + $oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + $oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE); + $oMessage->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []); - $this->iInternalTimeStampInUTC = + $oMessage->iInternalTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseInternalDateString($sInternalDate); $sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : ''; @@ -331,33 +316,33 @@ class Message implements \JsonSerializable $bCharsetAutoDetect = !\strlen($sCharset); - $this->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect); - $this->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID); - $this->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE); + $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); - $this->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect); - $this->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect); - $this->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect); - $this->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect); + $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 ($this->oFrom) { - $oHeaders->PopulateEmailColectionByDkim($this->oFrom); + if ($oMessage->oFrom) { + $oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom); } - $this->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect); - $this->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect); - $this->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect); + $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); - $this->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO); - $this->sReferences = Utils::StripSpaces( + $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); - $this->sHeaderDate = $sHeaderDate; - $this->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate); + $oMessage->sHeaderDate = $sHeaderDate; + $oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate); // Priority - $this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; + $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; $sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY); if (!\strlen($sPriority)) { @@ -376,7 +361,7 @@ class Message implements \JsonSerializable case '2(high)': case '1': case '2': - $this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH; + $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH; break; case 'low': @@ -384,78 +369,78 @@ class Message implements \JsonSerializable case '5(lowest)': case '4': case '5': - $this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW; + $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW; break; } } // Delivery Receipt - $this->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO)); + $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO)); // Read Receipt - $this->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO)); - if (empty($this->sReadReceipt)) + $oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO)); + if (empty($oMessage->sReadReceipt)) { - $this->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO)); + $oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO)); } // Unsubscribe links - $this->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE); - if (empty($this->aUnsubsribeLinks)) + $oMessage->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE); + if (empty($oMessage->aUnsubsribeLinks)) { - $this->aUnsubsribeLinks = array(); + $oMessage->aUnsubsribeLinks = array(); } else { - $this->aUnsubsribeLinks = explode(',', $this->aUnsubsribeLinks); - $this->aUnsubsribeLinks = array_map( + $oMessage->aUnsubsribeLinks = explode(',', $oMessage->aUnsubsribeLinks); + $oMessage->aUnsubsribeLinks = array_map( function ($link) { return trim($link, ' <>'); }, - $this->aUnsubsribeLinks + $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])) { - $this->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($match[1]) / $threshold))); - $this->sSpamResult = "{$match[1]} / {$match[2]}"; + $oMessage->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($match[1]) / $threshold))); + $oMessage->sSpamResult = "{$match[1]} / {$match[2]}"; } } - $this->bIsSpam = false !== \stripos($this->sSubject, '*** SPAM ***'); + $oMessage->bIsSpam = false !== \stripos($oMessage->sSubject, '*** SPAM ***'); } else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_BOGOSITY)) { - $this->sSpamResult = $spam; - $this->bIsSpam = !!\preg_match('/yes|spam/', $spam); + $oMessage->sSpamResult = $spam; + $oMessage->bIsSpam = !!\preg_match('/yes|spam/', $spam); if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) { - $this->iSpamScore = \intval(\max(0, \min(100, \floatval($spamicity[1])))); + $oMessage->iSpamScore = \intval(\max(0, \min(100, \floatval($spamicity[1])))); } } else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_STATUS)) { - $this->sSpamResult = $spam; + $oMessage->sSpamResult = $spam; if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value) && \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) { if ($threshold = \floatval($required[1])) { - $this->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($value[1]) / $threshold))); - $this->sSpamResult = "{$value[1]} / {$required[1]}"; + $oMessage->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($value[1]) / $threshold))); + $oMessage->sSpamResult = "{$value[1]} / {$required[1]}"; } } - $this->bIsSpam = 'Yes' === \substr($spam, 0, 3); + $oMessage->bIsSpam = 'Yes' === \substr($spam, 0, 3); // $spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAM_FLAG); -// $this->bIsSpam = false !== \stripos($spam, 'YES'); +// $oMessage->bIsSpam = false !== \stripos($spam, 'YES'); } if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS)) { - $this->bHasVirus = true; + $oMessage->bHasVirus = true; } if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) { if (false !== \stripos($spam, 'infected')) { - $this->bHasVirus = true; + $oMessage->bHasVirus = true; } else if (false !== \stripos($spam, 'clean')) { - $this->bHasVirus = false; + $oMessage->bHasVirus = false; } } if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED)) { - $this->sVirusScanned = $virus; + $oMessage->sVirusScanned = $virus; } $sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO); @@ -481,7 +466,7 @@ class Message implements \JsonSerializable } if (\strlen($sType) && \strlen($sFolder) && $iUid) { - $this->aDraftInfo = array($sType, $iUid, $sFolder); + $oMessage->aDraftInfo = array($sType, $iUid, $sFolder); } } } @@ -490,119 +475,141 @@ class Message implements \JsonSerializable $sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::ISO_8859_1; // date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id - $this->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, ''); - $this->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset); + $oMessage->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, ''); + $oMessage->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset); - $this->oFrom = $oFetchResponse->GetFetchEnvelopeEmailCollection(2, $sCharset); - $this->oSender = $oFetchResponse->GetFetchEnvelopeEmailCollection(3, $sCharset); - $this->oReplyTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(4, $sCharset); - $this->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset); - $this->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset); - $this->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset); - $this->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, ''); - } - - // Content-Type: multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature" - if ('multipart/signed' === \strtolower($this->sContentType) - && 'application/pgp-signature' === \strtolower($oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::PROTOCOL))) - { - $gPgpSignatureParts = $oBodyStructure ? $oBodyStructure->SearchByContentType('application/pgp-signature') : null; - $this->bPgpSigned = $gPgpSignatureParts && $gPgpSignatureParts->valid(); - if ($this->bPgpSigned) { - $sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$gPgpSignatureParts->current()->PartID().']'); - if (\is_string($sPgpSignatureText) && \strlen($sPgpSignatureText) && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) { - $this->sPgpSignature = \trim($sPgpSignatureText); - $this->sPgpSignatureMicAlg = (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg'); - } - } + $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, ''); } // Content-Type: multipart/encrypted; protocol="application/pgp-encrypted" - $this->bPgpEncrypted = ('multipart/encrypted' === \strtolower($this->sContentType) + $oMessage->bPgpEncrypted = ('multipart/encrypted' === \strtolower($oMessage->sContentType) && 'application/pgp-encrypted' === \strtolower($oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::PROTOCOL))); - $aTextParts = $oBodyStructure ? $oBodyStructure->GetHtmlAndPlainParts() : null; - - 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) - { - $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>'); - if (\is_string($sText) && \strlen($sText)) - { - $this->bTextPartIsTrimmed = true; - } - } - - if (\is_string($sText) && \strlen($sText)) - { - $sTextCharset = $oPart->Charset(); - if (empty($sTextCharset)) - { - $sTextCharset = $sCharset; - } - - $sTextCharset = Utils::NormalizeCharset($sTextCharset, true); - - $sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName()); - $sText = Utils::ConvertEncoding($sText, $sTextCharset, \MailSo\Base\Enumerations\Charset::UTF_8); - $sText = Utils::Utf8Clear($sText); - - if ('text/html' === $oPart->ContentType()) - { - $aHtmlParts[] = $sText; - } - else - { - if ($oPart->IsFlowedFormat()) - { - $sText = Utils::DecodeFlowedFormat($sText); - } - - $aPlainParts[] = $sText; - } - } - } - - $this->sHtml = \implode('
', $aHtmlParts); - $this->sPlain = \trim(\implode("\n", $aPlainParts)); - - $aMatch = array(); - if (!$this->bPgpSigned && \preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/ism', $this->sPlain, $aMatch) && !empty($aMatch[0])) - { - $this->sPgpSignature = \trim($aMatch[0]); - $this->bPgpSigned = true; - } - - $this->bPgpEncrypted = !$this->bPgpEncrypted && false !== \stripos($this->sPlain, '-----BEGIN PGP MESSAGE-----'); - - unset($aHtmlParts, $aPlainParts, $aMatch); - } - if ($oBodyStructure) { + $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); + foreach ($gSignatureParts as $oPart) { + if (!$oPart->IsPgpSigned()) { + continue; + } + $oPgpSignaturePart = $oBodyStructure->SubParts()[1]; + $oMessage->aPgpSigned = [ + // /?/Raw/&q[]=/0/Download/&q[]=/... + // /?/Raw/&q[]=/0/View/&q[]=/... + 'BodyPartId' => $oBodyStructure->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'] = $oBodyStructure->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) + { + $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>'); + if (\is_string($sText) && \strlen($sText)) + { + $oMessage->bTextPartIsTrimmed = true; + } + } + + if (\is_string($sText) && \strlen($sText)) + { + $sTextCharset = $oPart->Charset(); + if (empty($sTextCharset)) + { + $sTextCharset = $sCharset; + } + + $sTextCharset = Utils::NormalizeCharset($sTextCharset, true); + + $sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName()); + $sText = Utils::ConvertEncoding($sText, $sTextCharset, \MailSo\Base\Enumerations\Charset::UTF_8); + $sText = Utils::Utf8Clear($sText); + + if ('text/html' === $oPart->ContentType()) + { + $aHtmlParts[] = $sText; + } + else + { + if ($oPart->IsFlowedFormat()) + { + $sText = Utils::DecodeFlowedFormat($sText); + } + + $aPlainParts[] = $sText; + } + } + } + + $oMessage->sHtml = \implode('
', $aHtmlParts); + $oMessage->sPlain = \trim(\implode("\n", $aPlainParts)); + + $aMatch = array(); + if (!$oMessage->aPgpSigned && \preg_match('/-----BEGIN PGP SIGNATURE-----.+?-----END PGP SIGNATURE-----/ism', $oMessage->sPlain, $aMatch)) + { + $oMessage->aPgpSigned = [ + // /?/Raw/&q[]=/0/Download/&q[]=/... + // /?/Raw/&q[]=/0/View/&q[]=/... + 'BodyPartId' => 0, + 'SigPartId' => 0, + 'MicAlg' => '', + 'Signature' => \trim($aMatch[0]) + ]; + } + + $oMessage->bPgpEncrypted = !$oMessage->bPgpEncrypted && false !== \stripos($oMessage->sPlain, '-----BEGIN PGP MESSAGE-----'); + + unset($aHtmlParts, $aPlainParts, $aMatch); + } + $gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts(); if ($gAttachmentsParts->valid()) { - $this->oAttachments = new AttachmentCollection; + $oMessage->oAttachments = new AttachmentCollection; foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) { - $this->oAttachments->append( - Attachment::NewBodyStructureInstance($this->sFolder, $this->iUid, $oAttachmentItem) + $oMessage->oAttachments->append( + Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem) ); } } } - return $this; + return $oMessage; } public function jsonSerialize() diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 707400ac2..fc8bf4a70 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -254,10 +254,9 @@ trait Response $mResult['Plain'] = $mResponse->Plain(); - $mResult['isPgpSigned'] = $mResponse->isPgpSigned(); +// $this->GetCapa(false, Capa::OPEN_PGP) $mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted(); -// $mResult['PgpSignature'] = $mResponse->PgpSignature(); -// $mResult['PgpSignatureMicAlg'] = $mResponse->PgpSignatureMicAlg(); + $mResult['PgpSigned'] = $mResponse->PgpSigned(); $mResult['HasExternals'] = $bHasExternals; $mResult['HasInternals'] = \count($aFoundCIDs) || \count($aFoundContentLocationUrls); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php new file mode 100644 index 000000000..9e4568e76 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php @@ -0,0 +1,188 @@ +doRequest('GET', "{$host}/pks/lookup?op={$op}&options=mr{$fingerprint}&search={$search}"); + } + + private static $HTTP; + private static function HTTP() : \SnappyMail\HTTP\Request + { + if (!static::$HTTP) { + static::$HTTP = \SnappyMail\HTTP\Request::factory(/*'socket' or 'curl'*/); + static::$HTTP->max_response_kb = 0; + static::$HTTP->timeout = 15; // timeout in seconds. + } + return static::$HTTP; + } + + /** + * Request the public key from the hkp servers + * Returns PGP PUBLIC KEY BLOCK + */ + public static function get(string $keyId) : string + { + // add the 0x prefix if absent + if ('0x' !== \substr($keyId, 0, 2)) { + $keyId = '0x' . $keyId; + } + + foreach ($this->keyservers as $host) { + $oResponse = static::fetch($host, 'get', $keyId); + if (!$oResponse) { + \SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}"); + continue; + } + if (200 !== $oResponse->status) { + \SnappyMail\Log::info('PGP', "{$oResponse->status} for key {$keyId} on {$host}"); + continue; + } + + return $oResponse->body; + } + + throw new \Exception('Could not obtain public key from the keyserver.'); + } + + /** + * Returns all matching keys found on a public keyserver. + * + * @param string $search String to search for (usually an email, name, or username). + * @param bool $fingerprint Provide the key fingerprint for each key. + * @param bool $exact Instruct the server to search for an exact match. + * + * @throws Exception + */ + public static function index(string $search, bool $fingerprint = true, bool $exact = false) : array + { + $keys = []; + foreach ($this->keyservers as $host) { + $oResponse = static::fetch($host, 'index', $search, $fingerprint, $exact); + if (!$oResponse) { + \SnappyMail\Log::info('PGP', "No response for key {$keyId} on {$host}"); + continue; + } + if (200 !== $oResponse->status) { + \SnappyMail\Log::info('PGP', "{$oResponse->status} for search `{$search}` on {$host}"); + continue; + } + + $result = \explode("\n", $oResponse->body); + foreach ($result as $line) { + // https://datatracker.ietf.org/doc/html/draft-shaw-openpgp-hkp-00#section-5.2 + $line = \explode(':', $line); + // pub:::::: + if ('pub' === $line[0]) { + if ($curKey) { + $keys[] = $curKey; + $curKey = null; + } + // Ignore invalid line + if (7 !== \count($line)) { + \SnappyMail\Log::info('PGP', "Invalid pub line for search `{$search}` on {$host}"); + continue; + } + // Ignore flagged or expired key + if (!empty($line[6]) || (!empty($line[5]) && $line[5] <= time())) { + continue; + } + $keyids[$line[4]] = $line[1]; + $curKey = [ + 'keyid' => $line[1], + 'host' => $host, + 'algo' => \intval($line[2]), // https://datatracker.ietf.org/doc/html/rfc2440#section-9.1 + 'keylen' => \intval($line[3]), + 'creationdate' => \strlen($line[4]) ? \intval($line[4]) : null, + 'expirationdate' => \strlen($line[5]) ? \intval($line[5]) : null, +// 'revoked' => \str_contains($line[6], 'r'), +// 'disabled' => \str_contains($line[6], 'd'), +// 'expired' => \str_contains($line[6], 'e'), + 'uids' => [], + ]; + } + // uid:::: + else if ('uid' === $line[0] && $curKey) { + // Ignore invalid line + if (5 !== \count($line)) { + \SnappyMail\Log::info('PGP', "Invalid uid line for search `{$search}` on {$host}"); + continue; + } + // Ignore flagged or expired key + if (!empty($line[4]) || (!empty($line[3]) && $line[3] <= time())) { + continue; + } + $curKey['uids'][] = [ + 'uid' => \urldecode($line[1]), + 'creationdate' => \strlen($line[2]) ? \intval($line[2]) : null, + 'expirationdate' => \strlen($line[3]) ? \intval($line[3]) : null, +// 'revoked' => \str_contains($line[4], 'r'), +// 'disabled' => \str_contains($line[4], 'd'), +// 'expired' => \str_contains($line[4], 'e'), + ]; + } + } + + if ($curKey) { + $keys[] = $curKey; + } + + return $keys; + } + + throw new \Exception('Could not obtain public key from the keyservers'); + } + + /** + * Sends a PGP public key to a public keyserver. + * + * @param string $host + * @param mixed $key The PGP public key. + * + * @throws Exception + */ + public static function add(string $host, string $key) + { +/* + $key = PublicKey::create($key); + + if (!$this->get($key->id)) { + $keytext = \urlencode(\trim($key)); + static::HTTP()->doRequest('POST', "{$host}/pks/add", 'keytext=' . $keytext, [ + 'Content-Type: application/x-www-form-urlencoded', + 'Content-Length: ' . \strlen($keytext), + 'Connection: close' + ]); + } +*/ + } +} diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html index 4bc0ab01b..946c65416 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -320,6 +320,15 @@ +
+ 🔒 + +
+
+ 🔒 + +
+