mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 10:09:20 +03:00
Merge branch 'master' into php81
This commit is contained in:
commit
0985df857c
81 changed files with 1015 additions and 741 deletions
47
CHANGELOG.md
47
CHANGELOG.md
|
|
@ -1,3 +1,50 @@
|
|||
## 2.35.2 – 2024-02-26
|
||||
|
||||
### Added
|
||||
- GnuPG error handling
|
||||
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
|
||||
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
|
||||
|
||||
### Changed
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
|
||||
- Error 352 when getting Filters
|
||||
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
|
||||
- Nextcloud SetPassword(): Argument #1 must be of type SensitiveString, string given
|
||||
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
|
||||
|
||||
|
||||
## 2.35.1 – 2024-02-26
|
||||
|
||||
### Added
|
||||
- Search functionality in Admin -> Config
|
||||
- Cache S/MIME passphrases when "remember" is checked
|
||||
- Import S/MIME certificate popup
|
||||
- pre-verify S/MIME opaque signed messages so we have a body to view
|
||||
- Sort PGP keys and S/MIME certificates on email address
|
||||
- Optionally use existing private key to generate S/MIME certificate
|
||||
|
||||
### Changed
|
||||
- Better handling to detect which PGP or S/MIME sign/encrypt to use
|
||||
- Improved StorageType handling
|
||||
- Cleanup and improved Capa handling
|
||||
- OPEN_PGP should be OPENPGP as it is one word
|
||||
- Use get_debug_type() instead of gettype()
|
||||
- Require OpenSSL due to S/MIME
|
||||
- AbstractProvider::IsActive() is now an abstract method and must be defined in child class
|
||||
- Make better use of SnappyMail\SensitiveString
|
||||
- Update Polish translation by @tinola
|
||||
|
||||
### Fixed
|
||||
- Verify S/MIME signatures got broken allong the way while implementing this
|
||||
- Generate S/MIME self-signed certificate failed to keep existing private key
|
||||
- MIME parser RegExp didn't escape boundary which caused issues
|
||||
- TypeError: b64Encode(...).match(...) is null on saving compose draft
|
||||
- Fix timestampToString() for future dates
|
||||
|
||||
|
||||
## 2.35.0 – 2024-02-20
|
||||
|
||||
### Added
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 81.641 |
|
||||
|app.js |4.207.787 | 426.852 |
|
||||
|admin.js |2.170.153 | 82.445 |
|
||||
|app.js |4.207.787 | 429.565 |
|
||||
|boot.js | 868.735 | 4.142 |
|
||||
|libs.js | 658.812 | 192.625 |
|
||||
|libs.js | 658.812 | 192.786 |
|
||||
|sieve.js | 0 | 84.707 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.240.095 | 790.252 |
|
||||
|TOTAL |8.240.095 | 793.930 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 40.142 | 73.606 | 13.445 | 60.877 | 12.047 |
|
||||
|app.min.js | 515.367 | 193.738 |139.456 | 65.570 |110.485 | 56.281 |
|
||||
|admin.min.js | 256.831 | 40.573 | 73.606 | 13.585 | 60.877 | 12.188 |
|
||||
|app.min.js | 515.367 | 195.013 |139.456 | 66.135 |110.485 | 56.623 |
|
||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
||||
|libs.min.js | 584.772 | 92.664 |180.901 | 34.414 |155.182 | 30.850 |
|
||||
|libs.min.js | 584.772 | 92.746 |180.901 | 34.452 |155.182 | 30.890 |
|
||||
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|
||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||
|TOTAL user |1.217.635 | 288.486 |358.761 |101.186 |299.485 | 88.134 |
|
||||
|TOTAL user+sieve |1.217.635 | 329.650 |358.761 |111.551 |299.485 | 97.493 |
|
||||
|TOTAL admin | 959.099 | 134.890 |292.911 | 49.061 |249.877 | 43.900 |
|
||||
|TOTAL user |1.217.635 | 289.843 |358.761 |101.789 |299.485 | 88.516 |
|
||||
|TOTAL user+sieve |1.217.635 | 331.007 |358.761 |112.154 |299.485 | 97.875 |
|
||||
|TOTAL admin | 959.099 | 135.403 |292.911 | 49.239 |249.877 | 44.081 |
|
||||
|
||||
For a user it is around 69% smaller and faster than traditional RainLoop.
|
||||
|
||||
|
|
@ -188,8 +188,8 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|
|||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 84.435 | 46.946 | 17.612 | 15.091 |
|
||||
|app.min.css | 274.947 | 67.825 | 39.647 | 15.526 | 13.540 |
|
||||
|app.css | 340.331 | 84.472 | 46.946 | 17.622 | 15.112 |
|
||||
|app.min.css | 274.947 | 67.857 | 39.647 | 15.537 | 13.549 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.576 | | 7.013 | 6.096 |
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ export const GnuPGUserStore = new class {
|
|||
}
|
||||
};
|
||||
if (isPrivate) {
|
||||
key.password = async (btnTxt = 'SIGN') => {
|
||||
key.password = async (btnTxt = 'CRYPTO/SIGN') => {
|
||||
const pass = await Passphrases.ask(key,
|
||||
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
||||
'CRYPTO/'+btnTxt
|
||||
btnTxt
|
||||
);
|
||||
pass && pass.remember && Passphrases.set(key, pass.password);
|
||||
return pass?.password;
|
||||
|
|
@ -82,8 +82,8 @@ export const GnuPGUserStore = new class {
|
|||
if (key.armor) {
|
||||
callback && callback();
|
||||
} else {
|
||||
let pass = isPrivate ? await key.password('POPUP_VIEW_TITLE') : '';
|
||||
if (null != pass) {
|
||||
let pass = isPrivate ? await key.password('OPENPGP/POPUP_VIEW_TITLE') : '';
|
||||
if (null != pass) try {
|
||||
const result = await Remote.post('GnupgExportKey', null, {
|
||||
keyId: key.id,
|
||||
isPrivate: isPrivate,
|
||||
|
|
@ -95,6 +95,8 @@ export const GnuPGUserStore = new class {
|
|||
} else {
|
||||
Passphrases.delete(key);
|
||||
}
|
||||
} catch (e) {
|
||||
Passphrases.delete(key);
|
||||
}
|
||||
}
|
||||
return key.armor;
|
||||
|
|
@ -182,15 +184,20 @@ export const GnuPGUserStore = new class {
|
|||
uid: message.uid,
|
||||
partId: pgpInfo.partId,
|
||||
keyId: key.id,
|
||||
passphrase: await key.password('DECRYPT'),
|
||||
passphrase: await key.password('CRYPTO/DECRYPT'),
|
||||
data: '' // message.plain() optional
|
||||
}
|
||||
if (null !== params.passphrase) {
|
||||
const result = await Remote.post('GnupgDecrypt', null, params);
|
||||
if (result?.Result?.data) {
|
||||
return result.Result;
|
||||
if (null != params.passphrase) {
|
||||
try {
|
||||
const response = await Remote.post('GnupgDecrypt', null, params);
|
||||
if (response?.Result?.data) {
|
||||
return response.Result;
|
||||
}
|
||||
throw response;
|
||||
} catch (e) {
|
||||
Passphrases.delete(key);
|
||||
throw e;
|
||||
}
|
||||
Passphrases.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 100px;
|
||||
color: var(--folders-color, #333);
|
||||
}
|
||||
|
||||
hr {
|
||||
|
|
@ -77,22 +78,18 @@
|
|||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
line-height: 34px;
|
||||
background-color: transparent;
|
||||
vertical-align: middle;
|
||||
color: var(--folders-disabled-color, #666);
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
color: var(--folders-color, #333);
|
||||
display: block;
|
||||
line-height: 34px;
|
||||
padding: 0 2em 0 @folderItemPadding;
|
||||
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
z-index: 1;
|
||||
|
||||
&.selectable {
|
||||
|
||||
color: var(--folders-color, #333);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -115,6 +112,7 @@
|
|||
}
|
||||
}
|
||||
&:not(.selectable) {
|
||||
color: var(--folders-disabled-color, #666);
|
||||
cursor: default;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,13 @@ export class IdentityPopupView extends AbstractViewPopup {
|
|||
if (oData.Result.x509) {
|
||||
identity.smimeKey(oData.Result.pkey);
|
||||
identity.smimeCertificate(oData.Result.x509);
|
||||
} else {
|
||||
this.submitError(oData.ErrorMessage);
|
||||
}
|
||||
}, {
|
||||
name: identity.name(),
|
||||
email: identity.email(),
|
||||
privateKey: identity.smimeKey(),
|
||||
passphrase: pass.password
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,10 +587,13 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}
|
||||
} else {
|
||||
// TODO: translate
|
||||
alert('Decryption failed, canceled or not possible');
|
||||
throw 'Decryption failed, canceled or not possible';
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
alert(e.message);
|
||||
});
|
||||
}
|
||||
|
||||
pgpVerify(/*self, event*/) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
This app packages SnappyMail <upstream>2.35.0</upstream>.
|
||||
This app packages SnappyMail <upstream>2.35.2</upstream>.
|
||||
|
||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
|||
WORKDIR /app/code
|
||||
|
||||
# If you change the extraction below, be sure to test on scaleway
|
||||
VERSION=2.35.0
|
||||
VERSION=2.35.2
|
||||
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
||||
unzip /tmp/snappymail.zip -d /app/code && \
|
||||
rm /tmp/snappymail.zip && \
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<id>snappymail</id>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<version>2.35.0</version>
|
||||
<version>2.35.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class InstallStep implements IRepairStep
|
|||
if ('12345' == $sPassword || !$sPassword) {
|
||||
$output->info('Generate admin password');
|
||||
$sPassword = \substr(\base64_encode(\random_bytes(16)), 0, 12);
|
||||
$oConfig->SetPassword($sPassword);
|
||||
$oConfig->SetPassword(new \SnappyMail\SensitiveString($sPassword));
|
||||
\RainLoop\Utils::saveFile(APP_PRIVATE_DATA . 'admin_password.txt', $sPassword . "\n");
|
||||
$bSave = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ class SnappyMailHelper
|
|||
*/
|
||||
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
|
||||
try {
|
||||
$oActions->Logger()->AddSecret($aCredentials[2]);
|
||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
|
||||
if ($oAccount) {
|
||||
// Must be here due to bug #1241
|
||||
|
|
@ -202,9 +201,10 @@ class SnappyMailHelper
|
|||
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
|
||||
}
|
||||
|
||||
public static function decodePassword(string $sPassword, string $sSalt)/* : mixed */
|
||||
public static function decodePassword(string $sPassword, string $sSalt) : ?\SnappyMail\SensitiveString
|
||||
{
|
||||
static::loadApp();
|
||||
return \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
|
||||
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
|
||||
return $result ? new \SnappyMail\SensitiveString($result) : $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
|||
# script_snappymail_versions()
|
||||
sub script_snappymail_versions
|
||||
{
|
||||
return ( "2.35.0" );
|
||||
return ( "2.35.2" );
|
||||
}
|
||||
|
||||
sub script_snappymail_version_desc
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "SnappyMail",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"private": true,
|
||||
"version": "2.35.0",
|
||||
"version": "2.35.2",
|
||||
"homepage": "https://snappymail.eu",
|
||||
"author": {
|
||||
"name": "DJ Maze",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class LdapConfig
|
|||
public const CONFIG_SERVER = "server";
|
||||
public const CONFIG_PROTOCOL_VERSION = "server_version";
|
||||
public const CONFIG_STARTTLS = "starttls";
|
||||
public const CONFIG_MAIL_PREFIX = "mail_prefix";
|
||||
|
||||
public const CONFIG_BIND_USER = "bind_user";
|
||||
public const CONFIG_BIND_PASSWORD = "bind_password";
|
||||
|
|
@ -30,6 +31,7 @@ class LdapConfig
|
|||
public $server;
|
||||
public $protocol;
|
||||
public $starttls;
|
||||
public $mail_prefix;
|
||||
public $bind_user;
|
||||
public $bind_password;
|
||||
public $user_base;
|
||||
|
|
@ -51,6 +53,7 @@ class LdapConfig
|
|||
$ldap->server = trim($config->Get("plugin", self::CONFIG_SERVER));
|
||||
$ldap->protocol = (int)trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3));
|
||||
$ldap->starttls = (bool)trim($config->Get("plugin", self::CONFIG_STARTTLS));
|
||||
$ldap->mail_prefix = trim($config->Get("plugin", self::CONFIG_MAIL_PREFIX));
|
||||
$ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER));
|
||||
$ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD));
|
||||
$ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE));
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ class LdapIdentities implements IIdentities
|
|||
$this->config->user_base,
|
||||
$this->config->user_objectclass,
|
||||
$this->config->user_field_name,
|
||||
$this->config->user_field_mail
|
||||
$this->config->user_field_mail,
|
||||
$this->config->mail_prefix
|
||||
);
|
||||
} catch (LdapException $e) {
|
||||
return []; // exceptions are only thrown from the handleerror function that does logging already
|
||||
|
|
@ -104,7 +105,8 @@ class LdapIdentities implements IIdentities
|
|||
$this->config->group_base,
|
||||
$this->config->group_objectclass,
|
||||
$this->config->group_field_name,
|
||||
$this->config->group_field_mail
|
||||
$this->config->group_field_mail,
|
||||
$this->config->mail_prefix
|
||||
);
|
||||
} catch (LdapException $e) {
|
||||
return []; // exceptions are only thrown from the handleerror function that does logging already
|
||||
|
|
@ -241,7 +243,7 @@ class LdapIdentities implements IIdentities
|
|||
* @return LdapResult[]
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField): array
|
||||
private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField, string $mailPrefix): array
|
||||
{
|
||||
$this->EnsureBound();
|
||||
|
||||
|
|
@ -261,6 +263,8 @@ class LdapIdentities implements IIdentities
|
|||
return [];
|
||||
}
|
||||
|
||||
$entries = $this->CleanupMailAddresses($entries, $mailField, $mailPrefix);
|
||||
|
||||
$results = [];
|
||||
for ($i = 0; $i < $entries["count"]; $i++) {
|
||||
$entry = $entries[$i];
|
||||
|
|
@ -276,6 +280,49 @@ class LdapIdentities implements IIdentities
|
|||
return $results;
|
||||
}
|
||||
|
||||
// Function CleanupMailAddresses(): If a prefix is given this function removes addresses without / with the wrong prefix and then the prefix itself from all remaining values.
|
||||
// This is usefull for example for importing Active Directory LDAP entry "proxyAddresses" which can hold different address types with prefixes like "X400:", "smtp:" "sip:" and others.
|
||||
|
||||
/**
|
||||
@param array $entries
|
||||
@param string $mailField
|
||||
@paraam string $mailPrefix
|
||||
@return array
|
||||
*/
|
||||
private function CleanupMailAddresses(array $entries, string $mailField, string $mailPrefix)
|
||||
{
|
||||
if (!empty($mailPrefix)) {
|
||||
for ($i = 0; $i < $entries["count"]; $i++) {
|
||||
// Remove addresses without the given prefix
|
||||
$entries[$i]["$mailField"] = array_filter($entries[$i]["$mailField"],
|
||||
function($prefixMail)
|
||||
{
|
||||
// $mailPrefix can't be used here, because it's nailed to the CleanupMailAddresses function and can't be passed to the array_filter function afaik.
|
||||
// Ideas to avoid this are welcome.
|
||||
if (stripos($prefixMail, $this->config->mail_prefix) === 0) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
);
|
||||
// Set "count" to new value
|
||||
$newcount = count($entries[$i]["$mailField"]);
|
||||
if (array_key_exists("count", $entries[$i]["$mailField"])) {
|
||||
$newcount = $newcount - 1;
|
||||
}
|
||||
$entries[$i]["$mailField"]["count"] = $newcount;
|
||||
|
||||
// Remove the prefix
|
||||
for ($j = 0; $j < $entries[$i]["$mailField"]["count"]; $j++) {
|
||||
$mailPrefixLen = mb_strlen($mailPrefix);
|
||||
$entries[$i]["$mailField"][$j] = substr($entries[$i]["$mailField"][$j], $mailPrefixLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entry
|
||||
* @param string $attribute
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ class LdapIdentitiesPlugin extends AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'LDAP Identities',
|
||||
VERSION = '2.2',
|
||||
VERSION = '2.3',
|
||||
AUTHOR = 'FWest98',
|
||||
URL = 'https://github.com/FWest98',
|
||||
RELEASE = '2024-02-22',
|
||||
RELEASE = '2024-02-27',
|
||||
REQUIRED = '2.20.0',
|
||||
CATEGORY = 'Accounts',
|
||||
DESCRIPTION = 'Adds functionality to import account identities from LDAP.';
|
||||
|
|
@ -57,13 +57,19 @@ class LdapIdentitiesPlugin extends AbstractPlugin
|
|||
->SetLabel("LDAP Protocol Version")
|
||||
->SetType(PluginPropertyType::SELECTION)
|
||||
->SetDefaultValue([2, 3]),
|
||||
|
||||
|
||||
Property::NewInstance(LdapConfig::CONFIG_STARTTLS)
|
||||
->SetLabel("Use StartTLS")
|
||||
->SetType(PluginPropertyType::BOOL)
|
||||
->SetDescription("Whether or not to use TLS encrypted connection")
|
||||
->SetDefaultValue(true),
|
||||
|
||||
Property::NewInstance(LdapConfig::CONFIG_MAIL_PREFIX)
|
||||
->SetLabel("Email prefix")
|
||||
->SetType(PluginPropertyType::STRING)
|
||||
->SetDescription("Only addresses with this prefix will be used as identity. The prefix is removed from the identity list.\nThis is useful for example to import identities from Exchange, which stores mail addresses in the ProxyAddresses attribut of Active Directory with \"smtp:\" as prefix. (e.g. \"smtp:john.doe@topsecret.info\")\n-> To use addresses set by Exchange use \"smtp:\" as prefix.")
|
||||
->SetDefaultValue(""),
|
||||
|
||||
Property::NewInstance(LdapConfig::CONFIG_BIND_USER)
|
||||
->SetLabel("Bind User DN")
|
||||
->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind")
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
);
|
||||
}
|
||||
/*
|
||||
if ('storage' === $sName || 'storage-local' === $sName) {
|
||||
if ($this->Config()->Get('plugin', 'storage', false) && ('storage' === $sName || 'storage-local' === $sName)) {
|
||||
require_once __DIR__ . '/storage.php';
|
||||
$oDriver = new \NextcloudStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local');
|
||||
}
|
||||
|
|
@ -346,6 +346,11 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
\RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDefaultValue(true),
|
||||
/*
|
||||
\RainLoop\Plugins\Property::NewInstance('storage')->SetLabel('Use Nextcloud user ID in config storage path')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDefaultValue(false)
|
||||
*/
|
||||
\RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDefaultValue(false)
|
||||
|
|
|
|||
|
|
@ -184,7 +184,12 @@ class SieveClient extends \MailSo\Net\NetClient
|
|||
public function Logout() : void
|
||||
{
|
||||
if ($this->bIsLoggined) {
|
||||
$this->sendRequestWithCheck('LOGOUT');
|
||||
try {
|
||||
$this->sendRequestWithCheck('LOGOUT');
|
||||
} catch (\Throwable $e) {
|
||||
// https://github.com/the-djmaze/snappymail/issues/1455
|
||||
$this->writeLogException($e, \LOG_WARNING, false);
|
||||
}
|
||||
$this->bIsLoggined = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -408,6 +413,7 @@ class SieveClient extends \MailSo\Net\NetClient
|
|||
if (null === $sResponseBuffer) {
|
||||
break;
|
||||
}
|
||||
// \MailSo\Imap\Enumerations\ResponseStatus
|
||||
$bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO'));
|
||||
// convertEndOfLine
|
||||
$sLine = \trim($sResponseBuffer);
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ trait Accounts
|
|||
$aAccounts = $this->GetAccounts($oMainAccount);
|
||||
|
||||
$sEmail = \trim($this->GetActionParam('email', ''));
|
||||
$sPassword = $this->GetActionParam('password', '');
|
||||
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('password', ''));
|
||||
$sName = \trim($this->GetActionParam('name', ''));
|
||||
$bNew = !empty($this->GetActionParam('new', 1));
|
||||
|
||||
|
|
@ -92,8 +92,8 @@ trait Accounts
|
|||
throw new ClientException(Notifications::AccountDoesNotExist);
|
||||
}
|
||||
|
||||
if ($bNew || $sPassword) {
|
||||
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false);
|
||||
if ($bNew || \strlen($oPassword)) {
|
||||
$oNewAccount = $this->LoginProcess($sEmail, $oPassword, false);
|
||||
$aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount);
|
||||
} else {
|
||||
$aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]);
|
||||
|
|
@ -261,7 +261,11 @@ trait Accounts
|
|||
if (!$oIdentity->FromJSON($this->GetActionParams(), true)) {
|
||||
throw new ClientException(Notifications::InvalidInputArgument);
|
||||
}
|
||||
|
||||
/* // TODO: verify private key for certificate?
|
||||
if ($oIdentity->smimeCertificate && $oIdentity->smimeKey) {
|
||||
new \SnappyMail\SMime\Certificate($oIdentity->smimeCertificate, $oIdentity->smimeKey);
|
||||
}
|
||||
*/
|
||||
$this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity);
|
||||
return $this->TrueResponse();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ trait AdminDomains
|
|||
public function DoAdminDomainMatch() : array
|
||||
{
|
||||
$sEmail = $this->GetActionParam('username');
|
||||
$sPassword = '********';
|
||||
$oPassword = new \SnappyMail\SensitiveString('********');
|
||||
$sLogin = '';
|
||||
$this->resolveLoginCredentials($sEmail, $sPassword, $sLogin);
|
||||
$this->resolveLoginCredentials($sEmail, $oPassword, $sLogin);
|
||||
$oDomain = \str_contains($sEmail, '@')
|
||||
? $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true)
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -1065,8 +1065,7 @@ trait Messages
|
|||
}
|
||||
}
|
||||
|
||||
$sPassphrase = $this->GetActionParam('signPassphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('signPassphrase', ''));
|
||||
|
||||
$sFingerprint = $this->GetActionParam('signFingerprint', '');
|
||||
if ($sFingerprint) {
|
||||
|
|
@ -1084,7 +1083,7 @@ trait Messages
|
|||
$oMessage->SubParts->Clear();
|
||||
$oMessage->Attachments()->Clear();
|
||||
|
||||
$GPG->addSignKey($sFingerprint, $sPassphrase);
|
||||
$GPG->addSignKey($sFingerprint, $oPassphrase);
|
||||
$GPG->setsignmode(GNUPG_SIG_MODE_DETACH);
|
||||
$sSignature = $GPG->signStream($fp);
|
||||
if (!$sSignature) {
|
||||
|
|
@ -1127,7 +1126,7 @@ trait Messages
|
|||
|
||||
$SMIME = $this->SMIME();
|
||||
$SMIME->setCertificate($sCertificate);
|
||||
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
|
||||
$SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
|
||||
$sSignature = $SMIME->sign($tmp, $detached);
|
||||
|
||||
if (!$sSignature) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ trait Pgp
|
|||
|
||||
$GPG = $this->GnuPG();
|
||||
if ($GPG) {
|
||||
$keys = $GPG->keyInfo('');
|
||||
$keys = $GPG->allKeysInfo('');
|
||||
foreach ($keys['public'] as $key) {
|
||||
$key = $GPG->export($key['subkeys'][0]['fingerprint'] ?: $key['subkeys'][0]['keyid']);
|
||||
if ($key) {
|
||||
|
|
@ -47,7 +47,7 @@ trait Pgp
|
|||
/**
|
||||
* @throws \MailSo\RuntimeException
|
||||
*/
|
||||
public function GnuPG() : ?GnuPG
|
||||
public function GnuPG() : ?\SnappyMail\PGP\PGPInterface
|
||||
{
|
||||
$oAccount = $this->getMainAccountFromToken();
|
||||
if (!$oAccount) {
|
||||
|
|
@ -81,7 +81,7 @@ trait Pgp
|
|||
if (\is_link($link) || \symlink($homedir, $link)) {
|
||||
$homedir = $link;
|
||||
} else {
|
||||
\error_log("symlink('{$homedir}', '{$link}') failed");
|
||||
$this->logWrite("symlink('{$homedir}', '{$link}') failed", \LOG_WARNING, 'GnuPG');
|
||||
}
|
||||
}
|
||||
// Else try ~/.gnupg/ + hash(email address)
|
||||
|
|
@ -97,10 +97,6 @@ trait Pgp
|
|||
$homedir = $tmpdir;
|
||||
}
|
||||
}
|
||||
|
||||
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
|
||||
throw new \Exception("socket name for '{$homedir}/S.gpg-agent.extra' is too long");
|
||||
}
|
||||
}
|
||||
|
||||
return GnuPG::getInstance($homedir);
|
||||
|
|
@ -113,15 +109,14 @@ trait Pgp
|
|||
return $this->FalseResponse();
|
||||
}
|
||||
|
||||
$sPassphrase = $this->GetActionParam('passphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
|
||||
|
||||
$GPG->addDecryptKey($this->GetActionParam('keyId', ''), $sPassphrase);
|
||||
$GPG->addDecryptKey($this->GetActionParam('keyId', ''), $oPassphrase);
|
||||
|
||||
$sData = $this->GetActionParam('data', '');
|
||||
$oPart = null;
|
||||
$result = [
|
||||
'data' => '',
|
||||
'data' => null,
|
||||
'signatures' => []
|
||||
];
|
||||
if ($sData) {
|
||||
|
|
@ -131,11 +126,13 @@ trait Pgp
|
|||
$this->initMailClientConnection();
|
||||
$this->MailClient()->MessageMimeStream(
|
||||
function ($rResource) use ($GPG, &$result, &$oPart) {
|
||||
if (\is_resource($rResource)) {
|
||||
if (\is_resource($rResource)) try {
|
||||
$result['data'] = $GPG->decryptStream($rResource);
|
||||
// $oPart = \MailSo\Mime\Part::FromString($result);
|
||||
// $GPG->decryptStream($rResource, $rStreamHandle);
|
||||
// $oPart = \MailSo\Mime\Part::FromStream($rStreamHandle);
|
||||
} catch (\Throwable $e) {
|
||||
$result = $e;
|
||||
}
|
||||
},
|
||||
$this->GetActionParam('folder', ''),
|
||||
|
|
@ -149,23 +146,28 @@ trait Pgp
|
|||
// $result['signatures'] = $oPart->SubParts[0];
|
||||
}
|
||||
|
||||
if ($result instanceof \Throwable) {
|
||||
throw $result;
|
||||
}
|
||||
|
||||
return $this->DefaultResponse($result);
|
||||
}
|
||||
|
||||
public function DoGnupgGetKeys() : array
|
||||
{
|
||||
$GPG = $this->GnuPG();
|
||||
return $this->DefaultResponse($GPG ? $GPG->keyInfo('') : false);
|
||||
return $this->DefaultResponse($GPG ? $GPG->allKeysInfo('') : false);
|
||||
}
|
||||
|
||||
public function DoGnupgExportKey() : array
|
||||
{
|
||||
$sPassphrase = $this->GetActionParam('passphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = $this->GetActionParam('isPrivate', '')
|
||||
? new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''))
|
||||
: null;
|
||||
$GPG = $this->GnuPG();
|
||||
return $this->DefaultResponse($GPG ? $GPG->export(
|
||||
$this->GetActionParam('keyId', ''),
|
||||
$sPassphrase
|
||||
$oPassphrase
|
||||
) : false);
|
||||
}
|
||||
|
||||
|
|
@ -176,11 +178,10 @@ trait Pgp
|
|||
if ($GPG) {
|
||||
$sName = $this->GetActionParam('name', '');
|
||||
$sEmail = $this->GetActionParam('email', '');
|
||||
$sPassphrase = $this->GetActionParam('passphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
|
||||
$fingerprint = $GPG->generateKey(
|
||||
$sName ? "{$sName} <{$sEmail}>" : $sEmail,
|
||||
$sPassphrase
|
||||
$oPassphrase
|
||||
);
|
||||
}
|
||||
return $this->DefaultResponse($fingerprint);
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@ trait Response
|
|||
|
||||
public function ExceptionResponse(\Throwable $oException) : array
|
||||
{
|
||||
$iErrorCode = 0;
|
||||
$sErrorMessage = '';
|
||||
$iErrorCode = Notifications::UnknownError;
|
||||
$sErrorMessage = $oException->getMessage();
|
||||
$sErrorMessageAdditional = '';
|
||||
$iExceptionCode = 0;
|
||||
|
||||
if ($oException instanceof \RainLoop\Exceptions\ClientException) {
|
||||
$iErrorCode = $oException->getCode();
|
||||
|
|
@ -56,8 +57,7 @@ trait Response
|
|||
}
|
||||
$sErrorMessageAdditional = $oException->getAdditionalMessage();
|
||||
} else {
|
||||
$iErrorCode = Notifications::UnknownError;
|
||||
$sErrorMessage = $oException->getCode().' - '.$oException->getMessage();
|
||||
$iExceptionCode = $oException->getCode();
|
||||
}
|
||||
|
||||
$this->logException($oException->getPrevious() ?: $oException);
|
||||
|
|
@ -65,7 +65,8 @@ trait Response
|
|||
return $this->DefaultResponse(false, [
|
||||
'ErrorCode' => $iErrorCode,
|
||||
'ErrorMessage' => $sErrorMessage,
|
||||
'ErrorMessageAdditional' => $sErrorMessageAdditional
|
||||
'ErrorMessageAdditional' => $sErrorMessageAdditional,
|
||||
'ExceptionCode' => $iExceptionCode
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,21 +53,18 @@ trait SMime
|
|||
}
|
||||
|
||||
/**
|
||||
* Can be use by Identity
|
||||
* Can be used by Identity
|
||||
*/
|
||||
public function DoSMimeCreateCertificate() : array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
|
||||
$sName = $this->GetActionParam('name', '') ?: $oAccount->Name();
|
||||
$sEmail = $this->GetActionParam('email', '') ?: $oAccount->Email();
|
||||
$sPassphrase = $this->GetActionParam('passphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
|
||||
|
||||
$cert = new Certificate();
|
||||
$cert->distinguishedName['commonName'] = $sName;
|
||||
$cert->distinguishedName['emailAddress'] = $sEmail;
|
||||
$result = $cert->createSelfSigned($sPassphrase);
|
||||
$cert->distinguishedName['commonName'] = $this->GetActionParam('name', '') ?: $oAccount->Name();
|
||||
$cert->distinguishedName['emailAddress'] = $this->GetActionParam('email', '') ?: $oAccount->Email();
|
||||
$result = $cert->createSelfSigned($oPassphrase, $this->GetActionParam('privateKey', ''));
|
||||
return $this->DefaultResponse($result ?: false);
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +75,7 @@ trait SMime
|
|||
$sPartId = $this->GetActionParam('partId', '');
|
||||
$sCertificate = $this->GetActionParam('certificate', '');
|
||||
$sPrivateKey = $this->GetActionParam('privateKey', '');
|
||||
$sPassphrase = $this->GetActionParam('passphrase', '');
|
||||
$this->logMask($sPassphrase);
|
||||
$oPassphrase = new \SnappyMail\SensitiveString($this->GetActionParam('passphrase', ''));
|
||||
|
||||
$this->initMailClientConnection();
|
||||
$oImapClient = $this->ImapClient();
|
||||
|
|
@ -106,7 +102,7 @@ trait SMime
|
|||
|
||||
$SMIME = $this->SMIME();
|
||||
$SMIME->setCertificate($sCertificate);
|
||||
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
|
||||
$SMIME->setPrivateKey($sPrivateKey, $oPassphrase);
|
||||
$result = $SMIME->decrypt($sBody);
|
||||
|
||||
return $this->DefaultResponse($result ?: false);
|
||||
|
|
@ -142,9 +138,9 @@ trait SMime
|
|||
$sBody = \trim($sBody);
|
||||
$certificates = [];
|
||||
\openssl_pkcs7_read(
|
||||
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----",
|
||||
"-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
|
||||
$certificates
|
||||
) || \error_log("OpenSSL openssl_pkcs7_read: " . \openssl_error_string());
|
||||
) || $this->logWrite("openssl_pkcs7_read: " . \openssl_error_string(), \LOG_ERR, 'OpenSSL');
|
||||
foreach ($certificates as $certificate) {
|
||||
$this->SMIME()->storeCertificate($certificate);
|
||||
}
|
||||
|
|
@ -179,7 +175,7 @@ trait SMime
|
|||
$sBody = \trim($sBody);
|
||||
$certificates = [];
|
||||
\openssl_pkcs7_read(
|
||||
"-----BEGIN CERTIFICATE-----\n\n{$sBody}\n-----END CERTIFICATE-----",
|
||||
"-----BEGIN PKCS7-----\n\n{$sBody}\n-----END PKCS7-----",
|
||||
$certificates
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ trait User
|
|||
public function DoLogin() : array
|
||||
{
|
||||
$sEmail = \MailSo\Base\Utils::Trim($this->GetActionParam('Email', ''));
|
||||
$sPassword = $this->GetActionParam('Password', '');
|
||||
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
|
||||
|
||||
try {
|
||||
$oAccount = $this->LoginProcess($sEmail, $sPassword);
|
||||
$oAccount = $this->LoginProcess($sEmail, $oPassword);
|
||||
} catch (\Throwable $oException) {
|
||||
$this->loginErrorDelay();
|
||||
throw $oException;
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@ trait UserAuth
|
|||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function resolveLoginCredentials(string &$sEmail,
|
||||
#[\SensitiveParameter]
|
||||
string &$sPassword,
|
||||
string &$sLogin): void
|
||||
public function resolveLoginCredentials(string &$sEmail, \SnappyMail\SensitiveString $oPassword, string &$sLogin): void
|
||||
{
|
||||
$sEmail = \MailSo\Base\Utils::Trim($sEmail);
|
||||
if ($this->Config()->Get('login', 'login_lowercase', true)) {
|
||||
|
|
@ -101,43 +98,40 @@ trait UserAuth
|
|||
}
|
||||
}
|
||||
|
||||
$sPassword = (string) $oPassword;
|
||||
$this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword));
|
||||
$this->logMask($sPassword);
|
||||
|
||||
$sLogin = $sEmail;
|
||||
if ($this->Config()->Get('login', 'login_lowercase', true)) {
|
||||
$sLogin = \mb_strtolower($sLogin);
|
||||
}
|
||||
|
||||
$this->logMask($sPassword);
|
||||
$this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword));
|
||||
$this->logMask($sPassword);
|
||||
|
||||
$oPassword->setValue($sPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function LoginProcess(string &$sEmail,
|
||||
#[\SensitiveParameter]
|
||||
string &$sPassword,
|
||||
bool $bMainAccount = true
|
||||
): Account
|
||||
public function LoginProcess(string &$sEmail, \SnappyMail\SensitiveString $oPassword, bool $bMainAccount = true): Account
|
||||
{
|
||||
$sInputEmail = $sEmail;
|
||||
|
||||
$this->logMask($sPassword);
|
||||
|
||||
$sLogin = '';
|
||||
$this->resolveLoginCredentials($sEmail, $sPassword, $sLogin);
|
||||
$this->resolveLoginCredentials($sEmail, $oPassword, $sLogin);
|
||||
|
||||
if (!\str_contains($sEmail, '@') || !\strlen($sPassword)) {
|
||||
if (!\str_contains($sEmail, '@') || !\strlen($oPassword)) {
|
||||
throw new ClientException(Notifications::InvalidInputArgument);
|
||||
}
|
||||
|
||||
$oAccount = null;
|
||||
try {
|
||||
$oAccount = $bMainAccount
|
||||
? MainAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $sPassword, true)
|
||||
: AdditionalAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $sPassword, true);
|
||||
? MainAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $oPassword, true)
|
||||
: AdditionalAccount::NewInstanceFromCredentials($this, $sEmail, $sLogin, $oPassword, true);
|
||||
if (!$oAccount) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,17 +111,15 @@ class ActionsAdmin extends Actions
|
|||
public function DoAdminLogin() : array
|
||||
{
|
||||
$sLogin = trim($this->GetActionParam('Login', ''));
|
||||
$sPassword = $this->GetActionParam('Password', '');
|
||||
|
||||
$this->logMask($sPassword);
|
||||
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
|
||||
|
||||
$totp = $this->Config()->Get('security', 'admin_totp', '');
|
||||
|
||||
// \explode(':',`getent shadow root`)[1];
|
||||
if (!\strlen($sLogin) || !\strlen($sPassword) ||
|
||||
if (!\strlen($sLogin) || !\strlen($oPassword) ||
|
||||
!$this->Config()->Get('security', 'allow_admin_panel', true) ||
|
||||
$sLogin !== $this->Config()->Get('security', 'admin_login', '') ||
|
||||
!$this->Config()->ValidatePassword($sPassword)
|
||||
!$this->Config()->ValidatePassword($oPassword)
|
||||
|| ($totp && !\SnappyMail\TOTP::Verify($totp, $this->GetActionParam('TOTP', ''))))
|
||||
{
|
||||
$this->LoggerAuthHelper(null, $this->getAdditionalLogParamsByUserLogin($sLogin, true), true);
|
||||
|
|
@ -183,17 +181,13 @@ class ActionsAdmin extends Actions
|
|||
$bResult = false;
|
||||
$oConfig = $this->Config();
|
||||
|
||||
$sPassword = $this->GetActionParam('Password', '');
|
||||
$this->logMask($sPassword);
|
||||
$oPassword = new \SnappyMail\SensitiveString($this->GetActionParam('Password', ''));
|
||||
|
||||
$sNewPassword = $this->GetActionParam('newPassword', '');
|
||||
if (\strlen($sNewPassword)) {
|
||||
$this->logMask($sNewPassword);
|
||||
}
|
||||
$oNewPassword = new \SnappyMail\SensitiveString($this->GetActionParam('newPassword', ''));
|
||||
|
||||
$passfile = APP_PRIVATE_DATA.'admin_password.txt';
|
||||
|
||||
if ($oConfig->ValidatePassword($sPassword)) {
|
||||
if ($oConfig->ValidatePassword($oPassword)) {
|
||||
$sLogin = \trim($this->GetActionParam('Login', ''));
|
||||
if (\strlen($sLogin)) {
|
||||
$oConfig->Set('security', 'admin_login', $sLogin);
|
||||
|
|
@ -201,9 +195,9 @@ class ActionsAdmin extends Actions
|
|||
|
||||
$oConfig->Set('security', 'admin_totp', $this->GetActionParam('TOTP', ''));
|
||||
|
||||
if (\strlen($sNewPassword)) {
|
||||
$oConfig->SetPassword($sNewPassword);
|
||||
if (\is_file($passfile) && \trim(\file_get_contents($passfile)) !== $sNewPassword) {
|
||||
if (\strlen($oNewPassword)) {
|
||||
$oConfig->SetPassword($oNewPassword);
|
||||
if (\is_file($passfile) && \trim(\file_get_contents($passfile)) !== (string) $oNewPassword) {
|
||||
\unlink($passfile);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,25 +120,14 @@ class Application extends \RainLoop\Config\AbstractConfig
|
|||
parent::Set($sSectionKey, $sParamKey, $mParamValue);
|
||||
}
|
||||
|
||||
public function SetPassword(
|
||||
#[\SensitiveParameter]
|
||||
string $sPassword
|
||||
) : void
|
||||
public function SetPassword(\SnappyMail\SensitiveString $oPassword) : void
|
||||
{
|
||||
$this->Set('security', 'admin_password', \password_hash($sPassword, PASSWORD_DEFAULT));
|
||||
$this->Set('security', 'admin_password', \password_hash($oPassword, PASSWORD_DEFAULT));
|
||||
}
|
||||
|
||||
public function ValidatePassword(
|
||||
#[\SensitiveParameter]
|
||||
string $sPassword
|
||||
) : bool
|
||||
public function ValidatePassword(\SnappyMail\SensitiveString $oPassword) : bool
|
||||
{
|
||||
$sConfigPassword = (string) $this->Get('security', 'admin_password', '');
|
||||
if (32 == \strlen($sPassword) && \md5(APP_SALT.$sPassword.APP_SALT) === $sConfigPassword) {
|
||||
$this->SetPassword($sPassword);
|
||||
return true;
|
||||
}
|
||||
return \strlen($sPassword) && \password_verify($sPassword, $sConfigPassword);
|
||||
return \strlen($oPassword) && \password_verify($oPassword, $this->Get('security', 'admin_password', ''));
|
||||
}
|
||||
|
||||
public function Save() : bool
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ abstract class Account implements \JsonSerializable
|
|||
|
||||
private string $sLogin = '';
|
||||
|
||||
private ?SensitiveString $sPassword = null;
|
||||
private ?SensitiveString $oPassword = null;
|
||||
|
||||
private string $sSmtpLogin = '';
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ abstract class Account implements \JsonSerializable
|
|||
|
||||
public function IncPassword() : string
|
||||
{
|
||||
return $this->sPassword ? $this->sPassword->getValue() : '';
|
||||
return $this->oPassword ? $this->oPassword->getValue() : '';
|
||||
}
|
||||
|
||||
public function OutLogin() : string
|
||||
|
|
@ -66,16 +66,13 @@ abstract class Account implements \JsonSerializable
|
|||
$this->sEmail,
|
||||
$this->sLogin,
|
||||
// \json_encode($this->Domain()),
|
||||
// $this->sPassword
|
||||
// $this->oPassword
|
||||
]));
|
||||
}
|
||||
|
||||
public function SetPassword(
|
||||
#[\SensitiveParameter]
|
||||
string $sPassword
|
||||
) : void
|
||||
public function SetPassword(SensitiveString $oPassword) : void
|
||||
{
|
||||
$this->sPassword = new SensitiveString($sPassword);
|
||||
$this->oPassword = $oPassword;
|
||||
}
|
||||
|
||||
public function SetSmtpPassword(
|
||||
|
|
@ -125,12 +122,11 @@ abstract class Account implements \JsonSerializable
|
|||
|
||||
public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions,
|
||||
string $sEmail, string $sLogin,
|
||||
#[\SensitiveParameter]
|
||||
string $sPassword,
|
||||
SensitiveString $oPassword,
|
||||
bool $bThrowException = false): ?self
|
||||
{
|
||||
$oAccount = null;
|
||||
if ($sEmail && $sLogin && $sPassword) {
|
||||
if ($sEmail && $sLogin && \strlen($oPassword)) {
|
||||
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
|
||||
if ($oDomain) {
|
||||
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
|
||||
|
|
@ -138,7 +134,7 @@ abstract class Account implements \JsonSerializable
|
|||
|
||||
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
|
||||
$oAccount->SetPassword($sPassword);
|
||||
$oAccount->SetPassword($oPassword);
|
||||
$oAccount->oDomain = $oDomain;
|
||||
|
||||
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
|
||||
|
|
@ -194,25 +190,22 @@ abstract class Account implements \JsonSerializable
|
|||
$oActions,
|
||||
$aAccountHash['email'],
|
||||
$aAccountHash['login'],
|
||||
$aAccountHash['pass'],
|
||||
new SensitiveString($aAccountHash['pass']),
|
||||
$bThrowExceptionOnFalse
|
||||
);
|
||||
if ($oAccount) {
|
||||
if (isset($aAccountHash['name'])) {
|
||||
$oAccount->sName = $aAccountHash['name'];
|
||||
}
|
||||
$oActions->logMask($oAccount->IncPassword());
|
||||
// init smtp user/password
|
||||
if (isset($aAccountHash['smtp'])) {
|
||||
$oAccount->sSmtpLogin = $aAccountHash['smtp']['user'];
|
||||
$oAccount->SetSmtpPassword($aAccountHash['smtp']['pass']);
|
||||
$oActions->logMask($oAccount->sSmtpPassword);
|
||||
}
|
||||
// init proxy user/password
|
||||
if (isset($aAccountHash['proxy'])) {
|
||||
$oAccount->sProxyAuthUser = $aAccountHash['proxy']['user'];
|
||||
$oAccount->SetProxyAuthPassword($aAccountHash['proxy']['pass']);
|
||||
$oActions->logMask($oAccount->sProxyAuthPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,7 +233,7 @@ abstract class Account implements \JsonSerializable
|
|||
$oImapClient->Connect($oSettings);
|
||||
$oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $oSettings));
|
||||
|
||||
$oSettings->Password = $this->sPassword;
|
||||
$oSettings->Password = $this->oPassword;
|
||||
return $this->netClientLogin($oImapClient, $oPlugins);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +257,7 @@ abstract class Account implements \JsonSerializable
|
|||
throw new RequireCredentialsException
|
||||
}
|
||||
*/
|
||||
$oSettings->Password = $this->sSmtpPassword ?: $this->sPassword;
|
||||
$oSettings->Password = $this->sSmtpPassword ?: $this->oPassword;
|
||||
return $this->netClientLogin($oSmtpClient, $oPlugins);
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +272,7 @@ abstract class Account implements \JsonSerializable
|
|||
$oSieveClient->Connect($oSettings);
|
||||
$oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $oSettings));
|
||||
|
||||
$oSettings->Password = $this->sPassword;
|
||||
$oSettings->Password = $this->oPassword;
|
||||
return $this->netClientLogin($oSieveClient, $oPlugins);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,16 @@ class Identity implements \JsonSerializable
|
|||
$this->sEmail = $sEmail;
|
||||
}
|
||||
|
||||
function __get(string $name)
|
||||
{
|
||||
if (!\property_exists($this, $name)) {
|
||||
$name = \substr($name, 1);
|
||||
}
|
||||
if (\property_exists($this, $name)) {
|
||||
return $this->$name;
|
||||
}
|
||||
}
|
||||
|
||||
function toMime() : \MailSo\Mime\Email
|
||||
{
|
||||
return new \MailSo\Mime\Email($this->sEmail, $this->sName);
|
||||
|
|
|
|||
|
|
@ -112,24 +112,16 @@ class ServiceActions
|
|||
|
||||
$this->oActions->logWrite('Action: '.$sMethodName, \LOG_INFO, 'JSON');
|
||||
|
||||
$aPost = $_POST ?? null;
|
||||
if ($aPost) {
|
||||
$this->oActions->SetActionParams($aPost, $sMethodName);
|
||||
if ($_POST) {
|
||||
$this->oActions->SetActionParams($_POST, $sMethodName);
|
||||
$aPost = $_POST;
|
||||
foreach ($aPost as $key => $value) {
|
||||
if (false !== \stripos($key, 'Password')) {
|
||||
// password & passphrase
|
||||
if (false !== \stripos($key, 'pass')) {
|
||||
$aPost[$key] = '*******';
|
||||
// $this->oActions->logMask($value);
|
||||
}
|
||||
}
|
||||
/*
|
||||
switch ($sMethodName)
|
||||
{
|
||||
case 'DoLogin':
|
||||
case 'DoAdminLogin':
|
||||
case 'DoAccountAdd':
|
||||
$this->oActions->logMask($this->oActions->GetActionParam('Password', ''));
|
||||
break;
|
||||
}
|
||||
*/
|
||||
$this->oActions->logWrite(Utils::jsonEncode($aPost), \LOG_INFO, 'POST');
|
||||
} else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) {
|
||||
$this->oActions->SetActionParams(array(
|
||||
|
|
@ -561,14 +553,14 @@ class ServiceActions
|
|||
(0 === $aData['Time'] || \time() - 10 < $aData['Time']))
|
||||
{
|
||||
$sEmail = \trim($aData['Email']);
|
||||
$sPassword = $aData['Password'];
|
||||
$oPassword = new \SnappyMail\SensitiveString($aData['Password']);
|
||||
|
||||
$aAdditionalOptions = (isset($aData['AdditionalOptions']) && \is_array($aData['AdditionalOptions']))
|
||||
? $aData['AdditionalOptions'] : [];
|
||||
|
||||
try
|
||||
{
|
||||
$oAccount = $this->oActions->LoginProcess($sEmail, $sPassword);
|
||||
$oAccount = $this->oActions->LoginProcess($sEmail, $oPassword);
|
||||
|
||||
if ($aAdditionalOptions) {
|
||||
$bNeedToSettings = false;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
namespace SnappyMail\GPG;
|
||||
|
||||
use SnappyMail\SensitiveString;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
const
|
||||
|
|
@ -64,7 +66,13 @@ abstract class Base
|
|||
|
||||
function __construct(string $homedir)
|
||||
{
|
||||
$this->options['homedir'] = \rtrim($homedir, '/\\');
|
||||
$homedir = \rtrim($homedir, '/\\');
|
||||
// BSD 4.4 max length
|
||||
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
|
||||
throw new \Exception('Socket name for S.gpg-agent.extra is too long');
|
||||
}
|
||||
$this->options['homedir'] = $homedir;
|
||||
// \putenv("GNUPGHOME={$homedir}");
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
|
|
@ -169,22 +177,13 @@ abstract class Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Exports a public key
|
||||
* Exports a public or private key
|
||||
*/
|
||||
public function export(string $fingerprint) /*: string|false*/
|
||||
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a private key
|
||||
*/
|
||||
public function exportPrivateKey(string $fingerprint) /*: string|false*/
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Imports a key
|
||||
*/
|
||||
|
|
@ -209,12 +208,11 @@ abstract class Base
|
|||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function keyInfo(string $pattern, int $private = 0) : array
|
||||
public function keyInfo(string $pattern, bool $private = false) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the mode for error_reporting
|
||||
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
|
||||
|
|
@ -243,7 +241,7 @@ abstract class Base
|
|||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -284,10 +282,7 @@ abstract class Base
|
|||
/**
|
||||
* Add a key for decryption
|
||||
*/
|
||||
public function addDecryptKey(string $fingerprint,
|
||||
#[\SensitiveParameter]
|
||||
string $passphrase
|
||||
) : bool
|
||||
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool
|
||||
{
|
||||
$this->decryptKeys[$fingerprint] = $passphrase;
|
||||
// $this->decryptKeys[\substr($fingerprint, -16)] = $passphrase;
|
||||
|
|
@ -306,10 +301,7 @@ abstract class Base
|
|||
/**
|
||||
* Add a key for signing
|
||||
*/
|
||||
public function addSignKey(string $fingerprint,
|
||||
#[\SensitiveParameter]
|
||||
string $passphrase
|
||||
) : bool
|
||||
public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool
|
||||
{
|
||||
$this->signKeys[$fingerprint] = $passphrase;
|
||||
// $this->signKeys[\substr($fingerprint, -16)] = $passphrase;
|
||||
|
|
@ -356,10 +348,7 @@ abstract class Base
|
|||
];
|
||||
}
|
||||
|
||||
public function addPassphrase($keyId,
|
||||
#[\SensitiveParameter]
|
||||
$passphrase
|
||||
)
|
||||
public function addPassphrase($keyId, SensitiveString $passphrase)
|
||||
{
|
||||
$this->passphrases[$keyId] = $passphrase;
|
||||
return $this;
|
||||
|
|
@ -367,10 +356,11 @@ abstract class Base
|
|||
|
||||
/**
|
||||
* Toggle the armored output
|
||||
* When true the output is ASCII
|
||||
*/
|
||||
public function setArmor(int $armor = 1) : bool
|
||||
public function setArmor(bool $armor = true) : bool
|
||||
{
|
||||
$this->armor = !!$armor;
|
||||
$this->armor = $armor;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
namespace SnappyMail\GPG;
|
||||
|
||||
class PGP extends Base
|
||||
use SnappyMail\SensitiveString;
|
||||
|
||||
class PGP extends Base implements \SnappyMail\PGP\PGPInterface
|
||||
{
|
||||
private
|
||||
$_message,
|
||||
|
|
@ -95,7 +97,7 @@ class PGP extends Base
|
|||
$fclose = $this->setOutput($output);
|
||||
|
||||
if ($this->decryptKeys) {
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->decryptKeys);
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->decryptKeys));
|
||||
}
|
||||
|
||||
$result = $this->exec(['--decrypt','--skip-verify']);
|
||||
|
|
@ -137,6 +139,7 @@ class PGP extends Base
|
|||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
// \rewind($fp);
|
||||
return $this->_decrypt($fp, $output);
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +218,7 @@ class PGP extends Base
|
|||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
\rewind($fp);
|
||||
return $this->_encrypt($fp, $output);
|
||||
}
|
||||
|
||||
|
|
@ -234,14 +238,14 @@ class PGP extends Base
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function _exportKey($keyId, $private = false)
|
||||
protected function _exportKey($keyId, bool $private = false)
|
||||
{
|
||||
$keys = $this->keyInfo($keyId, $private ? 1 : 0);
|
||||
$keys = $this->keyInfo($keyId, $private);
|
||||
if (!$keys) {
|
||||
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
|
||||
}
|
||||
if ($private && $this->passphrases) {
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases);
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->passphrases));
|
||||
}
|
||||
$result = $this->exec([
|
||||
$private ? '--export-secret-keys' : '--export',
|
||||
|
|
@ -252,21 +256,19 @@ class PGP extends Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Exports a public key
|
||||
* Exports a public or private key
|
||||
*/
|
||||
public function export(string $fingerprint) /*: string|false*/
|
||||
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
|
||||
{
|
||||
// \SnappyMail\Log::debug('GnuPG', "export({$fingerprint}, {$passphrase})");
|
||||
if (null !== $passphrase) {
|
||||
return $this
|
||||
->addPassphrase($fingerprint, $passphrase)
|
||||
->_exportKey($fingerprint, true);
|
||||
}
|
||||
return $this->_exportKey($fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a private key
|
||||
*/
|
||||
public function exportPrivateKey(string $fingerprint) /*: string|false*/
|
||||
{
|
||||
return $this->_exportKey($fingerprint, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errortext, if a function fails
|
||||
*/
|
||||
|
|
@ -296,8 +298,13 @@ class PGP extends Base
|
|||
* Also saves revocation certificate in {homedir}/openpgp-revocs.d/
|
||||
* https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html
|
||||
*/
|
||||
public function generateKey(PGPKeySettings $settings) /*: string|false*/
|
||||
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/
|
||||
{
|
||||
$settings = new PGPKeySettings;
|
||||
$settings->name = $uid;
|
||||
$settings->email = $uid;
|
||||
$settings->passphrase = $passphrase;
|
||||
|
||||
$arguments = [
|
||||
'--batch',
|
||||
'--yes',
|
||||
|
|
@ -362,7 +369,7 @@ class PGP extends Base
|
|||
$arguments = ['--import'];
|
||||
|
||||
if ($this->passphrases) {
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases);
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->passphrases));
|
||||
} else {
|
||||
$arguments[] = '--batch';
|
||||
}
|
||||
|
|
@ -421,13 +428,13 @@ class PGP extends Base
|
|||
}
|
||||
}
|
||||
|
||||
public function deleteKey(string $keyId, bool $private)
|
||||
public function deleteKey(string $keyId, bool $private) : bool
|
||||
{
|
||||
$key = $this->keyInfo($keyId, $private ? 1 : 0);
|
||||
$key = $this->keyInfo($keyId, $private);
|
||||
if (!$key) {
|
||||
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
|
||||
}
|
||||
// if (!$private && $this->keyInfo($keyId, 1)) {
|
||||
// if (!$private && $this->keyInfo($keyId, true)) {
|
||||
// throw new \Exception('Delete private key first: ' . $keyId);
|
||||
// }
|
||||
|
||||
|
|
@ -448,7 +455,7 @@ class PGP extends Base
|
|||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function keyInfo(string $pattern, int $private = 0) : array
|
||||
public function keyInfo(string $pattern, bool $private = false) : array
|
||||
{
|
||||
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
|
||||
// double '--with-fingerprint' also prints the fingerprint for subkeys.
|
||||
|
|
@ -572,6 +579,30 @@ class PGP extends Base
|
|||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function allKeysInfo(string $pattern) : array
|
||||
{
|
||||
$keys = [
|
||||
'public' => [],
|
||||
'private' => []
|
||||
];
|
||||
// Public
|
||||
foreach (($this->keyinfo($pattern) ?: []) as $key) {
|
||||
$key['can_verify'] = $key['can_sign'];
|
||||
unset($key['can_sign']);
|
||||
$keys['public'][] = $key;
|
||||
}
|
||||
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
|
||||
foreach (($this->keyinfo($pattern, 1) ?: []) as $key) {
|
||||
$key['can_decrypt'] = $key['can_encrypt'];
|
||||
unset($key['can_encrypt']);
|
||||
$keys['private'][] = $key;
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode for error_reporting
|
||||
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
|
||||
|
|
@ -629,7 +660,7 @@ class PGP extends Base
|
|||
foreach ($this->signKeys as $fingerprint => $pass) {
|
||||
$arguments[] = '--local-user ' . \escapeshellarg($fingerprint);
|
||||
}
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->signKeys);
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode(\array_map('strval', $this->signKeys));
|
||||
}
|
||||
|
||||
$result = $this->exec($arguments);
|
||||
|
|
@ -666,15 +697,16 @@ class PGP extends Base
|
|||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
\rewind($fp);
|
||||
return $this->_sign($fp, $output);
|
||||
}
|
||||
|
||||
protected function _verify($input, string $signature)
|
||||
protected function _verify($input, string $signature) /*: array|false*/
|
||||
{
|
||||
$arguments = ['--verify'];
|
||||
if ($signature) {
|
||||
|
|
@ -768,6 +800,7 @@ class PGP extends Base
|
|||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
// \rewind($fp);
|
||||
return $this->_verify($fp, $signature);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
namespace SnappyMail\GPG;
|
||||
|
||||
use SnappyMail\SensitiveString;
|
||||
|
||||
class SMIME extends Base
|
||||
{
|
||||
private
|
||||
|
|
@ -235,9 +237,9 @@ class SMIME extends Base
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function _exportKey($keyId, $private = false)
|
||||
protected function _exportKey($keyId, bool $private = false)
|
||||
{
|
||||
$keys = $this->keyInfo($keyId, $private ? 1 : 0);
|
||||
$keys = $this->keyInfo($keyId, $private);
|
||||
if (!$keys) {
|
||||
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
|
||||
}
|
||||
|
|
@ -253,21 +255,18 @@ class SMIME extends Base
|
|||
}
|
||||
|
||||
/**
|
||||
* Exports a public key
|
||||
* Exports a public or private key
|
||||
*/
|
||||
public function export(string $fingerprint) /*: string|false*/
|
||||
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
|
||||
{
|
||||
if ($passphrase) {
|
||||
return $this
|
||||
->addPassphrase($fingerprint, $passphrase)
|
||||
->_exportKey($fingerprint, true);
|
||||
}
|
||||
return $this->_exportKey($fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a private key
|
||||
*/
|
||||
public function exportPrivateKey(string $fingerprint) /*: string|false*/
|
||||
{
|
||||
return $this->_exportKey($fingerprint, true);
|
||||
}
|
||||
|
||||
protected function _importKey($input) /*: array|false*/
|
||||
{
|
||||
$arguments = ['--import'];
|
||||
|
|
@ -334,7 +333,7 @@ class SMIME extends Base
|
|||
|
||||
public function deleteKey(string $keyId, bool $private)
|
||||
{
|
||||
$key = $this->keyInfo($keyId, $private ? 1 : 0);
|
||||
$key = $this->keyInfo($keyId, $private);
|
||||
if (!$key) {
|
||||
throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
|
||||
}
|
||||
|
|
@ -359,7 +358,7 @@ class SMIME extends Base
|
|||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function keyInfo(string $pattern, int $private = 0) : array
|
||||
public function keyInfo(string $pattern, bool $private = false) : array
|
||||
{
|
||||
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
|
||||
// double '--with-fingerprint' also prints the fingerprint for subkeys.
|
||||
|
|
@ -540,7 +539,7 @@ class SMIME extends Base
|
|||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
|
|
@ -548,7 +547,7 @@ class SMIME extends Base
|
|||
return $this->_sign($fp, $output);
|
||||
}
|
||||
|
||||
protected function _verify($input, string $signature)
|
||||
protected function _verify($input, string $signature) /*: array|false*/
|
||||
{
|
||||
$arguments = ['--verify'];
|
||||
if ($signature) {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ abstract class Request
|
|||
const
|
||||
/**
|
||||
* Authentication
|
||||
* These are bitwise options
|
||||
*/
|
||||
AUTH_BASIC = 1,
|
||||
AUTH_DIGEST = 2,
|
||||
AUTH_BEARER = 3;
|
||||
AUTH_BEARER = 4;
|
||||
|
||||
public
|
||||
$timeout = 5, // timeout in seconds.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ class Response
|
|||
if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
|
||||
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
|
||||
$this->body = \gzinflate(\substr($body, 10, -4));
|
||||
if (false === $this->body) {
|
||||
$err = \error_get_last() ?: ['message' => 'gzinflate failed'];
|
||||
throw new \RuntimeException("{$err['message']} for {$request_uri}");
|
||||
}
|
||||
} else {
|
||||
$this->body = $body;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,423 +8,26 @@ defined('GNUPG_SIG_MODE_CLEAR') || define('GNUPG_SIG_MODE_CLEAR', 2);
|
|||
|
||||
use SnappyMail\GPG\PGP as GPG;
|
||||
|
||||
class GnuPG
|
||||
abstract class GnuPG
|
||||
{
|
||||
private
|
||||
$homedir,
|
||||
// Instance of gnupg pecl extension https://www.php.net/gnupg
|
||||
$GnuPG,
|
||||
// Instance of \SnappyMail\GPG\PGP
|
||||
$GPG;
|
||||
|
||||
function __construct(string $homedir)
|
||||
{
|
||||
$homedir = \rtrim($homedir, '/\\');
|
||||
// BSD 4.4 max length
|
||||
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
|
||||
throw new \Exception('socket name for S.gpg-agent.extra is too long');
|
||||
}
|
||||
$this->homedir = $homedir;
|
||||
// \putenv("GNUPGHOME={$homedir}");
|
||||
|
||||
if (\class_exists('gnupg') && \version_compare(\phpversion('gnupg'), '1.5', '>=')) {
|
||||
$this->GnuPG = new \gnupg([
|
||||
// It is the file name of the executable program implementing this protocol which is usually path of the gpg executable.
|
||||
// 'file_name' => '/usr/bin/gpg',
|
||||
// It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose.
|
||||
'home_dir' => $homedir
|
||||
]);
|
||||
// Output is ASCII
|
||||
$this->GnuPG->setarmor(1);
|
||||
} else {
|
||||
$this->getGPG();
|
||||
}
|
||||
|
||||
/*
|
||||
$conf = "{$homedir}/gpg-agent.conf";
|
||||
if (!\file_exists($conf)) {
|
||||
\file_put_contents($conf, 'default-cache-ttl 1');
|
||||
}
|
||||
$conf = "{$homedir}/gpg.conf";
|
||||
if (!\file_exists($conf)) {
|
||||
\file_put_contents($conf, "batch\nno-comments");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public static function isSupported() : bool
|
||||
{
|
||||
return \class_exists('gnupg') || GPG::isSupported();
|
||||
return GPG::isSupported();
|
||||
}
|
||||
|
||||
private static $instance;
|
||||
public static function getInstance(string $homedir) : ?self
|
||||
public static function getInstance(string $homedir) : ?PGPInterface
|
||||
{
|
||||
if (!static::$instance) {
|
||||
static::$instance = new self($homedir);
|
||||
if (GPG::isSupported()) {
|
||||
static::$instance = new GPG($homedir);
|
||||
}
|
||||
/*
|
||||
else if (PECL::isSupported()) {
|
||||
static::$instance = new PECL($homedir);
|
||||
}
|
||||
*/
|
||||
}
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public function handler()
|
||||
{
|
||||
return $this->GnuPG ?: $this->GPG;
|
||||
}
|
||||
|
||||
public function getGPG(bool $throw = true) : ?GPG
|
||||
{
|
||||
if (!$this->GPG) {
|
||||
if (GPG::isSupported()) {
|
||||
$this->GPG = new GPG($this->homedir);
|
||||
} else if ($throw) {
|
||||
throw new \Exception('GnuPG not supported');
|
||||
}
|
||||
}
|
||||
return $this->GPG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for decryption
|
||||
*/
|
||||
public function addDecryptKey(string $fingerprint,
|
||||
#[\SensitiveParameter]
|
||||
string $passphrase
|
||||
) : bool
|
||||
{
|
||||
return $this->handler()->adddecryptkey($fingerprint, $passphrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for encryption
|
||||
*/
|
||||
public function addEncryptKey(string $fingerprint) : bool
|
||||
{
|
||||
return $this->handler()->addencryptkey($fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for signing
|
||||
*/
|
||||
public function addSignKey(string $fingerprint,
|
||||
#[\SensitiveParameter]
|
||||
?string $passphrase
|
||||
) : bool
|
||||
{
|
||||
return $this->handler()->addsignkey($fingerprint, $passphrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for decryption before
|
||||
*/
|
||||
public function clearDecryptKeys() : bool
|
||||
{
|
||||
return $this->handler()->cleardecryptkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for encryption before
|
||||
*/
|
||||
public function clearEncryptKeys() : bool
|
||||
{
|
||||
return $this->handler()->clearencryptkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for signing before
|
||||
*/
|
||||
public function clearSignKeys() : bool
|
||||
{
|
||||
return $this->handler()->clearsignkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given text
|
||||
*/
|
||||
public function decrypt(string $text) /*: string|false */
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->decrypt($text)
|
||||
: $this->GPG->decrypt($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given file
|
||||
*/
|
||||
public function decryptFile(string $filename) /*: string|false */
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->decrypt(\file_get_contents($filename))
|
||||
: $this->GPG->decryptFile($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given resource
|
||||
*/
|
||||
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->decrypt(\stream_get_contents($fp))
|
||||
: $this->GPG->decryptStream($fp, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts and verifies a given text
|
||||
*/
|
||||
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->decryptverify($text, $plaintext)
|
||||
: $this->GPG->decryptverify($text, $plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts and verifies a given file
|
||||
*/
|
||||
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext)
|
||||
: $this->GPG->decryptverifyFile($filename, $plaintext);
|
||||
}
|
||||
|
||||
public function deleteKey(string $keyId, bool $private) : bool
|
||||
{
|
||||
return $this->getGPG()->deleteKey($keyId, $private);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encrypt(string $plaintext) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->encrypt($plaintext)
|
||||
: $this->GPG->encrypt($plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encryptFile(string $filename) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->encrypt(\file_get_contents($filename))
|
||||
: $this->GPG->encryptFile($filename);
|
||||
}
|
||||
|
||||
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
\rewind($fp);
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->encrypt(\stream_get_contents($fp))
|
||||
: $this->GPG->encryptStream($fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a key
|
||||
*/
|
||||
public function export(string $fingerprint,
|
||||
#[\SensitiveParameter]
|
||||
string $passphrase = ''
|
||||
) /*: string|false*/
|
||||
{
|
||||
if ($passphrase) {
|
||||
return $this->getGPG()
|
||||
->addPassphrase($fingerprint, $passphrase)
|
||||
->exportPrivateKey($fingerprint);
|
||||
}
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->export($fingerprint)
|
||||
: $this->GPG->export($fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the engine info
|
||||
*/
|
||||
public function getEngineInfo() : array
|
||||
{
|
||||
return $this->handler()->getengineinfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errortext, if a function fails
|
||||
*/
|
||||
public function getError() /*: string|false*/
|
||||
{
|
||||
return $this->handler()->geterror();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error info
|
||||
*/
|
||||
public function getErrorInfo() : array
|
||||
{
|
||||
return $this->handler()->geterrorinfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active protocol for all operations
|
||||
*/
|
||||
public function getProtocol() : int
|
||||
{
|
||||
return $this->handler()->getprotocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key
|
||||
*/
|
||||
public function generateKey(string $uid,
|
||||
#[\SensitiveParameter]
|
||||
string $passphrase
|
||||
) /*: string|false*/
|
||||
{
|
||||
$GPG = $this->getGPG(false);
|
||||
return $GPG ? $GPG->generateKey($uid, $passphrase) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a key
|
||||
*/
|
||||
public function import(string $keydata) /*: array|false*/
|
||||
{
|
||||
return $this->handler()->import($keydata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a key
|
||||
*/
|
||||
public function importFile(string $filename) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->import(\file_get_contents($filename))
|
||||
: $this->GPG->importFile($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function keyInfo(string $pattern) : array
|
||||
{
|
||||
$keys = [
|
||||
'public' => [],
|
||||
'private' => []
|
||||
];
|
||||
// Public
|
||||
foreach (($this->handler()->keyinfo($pattern) ?: []) as $key) {
|
||||
$key['can_verify'] = $key['can_sign'];
|
||||
unset($key['can_sign']);
|
||||
$keys['public'][] = $key;
|
||||
}
|
||||
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
|
||||
foreach (($this->handler()->keyinfo($pattern, 1) ?: []) as $key) {
|
||||
$key['can_decrypt'] = $key['can_encrypt'];
|
||||
unset($key['can_encrypt']);
|
||||
$keys['private'][] = $key;
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle armored output
|
||||
* When true the output is ASCII
|
||||
*/
|
||||
public function setArmor(bool $armor = true) : bool
|
||||
{
|
||||
return $this->handler()->setarmor($armor ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode for error_reporting
|
||||
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
|
||||
* By default GNUPG_ERROR_SILENT is used.
|
||||
*/
|
||||
public function setErrorMode(int $errormode) : void
|
||||
{
|
||||
$this->handler()->seterrormode($errormode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode for signing
|
||||
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
|
||||
* By default GNUPG_SIG_MODE_CLEAR
|
||||
*/
|
||||
public function setSignMode(int $signmode) : bool
|
||||
{
|
||||
return $this->handler()->setsignmode($signmode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given text
|
||||
*/
|
||||
public function sign(string $plaintext) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->sign($plaintext)
|
||||
: $this->GPG->sign($plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signFile(string $filename) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->sign(\file_get_contents($filename))
|
||||
: $this->GPG->signFile($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
|
||||
{
|
||||
\rewind($fp);
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->sign(\stream_get_contents($fp))
|
||||
: $this->GPG->signStream($fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a signed text
|
||||
*/
|
||||
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
|
||||
{
|
||||
$result = $this->GnuPG
|
||||
? $this->GnuPG->verify($signed_text, $signature ?: false, $plaintext)
|
||||
: $this->GPG->verify($signed_text, $signature, $plaintext);
|
||||
if (!$result) {
|
||||
if ($this->GnuPG) {
|
||||
\SnappyMail\Log::notice('GnuPG', 'gnupg_verify() failed: ' . $this->GnuPG->geterror());
|
||||
\SnappyMail\Log::info('GnuPG', \print_r($this->GnuPG->geterrorinfo(),1));
|
||||
} else {
|
||||
\SnappyMail\Log::notice('GPG', 'GPG->verify() failed');
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a signed file
|
||||
*/
|
||||
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG
|
||||
? $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext)
|
||||
: $this->GPG->verifyFile($filename, $signature, $plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a given resource
|
||||
*/
|
||||
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: string|false */
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
return $this->getGPG()->verifyStream($fp, $signature, $plaintext);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
397
snappymail/v/0.0.0/app/libraries/snappymail/pgp/pecl.php
Normal file
397
snappymail/v/0.0.0/app/libraries/snappymail/pgp/pecl.php
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail\PGP;
|
||||
|
||||
defined('GNUPG_SIG_MODE_NORMAL') || define('GNUPG_SIG_MODE_NORMAL', 0);
|
||||
defined('GNUPG_SIG_MODE_DETACH') || define('GNUPG_SIG_MODE_DETACH', 1);
|
||||
defined('GNUPG_SIG_MODE_CLEAR') || define('GNUPG_SIG_MODE_CLEAR', 2);
|
||||
|
||||
use SnappyMail\GPG\PGP as GPG;
|
||||
use SnappyMail\SensitiveString;
|
||||
|
||||
class PECL implements \SnappyMail\PGP\PGPInterface
|
||||
{
|
||||
private
|
||||
$homedir,
|
||||
// Instance of gnupg pecl extension https://www.php.net/gnupg
|
||||
$GnuPG,
|
||||
// Instance of \SnappyMail\GPG\PGP
|
||||
$GPG;
|
||||
|
||||
function __construct(string $homedir)
|
||||
{
|
||||
$homedir = \rtrim($homedir, '/\\');
|
||||
// BSD 4.4 max length
|
||||
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
|
||||
throw new \Exception('Socket name for S.gpg-agent.extra is too long');
|
||||
}
|
||||
$this->homedir = $homedir;
|
||||
// \putenv("GNUPGHOME={$homedir}");
|
||||
|
||||
$this->GnuPG = new \gnupg([
|
||||
// It is the file name of the executable program implementing this protocol which is usually path of the gpg executable.
|
||||
// 'file_name' => '/usr/bin/gpg',
|
||||
// It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose.
|
||||
'home_dir' => $homedir
|
||||
]);
|
||||
// Output is ASCII
|
||||
$this->GnuPG->setarmor(1);
|
||||
/*
|
||||
$conf = "{$homedir}/gpg-agent.conf";
|
||||
if (!\file_exists($conf)) {
|
||||
\file_put_contents($conf, 'default-cache-ttl 1');
|
||||
}
|
||||
$conf = "{$homedir}/gpg.conf";
|
||||
if (!\file_exists($conf)) {
|
||||
\file_put_contents($conf, "batch\nno-comments");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
{
|
||||
$this->GnuPG->cleardecryptkeys();
|
||||
$this->GnuPG->clearsignkeys();
|
||||
}
|
||||
|
||||
public static function isSupported() : bool
|
||||
{
|
||||
return \class_exists('gnupg') && \version_compare(\phpversion('gnupg'), '1.5', '>=');
|
||||
}
|
||||
|
||||
public function gnupgError()
|
||||
{
|
||||
$error = $this->GnuPG->geterrorinfo();
|
||||
if ($error) {
|
||||
throw new \Exception("{$error['gpgme_source']} {$error['generic_message']}. {$error['gpgme_message']}", $error['gpgme_code']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getGPG(bool $throw = true) : ?GPG
|
||||
{
|
||||
if (!$this->GPG) {
|
||||
if (GPG::isSupported()) {
|
||||
$this->GPG = new GPG($this->homedir);
|
||||
} else if ($throw) {
|
||||
throw new \Exception('GPG not supported');
|
||||
}
|
||||
}
|
||||
return $this->GPG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for decryption
|
||||
*/
|
||||
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool
|
||||
{
|
||||
// \SnappyMail\Log::debug('GnuPG', "addDecryptKey({$fingerprint}, {$passphrase})");
|
||||
return $this->GnuPG->adddecryptkey($fingerprint, \strval($passphrase)) || $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for encryption
|
||||
*/
|
||||
public function addEncryptKey(string $fingerprint) : bool
|
||||
{
|
||||
return $this->GnuPG->addencryptkey($fingerprint) || $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for signing
|
||||
*/
|
||||
public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool
|
||||
{
|
||||
// \SnappyMail\Log::debug('GnuPG', "addSignKey({$fingerprint}, {$passphrase})");
|
||||
return $this->GnuPG->addsignkey($fingerprint, \strval($passphrase)) || $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for decryption before
|
||||
*/
|
||||
public function clearDecryptKeys() : bool
|
||||
{
|
||||
return $this->GnuPG->cleardecryptkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for encryption before
|
||||
*/
|
||||
public function clearEncryptKeys() : bool
|
||||
{
|
||||
return $this->GnuPG->clearencryptkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all keys which were set for signing before
|
||||
*/
|
||||
public function clearSignKeys() : bool
|
||||
{
|
||||
return $this->GnuPG->clearsignkeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given text
|
||||
*/
|
||||
public function decrypt(string $text) /*: string|false */
|
||||
{
|
||||
$result = $this->GnuPG->decrypt($text);
|
||||
(false === $result) && $this->gnupgError();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given file
|
||||
*/
|
||||
public function decryptFile(string $filename) /*: string|false */
|
||||
{
|
||||
$result = $this->GnuPG->decrypt(\file_get_contents($filename));
|
||||
(false === $result) && $this->gnupgError();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a given resource
|
||||
*/
|
||||
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
// \rewind($fp);
|
||||
$result = $this->GnuPG->decrypt(\stream_get_contents($fp));
|
||||
(false === $result) && $this->gnupgError();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts and verifies a given text
|
||||
*/
|
||||
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
|
||||
{
|
||||
$result = $this->GnuPG->decryptverify($text, $plaintext);
|
||||
(false === $result) && $this->gnupgError();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts and verifies a given file
|
||||
*/
|
||||
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
|
||||
{
|
||||
$result = $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext);
|
||||
(false === $result) && $this->gnupgError();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function deleteKey(string $keyId, bool $private) : bool
|
||||
{
|
||||
return $this->getGPG()->deleteKey($keyId, $private);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encrypt(string $plaintext) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG->encrypt($plaintext) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encryptFile(string $filename) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG->encrypt(\file_get_contents($filename)) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
\rewind($fp);
|
||||
return $this->GnuPG->encrypt(\stream_get_contents($fp)) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a key
|
||||
*/
|
||||
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/
|
||||
{
|
||||
if (null !== $passphrase) {
|
||||
return $this->getGPG()->export($fingerprint, $passphrase);
|
||||
}
|
||||
return $this->GnuPG->export($fingerprint) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the engine info
|
||||
*/
|
||||
public function getEngineInfo() : array
|
||||
{
|
||||
return $this->GnuPG->getengineinfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errortext, if a function fails
|
||||
*/
|
||||
public function getError() /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG->geterror();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error info
|
||||
*/
|
||||
public function getErrorInfo() : array
|
||||
{
|
||||
return $this->GnuPG->geterrorinfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active protocol for all operations
|
||||
*/
|
||||
public function getProtocol() : int
|
||||
{
|
||||
return $this->GnuPG->getprotocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key
|
||||
*/
|
||||
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/
|
||||
{
|
||||
$GPG = $this->getGPG(false);
|
||||
return $GPG ? $GPG->generateKey($uid, $passphrase) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a key
|
||||
*/
|
||||
public function import(string $keydata) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG->import($keydata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a key
|
||||
*/
|
||||
public function importFile(string $filename) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG->import(\file_get_contents($filename)) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
public function keyInfo(string $pattern, bool $private = false) : array
|
||||
{
|
||||
return $this->GnuPG->keyinfo($pattern, $private ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with information about all keys that matches the given pattern
|
||||
*/
|
||||
public function allKeysInfo(string $pattern) : array
|
||||
{
|
||||
$keys = [
|
||||
'public' => [],
|
||||
'private' => []
|
||||
];
|
||||
// Public
|
||||
foreach (($this->GnuPG->keyinfo($pattern) ?: []) as $key) {
|
||||
$key['can_verify'] = $key['can_sign'];
|
||||
unset($key['can_sign']);
|
||||
$keys['public'][] = $key;
|
||||
}
|
||||
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
|
||||
foreach (($this->GnuPG->keyinfo($pattern, 1) ?: []) as $key) {
|
||||
$key['can_decrypt'] = $key['can_encrypt'];
|
||||
unset($key['can_encrypt']);
|
||||
$keys['private'][] = $key;
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle armored output
|
||||
* When true the output is ASCII
|
||||
*/
|
||||
public function setArmor(bool $armor = true) : bool
|
||||
{
|
||||
return $this->GnuPG->setarmor($armor ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode for error_reporting
|
||||
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
|
||||
* By default GNUPG_ERROR_SILENT is used.
|
||||
*/
|
||||
public function setErrorMode(int $errormode) : void
|
||||
{
|
||||
$this->GnuPG->seterrormode($errormode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode for signing
|
||||
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
|
||||
* By default GNUPG_SIG_MODE_CLEAR
|
||||
*/
|
||||
public function setSignMode(int $signmode) : bool
|
||||
{
|
||||
return $this->GnuPG->setsignmode($signmode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given text
|
||||
*/
|
||||
public function sign(string $plaintext) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG->sign($plaintext) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signFile(string $filename) /*: string|false*/
|
||||
{
|
||||
return $this->GnuPG->sign(\file_get_contents($filename)) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given file
|
||||
*/
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
\rewind($fp);
|
||||
return $this->GnuPG->sign(\stream_get_contents($fp)) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a signed text
|
||||
*/
|
||||
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
|
||||
{
|
||||
$result = $this->GnuPG->verify($signed_text, $signature ?: false, $plaintext) ?: $this->gnupgError();
|
||||
if (!$result) {
|
||||
\SnappyMail\Log::notice('GnuPG', 'gnupg_verify() failed: ' . $this->GnuPG->geterror());
|
||||
\SnappyMail\Log::info('GnuPG', \print_r($this->GnuPG->geterrorinfo(),1));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a signed file
|
||||
*/
|
||||
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
|
||||
{
|
||||
return $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a given resource
|
||||
*/
|
||||
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: array|false */
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
// \rewind($fp);
|
||||
return $this->GnuPG->verify(\stream_get_contents($fp), $signature, $plaintext) ?: $this->gnupgError();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail\PGP;
|
||||
|
||||
use SnappyMail\SensitiveString;
|
||||
|
||||
interface PGPInterface
|
||||
{
|
||||
public static function isSupported() : bool;
|
||||
public function addDecryptKey(string $fingerprint, SensitiveString $passphrase) : bool;
|
||||
public function addEncryptKey(string $fingerprint) : bool;
|
||||
public function addSignKey(string $fingerprint, SensitiveString $passphrase) : bool;
|
||||
public function clearDecryptKeys() : bool;
|
||||
public function clearEncryptKeys() : bool;
|
||||
public function clearSignKeys() : bool;
|
||||
public function decrypt(string $text) /*: string|false */;
|
||||
public function decryptFile(string $filename) /*: string|false */;
|
||||
public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */;
|
||||
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/;
|
||||
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/;
|
||||
public function deleteKey(string $keyId, bool $private) : bool;
|
||||
public function encrypt(string $plaintext) /*: string|false*/;
|
||||
public function encryptFile(string $filename) /*: string|false*/;
|
||||
public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/;
|
||||
public function export(string $fingerprint, ?SensitiveString $passphrase = null) /*: string|false*/;
|
||||
public function getEngineInfo() : array;
|
||||
public function getError() /*: string|false*/;
|
||||
public function getErrorInfo() : array;
|
||||
public function getProtocol() : int;
|
||||
public function generateKey(string $uid, SensitiveString $passphrase) /*: string|false*/;
|
||||
public function import(string $keydata) /*: array|false*/;
|
||||
public function importFile(string $filename) /*: array|false*/;
|
||||
public function allKeysInfo(string $pattern) : array;
|
||||
public function setArmor(bool $armor = true) : bool;
|
||||
public function setErrorMode(int $errormode) : void;
|
||||
public function setSignMode(int $signmode) : bool;
|
||||
public function sign(string $plaintext) /*: string|false*/;
|
||||
public function signFile(string $filename) /*: string|false*/;
|
||||
public function signStream($fp, /*string|resource*/ $output = null) /*: string|false*/;
|
||||
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/;
|
||||
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/;
|
||||
public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: array|false */;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace SnappyMail;
|
||||
|
||||
class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable
|
||||
class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */ implements \Stringable, \JsonSerializable
|
||||
{
|
||||
private string $value, $nonce;
|
||||
private static ?string $key = null;
|
||||
|
|
@ -28,6 +28,7 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
|
|||
string $value
|
||||
) : void
|
||||
{
|
||||
\strlen($value) && \RainLoop\Api::Actions()->logMask($value);
|
||||
if (\is_callable('sodium_crypto_secretbox')) {
|
||||
$this->nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
if (!static::$key) {
|
||||
|
|
@ -60,6 +61,12 @@ class SensitiveString /* extends SensitiveParameterValue | SensitiveParameter */
|
|||
return $this->getValue();
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
throw new \Exception("JSON serialization of 'SnappyMail\\SensitiveString' is not allowed");
|
||||
}
|
||||
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class Certificate
|
|||
return \openssl_get_cipher_methods($aliases);
|
||||
}
|
||||
|
||||
public function createSelfSigned(string $passphrase = '') : array
|
||||
public function createSelfSigned(\SnappyMail\SensitiveString $passphrase, ?string $privateKey = null) : array
|
||||
{
|
||||
$options = array(
|
||||
'config' => __DIR__ . '/openssl.cnf',
|
||||
|
|
@ -134,7 +134,13 @@ class Certificate
|
|||
}
|
||||
|
||||
$pkey = null; // openssl_pkey_new($options);
|
||||
$csr = \openssl_csr_new($dn, $pkey, $options);
|
||||
if ($privateKey) {
|
||||
$pkey = \openssl_pkey_get_private($privateKey, $passphrase);
|
||||
if (!$pkey) {
|
||||
throw new \RuntimeException('OpenSSL pkey: ' . \openssl_error_string());
|
||||
}
|
||||
}
|
||||
$csr = \openssl_csr_new($dn, $pkey, $options);
|
||||
if ($csr) {
|
||||
$this->x509 = \openssl_csr_sign(
|
||||
$csr,
|
||||
|
|
@ -145,13 +151,11 @@ class Certificate
|
|||
);
|
||||
if ($this->x509/* && $this->canSign() && $this->canEncrypt()*/) {
|
||||
$this->pkey = $pkey;
|
||||
$privatekey = '';
|
||||
$privateKey || \openssl_pkey_export($pkey, $privateKey, $passphrase);
|
||||
$certificate = '';
|
||||
$csrStr = '';
|
||||
\openssl_pkey_export($pkey, $privatekey, $passphrase);
|
||||
\openssl_x509_export($this->x509, $certificate);
|
||||
return array(
|
||||
'pkey' => $privatekey,
|
||||
'pkey' => $privateKey,
|
||||
'x509' => $certificate,
|
||||
// 'pkcs12' => $this->asPKCS12($pkey, $passphrase/*, array $args = array()*/)
|
||||
// 'canSign' => $this->canSign(),
|
||||
|
|
@ -167,7 +171,11 @@ class Certificate
|
|||
}
|
||||
|
||||
// returns binary data
|
||||
public function asPKCS12(string $pass = '', array $args = array()) : string
|
||||
public function asPKCS12(
|
||||
#[\SensitiveParameter]
|
||||
string $pass = '',
|
||||
array $args = array()
|
||||
) : string
|
||||
{
|
||||
$out = '';
|
||||
\openssl_pkcs12_export($this->x509, $out, $this->pkey, $pass, $args);
|
||||
|
|
|
|||
|
|
@ -34,15 +34,18 @@ class OpenSSL
|
|||
{
|
||||
$data = \openssl_x509_parse(\openssl_x509_read($certificate));
|
||||
if (!$data) {
|
||||
\error_log("OpenSSL parse: " . \openssl_error_string());
|
||||
\SnappyMail\Log::error('OpenSSL', "parse: " . \openssl_error_string());
|
||||
return false;
|
||||
}
|
||||
$key = \str_replace(':', '', $data['extensions']['subjectKeyIdentifier'] ?? $data['hash']);
|
||||
$filename = "{$this->homedir}/{$key}.crt";
|
||||
if (!\file_exists($filename)) {
|
||||
if (\file_exists($filename)) {
|
||||
\SnappyMail\Log::debug('OpenSSL', "certificate {$key} already imported");
|
||||
} else {
|
||||
\file_put_contents("{$this->homedir}/{$key}.crt", $certificate);
|
||||
// \unlink("{$this->homedir}/certificates.json");
|
||||
$this->certificates(true);
|
||||
\SnappyMail\Log::debug('OpenSSL', "certificate {$key} imported");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -50,7 +53,7 @@ class OpenSSL
|
|||
public function certificates(bool $force = false) : array
|
||||
{
|
||||
$cacheFile = "{$this->homedir}/certificates.json";
|
||||
$result = \file_exists($cacheFile)
|
||||
$result = (!$force && \file_exists($cacheFile))
|
||||
? \json_decode(\file_get_contents($cacheFile), true)
|
||||
: null;
|
||||
if (!\is_array($result)) {
|
||||
|
|
@ -98,7 +101,7 @@ class OpenSSL
|
|||
}
|
||||
$result[] = $short;
|
||||
} else {
|
||||
\error_log("OpenSSL parse({$file}): " . \openssl_error_string());
|
||||
\SnappyMail\Log::error('OpenSSL', "parse({$file}): " . \openssl_error_string());
|
||||
}
|
||||
}
|
||||
\file_put_contents($cacheFile, \json_encode($result));
|
||||
|
|
@ -129,8 +132,7 @@ class OpenSSL
|
|||
}
|
||||
|
||||
public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey,
|
||||
#[\SensitiveParameter]
|
||||
?string $passphrase = null
|
||||
?\SnappyMail\SensitiveString $passphrase = null
|
||||
) : void
|
||||
{
|
||||
$this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase);
|
||||
|
|
@ -142,6 +144,19 @@ class OpenSSL
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public function asn1parse(/*string|Temporary* / $input) : ?string
|
||||
{
|
||||
if (\is_string($input)) {
|
||||
$tmp = new Temporary('smimein-');
|
||||
if (!$tmp->putContents($input)) {
|
||||
return null;
|
||||
}
|
||||
$input = $tmp;
|
||||
}
|
||||
`openssl asn1parse -in $input->filename()`
|
||||
}
|
||||
*/
|
||||
public function decrypt(/*string|Temporary*/ $input) : ?string
|
||||
{
|
||||
if (\is_string($input)) {
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "توقيع جيد من %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGP استيراد مفتاح",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME signed message",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Валиден подпис за %USER%",
|
||||
"ERROR": "%TYPE% грешка: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Внасяне на OpenPGP ключ",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Подписано с S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Шифровано с S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Ověřený podpis od %USER%",
|
||||
"ERROR": "%TYPE% chyba: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import OpenPGP klíče",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Zpráva podepsaná S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Zpráva šifrovaná S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Accepteret signatur for %USER\"",
|
||||
"ERROR": "%TYPE% fejl: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importer OpenPGP nøgle",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME signeret meddelelse",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME krypteret meddelelse",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Gültige Unterschrift von %USER%",
|
||||
"ERROR": "%TYPE%-Fehler: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGP-Schlüssel importieren",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME-signierte Nachricht",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME-verschlüsselte Nachricht",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Εισαγωγή κλειδιού OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Μήνυμα υπογεγραμμένο με S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Μήνυμα κωδικοποιημένο με S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME signed message",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S/MIME signed message",
|
||||
"ENCRYPTED_MESSAGE": "S/MIME encrypted message",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Buena firma de %USER%",
|
||||
"ERROR": "Error %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importar clave OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Mensaje firmado mediante S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Mensaje cifrado mediante S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Korralik signatuur kasutajalt %USER%",
|
||||
"ERROR": "%TYPE% viga: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Impordi OpenPGP võti",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME abil signeeritud kiri",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME abil krüpteeritud kiri",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "%USER%-en sinadura egokia",
|
||||
"ERROR": "%TYPE% errorea: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Inportatu OpenPGP gakoa",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME sinatutako mezua",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME zifratutako mezua",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "امضای صحیح از %USER%",
|
||||
"ERROR": "خطای %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "واردکردن کلید OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "پیام توسط S\/MIME امضاء شد",
|
||||
"ENCRYPTED_MESSAGE": "پیام توسط S\/MIME رمزنگاری شد",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Oikea allekirjoitus %USER% lle",
|
||||
"ERROR": "%TYPE% virhe: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Tuo OpenPGP avain",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME allekirjoitettu viesti",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME salattu visti",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Signature valide pour %USER%",
|
||||
"ERROR": "Erreur %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importer la clef OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Message signé par S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Message chiffré par S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Jó aláírás tőle: %USER%",
|
||||
"ERROR": "%TYPE% hiba: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGP kulcs importálás",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME aláírt üzenet",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME kódolt üzenet",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Tandatangan sah dari user %USER%",
|
||||
"ERROR": "Kesalahan %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Impor kunci OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Pesan bertanda-tangan S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Pesan terenkripsi S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Gild undirritun frá %USER%",
|
||||
"ERROR": "%TYPE% villa: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Flytja inn OpenPGP-lykil",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Skeyti undirritað með S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Skeyti dulritað með S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Firma valida da %USER%",
|
||||
"ERROR": "Errore di %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importa chiave OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Messaggio firmato con S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Messaggio cifrato con S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "%USER%からの正しい署名",
|
||||
"ERROR": "%TYPE% エラー: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGPキーをインポート",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME署名済みメッセージ",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME暗号化メッセージ",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "%USER%(으)로부터 수신한 양호한 서명",
|
||||
"ERROR": "%TYPE% 오류: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGP 키 가져오기",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME로 서명된 메세지입니다.",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME로 암호화된 메세지입니다.",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Tinkamas parašas nuo %USER%",
|
||||
"ERROR": "%TYPE% klaida: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importuoti OpenPGP raktą",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME pasirašytas pranešimas",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME šifruotas pranešimas",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME signed message",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Gyldig signatur fra %USER%",
|
||||
"ERROR": "%TYPE%-feil: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importer OpenPGP-nøkkel",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME-signert melding",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME-kryptert melding",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Juiste handtekening van %USER%",
|
||||
"ERROR": "%TYPE% foutmelding: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Zelfondertekend certificaat aanmaken",
|
||||
"VALID_UNTIL": "Geldig tot"
|
||||
"VALID_UNTIL": "Geldig tot",
|
||||
"PRIVATE_KEY": "Privésleutel"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importeer OpenPGP sleutel",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Importeer S\/MIME certificaat",
|
||||
"CERTIFICATE": "Certificaat",
|
||||
"CERTIFICATES": "S\/MIME Certificaten",
|
||||
"SIGNED_MESSAGE": "S\/MIME ondertekend bericht",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME versleuteld bericht",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Poprawnie podpisana przez %USER%",
|
||||
"ERROR": "Błąd %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Utwórz samodzielnie podpisany certyfikat",
|
||||
"VALID_UNTIL": "Ważny do"
|
||||
"VALID_UNTIL": "Ważny do",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importowanie klucza OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Importuj certyfikat S\/MIME",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "Certyfikaty S\/MIME",
|
||||
"SIGNED_MESSAGE": "Wiadomość podpisana S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Wiadomość zaszyfrowana S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
|
||||
"ERROR": "Erro %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Mensagem criptografada com S\/MIME",
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@
|
|||
"HAS_VIRUS_WARNING": "AVISO: vírus detetado",
|
||||
"TAGS": "Etiquetas",
|
||||
"NEW_TAG": "Nova etiqueta",
|
||||
"ENABLE_TRACKING_LINKS": "Enable tracking links",
|
||||
"TRACKING_ENABLED": "Links are now tracked when clicked!"
|
||||
"ENABLE_TRACKING_LINKS": "Ativar ligações de rastreamento",
|
||||
"TRACKING_ENABLED": "As ligações irão ser rastreadas quando clicadas!"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Importante",
|
||||
|
|
@ -255,7 +255,7 @@
|
|||
"POPUPS_IDENTITY": {
|
||||
"TITLE_ADD_IDENTITY": "Adicionar identidade?",
|
||||
"TITLE_UPDATE_IDENTITY": "Atualizar identidade?",
|
||||
"LABEL": "Label",
|
||||
"LABEL": "Rótulo",
|
||||
"BUTTON_ADD_IDENTITY": "Adicionar",
|
||||
"BUTTON_UPDATE_IDENTITY": "Atualizar",
|
||||
"SIGNATURE": "Assinatura",
|
||||
|
|
@ -281,8 +281,9 @@
|
|||
"VERIFY": "Validar",
|
||||
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
|
||||
"ERROR": "Erro %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"CREATE_SELF_SIGNED": "Criar auto-assinado",
|
||||
"VALID_UNTIL": "Válido até",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
|
||||
|
|
@ -295,18 +296,19 @@
|
|||
"SIGNED_MESSAGE": "Mensagem assinada com OpenPGP",
|
||||
"ENCRYPTED_MESSAGE": "Mensagem encriptada com OpenPGP",
|
||||
"STORE_IN_GNUPG": "Guardar no servidor em GnuPG",
|
||||
"STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG",
|
||||
"STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG",
|
||||
"STORE_PUBLIC_KEY_IN_GNUPG": "Guardar a chave pública no servidor em GnuPG",
|
||||
"STORE_PRIVATE_KEY_IN_GNUPG": "Guardar a chave privada no servidor em GnuPG",
|
||||
"BACKUP_ON_SERVER": "Guardar (encriptado) no servidor",
|
||||
"BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server",
|
||||
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
|
||||
"BACKUP_PUBLIC_KEY_ON_SERVER": "Fazer cópia de segurança da chave pública no servidor",
|
||||
"BACKUP_PRIVATE_KEY_ON_SERVER": "Fazer cópia de segurança da chave privada no servidor"
|
||||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"POPUP_IMPORT_TITLE": "Importar certificado S\/MIME",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "Certificados S\/MIME",
|
||||
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",
|
||||
"PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%"
|
||||
"PRIVATE_KEY_OF": "Chave privada S\/MIME de %EMAIL%"
|
||||
},
|
||||
"POPUPS_FILTER": {
|
||||
"TITLE_CREATE_FILTER": "Criar um filtro?",
|
||||
|
|
@ -405,7 +407,7 @@
|
|||
"IMAGEUPLOAD": "Escolher imagem",
|
||||
"UNDO": "Desfazer",
|
||||
"REDO": "Refazer",
|
||||
"REMOVESTYLE": "Remove style"
|
||||
"REMOVESTYLE": "Remover estilo"
|
||||
},
|
||||
"SETTINGS_LABELS": {
|
||||
"GENERAL": "Geral",
|
||||
|
|
@ -530,7 +532,7 @@
|
|||
"SETTINGS_OPENPGP": {
|
||||
"BUTTON_IMPORT_KEY": "Importar chave",
|
||||
"BUTTON_GENERATE_KEY_PAIR": "Gerar chaves",
|
||||
"IMPORT_FROM_SERVER": "Import from server",
|
||||
"IMPORT_FROM_SERVER": "Importar de servidor",
|
||||
"TITLE_PRIVATE": "Privada",
|
||||
"TITLE_PUBLIC": "Pública",
|
||||
"GET_MAILVELOPE": "Obter extensão Mailvelope para o navegador."
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@
|
|||
"HAS_VIRUS_WARNING": "AVISO: vírus detetado",
|
||||
"TAGS": "Etiquetas",
|
||||
"NEW_TAG": "Nova etiqueta",
|
||||
"ENABLE_TRACKING_LINKS": "Enable tracking links",
|
||||
"TRACKING_ENABLED": "Links are now tracked when clicked!"
|
||||
"ENABLE_TRACKING_LINKS": "Ativar ligações de rastreamento",
|
||||
"TRACKING_ENABLED": "As ligações irão ser rastreadas quando clicadas!"
|
||||
},
|
||||
"MESSAGE_TAGS": {
|
||||
"$important": "Importante",
|
||||
|
|
@ -255,7 +255,7 @@
|
|||
"POPUPS_IDENTITY": {
|
||||
"TITLE_ADD_IDENTITY": "Adicionar identidade?",
|
||||
"TITLE_UPDATE_IDENTITY": "Atualizar identidade?",
|
||||
"LABEL": "Label",
|
||||
"LABEL": "Rótulo",
|
||||
"BUTTON_ADD_IDENTITY": "Adicionar",
|
||||
"BUTTON_UPDATE_IDENTITY": "Atualizar",
|
||||
"SIGNATURE": "Assinatura",
|
||||
|
|
@ -281,8 +281,9 @@
|
|||
"VERIFY": "Validar",
|
||||
"GOOD_SIGNATURE": "Boa assinatura de %USER%",
|
||||
"ERROR": "Erro %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"CREATE_SELF_SIGNED": "Criar auto-assinado",
|
||||
"VALID_UNTIL": "Válido até",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importar chave OpenPGP",
|
||||
|
|
@ -295,18 +296,19 @@
|
|||
"SIGNED_MESSAGE": "Mensagem assinada com OpenPGP",
|
||||
"ENCRYPTED_MESSAGE": "Mensagem encriptada com OpenPGP",
|
||||
"STORE_IN_GNUPG": "Guardar no servidor em GnuPG",
|
||||
"STORE_PUBLIC_KEY_IN_GNUPG": "Store public key on server in GnuPG",
|
||||
"STORE_PRIVATE_KEY_IN_GNUPG": "Store private key on server in GnuPG",
|
||||
"STORE_PUBLIC_KEY_IN_GNUPG": "Guardar a chave pública no servidor em GnuPG",
|
||||
"STORE_PRIVATE_KEY_IN_GNUPG": "Guardar a chave privada no servidor em GnuPG",
|
||||
"BACKUP_ON_SERVER": "Guardar (encriptado) no servidor",
|
||||
"BACKUP_PUBLIC_KEY_ON_SERVER": "Backup public key on server",
|
||||
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
|
||||
"BACKUP_PUBLIC_KEY_ON_SERVER": "Fazer cópia de segurança da chave pública no servidor",
|
||||
"BACKUP_PRIVATE_KEY_ON_SERVER": "Fazer cópia de segurança da chave privada no servidor"
|
||||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"POPUP_IMPORT_TITLE": "Importar certificado S\/MIME",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "Certificados S\/MIME",
|
||||
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",
|
||||
"PRIVATE_KEY_OF": "S\/MIME private key of %EMAIL%"
|
||||
"PRIVATE_KEY_OF": "Chave privada S\/MIME de %EMAIL%"
|
||||
},
|
||||
"POPUPS_FILTER": {
|
||||
"TITLE_CREATE_FILTER": "Criar um filtro?",
|
||||
|
|
@ -405,7 +407,7 @@
|
|||
"IMAGEUPLOAD": "Escolher imagem",
|
||||
"UNDO": "Desfazer",
|
||||
"REDO": "Refazer",
|
||||
"REMOVESTYLE": "Remove style"
|
||||
"REMOVESTYLE": "Remover estilo"
|
||||
},
|
||||
"SETTINGS_LABELS": {
|
||||
"GENERAL": "Geral",
|
||||
|
|
@ -530,7 +532,7 @@
|
|||
"SETTINGS_OPENPGP": {
|
||||
"BUTTON_IMPORT_KEY": "Importar chave",
|
||||
"BUTTON_GENERATE_KEY_PAIR": "Gerar chaves",
|
||||
"IMPORT_FROM_SERVER": "Import from server",
|
||||
"IMPORT_FROM_SERVER": "Importar de servidor",
|
||||
"TITLE_PRIVATE": "Privada",
|
||||
"TITLE_PUBLIC": "Pública",
|
||||
"GET_MAILVELOPE": "Obter extensão Mailvelope para o navegador."
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import OpenPGP key",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME signed message",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Подтвержденная сигнатура для %USER%",
|
||||
"ERROR": "%TYPE% ошибка: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Импорт OpenPGP ключа",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME подписанное сообщение",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME шифрованное сообщение",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Good signature from %USER%",
|
||||
"ERROR": "%TYPE% error: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Importovať kľúč OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Správa podpísaná s S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Správa šifrovaná s S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Dober podpis od %USER%",
|
||||
"ERROR": "%TYPE% napaka: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Uvoz OpenPGP ključa",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Sporočilo, podpisano z S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Sporočilo, šifrirano z S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Godkänd signatur för %USER%",
|
||||
"ERROR": "%TYPE%-fel: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Import av OpenPGP-nyckel",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME-signerat meddelande",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME-krypterat meddelande",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Geçerli imza %USER%",
|
||||
"ERROR": "%TYPE% hatası: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "OpenPGP key'i içe aktar",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME imzalı mesaj",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME şifreli mesaj",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Підтверджена сигнатура для %USER%",
|
||||
"ERROR": "%TYPE% помилка: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Імпорт OpenPGP ключа",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "S\/MIME підписане повідомлення",
|
||||
"ENCRYPTED_MESSAGE": "S\/MIME шифроване повідомлення",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "Chữ ký ổn từ %USER%",
|
||||
"ERROR": "Lỗi %TYPE%: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "Nhập mật mã OpenPGP",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "Thư đã được ký xác nhận bằng mã S\/MIME",
|
||||
"ENCRYPTED_MESSAGE": "Thư đã được mã hóa bởi S\/MIME",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "来自 %USER% 的可用签名",
|
||||
"ERROR": "%TYPE% 错误: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "导入 OpenPGP 密钥",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "由 S\/MIME 签名",
|
||||
"ENCRYPTED_MESSAGE": "由 S\/MIME 加密",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,8 @@
|
|||
"GOOD_SIGNATURE": "來自「%USER%」的良好簽署",
|
||||
"ERROR": "%TYPE% 錯誤: %ERROR%",
|
||||
"CREATE_SELF_SIGNED": "Create self-signed",
|
||||
"VALID_UNTIL": "Valid until"
|
||||
"VALID_UNTIL": "Valid until",
|
||||
"PRIVATE_KEY": "Private key"
|
||||
},
|
||||
"OPENPGP": {
|
||||
"POPUP_IMPORT_TITLE": "匯入 OpenPGP 金鑰",
|
||||
|
|
@ -303,6 +304,7 @@
|
|||
},
|
||||
"SMIME": {
|
||||
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
|
||||
"CERTIFICATE": "Certificate",
|
||||
"CERTIFICATES": "S\/MIME Certificates",
|
||||
"SIGNED_MESSAGE": "已透過 S\/MIME 簽署郵件",
|
||||
"ENCRYPTED_MESSAGE": "已透過 S\/MIME 加密郵件",
|
||||
|
|
|
|||
|
|
@ -89,14 +89,14 @@
|
|||
<details>
|
||||
<summary><strong>S/MIME</strong></summary>
|
||||
<div class="control-group">
|
||||
<label>Private key</label>
|
||||
<label data-i18n="CRYPTO/PRIVATE_KEY"></label>
|
||||
<textarea name="smimeKey" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeKey"></textarea>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Certificate</label>
|
||||
<label data-i18n="SMIME/CERTIFICATE"></label>
|
||||
<textarea name="smimeCertificate" class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: smimeCertificate"></textarea>
|
||||
</div>
|
||||
<div class="control-group" data-bind="hidden:smimeKey">
|
||||
<div class="control-group" data-bind="hidden:smimeCertificate">
|
||||
<label></label>
|
||||
<button type="button" data-bind="click: $root.createSelfSigned" data-i18n="CRYPTO/CREATE_SELF_SIGNED"></button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue