Revamp the whole accounts system for better management and control.

This also solves RainLoop/#2134
This commit is contained in:
djmaze 2021-11-11 13:34:41 +01:00
parent ef4790d04e
commit a18d393f54
15 changed files with 441 additions and 401 deletions

View file

@ -1164,8 +1164,7 @@ END;
public static function UrlSafeBase64Decode(string $sValue) : string public static function UrlSafeBase64Decode(string $sValue) : string
{ {
$sValue = \rtrim(\strtr($sValue, '-_.', '+/='), '='); return \base64_decode(\strtr($sValue, '-_', '+/'), '=');
return static::Base64Decode(\str_pad($sValue, \strlen($sValue) + (\strlen($sValue) % 4), '=', STR_PAD_RIGHT));
} }
public static function ParseFetchSequence(string $sSequence) : array public static function ParseFetchSequence(string $sSequence) : array

View file

@ -31,6 +31,11 @@ class Actions
*/ */
const AUTH_SPEC_TOKEN_KEY = 'smaccount'; 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_TOKEN_KEY = 'smspeclogout';
const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk'; const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk';
@ -553,7 +558,7 @@ class Actions
{ {
$sKey = ''; $sKey = '';
if ($oAccount) { if ($oAccount) {
$sKey = $oAccount->ParentEmailHelper(); $sKey = $this->GetMainEmail($oAccount);
} }
$sIndexKey = empty($sKey) ? '_default_' : $sKey; $sIndexKey = empty($sKey) ? '_default_' : $sKey;
@ -918,7 +923,9 @@ class Actions
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true); $aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true);
} }
if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
$aResult['ParentEmail'] = $oAccount->ParentEmail(); $aResult['ParentEmail'] = $oAccount->ParentEmail();
}
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
@ -1128,32 +1135,6 @@ class Actions
return $bFirstOnEmpty && isset($aIdentities[0]) ? $aIdentities[0] : null; 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 public function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void
{ {
$sValue = $this->GetActionParam($sParamName, ''); $sValue = $this->GetActionParam($sParamName, '');
@ -1420,28 +1401,10 @@ class Actions
if (\count($aData)) { if (\count($aData)) {
$this->Logger()->Write('Import contacts from csv'); $this->Logger()->Write('Import contacts from csv');
$iCount = $oAddressBookProvider->ImportCsvArray($oAccount->ParentEmailHelper(), $aData); $iCount = $oAddressBookProvider->ImportCsvArray(
} $this->GetMainEmail($oAccount),
} $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);
} }
} }
} }

View file

@ -8,67 +8,54 @@ use RainLoop\Model\Account;
use RainLoop\Model\Identity; use RainLoop\Model\Identity;
use RainLoop\Notifications; use RainLoop\Notifications;
use RainLoop\Providers\Storage\Enumerations\StorageType; use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Utils;
trait Accounts trait Accounts
{ {
protected function GetMainEmail(Account $oAccount)
{
return $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email();
}
public function GetAccounts(Account $oAccount): array 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)) { if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
$sAccounts = $this->StorageProvider()->Get($oAccount, $sAccounts = $this->StorageProvider()->Get($oAccount,
StorageType::CONFIG, StorageType::CONFIG,
'accounts' 'additionalaccounts'
); );
$aAccounts = $sAccounts ? \json_decode($sAccounts, true) : array(); $aAccounts = $sAccounts ? \json_decode($sAccounts, true) : array();
if (\is_array($aAccounts) && \count($aAccounts)) { if ($aAccounts && \is_array($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);
});
}
}
return $aAccounts; return $aAccounts;
} }
} }
$aAccounts = array(); return array();
if (!$oAccount->IsAdditionalAccount()) {
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
}
return $aAccounts;
} }
protected function SetAccounts(Account $oAccount, array $aAccounts = array()): void protected function SetAccounts(Account $oAccount, array $aAccounts = array()): void
{ {
$sParentEmail = $oAccount->ParentEmailHelper(); if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
if (!$aAccounts || throw new \LogicException('Only main account can have sub accounts');
(1 === \count($aAccounts) && !empty($aAccounts[$sParentEmail]))) { }
$sParentEmail = $oAccount->Email();
if (!$aAccounts) {
$this->StorageProvider()->Clear( $this->StorageProvider()->Clear(
$oAccount, $oAccount,
StorageType::CONFIG, StorageType::CONFIG,
'accounts' 'additionalaccounts'
); );
} else { } else {
$this->StorageProvider()->Put( $this->StorageProvider()->Put(
$oAccount, $oAccount,
StorageType::CONFIG, StorageType::CONFIG,
'accounts', 'additionalaccounts',
\json_encode($aAccounts) \json_encode($aAccounts)
); );
} }
@ -79,36 +66,30 @@ trait Accounts
*/ */
public function DoAccountSetup(): array 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__); return $this->FalseResponse(__FUNCTION__);
} }
$sParentEmail = $oAccount->ParentEmailHelper(); $aAccounts = $this->GetAccounts($oMainAccount);
$aAccounts = $this->GetAccounts($oAccount);
$sEmail = \trim($this->GetActionParam('Email', '')); $sEmail = \trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', ''); $sPassword = $this->GetActionParam('Password', '');
$bNew = '1' === (string)$this->GetActionParam('New', '1'); $bNew = '1' === (string)$this->GetActionParam('New', '1');
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); $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); throw new ClientException(Notifications::AccountAlreadyExists);
} else if (!$bNew && !isset($aAccounts[$sEmail])) { } else if (!$bNew && !isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist); throw new ClientException(Notifications::AccountDoesNotExist);
} }
$oNewAccount = $this->LoginProcess($sEmail, $sPassword); $oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, $oMainAccount);
$oNewAccount->SetParentEmail($sParentEmail);
$aAccounts[$oNewAccount->Email()] = $oNewAccount->GetAuthToken(); $aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount);
if (!$oAccount->IsAdditionalAccount()) { $this->SetAccounts($oMainAccount, $aAccounts);
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
}
$this->SetAccounts($oAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__); return $this->TrueResponse(__FUNCTION__);
} }
@ -117,31 +98,29 @@ trait Accounts
*/ */
public function DoAccountDelete(): array 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__); return $this->FalseResponse(__FUNCTION__);
} }
$sParentEmail = $oAccount->ParentEmailHelper();
$sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', '')); $sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', ''));
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true); $sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
$aAccounts = $this->GetAccounts($oAccount); $aAccounts = $this->GetAccounts($oMainAccount);
if (\strlen($sEmailToDelete) && isset($aAccounts[$sEmailToDelete])) {
$bReload = false;
// $oAccount = $this->getAccountFromToken();
if ($oAccount->Email() === $sEmailToDelete) {
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bReload = true;
}
if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete])) {
unset($aAccounts[$sEmailToDelete]); unset($aAccounts[$sEmailToDelete]);
$this->SetAccounts($oMainAccount, $aAccounts);
$oAccountToChange = null; return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload));
if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail])) {
$oAccountToChange = $this->GetAccountFromCustomToken($aAccounts[$sParentEmail]);
if ($oAccountToChange) {
$this->SetAuthToken($oAccountToChange);
}
}
$this->SetAccounts($oAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__, array('Reload' => !!$oAccountToChange));
} }
return $this->FalseResponse(__FUNCTION__); return $this->FalseResponse(__FUNCTION__);
@ -188,8 +167,6 @@ trait Accounts
*/ */
public function DoAccountsAndIdentitiesSortOrder(): array public function DoAccountsAndIdentitiesSortOrder(): array
{ {
$oAccount = $this->getAccountFromToken();
$aAccounts = $this->GetActionParam('Accounts', null); $aAccounts = $this->GetActionParam('Accounts', null);
$aIdentities = $this->GetActionParam('Identities', null); $aIdentities = $this->GetActionParam('Identities', null);
@ -197,10 +174,20 @@ trait Accounts
return $this->FalseResponse(__FUNCTION__); return $this->FalseResponse(__FUNCTION__);
} }
return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put($oAccount, $oAccount = $this->getMainAccountFromToken();
StorageType::CONFIG, 'accounts_identities_order', 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( \json_encode(array(
'Accounts' => \is_array($aAccounts) ? $aAccounts : array(),
'Identities' => \is_array($aIdentities) ? $aIdentities : array() 'Identities' => \is_array($aIdentities) ? $aIdentities : array()
)) ))
)); ));
@ -211,39 +198,39 @@ trait Accounts
*/ */
public function DoAccountsAndIdentities(): array public function DoAccountsAndIdentities(): array
{ {
$oAccount = $this->getAccountFromToken(); $oAccount = $this->getMainAccountFromToken();
$mAccounts = false;
$aAccounts = false;
if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
$mAccounts = $this->GetAccounts($oAccount); $aAccounts = \array_map(
$mAccounts = \array_keys($mAccounts); 'MailSo\\Base\\Utils::IdnToUtf8',
\array_keys($this->GetAccounts($oAccount))
foreach ($mAccounts as $iIndex => $sName) { );
$mAccounts[$iIndex] = \MailSo\Base\Utils::IdnToUtf8($sName); if ($aAccounts) {
\array_unshift($aAccounts, \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()));
} }
} }
return $this->DefaultResponse(__FUNCTION__, array( return $this->DefaultResponse(__FUNCTION__, array(
'Accounts' => $mAccounts, 'Accounts' => $aAccounts,
'Identities' => $this->GetIdentities($oAccount) 'Identities' => $this->GetIdentities($oAccount)
)); ));
} }
/** /**
* @param Account $account * @param Account $oAccount
* @return Identity[] * @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 // 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 // Get all identities
$identities = $this->IdentitiesProvider()->GetIdentities($account, $allowMultipleIdentities); $identities = $this->IdentitiesProvider()->GetIdentities($oAccount, $allowMultipleIdentities);
// Sort identities // 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) ?? []; $order = \json_decode($orderString, true) ?? [];
if (isset($order['Identities']) && \is_array($order['Identities']) && \count($order['Identities']) > 1) { if (isset($order['Identities']) && \is_array($order['Identities']) && \count($order['Identities']) > 1) {
$list = \array_map(function ($item) { $list = \array_map(function ($item) {

View file

@ -5,6 +5,11 @@ namespace RainLoop\Actions;
trait Contacts trait Contacts
{ {
private function GetAccountParentEmail()
{
// $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email()
}
public function DoSaveContactsSyncData() : array public function DoSaveContactsSyncData() : array
{ {
$oAccount = $this->getAccountFromToken(); $oAccount = $this->getAccountFromToken();
@ -45,7 +50,7 @@ trait Contacts
if (isset($mData['Enable'], $mData['User'], $mData['Password'], $mData['Url']) && $mData['Enable']) if (isset($mData['Enable'], $mData['User'], $mData['Password'], $mData['Url']) && $mData['Enable'])
{ {
$bResult = $oAddressBookProvider->Sync( $bResult = $oAddressBookProvider->Sync(
$oAccount->ParentEmailHelper(), $this->GetMainEmail($oAccount),
$mData['Url'], $mData['User'], $mData['Password']); $mData['Url'], $mData['User'], $mData['Password']);
} }
} }
@ -75,7 +80,7 @@ trait Contacts
if ($oAbp->IsActive()) if ($oAbp->IsActive())
{ {
$iResultCount = 0; $iResultCount = 0;
$mResult = $oAbp->GetContacts($oAccount->ParentEmailHelper(), $mResult = $oAbp->GetContacts($this->GetMainEmail($oAccount),
$iOffset, $iLimit, $sSearch, $iResultCount); $iOffset, $iLimit, $sSearch, $iResultCount);
} }
@ -98,7 +103,7 @@ trait Contacts
$bResult = false; $bResult = false;
if (0 < \count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) 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); return $this->DefaultResponse(__FUNCTION__, $bResult);
@ -119,7 +124,7 @@ trait Contacts
$oContact = null; $oContact = null;
if (0 < \strlen($sUid)) if (0 < \strlen($sUid))
{ {
$oContact = $oAddressBookProvider->GetContactByID($oAccount->ParentEmailHelper(), $sUid); $oContact = $oAddressBookProvider->GetContactByID($this->GetMainEmail($oAccount), $sUid);
} }
if (!$oContact) if (!$oContact)
@ -159,7 +164,7 @@ trait Contacts
$oContact->PopulateDisplayAndFullNameValue(true); $oContact->PopulateDisplayAndFullNameValue(true);
$bResult = $oAddressBookProvider->ContactSave($oAccount->ParentEmailHelper(), $oContact); $bResult = $oAddressBookProvider->ContactSave($this->GetMainEmail($oAccount), $oContact);
} }
return $this->DefaultResponse(__FUNCTION__, array( return $this->DefaultResponse(__FUNCTION__, array(
@ -300,7 +305,7 @@ trait Contacts
$this->oHttp->ServerNoCache(); $this->oHttp->ServerNoCache();
return $this->AddressBookProvider($oAccount)->IsActive() ? 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 public function RawContactsCsv() : bool
@ -315,7 +320,31 @@ trait Contacts
$this->oHttp->ServerNoCache(); $this->oHttp->ServerNoCache();
return $this->AddressBookProvider($oAccount)->IsActive() ? 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;
} }
} }

View file

@ -324,7 +324,7 @@ trait Messages
$oSettings = $this->SettingsProvider()->Load($oAccount); $oSettings = $this->SettingsProvider()->Load($oAccount);
$this->AddressBookProvider($oAccount)->IncFrec( $this->AddressBookProvider($oAccount)->IncFrec(
$oAccount->ParentEmailHelper(), \array_values($aArrayToFrec), $this->GetMainEmail($oAccount), \array_values($aArrayToFrec),
!!$oSettings->GetConf('ContactsAutosave', !!$oSettings->GetConf('ContactsAutosave',
!!$oConfig->Get('defaults', 'contacts_autosave', true))); !!$oConfig->Get('defaults', 'contacts_autosave', true)));
} }

View file

@ -203,7 +203,7 @@ trait User
$oAccount = $this->getAccountFromToken(false); $oAccount = $this->getAccountFromToken(false);
if ($oAccount) if ($oAccount)
{ {
if (!$oAccount->IsAdditionalAccount()) if (!\is_subclass_of($oAccount, 'RainLoop\\Model\\Account'))
{ {
$this->ClearSignMeData(); $this->ClearSignMeData();
Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
@ -393,7 +393,7 @@ trait User
$oAddressBookProvider = $this->AddressBookProvider($oAccount); $oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) if ($oAddressBookProvider && $oAddressBookProvider->IsActive())
{ {
$aSuggestions = $oAddressBookProvider->GetSuggestions($oAccount->ParentEmailHelper(), $sQuery, $iLimit); $aSuggestions = $oAddressBookProvider->GetSuggestions($this->GetMainEmail($oAccount), $sQuery, $iLimit);
if (!\count($aResult)) if (!\count($aResult))
{ {
$aResult = $aSuggestions; $aResult = $aSuggestions;

View file

@ -5,6 +5,7 @@ namespace RainLoop\Actions;
use RainLoop\Notifications; use RainLoop\Notifications;
use RainLoop\Utils; use RainLoop\Utils;
use RainLoop\Model\Account; use RainLoop\Model\Account;
use RainLoop\Model\AdditionalAccount;
use RainLoop\Providers\Storage\Enumerations\StorageType; use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Exceptions\ClientException; use RainLoop\Exceptions\ClientException;
@ -13,18 +14,13 @@ trait UserAuth
/** /**
* @var string * @var string
*/ */
private $sSpecAuthToken = null; private $oAdditionalAuthAccount = null;
private $oSpecAuthAccount = null; private $oMainAuthAccount = null;
/**
* Or use 'aes-256-xts' ?
*/
private static $sCipher = 'aes-256-cbc-hmac-sha1';
/** /**
* @throws \RainLoop\Exceptions\ClientException * @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; $sInputEmail = $sEmail;
@ -117,7 +113,14 @@ trait UserAuth
$oAccount = null; $oAccount = null;
$sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', '')); $sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', ''));
try { try {
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); $oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
}
if (!$oAccount) { if (!$oAccount) {
throw new ClientException(Notifications::AuthError); throw new ClientException(Notifications::AuthError);
@ -141,86 +144,103 @@ trait UserAuth
} }
/** /**
* @throws \RainLoop\Exceptions\ClientException * Returns RainLoop\Model\AdditionalAccount when it exists,
*/ * else returns RainLoop\Model\Account when it exists,
public function GetAccountFromCustomToken(string $sToken): ?Account * else null
{ *
return empty($sToken) ? null : Account::NewInstanceFromAuthToken($sToken);
}
/**
* @throws \RainLoop\Exceptions\ClientException * @throws \RainLoop\Exceptions\ClientException
*/ */
public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
{ {
if (!$this->oSpecAuthAccount) { if (\is_null($this->oAdditionalAuthAccount) && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) {
if (!\is_string($this->sSpecAuthToken)) { $aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
$this->sSpecAuthToken = ''; if ($aData) {
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) { $this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray(
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(
$this, $this,
$aData, $aData,
$bThrowExceptionOnFalse $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); throw new ClientException(\RainLoop\Notifications::AuthError);
} }
} }
return $this->oSpecAuthAccount; return $this->oMainAuthAccount;
} }
public function SetAuthToken(Account $oAccount): void 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; public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void
$this->oSpecAuthAccount = null; {
Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken); $this->oAdditionalAuthAccount = $oAccount ?: false;
if ($oAccount) {
if (isset($aAccounts[$oAccount->Email()])) { Utils::SetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount);
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken(); } else {
$this->SetAccounts($oAccount, $aAccounts); Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
} }
} }
/**
* SignMe methods used for the "remember me" cookie
*/
private static function GetSignMeToken(): ?array private static function GetSignMeToken(): ?array
{ {
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY); $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
if (empty($sSignMeToken)) { if ($sSignMeToken) {
return null;
}
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken); $aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
return \is_array($aResult) ? $aResult : null; if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) {
return $aResult;
} }
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
private static function SetSignMeTokenCookie(array $aData): void }
{ return null;
Utils::SetCookie(
self::AUTH_SIGN_ME_TOKEN_KEY,
\SnappyMail\Crypt::EncryptUrlSafe($aData),
\time() + 3600 * 24 * 30 // 30 days
);
} }
private function SetSignMeToken(Account $oAccount): void private function SetSignMeToken(Account $oAccount): void
@ -230,25 +250,15 @@ trait UserAuth
$uuid = \SnappyMail\UUID::generate(); $uuid = \SnappyMail\UUID::generate();
$data = \SnappyMail\Crypt::Encrypt($oAccount); $data = \SnappyMail\Crypt::Encrypt($oAccount);
if ('xxtea' === $data[0]) { Utils::SetCookie(
static::SetSignMeTokenCookie(array( self::AUTH_SIGN_ME_TOKEN_KEY,
\SnappyMail\Crypt::EncryptUrlSafe([
'e' => $oAccount->Email(), 'e' => $oAccount->Email(),
'u' => $uuid, 'u' => $uuid,
'x' => \base64_encode($data[1]) $data[0] => \base64_encode($data[1])
)); ]),
} else if ('sodium' === $data[0]) { \time() + 3600 * 24 * 30 // 30 days
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])
));
}
$this->StorageProvider()->Put( $this->StorageProvider()->Put(
$oAccount, $oAccount,
@ -261,58 +271,55 @@ trait UserAuth
public function GetAccountFromSignMeToken(): ?Account public function GetAccountFromSignMeToken(): ?Account
{ {
$aTokenData = static::GetSignMeToken(); $aTokenData = static::GetSignMeToken();
if (!empty($aTokenData)) { if ($aTokenData) {
$oAccount = null; try
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) { {
$sAuthToken = $this->StorageProvider()->Get( $sAuthToken = $this->StorageProvider()->Get(
$aTokenData['e'], $aTokenData['e'],
StorageType::SIGN_ME, StorageType::SIGN_ME,
$aTokenData['u'] $aTokenData['u']
); );
if (empty($sAuthToken)) { if ($sAuthToken) {
return null; $aAccountHash = \SnappyMail\Crypt::Decrypt([
} \array_key_last($aTokenData),
if (!empty($aTokenData['x'])) { \base64_decode(\end($aTokenData)),
$aAccountHash = \SnappyMail\Crypt::XxteaDecrypt($sAuthToken, \base64_decode($aTokenData['x'])); $sAuthToken
} else if (!empty($aTokenData['s'])) { ]);
$aAccountHash = \SnappyMail\Crypt::SodiumDecrypt($sAuthToken, \base64_decode($aTokenData['s']));
} else if (!empty($aTokenData['o'])) { $oAccount = \is_array($aAccountHash)
$aAccountHash = \SnappyMail\Crypt::OpenSSLDecrypt($sAuthToken, \base64_decode($aTokenData['o'])); ? Account::NewInstanceFromTokenArray($this, $aAccountHash) : null;
}
if (!empty($aAccountHash) && \is_array($aAccountHash)) {
$oAccount = Account::NewInstanceFromTokenArray($this, $aAccountHash);
if ($oAccount) { if ($oAccount) {
try
{
$this->CheckMailConnection($oAccount); $this->CheckMailConnection($oAccount);
// Update lifetime // Update lifetime
$this->SetSignMeToken($oAccount); $this->SetSignMeToken($oAccount);
return $oAccount; return $oAccount;
} }
}
}
catch (\Throwable $oException) catch (\Throwable $oException)
{ {
} }
}
}
}
}
$this->ClearSignMeData(); $this->ClearSignMeData();
}
return null; return null;
} }
protected function ClearSignMeData() : void protected function ClearSignMeData() : void
{ {
if (isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY])) {
$aTokenData = static::GetSignMeToken(); $aTokenData = static::GetSignMeToken();
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) { if ($aTokenData) {
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']); $this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
}
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
} }
} }
/**
* Logout methods
*/
public function SetAuthLogoutToken(): void public function SetAuthLogoutToken(): void
{ {
\header('X-RainLoop-Action: Logout'); \header('X-RainLoop-Action: Logout');

View file

@ -42,21 +42,11 @@ class Account implements \JsonSerializable
*/ */
private $oDomain; private $oDomain;
/**
* @var string
*/
private $sParentEmail = '';
public function Email() : string public function Email() : string
{ {
return $this->sEmail; return $this->sEmail;
} }
public function ParentEmail() : string
{
return $this->sParentEmail;
}
public function ProxyAuthUser() : string public function ProxyAuthUser() : string
{ {
return $this->sProxyAuthUser; return $this->sProxyAuthUser;
@ -67,16 +57,6 @@ class Account implements \JsonSerializable
return $this->sProxyAuthPassword; return $this->sProxyAuthPassword;
} }
public function ParentEmailHelper() : string
{
return $this->sParentEmail ?: $this->sEmail;
}
public function IsAdditionalAccount() : bool
{
return !empty($this->sParentEmail);
}
public function IncLogin() : string public function IncLogin() : string
{ {
$sLogin = $this->sLogin; $sLogin = $this->sLogin;
@ -135,8 +115,7 @@ class Account implements \JsonSerializable
$this->sEmail, $this->sEmail,
$this->Domain()->IncHost(), $this->Domain()->IncHost(),
$this->Domain()->IncPort(), $this->Domain()->IncPort(),
$this->sPassword, $this->sPassword
$this->sParentEmail
])); ]));
} }
@ -145,11 +124,6 @@ class Account implements \JsonSerializable
$this->sPassword = $sPassword; $this->sPassword = $sPassword;
} }
public function SetParentEmail(string $sParentEmail) : void
{
$this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true));
}
public function SetProxyAuthUser(string $sProxyAuthUser) : void public function SetProxyAuthUser(string $sProxyAuthUser) : void
{ {
$this->sProxyAuthUser = $sProxyAuthUser; $this->sProxyAuthUser = $sProxyAuthUser;
@ -168,29 +142,15 @@ class Account implements \JsonSerializable
$this->sLogin, // 2 $this->sLogin, // 2
$this->sPassword, // 3 $this->sPassword, // 3
$this->sClientCert, // 4 $this->sClientCert, // 4
$this->sParentEmail, // 5 $this->sProxyAuthUser, // 5
$this->sProxyAuthUser, // 6 $this->sProxyAuthPassword // 6
$this->sProxyAuthPassword // 7
); );
} }
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 * @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 return empty($sToken) ? null
: static::NewInstanceFromTokenArray( : 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; $oAccount = null;
if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) { if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) {
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true); $oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
if ($oDomain) { if ($oDomain) {
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) { if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
$oAccount = new self; $oAccount = new static;
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); $oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin); $oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
@ -236,7 +198,7 @@ class Account implements \JsonSerializable
array $aAccountHash, array $aAccountHash,
bool $bThrowExceptionOnFalse = false): ?self 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( $oAccount = static::NewInstanceByLogin(
$oActions, $oActions,
$aAccountHash[1], $aAccountHash[1],
@ -248,15 +210,14 @@ class Account implements \JsonSerializable
if ($oAccount) { if ($oAccount) {
// init proxy user/password // init proxy user/password
if (!empty($aAccountHash[6]) && !empty($aAccountHash[7])) { if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) {
$oAccount->SetProxyAuthUser($aAccountHash[6]); $oAccount->SetProxyAuthUser($aAccountHash[5]);
$oAccount->SetProxyAuthPassword($aAccountHash[7]); $oAccount->SetProxyAuthPassword($aAccountHash[6]);
} }
$oActions->Logger()->AddSecret($oAccount->Password()); $oActions->Logger()->AddSecret($oAccount->Password());
$oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword()); $oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword());
$oAccount->SetParentEmail($aAccountHash[5]);
return $oAccount; return $oAccount;
} }
} }

View file

@ -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;
}
}

View file

@ -142,8 +142,13 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
{ {
$sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld'; if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
$sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : ''; $sEmail = $oAccount->ParentEmail();
$sSubEmail = $oAccount->Email();
} else {
$sEmail = $oAccount->Email() ?: 'nobody@unknown.tld';
$sSubEmail = '';
}
$aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld');
$sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : '');

View file

@ -103,8 +103,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
if (null === $mAccount) { if (null === $mAccount) {
$iStorageType = StorageType::NOBODY; $iStorageType = StorageType::NOBODY;
} else if ($mAccount instanceof \RainLoop\Model\Account) { } else if ($mAccount instanceof \RainLoop\Model\Account) {
$sEmail = $mAccount->ParentEmailHelper(); $sEmail = $mAccount instanceof \RainLoop\Model\AdditionalAccount ? $mAccount->ParentEmail() : $mAccount->Email();
if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction) if ($this->bLocal && $mAccount instanceof \RainLoop\Model\AdditionalAccount && !$bForDeleteAction)
{ {
$sSubEmail = $mAccount->Email(); $sSubEmail = $mAccount->Email();
} }

View file

@ -811,14 +811,16 @@ class ServiceActions
return ''; return '';
} }
/**
* Switch to AdditionalAccount
*/
public function ServiceChange() : string public function ServiceChange() : string
{ {
$this->oHttp->ServerNoCache(); $this->oHttp->ServerNoCache();
// $oAccount = $this->oActions->getAccountFromToken(false); $oMainAccount = $this->oActions->getMainAccountFromToken(false);
$oAccount = $this->oActions->GetAccount();
if ($oAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oAccount)) if ($oMainAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oMainAccount))
{ {
$oAccountToLogin = null; $oAccountToLogin = null;
$sEmail = empty($this->aPaths[2]) ? '' : \urldecode(\trim($this->aPaths[2])); $sEmail = empty($this->aPaths[2]) ? '' : \urldecode(\trim($this->aPaths[2]));
@ -826,17 +828,16 @@ class ServiceActions
{ {
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail); $sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail);
$aAccounts = $this->oActions->GetAccounts($oAccount); $aAccounts = $this->oActions->GetAccounts($oMainAccount);
if (isset($aAccounts[$sEmail])) if (isset($aAccounts[$sEmail]))
{ {
$oAccountToLogin = $this->oActions->GetAccountFromCustomToken($aAccounts[$sEmail]); $oAccountToLogin = \RainLoop\Model\AdditionalAccount::NewInstanceFromTokenArray(
$this->oActions,
$aAccounts[$sEmail]
);
} }
} }
$this->oActions->SetAdditionalAuthToken($oAccountToLogin);
if ($oAccountToLogin)
{
$this->oActions->SetAuthToken($oAccountToLogin);
}
} }
$this->oActions->Location('./'); $this->oActions->Location('./');

View file

@ -147,6 +147,13 @@ class Utils
return isset($_COOKIE[$sName]) ? $_COOKIE[$sName] : $mDefault; 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) public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0, bool $bHttpOnly = true)
{ {
$sPath = static::$CookieDefaultPath; $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) public static function ClearCookie(string $sName)
{ {
if (isset($_COOKIE[$sName])) { if (isset($_COOKIE[$sName])) {

View file

@ -28,8 +28,8 @@ abstract class Crypt
public static function setCipher(string $cipher) : bool public static function setCipher(string $cipher) : bool
{ {
if ($cipher) { if ($cipher) {
$aCiphers = static::listCiphers(); $ciphers = static::listCiphers();
if (\in_array($cipher, $aCiphers)) { if (\in_array($cipher, $ciphers)) {
static::$cipher = $cipher; static::$cipher = $cipher;
return true; 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 */ public static function Decrypt(array $data, string $key = null) /* : mixed */
{ {
if (3 === \count($data) && isset($data[0], $data[1], $data[2])) { if (3 === \count($data) && isset($data[0], $data[1], $data[2])) {
try {
$fn = "{$data[0]}Decrypt"; $fn = "{$data[0]}Decrypt";
if (\method_exists(__CLASS__, $fn)) { if (\method_exists(__CLASS__, $fn)) {
return \SnappyMail\Crypt::{$fn}($data[2], $data[1], $key); $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 */ public static function DecryptFromJSON(string $data, string $key = null) /* : mixed */
{ {
$aData = \json_decode($data, true); $data = \json_decode($data, true);
return \is_array($aData) ? static::Decrypt(\array_map('base64_decode', $aData), $key) : null; 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 public static function Encrypt($data, string $key = null) : array
{ {
$data = \json_encode($data);
if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
$nonce = \random_bytes(24); $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce)]; $result = ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)];
} } else if (static::$cipher && \is_callable('openssl_encrypt')) {
if (static::$cipher && \is_callable('openssl_encrypt')) {
$iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher)); $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); $salt = \random_bytes(16);
return ['xxtea', $salt, static::XxteaEncrypt($data, $salt)]; $result = ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)];
}
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 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))); 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 */ public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : mixed */
{ {
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
return null; return null;
} }
return \json_decode(\zlib_decode(\sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$data, $data,
APP_SALT, APP_SALT,
$nonce, $nonce,
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) \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')) { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
return null; return null;
} }
return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9), $data,
APP_SALT, APP_SALT,
$nonce, $nonce,
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) \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')) { if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_decrypt')) {
return null; return null;
} }
return \json_decode(\zlib_decode(\openssl_decrypt( return \openssl_decrypt(
$data, $data,
static::$cipher, static::$cipher,
static::Passphrase($key), static::Passphrase($key),
OPENSSL_RAW_DATA, OPENSSL_RAW_DATA,
$iv $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')) { if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_encrypt')) {
return null; return null;
} }
return \openssl_encrypt( return \openssl_encrypt(
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9), $data,
static::$cipher, static::$cipher,
static::Passphrase($key), static::Passphrase($key),
OPENSSL_RAW_DATA, OPENSSL_RAW_DATA,
@ -172,18 +174,16 @@ abstract class Crypt
return null; return null;
} }
$key = $salt . static::Passphrase($key); $key = $salt . static::Passphrase($key);
return \json_decode(\zlib_decode(\is_callable('xxtea_decrypt') return \is_callable('xxtea_decrypt')
? \xxtea_decrypt($data, $key) ? \xxtea_decrypt($data, $key)
: \MailSo\Base\Xxtea::decrypt($data, $key) : \MailSo\Base\Xxtea::decrypt($data, $key);
), true);
} }
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) { if (!$data || !$salt) {
return null; return null;
} }
$data = \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9);
$key = $salt . static::Passphrase($key); $key = $salt . static::Passphrase($key);
return \is_callable('xxtea_encrypt') return \is_callable('xxtea_encrypt')
? \xxtea_encrypt($data, $key) ? \xxtea_encrypt($data, $key)

View file

@ -5,8 +5,7 @@
<span data-bind="visible: !isNew()" data-i18n="POPUPS_ADD_ACCOUNT/TITLE_UPDATE_ACCOUNT"></span> <span data-bind="visible: !isNew()" data-i18n="POPUPS_ADD_ACCOUNT/TITLE_UPDATE_ACCOUNT"></span>
</h3> </h3>
</header> </header>
<div class="modal-body"> <form class="modal-body form-horizontal" autocomplete="off">
<div class="form-horizontal">
<div class="alert" data-bind="visible: '' !== submitError()"> <div class="alert" data-bind="visible: '' !== submitError()">
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button> <button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
<span data-bind="text: submitError"></span> <span data-bind="text: submitError"></span>
@ -21,19 +20,18 @@
<div class="controls"> <div class="controls">
<label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label> <label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label>
<input type="email" class="input-xlarge" <input type="email" class="input-xlarge"
autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" /> data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" />
</div> </div>
</div> </div>
<div class="control-group" data-bind="css: {'error': passwordError}"> <div class="control-group" data-bind="css: {'error': passwordError}">
<label class="control-label" data-i18n="GLOBAL/PASSWORD"></label> <label class="control-label" data-i18n="GLOBAL/PASSWORD"></label>
<div class="controls"> <div class="controls">
<input type="password" class="inputPassword input-xlarge" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false" <input type="password" class="inputPassword input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: password, onEnter: addAccountCommand" /> data-bind="value: password, onEnter: addAccountCommand" />
</div> </div>
</div> </div>
</div> </form>
</div>
<footer> <footer>
<a class="btn buttonAddAccount" data-bind="command: addAccountCommand"> <a class="btn buttonAddAccount" data-bind="command: addAccountCommand">
<i data-bind="visible: isNew, css: {'icon-user-add': !submitRequest(), 'icon-spinner': submitRequest()}"></i> <i data-bind="visible: isNew, css: {'icon-user-add': !submitRequest(), 'icon-spinner': submitRequest()}"></i>