Merge branch 'master' into plugin-2fa

This commit is contained in:
djmaze 2021-04-14 14:38:13 +02:00
commit c30de83da9
32 changed files with 1079 additions and 866 deletions

View file

@ -11,113 +11,109 @@ const
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
exts = {
eml: 'message/rfc822',
mime: 'message/rfc822',
rtx: 'text/richtext',
vcard: 'text/vcard',
vcf: 'text/vcard',
htm: 'text/html',
html: 'text/html',
csv: 'text/csv',
ics: 'text/calendar',
ifb: 'text/calendar',
xml: 'text/xml',
json: app+'json',
p10: app+'pkcs10',
p7c: app+'pkcs7-mime',
p7m: app+'pkcs7-mime',
p7s: app+'pkcs7-signature',
torrent: app+'x-bittorrent',
eml: 'message/rfc822',
mime: 'message/rfc822',
vcard: 'text/vcard',
vcf: 'text/vcard',
htm: 'text/html',
html: 'text/html',
csv: 'text/csv',
ics: 'text/calendar',
xml: 'text/xml',
json: app+'json',
asc: app+'pgp-signature',
p10: app+'pkcs10',
p7c: app+'pkcs7-mime',
p7m: app+'pkcs7-mime',
p7s: app+'pkcs7-signature',
torrent: app+'x-bittorrent',
// scripts
js: app+'javascript',
pl: 'text/perl',
css: 'text/css',
asp: 'text/asp',
php: app+'x-httpd-php',
phtml: app+'x-httpd-php',
// scripts
js: app+'javascript',
pl: 'text/perl',
css: 'text/css',
asp: 'text/asp',
php: app+'x-php',
// images
jpg: 'image/jpeg',
ico: 'image/x-icon',
tif: 'image/tiff',
svg: 'image/svg+xml',
svgz: 'image/svg+xml',
// images
jpg: 'image/jpeg',
ico: 'image/x-icon',
tif: 'image/tiff',
svg: 'image/svg+xml',
svgz: 'image/svg+xml',
// archives
zip: app+'zip',
'7z': app+'x-7z-compressed',
rar: app+'x-rar-compressed',
cab: app+'vnd.ms-cab-compressed',
gz: app+'x-gzip',
tgz: app+'x-gzip',
bz: app+'x-bzip',
bz2: app+'x-bzip2',
deb: app+'x-debian-package',
// archives
zip: app+'zip',
'7z': app+'x-7z-compressed',
rar: app+'x-rar-compressed',
cab: app+'vnd.ms-cab-compressed',
gz: app+'x-gzip',
tgz: app+'x-gzip',
bz: app+'x-bzip',
bz2: app+'x-bzip2',
deb: app+'x-debian-package',
// audio
mp3: 'audio/mpeg',
wav: 'audio/x-wav',
mp4a: 'audio/mp4',
weba: 'audio/webm',
m3u: 'audio/x-mpegurl',
// audio
mp3: 'audio/mpeg',
wav: 'audio/x-wav',
mp4a: 'audio/mp4',
weba: 'audio/webm',
m3u: 'audio/x-mpegurl',
// video
qt: 'video/quicktime',
mov: 'video/quicktime',
wmv: 'video/windows-media',
avi: 'video/x-msvideo',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
mp4v: 'video/mp4',
mpg4: 'video/mp4',
ogv: 'video/ogg',
m4v: 'video/x-m4v',
asf: 'video/x-ms-asf',
asx: 'video/x-ms-asf',
wm: 'video/x-ms-wm',
wmx: 'video/x-ms-wmx',
wvx: 'video/x-ms-wvx',
movie: 'video/x-sgi-movie',
// video
qt: 'video/quicktime',
mov: 'video/quicktime',
wmv: 'video/windows-media',
avi: 'video/x-msvideo',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
mp4v: 'video/mp4',
mpg4: 'video/mp4',
ogv: 'video/ogg',
m4v: 'video/x-m4v',
asf: 'video/x-ms-asf',
asx: 'video/x-ms-asf',
wm: 'video/x-ms-wm',
wmx: 'video/x-ms-wmx',
wvx: 'video/x-ms-wvx',
movie: 'video/x-sgi-movie',
// adobe
pdf: app+'pdf',
psd: 'image/vnd.adobe.photoshop',
ai: app+'postscript',
eps: app+'postscript',
ps: app+'postscript',
// adobe
pdf: app+'pdf',
psd: 'image/vnd.adobe.photoshop',
ai: app+'postscript',
eps: app+'postscript',
ps: app+'postscript',
// ms office
doc: app+'msword',
rtf: app+'rtf',
xls: app+'vnd.ms-excel',
ppt: app+'vnd.ms-powerpoint',
docx: msOffice+'wordprocessingml.document',
xlsx: msOffice+'spreadsheetml.sheet',
dotx: msOffice+'wordprocessingml.template',
pptx: msOffice+'presentationml.presentation',
// ms office
doc: app+'msword',
rtf: app+'rtf',
xls: app+'vnd.ms-excel',
ppt: app+'vnd.ms-powerpoint',
docx: msOffice+'wordprocessingml.document',
xlsx: msOffice+'spreadsheetml.sheet',
dotx: msOffice+'wordprocessingml.template',
pptx: msOffice+'presentationml.presentation',
// open office
odt: openDoc+'text',
ods: openDoc+'spreadsheet',
odp: openDoc+'presentation'
};
// open office
odt: openDoc+'text',
ods: openDoc+'spreadsheet',
odp: openDoc+'presentation'
};
export const FileType = {
Unknown: 'unknown',
Text: 'text',
Html: 'html',
Code: 'code',
Eml: 'eml',
WordText: 'word-text',
Word: 'word',
Pdf: 'pdf',
Image: 'image',
Audio: 'audio',
Video: 'video',
Sheet: 'sheet',
Spreadsheet: 'spreadsheet',
Presentation: 'presentation',
Certificate: 'certificate',
CertificateBin: 'certificate-bin',
Archive: 'archive'
};
@ -138,7 +134,7 @@ export const FileInfo = {
return app + 'ms-tnef';
}
let ext = fileName.split('.').pop();
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf|asc)$/.test(ext))
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
return 'text/plain';
if (/^(mpe?g|mpe|m1v|m2v)$/.test(ext))
return 'video/mpeg';
@ -186,15 +182,12 @@ export const FileInfo = {
case 'video' == mimeTypeParts[0] || 'mkv' == ext || 'avi' == ext:
result = FileType.Video;
break;
case ['php', 'js', 'css'].includes(ext):
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
result = FileType.Code;
break;
case 'eml' == ext || ['message/delivery-status', 'message/rfc822'].includes(mimeType):
result = FileType.Eml;
break;
case 'text/html' == mimeType || 'html' == ext:
result = FileType.Html;
break;
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
result = FileType.Text;
break;
@ -204,19 +197,17 @@ export const FileInfo = {
case 'pdf' == type || 'pdf' == ext:
result = FileType.Pdf;
break;
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType) ||
['asc', 'pem', 'ppk'].includes(ext):
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|| ['asc', 'pem', 'ppk'].includes(ext)
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
result = FileType.Certificate;
break;
case [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
result = FileType.CertificateBin;
break;
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|| ['rtf', 'msword', 'vnd.msword'].includes(type):
result = FileType.WordText;
result = FileType.Word;
break;
case match(msOffice+'.spreadsheetml') || match(openDoc+'.spreadsheet') || match('ms-excel'):
result = FileType.Sheet;
result = FileType.Spreadsheet;
break;
case match(msOffice+'.presentationml') || match(openDoc+'.presentation') || match('ms-powerpoint'):
result = FileType.Presentation;
@ -232,43 +223,22 @@ export const FileInfo = {
* @returns {string}
*/
getTypeIconClass: fileType => {
let result = ['icon-file', ''];
let result = 'icon-file';
switch (fileType) {
case FileType.Text:
case FileType.Eml:
case FileType.WordText:
result[0] += '-text';
break;
case FileType.Html:
case FileType.Code:
result[0] += '-code';
break;
case FileType.Image:
result[0] += '-image';
break;
case FileType.Audio:
result[0] += '-music';
break;
case FileType.Video:
result[0] += '-movie';
break;
case FileType.Archive:
result[0] += '-zip';
break;
case FileType.Certificate:
case FileType.CertificateBin:
result[0] += '-certificate';
break;
case FileType.Sheet:
result[0] += '-excel';
break;
case FileType.Presentation:
result[0] += '-chart-graph';
break;
case FileType.Pdf:
result[1] = 'pdf';
break;
// no default
case FileType.Word:
return result + '-text';
case FileType.Code:
case FileType.Image:
case FileType.Audio:
case FileType.Video:
case FileType.Archive:
case FileType.Certificate:
case FileType.Spreadsheet:
case FileType.Presentation:
return result + '-' + fileType;
}
return result;
},
@ -282,7 +252,7 @@ export const FileInfo = {
getCombinedIconClass: data => {
if (isNonEmptyArray(data)) {
let icons = data
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1])[0] : '')
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
.validUnique();
return (icons && 1 === icons.length && 'icon-file' !== icons[0])

View file

@ -27,7 +27,6 @@ class AbstractCheckbox extends AbstractComponent {
this.inline = !!params.inline;
this.labeled = undefined !== params.label;
this.labelAnimated = !!params.labelAnimated;
}
click() {

View file

@ -10,8 +10,6 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import { SMAudio } from 'Common/Audio';
const bAllowPdfPreview = undefined !== navigator.mimeTypes['application/pdf'];
export class AttachmentModel extends AbstractModel {
constructor() {
super();
@ -92,27 +90,21 @@ export class AttachmentModel extends AbstractModel {
* @returns {boolean}
*/
isText() {
return (
FileType.Text === this.fileType ||
FileType.Eml === this.fileType ||
FileType.Certificate === this.fileType ||
FileType.Html === this.fileType ||
FileType.Code === this.fileType
);
return FileType.Text === this.fileType || FileType.Eml === this.fileType;
}
/**
* @returns {boolean}
*/
isPdf() {
return FileType.Pdf === this.fileType;
pdfPreview() {
return null != navigator.mimeTypes['application/pdf'] && FileType.Pdf === this.fileType;
}
/**
* @returns {boolean}
*/
hasPreview() {
return this.isImage() || (this.isPdf() && bAllowPdfPreview) || this.isText();
return this.isImage() || this.pdfPreview() || this.isText();
}
/**
@ -155,13 +147,6 @@ export class AttachmentModel extends AbstractModel {
return link ? 'background:url(' + link + ')' : '';
}
/**
* @returns {string}
*/
linkPreviewAsPlain() {
return serverRequestRaw('ViewAsPlain', this.download);
}
/**
* @returns {string}
*/
@ -169,11 +154,11 @@ export class AttachmentModel extends AbstractModel {
let result = '';
switch (true) {
case this.isImage():
case this.isPdf() && bAllowPdfPreview:
case this.pdfPreview():
result = this.linkPreview();
break;
case this.isText():
result = this.linkPreviewAsPlain();
result = serverRequestRaw('ViewAsPlain', this.download);
break;
// no default
}
@ -181,18 +166,6 @@ export class AttachmentModel extends AbstractModel {
return result;
}
/**
* @returns {string}
*/
generateTransferDownloadUrl() {
let link = this.linkDownload();
if ('http' !== link.substr(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link;
}
return this.mimeType + ':' + this.fileName + ':' + link;
}
/**
* @param {AttachmentModel} attachment
* @param {*} event
@ -201,7 +174,11 @@ export class AttachmentModel extends AbstractModel {
eventDragStart(attachment, event) {
const localEvent = event.originalEvent || event;
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
localEvent.dataTransfer.setData('DownloadURL', this.generateTransferDownloadUrl());
let link = this.linkDownload();
if ('http' !== link.substr(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link;
}
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
}
return true;
@ -211,13 +188,6 @@ export class AttachmentModel extends AbstractModel {
* @returns {string}
*/
iconClass() {
return FileInfo.getTypeIconClass(this.fileType)[0];
}
/**
* @returns {string}
*/
iconText() {
return FileInfo.getTypeIconClass(this.fileType)[1];
return FileInfo.getTypeIconClass(this.fileType);
}
}

View file

@ -96,13 +96,6 @@ export class ComposeAttachmentModel extends AbstractModel {
* @returns {string}
*/
iconClass() {
return FileInfo.getIconClass(this.fileExt(), this.mimeType())[0];
}
/**
* @returns {string}
*/
iconText() {
return FileInfo.getIconClass(this.fileExt(), this.mimeType())[1];
return FileInfo.getIconClass(this.fileExt(), this.mimeType());
}
}

View file

@ -103,16 +103,6 @@
}
}
.attachmentIconText {
display: inline-block;
font-size: 28px;
width: 60px;
height: 56px;
color: #aaa;
line-height: 56px;
font-style: normal;
}
/*
.attachmentIconParent.hasPreview.isImage {
.iconMain {

345
plugins/README.md Normal file
View file

@ -0,0 +1,345 @@
class Plugin extends \RainLoop\Plugins\AbstractPlugin
# Hooks
$Plugin->addHook('hook.name', 'functionName');
## Login
### login.credentials.step-1
params:
string &$sEmail
### login.credentials.step-2
params:
string &$sEmail
string &$sPassword
### login.credentials
params:
string &$sEmail
string &$sLogin
string &$sPassword
### login.success
params:
\RainLoop\Model\Account $oAccount
## IMAP
### imap.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### imap.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
array $aCredentials
### imap.after-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
array $aCredentials
### imap.before-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
array $aCredentials
### imap.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
bool $bSuccess
array $aCredentials
## Sieve
### sieve.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### sieve.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
array $aCredentials
### sieve.after-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
array $aCredentials
### sieve.before-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
bool $bSuccess
array $aCredentials
### sieve.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
array $aCredentials
## SMTP
### smtp.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### smtp.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials
### smtp.after-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials
### smtp.before-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials
### smtp.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
bool $bSuccess
array $aCredentials
## Folders
### filter.folders-post
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\FolderCollection $oFolderCollection
### filter.folders-complete
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\FolderCollection $oFolderCollection
### filter.folders-system-types
params:
\RainLoop\Model\Account $oAccount
array &$aList
### filter.system-folders-names
params:
\RainLoop\Model\Account $oAccount
array &$aCache
## Others
### filter.account
params:
\RainLoop\Model\Account $oAccount
### filter.action-params
params:
string $sMethodName
array &$aCurrentActionParams
### filter.app-data
params:
bool $bAdmin
array &$aAppData
### filter.application-config
params:
\RainLoop\Config\Application $oConfig
### filter.build-message
params:
\MailSo\Mime\Message $oMessage
### filter.build-read-receipt-message
params:
\MailSo\Mime\Message $oMessage
\RainLoop\Model\Account $oAccount
### filter.domain
params:
\RainLoop\Model\Domain &$oDomain
### filter.fabrica
params:
string $sName
mixed &$mResult
\RainLoop\Model\Account $oAccount
### filter.http-paths
params:
array &$aPaths
### filter.http-query
params:
string &$sQuery
### filter.json-response
params:
string $sAction
array &$aResponseItem
### filter.message-html'
### filter.message-plain
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\Message $oMessage
string &$sTextConverted
### filter.message-rcpt
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\EmailCollection $oRcpt
### filter.read-receipt-message-plain
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\Message $oMessage
string &$sText
### filter.result-message
params:
\MailSo\Mime\Message $oMessage
### filter.save-message
params:
\MailSo\Mime\Message $oMessage
### filter.send-message
params:
\MailSo\Mime\Message $oMessage
### filter.send-message-stream
params:
\RainLoop\Model\Account $oAccount
resource &$rMessageStream
int &$iMessageStreamSize
### filter.send-read-receipt-message
params:
\MailSo\Mime\Message $oMessage
\RainLoop\Model\Account $oAccount
### filter.smtp-from
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\Message $oMessage
string &$sFrom
### filter.smtp-hidden-rcpt
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\Message $oMessage
array &$aHiddenRcpt
### filter.smtp-message-stream
params:
\RainLoop\Model\Account $oAccount
resource &$rMessageStream
int &$iMessageStreamSize
### filter.upload-response
params:
array &$aResponseItem
### json.action-post-call
params:
string $sAction
array &$aResponseItem
### json.action-post-call
params:
string $sAction
array &$aResponseItem
### json.action-pre-call
params:
string $sAction
### json.action-pre-call
params:
string $sAction
### json.suggestions-input-parameters
params:
string &$sQuery
int &$iLimit
\RainLoop\Model\Account $oAccount
### json.suggestions-post
params:
array &$aResult
string $sQuery
\RainLoop\Model\Account $oAccount
int $iLimit
### json.suggestions-pre
params:
array &$aResult
string $sQuery
\RainLoop\Model\Account $oAccount
int $iLimit
### main.default-response
params:
string $sActionName
array &$aResponseItem
### main.default-response
params:
string $sActionName
array &$aResponseItem
### main.default-response
params:
string $sActionName
array &$aResponseItem
### main.default-response-data
params:
string $sActionName
mixed &$mResult
### main.default-response-data
params:
string $sActionName
mixed &$mResult
### main.default-response-data
params:
string $sActionName
mixed &$mResult
### main.default-response-error-data
params:
string $sActionName
int &$iErrorCode
string &$sErrorMessage
### main.fabrica
params:
string $sName
mixed &$mResult
### service.app-delay-start-begin
no params
### service.app-delay-start-end
no params

View file

@ -22,8 +22,8 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials');
$this->addHook('filter.imap-credentials', 'FilterImapCredentials');
$this->addHook('smtp.credentials', 'FilterSmtpCredentials');
$this->addHook('imap.credentials', 'FilterImapCredentials');
}
/**

View file

@ -10,7 +10,7 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.login-credentials', 'FilterLoginCredentials');
$this->addHook('login.credentials', 'FilterLoginCredentials');
}
/**

View file

@ -9,7 +9,7 @@ class ChangeSmtpEhloMessagePlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials');
$this->addHook('smtp.credentials', 'FilterSmtpCredentials');
}
/**

View file

@ -9,7 +9,7 @@ class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.login-credentials', 'FilterLoginСredentials');
$this->addHook('login.credentials', 'FilterLoginCredentials');
}
/**
@ -19,7 +19,7 @@ class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
*
* @throws \RainLoop\Exceptions\ClientException
*/
public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword)
public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
{
// Your custom php logic
// You may change login credentials

View file

@ -10,7 +10,7 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.login-credentials', 'FilterLoginСredentials');
$this->addHook('login.credentials', 'FilterLoginCredentials');
}
/**
@ -20,7 +20,7 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
*
* @throws \RainLoop\Exceptions\ClientException
*/
public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword)
public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
{
$sMapping = \trim($this->Config()->Get('plugin', 'mapping', ''));
if (!empty($sMapping))

View file

@ -10,7 +10,7 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.smtp-credentials', 'FilterSmtpCredentials');
$this->addHook('smtp.credentials', 'FilterSmtpCredentials');
}
/**

View file

@ -9,7 +9,7 @@ class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('event.login-post-login-provide', 'EventLoginPostLoginProvide');
$this->addHook('login.success', 'EventLoginPostLoginProvide');
}
/**

View file

@ -10,7 +10,7 @@ class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin
public function Init() : void
{
$this->addHook('filter.login-credentials', 'FilterLoginCredentials');
$this->addHook('login.credentials', 'FilterLoginCredentials');
}
/**

View file

@ -744,7 +744,6 @@ END;
'sql' => 'text/plain',
'cfg' => 'text/plain',
'conf' => 'text/plain',
'asc' => 'text/plain',
'rtx' => 'text/richtext',
'vcard' => 'text/vcard',
'vcf' => 'text/vcard',
@ -759,6 +758,7 @@ END;
'hlp' => 'application/winhlp',
'wgt' => 'application/widget',
'chm' => 'application/vnd.ms-htmlhelp',
'asc' => 'application/pgp-signature',
'p10' => 'application/pkcs10',
'p7c' => 'application/pkcs7-mime',
'p7m' => 'application/pkcs7-mime',
@ -770,11 +770,7 @@ END;
'pl' => 'text/perl',
'css' => 'text/css',
'asp' => 'text/asp',
'php' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'php4' => 'application/x-httpd-php',
'php5' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'php' => 'application/x-php',
// images
'png' => 'image/png',

View file

@ -871,7 +871,7 @@ class Actions
}
}
public function LoginProvide(string $sEmail, string $sLogin, string $sPassword, string $sSignMeToken = '', string $sClientCert = '', bool $bThrowProvideException = false): ?Model\Account
protected function LoginProvide(string $sEmail, string $sLogin, string $sPassword, string $sSignMeToken = '', string $sClientCert = '', bool $bThrowProvideException = false): ?Model\Account
{
$oAccount = null;
if (0 < \strlen($sEmail) && 0 < \strlen($sLogin) && 0 < \strlen($sPassword)) {
@ -879,7 +879,7 @@ class Actions
if ($oDomain) {
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
$oAccount = new Model\Account($sEmail, $sLogin, $sPassword, $oDomain, $sSignMeToken, '', '', $sClientCert);
$this->Plugins()->RunHook('filter.acount', array($oAccount));
$this->Plugins()->RunHook('filter.account', array($oAccount));
if ($bThrowProvideException && !$oAccount) {
throw new Exceptions\ClientException(Notifications::AuthError);
@ -1419,7 +1419,7 @@ class Actions
{
$sInputEmail = $sEmail;
$this->Plugins()->RunHook('filter.login-credentials.step-1', array(&$sEmail, &$sPassword));
$this->Plugins()->RunHook('login.credentials.step-1', array(&$sEmail));
$sEmail = \MailSo\Base\Utils::Trim($sEmail);
if ($this->Config()->Get('login', 'login_lowercase', true)) {
@ -1486,7 +1486,7 @@ class Actions
}
}
$this->Plugins()->RunHook('filter.login-credentials.step-2', array(&$sEmail, &$sPassword));
$this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword));
if (false === \strpos($sEmail, '@') || 0 === \strlen($sPassword)) {
$this->loginErrorDelay();
@ -1501,12 +1501,10 @@ class Actions
$sLogin = \MailSo\Base\Utils::StrToLowerIfAscii($sLogin);
}
$this->Plugins()->RunHook('filter.login-credentials', array(&$sEmail, &$sLogin, &$sPassword));
$this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword));
$this->Logger()->AddSecret($sPassword);
$this->Plugins()->RunHook('event.login-pre-login-provide', array());
$oAccount = null;
$sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', ''));
try {
@ -1516,7 +1514,7 @@ class Actions
throw new Exceptions\ClientException(Notifications::AuthError);
}
$this->Plugins()->RunHook('event.login-post-login-provide', array($oAccount));
$this->Plugins()->RunHook('login.success', array($oAccount));
} catch (\Throwable $oException) {
$this->loginErrorDelay();
$this->LoggerAuthHelper($oAccount, $this->getAdditionalLogParamsByUserLogin($sInputEmail));

View file

@ -14,28 +14,22 @@ trait Folders
{
$oAccount = $this->initMailClientConnection();
$oFolderCollection = null;
$this->Plugins()->RunHook('filter.folders-before', array($oAccount, $oFolderCollection));
$HideUnsubscribed = $this->Config()->Get('labs', 'use_imap_list_subscribe', true);
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof \RainLoop\Settings) {
$HideUnsubscribed = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $HideUnsubscribed);
}
if (null === $oFolderCollection)
{
$oFolderCollection = $this->MailClient()->Folders('',
'*',
$HideUnsubscribed,
(int) $this->Config()->Get('labs', 'imap_folder_list_limit', 200)
);
}
$this->Plugins()->RunHook('filter.folders-post', array($oAccount, $oFolderCollection));
$oFolderCollection = $this->MailClient()->Folders('',
'*',
$HideUnsubscribed,
(int) $this->Config()->Get('labs', 'imap_folder_list_limit', 200)
);
if ($oFolderCollection instanceof \MailSo\Mail\FolderCollection)
{
$this->Plugins()->RunHook('filter.folders-post', array($oAccount, $oFolderCollection));
$aFolders = $oFolderCollection->getArrayCopy();
foreach ($aFolders as $i => $oFolder) {
if (!$oFolder->IsSelectable()) {
@ -158,9 +152,9 @@ trait Folders
{
$oFolderCollection->FoldersHash = \md5(\implode("\x0", $this->recFoldersNames($oFolderCollection)));
}
}
$this->Plugins()->RunHook('filter.folders-complete', array($oAccount, $oFolderCollection));
$this->Plugins()->RunHook('filter.folders-complete', array($oAccount, $oFolderCollection));
}
return $this->DefaultResponse(__FUNCTION__, $oFolderCollection);
}

View file

@ -4,7 +4,6 @@ namespace RainLoop\Enumerations;
class Capa
{
const PREM = 'PREM';
const OPEN_PGP = 'OPEN_PGP';
const PREFETCH = 'PREFETCH';
const THEMES = 'THEMES';

View file

@ -285,21 +285,18 @@ class Account
$oPlugins->RunHook('filter.imap-credentials', array($this, &$aImapCredentials));
$oPlugins->RunHook('event.imap-pre-connect', array($this, $aImapCredentials['UseConnect'], $aImapCredentials));
if ($aImapCredentials['UseConnect'])
{
$oPlugins->RunHook('imap.before-connect', array($this, $oMailClient, $aImapCredentials));
if ($aImapCredentials['UseConnect']) {
$oMailClient
->Connect($aImapCredentials['Host'], $aImapCredentials['Port'],
$aImapCredentials['Secure'], $aImapCredentials['VerifySsl'],
$aImapCredentials['AllowSelfSigned'], $aImapCredentials['ClientCert']);
}
$oPlugins->RunHook('imap.after-connect', array($this, $oMailClient, $aImapCredentials));
$oPlugins->RunHook('event.imap-pre-login', array($this, $aImapCredentials['UseAuth'], $aImapCredentials));
if ($aImapCredentials['UseAuth'])
{
$oPlugins->RunHook('imap.before-login', array($this, $oMailClient, $aImapCredentials));
if ($aImapCredentials['UseAuth']) {
if (0 < \strlen($aImapCredentials['ProxyAuthUser']) &&
0 < \strlen($aImapCredentials['ProxyAuthPassword']))
{
@ -315,8 +312,7 @@ class Account
$bLogin = true;
}
$oPlugins->RunHook('event.imap-post-login', array($this, $aImapCredentials['UseAuth'], $bLogin, $aImapCredentials));
$oPlugins->RunHook('imap.after-login', array($this, $oMailClient, $bLogin, $aImapCredentials));
return $bLogin;
}
@ -326,7 +322,7 @@ class Account
$bLogin = false;
$aSmtpCredentials = array(
'UseConnect' => true,
'UseConnect' => !$bUsePhpMail,
'UseAuth' => $this->DomainOutAuth(),
'UsePhpMail' => $bUsePhpMail,
'Ehlo' => \MailSo\Smtp\SmtpClient::EhloHelper(),
@ -343,23 +339,20 @@ class Account
'UseAuthCramMd5IfSupported' => !!$oConfig->Get('labs', 'smtp_use_auth_cram_md5', true)
);
$oPlugins->RunHook('filter.smtp-credentials', array($this, &$aSmtpCredentials));
$oPlugins->RunHook('smtp.credentials', array($this, &$aSmtpCredentials));
$bUsePhpMail = $aSmtpCredentials['UsePhpMail'];
$oPlugins->RunHook('event.smtp-pre-connect', array($this, $aSmtpCredentials['UseConnect'], $aSmtpCredentials));
if ($aSmtpCredentials['UseConnect'] && !$aSmtpCredentials['UsePhpMail'] && $oSmtpClient)
{
$oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, $aSmtpCredentials));
if ($aSmtpCredentials['UseConnect']) {
$oSmtpClient->Connect($aSmtpCredentials['Host'], $aSmtpCredentials['Port'],
$aSmtpCredentials['Secure'], $aSmtpCredentials['VerifySsl'], $aSmtpCredentials['AllowSelfSigned'],
'', $aSmtpCredentials['Ehlo']
);
}
$oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $aSmtpCredentials));
$oPlugins->RunHook('event.smtp-post-connect', array($this, $aSmtpCredentials['UseConnect'], $aSmtpCredentials));
$oPlugins->RunHook('event.smtp-pre-login', array($this, $aSmtpCredentials['UseAuth'], $aSmtpCredentials));
$oPlugins->RunHook('smtp.before-login', array($this, $oSmtpClient, $aSmtpCredentials));
if ($aSmtpCredentials['UseAuth'] && !$aSmtpCredentials['UsePhpMail'] && $oSmtpClient)
{
$oSmtpClient->Login($aSmtpCredentials['Login'], $aSmtpCredentials['Password'],
@ -367,8 +360,7 @@ class Account
$bLogin = true;
}
$oPlugins->RunHook('event.smtp-post-login', array($this, $aSmtpCredentials['UseAuth'], $bLogin, $aSmtpCredentials));
$oPlugins->RunHook('smtp.after-login', array($this, $oSmtpClient, $bLogin, $aSmtpCredentials));
return $bLogin;
}
@ -390,34 +382,24 @@ class Account
'InitialAuthPlain' => !!$oConfig->Get('ssl', 'sieve_auth_plain_initial', true)
);
$oPlugins->RunHook('filter.sieve-credentials', array($this, &$aSieveCredentials));
$oPlugins->RunHook('sieve.credentials', array($this, &$aSieveCredentials));
$oPlugins->RunHook('event.sieve-pre-connect', array($this, $aSieveCredentials['UseConnect'], $aSieveCredentials));
$oSieveClient->__USE_INITIAL_AUTH_PLAIN_COMMAND = $aSieveCredentials['InitialAuthPlain'];
if ($oSieveClient)
{
$oSieveClient->__USE_INITIAL_AUTH_PLAIN_COMMAND = $aSieveCredentials['InitialAuthPlain'];
if ($aSieveCredentials['UseConnect'])
{
$oSieveClient->Connect($aSieveCredentials['Host'], $aSieveCredentials['Port'],
$aSieveCredentials['Secure'], $aSieveCredentials['VerifySsl'], $aSieveCredentials['AllowSelfSigned']
);
}
$oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, $aSieveCredentials));
if ($aSieveCredentials['UseConnect']) {
$oSieveClient->Connect($aSieveCredentials['Host'], $aSieveCredentials['Port'],
$aSieveCredentials['Secure'], $aSieveCredentials['VerifySsl'], $aSieveCredentials['AllowSelfSigned']
);
}
$oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $aSieveCredentials));
$oPlugins->RunHook('event.sieve-post-connect', array($this, $aSieveCredentials['UseConnect'], $aSieveCredentials));
$oPlugins->RunHook('event.sieve-pre-login', array($this, $aSieveCredentials['UseAuth'], $aSieveCredentials));
if ($aSieveCredentials['UseAuth'])
{
$oPlugins->RunHook('event.sieve-pre-login', array($this, $oSieveClient, $aSieveCredentials));
if ($aSieveCredentials['UseAuth']) {
$oSieveClient->Login($aSieveCredentials['Login'], $aSieveCredentials['Password']);
$bLogin = true;
}
$oPlugins->RunHook('event.sieve-post-login', array($this, $aSieveCredentials['UseAuth'], $bLogin, $aSieveCredentials));
$oPlugins->RunHook('sieve.after-login', array($this, $oSieveClient, $bLogin, $aSieveCredentials));
return $bLogin;
}

View file

@ -57,8 +57,6 @@ class Manager
$oConfig->Save();
}
}
$this->RunHook('api.bootstrap.plugins');
}
}

View file

@ -0,0 +1,484 @@
<?php
namespace RainLoop\Providers\AddressBook;
use \SnappyMail\DAV\Client as DAVClient;
trait CardDAV
{
protected function prepareDavSyncData(DAVClient $oClient, string $sPath)
{
$mResult = false;
$aResponse = null;
try
{
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}getlastmodified',
'{DAV:}resourcetype',
'{DAV:}getetag'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
if (\is_array($aResponse))
{
$mResult = array();
foreach ($aResponse as $sKey => $aItem)
{
$sKey = \rtrim(\trim($sKey), '\\/');
if (!empty($sKey) && is_array($aItem))
{
$aItem = \array_change_key_case($aItem, \CASE_LOWER);
if (isset($aItem['{DAV:}getetag']))
{
$aMatch = array();
if (\preg_match('/\/([^\/?]+)$/', $sKey, $aMatch) && !empty($aMatch[1])
&& !static::hasDAVCollection($aItem))
{
$sVcfFileName = \urldecode(\urldecode($aMatch[1]));
$sKeyID = \preg_replace('/\.vcf$/i', '', $sVcfFileName);
$mResult[$sKeyID] = array(
'deleted' => false,
'uid' => $sKeyID,
'vcf' => $sVcfFileName,
'etag' => \trim(\trim($aItem['{DAV:}getetag']), '"\''),
'lastmodified' => '',
'changed' => 0
);
if (isset($aItem['{DAV:}getlastmodified']))
{
$mResult[$sKeyID]['lastmodified'] = $aItem['{DAV:}getlastmodified'];
$mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
$aItem['{DAV:}getlastmodified']);
}
else
{
$mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::TryToParseSpecEtagFormat($mResult[$sKeyID]['etag']);
$mResult[$sKeyID]['lastmodified'] = 0 < $mResult[$sKeyID]['changed'] ?
\gmdate('c', $mResult[$sKeyID]['changed']) : '';
}
$mResult[$sKeyID]['changed_'] = \gmdate('c', $mResult[$sKeyID]['changed']);
}
}
}
}
}
return $mResult;
}
protected function davClientRequest(DAVClient $oClient, string $sCmd, string $sUrl, $mData = null) : ?array
{
\MailSo\Base\Utils::ResetTimeLimit();
$this->oLogger->Write($sCmd.' '.$sUrl.(('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData ? ' ('.\strlen($mData).')' : ''),
\MailSo\Log\Enumerations\Type::INFO, 'DAV');
// if ('PUT' === $sCmd || 'POST' === $sCmd)
// {
// $this->oLogger->Write($mData, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
// }
$aResponse = null;
try
{
if (('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData)
{
$aResponse = $oClient->request($sCmd, $sUrl, $mData, array(
'Content-Type' => 'text/vcard; charset=utf-8'
));
}
else
{
$aResponse = $oClient->request($sCmd, $sUrl);
}
// if ('GET' === $sCmd)
// {
// $this->oLogger->WriteDump($aResponse, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
// }
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
return $aResponse;
}
private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array
{
$aResponse = null;
try
{
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}current-user-principal',
'{DAV:}resourcetype',
'{DAV:}displayname',
'{urn:ietf:params:xml:ns:carddav}addressbook-home-set'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
return $aResponse;
}
private function getContactsPaths(DAVClient $oClient, string $sUser, string $sPassword, string $sProxy = '') : array
{
$aContactsPaths = array();
$sCurrentUserPrincipal = '';
$sAddressbookHomeSet = '';
// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/
// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/
$aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav');
$sNextPath = '';
$sFirstNextPath = '';
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']))
{
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal'];
continue;
}
if (!empty($sKey))
{
if (empty($sFirstNextPath))
{
$sFirstNextPath = $sKey;
}
if (empty($sNextPath))
{
if (static::hasDAVCollection($aItem))
{
$sNextPath = $sKey;
continue;
}
}
}
}
if (empty($sNextPath) && empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet) && !empty($sFirstNextPath))
{
$sNextPath = $sFirstNextPath;
}
}
if (empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet))
{
if (empty($sNextPath))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sNextPath))
{
$oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sNextPath = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sNextPath);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']))
{
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal'];
continue;
}
}
}
}
}
if (empty($sAddressbookHomeSet))
{
if (empty($sCurrentUserPrincipal))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal))
{
$oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sCurrentUserPrincipal = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
}
}
}
}
if (empty($sAddressbookHomeSet))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet))
{
$oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sAddressbookHomeSet = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (!empty($sKey) && static::hasDAVCollection($aItem)
&& \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype']))
{
$aContactsPaths[$sKey] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : '';
}
}
}
}
return $aContactsPaths;
}
private function checkContactsPath(DAVClient $oClient, string $sPath) : bool
{
if (!$oClient)
{
return false;
}
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = null;
try
{
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}resourcetype'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
$bGood = false;
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (!empty($sKey) && static::hasDAVCollection($aItem)
&& \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype']))
{
$bGood = true;
}
}
}
if ($bGood)
{
$oClient->__UrlPath__ = $sPath;
}
return $bGood;
}
private function getDavClientFromUrl(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : DAVClient
{
if (!\preg_match('/^http[s]?:\/\//i', $sUrl))
{
$sUrl = \preg_replace('/^fruux\.com/i', 'dav.fruux.com', $sUrl);
$sUrl = \preg_replace('/^icloud\.com/i', 'contacts.icloud.com', $sUrl);
$sUrl = \preg_replace('/^gmail\.com/i', 'google.com', $sUrl);
$sUrl = 'https://'.$sUrl;
}
$aUrl = \parse_url($sUrl);
if (!\is_array($aUrl))
{
$aUrl = array();
}
$aUrl['scheme'] = $aUrl['scheme'] ?? 'http';
$aUrl['host'] = $aUrl['host'] ?? 'localhost';
$aUrl['port'] = $aUrl['port'] ?? 0;
$aUrl['path'] = isset($aUrl['path']) ? \rtrim($aUrl['path'], '\\/').'/' : '/';
$aSettings = array(
'baseUri' => $aUrl['scheme'].'://'.$aUrl['host'].($aUrl['port'] ? ':'.$aUrl['port'] : ''),
'userName' => $sUser,
'password' => $sPassword
);
$this->oLogger->AddSecret($sPassword);
if (!empty($sProxy))
{
$aSettings['proxy'] = $sProxy;
}
$oClient = new DAVClient($aSettings);
$oClient->setVerifyPeer(false);
$oClient->__UrlPath__ = $aUrl['path'];
$this->oLogger->Write('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
return $oClient;
}
protected function getDavClient(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : ?DAVClient
{
$aMatch = array();
$sUserAddressBookNameName = '';
if (\preg_match('/\|(.+)$/', $sUrl, $aMatch) && !empty($aMatch[1]))
{
$sUserAddressBookNameName = \trim($aMatch[1]);
$sUserAddressBookNameName = \MailSo\Base\Utils::StrToLowerIfAscii($sUserAddressBookNameName);
$sUrl = \preg_replace('/\|(.+)$/', '', $sUrl);
}
$oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy);
$sPath = $oClient->__UrlPath__;
$bGood = true;
if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath))
{
$sNewPath = '';
$aPaths = $this->getContactsPaths($oClient, $sUser, $sPassword, $sProxy);
$this->oLogger->WriteDump($aPaths);
if (\is_array($aPaths))
{
if (1 < \count($aPaths))
{
if ('' !== $sUserAddressBookNameName)
{
foreach ($aPaths as $sKey => $sValue)
{
$sValue = \MailSo\Base\Utils::StrToLowerIfAscii(\trim($sValue));
if ($sValue === $sUserAddressBookNameName)
{
$sNewPath = $sKey;
break;
}
}
}
if (empty($sNewPath))
{
foreach ($aPaths as $sKey => $sValue)
{
$sValue = \MailSo\Base\Utils::StrToLowerIfAscii($sValue);
if (\in_array($sValue, array('contacts', 'default', 'addressbook', 'address book')))
{
$sNewPath = $sKey;
break;
}
}
}
}
if (empty($sNewPath))
{
foreach ($aPaths as $sKey => $sValue)
{
$sNewPath = $sKey;
break;
}
}
}
$sPath = $sNewPath;
$bGood = $this->checkContactsPath($oClient, $sPath);
}
return $bGood ? $oClient : null;
}
private static function hasDAVCollection($aItem)
{
return !empty($aItem['{DAV:}resourcetype'])
&& \is_array($aItem['{DAV:}resourcetype'])
&& \in_array('{DAV:}collection', $aItem['{DAV:}resourcetype']);
}
}

View file

@ -3,12 +3,13 @@
namespace RainLoop\Providers\AddressBook;
use \RainLoop\Providers\AddressBook\Enumerations\PropertyType;
use \SnappyMail\DAV\Client as DAVClient;
class PdoAddressBook
extends \RainLoop\Common\PdoAbstract
implements \RainLoop\Providers\AddressBook\AddressBookInterface
{
use CardDAV;
/**
* @var string
*/
@ -105,490 +106,6 @@ class PdoAddressBook
return $aResult;
}
private function prepearRemoteSyncData(DAVClient $oClient, string $sPath)
{
$mResult = false;
$aResponse = null;
try
{
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}getlastmodified',
'{DAV:}resourcetype',
'{DAV:}getetag'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
if (\is_array($aResponse))
{
$mResult = array();
foreach ($aResponse as $sKey => $aItem)
{
$sKey = \rtrim(\trim($sKey), '\\/');
if (!empty($sKey) && is_array($aItem))
{
$aItem = \array_change_key_case($aItem, \CASE_LOWER);
if (isset($aItem['{dav:}getetag']))
{
$aMatch = array();
if (\preg_match('/\/([^\/?]+)$/', $sKey, $aMatch) && !empty($aMatch[1]) &&
(!$aItem['{dav:}resourcetype'] || !$aItem['{dav:}resourcetype']->is('{DAV:}collection')))
{
$sVcfFileName = \urldecode(\urldecode($aMatch[1]));
$sKeyID = \preg_replace('/\.vcf$/i', '', $sVcfFileName);
$mResult[$sKeyID] = array(
'deleted' => false,
'uid' => $sKeyID,
'vcf' => $sVcfFileName,
'etag' => \trim(\trim($aItem['{dav:}getetag']), '"\''),
'lastmodified' => '',
'changed' => 0
);
if (isset($aItem['{dav:}getlastmodified']))
{
$mResult[$sKeyID]['lastmodified'] = $aItem['{dav:}getlastmodified'];
$mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
$aItem['{dav:}getlastmodified']);
}
else
{
$mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::TryToParseSpecEtagFormat($mResult[$sKeyID]['etag']);
$mResult[$sKeyID]['lastmodified'] = 0 < $mResult[$sKeyID]['changed'] ?
\gmdate('c', $mResult[$sKeyID]['changed']) : '';
}
$mResult[$sKeyID]['changed_'] = \gmdate('c', $mResult[$sKeyID]['changed']);
}
}
}
}
}
return $mResult;
}
private function davClientRequest(DAVClient $oClient, string $sCmd, string $sUrl, $mData = null) : ?array
{
\MailSo\Base\Utils::ResetTimeLimit();
$this->oLogger->Write($sCmd.' '.$sUrl.(('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData ? ' ('.\strlen($mData).')' : ''),
\MailSo\Log\Enumerations\Type::INFO, 'DAV');
// if ('PUT' === $sCmd || 'POST' === $sCmd)
// {
// $this->oLogger->Write($mData, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
// }
$aResponse = null;
try
{
if (('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData)
{
$aResponse = $oClient->request($sCmd, $sUrl, $mData, array(
'Content-Type' => 'text/vcard; charset=utf-8'
));
}
else
{
$aResponse = $oClient->request($sCmd, $sUrl);
}
// if ('GET' === $sCmd)
// {
// $this->oLogger->WriteDump($aResponse, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
// }
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
return $aResponse;
}
private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array
{
$aResponse = null;
try
{
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}current-user-principal',
'{DAV:}resourcetype',
'{DAV:}displayname',
'{urn:ietf:params:xml:ns:carddav}addressbook-home-set'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
return $aResponse;
}
private function getContactsPaths(DAVClient $oClient, string $sUser, string $sPassword, string $sProxy = '') : array
{
$aContactsPaths = array();
$sCurrentUserPrincipal = '';
$sAddressbookHomeSet = '';
// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/
// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/
$aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav');
$sNextPath = '';
$sFirstNextPath = '';
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']))
{
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal'];
continue;
}
if (!empty($sKey))
{
if (empty($sFirstNextPath))
{
$sFirstNextPath = $sKey;
}
if (empty($sNextPath))
{
$oResourceType = isset($aItem['{DAV:}resourcetype']) ? $aItem['{DAV:}resourcetype'] : null;
/* @var $oResourceType \Sabre\DAV\Property\ResourceType */
if ($oResourceType && $oResourceType->is('{DAV:}collection'))
{
$sNextPath = $sKey;
continue;
}
}
}
}
if (empty($sNextPath) && empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet) && !empty($sFirstNextPath))
{
$sNextPath = $sFirstNextPath;
}
}
if (empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet))
{
if (empty($sNextPath))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sNextPath))
{
$oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sNextPath = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sNextPath);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']))
{
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal'];
continue;
}
}
}
}
}
if (empty($sAddressbookHomeSet))
{
if (empty($sCurrentUserPrincipal))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal))
{
$oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sCurrentUserPrincipal = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy'))
{
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'];
continue;
}
}
}
}
}
if (empty($sAddressbookHomeSet))
{
return $aContactsPaths;
}
else
{
if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet))
{
$oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy);
if ($oClient)
{
$sAddressbookHomeSet = $oClient->__UrlPath__;
}
else
{
return $aContactsPaths;
}
}
$aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet);
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (!empty($sKey) && $aItem && isset($aItem['{DAV:}resourcetype']))
{
$oResourceType = $aItem['{DAV:}resourcetype'];
/* @var $oResourceType \Sabre\DAV\Property\ResourceType */
if ($oResourceType && $oResourceType->is('{DAV:}collection'))
{
if ($oResourceType->is('{urn:ietf:params:xml:ns:carddav}addressbook'))
{
$aContactsPaths[$sKey] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : '';
}
}
}
}
}
}
return $aContactsPaths;
}
private function checkContactsPath(DAVClient $oClient, string $sPath) : bool
{
if (!$oClient)
{
return false;
}
$this->oLogger->Write('PROPFIND '.$sPath, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
$aResponse = null;
try
{
$aResponse = $oClient->propFind($sPath, array(
'{DAV:}resourcetype'
), 1);
// $this->oLogger->WriteDump($aResponse);
}
catch (\Throwable $oException)
{
$this->oLogger->WriteException($oException);
}
$bGood = false;
if (\is_array($aResponse))
{
foreach ($aResponse as $sKey => $aItem)
{
if (!empty($sKey) && isset($aItem['{DAV:}resourcetype']))
{
$oResourceType = $aItem['{DAV:}resourcetype'];
/* @var $oResourceType \Sabre\DAV\Property\ResourceType */
if ($oResourceType && $oResourceType->is('{DAV:}collection') &&
$oResourceType->is('{urn:ietf:params:xml:ns:carddav}addressbook'))
{
$bGood = true;
}
}
}
}
if ($bGood)
{
$oClient->__UrlPath__ = $sPath;
}
return $bGood;
}
public function getDavClientFromUrl(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : DAVClient
{
if (!\preg_match('/^http[s]?:\/\//i', $sUrl))
{
$sUrl = \preg_replace('/^fruux\.com/i', 'dav.fruux.com', $sUrl);
$sUrl = \preg_replace('/^icloud\.com/i', 'contacts.icloud.com', $sUrl);
$sUrl = \preg_replace('/^gmail\.com/i', 'google.com', $sUrl);
$sUrl = 'https://'.$sUrl;
}
$aUrl = \parse_url($sUrl);
if (!\is_array($aUrl))
{
$aUrl = array();
}
$aUrl['scheme'] = $aUrl['scheme'] ?? 'http';
$aUrl['host'] = $aUrl['host'] ?? 'localhost';
$aUrl['port'] = $aUrl['port'] ?? 0;
$aUrl['path'] = isset($aUrl['path']) ? \rtrim($aUrl['path'], '\\/').'/' : '/';
$aSettings = array(
'baseUri' => $aUrl['scheme'].'://'.$aUrl['host'].($aUrl['port'] ? ':'.$aUrl['port'] : ''),
'userName' => $sUser,
'password' => $sPassword
);
$this->oLogger->AddSecret($sPassword);
if (!empty($sProxy))
{
$aSettings['proxy'] = $sProxy;
}
$oClient = new DAVClient($aSettings);
$oClient->setVerifyPeer(false);
$oClient->__UrlPath__ = $aUrl['path'];
$this->oLogger->Write('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \MailSo\Log\Enumerations\Type::INFO, 'DAV');
return $oClient;
}
public function getDavClient(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : ?DAVClient
{
$aMatch = array();
$sUserAddressBookNameName = '';
if (\preg_match('/\|(.+)$/', $sUrl, $aMatch) && !empty($aMatch[1]))
{
$sUserAddressBookNameName = \trim($aMatch[1]);
$sUserAddressBookNameName = \MailSo\Base\Utils::StrToLowerIfAscii($sUserAddressBookNameName);
$sUrl = \preg_replace('/\|(.+)$/', '', $sUrl);
}
$oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy);
$sPath = $oClient->__UrlPath__;
$bGood = true;
if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath))
{
$sNewPath = '';
$aPaths = $this->getContactsPaths($oClient, $sUser, $sPassword, $sProxy);
$this->oLogger->WriteDump($aPaths);
if (\is_array($aPaths))
{
if (1 < \count($aPaths))
{
if ('' !== $sUserAddressBookNameName)
{
foreach ($aPaths as $sKey => $sValue)
{
$sValue = \MailSo\Base\Utils::StrToLowerIfAscii(\trim($sValue));
if ($sValue === $sUserAddressBookNameName)
{
$sNewPath = $sKey;
break;
}
}
}
if (empty($sNewPath))
{
foreach ($aPaths as $sKey => $sValue)
{
$sValue = \MailSo\Base\Utils::StrToLowerIfAscii($sValue);
if (\in_array($sValue, array('contacts', 'default', 'addressbook', 'address book')))
{
$sNewPath = $sKey;
break;
}
}
}
}
if (empty($sNewPath))
{
foreach ($aPaths as $sKey => $sValue)
{
$sNewPath = $sKey;
break;
}
}
}
$sPath = $sNewPath;
$bGood = $this->checkContactsPath($oClient, $sPath);
}
return $bGood ? $oClient : null;
}
public function Sync(string $sEmail, string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : bool
{
$this->SyncDatabase();
@ -607,7 +124,7 @@ class PdoAddressBook
$sPath = $oClient->__UrlPath__;
$aRemoteSyncData = $this->prepearRemoteSyncData($oClient, $sPath);
$aRemoteSyncData = $this->prepareDavSyncData($oClient, $sPath);
if (false === $aRemoteSyncData)
{
return false;

View file

@ -10,21 +10,7 @@ namespace SnappyMail\DAV;
class Client
{
/**
* The propertyMap is a key-value array.
*
* If you use the propertyMap, any {DAV:}multistatus responses with the
* proeprties listed in this array, will automatically be mapped to a
* respective class.
*
* The {DAV:}resourcetype property is automatically added. This maps to
* Sabre\DAV\Property\ResourceType
*
* @var array
*/
public $propertyMap = array(
// '{DAV:}resourcetype' => 'SnappyMail\\DAV\\Property\\ResourceType'
);
// public $__UrlPath__;
protected $baseUri;
@ -62,6 +48,40 @@ class Client
$this->HTTP->verify_peer = $value;
}
/**
* Performs an actual HTTP request, and returns the result.
*
* If the specified url is relative, it will be expanded based on the base url.
*/
public function request(string $method, string $url = '', string $body = null, array $headers = array()) : \SnappyMail\HTTP\Response
{
if (!\preg_match('@^(https?:)?//@', $url)) {
// If the url starts with a slash, we must calculate the url based off
// the root of the base url.
if (0 === \strpos($url, '/')) {
$parts = \parse_url($this->baseUri);
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
} else {
$url = $this->baseUri . $url;
}
}
$response = $this->HTTP->doRequest('PROPFIND', $url, $body, $headers);
if (301 == $response->status) {
// Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L]
$location = $response->getRedirectLocation();
\trigger_error("Redirect {$url} to {$location}");
$url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location);
$parts = \parse_url($this->baseUri);
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
$response = $this->HTTP->doRequest('PROPFIND', $url, $body, $headers);
}
if (300 <= $response->status) {
throw new \SnappyMail\HTTP\Exception("PROPFIND {$url}", $response->status, $response);
}
return $response;
}
/**
* Does a PROPFIND request
*
@ -98,35 +118,10 @@ class Client
$body .= ' </d:prop>' . "\n";
$body .= '</d:propfind>';
if (!\preg_match('@^(https?:)?//@', $url)) {
// If the url starts with a slash, we must calculate the url based off
// the root of the base url.
if (0 === \strpos($url, '/')) {
$parts = \parse_url($this->baseUri);
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
} else {
$url = $this->baseUri . $url;
}
}
$response = $this->HTTP->doRequest('PROPFIND', $url, $body, array(
$response = $this->request('PROPFIND', $url, $body, array(
"Depth: {$depth}",
'Content-Type: application/xml'
));
if (301 == $response->status) {
// Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L]
$location = $response->getRedirectLocation();
\trigger_error("Redirect {$url} to {$location}");
$url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location);
$parts = \parse_url($this->baseUri);
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
$response = $this->HTTP->doRequest('PROPFIND', $url, $body, array(
"Depth: {$depth}",
'Content-Type: application/xml'
));
}
if (300 <= $response->status) {
throw new \SnappyMail\HTTP\Exception("PROPFIND {$url}", $response->status, $response);
}
/**
* Parse the WebDAV multistatus response body
@ -148,26 +143,22 @@ class Client
throw new \InvalidArgumentException('The passed data is not valid XML');
}
$responseXML->registerXPathNamespace('d', 'urn:DAV');
$ns = \array_search('urn:DAV', $responseXML->getNamespaces(true));
$ns = $ns ? "{$ns}:" : '';
// $ns_card = \array_search('urn:ietf:params:xml:ns:carddav', $responseXML->getNamespaces(true));
$result = array();
foreach ($responseXML->xpath('d:response') as $response) {
$response->registerXPathNamespace('d', 'urn:DAV');
$href = $response->xpath('d:href');
foreach ($responseXML->xpath("{$ns}response") as $response) {
$href = $response->xpath("{$ns}href");
$href = (string) $href[0];
$properties = array();
foreach ($response->xpath('d:propstat') as $propStat) {
$propStat->registerXPathNamespace('d', 'urn:DAV');
$status = $propStat->xpath('d:status');
foreach ($response->xpath("{$ns}propstat") as $propStat) {
$status = $propStat->xpath("{$ns}status");
list($httpVersion, $statusCode, $message) = \explode(' ', (string)$status[0], 3);
// Only using the propertymap for results with status 200.
$propertyMap = $statusCode === '200' ? $this->propertyMap : array();
$properties[$statusCode] = static::parseProperties(\dom_import_simplexml($propStat), $propertyMap);
$properties[$statusCode] = static::parseProperties(\dom_import_simplexml($propStat));
}
$result[$href] = $properties;
@ -218,14 +209,10 @@ class Client
* If no value was given (self-closing element) null will be used as the
* value. This is used in for example PROPFIND requests.
*
* Complex values are supported through the propertyMap argument. The
* propertyMap should have the clark-notation properties as it's keys, and
* classnames as values.
*
* When any of these properties are found, the fromDOMElement() method will be
* (statically) called. The result of this method is used as the value.
*/
protected static function parseProperties(\DOMElement $parentNode, array $propertyMap = array()) : array
protected static function parseProperties(\DOMElement $parentNode) : array
{
$propList = array();
foreach ($parentNode->childNodes as $propNode) {
@ -234,8 +221,11 @@ class Client
/* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
if (XML_ELEMENT_NODE == $propNodeData->nodeType) {
$propertyName = self::toClarkNotation($propNodeData);
if (isset($propertyMap[$propertyName])) {
$propList[$propertyName] = \call_user_func(array($propertyMap[$propertyName], 'fromDOMElement'), $propNodeData);
if ('{DAV:}resourcetype' === $propertyName) {
$propList[$propertyName] = [];
foreach ($propNodeData->childNodes as $resourcetype) {
$propList[$propertyName][] = self::toClarkNotation($resourcetype);
}
} else {
$propList[$propertyName] = $propNodeData->textContent;
}
@ -245,5 +235,4 @@ class Client
}
return $propList;
}
}

View file

@ -10,8 +10,8 @@
<div class="input-append">
<input required="" type="text" class="input-block-level inputLogin"
autofocus="" autocomplete="username" autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]LOGIN/LABEL_LOGIN"
data-bind="textInput: login, disable: submitRequest" />
data-bind="textInput: login, disable: submitRequest"
data-i18n="[placeholder]LOGIN/LABEL_LOGIN" />
<span class="add-on">
<i class="fontastic">👤</i>
</span>
@ -21,19 +21,20 @@
<div class="input-append">
<input required="" type="password" class="input-block-level inputPassword"
autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]LOGIN/LABEL_PASSWORD"
data-bind="textInput: password, disable: submitRequest" />
<span class="add-on" tabindex="-1" data-bind="command: submitCommand" data-i18n="[title]LOGIN/BUTTON_LOGIN">
data-bind="textInput: password, disable: submitRequest"
data-i18n="[placeholder]LOGIN/LABEL_PASSWORD" />
<span class="add-on" tabindex="-1"
data-bind="command: submitCommand" data-i18n="[title]LOGIN/BUTTON_LOGIN">
<i class="fontastic" data-bind="visible: '' === password()">🔑</i>
<button type="submit" class="btn-submit-icon-wrp login-submit-icon fontastic" data-bind="visible: '' !== password()"></button>
</span>
</div>
</div>
<div id="plugin-Login-BottomControlGroup"></div>
<div class="controls" data-bind="hidden: hideSubmitButton">
<button type="submit" class="btn btn-large btn-block buttonLogin"
data-bind="command: submitCommand"
data-i18n="LOGIN/BUTTON_LOGIN">
</button>
data-i18n="LOGIN/BUTTON_LOGIN"></button>
</div>
</form>
</div>

View file

@ -1,6 +1,6 @@
<span class="e-component e-checkbox inline" tabindex="0" data-bind="click: click, onSpace: click, css: { 'disabled': disable() || !enable() }">
<i role="checkbox" class="e-checkbox-icon fontastic" data-bind="text: value() ? '☑' : '☐'"></i>
<!-- ko if: labeled -->
<span class="sub-label" data-bind="attr: {'data-i18n': label}, css: {'i18n-animation': labelAnimated}"></span>
<span class="sub-label" data-bind="attr: {'data-i18n': label}"></span>
<!-- /ko -->
</span>

View file

@ -5,6 +5,6 @@
'box': animationBox, 'checkmark': animationCheckmark}"></div>
</div>
<!-- ko if: labeled -->
<span class="sub-label" data-bind="attr: {'data-i18n': label}, css: {'i18n-animation': labelAnimated}"></span>
<span class="sub-label" data-bind="attr: {'data-i18n': label}"></span>
<!-- /ko -->
</span>

View file

@ -6,18 +6,14 @@
<div class="alert" data-bind="hidden: !submitError()" hidden="">
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
<span data-bind="text: submitError"></span>
<div data-bind="visible: '' !== submitErrorAddidional()">
<br />
<span data-bind="text: submitErrorAddidional"></span>
</div>
<p data-bind="visible: '' !== submitErrorAddidional(), text: submitErrorAddidional"></p>
</div>
<div class="wrapper-parent">
<form class="wrapper submitting-pane loginForm" action="#/"
data-bind="submit: submitForm, css: {'errorAnimated': formError, 'submitting': submitRequest()}">
<div class="controls" data-bind="css: {'error': emailError}">
<div class="input-append">
<input required="" type="text" class="input-block-level inputEmail"
pattern="[^@\s]+(@[^\s]+)?" inputmode="email"
<input required="" type="text" class="input-block-level inputEmail" pattern="[^@\s]+(@[^\s]+)?" inputmode="email"
autofocus="" autocomplete="email" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: email, disable: submitRequest"
data-i18n="[placeholder]GLOBAL/EMAIL" />
@ -41,9 +37,9 @@
</div>
<div id="plugin-Login-BottomControlGroup"></div>
<div class="controls" data-bind="hidden: hideSubmitButton">
<button type="submit" class="btn btn-large btn-block buttonLogin" data-bind="command: submitCommand">
<span class="i18n-animation" data-i18n="LOGIN/BUTTON_SIGN_IN"></span>
</button>
<button type="submit" class="btn btn-large btn-block buttonLogin"
data-bind="command: submitCommand"
data-i18n="LOGIN/BUTTON_SIGN_IN"></button>
</div>
<div class="controls clearfix" style="margin-bottom: 10px">
<div class="pull-right language-buttons">
@ -57,7 +53,6 @@
name: 'CheckboxSimple',
params: {
label: 'LOGIN/LABEL_SIGN_ME',
labelAnimated: true,
value: signMe
}
}"></div>

View file

@ -278,15 +278,13 @@
<div class="attachmentIconParent" data-bind="css: { 'hasPreview': hasPreview(), 'hasPreplay': hasPreplay(), 'isImage': isImage() }">
<div class="hidePreview">
<div class="iconMain">
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText()"></i>
<i class="attachmentIcon" data-bind="css: iconClass()"></i>
</div>
</div>
<div class="showPreview">
<a data-bind="click: $root.attachmentPreview, css: {'attachmentImagePreview': isImage(), 'visible': !isLinked}, attr: { 'title': fileName, 'href': linkPreviewMain() }" target="_blank">
<div class="iconMain">
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText()"></i>
<i class="attachmentIcon" data-bind="css: iconClass()"></i>
</div>
<div class="iconBG" data-bind="attr: { 'style': linkThumbnailPreviewStyle() }"></div>
<div class="iconPreview attachmentIcon fontastic show-hover">👁</div>
@ -294,7 +292,7 @@
</div>
<div class="showPreplay">
<div class="iconMain">
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
<i class="attachmentIcon" data-bind="css: iconClass()"></i>
</div>
<div class="iconPreview attachmentIcon fontastic show-hover"></div>
</div>
@ -316,8 +314,8 @@
<span data-bind="visible: downloadAsZipAllowed">
<i class="fontastic iconcolor-red" data-bind="visible: downloadAsZipError"></i>
<i class="icon-file-zip" data-bind="visible: !downloadAsZipError(),
css: {'icon-file-zip': !downloadAsZipLoading(), 'icon-spinner': downloadAsZipLoading()}"></i>
<i class="icon-file-archive" data-bind="visible: !downloadAsZipError(),
css: {'icon-file-archive': !downloadAsZipLoading(), 'icon-spinner': downloadAsZipLoading()}"></i>
<span class="g-ui-link" data-bind="click: downloadAsZip"
data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
</span>

View file

@ -171,8 +171,7 @@
<li class="attachmentItem" data-bind="attr: { 'title': title }, css: { 'waiting': waiting, 'error': '' !== error() }">
<div class="attachmentIconParent">
<div class="iconMain">
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass(), visible: !uploading() || 0 === progress()"></i>
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText(), visible: !uploading() || 0 === progress()"></i>
<i class="attachmentIcon" data-bind="css: iconClass(), visible: !uploading() || 0 === progress()"></i>
</div>
<div class="iconProgress" data-bind="attr: { 'style': progressStyle }, visible: uploading"></div>
<div class="iconBG" data-bind="text: progressText, visible: uploading"></div>

View file

@ -1,8 +1,6 @@
<div class="b-settings-general g-ui-user-select-none">
<div class="form-horizontal">
<div class="legend">
<span class="i18n-animation" data-i18n="SETTINGS_GENERAL/LEGEND_GENERAL"></span>
</div>
<div class="legend" data-i18n="SETTINGS_GENERAL/LEGEND_GENERAL"></div>
<div class="control-group">
<label class="control-label" data-i18n="SETTINGS_GENERAL/LABEL_IDENTITY"></label>
<div class="controls">
@ -108,9 +106,7 @@
</div>
</div>
<div class="form-horizontal">
<div class="legend">
<span class="i18n-animation" data-i18n="SETTINGS_GENERAL/LABEL_NOTIFICATIONS"></span>
</div>
<div class="legend" data-i18n="SETTINGS_GENERAL/LABEL_NOTIFICATIONS"></div>
<div class="control-group">
<div class="controls">
<div>

View file

@ -9,7 +9,7 @@
</div>
<a class="btn button-back" data-bind="click: backToMailBoxClick">
<i class="fontastic"></i>
<span class="i18n-animation" data-i18n="GLOBAL/BACK"></span>
<span data-i18n="GLOBAL/BACK"></span>
</a>
</div>
</div>

View file

@ -55,13 +55,13 @@
.icon-file-code::before {
content: "\e030";
}
.icon-file-chart-graph::before {
.icon-file-presentation::before {
content: "\e031"; /* 📊 */
}
.icon-file-zip::before {
.icon-file-archive::before {
content: "\e032";
}
.icon-file-music::before {
.icon-file-audio::before {
content: "\e033";
}
.icon-file-text::before {
@ -76,10 +76,10 @@
.icon-attachment::before {
content: "📎";
}
.icon-file-excel::before {
.icon-file-spreadsheet::before {
content: "\e03a";
}
.icon-file-movie::before {
.icon-file-video::before {
content: "\e03b";
}
.icon-folder-add::before {