mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-06-28 06:46:27 +00:00
Automatically verify PGP and S/MIME signed messages
This commit is contained in:
@@ -120,12 +120,10 @@ export class MessageModel extends AbstractModel {
|
||||
encrypted: false,
|
||||
|
||||
pgpSigned: null,
|
||||
pgpVerified: null,
|
||||
pgpEncrypted: null,
|
||||
pgpDecrypted: false,
|
||||
|
||||
smimeSigned: null,
|
||||
smimeVerified: null,
|
||||
smimeEncrypted: null,
|
||||
smimeDecrypted: false,
|
||||
|
||||
@@ -199,10 +197,9 @@ export class MessageModel extends AbstractModel {
|
||||
}
|
||||
});
|
||||
|
||||
this.smimeSigned.subscribe(value => {
|
||||
value?.body && MimeToMessage(value.body, this);
|
||||
'verified' in value && this.smimeVerified(value.verified);
|
||||
});
|
||||
this.smimeSigned.subscribe(value =>
|
||||
value?.body && MimeToMessage(value.body, this)
|
||||
);
|
||||
}
|
||||
|
||||
get requestHash() {
|
||||
|
||||
@@ -583,8 +583,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||
MimeToMessage(result.data, oMessage);
|
||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||
if (result.signatures?.length) {
|
||||
oMessage.pgpSigned(true);
|
||||
oMessage.pgpVerified({
|
||||
oMessage.pgpSigned({
|
||||
signatures: result.signatures,
|
||||
success: !!result.signatures.length
|
||||
});
|
||||
@@ -603,7 +602,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
||||
PgpUserStore.verify(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpVerified(result);
|
||||
oMessage.pgpSigned(result);
|
||||
} else {
|
||||
alert('Verification failed or no valid public key found');
|
||||
}
|
||||
@@ -668,22 +667,22 @@ export class MailMessageView extends AbstractViewRight {
|
||||
}
|
||||
|
||||
smimeVerify(/*self, event*/) {
|
||||
const message = currentMessage();
|
||||
let data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||
const message = currentMessage(),
|
||||
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||
if (data) {
|
||||
data = { ...data }; // clone
|
||||
data.folder = message.folder;
|
||||
data.uid = message.uid;
|
||||
data.bodyPart = data.bodyPart?.raw;
|
||||
data.sigPart = data.sigPart?.bodyRaw;
|
||||
Remote.post('SMimeVerifyMessage', null, data).then(response => {
|
||||
const params = { ...data }; // clone
|
||||
params.folder = message.folder;
|
||||
params.uid = message.uid;
|
||||
params.bodyPart = data.bodyPart?.raw;
|
||||
params.sigPart = data.sigPart?.bodyRaw;
|
||||
Remote.post('SMimeVerifyMessage', null, params).then(response => {
|
||||
if (response?.Result) {
|
||||
if (response.Result.body) {
|
||||
MimeToMessage(response.Result.body, message);
|
||||
message.html() ? message.viewHtml() : message.viewPlain();
|
||||
response.Result.body = null;
|
||||
}
|
||||
message.smimeVerified(response.Result.success);
|
||||
data.success = response.Result.success;
|
||||
message.smimeSigned(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@ class MailClient
|
||||
$this->oImapClient->FolderExamine($sFolderName);
|
||||
|
||||
$oBodyStructure = null;
|
||||
$oMessage = null;
|
||||
|
||||
$aFetchItems = array(
|
||||
FetchType::UID,
|
||||
@@ -169,28 +168,10 @@ class MailClient
|
||||
}
|
||||
|
||||
$aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid);
|
||||
if (\count($aFetchResponse)) {
|
||||
$oMessage = Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure);
|
||||
// S/MIME signed. Verify it, so we have the raw mime body to show
|
||||
if ($oMessage->smimeSigned) try {
|
||||
$bOpaque = !$oMessage->smimeSigned['detached'];
|
||||
$sBody = $this->oImapClient->FetchMessagePart(
|
||||
$oMessage->Uid,
|
||||
$oMessage->smimeSigned['partId']
|
||||
);
|
||||
$result = (new \SnappyMail\SMime\OpenSSL(''))->verify($sBody, null, $bOpaque);
|
||||
if ($result) {
|
||||
if ($bOpaque) {
|
||||
$oMessage->smimeSigned['body'] = $result['body'];
|
||||
}
|
||||
$oMessage->smimeSigned['verified'] = $result['success'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $oMessage;
|
||||
return \count($aFetchResponse)
|
||||
? Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,7 +66,7 @@ class Message implements \JsonSerializable
|
||||
|
||||
private ?array $DraftInfo = null;
|
||||
|
||||
private ?array $pgpSigned = null;
|
||||
public ?array $pgpSigned = null;
|
||||
private ?array $pgpEncrypted = null;
|
||||
|
||||
public ?array $smimeSigned = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ use RainLoop\Exceptions\ClientException;
|
||||
use RainLoop\Model\Account;
|
||||
use RainLoop\Notifications;
|
||||
use MailSo\Imap\SequenceSet;
|
||||
use MailSo\Imap\Enumerations\FetchType;
|
||||
use MailSo\Imap\Enumerations\MessageFlag;
|
||||
use MailSo\Mime\Part as MimePart;
|
||||
use MailSo\Mime\Enumerations\Header as MimeEnumHeader;
|
||||
@@ -455,6 +456,63 @@ trait Messages
|
||||
try
|
||||
{
|
||||
$oMessage = $this->MailClient()->Message($sFolder, $iUid, true, $this->Cacher($oAccount));
|
||||
|
||||
// S/MIME signed. Verify it, so we have the raw mime body to show
|
||||
if ($oMessage->smimeSigned) try {
|
||||
$bOpaque = !$oMessage->smimeSigned['detached'];
|
||||
$sBody = $this->ImapClient()->FetchMessagePart(
|
||||
$oMessage->Uid,
|
||||
$oMessage->smimeSigned['partId']
|
||||
);
|
||||
$result = (new \SnappyMail\SMime\OpenSSL(''))->verify($sBody, null, $bOpaque);
|
||||
if ($result) {
|
||||
if ($bOpaque) {
|
||||
$oMessage->smimeSigned['body'] = $result['body'];
|
||||
}
|
||||
$oMessage->smimeSigned['success'] = $result['success'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
|
||||
if ($oMessage->pgpSigned) try {
|
||||
$GPG = $this->GnuPG();
|
||||
if ($GPG) {
|
||||
if ($oMessage->pgpSigned['sigPartId']) {
|
||||
$sPartId = $oMessage->pgpSigned['partId'];
|
||||
$sSigPartId = $oMessage->pgpSigned['sigPartId'];
|
||||
$aParts = [
|
||||
FetchType::BODY_PEEK.'['.$sPartId.']',
|
||||
// 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].
|
||||
FetchType::BODY_PEEK.'['.$sPartId.'.MIME]',
|
||||
FetchType::BODY_PEEK.'['.$sSigPartId.']'
|
||||
];
|
||||
$oFetchResponse = $this->ImapClient()->Fetch($aParts, $oMessage->Uid, true)[0];
|
||||
$sBodyMime = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.'.MIME]');
|
||||
$info = $this->GnuPG()->verify(
|
||||
\preg_replace('/\\r?\\n/su', "\r\n",
|
||||
$sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']')
|
||||
),
|
||||
\preg_replace('/[^\x00-\x7F]/', '',
|
||||
$oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// clearsigned text
|
||||
$info = $this->GnuPG()->verify($oMessage->sPlain, '');
|
||||
}
|
||||
if (!empty($info[0]) && 0 == $info[0]['status']) {
|
||||
$info = $info[0];
|
||||
$oMessage->pgpSigned = [
|
||||
'fingerprint' => $info['fingerprint'],
|
||||
'success' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logException($e);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
|
||||
@@ -329,7 +329,7 @@ trait Pgp
|
||||
'text' => \preg_replace('/\\r?\\n/su', "\r\n",
|
||||
$sBodyMime . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sPartId.']')
|
||||
),
|
||||
'signature' => preg_replace('/[^\x00-\x7F]/', '',
|
||||
'signature' => \preg_replace('/[^\x00-\x7F]/', '',
|
||||
$oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']')
|
||||
)
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ abstract class GnuPG
|
||||
return GPG::isSupported();
|
||||
}
|
||||
|
||||
private static $instance;
|
||||
private static $instance = null;
|
||||
public static function getInstance(string $homedir) : ?PGPInterface
|
||||
{
|
||||
if (!static::$instance) {
|
||||
|
||||
@@ -300,10 +300,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: message().pgpSigned()">
|
||||
<div class="crypto-control signed" data-bind="css: {success: message().pgpVerified()?.success, error: message().pgpVerified() && !message().pgpVerified().success}">
|
||||
<div class="crypto-control signed" data-bind="css: {success: true === message().pgpSigned()?.success, error: false === message().pgpSigned()?.success}">
|
||||
<span data-icon="✍" data-i18n="OPENPGP/SIGNED_MESSAGE"></span>
|
||||
<button class="btn" data-bind="visible: pgpSupported, click: pgpVerify" data-i18n="CRYPTO/VERIFY"></button>
|
||||
<div data-icon="⚠" data-bind="visible: message().pgpVerified()?.error, text: message().pgpVerified()?.error"></div>
|
||||
<div data-icon="⚠" data-bind="visible: message().pgpSigned()?.error, text: message().pgpSigned()?.error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -315,7 +315,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: message().smimeSigned()">
|
||||
<div class="crypto-control signed" data-bind="css: {success: true === message().smimeVerified(), error: false === message().smimeVerified()}">
|
||||
<div class="crypto-control signed" data-bind="css: {success: true === message().smimeSigned()?.success, error: false === message().smimeSigned()?.success}">
|
||||
<span data-icon="✍" data-i18n="SMIME/SIGNED_MESSAGE"></span>
|
||||
<button class="btn" data-bind="click: smimeVerify" data-i18n="CRYPTO/VERIFY"></button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user