Resolve #571 by allowing to give an account a name/label

This commit is contained in:
the-djmaze 2022-11-08 17:40:12 +01:00
parent 2ee9d973e2
commit decbbd8817
13 changed files with 168 additions and 122 deletions

View file

@ -152,41 +152,29 @@ export class AppUser extends AbstractApp {
IdentityUserStore.loading(false); IdentityUserStore.loading(false);
if (!iError) { if (!iError) {
const let items = oData.Result.Accounts;
// counts = {}, AccountUserStore(isArray(items)
accounts = oData.Result.Accounts, ? items.map(oValue => new AccountModel(oValue.email, oValue.name))
mainEmail = SettingsGet('MainEmail'); : []
);
AccountUserStore.unshift(new AccountModel(SettingsGet('MainEmail'), '', false));
if (isArray(accounts)) { items = oData.Result.Identities;
// AccountUserStore.accounts.forEach(oAccount => counts[oAccount.email] = oAccount.count()); IdentityUserStore(isArray(items)
? items.map(identityData => {
AccountUserStore.accounts( const identity = new IdentityModel(
accounts.map( pString(identityData.Id),
sValue => new AccountModel(sValue/*, counts[sValue]*/) pString(identityData.Email)
) );
); identity.name(pString(identityData.Name));
// accounts.length && identity.replyTo(pString(identityData.ReplyTo));
AccountUserStore.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false)); identity.bcc(pString(identityData.Bcc));
} identity.signature(pString(identityData.Signature));
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
if (isArray(oData.Result.Identities)) { return identity;
IdentityUserStore( })
oData.Result.Identities.map(identityData => { : []
const identity = new IdentityModel( );
pString(identityData.Id),
pString(identityData.Email)
);
identity.name(pString(identityData.Name));
identity.replyTo(pString(identityData.ReplyTo));
identity.bcc(pString(identityData.Bcc));
identity.signature(pString(identityData.Signature));
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
return identity;
})
);
}
} }
}); });
} }

View file

@ -7,11 +7,14 @@ export class AccountModel extends AbstractModel {
* @param {boolean=} canBeDelete = true * @param {boolean=} canBeDelete = true
* @param {number=} count = 0 * @param {number=} count = 0
*/ */
constructor(email/*, count = 0*/, isAdditional = true) { constructor(email, name/*, count = 0*/, isAdditional = true) {
super(); super();
this.name = name;
this.email = email; this.email = email;
this.displayName = name ? name + ' <' + email + '>' : email;
addObservablesTo(this, { addObservablesTo(this, {
// count: count || 0, // count: count || 0,
askDelete: false, askDelete: false,

View file

@ -101,7 +101,7 @@ export class MailBoxUserScreen extends AbstractScreen {
FolderUserStore.foldersInboxUnreadCount(e.detail); FolderUserStore.foldersInboxUnreadCount(e.detail);
/* // Disabled in SystemDropDown.html /* // Disabled in SystemDropDown.html
const email = AccountUserStore.email(); const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item => AccountUserStore.forEach(item =>
email === item?.email && item?.count(e.detail) email === item?.email && item?.count(e.detail)
); );
*/ */

View file

@ -16,7 +16,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts'); this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
this.allowIdentities = SettingsCapa('Identities'); this.allowIdentities = SettingsCapa('Identities');
this.accounts = AccountUserStore.accounts; this.accounts = AccountUserStore;
this.loading = AccountUserStore.loading; this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore; this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail'); this.mainEmail = SettingsGet('MainEmail');

View file

@ -1,11 +1,10 @@
import { addObservablesTo, koArrayWithDestroy } from 'External/ko'; import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = { export const AccountUserStore = koArrayWithDestroy();
accounts: koArrayWithDestroy(),
loading: ko.observable(false).extend({ debounce: 100 }),
getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email) AccountUserStore.loading = ko.observable(false).extend({ debounce: 100 });
};
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
addObservablesTo(AccountUserStore, { addObservablesTo(AccountUserStore, {
email: '', email: '',

View file

@ -12,6 +12,7 @@ export class AccountPopupView extends AbstractViewPopup {
addObservablesTo(this, { addObservablesTo(this, {
isNew: true, isNew: true,
name: '',
email: '', email: '',
password: '', password: '',
@ -40,18 +41,17 @@ export class AccountPopupView extends AbstractViewPopup {
} }
} }
onShow(account) { onHide() {
if (account?.isAdditional()) {
this.isNew(false);
this.email(account.email);
} else {
this.isNew(true);
this.email('');
}
this.password(''); this.password('');
this.submitRequest(false); this.submitRequest(false);
this.submitError(''); this.submitError('');
this.submitErrorAdditional(''); this.submitErrorAdditional('');
} }
onShow(account) {
let edit = account?.isAdditional();
this.isNew(!edit);
this.name(edit ? account.name : '');
this.email(edit ? account.email : '');
}
} }

View file

@ -30,11 +30,11 @@ export class SystemDropDownUserView extends AbstractViewRight {
this.accountEmail = AccountUserStore.email; this.accountEmail = AccountUserStore.email;
this.accounts = AccountUserStore.accounts; this.accounts = AccountUserStore;
this.accountsLoading = AccountUserStore.loading; this.accountsLoading = AccountUserStore.loading;
/* /*
this.accountsUnreadCount = : koComputable(() => 0); this.accountsUnreadCount = : koComputable(() => 0);
this.accountsUnreadCount = : koComputable(() => AccountUserStore.accounts().reduce((result, item) => result + item.count(), 0)); this.accountsUnreadCount = : koComputable(() => AccountUserStore().reduce((result, item) => result + item.count(), 0));
*/ */
addObservablesTo(this, { addObservablesTo(this, {
@ -91,8 +91,10 @@ export class SystemDropDownUserView extends AbstractViewRight {
return true; return true;
} }
emailTitle() { accountName() {
return AccountUserStore.email(); let email = AccountUserStore.email(),
account = AccountUserStore.find(account => account.email == email);
return account?.name || email;
} }
settingsClick() { settingsClick() {

View file

@ -41,7 +41,8 @@ trait Accounts
StorageType::CONFIG, StorageType::CONFIG,
'additionalaccounts' 'additionalaccounts'
); );
$aAccounts = $sAccounts ? \json_decode($sAccounts, true) : \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount); $aAccounts = $sAccounts ? \json_decode($sAccounts, true)
: \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount);
if ($aAccounts && \is_array($aAccounts)) { if ($aAccounts && \is_array($aAccounts)) {
return $aAccounts; return $aAccounts;
} }
@ -84,6 +85,7 @@ trait Accounts
$sEmail = \trim($this->GetActionParam('Email', '')); $sEmail = \trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', ''); $sPassword = $this->GetActionParam('Password', '');
$sName = $this->GetActionParam('Name', '');
$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);
@ -93,10 +95,17 @@ trait Accounts
throw new ClientException(Notifications::AccountDoesNotExist); throw new ClientException(Notifications::AccountDoesNotExist);
} }
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false); if ($bNew || $sPassword) {
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false);
$aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount);
} else {
$aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]);
}
$aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount); if ($aAccounts[$sEmail]) {
$this->SetAccounts($oMainAccount, $aAccounts); $aAccounts[$sEmail]['name'] = $sName;
$this->SetAccounts($oMainAccount, $aAccounts);
}
return $this->TrueResponse(__FUNCTION__); return $this->TrueResponse(__FUNCTION__);
} }
@ -241,11 +250,16 @@ trait Accounts
*/ */
public function DoAccountsAndIdentities(): array public function DoAccountsAndIdentities(): array
{ {
// https://github.com/the-djmaze/snappymail/issues/571
return $this->DefaultResponse(__FUNCTION__, array( return $this->DefaultResponse(__FUNCTION__, array(
'Accounts' => \array_map( 'Accounts' => \array_values(\array_map(function($value){
'MailSo\\Base\\Utils::IdnToUtf8', return [
\array_keys($this->GetAccounts($this->getMainAccountFromToken())) 'email' => \MailSo\Base\Utils::IdnToUtf8($value['email'] ?? $value[1]),
), 'name' => $value['name'] ?? ''
];
},
$this->GetAccounts($this->getMainAccountFromToken())
)),
'Identities' => $this->GetIdentities($this->getAccountFromToken()) 'Identities' => $this->GetIdentities($this->getAccountFromToken())
)); ));
} }

View file

@ -8,41 +8,30 @@ use RainLoop\Exceptions\ClientException;
abstract class Account implements \JsonSerializable abstract class Account implements \JsonSerializable
{ {
/** private string $sName = '';
* @var string
*/
private $sEmail;
/** private string $sEmail = '';
* @var string
*/
private $sLogin;
/** private string $sLogin = '';
* @var string
*/
private $sPassword;
/** private string $sPassword = '';
* @var string
*/
private $sProxyAuthUser = '';
/** private string $sProxyAuthUser = '';
* @var string
*/
private $sProxyAuthPassword = '';
/** private string $sProxyAuthPassword = '';
* @var \RainLoop\Model\Domain
*/ private Domain $oDomain;
private $oDomain;
public function Email() : string public function Email() : string
{ {
return $this->sEmail; return $this->sEmail;
} }
public function Name() : string
{
return $this->sName;
}
public function ProxyAuthUser() : string public function ProxyAuthUser() : string
{ {
return $this->sProxyAuthUser; return $this->sProxyAuthUser;
@ -123,15 +112,21 @@ abstract class Account implements \JsonSerializable
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function jsonSerialize() public function jsonSerialize()
{ {
return array( $result = [
'account', // 0 // 'account', // 0
$this->sEmail, // 1 'email' => $this->sEmail, // 1
$this->sLogin, // 2 'login' => $this->sLogin, // 2
$this->sPassword, // 3 'pass' => $this->sPassword, // 3
'', // 4 sClientCert // '', // 4 sClientCert
$this->sProxyAuthUser, // 5 'name' => $this->sName
$this->sProxyAuthPassword // 6 ];
); if ($this->sProxyAuthUser && $this->sProxyAuthPassword) {
$result['proxy'] = [
'user' => $this->sProxyAuthUser, // 5
'pass' => $this->sProxyAuthPassword // 6
];
}
return $result;
} }
public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions, public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions,
@ -165,34 +160,60 @@ abstract class Account implements \JsonSerializable
return $oAccount; return $oAccount;
} }
/**
* Converts old numeric array to new associative array
*/
public static function convertArray(array $aAccount) : array
{
if (isset($aAccount['email'])) {
return $aAccount;
}
if (empty($aAccount[0]) || 'account' != $aAccount[0] || 7 > \count($aAccount)) {
return [];
}
$aResult = [
'email' => $aAccount[1] ?: '',
'login' => $aAccount[2] ?: '',
'pass' => $aAccount[3] ?: ''
];
if ($aAccount[5] && $aAccount[6]) {
$aResult['proxy'] = [
'user' => $aAccount[5],
'pass' => $aAccount[6]
];
}
return $aResult;
}
public static function NewInstanceFromTokenArray( public static function NewInstanceFromTokenArray(
\RainLoop\Actions $oActions, \RainLoop\Actions $oActions,
array $aAccountHash, array $aAccountHash,
bool $bThrowExceptionOnFalse = false): ?self bool $bThrowExceptionOnFalse = false): ?self
{ {
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= \count($aAccountHash)) { $oAccount = null;
$aAccountHash = static::convertArray($aAccountHash);
if (!empty($aAccountHash['email']) && 3 <= \count($aAccountHash)) {
$oAccount = static::NewInstanceFromCredentials( $oAccount = static::NewInstanceFromCredentials(
$oActions, $oActions,
$aAccountHash[1] ?: '', $aAccountHash['email'],
$aAccountHash[2] ?: '', $aAccountHash['login'],
$aAccountHash[3] ?: '', $aAccountHash['pass'],
$bThrowExceptionOnFalse $bThrowExceptionOnFalse
); );
if ($oAccount) { if ($oAccount) {
// init proxy user/password if (isset($aAccountHash['name'])) {
if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) { $oAccount->sName = $aAccountHash['name'];
$oAccount->SetProxyAuthUser($aAccountHash[5]); }
$oAccount->SetProxyAuthPassword($aAccountHash[6]); // init proxy user/password
if (isset($aAccountHash['proxy'])) {
$oAccount->sProxyAuthUser = $aAccountHash['proxy']['user'];
$oAccount->sProxyAuthPassword = $aAccountHash['proxy']['pass'];
} }
$oActions->Logger()->AddSecret($oAccount->Password()); $oActions->Logger()->AddSecret($oAccount->Password());
$oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword()); $oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword());
return $oAccount;
} }
} }
return null; return $oAccount;
} }
public function ImapConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig) : bool public function ImapConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig) : bool

View file

@ -17,13 +17,25 @@ class AdditionalAccount extends Account
return \md5(parent::Hash() . $this->ParentEmail()); return \md5(parent::Hash() . $this->ParentEmail());
} }
public static function convertArray(array $aAccount) : array
{
$aResult = parent::convertArray($aAccount);
$iCount = \count($aAccount);
if ($aResult && 7 < $iCount && 9 >= $iCount) {
$aResult['hmac'] = \array_pop($aAccount);
}
return $aResult;
}
public function asTokenArray(MainAccount $oMainAccount) : array public function asTokenArray(MainAccount $oMainAccount) : array
{ {
$sHash = $oMainAccount->CryptKey(); $sHash = $oMainAccount->CryptKey();
$aData = $this->jsonSerialize(); $aData = $this->jsonSerialize();
$aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash); // sPassword $aData['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['pass'], $sHash); // sPassword
$aData[6] = \SnappyMail\Crypt::EncryptUrlSafe($aData[6], $sHash); // sProxyAuthPassword if (isset($aAccountHash['proxy'])) {
$aData[] = \hash_hmac('sha1', $aData[3], $sHash); $aData['proxy']['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['proxy']['pass'], $sHash); // sProxyAuthPassword
}
$aData['hmac'] = \hash_hmac('sha1', $aData['pass'], $sHash);
return $aData; return $aData;
} }
@ -32,13 +44,16 @@ class AdditionalAccount extends Account
array $aAccountHash, array $aAccountHash,
bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/ bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/
{ {
$iCount = \count($aAccountHash); $aAccountHash = static::convertArray($aAccountHash);
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= $iCount && 9 >= $iCount) { if (!empty($aAccountHash['email'])) {
$sHash = $oActions->getMainAccountFromToken()->CryptKey(); $sHash = $oActions->getMainAccountFromToken()->CryptKey();
$sPasswordHMAC = (7 < $iCount) ? \array_pop($aAccountHash) : null; // hmac only set when asTokenArray() was used
if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) { $sPasswordHMAC = $aAccountHash['hmac'] ?? null;
$aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash); if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash['pass'], $sHash)) {
$aAccountHash[6] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[6], $sHash); $aAccountHash['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['pass'], $sHash);
if (isset($aAccountHash['proxy'])) {
$aAccountHash['proxy']['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['proxy']['pass'], $sHash);
}
} }
return parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse); return parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse);
} }

View file

@ -19,7 +19,11 @@
<div class="control-group"> <div class="control-group">
<label data-i18n="GLOBAL/PASSWORD"></label> <label data-i18n="GLOBAL/PASSWORD"></label>
<input name="Password" type="password" class="input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off" <input name="Password" type="password" class="input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off"
required="" data-bind="value: password"> data-bind="value: password, attr: {required:isNew}">
</div>
<div class="control-group">
<label data-i18n="GLOBAL/NAME"></label>
<input name="Name" type="text" class="input-xlarge" data-bind="value: name">
</div> </div>
</form> </form>
<footer> <footer>

View file

@ -20,7 +20,7 @@
<i class="fontastic drag-handle"></i> <i class="fontastic drag-handle"></i>
</td> </td>
<td class="e-action"> <td class="e-action">
<span class="account-name" data-bind="text: email"></span> <span class="account-name" data-bind="text: displayName"></span>
</td> </td>
<td> <td>
<a class="btn btn-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete}, click: function(oAccount) { $root.deleteAccount(oAccount); }" <a class="btn btn-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete}, click: function(oAccount) { $root.deleteAccount(oAccount); }"

View file

@ -4,7 +4,7 @@
<div class="playIcon"><div></div></div> <div class="playIcon"><div></div></div>
<i class="stopIcon fontastic"></i> <i class="stopIcon fontastic"></i>
</div> </div>
<div class="accountPlace hide-mobile" data-bind="text: emailTitle()"></div> <div class="accountPlace hide-mobile" data-bind="text: accountName(), title: accountEmail"></div>
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true, openDropdownTrigger: accountMenuDropdownTrigger"> <div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true, openDropdownTrigger: accountMenuDropdownTrigger">
<a id="top-system-dropdown-id" href="#" tabindex="-1" class="btn single btn-block dropdown-toggle"> <a id="top-system-dropdown-id" href="#" tabindex="-1" class="btn single btn-block dropdown-toggle">
<i style="vertical-align:middle" class="fontastic" data-bind="css: {'icon-spinner': accountsLoading()}">👤</i> <i style="vertical-align:middle" class="fontastic" data-bind="css: {'icon-spinner': accountsLoading()}">👤</i>
@ -24,7 +24,7 @@
<span data-bind="visible: 99 < count()">99+</span> <span data-bind="visible: 99 < count()">99+</span>
</b>--> </b>-->
<i class="fontastic" data-bind="text: $root.accountEmail() === email ? '✔' : '👤'"></i> <i class="fontastic" data-bind="text: $root.accountEmail() === email ? '✔' : '👤'"></i>
<span class="email-title" data-bind="text: email, attr: {title: email}"></span> <span class="email-title" data-bind="text: name || email, attr: {title: email}"></span>
</a> </a>
</li> </li>
<!-- /ko --> <!-- /ko -->