mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03: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() {
|
reloadOpenPgpKeys() {
|
||||||
if (PgpUserStore.capaOpenPGP()) {
|
if (PgpUserStore.openpgp) {
|
||||||
const keys = [],
|
const keys = [],
|
||||||
email = new EmailModel(),
|
email = new EmailModel(),
|
||||||
openpgpKeyring = PgpUserStore.openpgpKeyring,
|
openpgpKeyring = PgpUserStore.openpgpKeyring,
|
||||||
|
|
@ -784,7 +784,6 @@ class AppUser extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PgpUserStore.openpgpKeyring = new openpgp.Keyring();
|
PgpUserStore.openpgpKeyring = new openpgp.Keyring();
|
||||||
PgpUserStore.capaOpenPGP(true);
|
|
||||||
this.reloadOpenPgpKeys();
|
this.reloadOpenPgpKeys();
|
||||||
};
|
};
|
||||||
script.onerror = () => console.error(script.src);
|
script.onerror = () => console.error(script.src);
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||||
|
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
|
||||||
|
|
||||||
const
|
const
|
||||||
/*eslint-disable max-len*/
|
/*eslint-disable max-len*/
|
||||||
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
|
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,
|
hasImages: false,
|
||||||
hasExternals: false,
|
hasExternals: false,
|
||||||
|
|
||||||
isPgpSigned: false,
|
pgpSigned: null, // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
|
||||||
isPgpEncrypted: false,
|
isPgpEncrypted: false,
|
||||||
pgpSignedVerifyStatus: SignedVerifyStatus.None,
|
pgpSignedVerifyStatus: SignedVerifyStatus.None,
|
||||||
pgpSignedVerifyUser: '',
|
pgpSignedVerifyUser: '',
|
||||||
|
|
@ -181,7 +179,7 @@ export class MessageModel extends AbstractModel {
|
||||||
this.hasExternals(false);
|
this.hasExternals(false);
|
||||||
this.attachments(new AttachmentCollectionModel);
|
this.attachments(new AttachmentCollectionModel);
|
||||||
|
|
||||||
this.isPgpSigned(false);
|
this.pgpSigned(null);
|
||||||
this.isPgpEncrypted(false);
|
this.isPgpEncrypted(false);
|
||||||
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
|
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
|
||||||
this.pgpSignedVerifyUser('');
|
this.pgpSignedVerifyUser('');
|
||||||
|
|
@ -404,7 +402,7 @@ export class MessageModel extends AbstractModel {
|
||||||
viewHtml() {
|
viewHtml() {
|
||||||
const body = this.body;
|
const body = this.body;
|
||||||
if (body && this.html()) {
|
if (body && this.html()) {
|
||||||
let html = this.html().toString()
|
let html = this.html()
|
||||||
.replace(/font-size:\s*[0-9]px/g, 'font-size:11px')
|
.replace(/font-size:\s*[0-9]px/g, 'font-size:11px')
|
||||||
// Strip utm_* tracking
|
// Strip utm_* tracking
|
||||||
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1');
|
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1');
|
||||||
|
|
@ -465,7 +463,7 @@ export class MessageModel extends AbstractModel {
|
||||||
if (body && this.plain()) {
|
if (body && this.plain()) {
|
||||||
body.classList.toggle('html', 0);
|
body.classList.toggle('html', 0);
|
||||||
body.classList.toggle('plain', 1);
|
body.classList.toggle('plain', 1);
|
||||||
body.innerHTML = plainToHtml(this.plain().toString())
|
body.innerHTML = plainToHtml(this.plain())
|
||||||
// Strip utm_* tracking
|
// Strip utm_* tracking
|
||||||
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1')
|
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1')
|
||||||
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
|
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
|
||||||
|
|
@ -479,8 +477,6 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
initView() {
|
initView() {
|
||||||
PgpUserStore.initMessageBodyControls(this.body, this);
|
|
||||||
|
|
||||||
// init BlockquoteSwitcher
|
// init BlockquoteSwitcher
|
||||||
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
|
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
|
||||||
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
|
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ export const MessageUserStore = new class {
|
||||||
message.hasImages(body.rlHasImages);
|
message.hasImages(body.rlHasImages);
|
||||||
} else {
|
} else {
|
||||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||||
+ (message.isPgpSigned() ? ' openpgp-signed' : '')
|
+ (message.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
+ (message.isPgpEncrypted() ? ' openpgp-encrypted' : '')
|
+ (message.isPgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
+ '">'
|
+ '">'
|
||||||
+ '</div>');
|
+ '</div>');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { i18n } from 'Common/Translator';
|
|
||||||
import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils';
|
import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils';
|
||||||
|
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
|
|
@ -9,132 +8,8 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp';
|
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 {
|
export const PgpUserStore = new class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.capaOpenPGP = ko.observable(false);
|
|
||||||
|
|
||||||
this.openpgp = null;
|
this.openpgp = null;
|
||||||
|
|
||||||
this.openpgpkeys = ko.observableArray();
|
this.openpgpkeys = ko.observableArray();
|
||||||
|
|
@ -335,28 +210,4 @@ export const PgpUserStore = new class {
|
||||||
return false;
|
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;
|
.b-openpgp-control {
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
opacity: 0.5;
|
|
||||||
margin: 15px;
|
|
||||||
|
|
||||||
&:hover {
|
color: #FA0;
|
||||||
opacity: 1;
|
cursor: pointer;
|
||||||
}
|
display: block;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin: 15px;
|
||||||
|
|
||||||
&.success {
|
&:hover {
|
||||||
color: green;
|
opacity: 1;
|
||||||
opacity: 1;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
&.success {
|
||||||
color: red;
|
color: green;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: red;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
this.bSkipNextHide = false;
|
this.bSkipNextHide = false;
|
||||||
|
|
||||||
this.capaOpenPGP = PgpUserStore.capaOpenPGP;
|
this.capaOpenPGP = !!PgpUserStore.openpgp;
|
||||||
|
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
|
||||||
|
|
@ -552,7 +552,7 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
|
|
||||||
openOpenPgpPopup() {
|
openOpenPgpPopup() {
|
||||||
if (PgpUserStore.capaOpenPGP() && !this.oEditor.isHtml()) {
|
if (PgpUserStore.openpgp && !this.oEditor.isHtml()) {
|
||||||
showScreenPopup(ComposeOpenPgpPopupView, [
|
showScreenPopup(ComposeOpenPgpPopupView, [
|
||||||
result => this.editor(editor => editor.setPlain(result)),
|
result => this.editor(editor => editor.setPlain(result)),
|
||||||
this.oEditor.getData(false),
|
this.oEditor.getData(false),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,67 @@ import Remote from 'Remote/User/Fetch';
|
||||||
import { decorateKoCommands, createCommand } from 'Knoin/Knoin';
|
import { decorateKoCommands, createCommand } from 'Knoin/Knoin';
|
||||||
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
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 {
|
export class MailMessageView extends AbstractViewRight {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('MailMessageView');
|
super('MailMessageView');
|
||||||
|
|
@ -176,6 +237,12 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pgpSigned: () => PgpUserStore.openpgp
|
||||||
|
&& MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(),
|
||||||
|
|
||||||
|
pgpEncrypted: () => PgpUserStore.openpgp
|
||||||
|
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
|
||||||
|
|
||||||
messageListOrViewLoading:
|
messageListOrViewLoading:
|
||||||
() => MessageUserStore.listIsLoading() | MessageUserStore.messageLoading()
|
() => MessageUserStore.listIsLoading() | MessageUserStore.messageLoading()
|
||||||
});
|
});
|
||||||
|
|
@ -614,4 +681,14 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
|
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;
|
return $this->sLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function SubParts() : array
|
||||||
|
{
|
||||||
|
return $this->aSubParts;
|
||||||
|
}
|
||||||
|
|
||||||
public function IsInline() : bool
|
public function IsInline() : bool
|
||||||
{
|
{
|
||||||
return 'inline' === $this->sDisposition || \strlen($this->sContentID);
|
return 'inline' === $this->sDisposition || \strlen($this->sContentID);
|
||||||
|
|
@ -163,6 +168,17 @@ class BodyStructure
|
||||||
return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
|
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
|
public function IsPgpSignature() : bool
|
||||||
{
|
{
|
||||||
return \in_array($this->sContentType,
|
return \in_array($this->sContentType,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ namespace MailSo\Imap\Enumerations;
|
||||||
* @category MailSo
|
* @category MailSo
|
||||||
* @package Imap
|
* @package Imap
|
||||||
* @subpackage Enumerations
|
* @subpackage Enumerations
|
||||||
|
*
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.5
|
||||||
*/
|
*/
|
||||||
abstract class FetchType
|
abstract class FetchType
|
||||||
{
|
{
|
||||||
|
|
@ -25,18 +27,25 @@ abstract class FetchType
|
||||||
// Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
|
// Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)
|
||||||
const FULL = 'FULL';
|
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';
|
const BODY = 'BODY';
|
||||||
|
// An alternate form of BODY[<section>] that does not implicitly set the \Seen flag.
|
||||||
const BODY_PEEK = 'BODY.PEEK';
|
const BODY_PEEK = 'BODY.PEEK';
|
||||||
|
// The text of a particular body section.
|
||||||
const BODY_HEADER = 'BODY[HEADER]';
|
const BODY_HEADER = 'BODY[HEADER]';
|
||||||
const BODY_HEADER_PEEK = 'BODY.PEEK[HEADER]';
|
const BODY_HEADER_PEEK = 'BODY.PEEK[HEADER]';
|
||||||
const BODYSTRUCTURE = 'BODYSTRUCTURE';
|
const BODYSTRUCTURE = 'BODYSTRUCTURE';
|
||||||
const ENVELOPE = 'ENVELOPE';
|
const ENVELOPE = 'ENVELOPE';
|
||||||
const FLAGS = 'FLAGS';
|
const FLAGS = 'FLAGS';
|
||||||
const INTERNALDATE = 'INTERNALDATE';
|
const INTERNALDATE = 'INTERNALDATE';
|
||||||
const RFC822 = 'RFC822';
|
// const RFC822 = 'RFC822'; // Functionally equivalent to BODY[]
|
||||||
const RFC822_HEADER = 'RFC822.HEADER';
|
// const RFC822_HEADER = 'RFC822.HEADER'; // Functionally equivalent to BODY.PEEK[HEADER]
|
||||||
const RFC822_SIZE = 'RFC822.SIZE';
|
const RFC822_SIZE = 'RFC822.SIZE';
|
||||||
const RFC822_TEXT = 'RFC822.TEXT';
|
// const RFC822_TEXT = 'RFC822.TEXT'; // Functionally equivalent to BODY[TEXT]
|
||||||
const UID = 'UID';
|
const UID = 'UID';
|
||||||
// RFC 3516
|
// RFC 3516
|
||||||
const BINARY = 'BINARY';
|
const BINARY = 'BINARY';
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ class MailClient
|
||||||
|
|
||||||
$aFetchItems = array(
|
$aFetchItems = array(
|
||||||
FetchType::UID,
|
FetchType::UID,
|
||||||
|
// FetchType::FAST,
|
||||||
FetchType::RFC822_SIZE,
|
FetchType::RFC822_SIZE,
|
||||||
FetchType::INTERNALDATE,
|
FetchType::INTERNALDATE,
|
||||||
FetchType::FLAGS,
|
FetchType::FLAGS,
|
||||||
|
|
@ -180,12 +181,18 @@ class MailClient
|
||||||
|
|
||||||
$aFetchItems[] = $sLine;
|
$aFetchItems[] = $sLine;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
$gSignatureParts = $oBodyStructure->SearchByContentType('application/pgp-signature');
|
$gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed');
|
||||||
foreach ($gSignatureParts as $oPart)
|
foreach ($gSignatureParts as $oPart) {
|
||||||
{
|
if ($oPart->IsPgpSigned()) {
|
||||||
$aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->PartID().']';
|
// 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);
|
$aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid);
|
||||||
if (\count($aFetchResponse))
|
if (\count($aFetchResponse))
|
||||||
{
|
{
|
||||||
$oMessage = Message::NewFetchResponseInstance(
|
$oMessage = Message::NewFetchResponseInstance($sFolderName, $aFetchResponse[0], $oBodyStructure);
|
||||||
$sFolderName, $aFetchResponse[0], $oBodyStructure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $oMessage;
|
return $oMessage;
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ class Message implements \JsonSerializable
|
||||||
|
|
||||||
$bTextPartIsTrimmed = false,
|
$bTextPartIsTrimmed = false,
|
||||||
|
|
||||||
$sPgpSignature = '',
|
$aPgpSigned = null,
|
||||||
$sPgpSignatureMicAlg = '',
|
|
||||||
$bPgpSigned = false,
|
|
||||||
$bPgpEncrypted = false;
|
$bPgpEncrypted = false;
|
||||||
|
|
||||||
function __construct()
|
function __construct()
|
||||||
|
|
@ -99,19 +97,9 @@ class Message implements \JsonSerializable
|
||||||
return $this->sHtml;
|
return $this->sHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function PgpSignature() : string
|
public function PgpSigned() : ?array
|
||||||
{
|
{
|
||||||
return $this->sPgpSignature;
|
return $this->aPgpSigned;
|
||||||
}
|
|
||||||
|
|
||||||
public function PgpSignatureMicAlg() : string
|
|
||||||
{
|
|
||||||
return $this->sPgpSignatureMicAlg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isPgpSigned() : bool
|
|
||||||
{
|
|
||||||
return $this->bPgpSigned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPgpEncrypted() : bool
|
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
|
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)
|
if (!$oBodyStructure)
|
||||||
{
|
{
|
||||||
$oBodyStructure = $oFetchResponse->GetFetchBodyStructure();
|
$oBodyStructure = $oFetchResponse->GetFetchBodyStructure();
|
||||||
|
|
@ -299,12 +284,12 @@ class Message implements \JsonSerializable
|
||||||
$sInternalDate = $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE);
|
$sInternalDate = $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE);
|
||||||
$aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS);
|
$aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS);
|
||||||
|
|
||||||
$this->sFolder = $sFolder;
|
$oMessage->sFolder = $sFolder;
|
||||||
$this->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
$oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
|
||||||
$this->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE);
|
$oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE);
|
||||||
$this->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []);
|
$oMessage->aFlagsLowerCase = \array_map('strtolower', $aFlags ?: []);
|
||||||
|
|
||||||
$this->iInternalTimeStampInUTC =
|
$oMessage->iInternalTimeStampInUTC =
|
||||||
\MailSo\Base\DateTimeHelper::ParseInternalDateString($sInternalDate);
|
\MailSo\Base\DateTimeHelper::ParseInternalDateString($sInternalDate);
|
||||||
|
|
||||||
$sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : '';
|
$sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : '';
|
||||||
|
|
@ -331,33 +316,33 @@ class Message implements \JsonSerializable
|
||||||
|
|
||||||
$bCharsetAutoDetect = !\strlen($sCharset);
|
$bCharsetAutoDetect = !\strlen($sCharset);
|
||||||
|
|
||||||
$this->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect);
|
$oMessage->sSubject = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::SUBJECT, $bCharsetAutoDetect);
|
||||||
$this->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID);
|
$oMessage->sMessageId = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::MESSAGE_ID);
|
||||||
$this->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
|
$oMessage->sContentType = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
|
||||||
|
|
||||||
$this->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect);
|
$oMessage->oFrom = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::FROM_, $bCharsetAutoDetect);
|
||||||
$this->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect);
|
$oMessage->oTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::TO_, $bCharsetAutoDetect);
|
||||||
$this->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
|
$oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
|
||||||
$this->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
|
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
|
||||||
|
|
||||||
if ($this->oFrom) {
|
if ($oMessage->oFrom) {
|
||||||
$oHeaders->PopulateEmailColectionByDkim($this->oFrom);
|
$oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
|
$oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
|
||||||
$this->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
|
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
|
||||||
$this->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
|
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
|
||||||
|
|
||||||
$this->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
|
$oMessage->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
|
||||||
$this->sReferences = Utils::StripSpaces(
|
$oMessage->sReferences = Utils::StripSpaces(
|
||||||
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
|
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
|
||||||
|
|
||||||
$sHeaderDate = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE);
|
$sHeaderDate = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE);
|
||||||
$this->sHeaderDate = $sHeaderDate;
|
$oMessage->sHeaderDate = $sHeaderDate;
|
||||||
$this->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate);
|
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate);
|
||||||
|
|
||||||
// Priority
|
// 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);
|
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY);
|
||||||
if (!\strlen($sPriority))
|
if (!\strlen($sPriority))
|
||||||
{
|
{
|
||||||
|
|
@ -376,7 +361,7 @@ class Message implements \JsonSerializable
|
||||||
case '2(high)':
|
case '2(high)':
|
||||||
case '1':
|
case '1':
|
||||||
case '2':
|
case '2':
|
||||||
$this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH;
|
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'low':
|
case 'low':
|
||||||
|
|
@ -384,78 +369,78 @@ class Message implements \JsonSerializable
|
||||||
case '5(lowest)':
|
case '5(lowest)':
|
||||||
case '4':
|
case '4':
|
||||||
case '5':
|
case '5':
|
||||||
$this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW;
|
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery Receipt
|
// 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
|
// Read Receipt
|
||||||
$this->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
|
$oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
|
||||||
if (empty($this->sReadReceipt))
|
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
|
// Unsubscribe links
|
||||||
$this->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
|
$oMessage->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
|
||||||
if (empty($this->aUnsubsribeLinks))
|
if (empty($oMessage->aUnsubsribeLinks))
|
||||||
{
|
{
|
||||||
$this->aUnsubsribeLinks = array();
|
$oMessage->aUnsubsribeLinks = array();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$this->aUnsubsribeLinks = explode(',', $this->aUnsubsribeLinks);
|
$oMessage->aUnsubsribeLinks = explode(',', $oMessage->aUnsubsribeLinks);
|
||||||
$this->aUnsubsribeLinks = array_map(
|
$oMessage->aUnsubsribeLinks = array_map(
|
||||||
function ($link) {
|
function ($link) {
|
||||||
return trim($link, ' <>');
|
return trim($link, ' <>');
|
||||||
},
|
},
|
||||||
$this->aUnsubsribeLinks
|
$oMessage->aUnsubsribeLinks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAMD_RESULT)) {
|
if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_SPAMD_RESULT)) {
|
||||||
if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) {
|
if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) {
|
||||||
if ($threshold = \floatval($match[2])) {
|
if ($threshold = \floatval($match[2])) {
|
||||||
$this->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($match[1]) / $threshold)));
|
$oMessage->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($match[1]) / $threshold)));
|
||||||
$this->sSpamResult = "{$match[1]} / {$match[2]}";
|
$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)) {
|
} else if ($spam = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_BOGOSITY)) {
|
||||||
$this->sSpamResult = $spam;
|
$oMessage->sSpamResult = $spam;
|
||||||
$this->bIsSpam = !!\preg_match('/yes|spam/', $spam);
|
$oMessage->bIsSpam = !!\preg_match('/yes|spam/', $spam);
|
||||||
if (\preg_match('/spamicity=([\\d\\.]+)/', $spam, $spamicity)) {
|
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)) {
|
} 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)
|
if (\preg_match('/(?:hits|score)=([\\d\\.-]+)/', $spam, $value)
|
||||||
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
|
&& \preg_match('/required=([\\d\\.-]+)/', $spam, $required)) {
|
||||||
if ($threshold = \floatval($required[1])) {
|
if ($threshold = \floatval($required[1])) {
|
||||||
$this->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($value[1]) / $threshold)));
|
$oMessage->iSpamScore = \intval(\max(0, \min(100, 100 * \floatval($value[1]) / $threshold)));
|
||||||
$this->sSpamResult = "{$value[1]} / {$required[1]}";
|
$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);
|
// $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)) {
|
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 ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_STATUS)) {
|
||||||
if (false !== \stripos($spam, 'infected')) {
|
if (false !== \stripos($spam, 'infected')) {
|
||||||
$this->bHasVirus = true;
|
$oMessage->bHasVirus = true;
|
||||||
} else if (false !== \stripos($spam, 'clean')) {
|
} else if (false !== \stripos($spam, 'clean')) {
|
||||||
$this->bHasVirus = false;
|
$oMessage->bHasVirus = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($virus = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_VIRUS_SCANNED)) {
|
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);
|
$sDraftInfo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_DRAFT_INFO);
|
||||||
|
|
@ -481,7 +466,7 @@ class Message implements \JsonSerializable
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\strlen($sType) && \strlen($sFolder) && $iUid) {
|
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;
|
$sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::ISO_8859_1;
|
||||||
|
|
||||||
// date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id
|
// date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id
|
||||||
$this->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, '');
|
$oMessage->sMessageId = $oFetchResponse->GetFetchEnvelopeValue(9, '');
|
||||||
$this->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset);
|
$oMessage->sSubject = Utils::DecodeHeaderValue($oFetchResponse->GetFetchEnvelopeValue(1, ''), $sCharset);
|
||||||
|
|
||||||
$this->oFrom = $oFetchResponse->GetFetchEnvelopeEmailCollection(2, $sCharset);
|
$oMessage->oFrom = $oFetchResponse->GetFetchEnvelopeEmailCollection(2, $sCharset);
|
||||||
$this->oSender = $oFetchResponse->GetFetchEnvelopeEmailCollection(3, $sCharset);
|
$oMessage->oSender = $oFetchResponse->GetFetchEnvelopeEmailCollection(3, $sCharset);
|
||||||
$this->oReplyTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(4, $sCharset);
|
$oMessage->oReplyTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(4, $sCharset);
|
||||||
$this->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset);
|
$oMessage->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset);
|
||||||
$this->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset);
|
$oMessage->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset);
|
||||||
$this->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset);
|
$oMessage->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset);
|
||||||
$this->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, '');
|
$oMessage->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"
|
// 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)));
|
&& '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)
|
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();
|
$gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts();
|
||||||
if ($gAttachmentsParts->valid())
|
if ($gAttachmentsParts->valid())
|
||||||
{
|
{
|
||||||
$this->oAttachments = new AttachmentCollection;
|
$oMessage->oAttachments = new AttachmentCollection;
|
||||||
foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem)
|
foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem)
|
||||||
{
|
{
|
||||||
$this->oAttachments->append(
|
$oMessage->oAttachments->append(
|
||||||
Attachment::NewBodyStructureInstance($this->sFolder, $this->iUid, $oAttachmentItem)
|
Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $oMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize()
|
public function jsonSerialize()
|
||||||
|
|
|
||||||
|
|
@ -254,10 +254,9 @@ trait Response
|
||||||
|
|
||||||
$mResult['Plain'] = $mResponse->Plain();
|
$mResult['Plain'] = $mResponse->Plain();
|
||||||
|
|
||||||
$mResult['isPgpSigned'] = $mResponse->isPgpSigned();
|
// $this->GetCapa(false, Capa::OPEN_PGP)
|
||||||
$mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted();
|
$mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted();
|
||||||
// $mResult['PgpSignature'] = $mResponse->PgpSignature();
|
$mResult['PgpSigned'] = $mResponse->PgpSigned();
|
||||||
// $mResult['PgpSignatureMicAlg'] = $mResponse->PgpSignatureMicAlg();
|
|
||||||
|
|
||||||
$mResult['HasExternals'] = $bHasExternals;
|
$mResult['HasExternals'] = $bHasExternals;
|
||||||
$mResult['HasInternals'] = \count($aFoundCIDs) || \count($aFoundContentLocationUrls);
|
$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>
|
</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"
|
<div class="bodyText"
|
||||||
data-bind="initDom: messagesBodiesDom"></div>
|
data-bind="initDom: messagesBodiesDom"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue