mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-01-01 04:22:15 +08:00
Changes for #89
Now it does not fetch the PGP signature, because validation was broken anyway. Instead it validates multipart/signed according to RFC 3156 section 5 and returns details for the signed part: * BodyPartId * SigPartId * MicAlg So in the future several implementations (GnuPG, OpenPGP.js, etc.) can use the correct data for verification.
This commit is contained in:
parent
ba49d06d1a
commit
8dcd0cf833
14 changed files with 514 additions and 356 deletions
|
@ -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);
|
||||
|
|
|
@ -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<a href="$2" target="_blank">$2</a>')
|
||||
|
@ -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')) {
|
||||
|
|
|
@ -370,7 +370,7 @@ export const MessageUserStore = new class {
|
|||
message.hasImages(body.rlHasImages);
|
||||
} else {
|
||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||
+ (message.isPgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (message.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (message.isPgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
+ '">'
|
||||
+ '</div>');
|
||||
|
|
|
@ -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('<div class="b-openpgp-control"><i class="fontastic">🔒</i></div>');
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[<section>] 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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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('<br />', $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('<br />', $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()
|
||||
|
|
|
@ -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);
|
||||
|
|
188
snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php
Normal file
188
snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
/**
|
||||
* https://datatracker.ietf.org/doc/html/draft-shaw-openpgp-hkp-00
|
||||
|
||||
* The last IETF draft for HKP also defines a distributed key server network, based on DNS SRV records:
|
||||
* to find the key of someone@example.com, one can ask it by requesting example.com's key server.
|
||||
*
|
||||
* https://datatracker.ietf.org/doc/html/rfc4387
|
||||
* https://datatracker.ietf.org/doc/html/rfc7929
|
||||
*/
|
||||
|
||||
namespace SnappyMail\PGP;
|
||||
|
||||
abstract class Keyservers
|
||||
{
|
||||
public static $keyservers = [
|
||||
/*
|
||||
'hkp://keys.gnupg.net',
|
||||
'hkps://keyserver.ubuntu.com',
|
||||
keys.openpgp.org
|
||||
pgp.mit.edu
|
||||
keyring.debian.org
|
||||
keyserver.ubuntu.com
|
||||
attester.flowcrypt.com
|
||||
zimmermann.mayfirst.org
|
||||
*/
|
||||
'https://keys.fedoraproject.org'
|
||||
];
|
||||
|
||||
private static function fetch(string $host, string $op, string $search, bool $fingerprint = false, bool $exact = false) : ?Response
|
||||
{
|
||||
$search = \urlencode($search);
|
||||
$fingerprint = $fingerprint ? '&fingerprint=on' : '';
|
||||
$exact = $exact ? '&exact=on' : '';
|
||||
return static::HTTP()->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:<keyid>:<algo>:<keylen>:<creationdate>:<expirationdate>:<flags>
|
||||
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:<escaped uid string>:<creationdate>:<expirationdate>:<flags>
|
||||
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'
|
||||
]);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
|
@ -320,6 +320,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="b-openpgp-control" data-bind="visible: pgpEncrypted, click: pgpDecrypt">
|
||||
<i class="fontastic">🔒</i>
|
||||
<span data-i18n="MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC"></span>
|
||||
</div>
|
||||
<div class="b-openpgp-control" data-bind="visible: pgpSigned, click: pgpVerify">
|
||||
<i class="fontastic">🔒</i>
|
||||
<span data-i18n="MESSAGE/PGP_SIGNED_MESSAGE_DESC"></span>
|
||||
</div>
|
||||
|
||||
<div class="bodyText"
|
||||
data-bind="initDom: messagesBodiesDom"></div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue