Added signing using S/MIME #259

This commit is contained in:
the-djmaze 2024-02-19 04:35:37 +01:00
parent f83b60782a
commit 128e2f6254
10 changed files with 143 additions and 101 deletions

View file

@ -250,12 +250,16 @@ export class ComposePopupView extends AbstractViewPopup {
showBcc: false, showBcc: false,
showReplyTo: false, showReplyTo: false,
pgpSign: false, doSign: false,
doEncrypt: false,
canPgpSign: false, canPgpSign: false,
pgpEncrypt: false,
canPgpEncrypt: false, canPgpEncrypt: false,
canMailvelope: false, canMailvelope: false,
canSMimeSign: false,
canSMimeEncrypt: false,
draftsFolder: '', draftsFolder: '',
draftUid: 0, draftUid: 0,
sending: false, sending: false,
@ -330,6 +334,9 @@ export class ComposePopupView extends AbstractViewPopup {
attachmentsInProcessCount: () => this.attachmentsInProcess.length, attachmentsInProcessCount: () => this.attachmentsInProcess.length,
isDraft: () => this.draftsFolder() && this.draftUid(), isDraft: () => this.draftsFolder() && this.draftUid(),
canSign: () => this.canPgpSign() | this.canSMimeSign(),
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
identitiesOptions: () => identitiesOptions: () =>
IdentityUserStore.map(item => ({ IdentityUserStore.map(item => ({
item: item, item: item,
@ -349,9 +356,11 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => { currentIdentity: value => {
if (value) { if (value) {
const smime = !!(value.smimeKey() && value.smimeCertificate());
this.from(value.formattedName()); this.from(value.formattedName());
this.pgpEncrypt(value.pgpEncrypt()/* || SettingsUserStore.pgpEncrypt()*/); this.doEncrypt(value.pgpEncrypt()/* || SettingsUserStore.pgpEncrypt()*/);
this.pgpSign(value.pgpSign()/* || SettingsUserStore.pgpSign()*/); this.doSign(smime || value.pgpSign()/* || SettingsUserStore.pgpSign()*/);
this.canSMimeSign(smime);
} }
}, },
@ -361,9 +370,9 @@ export class ComposePopupView extends AbstractViewPopup {
value && PgpUserStore.getKeyForSigning(value).then(result => { value && PgpUserStore.getKeyForSigning(value).then(result => {
console.log({ console.log({
email: value, email: value,
canPgpSign:!!result canPgpSign:result
}); });
this.canPgpSign(!!result) this.canPgpSign(result)
}); });
this.initPgpEncrypt(); this.initPgpEncrypt();
}, },
@ -426,7 +435,7 @@ export class ComposePopupView extends AbstractViewPopup {
case 'K': quota *= 1024; case 'K': quota *= 1024;
} }
// Issue: can't select signing key // Issue: can't select signing key
// this.pgpSign(this.pgpSign() || confirm('Sign this message?')); // this.doSign(this.doSign() || confirm('Sign this message?'));
mailvelope.createEditorContainer('#mailvelope-editor', PgpUserStore.mailvelopeKeyring, { mailvelope.createEditorContainer('#mailvelope-editor', PgpUserStore.mailvelopeKeyring, {
// https://mailvelope.github.io/mailvelope/global.html#EditorContainerOptions // https://mailvelope.github.io/mailvelope/global.html#EditorContainerOptions
quota: Math.max(2048, (quota / 1024)) - 48, // (text + attachments) limit in kilobytes quota: Math.max(2048, (quota / 1024)) - 48, // (text + attachments) limit in kilobytes
@ -438,7 +447,7 @@ export class ComposePopupView extends AbstractViewPopup {
quotedMailHeader: '', // header to be added before the quoted mail quotedMailHeader: '', // header to be added before the quoted mail
keepAttachments: false, // add attachments of quotedMail to editor (default: false) keepAttachments: false, // add attachments of quotedMail to editor (default: false)
// Issue: can't select signing key // Issue: can't select signing key
signMsg: this.pgpSign() signMsg: this.doSign()
*/ */
}).then(editor => this.mailvelope = editor); }).then(editor => this.mailvelope = editor);
} }
@ -1338,8 +1347,8 @@ export class ComposePopupView extends AbstractViewPopup {
this.showBcc(false); this.showBcc(false);
this.showReplyTo(false); this.showReplyTo(false);
this.pgpSign(SettingsUserStore.pgpSign()); this.doSign(SettingsUserStore.pgpSign());
this.pgpEncrypt(SettingsUserStore.pgpEncrypt()); this.doEncrypt(SettingsUserStore.pgpEncrypt());
this.attachments([]); this.attachments([]);
@ -1449,8 +1458,8 @@ export class ComposePopupView extends AbstractViewPopup {
linkedData: [] linkedData: []
}, },
recipients = draft ? [identity.email()] : this.allRecipients(), recipients = draft ? [identity.email()] : this.allRecipients(),
sign = !draft && this.pgpSign() && this.canPgpSign(), sign = !draft && this.doSign() && (this.canPgpSign() || this.canSMimeSign()),
encrypt = this.pgpEncrypt() && this.canPgpEncrypt(), encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()),
isHtml = this.oEditor.isHtml(); isHtml = this.oEditor.isHtml();
if (isHtml) { if (isHtml) {
@ -1497,30 +1506,37 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data); alternative.children.push(data);
data = alternative; data = alternative;
} }
if (!draft && sign?.[1]) { if (sign) {
if ('openpgp' == sign[0]) { if (sign?.[1]) {
// Doesn't sign attachments if ('openpgp' == sign[0]) {
params.html = params.plain = ''; // Doesn't sign attachments
let signed = new MimePart; params.html = params.plain = '';
signed.headers['Content-Type'] = let signed = new MimePart;
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"'; signed.headers['Content-Type'] =
signed.headers['Content-Transfer-Encoding'] = '7Bit'; 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
signed.children.push(data); signed.headers['Content-Transfer-Encoding'] = '7Bit';
let signature = new MimePart; signed.children.push(data);
signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"'; let signature = new MimePart;
signature.headers['Content-Transfer-Encoding'] = '7Bit'; signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1); signature.headers['Content-Transfer-Encoding'] = '7Bit';
signed.children.push(signature); signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1);
params.signed = signed.toString(); signed.children.push(signature);
params.boundary = signed.boundary; params.signed = signed.toString();
data = signed; params.boundary = signed.boundary;
} else if ('gnupg' == sign[0]) { data = signed;
// TODO: sign in PHP fails } else if ('gnupg' == sign[0]) {
// params.signData = data.toString(); // TODO: sign in PHP fails
params.signFingerprint = sign[1].fingerprint; // params.signData = data.toString();
params.signPassphrase = await GnuPGUserStore.sign(sign[1]); params.signFingerprint = sign[1].fingerprint;
} else { params.signPassphrase = await GnuPGUserStore.sign(sign[1]);
throw 'Signing with ' + sign[0] + ' not yet implemented'; } else {
throw 'Signing with ' + sign[0] + ' not yet implemented';
}
} else if (this.canSMimeSign()) {
params.signCertificate = identity.smimeCertificate();
params.signPrivateKey = identity.smimeKey();
const pass = await AskPopupView.password('S/MIME key', 'OPENPGP/LABEL_SIGN');
params.signPassphrase = pass?.password;
} }
} }
if (encrypt) { if (encrypt) {
@ -1534,6 +1550,9 @@ export class ComposePopupView extends AbstractViewPopup {
} else if ('gnupg' == encrypt) { } else if ('gnupg' == encrypt) {
// Does encrypt attachments // Does encrypt attachments
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients)); params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
// } else {
// // S/MIME
// params.encryptCertificates = '';
} else { } else {
throw 'Encryption with ' + encrypt + ' not yet implemented'; throw 'Encryption with ' + encrypt + ' not yet implemented';
} }

View file

@ -11,6 +11,8 @@
namespace MailSo\Imap; namespace MailSo\Imap;
use MailSo\Mime\Enumerations\ContentType;
/** /**
* @category MailSo * @category MailSo
* @package Imap * @package Imap
@ -165,10 +167,10 @@ class BodyStructure implements \JsonSerializable
{ {
return ('multipart/signed' === $this->sContentType return ('multipart/signed' === $this->sContentType
&& !empty($this->aContentTypeParams['protocol']) && !empty($this->aContentTypeParams['protocol'])
&& 'application/pkcs7-signature' === \strtolower(\trim($this->aContentTypeParams['protocol'])) && ContentType::isPkcs7Signature(\strtolower(\trim($this->aContentTypeParams['protocol'])))
// The multipart/signed body MUST consist of exactly two parts. // The multipart/signed body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts) && 2 === \count($this->aSubParts)
&& 'application/pkcs7-signature' === $this->aSubParts[1]->ContentType() && ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType())
) || ('application/pkcs7-mime' === $this->sContentType ) || ('application/pkcs7-mime' === $this->sContentType
&& !empty($this->aContentTypeParams['smime-type']) && !empty($this->aContentTypeParams['smime-type'])
&& 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type'])) && 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type']))

View file

@ -24,4 +24,10 @@ abstract class ContentType
const PGP_SIGNATURE = 'application/pgp-signature'; const PGP_SIGNATURE = 'application/pgp-signature';
const PKCS7_SIGNATURE = 'application/pkcs7-signature'; const PKCS7_SIGNATURE = 'application/pkcs7-signature';
const PKCS7_MIME = 'application/pkcs7-mime'; const PKCS7_MIME = 'application/pkcs7-mime';
public static function isPkcs7Signature(string $data) : bool
{
return 'application/pkcs7-signature' === $data
|| 'application/x-pkcs7-signature' === $data;
}
} }

View file

@ -11,6 +11,8 @@
namespace MailSo\Mime; namespace MailSo\Mime;
use MailSo\Mime\Enumerations\ContentType;
/** /**
* @category MailSo * @category MailSo
* @package Mime * @package Mime
@ -96,10 +98,10 @@ class Part
{ {
$header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE); $header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE);
return ($header return ($header
&& \preg_match('#multipart/signed.+protocol=["\']?application/pkcs7-signature#si', $header->FullValue()) && \preg_match('#multipart/signed.+protocol=["\']?application/(x-)?pkcs7-signature#si', $header->FullValue())
// The multipart/signed body MUST consist of exactly two parts. // The multipart/signed body MUST consist of exactly two parts.
&& 2 === \count($this->SubParts) && 2 === \count($this->SubParts)
&& 'application/pkcs7-signature' === $this->SubParts[1]->ContentType() && ContentType::isPkcs7Signature($this->SubParts[1]->ContentType())
) || ($header ) || ($header
&& \preg_match('#application/pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue()) && \preg_match('#application/pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue())
); );

View file

@ -1231,7 +1231,7 @@ trait Messages
$resource = $oBody->ToStream(); $resource = $oBody->ToStream();
\MailSo\Base\StreamFilters\LineEndings::appendTo($resource); \MailSo\Base\StreamFilters\LineEndings::appendTo($resource);
$tmp = new \SnappyMail\File\Temporary; $tmp = new \SnappyMail\File\Temporary('mimepart');
$tmp->writeFromStream($resource); $tmp->writeFromStream($resource);
$oBody->Body = null; $oBody->Body = null;
@ -1244,7 +1244,7 @@ trait Messages
$sSignature = $SMIME->sign($tmp, $sCertificate); $sSignature = $SMIME->sign($tmp, $sCertificate);
if (!$sSignature) { if (!$sSignature) {
throw new \Exception('GnuPG sign() failed'); throw new \Exception('S/MIME sign() failed');
} }
$oPart = new MimePart; $oPart = new MimePart;
@ -1261,7 +1261,7 @@ trait Messages
$oSignaturePart = new MimePart; $oSignaturePart = new MimePart;
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"'); $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"');
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
$oSignaturePart->Body = $sSignature; $oSignaturePart->Body = $sSignature;
$oPart->SubParts->append($oSignaturePart); $oPart->SubParts->append($oSignaturePart);
} }
@ -1285,7 +1285,7 @@ trait Messages
} else { } else {
$aCertificates = \json_decode($this->GetActionParam('encryptCertificates', ''), true); $aCertificates = \json_decode($this->GetActionParam('encryptCertificates', ''), true);
if ($aCertificates) { if ($aCertificates) {
$tmp = new \SnappyMail\File\Temporary; $tmp = new \SnappyMail\File\Temporary('mimepart');
$tmp->writeFromStream($oMessage->GetRootPart()->ToStream()); $tmp->writeFromStream($oMessage->GetRootPart()->ToStream());
$oMessage->SubParts->Clear(); $oMessage->SubParts->Clear();

View file

@ -4,7 +4,7 @@ namespace SnappyMail\File;
class Temporary class Temporary
{ {
protected static string $filename = ''; protected string $filename = '';
function __construct(string $name, bool $prefix = true) function __construct(string $name, bool $prefix = true)
{ {
@ -15,7 +15,7 @@ class Temporary
throw new \Exception("Failed to create directory {$tmpdir}"); throw new \Exception("Failed to create directory {$tmpdir}");
} }
if (!\is_writable($tmpdir)) { if (!\is_writable($tmpdir)) {
throw new \Exception("Failed to access directory {$tmpdir}"); // throw new \Exception("Failed to access directory {$tmpdir}");
} }
if ($prefix) { if ($prefix) {
$this->filename = \tempnam($tmpdir, $name); $this->filename = \tempnam($tmpdir, $name);
@ -42,10 +42,10 @@ class Temporary
private $fp = null; private $fp = null;
public function fopen()/* : resource|false*/ public function fopen()/* : resource|false*/
{ {
if (!$fp) { if (!$this->fp) {
$fp = \fopen($this->filename, 'r+b'); $this->fp = \fopen($this->filename, 'r+b');
} }
return $fp; return $this->fp;
} }
public function writeFromStream(/*resource*/ $from)/* : int|false*/ public function writeFromStream(/*resource*/ $from)/* : int|false*/

View file

@ -6,6 +6,8 @@ class Certificate
{ {
public public
$x509 = null, $x509 = null,
$pkey = null,
$digest = 'sha256', $digest = 'sha256',
$cipher = \OPENSSL_CIPHER_AES_256_CBC, $cipher = \OPENSSL_CIPHER_AES_256_CBC,
$keyBits = 4096, $keyBits = 4096,
@ -32,10 +34,17 @@ class Certificate
* A string having the format file://path/to/cert.pem; the named file must contain a PEM encoded certificate * A string having the format file://path/to/cert.pem; the named file must contain a PEM encoded certificate
* A string containing the content of a certificate, PEM encoded, may start with -----BEGIN CERTIFICATE----- * A string containing the content of a certificate, PEM encoded, may start with -----BEGIN CERTIFICATE-----
*/ */
function __construct($x509cert = null) function __construct($x509cert = null, $privateKey = null)
{ {
if ($x509cert) { if ($x509cert) {
$this->x509 = \openssl_x509_read($x509cert); $x509cert = \openssl_x509_read($x509cert);
if (!$x509cert) {
throw new \RuntimeException('OpenSSL x509: ' . \openssl_error_string());
}
$this->x509 = $x509cert;
if ($privateKey && \openssl_x509_check_private_key($this->x509, $privateKey)) {
$this->pkey = $privateKey;
}
} }
} }

View file

@ -24,6 +24,9 @@ class OpenSSL
) : void ) : void
{ {
$this->private_key = \openssl_pkey_get_private($private_key, $passphrase); $this->private_key = \openssl_pkey_get_private($private_key, $passphrase);
if (!$this->private_key) {
throw new \RuntimeException('OpenSSL setPrivateKey: ' . \openssl_error_string());
}
} }
public function decrypt(string $data, $certificate = null, $private_key = null) : ?string public function decrypt(string $data, $certificate = null, $private_key = null) : ?string
@ -67,31 +70,35 @@ class OpenSSL
} }
} }
$output = new Temporary('smimeout-'); $output = new Temporary('smimeout-');
if (!$input->putContents($data) || !\openssl_pkcs7_sign( if (!\openssl_pkcs7_sign(
$input->filename(), $input->filename(),
$output->filename(), $output->filename(),
$certificate ?: $this->certificate, // \openssl_pkey_get_public(); $certificate ?: $this->certificate, // \openssl_pkey_get_public();
$private_key ?: $this->private_key, // \openssl_pkey_get_private($private_key, ?string $passphrase = null); $private_key ?: $this->private_key, // \openssl_pkey_get_private($private_key, ?string $passphrase = null);
$this->headers, $this->headers,
\PKCS7_DETACHED, // | PKCS7_NOCERTS | PKCS7_NOATTR \PKCS7_DETACHED | \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR
$this->untrusted_certificates_filename $this->untrusted_certificates_filename
)) { )) {
return null; throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
} }
/*
$body = $output->getContents(); $body = $output->getContents();
// The message returned by openssl contains both headers and body, so need to split them up if (\preg_match('/\\.p7s"\R\R(.+?)------/s', $body, $match)) {
$parts = explode("\n\n", $body, 2); return \trim($match[1]);
$this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; }
$body = $parts[1];
*/ throw new \RuntimeException('OpenSSL sign: failed to find p7s');
return $output->getContents();
} }
public function verify(string $data, $signers_certificates_filename = null) public function verify(string $data, $signers_certificates_filename = null)
{ {
$input = new Temporary('smimein-'); if (\is_string($input)) {
return $input->putContents($data) && true === \openssl_pkcs7_verify( $input = new Temporary('smimein-');
if (!$input->putContents($data)) {
return null;
}
}
return true === \openssl_pkcs7_verify(
$input->filename(), $input->filename(),
$flags = 0, $flags = 0,
$signers_certificates_filename ?: null, $signers_certificates_filename ?: null,

View file

@ -60,15 +60,15 @@
<span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span> <span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span>
</a> </a>
</li> </li>
<li data-bind="toggle: pgpSign, visible: canPgpSign"> <li data-bind="toggle: doSign, visible: canSign">
<a> <a>
<i class="fontastic" data-bind="text: pgpSign() ? '☑' : '☐'"></i> <i class="fontastic" data-bind="text: doSign() ? '☑' : '☐'"></i>
<span data-icon="✍" data-i18n="OPENPGP/LABEL_SIGN"></span> <span data-icon="✍" data-i18n="OPENPGP/LABEL_SIGN"></span>
</a> </a>
</li> </li>
<li data-bind="toggle: pgpEncrypt, visible: canPgpEncrypt"> <li data-bind="toggle: doEncrypt, visible: canEncrypt">
<a> <a>
<i class="fontastic" data-bind="text: pgpEncrypt() || 'mailvelope' == viewArea() ? '☑' : '☐'"></i> <i class="fontastic" data-bind="text: doEncrypt() || 'mailvelope' == viewArea() ? '☑' : '☐'"></i>
<span data-icon="🔒" data-i18n="OPENPGP/LABEL_ENCRYPT"></span> <span data-icon="🔒" data-i18n="OPENPGP/LABEL_ENCRYPT"></span>
</a> </a>
</li> </li>
@ -137,10 +137,10 @@
<div style="display:flex"> <div style="display:flex">
<div class="btn-group" style="flex-grow:1"></div> <div class="btn-group" style="flex-grow:1"></div>
<div class="btn-group"> <div class="btn-group">
<a class="btn fontastic" data-bind="toggle: pgpSign, visible: canPgpSign, css: {'btn-success': pgpSign()}" data-i18n="[title]OPENPGP/LABEL_SIGN"> <a class="btn fontastic" data-bind="toggle: doSign, visible: canSign, css: {'btn-success': doSign()}" data-i18n="[title]OPENPGP/LABEL_SIGN">
</a> </a>
<a class="btn fontastic" data-bind="toggle: pgpEncrypt, visible: canPgpEncrypt, css: {'btn-success': pgpEncrypt() || 'mailvelope' == viewArea()}" data-i18n="[title]OPENPGP/LABEL_ENCRYPT"> <a class="btn fontastic" data-bind="toggle: doEncrypt, visible: canEncrypt, css: {'btn-success': doEncrypt() || 'mailvelope' == viewArea()}" data-i18n="[title]OPENPGP/LABEL_ENCRYPT">
🔒 🔒
</a> </a>
</div> </div>

View file

@ -55,40 +55,37 @@
<input type="radio" name="identitytabs" id="tab-identity-crypto"> <input type="radio" name="identitytabs" id="tab-identity-crypto">
<label data-i18n="CONTACTS/TAB_CRYPTO" for="tab-identity-crypto" role="tab" aria-selected="false" aria-controls="panel3" tabindex="0"></label> <label data-i18n="CONTACTS/TAB_CRYPTO" for="tab-identity-crypto" role="tab" aria-selected="false" aria-controls="panel3" tabindex="0"></label>
<div class="form-horizontal tab-content" role="tabpanel" aria-hidden="false"> <div class="form-horizontal tab-content" role="tabpanel" aria-hidden="false">
<details> <div class="control-group">
<summary><strong>PGP</strong></summary> <div data-bind="component: {
<div class="control-group"> name: 'Checkbox',
<div data-bind="component: { params: {
name: 'Checkbox', label: 'OPENPGP/LABEL_SIGN',
params: { value: pgpSign,
label: 'OPENPGP/LABEL_SIGN', name: 'pgpSign'
value: pgpSign, }
name: 'pgpSign' }"></div>
} </div>
}"></div> <div class="control-group">
</div> <div data-bind="component: {
<div class="control-group"> name: 'Checkbox',
<div data-bind="component: { params: {
name: 'Checkbox', label: 'OPENPGP/LABEL_ENCRYPT',
params: { value: pgpEncrypt,
label: 'OPENPGP/LABEL_ENCRYPT', name: 'pgpEncrypt'
value: pgpEncrypt, }
name: 'pgpEncrypt' }"></div>
} </div>
}"></div>
</div>
<!-- <!--
<div class="control-group"> <div class="control-group">
<label data-i18n="OPENPGP/LABEL_ENCRYPT"></label> <label data-i18n="OPENPGP/LABEL_ENCRYPT"></label>
<select name="PgpEncrypt" data-bind="value: pgpEncrypt"> <select name="PgpEncrypt" data-bind="value: pgpEncrypt">
<option value="Ask" data-i18n="CONTACTS/ASK"></option> <option value="Ask" data-i18n="CONTACTS/ASK"></option>
<option value="Never" data-i18n="CONTACTS/NEVER"></option> <option value="Never" data-i18n="CONTACTS/NEVER"></option>
<option value="Always" data-i18n="CONTACTS/ALWAYS"></option> <option value="Always" data-i18n="CONTACTS/ALWAYS"></option>
<option value="IfPossible" data-i18n="CONTACTS/ALWAYS_IF_POSSIBLE"></option> <option value="IfPossible" data-i18n="CONTACTS/ALWAYS_IF_POSSIBLE"></option>
</select> </select>
</div> </div>
--> -->
</details>
<details> <details>
<summary><strong>S/MIME</strong></summary> <summary><strong>S/MIME</strong></summary>
<div class="control-group"> <div class="control-group">