mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-11 00:14:50 +03:00
Revamp the whole accounts system for better management and control.
This also solves RainLoop/#2134
This commit is contained in:
parent
ef4790d04e
commit
a18d393f54
15 changed files with 441 additions and 401 deletions
|
|
@ -1164,8 +1164,7 @@ END;
|
|||
|
||||
public static function UrlSafeBase64Decode(string $sValue) : string
|
||||
{
|
||||
$sValue = \rtrim(\strtr($sValue, '-_.', '+/='), '=');
|
||||
return static::Base64Decode(\str_pad($sValue, \strlen($sValue) + (\strlen($sValue) % 4), '=', STR_PAD_RIGHT));
|
||||
return \base64_decode(\strtr($sValue, '-_', '+/'), '=');
|
||||
}
|
||||
|
||||
public static function ParseFetchSequence(string $sSequence) : array
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ class Actions
|
|||
*/
|
||||
const AUTH_SPEC_TOKEN_KEY = 'smaccount';
|
||||
|
||||
/**
|
||||
* This session cookie optionally contains a \RainLoop\Model\AdditionalAccount
|
||||
*/
|
||||
const AUTH_ADDITIONAL_TOKEN_KEY = 'smadditional';
|
||||
|
||||
const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout';
|
||||
const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk';
|
||||
|
||||
|
|
@ -553,7 +558,7 @@ class Actions
|
|||
{
|
||||
$sKey = '';
|
||||
if ($oAccount) {
|
||||
$sKey = $oAccount->ParentEmailHelper();
|
||||
$sKey = $this->GetMainEmail($oAccount);
|
||||
}
|
||||
|
||||
$sIndexKey = empty($sKey) ? '_default_' : $sKey;
|
||||
|
|
@ -918,7 +923,9 @@ class Actions
|
|||
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true);
|
||||
}
|
||||
|
||||
$aResult['ParentEmail'] = $oAccount->ParentEmail();
|
||||
if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
|
||||
$aResult['ParentEmail'] = $oAccount->ParentEmail();
|
||||
}
|
||||
|
||||
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
|
||||
|
||||
|
|
@ -1128,32 +1135,6 @@ class Actions
|
|||
return $bFirstOnEmpty && isset($aIdentities[0]) ? $aIdentities[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \MailSo\Base\Exceptions\Exception
|
||||
*/
|
||||
public function getAccountUnreadCountFromHash(string $sHash): int
|
||||
{
|
||||
$iResult = 0;
|
||||
|
||||
$oAccount = $this->GetAccountFromCustomToken($sHash);
|
||||
if ($oAccount) {
|
||||
try {
|
||||
$oMailClient = new \MailSo\Mail\MailClient();
|
||||
$oMailClient->SetLogger($this->Logger());
|
||||
|
||||
$oAccount->IncConnectAndLoginHelper($this->Plugins(), $oMailClient, $this->Config());
|
||||
|
||||
$iResult = $oMailClient->InboxUnreadCount();
|
||||
|
||||
$oMailClient->Disconnect();
|
||||
} catch (\Throwable $oException) {
|
||||
$this->Logger()->WriteException($oException);
|
||||
}
|
||||
}
|
||||
|
||||
return $iResult;
|
||||
}
|
||||
|
||||
public function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void
|
||||
{
|
||||
$sValue = $this->GetActionParam($sParamName, '');
|
||||
|
|
@ -1420,28 +1401,10 @@ class Actions
|
|||
|
||||
if (\count($aData)) {
|
||||
$this->Logger()->Write('Import contacts from csv');
|
||||
$iCount = $oAddressBookProvider->ImportCsvArray($oAccount->ParentEmailHelper(), $aData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $iCount;
|
||||
}
|
||||
|
||||
private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int
|
||||
{
|
||||
$iCount = 0;
|
||||
if ($oAccount && \is_resource($rFile)) {
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
$sFile = \stream_get_contents($rFile);
|
||||
if (\is_resource($rFile)) {
|
||||
\fclose($rFile);
|
||||
}
|
||||
|
||||
if (is_string($sFile) && 5 < \strlen($sFile)) {
|
||||
$this->Logger()->Write('Import contacts from vcf');
|
||||
$iCount = $oAddressBookProvider->ImportVcfFile($oAccount->ParentEmailHelper(), $sFile);
|
||||
$iCount = $oAddressBookProvider->ImportCsvArray(
|
||||
$this->GetMainEmail($oAccount),
|
||||
$aData
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,67 +8,54 @@ use RainLoop\Model\Account;
|
|||
use RainLoop\Model\Identity;
|
||||
use RainLoop\Notifications;
|
||||
use RainLoop\Providers\Storage\Enumerations\StorageType;
|
||||
use RainLoop\Utils;
|
||||
|
||||
trait Accounts
|
||||
{
|
||||
|
||||
protected function GetMainEmail(Account $oAccount)
|
||||
{
|
||||
return $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email();
|
||||
}
|
||||
|
||||
public function GetAccounts(Account $oAccount): array
|
||||
{
|
||||
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
|
||||
throw new \LogicException('Only main account can have sub accounts');
|
||||
}
|
||||
if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
|
||||
$sAccounts = $this->StorageProvider()->Get($oAccount,
|
||||
StorageType::CONFIG,
|
||||
'accounts'
|
||||
'additionalaccounts'
|
||||
);
|
||||
|
||||
$aAccounts = $sAccounts ? \json_decode($sAccounts, true) : array();
|
||||
|
||||
if (\is_array($aAccounts) && \count($aAccounts)) {
|
||||
if (1 === \count($aAccounts)) {
|
||||
$this->SetAccounts($oAccount, array());
|
||||
|
||||
} else if (1 < \count($aAccounts)) {
|
||||
$sOrder = $this->StorageProvider()->Get($oAccount,
|
||||
StorageType::CONFIG,
|
||||
'accounts_identities_order'
|
||||
);
|
||||
|
||||
$aOrder = empty($sOrder) ? array() : \json_decode($sOrder, true);
|
||||
if (isset($aOrder['Accounts']) && \is_array($aOrder['Accounts']) && 1 < \count($aOrder['Accounts'])) {
|
||||
$aAccounts = \array_merge(\array_flip($aOrder['Accounts']), $aAccounts);
|
||||
|
||||
$aAccounts = \array_filter($aAccounts, function ($sHash) {
|
||||
return 5 < \strlen($sHash);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ($aAccounts && \is_array($aAccounts)) {
|
||||
return $aAccounts;
|
||||
}
|
||||
}
|
||||
|
||||
$aAccounts = array();
|
||||
if (!$oAccount->IsAdditionalAccount()) {
|
||||
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
|
||||
}
|
||||
|
||||
return $aAccounts;
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function SetAccounts(Account $oAccount, array $aAccounts = array()): void
|
||||
{
|
||||
$sParentEmail = $oAccount->ParentEmailHelper();
|
||||
if (!$aAccounts ||
|
||||
(1 === \count($aAccounts) && !empty($aAccounts[$sParentEmail]))) {
|
||||
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
|
||||
throw new \LogicException('Only main account can have sub accounts');
|
||||
}
|
||||
$sParentEmail = $oAccount->Email();
|
||||
if (!$aAccounts) {
|
||||
$this->StorageProvider()->Clear(
|
||||
$oAccount,
|
||||
StorageType::CONFIG,
|
||||
'accounts'
|
||||
'additionalaccounts'
|
||||
);
|
||||
} else {
|
||||
$this->StorageProvider()->Put(
|
||||
$oAccount,
|
||||
StorageType::CONFIG,
|
||||
'accounts',
|
||||
'additionalaccounts',
|
||||
\json_encode($aAccounts)
|
||||
);
|
||||
}
|
||||
|
|
@ -79,36 +66,30 @@ trait Accounts
|
|||
*/
|
||||
public function DoAccountSetup(): array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
$oMainAccount = $this->getMainAccountFromToken();
|
||||
|
||||
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
|
||||
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) {
|
||||
return $this->FalseResponse(__FUNCTION__);
|
||||
}
|
||||
|
||||
$sParentEmail = $oAccount->ParentEmailHelper();
|
||||
|
||||
$aAccounts = $this->GetAccounts($oAccount);
|
||||
$aAccounts = $this->GetAccounts($oMainAccount);
|
||||
|
||||
$sEmail = \trim($this->GetActionParam('Email', ''));
|
||||
$sPassword = $this->GetActionParam('Password', '');
|
||||
$bNew = '1' === (string)$this->GetActionParam('New', '1');
|
||||
|
||||
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||
if ($bNew && ($oAccount->Email() === $sEmail || $sParentEmail === $sEmail || isset($aAccounts[$sEmail]))) {
|
||||
if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) {
|
||||
throw new ClientException(Notifications::AccountAlreadyExists);
|
||||
} else if (!$bNew && !isset($aAccounts[$sEmail])) {
|
||||
throw new ClientException(Notifications::AccountDoesNotExist);
|
||||
}
|
||||
|
||||
$oNewAccount = $this->LoginProcess($sEmail, $sPassword);
|
||||
$oNewAccount->SetParentEmail($sParentEmail);
|
||||
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, $oMainAccount);
|
||||
|
||||
$aAccounts[$oNewAccount->Email()] = $oNewAccount->GetAuthToken();
|
||||
if (!$oAccount->IsAdditionalAccount()) {
|
||||
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
|
||||
}
|
||||
$aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount);
|
||||
$this->SetAccounts($oMainAccount, $aAccounts);
|
||||
|
||||
$this->SetAccounts($oAccount, $aAccounts);
|
||||
return $this->TrueResponse(__FUNCTION__);
|
||||
}
|
||||
|
||||
|
|
@ -117,31 +98,29 @@ trait Accounts
|
|||
*/
|
||||
public function DoAccountDelete(): array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
$oMainAccount = $this->getMainAccountFromToken();
|
||||
|
||||
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
|
||||
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) {
|
||||
return $this->FalseResponse(__FUNCTION__);
|
||||
}
|
||||
|
||||
$sParentEmail = $oAccount->ParentEmailHelper();
|
||||
$sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', ''));
|
||||
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
|
||||
|
||||
$aAccounts = $this->GetAccounts($oAccount);
|
||||
$aAccounts = $this->GetAccounts($oMainAccount);
|
||||
|
||||
if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete])) {
|
||||
unset($aAccounts[$sEmailToDelete]);
|
||||
|
||||
$oAccountToChange = null;
|
||||
if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail])) {
|
||||
$oAccountToChange = $this->GetAccountFromCustomToken($aAccounts[$sParentEmail]);
|
||||
if ($oAccountToChange) {
|
||||
$this->SetAuthToken($oAccountToChange);
|
||||
}
|
||||
if (\strlen($sEmailToDelete) && isset($aAccounts[$sEmailToDelete])) {
|
||||
$bReload = false;
|
||||
// $oAccount = $this->getAccountFromToken();
|
||||
if ($oAccount->Email() === $sEmailToDelete) {
|
||||
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
|
||||
$bReload = true;
|
||||
}
|
||||
|
||||
$this->SetAccounts($oAccount, $aAccounts);
|
||||
return $this->TrueResponse(__FUNCTION__, array('Reload' => !!$oAccountToChange));
|
||||
unset($aAccounts[$sEmailToDelete]);
|
||||
$this->SetAccounts($oMainAccount, $aAccounts);
|
||||
|
||||
return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload));
|
||||
}
|
||||
|
||||
return $this->FalseResponse(__FUNCTION__);
|
||||
|
|
@ -188,8 +167,6 @@ trait Accounts
|
|||
*/
|
||||
public function DoAccountsAndIdentitiesSortOrder(): array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
|
||||
$aAccounts = $this->GetActionParam('Accounts', null);
|
||||
$aIdentities = $this->GetActionParam('Identities', null);
|
||||
|
||||
|
|
@ -197,10 +174,20 @@ trait Accounts
|
|||
return $this->FalseResponse(__FUNCTION__);
|
||||
}
|
||||
|
||||
return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put($oAccount,
|
||||
StorageType::CONFIG, 'accounts_identities_order',
|
||||
$oAccount = $this->getMainAccountFromToken();
|
||||
if (1 < \count($aAccounts)) {
|
||||
$aAccounts = \array_filter(\array_merge(
|
||||
\array_fill_keys($aOrder['Accounts'], null),
|
||||
$this->GetAccounts($oAccount)
|
||||
));
|
||||
$this->SetAccounts($oAccount, $aAccounts);
|
||||
}
|
||||
|
||||
return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put(
|
||||
$this->getMainAccountFromToken(),
|
||||
StorageType::CONFIG,
|
||||
'identities_order',
|
||||
\json_encode(array(
|
||||
'Accounts' => \is_array($aAccounts) ? $aAccounts : array(),
|
||||
'Identities' => \is_array($aIdentities) ? $aIdentities : array()
|
||||
))
|
||||
));
|
||||
|
|
@ -211,39 +198,39 @@ trait Accounts
|
|||
*/
|
||||
public function DoAccountsAndIdentities(): array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
|
||||
$mAccounts = false;
|
||||
$oAccount = $this->getMainAccountFromToken();
|
||||
|
||||
$aAccounts = false;
|
||||
if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
|
||||
$mAccounts = $this->GetAccounts($oAccount);
|
||||
$mAccounts = \array_keys($mAccounts);
|
||||
|
||||
foreach ($mAccounts as $iIndex => $sName) {
|
||||
$mAccounts[$iIndex] = \MailSo\Base\Utils::IdnToUtf8($sName);
|
||||
$aAccounts = \array_map(
|
||||
'MailSo\\Base\\Utils::IdnToUtf8',
|
||||
\array_keys($this->GetAccounts($oAccount))
|
||||
);
|
||||
if ($aAccounts) {
|
||||
\array_unshift($aAccounts, \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->DefaultResponse(__FUNCTION__, array(
|
||||
'Accounts' => $mAccounts,
|
||||
'Accounts' => $aAccounts,
|
||||
'Identities' => $this->GetIdentities($oAccount)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Account $account
|
||||
* @param Account $oAccount
|
||||
* @return Identity[]
|
||||
*/
|
||||
public function GetIdentities(Account $account): array
|
||||
public function GetIdentities(Account $oAccount): array
|
||||
{
|
||||
// A custom name for a single identity is also stored in this system
|
||||
$allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $account);
|
||||
$allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $oAccount);
|
||||
|
||||
// Get all identities
|
||||
$identities = $this->IdentitiesProvider()->GetIdentities($account, $allowMultipleIdentities);
|
||||
$identities = $this->IdentitiesProvider()->GetIdentities($oAccount, $allowMultipleIdentities);
|
||||
|
||||
// Sort identities
|
||||
$orderString = $this->StorageProvider()->Get($account, StorageType::CONFIG, 'accounts_identities_order');
|
||||
$orderString = $this->StorageProvider()->Get($oAccount, StorageType::CONFIG, 'identities_order');
|
||||
$order = \json_decode($orderString, true) ?? [];
|
||||
if (isset($order['Identities']) && \is_array($order['Identities']) && \count($order['Identities']) > 1) {
|
||||
$list = \array_map(function ($item) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ namespace RainLoop\Actions;
|
|||
trait Contacts
|
||||
{
|
||||
|
||||
private function GetAccountParentEmail()
|
||||
{
|
||||
// $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email()
|
||||
}
|
||||
|
||||
public function DoSaveContactsSyncData() : array
|
||||
{
|
||||
$oAccount = $this->getAccountFromToken();
|
||||
|
|
@ -45,7 +50,7 @@ trait Contacts
|
|||
if (isset($mData['Enable'], $mData['User'], $mData['Password'], $mData['Url']) && $mData['Enable'])
|
||||
{
|
||||
$bResult = $oAddressBookProvider->Sync(
|
||||
$oAccount->ParentEmailHelper(),
|
||||
$this->GetMainEmail($oAccount),
|
||||
$mData['Url'], $mData['User'], $mData['Password']);
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +80,7 @@ trait Contacts
|
|||
if ($oAbp->IsActive())
|
||||
{
|
||||
$iResultCount = 0;
|
||||
$mResult = $oAbp->GetContacts($oAccount->ParentEmailHelper(),
|
||||
$mResult = $oAbp->GetContacts($this->GetMainEmail($oAccount),
|
||||
$iOffset, $iLimit, $sSearch, $iResultCount);
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +103,7 @@ trait Contacts
|
|||
$bResult = false;
|
||||
if (0 < \count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive())
|
||||
{
|
||||
$bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($oAccount->ParentEmailHelper(), $aFilteredUids);
|
||||
$bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($this->GetMainEmail($oAccount), $aFilteredUids);
|
||||
}
|
||||
|
||||
return $this->DefaultResponse(__FUNCTION__, $bResult);
|
||||
|
|
@ -119,7 +124,7 @@ trait Contacts
|
|||
$oContact = null;
|
||||
if (0 < \strlen($sUid))
|
||||
{
|
||||
$oContact = $oAddressBookProvider->GetContactByID($oAccount->ParentEmailHelper(), $sUid);
|
||||
$oContact = $oAddressBookProvider->GetContactByID($this->GetMainEmail($oAccount), $sUid);
|
||||
}
|
||||
|
||||
if (!$oContact)
|
||||
|
|
@ -159,7 +164,7 @@ trait Contacts
|
|||
|
||||
$oContact->PopulateDisplayAndFullNameValue(true);
|
||||
|
||||
$bResult = $oAddressBookProvider->ContactSave($oAccount->ParentEmailHelper(), $oContact);
|
||||
$bResult = $oAddressBookProvider->ContactSave($this->GetMainEmail($oAccount), $oContact);
|
||||
}
|
||||
|
||||
return $this->DefaultResponse(__FUNCTION__, array(
|
||||
|
|
@ -300,7 +305,7 @@ trait Contacts
|
|||
$this->oHttp->ServerNoCache();
|
||||
|
||||
return $this->AddressBookProvider($oAccount)->IsActive() ?
|
||||
$this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'vcf') : false;
|
||||
$this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'vcf') : false;
|
||||
}
|
||||
|
||||
public function RawContactsCsv() : bool
|
||||
|
|
@ -315,7 +320,31 @@ trait Contacts
|
|||
$this->oHttp->ServerNoCache();
|
||||
|
||||
return $this->AddressBookProvider($oAccount)->IsActive() ?
|
||||
$this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'csv') : false;
|
||||
$this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'csv') : false;
|
||||
}
|
||||
|
||||
private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int
|
||||
{
|
||||
$iCount = 0;
|
||||
if ($oAccount && \is_resource($rFile)) {
|
||||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
|
||||
$sFile = \stream_get_contents($rFile);
|
||||
if (\is_resource($rFile)) {
|
||||
\fclose($rFile);
|
||||
}
|
||||
|
||||
if (is_string($sFile) && 5 < \strlen($sFile)) {
|
||||
$this->Logger()->Write('Import contacts from vcf');
|
||||
$iCount = $oAddressBookProvider->ImportVcfFile(
|
||||
$this->GetMainEmail($oAccount),
|
||||
$sFile
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $iCount;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ trait Messages
|
|||
$oSettings = $this->SettingsProvider()->Load($oAccount);
|
||||
|
||||
$this->AddressBookProvider($oAccount)->IncFrec(
|
||||
$oAccount->ParentEmailHelper(), \array_values($aArrayToFrec),
|
||||
$this->GetMainEmail($oAccount), \array_values($aArrayToFrec),
|
||||
!!$oSettings->GetConf('ContactsAutosave',
|
||||
!!$oConfig->Get('defaults', 'contacts_autosave', true)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ trait User
|
|||
$oAccount = $this->getAccountFromToken(false);
|
||||
if ($oAccount)
|
||||
{
|
||||
if (!$oAccount->IsAdditionalAccount())
|
||||
if (!\is_subclass_of($oAccount, 'RainLoop\\Model\\Account'))
|
||||
{
|
||||
$this->ClearSignMeData();
|
||||
Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
|
|
@ -393,7 +393,7 @@ trait User
|
|||
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
|
||||
if ($oAddressBookProvider && $oAddressBookProvider->IsActive())
|
||||
{
|
||||
$aSuggestions = $oAddressBookProvider->GetSuggestions($oAccount->ParentEmailHelper(), $sQuery, $iLimit);
|
||||
$aSuggestions = $oAddressBookProvider->GetSuggestions($this->GetMainEmail($oAccount), $sQuery, $iLimit);
|
||||
if (!\count($aResult))
|
||||
{
|
||||
$aResult = $aSuggestions;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace RainLoop\Actions;
|
|||
use RainLoop\Notifications;
|
||||
use RainLoop\Utils;
|
||||
use RainLoop\Model\Account;
|
||||
use RainLoop\Model\AdditionalAccount;
|
||||
use RainLoop\Providers\Storage\Enumerations\StorageType;
|
||||
use RainLoop\Exceptions\ClientException;
|
||||
|
||||
|
|
@ -13,18 +14,13 @@ trait UserAuth
|
|||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sSpecAuthToken = null;
|
||||
private $oSpecAuthAccount = null;
|
||||
|
||||
/**
|
||||
* Or use 'aes-256-xts' ?
|
||||
*/
|
||||
private static $sCipher = 'aes-256-cbc-hmac-sha1';
|
||||
private $oAdditionalAuthAccount = null;
|
||||
private $oMainAuthAccount = null;
|
||||
|
||||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false): Account
|
||||
public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false, Account $oMainAccount = null): Account
|
||||
{
|
||||
$sInputEmail = $sEmail;
|
||||
|
||||
|
|
@ -117,7 +113,14 @@ trait UserAuth
|
|||
$oAccount = null;
|
||||
$sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', ''));
|
||||
try {
|
||||
$oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
|
||||
if ($oMainAccount) {
|
||||
$oAccount = AdditionalAccount::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
|
||||
if ($oAccount) {
|
||||
$oAccount->SetParentEmail($oMainAccount->Email());
|
||||
}
|
||||
} else {
|
||||
$oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
|
||||
}
|
||||
|
||||
if (!$oAccount) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
|
|
@ -141,86 +144,103 @@ trait UserAuth
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function GetAccountFromCustomToken(string $sToken): ?Account
|
||||
{
|
||||
return empty($sToken) ? null : Account::NewInstanceFromAuthToken($sToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns RainLoop\Model\AdditionalAccount when it exists,
|
||||
* else returns RainLoop\Model\Account when it exists,
|
||||
* else null
|
||||
*
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
|
||||
{
|
||||
if (!$this->oSpecAuthAccount) {
|
||||
if (!\is_string($this->sSpecAuthToken)) {
|
||||
$this->sSpecAuthToken = '';
|
||||
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
|
||||
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
} else {
|
||||
$sAuthAccountHash = Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
if (empty($sAuthAccountHash)) {
|
||||
$oAccount = $this->GetAccountFromSignMeToken();
|
||||
if ($oAccount) {
|
||||
$this->SetAuthToken($oAccount);
|
||||
}
|
||||
} else {
|
||||
$this->sSpecAuthToken = $sAuthAccountHash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$aData = \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($this->sSpecAuthToken));
|
||||
if (!empty($aData)) {
|
||||
$this->oSpecAuthAccount = Account::NewInstanceFromTokenArray(
|
||||
if (\is_null($this->oAdditionalAuthAccount) && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) {
|
||||
$aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
|
||||
if ($aData) {
|
||||
$this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray(
|
||||
$this,
|
||||
$aData,
|
||||
$bThrowExceptionOnFalse
|
||||
);
|
||||
}
|
||||
if (!$this->oAdditionalAuthAccount) {
|
||||
$this->oAdditionalAuthAccount = false;
|
||||
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
return $this->oAdditionalAuthAccount ?: $this->getMainAccountFromToken($bThrowExceptionOnFalse);
|
||||
}
|
||||
|
||||
if ($bThrowExceptionOnFalse && !$this->oSpecAuthAccount) {
|
||||
/**
|
||||
* Returns RainLoop\Model\Account when it exists, else null
|
||||
*
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function getMainAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
|
||||
{
|
||||
if (!$this->oMainAuthAccount) {
|
||||
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
|
||||
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
// Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
|
||||
}
|
||||
|
||||
$aData = Utils::GetSecureCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
if ($aData) {
|
||||
$this->oMainAuthAccount = Account::NewInstanceFromTokenArray(
|
||||
$this,
|
||||
$aData,
|
||||
$bThrowExceptionOnFalse
|
||||
);
|
||||
} else {
|
||||
$oAccount = $this->GetAccountFromSignMeToken();
|
||||
if ($oAccount) {
|
||||
$this->SetAuthToken($oAccount);
|
||||
}
|
||||
}
|
||||
|
||||
if ($bThrowExceptionOnFalse && !$this->oMainAuthAccount) {
|
||||
throw new ClientException(\RainLoop\Notifications::AuthError);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->oSpecAuthAccount;
|
||||
return $this->oMainAuthAccount;
|
||||
}
|
||||
|
||||
public function SetAuthToken(Account $oAccount): void
|
||||
{
|
||||
$sSpecAuthToken = \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount));
|
||||
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
|
||||
throw new \LogicException('Only main Account can be set as AuthToken');
|
||||
}
|
||||
$this->oAdditionalAuthAccount = false;
|
||||
$this->oMainAuthAccount = $oAccount;
|
||||
Utils::SetSecureCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount);
|
||||
}
|
||||
|
||||
$this->sSpecAuthToken = $sSpecAuthToken;
|
||||
$this->oSpecAuthAccount = null;
|
||||
Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken);
|
||||
|
||||
if (isset($aAccounts[$oAccount->Email()])) {
|
||||
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
|
||||
$this->SetAccounts($oAccount, $aAccounts);
|
||||
public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void
|
||||
{
|
||||
$this->oAdditionalAuthAccount = $oAccount ?: false;
|
||||
if ($oAccount) {
|
||||
Utils::SetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount);
|
||||
} else {
|
||||
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SignMe methods used for the "remember me" cookie
|
||||
*/
|
||||
|
||||
private static function GetSignMeToken(): ?array
|
||||
{
|
||||
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
if (empty($sSignMeToken)) {
|
||||
return null;
|
||||
if ($sSignMeToken) {
|
||||
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
|
||||
if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) {
|
||||
return $aResult;
|
||||
}
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
}
|
||||
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
|
||||
return \is_array($aResult) ? $aResult : null;
|
||||
}
|
||||
|
||||
private static function SetSignMeTokenCookie(array $aData): void
|
||||
{
|
||||
Utils::SetCookie(
|
||||
self::AUTH_SIGN_ME_TOKEN_KEY,
|
||||
\SnappyMail\Crypt::EncryptUrlSafe($aData),
|
||||
\time() + 3600 * 24 * 30 // 30 days
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
private function SetSignMeToken(Account $oAccount): void
|
||||
|
|
@ -230,25 +250,15 @@ trait UserAuth
|
|||
$uuid = \SnappyMail\UUID::generate();
|
||||
$data = \SnappyMail\Crypt::Encrypt($oAccount);
|
||||
|
||||
if ('xxtea' === $data[0]) {
|
||||
static::SetSignMeTokenCookie(array(
|
||||
Utils::SetCookie(
|
||||
self::AUTH_SIGN_ME_TOKEN_KEY,
|
||||
\SnappyMail\Crypt::EncryptUrlSafe([
|
||||
'e' => $oAccount->Email(),
|
||||
'u' => $uuid,
|
||||
'x' => \base64_encode($data[1])
|
||||
));
|
||||
} else if ('sodium' === $data[0]) {
|
||||
static::SetSignMeTokenCookie(array(
|
||||
'e' => $oAccount->Email(),
|
||||
'u' => $uuid,
|
||||
's' => \base64_encode($data[1])
|
||||
));
|
||||
} else {
|
||||
static::SetSignMeTokenCookie(array(
|
||||
'e' => $oAccount->Email(),
|
||||
'u' => $uuid,
|
||||
'o' => \base64_encode($data[1])
|
||||
));
|
||||
}
|
||||
$data[0] => \base64_encode($data[1])
|
||||
]),
|
||||
\time() + 3600 * 24 * 30 // 30 days
|
||||
);
|
||||
|
||||
$this->StorageProvider()->Put(
|
||||
$oAccount,
|
||||
|
|
@ -261,58 +271,55 @@ trait UserAuth
|
|||
public function GetAccountFromSignMeToken(): ?Account
|
||||
{
|
||||
$aTokenData = static::GetSignMeToken();
|
||||
if (!empty($aTokenData)) {
|
||||
$oAccount = null;
|
||||
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
if ($aTokenData) {
|
||||
try
|
||||
{
|
||||
$sAuthToken = $this->StorageProvider()->Get(
|
||||
$aTokenData['e'],
|
||||
StorageType::SIGN_ME,
|
||||
$aTokenData['u']
|
||||
);
|
||||
if (empty($sAuthToken)) {
|
||||
return null;
|
||||
}
|
||||
if (!empty($aTokenData['x'])) {
|
||||
$aAccountHash = \SnappyMail\Crypt::XxteaDecrypt($sAuthToken, \base64_decode($aTokenData['x']));
|
||||
} else if (!empty($aTokenData['s'])) {
|
||||
$aAccountHash = \SnappyMail\Crypt::SodiumDecrypt($sAuthToken, \base64_decode($aTokenData['s']));
|
||||
} else if (!empty($aTokenData['o'])) {
|
||||
$aAccountHash = \SnappyMail\Crypt::OpenSSLDecrypt($sAuthToken, \base64_decode($aTokenData['o']));
|
||||
}
|
||||
if (!empty($aAccountHash) && \is_array($aAccountHash)) {
|
||||
$oAccount = Account::NewInstanceFromTokenArray($this, $aAccountHash);
|
||||
if ($oAccount) {
|
||||
try
|
||||
{
|
||||
$this->CheckMailConnection($oAccount);
|
||||
// Update lifetime
|
||||
$this->SetSignMeToken($oAccount);
|
||||
if ($sAuthToken) {
|
||||
$aAccountHash = \SnappyMail\Crypt::Decrypt([
|
||||
\array_key_last($aTokenData),
|
||||
\base64_decode(\end($aTokenData)),
|
||||
$sAuthToken
|
||||
]);
|
||||
|
||||
return $oAccount;
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
}
|
||||
$oAccount = \is_array($aAccountHash)
|
||||
? Account::NewInstanceFromTokenArray($this, $aAccountHash) : null;
|
||||
if ($oAccount) {
|
||||
$this->CheckMailConnection($oAccount);
|
||||
// Update lifetime
|
||||
$this->SetSignMeToken($oAccount);
|
||||
|
||||
return $oAccount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
}
|
||||
|
||||
$this->ClearSignMeData();
|
||||
}
|
||||
|
||||
$this->ClearSignMeData();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function ClearSignMeData() : void
|
||||
{
|
||||
if (isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY])) {
|
||||
$aTokenData = static::GetSignMeToken();
|
||||
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
|
||||
}
|
||||
$aTokenData = static::GetSignMeToken();
|
||||
if ($aTokenData) {
|
||||
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout methods
|
||||
*/
|
||||
|
||||
public function SetAuthLogoutToken(): void
|
||||
{
|
||||
\header('X-RainLoop-Action: Logout');
|
||||
|
|
|
|||
|
|
@ -42,21 +42,11 @@ class Account implements \JsonSerializable
|
|||
*/
|
||||
private $oDomain;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sParentEmail = '';
|
||||
|
||||
public function Email() : string
|
||||
{
|
||||
return $this->sEmail;
|
||||
}
|
||||
|
||||
public function ParentEmail() : string
|
||||
{
|
||||
return $this->sParentEmail;
|
||||
}
|
||||
|
||||
public function ProxyAuthUser() : string
|
||||
{
|
||||
return $this->sProxyAuthUser;
|
||||
|
|
@ -67,16 +57,6 @@ class Account implements \JsonSerializable
|
|||
return $this->sProxyAuthPassword;
|
||||
}
|
||||
|
||||
public function ParentEmailHelper() : string
|
||||
{
|
||||
return $this->sParentEmail ?: $this->sEmail;
|
||||
}
|
||||
|
||||
public function IsAdditionalAccount() : bool
|
||||
{
|
||||
return !empty($this->sParentEmail);
|
||||
}
|
||||
|
||||
public function IncLogin() : string
|
||||
{
|
||||
$sLogin = $this->sLogin;
|
||||
|
|
@ -135,8 +115,7 @@ class Account implements \JsonSerializable
|
|||
$this->sEmail,
|
||||
$this->Domain()->IncHost(),
|
||||
$this->Domain()->IncPort(),
|
||||
$this->sPassword,
|
||||
$this->sParentEmail
|
||||
$this->sPassword
|
||||
]));
|
||||
}
|
||||
|
||||
|
|
@ -145,11 +124,6 @@ class Account implements \JsonSerializable
|
|||
$this->sPassword = $sPassword;
|
||||
}
|
||||
|
||||
public function SetParentEmail(string $sParentEmail) : void
|
||||
{
|
||||
$this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true));
|
||||
}
|
||||
|
||||
public function SetProxyAuthUser(string $sProxyAuthUser) : void
|
||||
{
|
||||
$this->sProxyAuthUser = $sProxyAuthUser;
|
||||
|
|
@ -168,29 +142,15 @@ class Account implements \JsonSerializable
|
|||
$this->sLogin, // 2
|
||||
$this->sPassword, // 3
|
||||
$this->sClientCert, // 4
|
||||
$this->sParentEmail, // 5
|
||||
$this->sProxyAuthUser, // 6
|
||||
$this->sProxyAuthPassword // 7
|
||||
$this->sProxyAuthUser, // 5
|
||||
$this->sProxyAuthPassword // 6
|
||||
);
|
||||
}
|
||||
|
||||
public function GetAuthToken() : string
|
||||
{
|
||||
return Utils::EncodeKeyValues($this->jsonSerialize());
|
||||
/*
|
||||
return \MailSo\Base\Utils::UrlSafeBase64Encode(
|
||||
\MailSo\Base\Crypt::Encrypt(
|
||||
\json_encode($this),
|
||||
\md5(APP_SALT.$sCustomKey)
|
||||
)
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?Account
|
||||
public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?self
|
||||
{
|
||||
return empty($sToken) ? null
|
||||
: static::NewInstanceFromTokenArray(
|
||||
|
|
@ -200,14 +160,16 @@ class Account implements \JsonSerializable
|
|||
);
|
||||
}
|
||||
|
||||
public static function NewInstanceByLogin(\RainLoop\Actions $oActions, string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '', bool $bThrowException = false): ?self
|
||||
public static function NewInstanceByLogin(\RainLoop\Actions $oActions,
|
||||
string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '',
|
||||
bool $bThrowException = false): ?self
|
||||
{
|
||||
$oAccount = null;
|
||||
if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) {
|
||||
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
|
||||
if ($oDomain) {
|
||||
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
|
||||
$oAccount = new self;
|
||||
$oAccount = new static;
|
||||
|
||||
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
|
||||
|
|
@ -236,7 +198,7 @@ class Account implements \JsonSerializable
|
|||
array $aAccountHash,
|
||||
bool $bThrowExceptionOnFalse = false): ?self
|
||||
{
|
||||
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 === \count($aAccountHash)) {
|
||||
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 === \count($aAccountHash)) {
|
||||
$oAccount = static::NewInstanceByLogin(
|
||||
$oActions,
|
||||
$aAccountHash[1],
|
||||
|
|
@ -248,15 +210,14 @@ class Account implements \JsonSerializable
|
|||
|
||||
if ($oAccount) {
|
||||
// init proxy user/password
|
||||
if (!empty($aAccountHash[6]) && !empty($aAccountHash[7])) {
|
||||
$oAccount->SetProxyAuthUser($aAccountHash[6]);
|
||||
$oAccount->SetProxyAuthPassword($aAccountHash[7]);
|
||||
if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) {
|
||||
$oAccount->SetProxyAuthUser($aAccountHash[5]);
|
||||
$oAccount->SetProxyAuthPassword($aAccountHash[6]);
|
||||
}
|
||||
|
||||
$oActions->Logger()->AddSecret($oAccount->Password());
|
||||
$oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword());
|
||||
|
||||
$oAccount->SetParentEmail($aAccountHash[5]);
|
||||
return $oAccount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace RainLoop\Model;
|
||||
|
||||
use RainLoop\Utils;
|
||||
use RainLoop\Exceptions\ClientException;
|
||||
|
||||
class AdditionalAccount extends Account
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sParentEmail = '';
|
||||
|
||||
public function ParentEmail() : string
|
||||
{
|
||||
return $this->sParentEmail;
|
||||
}
|
||||
|
||||
public function SetParentEmail(string $sParentEmail) : void
|
||||
{
|
||||
$this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true));
|
||||
}
|
||||
|
||||
public function PasswordHash() : string
|
||||
{
|
||||
throw new \LogicException('Not allowed on AdditionalAccount');
|
||||
}
|
||||
|
||||
public function Hash() : string
|
||||
{
|
||||
return \md5(parent::Hash() . $this->sParentEmail);
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$aData = parent::jsonSerialize();
|
||||
$aData[] = $this->sParentEmail;
|
||||
return $aData;
|
||||
}
|
||||
|
||||
public function asTokenArray(Account $oMainAccount) : array
|
||||
{
|
||||
$sHash = $oMainAccount->PasswordHash();
|
||||
$aData = $this->jsonSerialize();
|
||||
$aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash);
|
||||
$aData[] = \hash_hmac('sha1', $aData[3], $sHash);
|
||||
return $aData;
|
||||
}
|
||||
|
||||
public static function NewInstanceFromTokenArray(
|
||||
\RainLoop\Actions $oActions,
|
||||
array $aAccountHash,
|
||||
bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/
|
||||
{
|
||||
$iCount = \count($aAccountHash);
|
||||
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 <= $iCount && 9 >= $iCount) {
|
||||
$sHash = $oActions->getMainAccountFromToken()->PasswordHash();
|
||||
$sPasswordHMAC = (8 < $iCount) ? \array_pop($aAccountHash) : null;
|
||||
$sParentEmail = \array_pop($aAccountHash);
|
||||
if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) {
|
||||
$aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash);
|
||||
}
|
||||
$oAccount = parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse);
|
||||
if ($oAccount) {
|
||||
$oAccount->SetParentEmail($sParentEmail);
|
||||
return $oAccount;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -142,8 +142,13 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
|
|||
|
||||
private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
|
||||
{
|
||||
$sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld';
|
||||
$sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : '';
|
||||
if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
|
||||
$sEmail = $oAccount->ParentEmail();
|
||||
$sSubEmail = $oAccount->Email();
|
||||
} else {
|
||||
$sEmail = $oAccount->Email() ?: 'nobody@unknown.tld';
|
||||
$sSubEmail = '';
|
||||
}
|
||||
|
||||
$aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld');
|
||||
$sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : '');
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
|||
if (null === $mAccount) {
|
||||
$iStorageType = StorageType::NOBODY;
|
||||
} else if ($mAccount instanceof \RainLoop\Model\Account) {
|
||||
$sEmail = $mAccount->ParentEmailHelper();
|
||||
if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction)
|
||||
$sEmail = $mAccount instanceof \RainLoop\Model\AdditionalAccount ? $mAccount->ParentEmail() : $mAccount->Email();
|
||||
if ($this->bLocal && $mAccount instanceof \RainLoop\Model\AdditionalAccount && !$bForDeleteAction)
|
||||
{
|
||||
$sSubEmail = $mAccount->Email();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -811,14 +811,16 @@ class ServiceActions
|
|||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to AdditionalAccount
|
||||
*/
|
||||
public function ServiceChange() : string
|
||||
{
|
||||
$this->oHttp->ServerNoCache();
|
||||
|
||||
// $oAccount = $this->oActions->getAccountFromToken(false);
|
||||
$oAccount = $this->oActions->GetAccount();
|
||||
$oMainAccount = $this->oActions->getMainAccountFromToken(false);
|
||||
|
||||
if ($oAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oAccount))
|
||||
if ($oMainAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oMainAccount))
|
||||
{
|
||||
$oAccountToLogin = null;
|
||||
$sEmail = empty($this->aPaths[2]) ? '' : \urldecode(\trim($this->aPaths[2]));
|
||||
|
|
@ -826,17 +828,16 @@ class ServiceActions
|
|||
{
|
||||
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail);
|
||||
|
||||
$aAccounts = $this->oActions->GetAccounts($oAccount);
|
||||
$aAccounts = $this->oActions->GetAccounts($oMainAccount);
|
||||
if (isset($aAccounts[$sEmail]))
|
||||
{
|
||||
$oAccountToLogin = $this->oActions->GetAccountFromCustomToken($aAccounts[$sEmail]);
|
||||
$oAccountToLogin = \RainLoop\Model\AdditionalAccount::NewInstanceFromTokenArray(
|
||||
$this->oActions,
|
||||
$aAccounts[$sEmail]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($oAccountToLogin)
|
||||
{
|
||||
$this->oActions->SetAuthToken($oAccountToLogin);
|
||||
}
|
||||
$this->oActions->SetAdditionalAuthToken($oAccountToLogin);
|
||||
}
|
||||
|
||||
$this->oActions->Location('./');
|
||||
|
|
|
|||
|
|
@ -147,6 +147,13 @@ class Utils
|
|||
return isset($_COOKIE[$sName]) ? $_COOKIE[$sName] : $mDefault;
|
||||
}
|
||||
|
||||
public static function GetSecureCookie(string $sName)
|
||||
{
|
||||
return isset($_COOKIE[$sName])
|
||||
? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($_COOKIE[$sName]))
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0, bool $bHttpOnly = true)
|
||||
{
|
||||
$sPath = static::$CookieDefaultPath;
|
||||
|
|
@ -161,6 +168,16 @@ class Utils
|
|||
));
|
||||
}
|
||||
|
||||
public static function SetSecureCookie(string $sName, $mValue, int $iExpire = 0, bool $bHttpOnly = true)
|
||||
{
|
||||
static::SetCookie(
|
||||
$sName,
|
||||
\MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($mValue)),
|
||||
$iExpire,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public static function ClearCookie(string $sName)
|
||||
{
|
||||
if (isset($_COOKIE[$sName])) {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ abstract class Crypt
|
|||
public static function setCipher(string $cipher) : bool
|
||||
{
|
||||
if ($cipher) {
|
||||
$aCiphers = static::listCiphers();
|
||||
if (\in_array($cipher, $aCiphers)) {
|
||||
$ciphers = static::listCiphers();
|
||||
if (\in_array($cipher, $ciphers)) {
|
||||
static::$cipher = $cipher;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -48,63 +48,60 @@ abstract class Crypt
|
|||
);
|
||||
}
|
||||
|
||||
public static function DecryptUrlSafe($sData, string $key = null) /* : mixed */
|
||||
{
|
||||
return \json_decode(
|
||||
\zlib_decode(
|
||||
static::XxteaDecrypt(
|
||||
\MailSo\Base\Utils::UrlSafeBase64Decode($sData),
|
||||
static::Passphrase($key)
|
||||
)
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public static function EncryptUrlSafe($mData, string $key = null) : string
|
||||
{
|
||||
return \MailSo\Base\Utils::UrlSafeBase64Encode(
|
||||
static::XxteaEncrypt(
|
||||
\zlib_encode(
|
||||
\json_encode($mData),
|
||||
ZLIB_ENCODING_RAW,
|
||||
9
|
||||
),
|
||||
static::Passphrase($key)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function Decrypt(array $data, string $key = null) /* : mixed */
|
||||
{
|
||||
if (3 === \count($data) && isset($data[0], $data[1], $data[2])) {
|
||||
$fn = "{$data[0]}Decrypt";
|
||||
if (\method_exists(__CLASS__, $fn)) {
|
||||
return \SnappyMail\Crypt::{$fn}($data[2], $data[1], $key);
|
||||
try {
|
||||
$fn = "{$data[0]}Decrypt";
|
||||
if (\method_exists(__CLASS__, $fn)) {
|
||||
$result = static::{$fn}($data[2], $data[1], $key);
|
||||
return \json_decode($result, true);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\trigger_error(__CLASS__ . "::{$fn}(): " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
\trigger_error(__CLASS__ . '::Decrypt() invalid $data');
|
||||
}
|
||||
}
|
||||
|
||||
public static function DecryptFromJSON(string $data, string $key = null) /* : mixed */
|
||||
{
|
||||
$aData = \json_decode($data, true);
|
||||
return \is_array($aData) ? static::Decrypt(\array_map('base64_decode', $aData), $key) : null;
|
||||
$data = \json_decode($data, true);
|
||||
if (!\is_array($data)) {
|
||||
\trigger_error(__CLASS__ . '::DecryptFromJSON() invalid $data');
|
||||
return null;
|
||||
}
|
||||
return static::Decrypt(\array_map('base64_decode', $data), $key);
|
||||
}
|
||||
|
||||
public static function DecryptUrlSafe(string $data, string $key = null) /* : mixed */
|
||||
{
|
||||
$data = \explode('.', $data);
|
||||
if (!\is_array($data)) {
|
||||
\trigger_error(__CLASS__ . '::DecryptUrlSafe() invalid $data');
|
||||
return null;
|
||||
}
|
||||
return static::Decrypt(\array_map('MailSo\\Base\\Utils::UrlSafeBase64Decode', $data), $key);
|
||||
}
|
||||
|
||||
public static function Encrypt($data, string $key = null) : array
|
||||
{
|
||||
$data = \json_encode($data);
|
||||
if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
|
||||
$nonce = \random_bytes(24);
|
||||
return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce)];
|
||||
}
|
||||
|
||||
if (static::$cipher && \is_callable('openssl_encrypt')) {
|
||||
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
||||
$result = ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)];
|
||||
} else if (static::$cipher && \is_callable('openssl_encrypt')) {
|
||||
$iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher));
|
||||
return ['openssl', $iv, static::OpenSSLEncrypt($data, $iv)];
|
||||
$result = ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)];
|
||||
} else {
|
||||
$salt = \random_bytes(16);
|
||||
$result = ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)];
|
||||
}
|
||||
|
||||
$salt = \random_bytes(16);
|
||||
return ['xxtea', $salt, static::XxteaEncrypt($data, $salt)];
|
||||
if (static::{"{$result[0]}Decrypt"}($result[2], $result[1], $key) !== $data) {
|
||||
throw new \Exception('Encrypt/Decrypt mismatch');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function EncryptToJSON($data, string $key = null) : string
|
||||
|
|
@ -112,26 +109,31 @@ abstract class Crypt
|
|||
return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key)));
|
||||
}
|
||||
|
||||
public static function EncryptUrlSafe($data, string $key = null) : string
|
||||
{
|
||||
return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key)));
|
||||
}
|
||||
|
||||
public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : mixed */
|
||||
{
|
||||
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
|
||||
return null;
|
||||
}
|
||||
return \json_decode(\zlib_decode(\sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
$data,
|
||||
APP_SALT,
|
||||
$nonce,
|
||||
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key))
|
||||
)));
|
||||
);
|
||||
}
|
||||
|
||||
public static function SodiumEncrypt($data, string $nonce, string $key = null) : ?string
|
||||
public static function SodiumEncrypt(string $data, string $nonce, string $key = null) : ?string
|
||||
{
|
||||
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
|
||||
return null;
|
||||
}
|
||||
return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9),
|
||||
$data,
|
||||
APP_SALT,
|
||||
$nonce,
|
||||
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key))
|
||||
|
|
@ -143,22 +145,22 @@ abstract class Crypt
|
|||
if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_decrypt')) {
|
||||
return null;
|
||||
}
|
||||
return \json_decode(\zlib_decode(\openssl_decrypt(
|
||||
return \openssl_decrypt(
|
||||
$data,
|
||||
static::$cipher,
|
||||
static::Passphrase($key),
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv
|
||||
)), true);
|
||||
);
|
||||
}
|
||||
|
||||
public static function OpenSSLEncrypt($data, string $iv, string $key = null) : ?string
|
||||
public static function OpenSSLEncrypt(string $data, string $iv, string $key = null) : ?string
|
||||
{
|
||||
if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_encrypt')) {
|
||||
return null;
|
||||
}
|
||||
return \openssl_encrypt(
|
||||
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9),
|
||||
$data,
|
||||
static::$cipher,
|
||||
static::Passphrase($key),
|
||||
OPENSSL_RAW_DATA,
|
||||
|
|
@ -172,18 +174,16 @@ abstract class Crypt
|
|||
return null;
|
||||
}
|
||||
$key = $salt . static::Passphrase($key);
|
||||
return \json_decode(\zlib_decode(\is_callable('xxtea_decrypt')
|
||||
return \is_callable('xxtea_decrypt')
|
||||
? \xxtea_decrypt($data, $key)
|
||||
: \MailSo\Base\Xxtea::decrypt($data, $key)
|
||||
), true);
|
||||
: \MailSo\Base\Xxtea::decrypt($data, $key);
|
||||
}
|
||||
|
||||
public static function XxteaEncrypt($data, string $salt, string $key = null) : ?string
|
||||
public static function XxteaEncrypt(string $data, string $salt, string $key = null) : ?string
|
||||
{
|
||||
if (!$data || !$salt) {
|
||||
return null;
|
||||
}
|
||||
$data = \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9);
|
||||
$key = $salt . static::Passphrase($key);
|
||||
return \is_callable('xxtea_encrypt')
|
||||
? \xxtea_encrypt($data, $key)
|
||||
|
|
|
|||
|
|
@ -5,35 +5,33 @@
|
|||
<span data-bind="visible: !isNew()" data-i18n="POPUPS_ADD_ACCOUNT/TITLE_UPDATE_ACCOUNT"></span>
|
||||
</h3>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="form-horizontal">
|
||||
<div class="alert" data-bind="visible: '' !== submitError()">
|
||||
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
|
||||
<span data-bind="text: submitError"></span>
|
||||
<div data-bind="visible: submitErrorAdditional">
|
||||
<br />
|
||||
<span data-bind="text: submitErrorAdditional"></span>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="control-group" data-bind="css: {'error': emailError}">
|
||||
<label class="control-label" data-i18n="GLOBAL/EMAIL"></label>
|
||||
<div class="controls">
|
||||
<label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label>
|
||||
<input type="email" class="input-xlarge"
|
||||
autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {'error': passwordError}">
|
||||
<label class="control-label" data-i18n="GLOBAL/PASSWORD"></label>
|
||||
<div class="controls">
|
||||
<input type="password" class="inputPassword input-xlarge" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: password, onEnter: addAccountCommand" />
|
||||
</div>
|
||||
<form class="modal-body form-horizontal" autocomplete="off">
|
||||
<div class="alert" data-bind="visible: '' !== submitError()">
|
||||
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
|
||||
<span data-bind="text: submitError"></span>
|
||||
<div data-bind="visible: submitErrorAdditional">
|
||||
<br />
|
||||
<span data-bind="text: submitErrorAdditional"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="control-group" data-bind="css: {'error': emailError}">
|
||||
<label class="control-label" data-i18n="GLOBAL/EMAIL"></label>
|
||||
<div class="controls">
|
||||
<label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label>
|
||||
<input type="email" class="input-xlarge"
|
||||
autofocus="" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" data-bind="css: {'error': passwordError}">
|
||||
<label class="control-label" data-i18n="GLOBAL/PASSWORD"></label>
|
||||
<div class="controls">
|
||||
<input type="password" class="inputPassword input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="value: password, onEnter: addAccountCommand" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<footer>
|
||||
<a class="btn buttonAddAccount" data-bind="command: addAccountCommand">
|
||||
<i data-bind="visible: isNew, css: {'icon-user-add': !submitRequest(), 'icon-spinner': submitRequest()}"></i>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue