Improved handling of Internationalized Domain Names in punycode

This commit is contained in:
the-djmaze 2024-03-12 16:06:17 +01:00
parent fd54796710
commit 696a2bbd3c
45 changed files with 661 additions and 236 deletions

View file

@ -35,7 +35,9 @@ module.exports = {
// vendors/bootstrap/bootstrap.native.js
'BSN': "readonly",
// Mailvelope
'mailvelope': "readonly"
'mailvelope': "readonly",
// Punycode
'IDN': "readonly"
},
// http://eslint.org/docs/rules/
rules: {

View file

@ -28,6 +28,10 @@ export class AccountModel extends AbstractModel {
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
}
label() {
return this.name || IDN.toUnicode(this.email);
}
/**
* Get INBOX unread messages
*/

View file

@ -13,6 +13,7 @@ DomainAdminStore.fetch = () => {
if (!iError) {
DomainAdminStore(
data.Result.map(item => {
item.name = IDN.toUnicode(item.name);
item.disabled = ko.observable(item.disabled);
item.askDelete = ko.observable(false);
return item;

View file

@ -87,9 +87,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
}
accountName() {
let email = AccountUserStore.email(),
account = AccountUserStore.find(account => account.email == email);
return account?.name || email;
const email = AccountUserStore.email();
return AccountUserStore.find(account => account.email == email)?.label() || IDN.toUnicode(email);
}
settingsClick() {

View file

@ -10,8 +10,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Avatars',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
VERSION = '1.15',
RELEASE = '2024-01-22',
VERSION = '1.16',
RELEASE = '2024-03-12',
REQUIRED = '2.25.0',
CATEGORY = 'Contacts',
LICENSE = 'MIT',
@ -208,7 +208,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
return null;
}
$sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
$sAsciiEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
$sEmailId = \sha1($sAsciiEmail);
\MailSo\Base\Http::setETag($sEmailId);
@ -310,7 +310,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function cacheImage(string $sEmail, array $aResult) : void
{
$sEmailId = \sha1(\mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)));
$sEmailId = \sha1(\mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)));
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
}
@ -323,7 +323,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function getCachedImage(string $sEmail) : ?array
{
$sEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
$sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
$aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*");
if ($aFiles) {
\MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));

View file

@ -57,7 +57,7 @@ class ChangePasswordHMailServerDriver
$this->oConfig->Get('plugin', 'hmailserver_password', '')
)) {
$sEmail = $oAccount->Email();
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail);
$oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain);
if ($oHmailDomain) {
$oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail);

View file

@ -6,9 +6,9 @@ class ChangePasswordHMailServerPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password hMailServer',
VERSION = '2.0',
RELEASE = '2022-10-14',
REQUIRED = '2.15.3',
VERSION = '2.1',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through hMailServer';

View file

@ -57,12 +57,12 @@ class ChangePasswordDriverLDAP
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
$sUserDn = \strtr($this->sUserDnFormat, array(
'{domain}' => $sDomain,
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
'{email}' => $oAccount->Email(),
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
'{email:domain}' => $sDomain,
'{login}' => $oAccount->IncLogin(),
'{imap:login}' => $oAccount->IncLogin(),

View file

@ -87,8 +87,8 @@ class ChangePasswordDriverPDO
':email' => $sEmail,
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword),
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword),
':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail),
':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail),
':domain' => \MailSo\Base\Utils::getEmailAddressDomain($sEmail),
':username' => \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail),
':login_name' => $oAccount->IncLogin()
);

View file

@ -7,8 +7,8 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
const
NAME = 'Change Password',
VERSION = '2.20',
RELEASE = '2024-03-10',
REQUIRED = '2.23.0',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords';

View file

@ -1,4 +1,4 @@
<?php
getEmailAddressDomain<?php
class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
{
@ -93,12 +93,12 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
return $aResult;
}
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
$sBaseDn = \strtr($this->sBaseDn, array(
'{domain}' => $sDomain,
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
'{email}' => $oAccount->Email(),
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
'{email:domain}' => $sDomain,
'{login}' => $oAccount->IncLogin(),
'{imap:login}' => $oAccount->IncLogin(),

View file

@ -5,8 +5,8 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
const
NAME = 'Contacts suggestions (LDAP)',
VERSION = '2.14',
RELEASE = '2024-03-10',
REQUIRED = '2.23.0',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Contacts',
DESCRIPTION = 'Get contacts suggestions from LDAP.';

View file

@ -8,10 +8,10 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
{
const
NAME = 'LDAP login mapping',
VERSION = '2.1',
VERSION = '2.2',
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
RELEASE = '2023-01-19',
REQUIRED = '2.19.2',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Login',
DESCRIPTION = 'Enable custom mapping using ldap field';
/**
@ -175,7 +175,7 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
'LDAP');
return FALSE;
}
$sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
$sLogin = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail);
$this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP');

View file

@ -69,7 +69,7 @@ class LdapMailAccounts
// and removes the domainname if this was configured inside the domain config.
$username = $sEmail;
$oActions = \RainLoop\Api::Actions();
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true);
if ($oDomain->ImapSettings()->shortLogin){
$username = @ldap_escape($this->RemoveEventualDomainPart($sEmail), "", LDAP_ESCAPE_FILTER);
}
@ -415,8 +415,8 @@ class LdapMailAccounts
*/
public static function RemoveEventualDomainPart(string $sInput) : string
{
// Copy of \MailSo\Base\Utils::GetAccountNameFromEmail to make sure that also after eventual future
// updates the input string gets returned when no '@' is found (GetDomainFromEmail already doesn't do this)
// Copy of \MailSo\Base\Utils::getEmailAddressLocalPart to make sure that also after eventual future
// updates the input string gets returned when no '@' is found (getEmailAddressDomain already doesn't do this)
$sResult = '';
if (\strlen($sInput))
{

View file

@ -15,8 +15,8 @@ class LdapMailAccountsPlugin extends AbstractPlugin
VERSION = '2.1.0',
AUTHOR = 'cm-schl',
URL = 'https://github.com/cm-sch',
RELEASE = '2024-03-10',
REQUIRED = '2.25.4',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Accounts',
DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).';

View file

@ -12,8 +12,8 @@ class LoginAutoconfigPlugin extends \RainLoop\Plugins\AbstractPlugin
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
VERSION = '1.1',
RELEASE = '2024-03-10',
REQUIRED = '2.34.0',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Login',
LICENSE = 'MIT',
DESCRIPTION = 'Tries to login using the domain autoconfig';
@ -27,7 +27,7 @@ class LoginAutoconfigPlugin extends \RainLoop\Plugins\AbstractPlugin
{
if (\str_contains($sEmail, '@')) {
$oProvider = $this->Manager()->Actions()->DomainProvider();
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail);
$oDomain = $oProvider->Load($sDomain, false);
if (!$oDomain) {
$result = \RainLoop\Providers\Domain\Autoconfig::discover($sEmail);

View file

@ -4,9 +4,9 @@ class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'OAuth2',
VERSION = '1.1',
RELEASE = '2022-11-11',
REQUIRED = '2.21.0',
VERSION = '1.2',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Login',
DESCRIPTION = 'IMAP, Sieve & SMTP login using RFC 7628 OAuth2';
@ -55,7 +55,7 @@ class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin
public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
{
$sPassword = $oSettings->Password;
$sPassword = $oSettings->passphrase;
$iGatLen = \strlen(static::GMAIL_TOKENS_PREFIX);
if ($sPassword && static::GMAIL_TOKENS_PREFIX === \substr($sPassword, 0, $iGatLen)) {
$aTokens = \json_decode(\substr($sPassword, $iGatLen));
@ -63,7 +63,7 @@ class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin
$sRefreshToken = !empty($aTokens[1]) ? $aTokens[1] : '';
}
if ($sAccessToken && $sRefreshToken) {
$oSettings->Password = $this->gmailRefreshToken($sAccessToken, $sRefreshToken);
$oSettings->passphrase = $this->gmailRefreshToken($sAccessToken, $sRefreshToken);
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2');
}
}

View file

@ -4,9 +4,9 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Login Override',
VERSION = '2.2',
RELEASE = '2023-02-08',
REQUIRED = '2.25.3',
VERSION = '2.3',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Filters',
DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.';
@ -50,10 +50,10 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
$line = \explode(':', $line, 3);
if (!empty($line[0]) && $line[0] === $sEmail) {
if (!empty($line[1])) {
$oSettings->Login = $line[1];
$oSettings->username = $line[1];
}
if (!empty($line[2])) {
$oSettings->Password = $line[2];
$oSettings->passphrase = $line[2];
}
break;
}

View file

@ -4,11 +4,11 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Nextcloud',
VERSION = '2.31',
RELEASE = '2024-02-18',
VERSION = '2.32',
RELEASE = '2024-03-12',
CATEGORY = 'Integrations',
DESCRIPTION = 'Integrate with Nextcloud v20+',
REQUIRED = '2.34.0';
REQUIRED = '2.35.3';
public function Init() : void
{
@ -80,7 +80,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
) {
$sAccessToken = \OC::$server->getSession()->get('oidc_access_token');
if ($sAccessToken) {
$oSettings->Password = $sAccessToken;
$oSettings->passphrase = $sAccessToken;
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
}
}

View file

@ -4,9 +4,9 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Override SMTP Credentials',
VERSION = '2.4',
RELEASE = '2023-01-19',
REQUIRED = '2.23.0',
VERSION = '2.5',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Filters',
DESCRIPTION = 'Override SMTP credentials for specific users.';
@ -67,8 +67,8 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
$sFoundValue = '';
if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $sWhiteList, $sFoundValue)) {
$oSettings->useAuth = (bool) $this->Config()->Get('plugin', 'smtp_auth', true);
$oSettings->Login = \trim($this->Config()->Get('plugin', 'smtp_user', ''));
$oSettings->Password = (string) $this->Config()->Get('plugin', 'smtp_password', '');
$oSettings->username = \trim($this->Config()->Get('plugin', 'smtp_user', ''));
$oSettings->passphrase = (string) $this->Config()->Get('plugin', 'smtp_password', '');
}
}

View file

@ -7,8 +7,8 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
VERSION = '2.16',
RELEASE = '2024-03-11',
REQUIRED = '2.26.4',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'General',
LICENSE = 'MIT',
DESCRIPTION = 'A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. For example, humans can read distorted text as the one shown below, but current computer programs can\'t. More info at https://developers.google.com/recaptcha';

View file

@ -7,9 +7,9 @@ class SmtpUseFromAdrAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Use From-Address-Account for smtp',
AUTHOR = 'attike',
URL = 'https://github.com/attike',
VERSION = '1.1',
RELEASE = '2024-03-10',
REQUIRED = '2.23.0',
VERSION = '1.1',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Filters',
DESCRIPTION = 'Set smpt-config and -credentials based on selected from-address-account';
@ -88,9 +88,9 @@ class SmtpUseFromAdrAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
if ( isset($this->aFromAccount[$oAccount->Email()]) ) {
$oFromAccount = $this->aFromAccount[$oAccount->Email()];
unset($this->aFromAccount[$oAccount->Email()]);
$oSettings->Login = $oFromAccount->OutLogin();
$oSettings->useAuth = $oFromAccount->Domain()->SmtpSettings()->useAuth;
$oSettings->Password = $oFromAccount->IncPassword();
$oSettings->username = $oFromAccount->OutLogin();
$oSettings->passphrase = $oFromAccount->IncPassword();
\SnappyMail\LOG::info(get_class($this),'user/pwd rewrite: '. $oFromAccount->Email());
}
}

View file

@ -409,13 +409,27 @@ abstract class Utils
return \trim($sValue);
}
/**
* @deprecated use getEmailAddressLocalPart
*/
public static function GetAccountNameFromEmail(string $sEmail) : string
{
return static::getEmailAddressLocalPart($sEmail);
}
public static function getEmailAddressLocalPart(string $sEmail) : string
{
$iPos = \strrpos($sEmail, '@');
return (false === $iPos) ? $sEmail : \substr($sEmail, 0, $iPos);
}
/**
* @deprecated use getEmailAddressDomain
*/
public static function GetDomainFromEmail(string $sEmail) : string
{
return static::getEmailAddressDomain($sEmail);
}
public static function getEmailAddressDomain(string $sEmail) : string
{
$iPos = \strrpos($sEmail, '@');
return (false === $iPos) ? '' : \substr($sEmail, $iPos + 1);
@ -676,10 +690,12 @@ abstract class Utils
}
/**
* Converts xn--du8h.snappymail.eu to 📧.snappymail.eu
* @deprecated
*/
public static function IdnToUtf8(string $sStr) : string
{
$trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
\trigger_error("Deprecated function IdnToAscii called at {$trace['file']}#{$trace['line']}", \E_USER_DEPRECATED);
if (\preg_match('/(^|\.|@)xn--/i', $sStr)) {
try
{
@ -691,10 +707,12 @@ abstract class Utils
}
/**
* Converts 📧.snappymail.eu to xn--du8h.snappymail.eu
* @deprecated
*/
public static function IdnToAscii(string $sStr, bool $bLowerCase = false) : string
{
$trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
\trigger_error("Deprecated function IdnToAscii called at {$trace['file']}#{$trace['line']}", \E_USER_DEPRECATED);
$aParts = \explode('@', $sStr);
$sDomain = \array_pop($aParts);
if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain)) {

View file

@ -52,7 +52,7 @@ class ImapClient extends \MailSo\Net\NetClient
public function Hash() : string
{
return \md5('ImapClientHash/'.
$this->Settings->Login . '@' .
$this->Settings->username . '@' .
$this->Settings->host . ':' .
$this->Settings->port
);
@ -110,8 +110,8 @@ class ImapClient extends \MailSo\Net\NetClient
return $this;
}
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($oSettings->Login));
$sPassword = $oSettings->Password;
$sLogin = $oSettings->username;
$sPassword = $oSettings->passphrase;
if (!\strlen($sLogin) || !\strlen($sPassword)) {
$this->writeLogException(new \UnexpectedValueException, \LOG_ERR);

View file

@ -17,8 +17,14 @@ namespace MailSo\Mime;
*/
class Email implements \JsonSerializable
{
/**
* display-name https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
*/
private string $sDisplayName;
/**
* addr-spec https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
*/
private string $sEmail;
private string $sDkimStatus = Enumerations\DkimStatus::NONE;
@ -32,8 +38,7 @@ class Email implements \JsonSerializable
throw new \ValueError;
}
$this->sEmail = \MailSo\Base\Utils::IdnToAscii(
\MailSo\Base\Utils::Trim($sEmail), true);
$this->sEmail = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($sEmail));
$this->sDisplayName = \MailSo\Base\Utils::Trim($sDisplayName);
}
@ -156,7 +161,7 @@ class Email implements \JsonSerializable
public function GetEmail(bool $bIdn = false) : string
{
return $bIdn ? \MailSo\Base\Utils::IdnToUtf8($this->sEmail) : $this->sEmail;
return $bIdn ? \SnappyMail\IDN::emailToUtf8($this->sEmail) : $this->sEmail;
}
public function GetDisplayName() : string
@ -164,14 +169,14 @@ class Email implements \JsonSerializable
return $this->sDisplayName;
}
public function GetAccountName() : string
public function getLocalPart() : string
{
return \MailSo\Base\Utils::GetAccountNameFromEmail($this->GetEmail(false));
return \MailSo\Base\Utils::getEmailAddressLocalPart($this->sEmail);
}
public function GetDomain(bool $bIdn = false) : string
{
return \MailSo\Base\Utils::GetDomainFromEmail($this->GetEmail($bIdn));
return \MailSo\Base\Utils::getEmailAddressDomain($this->GetEmail($bIdn));
}
public function SetDkimStatus(string $sDkimStatus)

View file

@ -48,8 +48,9 @@ class ConnectSettings implements \JsonSerializable
'PLAIN',
'LOGIN'
];
public string $Login = '';
private ?SensitiveString $Password = null;
private string $username = '';
private ?SensitiveString $passphrase = null;
public function __construct()
{
@ -58,21 +59,27 @@ class ConnectSettings implements \JsonSerializable
public function __get(string $name)
{
if ('Password' === $name) {
return $this->Password ? $this->Password->getValue() : '';
$name = \strtolower($name);
if ('passphrase' === $name || 'password' === $name) {
return $this->passphrase ? $this->passphrase->getValue() : '';
}
if ('username' === $name || 'login' === $name) {
return $this->username;
}
}
public function __set(string $name, $value)
{
if ('Password' === $name) {
$this->Password = \is_string($value) ? new SensitiveString($value) : $value;
public function __set(string $name,
#[\SensitiveParameter]
$value
) {
$name = \strtolower($name);
if ('passphrase' === $name || 'password' === $name) {
$this->passphrase = \is_string($value) ? new SensitiveString($value) : $value;
}
if ('username' === $name || 'login' === $name) {
$this->username = \SnappyMail\IDN::emailToAscii($value);
// $this->username = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($value));
}
}
public static function Host() : string
{
return \idn_to_ascii($this->host);
}
public static function fromArray(array $aSettings) : self

View file

@ -87,8 +87,8 @@ class SieveClient extends \MailSo\Net\NetClient
return $this;
}
$sLogin = $oSettings->Login;
$sPassword = $oSettings->Password;
$sLogin = $oSettings->username;
$sPassword = $oSettings->passphrase;
$sLoginAuthKey = '';
if (!\strlen($sLogin) || !\strlen($sPassword)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);

View file

@ -15,6 +15,7 @@
namespace MailSo\Smtp;
use MailSo\Net\Enumerations\ConnectionSecurityType;
use SnappyMail\IDN;
/**
* @category MailSo
@ -127,8 +128,8 @@ class SmtpClient extends \MailSo\Net\NetClient
return $this;
}
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($oSettings->Login));
$sPassword = $oSettings->Password;
$sLogin = $oSettings->username;
$sPassword = $oSettings->passphrase;
$type = '';
foreach ($oSettings->SASLMechanisms as $sasl_type) {
@ -227,9 +228,13 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function MailFrom(string $sFrom, int $iSizeIfSupported = 0, bool $bDsn = false, bool $bRequireTLS = false) : self
{
$sFrom = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sFrom), true);
// $sFrom = IDN::emailToAscii($sFrom);
$sCmd = "FROM:<{$sFrom}>";
// RFC 6531
if ($this->hasCapability('SMTPUTF8')) {
// $sFrom = IDN::emailToUtf8($sFrom);
// $sCmd = "FROM:<{$sFrom}> SMTPUTF8";
}
if (0 < $iSizeIfSupported && $this->hasCapability('SIZE')) {
$sCmd .= ' SIZE='.$iSizeIfSupported;
@ -243,18 +248,10 @@ class SmtpClient extends \MailSo\Net\NetClient
// RFC 6152
if ($this->hasCapability('8BITMIME')) {
// $sCmd .= ' BODY=8BITMIME';
// RFC 6531
if ($this->hasCapability('SMTPUTF8')) {
// $sCmd .= ' SMTPUTF8';
}
}
// RFC 3030
else if ($this->hasCapability('BINARYMIME')) {
// $sCmd .= ' BODY=BINARYMIME';
// RFC 6531
if ($this->hasCapability('SMTPUTF8')) {
// $sCmd .= ' SMTPUTF8';
}
}
// RFC 8689
@ -281,7 +278,7 @@ class SmtpClient extends \MailSo\Net\NetClient
$this->writeLogException(new \MailSo\RuntimeException('No sender reverse path has been supplied'), \LOG_ERR);
}
$sTo = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sTo), true);
// $sTo = IDN::emailToAscii($sTo);
$sCmd = 'TO:<'.$sTo.'>';
@ -413,18 +410,16 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function Vrfy(string $sUser) : self
{
$sUser = \MailSo\Base\Utils::Trim($sUser);
/*
// RFC 6531
if ($this->hasCapability('SMTPUTF8')) {
$this->sendRequestWithCheck('VRFY '
. \MailSo\Base\Utils::IdnToUtf8(\MailSo\Base\Utils::Trim($sUser))
. ' SMTPUTF8',
$this->sendRequestWithCheck('VRFY ' . IDN::emailToUtf8($sUser) . ' SMTPUTF8',
array(250, 251, 252),
);
} else {
*/
$this->sendRequestWithCheck(
'VRFY ' . \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser)),
$this->sendRequestWithCheck('VRFY ' . IDN::emailToAscii($sUser),
array(250, 251, 252)
);
return $this;
@ -442,16 +437,14 @@ class SmtpClient extends \MailSo\Net\NetClient
/*
public function Expn(string $sUser) : self
{
$sUser = \MailSo\Base\Utils::Trim($sUser);
// RFC 6531
if ($this->hasCapability('SMTPUTF8')) {
$this->sendRequestWithCheck('EXPN '
. \MailSo\Base\Utils::IdnToUtf8(\MailSo\Base\Utils::Trim($sUser))
. ' SMTPUTF8',
$this->sendRequestWithCheck('EXPN ' . IDN::emailToUtf8($sUser) . ' SMTPUTF8',
array(250, 251, 252)
);
} else {
$this->sendRequestWithCheck('EXPN '
. \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser)),
$this->sendRequestWithCheck('EXPN ' . IDN::emailToAscii($sUser),
array(250, 251, 252),
);
return $this;

View file

@ -326,10 +326,10 @@ class Actions
$sEmail = $oAccount->Email();
$sLine = \str_replace('{user:email}', $sEmail, $sLine);
$sLine = \str_replace('{user:login}', \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $sLine);
$sLine = \str_replace('{user:domain}', \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sLine);
$sLine = \str_replace('{user:login}', \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail), $sLine);
$sLine = \str_replace('{user:domain}', \MailSo\Base\Utils::getEmailAddressDomain($sEmail), $sLine);
$sLine = \str_replace('{user:domain-clear}',
\MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::GetDomainFromEmail($sEmail)),
\MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::getEmailAddressDomain($sEmail)),
$sLine);
}
}
@ -533,17 +533,17 @@ class Actions
return $this->oPlugins;
}
protected function LoggerAuthHelper(?Model\Account $oAccount, string $sLogin, bool $admin = false): void
protected function LoggerAuthHelper(?Model\Account $oAccount, string $sLogin = '', bool $admin = false): void
{
if ($sLogin) {
$sHost = $admin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::GetDomainFromEmail($sLogin);
$sHost = $admin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::getEmailAddressDomain($sLogin);
$aAdditionalParams = array(
'{imap:login}' => $sLogin,
'{imap:host}' => $sHost,
'{smtp:login}' => $sLogin,
'{smtp:host}' => $sHost,
'{user:email}' => $sLogin,
'{user:login}' => $admin ? $sLogin : \MailSo\Base\Utils::GetAccountNameFromEmail($sLogin),
'{user:login}' => $admin ? $sLogin : \MailSo\Base\Utils::getEmailAddressLocalPart($sLogin),
'{user:domain}' => $sHost,
);
} else {
@ -605,43 +605,43 @@ class Actions
} else {
$oAccount = $this->getAccountFromToken(false);
if ($oAccount) {
$aResult = \array_merge($aResult, [
'Auth' => true,
'Email' => \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()),
'accountHash' => $oAccount->Hash(),
'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]),
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'allowSpellcheck' => $oConfig->Get('defaults', 'allow_spellcheck', false),
'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true),
'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'),
'ViewImagesWhitelist' => '',
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
'AllowStyles' => false,
'ListInlineAttachments' => false,
'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true),
'MaxBlockquotesLevel' => 0,
'simpleAttachmentsList' => false,
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25),
'messageNewWindow' => false,
'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true),
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1),
'SoundNotification' => true,
'NotificationSound' => 'new-mail',
'DesktopNotifications' => true,
'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW),
'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')),
'editorWysiwyg' => 'Squire',
'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true),
'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false),
'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30),
'AllowDraftAutosave' => (bool) $oConfig->Get('defaults', 'allow_draft_autosave', true),
'ContactsAutosave' => (bool) $oConfig->Get('defaults', 'contacts_autosave', true),
'sieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false)
]);
$aResult = \array_merge(
$aResult,
[
'Auth' => true,
'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]),
'allowSpellcheck' => $oConfig->Get('defaults', 'allow_spellcheck', false),
'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true),
'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'),
'ViewImagesWhitelist' => '',
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
'AllowStyles' => false,
'ListInlineAttachments' => false,
'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true),
'MaxBlockquotesLevel' => 0,
'simpleAttachmentsList' => false,
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25),
'messageNewWindow' => false,
'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true),
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1),
'SoundNotification' => true,
'NotificationSound' => 'new-mail',
'DesktopNotifications' => true,
'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW),
'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')),
'editorWysiwyg' => 'Squire',
'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true),
'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false),
'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30),
'AllowDraftAutosave' => (bool) $oConfig->Get('defaults', 'allow_draft_autosave', true),
'ContactsAutosave' => (bool) $oConfig->Get('defaults', 'contacts_autosave', true),
'sieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false)
],
// MainAccount or AdditionalAccount
$this->getAccountData($oAccount)
);
$aAttachmentsActions = array();
if ($this->GetCapa(Capa::ATTACHMENTS_ACTIONS)) {
@ -677,13 +677,10 @@ class Actions
$mMailToData = Utils::DecodeKeyValuesQ($sToken);
if (!empty($mMailToData['MailTo']) && 'MailTo' === $mMailToData['MailTo'] && !empty($mMailToData['To'])) {
$aResult['mailToEmail'] = \MailSo\Base\Utils::IdnToUtf8($mMailToData['To']);
$aResult['mailToEmail'] = \SnappyMail\IDN::emailToUtf8($mMailToData['To']);
}
}
// MainAccount or AdditionalAccount
$aResult = \array_merge($aResult, $this->getAccountData($oAccount));
// MainAccount
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings instanceof Settings) {

View file

@ -12,6 +12,7 @@ use RainLoop\Notifications;
use RainLoop\Providers\Identities;
use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Utils;
use SnappyMail\IDN;
trait Accounts
{
@ -85,7 +86,7 @@ trait Accounts
$sName = \trim($this->GetActionParam('name', ''));
$bNew = !empty($this->GetActionParam('new', 1));
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
$sEmail = IDN::emailToAscii($sEmail);
if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) {
throw new ClientException(Notifications::AccountAlreadyExists);
} else if (!$bNew && !isset($aAccounts[$sEmail])) {
@ -109,7 +110,7 @@ trait Accounts
protected function loadAdditionalAccountImapClient(string $sEmail): \MailSo\Imap\ImapClient
{
$sEmail = \MailSo\Base\Utils::IdnToAscii(\trim($sEmail), true);
$sEmail = IDN::emailToAscii($sEmail);
if (!\strlen($sEmail)) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
@ -173,7 +174,7 @@ trait Accounts
}
$sEmailToDelete = \trim($this->GetActionParam('emailToDelete', ''));
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
$sEmailToDelete = IDN::emailToAscii($sEmailToDelete);
$aAccounts = $this->GetAccounts($oMainAccount);
@ -198,6 +199,7 @@ trait Accounts
{
$oConfig = $this->Config();
$aResult = [
// 'Email' => IDN::emailToUtf8($oAccount->Email()),
'Email' => $oAccount->Email(),
'accountHash' => $oAccount->Hash(),
'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(),
@ -330,7 +332,7 @@ trait Accounts
return $this->DefaultResponse(array(
'Accounts' => \array_values(\array_map(function($value){
return [
'email' => \MailSo\Base\Utils::IdnToUtf8($value['email'] ?? $value[1]),
'email' => IDN::emailToUtf8($value['email'] ?? $value[1]),
'name' => $value['name'] ?? ''
];
},

View file

@ -64,7 +64,7 @@ trait AdminDomains
$sLogin = '';
$this->resolveLoginCredentials($sEmail, $oPassword, $sLogin);
$oDomain = \str_contains($sEmail, '@')
? $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true)
? $this->DomainProvider()->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true)
: null;
return $this->DefaultResponse(array(
'email' => $sEmail,
@ -111,8 +111,8 @@ trait AdminDomains
'connectCapa' => \array_values(\array_diff($oImapClient->Capabilities(), ['STARTTLS']))
];
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oSettings->username = $aAuth['user'];
$oSettings->passphrase = $aAuth['pass'];
$oImapClient->Login($oSettings);
$mImapResult['authCapa'] = \array_values(\array_unique(\array_map(function($n){
return \str_starts_with($n, 'THREAD=') ? 'THREAD' : $n;
@ -156,8 +156,8 @@ trait AdminDomains
];
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oSettings->username = $aAuth['user'];
$oSettings->passphrase = $aAuth['pass'];
$oSmtpClient->Login($oSettings);
$mSmtpResult['authCapa'] = $oSmtpClient->Capability();
}
@ -192,8 +192,8 @@ trait AdminDomains
];
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oSettings->username = $aAuth['user'];
$oSettings->passphrase = $aAuth['pass'];
$oSieveClient->Login($oSettings);
$mSieveResult['authCapa'] = $oSieveClient->Capability();
}

View file

@ -762,8 +762,7 @@ trait Messages
$this->Plugins()->RunHook('filter.smtp-from', array($oAccount, $oMessage, &$sFrom));
$aHiddenRcpt = array();
if ($bAddHiddenRcpt)
{
if ($bAddHiddenRcpt) {
$this->Plugins()->RunHook('filter.smtp-hidden-rcpt', array($oAccount, $oMessage, &$aHiddenRcpt));
}

View file

@ -154,26 +154,23 @@ trait UserAuth
$oMainAccount = $this->getMainAccountFromToken(false);
if ($sEmail && $oMainAccount && $this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) {
$oAccount = null;
if ($oMainAccount->Email() === $sEmail) {
$this->SetAdditionalAuthToken($oAccount);
return true;
}
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail);
$aAccounts = $this->GetAccounts($oMainAccount);
if (!isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oAccount = AdditionalAccount::NewInstanceFromTokenArray(
$this, $aAccounts[$sEmail]
);
if (!$oAccount) {
throw new ClientException(Notifications::AccountSwitchFailed);
}
// Test the login
$oImapClient = new \MailSo\Imap\ImapClient;
$this->imapConnect($oAccount, false, $oImapClient);
if ($oMainAccount->Email() !== $sEmail) {
$sEmail = \SnappyMail\IDN::emailToAscii($sEmail);
$aAccounts = $this->GetAccounts($oMainAccount);
if (!isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oAccount = AdditionalAccount::NewInstanceFromTokenArray(
$this, $aAccounts[$sEmail]
);
if (!$oAccount) {
throw new ClientException(Notifications::AccountSwitchFailed);
}
// Test the login
$oImapClient = new \MailSo\Imap\ImapClient;
$this->imapConnect($oAccount, false, $oImapClient);
}
$this->SetAdditionalAuthToken($oAccount);
return true;
}

View file

@ -104,7 +104,7 @@ abstract class Api
public static function ClearUserData(string $sEmail) : bool
{
if (\strlen($sEmail)) {
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail);
$sEmail = \SnappyMail\IDN::emailToAscii($sEmail);
$oStorageProvider = static::Actions()->StorageProvider();
if ($oStorageProvider && $oStorageProvider->IsActive()) {

View file

@ -19,7 +19,7 @@ abstract class Account implements \JsonSerializable
private string $sSmtpLogin = '';
private ?SensitiveString $sSmtpPassword = null;
private ?SensitiveString $oSmtpPassword = null;
private Domain $oDomain;
@ -36,7 +36,7 @@ abstract class Account implements \JsonSerializable
public function IncLogin() : string
{
return $this->oDomain->ImapSettings()->shortLogin
? \MailSo\Base\Utils::GetAccountNameFromEmail($this->sLogin)
? \MailSo\Base\Utils::getEmailAddressLocalPart($this->sLogin)
: $this->sLogin;
}
@ -49,7 +49,7 @@ abstract class Account implements \JsonSerializable
{
$sSmtpLogin = $this->sSmtpLogin ?: $this->sLogin;
return $this->oDomain->SmtpSettings()->shortLogin
? \MailSo\Base\Utils::GetAccountNameFromEmail($sSmtpLogin)
? \MailSo\Base\Utils::getEmailAddressLocalPart($sSmtpLogin)
: $sSmtpLogin;
}
@ -78,7 +78,7 @@ abstract class Account implements \JsonSerializable
string $sPassword
) : void
{
$this->sSmtpPassword = new SensitiveString($sPassword);
$this->oSmtpPassword = new SensitiveString($sPassword);
}
#[\ReturnTypeWillChange]
@ -90,10 +90,10 @@ abstract class Account implements \JsonSerializable
'pass' => $this->IncPassword(),
'name' => $this->sName
];
if ($this->sSmtpLogin && $this->sSmtpPassword) {
if ($this->sSmtpLogin && $this->oSmtpPassword) {
$result['smtp'] = [
'user' => $this->sSmtpLogin,
'pass' => $this->sSmtpPassword->getValue()
'pass' => $this->oSmtpPassword->getValue()
];
}
return $result;
@ -106,13 +106,13 @@ abstract class Account implements \JsonSerializable
{
$oAccount = null;
if ($sEmail && $sLogin && \strlen($oPassword)) {
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true);
if ($oDomain) {
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
$oAccount = new static;
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
$oAccount->sEmail = \SnappyMail\IDN::emailToAscii($sEmail);
$oAccount->sLogin = \SnappyMail\IDN::emailToAscii($sLogin);
$oAccount->SetPassword($oPassword);
$oAccount->oDomain = $oDomain;
@ -143,18 +143,11 @@ abstract class Account implements \JsonSerializable
if (empty($aAccount[0]) || 'account' != $aAccount[0] || 7 > \count($aAccount)) {
return [];
}
$aResult = [
return [
'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(
@ -190,7 +183,7 @@ abstract class Account implements \JsonSerializable
{
$oSettings = $this->Domain()->ImapSettings();
$oSettings->timeout = \max($oSettings->timeout, (int) $oConfig->Get('imap', 'timeout', $oSettings->timeout));
$oSettings->Login = $this->IncLogin();
$oSettings->username = $this->IncLogin();
$oSettings->expunge_all_on_delete |= !!$oConfig->Get('imap', 'use_expunge_all_on_delete', false);
$oSettings->fast_simple_search = !(!$oSettings->fast_simple_search || !$oConfig->Get('imap', 'message_list_fast_simple_search', true));
@ -207,14 +200,14 @@ abstract class Account implements \JsonSerializable
$oImapClient->Connect($oSettings);
$oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $oSettings));
$oSettings->Password = $this->oPassword;
$oSettings->passphrase = $this->oPassword;
return $this->netClientLogin($oImapClient, $oPlugins);
}
public function SmtpConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient) : bool
{
$oSettings = $this->Domain()->SmtpSettings();
$oSettings->Login = $this->OutLogin();
$oSettings->username = $this->OutLogin();
$oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper();
$oSmtpClient->Settings = $oSettings;
@ -227,18 +220,18 @@ abstract class Account implements \JsonSerializable
$oSmtpClient->Connect($oSettings);
$oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $oSettings));
/*
if ($this->oDomain->OutAskCredentials() && !($this->sSmtpPassword && $this->sSmtpLogin)) {
if ($this->oDomain->OutAskCredentials() && !($this->oSmtpPassword && $this->sSmtpLogin)) {
throw new RequireCredentialsException
}
*/
$oSettings->Password = $this->sSmtpPassword ?: $this->oPassword;
$oSettings->passphrase = $this->oSmtpPassword ?: $this->oPassword;
return $this->netClientLogin($oSmtpClient, $oPlugins);
}
public function SieveConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\SieveClient $oSieveClient, \RainLoop\Config\Application $oConfig)
{
$oSettings = $this->Domain()->SieveSettings();
$oSettings->Login = $this->IncLogin();
$oSettings->username = $this->IncLogin();
$oSieveClient->Settings = $oSettings;
@ -246,7 +239,7 @@ abstract class Account implements \JsonSerializable
$oSieveClient->Connect($oSettings);
$oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $oSettings));
$oSettings->Password = $this->oPassword;
$oSettings->passphrase = $this->oPassword;
return $this->netClientLogin($oSieveClient, $oPlugins);
}

View file

@ -9,7 +9,7 @@ class AdditionalAccount extends Account
{
public function ParentEmail() : string
{
return \trim(\MailSo\Base\Utils::IdnToAscii(\RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), true));
return \SnappyMail\IDN::emailToAscii(\RainLoop\Api::Actions()->getMainAccountFromToken()->Email());
}
public function Hash() : string

View file

@ -108,7 +108,7 @@ class Domain implements \JsonSerializable
if ($sW) {
$sEmail = \mb_strtolower($sEmail);
$sLogin = \mb_strtolower($sLogin);
$sUserPart = \MailSo\Base\Utils::GetAccountNameFromEmail($sLogin ?: $sEmail);
$sUserPart = \MailSo\Base\Utils::getEmailAddressLocalPart($sLogin ?: $sEmail);
$sItem = \strtok($sW, " ;,\n");
while (false !== $sItem) {
$sItem = \mb_strtolower(\idn_to_ascii(\trim($sItem)));
@ -192,17 +192,17 @@ class Domain implements \JsonSerializable
if (\strlen($sName) && \strlen($aDomain['imap_host'])) {
$oDomain = new self($sName);
$oDomain->IMAP->host = \idn_to_utf8($aDomain['imap_host']);
$oDomain->IMAP->host = $aDomain['imap_host'];
$oDomain->IMAP->port = (int) $aDomain['imap_port'];
$oDomain->IMAP->type = self::StrConnectionSecurityTypeToCons($aDomain['imap_secure'] ?? '');
$oDomain->IMAP->shortLogin = !empty($aDomain['imap_short_login']);
$oDomain->Sieve->enabled = !empty($aDomain['sieve_use']);
$oDomain->Sieve->host = \idn_to_utf8($aDomain['sieve_host']);
$oDomain->Sieve->host = $aDomain['sieve_host'];
$oDomain->Sieve->port = (int) ($aDomain['sieve_port'] ?? 4190);;
$oDomain->Sieve->type = self::StrConnectionSecurityTypeToCons($aDomain['sieve_secure'] ?? '');
$oDomain->SMTP->host = \idn_to_utf8($aDomain['smtp_host']);
$oDomain->SMTP->host = $aDomain['smtp_host'];
$oDomain->SMTP->port = (int) ($aDomain['smtp_port'] ?? 25);
$oDomain->SMTP->type = self::StrConnectionSecurityTypeToCons($aDomain['smtp_secure'] ?? '');
$oDomain->SMTP->shortLogin = !empty($aDomain['smtp_short_login']);

View file

@ -2,7 +2,6 @@
namespace RainLoop\Model;
use MailSo\Base\Utils;
use SnappyMail\SensitiveString;
class Identity implements \JsonSerializable
@ -102,7 +101,7 @@ class Identity implements \JsonSerializable
if (!empty($aData['Email'])) {
$this->sId = !empty($aData['Id']) ? $aData['Id'] : '';
$this->sLabel = isset($aData['Label']) ? $aData['Label'] : '';
$this->sEmail = $bJson ? Utils::IdnToAscii($aData['Email'], true) : $aData['Email'];
$this->sEmail = $bJson ? \SnappyMail\IDN::emailToAscii($aData['Email']) : $aData['Email'];
$this->sName = isset($aData['Name']) ? $aData['Name'] : '';
$this->sReplyTo = !empty($aData['ReplyTo']) ? $aData['ReplyTo'] : '';
$this->sBcc = !empty($aData['Bcc']) ? $aData['Bcc'] : '';
@ -146,7 +145,7 @@ class Identity implements \JsonSerializable
'@Object' => 'Object/Identity',
'id' => $this->sId,
'label' => $this->sLabel,
'email' => Utils::IdnToUtf8($this->sEmail),
'email' => \SnappyMail\IDN::emailToUtf8($this->sEmail),
'name' => $this->sName,
'replyTo' => $this->sReplyTo,
'bcc' => $this->sBcc,

View file

@ -1161,7 +1161,7 @@ class PdoAddressBook
{
static $aCache = array();
$sEmail = \MailSo\Base\Utils::IdnToAscii(\trim($sEmail), true);
$sEmail = \SnappyMail\IDN::emailToAscii(\trim($sEmail));
if (empty($sEmail)) {
throw new \ValueError('Empty Email argument');
}

View file

@ -9,8 +9,9 @@ abstract class Autoconfig
{
public static function discover(string $emailaddress) : ?array
{
$domain = \MailSo\Base\Utils::GetDomainFromEmail($emailaddress);
// $domain = \strtolower(\idn_to_ascii($domain));
// $emailaddress = \SnappyMail\IDN::emailToAscii($emailaddress);
$domain = \MailSo\Base\Utils::getEmailAddressDomain($emailaddress);
$domain = \strtolower(\idn_to_ascii($domain));
// First try
$autoconfig = static::resolve($domain, $emailaddress);
if ($autoconfig) {

View file

@ -207,23 +207,20 @@ class DefaultDomain implements DomainInterface
if ($bAlias) {
if ($bIncludeAliases) {
$aAliases[$sName] = array(
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'name' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => true
);
}
} else if (false !== \strpos($sName, '*')) {
$aWildCards[$sName] = array(
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'name' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => false
);
} else {
$aResult[$sName] = array(
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'name' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => false
);

View file

@ -13,7 +13,7 @@ abstract class IDN
return static::uri($string, true);
}
if (\str_contains($string, '@')) {
return static::emailAddress($string, true);
return static::emailToAscii($string);
}
return \idn_to_ascii($string);
}
@ -24,21 +24,35 @@ abstract class IDN
return static::uri($string, false);
}
if (\str_contains($string, '@')) {
return static::emailAddress($string, false);
return static::emailToUtf8($string);
}
return \idn_to_utf8($string);
}
/**
* Converts IDN domain part to lowercased punycode
* Like: 'Smile😀@📧.SnappyMail.eu' to 'Smile😀@xn--du8h.snappymail.eu'
* When the '@' is missing, it does nothing
*/
public static function emailToAscii(string $address) : string
{
return static::emailAddress($address, true);
}
/**
* Converts IDN domain part to unicode
* Like: 'Smile😀@xn--du8h.SnappyMail.eu' to 'Smile😀@📧.SnappyMail.eu'
* When the '@' is missing, it does nothing
*/
public static function emailToUtf8(string $address) : string
{
return static::emailAddress($address, false);
}
private static function domain(string $domain, bool $toAscii) : string
{
// if ($toAscii && \preg_match('/[^\x20-\x7E]/', $domain)) {
// if (!$toAscii && \preg_match('/(^|\\.)xn--/i', $domain)) {
return $toAscii ? \strtolower(\idn_to_ascii($domain)) : \idn_to_utf8($domain);
/*
$domain = \explode('.', $domain);
@ -53,7 +67,8 @@ abstract class IDN
private static function emailAddress(string $address, bool $toAscii) : string
{
if (!\str_contains($address, '@')) {
throw new \RuntimeException("Invalid email address: {$address}");
// throw new \RuntimeException("Invalid email address: {$address}");
return $address;
}
$local = \explode('@', $address);
$domain = static::domain(\array_pop($local), $toAscii);

View file

@ -18,7 +18,7 @@
<!-- ko foreach: accounts -->
<li role="presentation">
<a class="email-title" href="#" data-bind="click: $root.accountClick, text: name || email,
<a class="email-title" href="#" data-bind="click: $root.accountClick, text: label(),
attr: {title: email, 'data-unread': unreadEmails, 'data-icon': $root.accountEmail() === email ? '✔' : '👤'}"></a>
</li>
<!-- /ko -->

View file

@ -64,6 +64,7 @@ config.paths.js = {
'vendors/knockout/build/output/knockout-latest.js',
// 'vendors/knockout/build/output/knockout-latest.debug.js',
'vendors/squire/build/squire-raw.js',
'vendors/mathiasbynens/punycode.js',
'dev/External/SquireUI.js'
]
},

395
vendors/mathiasbynens/punycode.js vendored Normal file
View file

@ -0,0 +1,395 @@
/**
* Modified version of https://github.com/mathiasbynens/punycode.js
*/
(() => {
'use strict';
const
/** Highest positive signed 32-bit float value */
maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
/** Bootstring parameters */
base = 36,
tMin = 1,
tMax = 26,
skew = 38,
damp = 700,
initialBias = 72,
initialN = 128, // 0x80
delimiter = '-', // '\x2D'
/** Regular expressions */
regexPunycode = /^xn--/,
regexNonASCII = /[^\0-\x7F]/, // Note: U+007F DEL is excluded too.
regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g, // RFC 3490 separators
/** Error messages */
errors = {
'overflow': 'Overflow: input needs wider integers to process',
'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
'invalid-input': 'Invalid input'
},
/** Convenience shortcuts */
baseMinusTMin = base - tMin,
floor = Math.floor,
stringFromCharCode = String.fromCharCode,
/*--------------------------------------------------------------------------*/
/**
* A generic error utility function.
* @private
* @param {String} type The error type.
* @returns {Error} Throws a `RangeError` with the applicable error message.
*/
error = type => {
throw new RangeError(errors[type])
},
/**
* A simple `Array#map`-like wrapper to work with domain name strings or email
* addresses.
* @private
* @param {String} domain The domain name or email address.
* @param {Function} callback The function that gets called for every
* character.
* @returns {String} A new string of characters returned by the callback
* function.
*/
mapDomain = (domain, callback) => {
// In email addresses, only the domain name should be punycoded.
// Leave the local part (i.e. everything up to `@`) intact.
const parts = domain.split('@');
parts.push(
parts.pop()
.split(regexSeparators)
.map(label => callback(label))
.join('.')
);
return parts.join('@');
},
/**
* Creates an array containing the numeric code points of each Unicode
* character in the string. While JavaScript uses UCS-2 internally,
* this function will convert a pair of surrogate halves (each of which
* UCS-2 exposes as separate characters) into a single code point,
* matching UTF-16.
* @see <https://mathiasbynens.be/notes/javascript-encoding>
* @name decode
* @param {String} string The Unicode input string (UCS-2).
* @returns {Array} The new array of code points.
*/
ucs2decode = string => {
const output = [];
let counter = 0;
const length = string.length;
while (counter < length) {
const value = string.charCodeAt(counter++);
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
// It's a high surrogate, and there is a next character.
const extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) { // Low surrogate.
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
} else {
// It's an unmatched surrogate; only append this code unit, in case the
// next code unit is the high surrogate of a surrogate pair.
output.push(value);
counter--;
}
} else {
output.push(value);
}
}
return output;
},
/**
* Converts a basic code point into a digit/integer.
* @see `digitToBasic()`
* @private
* @param {Number} codePoint The basic numeric code point value.
* @returns {Number} The numeric value of a basic code point (for use in
* representing integers) in the range `0` to `base - 1`, or `base` if
* the code point does not represent a value.
*/
basicToDigit = codePoint => {
if (codePoint >= 0x30 && codePoint < 0x3A) {
return 26 + (codePoint - 0x30);
}
if (codePoint >= 0x41 && codePoint < 0x5B) {
return codePoint - 0x41;
}
if (codePoint >= 0x61 && codePoint < 0x7B) {
return codePoint - 0x61;
}
return base;
},
/**
* Converts a digit/integer into a basic code point.
* @see `basicToDigit()`
* @private
* @param {Number} digit The numeric value of a basic code point.
* @returns {Number} The basic code point whose value (when used for
* representing integers) is `digit`, which needs to be in the range
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
* used; else, the lowercase form is used. The behavior is undefined
* if `flag` is non-zero and `digit` has no uppercase form.
*/
digitToBasic = (digit, flag) =>
// 0..25 map to ASCII a..z or A..Z
// 26..35 map to ASCII 0..9
digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5),
/**
* Bias adaptation function as per section 3.4 of RFC 3492.
* https://tools.ietf.org/html/rfc3492#section-3.4
* @private
*/
adapt = (delta, numPoints, firstTime) => {
let k = 0;
delta = firstTime ? floor(delta / damp) : delta >> 1;
delta += floor(delta / numPoints);
for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
delta = floor(delta / baseMinusTMin);
}
return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
},
/**
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
* symbols.
* @memberOf punycode
* @param {String} input The Punycode string of ASCII-only symbols.
* @returns {String} The resulting string of Unicode symbols.
*/
decode = input => {
// Don't use UCS-2.
const output = [];
const inputLength = input.length;
let i = 0;
let n = initialN;
let bias = initialBias;
// Handle the basic code points: let `basic` be the number of input code
// points before the last delimiter, or `0` if there is none, then copy
// the first basic code points to the output.
let basic = input.lastIndexOf(delimiter);
if (basic < 0) {
basic = 0;
}
for (let j = 0; j < basic; ++j) {
// if it's not a basic code point
if (input.charCodeAt(j) >= 0x80) {
error('not-basic');
}
output.push(input.charCodeAt(j));
}
// Main decoding loop: start just after the last delimiter if any basic code
// points were copied; start at the beginning otherwise.
for (let index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
// `index` is the index of the next character to be consumed.
// Decode a generalized variable-length integer into `delta`,
// which gets added to `i`. The overflow checking is easier
// if we increase `i` as we go, then subtract off its starting
// value at the end to obtain `delta`.
const oldi = i;
for (let w = 1, k = base; /* no condition */; k += base) {
if (index >= inputLength) {
error('invalid-input');
}
const digit = basicToDigit(input.charCodeAt(index++));
if (digit >= base) {
error('invalid-input');
}
if (digit > floor((maxInt - i) / w)) {
error('overflow');
}
i += digit * w;
const t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
if (digit < t) {
break;
}
const baseMinusT = base - t;
if (w > floor(maxInt / baseMinusT)) {
error('overflow');
}
w *= baseMinusT;
}
const out = output.length + 1;
bias = adapt(i - oldi, out, oldi == 0);
// `i` was supposed to wrap around from `out` to `0`,
// incrementing `n` each time, so we'll fix that now:
if (floor(i / out) > maxInt - n) {
error('overflow');
}
n += floor(i / out);
i %= out;
// Insert `n` at position `i` of the output.
output.splice(i++, 0, n);
}
return String.fromCodePoint(...output);
},
/**
* Converts a string of Unicode symbols (e.g. a domain name label) to a
* Punycode string of ASCII-only symbols.
* @memberOf punycode
* @param {String} input The string of Unicode symbols.
* @returns {String} The resulting Punycode string of ASCII-only symbols.
*/
encode = input => {
const output = [];
// Convert the input in UCS-2 to an array of Unicode code points.
input = ucs2decode(input);
// Cache the length.
const inputLength = input.length;
// Initialize the state.
let n = initialN;
let delta = 0;
let bias = initialBias;
// Handle the basic code points.
for (const currentValue of input) {
if (currentValue < 0x80) {
output.push(stringFromCharCode(currentValue));
}
}
const basicLength = output.length;
let handledCPCount = basicLength;
// `handledCPCount` is the number of code points that have been handled;
// `basicLength` is the number of basic code points.
// Finish the basic string with a delimiter unless it's empty.
if (basicLength) {
output.push(delimiter);
}
// Main encoding loop:
while (handledCPCount < inputLength) {
// All non-basic code points < n have been handled already. Find the next
// larger one:
let m = maxInt;
for (const currentValue of input) {
if (currentValue >= n && currentValue < m) {
m = currentValue;
}
}
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
// but guard against overflow.
const handledCPCountPlusOne = handledCPCount + 1;
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
error('overflow');
}
delta += (m - n) * handledCPCountPlusOne;
n = m;
for (const currentValue of input) {
if (currentValue < n && ++delta > maxInt) {
error('overflow');
}
if (currentValue === n) {
// Represent delta as a generalized variable-length integer.
let q = delta;
for (let k = base; /* no condition */; k += base) {
const t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
if (q < t) {
break;
}
const qMinusT = q - t;
const baseMinusT = base - t;
output.push(
stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
);
q = floor(qMinusT / baseMinusT);
}
output.push(stringFromCharCode(digitToBasic(q, 0)));
bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLength);
delta = 0;
++handledCPCount;
}
}
++delta;
++n;
}
return output.join('');
};
/*--------------------------------------------------------------------------*/
/** Define the public API */
window.IDN = {
/**
* A string representing the current Punycode.js version number.
* @memberOf punycode
* @type String
*/
version: '2.3.1',
/**
* Converts a Punycode string representing a domain name or an email address
* to Unicode. Only the Punycoded parts of the input will be converted, i.e.
* it doesn't matter if you call it on a string that has already been
* converted to Unicode.
* @memberOf punycode
* @param {String} input The Punycoded domain name or email address to
* convert to Unicode.
* @returns {String} The Unicode representation of the given Punycode
* string.
*/
toUnicode: input => mapDomain(
input,
string => regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string
),
/**
* Converts a Unicode string representing a domain name or an email address to
* Punycode. Only the non-ASCII parts of the domain name will be converted,
* i.e. it doesn't matter if you call it with a domain that's already in
* ASCII.
* @memberOf punycode
* @param {String} input The domain name or email address to convert, as a
* Unicode string.
* @returns {String} The Punycode representation of the given domain name or
* email address.
*/
toASCII: input => mapDomain(
input,
string => regexNonASCII.test(string) ? 'xn--' + encode(string) : string
)
};
})();