diff --git a/.docker/release/files/usr/local/include/application.ini b/.docker/release/files/usr/local/include/application.ini
index 6d8745dce..9f0a6b09e 100644
--- a/.docker/release/files/usr/local/include/application.ini
+++ b/.docker/release/files/usr/local/include/application.ini
@@ -259,8 +259,6 @@ use_imap_force_selection = Off
use_imap_thread = On
use_imap_move = Off
use_imap_expunge_all_on_delete = Off
-imap_forwarded_flag = "$Forwarded"
-imap_read_receipt_flag = "$ReadReceipt"
imap_body_text_limit = 555000
imap_message_list_fast_simple_search = On
imap_message_list_count_limit_trigger = 0
@@ -307,5 +305,5 @@ dev_email = ""
dev_password = ""
[version]
-current = "2.16.1"
+current = "2.16.3"
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
diff --git a/README.md b/README.md
index 376cc8db7..75341a93d 100644
--- a/README.md
+++ b/README.md
@@ -140,26 +140,26 @@ RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
-|admin.js |2.158.025 | 80.335 |
-|app.js |4.215.733 | 405.051 |
+|admin.js |2.158.025 | 80.521 |
+|app.js |4.215.733 | 405.835 |
|boot.js | 672.433 | 2.050 |
|libs.js | 647.679 | 197.160 |
-|sieve.js | 0 | 86.504 |
+|sieve.js | 0 | 86.362 |
|polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 |
-|TOTAL |8.019.778 | 771.385 |
+|TOTAL |8.019.778 | 772.213 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
-|admin.min.js | 255.514 | 40.186 | 73.899 | 13.359 | 60.674 | 11.966 |
-|app.min.js | 516.000 | 191.792 |140.430 | 61.896 |110.657 | 53.154 |
+|admin.min.js | 255.514 | 40.289 | 73.899 | 13.383 | 60.674 | 11.971 |
+|app.min.js | 516.000 | 192.189 |140.430 | 62.006 |110.657 | 53.281 |
|boot.min.js | 66.456 | 1.252 | 22.553 | 778 | 20.043 | 628 |
|libs.min.js | 574.626 | 95.110 |177.280 | 35.149 |151.855 | 31.461 |
-|sieve.min.js | 0 | 42.147 | 0 | 10.551 | 0 | 9.513 |
+|sieve.min.js | 0 | 42.091 | 0 | 10.543 | 0 | 9.511 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
-|TOTAL user |1.189.690 | 288.154 |351.061 | 97.823 |292.627 | 85.243 |
-|TOTAL user+sieve |1.189.690 | 330.301 |351.061 |108.374 |292.627 | 94.756 |
-|TOTAL admin | 929.204 | 136.548 |285.047 | 49.286 |242.644 | 44.055 |
+|TOTAL user |1.189.690 | 288.551 |351.061 | 97.933 |292.627 | 85.370 |
+|TOTAL user+sieve |1.189.690 | 330.642 |351.061 |108.476 |292.627 | 94.881 |
+|TOTAL admin | 929.204 | 136.651 |285.047 | 49.310 |242.644 | 44.060 |
For a user its around 70% smaller and faster than traditional RainLoop.
@@ -187,8 +187,8 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
-|app.css | 340.334 | 80.643 | 46.959 | 16.709 | 14.386 |
-|app.min.css | 274.791 | 64.822 | 39.618 | 14.793 | 13.044 |
+|app.css | 340.334 | 80.979 | 46.959 | 16.774 | 14.439 |
+|app.min.css | 274.791 | 65.092 | 39.618 | 14.841 | 13.085 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 29.828 | | 6.777 | 5.875 |
diff --git a/dev/Model/Message.js b/dev/Model/Message.js
index 276adf7a8..ab46130ac 100644
--- a/dev/Model/Message.js
+++ b/dev/Model/Message.js
@@ -97,11 +97,8 @@ export class MessageModel extends AbstractModel {
isImportant: () => MessagePriority.High === this.priority(),
hasAttachments: () => this.attachments().hasVisible(),
- isDeleted: () => this.flags().includes('\\deleted'),
- isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
+ isUnseen: () => !this.flags().includes('\\seen'),
isFlagged: () => this.flags().includes('\\flagged'),
- isAnswered: () => this.flags().includes('\\answered'),
- isForwarded: () => this.flags().includes('$forwarded'),
isReadReceipt: () => this.flags().includes('$mdnsent')
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing')
@@ -291,13 +288,9 @@ export class MessageModel extends AbstractModel {
let classes = [];
forEachObjectEntry({
deleted: this.deleted(),
- 'deleted-mark': this.isDeleted(),
selected: this.selected(),
checked: this.checked(),
- flagged: this.isFlagged(),
unseen: this.isUnseen(),
- answered: this.isAnswered(),
- forwarded: this.isForwarded(),
focused: this.focused(),
important: this.isImportant(),
withAttachments: !!this.attachments().length,
@@ -306,9 +299,18 @@ export class MessageModel extends AbstractModel {
hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
}, (key, value) => value && classes.push(key));
+ this.flags().forEach(value => classes.push('flag-'+value));
return classes.join(' ');
}
+ /**
+ * @return array
+ * https://datatracker.ietf.org/doc/html/rfc5788
+ */
+ keywords() {
+ return this.flags().filter(value => '\\' !== value[0]);
+ }
+
/**
* @returns {string}
*/
@@ -567,19 +569,4 @@ export class MessageModel extends AbstractModel {
return result.html || plainToHtml(this.plain());
}
- /**
- * @returns {string}
- */
- flagHash() {
- return [
- this.deleted(),
- this.isDeleted(),
- this.isUnseen(),
- this.isFlagged(),
- this.isAnswered(),
- this.isForwarded(),
- this.isReadReceipt()
- ].join(',');
- }
-
}
diff --git a/dev/Stores/User/Messagelist.js b/dev/Stores/User/Messagelist.js
index d24848ade..1b3750664 100644
--- a/dev/Stores/User/Messagelist.js
+++ b/dev/Stores/User/Messagelist.js
@@ -332,11 +332,7 @@ MessagelistUserStore.removeMessagesFromList = (
? messageList.filter(item => item && uidForRemove.includes(pInt(item.uid)))
: [];
- messages.forEach(item => {
- if (item && item.isUnseen()) {
- ++unseenCount;
- }
- });
+ messages.forEach(item => item && item.isUnseen() && ++unseenCount);
if (fromFolder && !copy) {
fromFolder.messageCountAll(
diff --git a/dev/Styles/User/Attachments.less b/dev/Styles/User/Attachments.less
index 4ef482afd..93f73c80d 100644
--- a/dev/Styles/User/Attachments.less
+++ b/dev/Styles/User/Attachments.less
@@ -19,10 +19,6 @@
opacity: 0.6;
}
- &.checked {
- box-shadow: 0 0 0 1px rgba(0, 0, 255, 0.1), 0 1px 5px rgba(0, 0, 255, 0.2);
- }
-
.checkboxAttachment {
bottom: 6px;
cursor: pointer;
diff --git a/dev/Styles/User/MessageList.less b/dev/Styles/User/MessageList.less
index 81b4414e7..791f1904d 100644
--- a/dev/Styles/User/MessageList.less
+++ b/dev/Styles/User/MessageList.less
@@ -216,7 +216,7 @@ html:not(rl-mobile) {
margin-right:5px
}
- &.deleted-mark {
+ &.flag-\\deleted {
opacity: .7;
.subjectParent {
text-decoration: line-through;
@@ -228,7 +228,7 @@ html:not(rl-mobile) {
}
.checkboxMessage {
- padding: 0 6px;
+ margin: 0 6px;
font-size: 16px;
}
@@ -242,8 +242,6 @@ html:not(rl-mobile) {
.attachmentParent {
position: relative;
margin: 2px 10px 0 5px;
- color: #666;
- text-shadow: 0 1px 0 #eee;
}
.senderParent, .subjectParent {
@@ -340,15 +338,15 @@ html:not(rl-mobile) {
.flagParent {
padding: 0 10px 0 5px;
}
- &.flagged .flagParent::after,
+ &.flag-\\flagged .flagParent::after,
&.hasFlaggedSubMessage .flagParent::after {
color: orange;
content: '★'; /*⚑*/
}
- &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent::after {
+ &:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent::after {
content: '☆'; /*⚐*/
}
- &:not(.flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
+ &:not(.flag-\\flagged):not(.hasFlaggedSubMessage) .flagParent:not(:hover) {
opacity: 0.5;
}
}
@@ -392,7 +390,7 @@ html.rl-ctrl-key-pressed .messageListItem {
.checkboxMessage {
line-height: 12px;
- margin: 10px 0 -5px;
+ margin: 10px 6px -5px;
}
.subjectParent {
@@ -444,3 +442,35 @@ html:not(.rl-mobile):not(.rl-side-preview-pane) {
}
}
}
+
+.senderParent::before {
+ font-family: snappymail;
+}
+.flag-\\answered .senderParent::before {
+ content: '← ';
+}
+.flag-\$forwarded .senderParent::before {
+ content: '→ ';
+}
+.flag-\\answered.flag-\$forwarded .senderParent::before {
+ content: '←→ ';
+}
+
+/* Thunderbird labels */
+/*
+.flag-\$label5 .checkboxMessage { background-color: #808; }
+.flag-\$label4 .checkboxMessage { background-color: #00F; }
+.flag-\$label3 .checkboxMessage { background-color: #080; }
+.flag-\$label2 .checkboxMessage { background-color: #FA0; }
+.flag-\$label1 .checkboxMessage { background-color: #F00; }
+*/
+.messageListItem.flag-\$label5.focused { background-color: rgba(255, 0, 255, 0.30); }
+.messageListItem.flag-\$label4.focused { background-color: rgba( 64, 64, 255, 0.30); }
+.messageListItem.flag-\$label3.focused { background-color: rgba( 0, 255, 0, 0.30); }
+.messageListItem.flag-\$label2.focused { background-color: rgba(255, 170, 0, 0.30); }
+.messageListItem.flag-\$label1.focused { background-color: rgba(255, 0, 0, 0.30); }
+.messageListItem.flag-\$label5:not(.focused) { color: #939; }
+.messageListItem.flag-\$label4:not(.focused) { color: #33F; }
+.messageListItem.flag-\$label3:not(.focused) { color: #090; }
+.messageListItem.flag-\$label2:not(.focused) { color: #F90; }
+.messageListItem.flag-\$label1:not(.focused) { color: #F00; }
diff --git a/dev/Styles/User/MessageView.less b/dev/Styles/User/MessageView.less
index c23deddeb..b39b406ff 100644
--- a/dev/Styles/User/MessageView.less
+++ b/dev/Styles/User/MessageView.less
@@ -42,13 +42,10 @@ html.rl-no-preview-pane {
line-height: 70px;
padding-top: 100px;
color: #999;
-
- .icon-mail {
- font-size: 100px;
- font-size: 50px;
- line-height: 90px;
- padding-left: 10px;
- }
+ }
+ .b-message-view-checked-helper::after {
+ content: ' ✉';
+ font-family: snappymail;
}
.b-message-view-desc {
@@ -303,14 +300,6 @@ html.rl-no-preview-pane {
}
}
- &.unselectedAttachmentsError {
- .attachmentItem {
- box-shadow: 0 1px 4px red;
- box-shadow: 0 1px 5px rgba(255, 0, 0, 0.4);
- box-shadow: 0 0 0 1px rgba(255, 0, 0, 0.2), 0 1px 5px rgba(255, 0, 0, 0.3);
- }
- }
-
.controls-handle {
position: absolute;
bottom: 5px;
diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js
index 8d67433e0..cc366ab4d 100644
--- a/dev/View/Popup/Compose.js
+++ b/dev/View/Popup/Compose.js
@@ -20,7 +20,7 @@ import { messagesDeleteHelper } from 'Common/Folders';
import { serverRequest } from 'Common/Links';
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
-import { Settings, SettingsGet, elementById, addShortcut } from 'Common/Globals';
+import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut } from 'Common/Globals';
//import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
import { AppUserStore } from 'Stores/User/App';
@@ -236,6 +236,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.sLastFocusedField = 'to';
this.allowContacts = AppUserStore.allowContacts();
+ this.allowIdentities = SettingsCapa('Identities');
this.bSkipNextHide = false;
diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js
index 4b5f6595a..b49fd1dc9 100644
--- a/dev/View/User/MailBox/MessageList.js
+++ b/dev/View/User/MailBox/MessageList.js
@@ -571,21 +571,12 @@ export class MailMessageList extends AbstractViewRight {
seenMessagesFast(seen) {
const checked = MessagelistUserStore.listCheckedOrSelected();
- if (checked.length) {
- if (undefined === seen) {
- const unseen = checked.filter(message => message.isUnseen());
- listAction(
- checked[0].folder,
- unseen.length ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
- checked
- );
- } else {
- listAction(
- checked[0].folder,
- seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
- checked
- );
- }
+ if (checked.length && null != seen) {
+ listAction(
+ checked[0].folder,
+ seen ? MessageSetAction.SetSeen : MessageSetAction.UnsetSeen,
+ checked
+ );
}
}
diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js
index d66b06254..75768be08 100644
--- a/dev/View/User/MailBox/MessageView.js
+++ b/dev/View/User/MailBox/MessageView.js
@@ -123,9 +123,6 @@ export class MailMessageView extends AbstractViewRight {
this.fullScreenMode = isFullscreen;
this.toggleFullScreen = toggleFullscreen;
- this.messageListOfThreadsLoading = ko.observable(false).extend({ rateLimit: 1 });
- this.highlightUnselectedAttachments = ko.observable(false).extend({ falseTimeout: 2000 });
-
this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
@@ -136,7 +133,10 @@ export class MailMessageView extends AbstractViewRight {
this.addComputables({
allowAttachmentControls: () => arrayLength(attachmentsActions) && SettingsCapa('AttachmentsActions'),
- downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') && this.allowAttachmentControls(),
+ downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
+ && (currentMessage() ? currentMessage().attachments : [])
+ .filter(item => item && !item.isLinked() && item.checked() && item.download)
+ .length,
lastReplyAction: {
read: this.lastReplyAction_,
@@ -538,8 +538,6 @@ export class MailMessageView extends AbstractViewRight {
}
})
.catch(() => this.downloadAsZipError(true));
- } else {
- this.highlightUnselectedAttachments(true);
}
}
@@ -566,20 +564,20 @@ export class MailMessageView extends AbstractViewRight {
readReceipt() {
let oMessage = currentMessage()
if (oMessage.readReceipt()) {
- Remote.request('SendReadReceiptMessage', null, {
+ Remote.request('SendReadReceiptMessage', iError => {
+ if (!iError) {
+ oMessage.flags.push('$mdnsent');
+// oMessage.flags.valueHasMutated();
+ MessageFlagsCache.store(oMessage);
+ MessagelistUserStore.reloadFlagsAndCachedMessage();
+ }
+ }, {
MessageFolder: oMessage.folder,
MessageUid: oMessage.uid,
ReadReceipt: oMessage.readReceipt(),
Subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
Text: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
});
-
- oMessage.flags.push('$mdnsent');
-// oMessage.flags.valueHasMutated();
-
- MessageFlagsCache.store(oMessage);
-
- MessagelistUserStore.reloadFlagsAndCachedMessage();
}
}
diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md
index f496250f0..f59fbbfb9 100644
--- a/integrations/cloudron/DESCRIPTION.md
+++ b/integrations/cloudron/DESCRIPTION.md
@@ -1,4 +1,4 @@
-This app packages SnappyMail 2.16.1.
+This app packages SnappyMail 2.16.3.
SnappyMail is a simple, modern, lightweight & fast web-based email client.
diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile
index 93b3dacde..0e65debbf 100644
--- a/integrations/cloudron/Dockerfile
+++ b/integrations/cloudron/Dockerfile
@@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
-VERSION=2.16.1
+VERSION=2.16.3
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \
diff --git a/integrations/nextcloud/snappymail/VERSION b/integrations/nextcloud/snappymail/VERSION
index 15c5c9d5b..38275d2df 100755
--- a/integrations/nextcloud/snappymail/VERSION
+++ b/integrations/nextcloud/snappymail/VERSION
@@ -1 +1 @@
-2.16.1
\ No newline at end of file
+2.16.3
\ No newline at end of file
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
index a781d80fd..b6369e00a 100644
--- a/integrations/nextcloud/snappymail/appinfo/info.xml
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -4,7 +4,7 @@
SnappyMail
SnappyMail Webmail
Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.6.0 (2021-07).
- 2.16.1
+ 2.16.3
agpl
SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli
SnappyMail
diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl
index 3987b5f50..be2cf24bd 100644
--- a/integrations/virtualmin/snappymail.pl
+++ b/integrations/virtualmin/snappymail.pl
@@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions()
sub script_snappymail_versions
{
-return ( "2.16.1" );
+return ( "2.16.3" );
}
sub script_snappymail_version_desc
diff --git a/package.json b/package.json
index 4bc01b4b0..2ea4e0581 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
- "version": "2.16.1",
+ "version": "2.16.3",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",
diff --git a/plugins/change-password-froxlor/driver.php b/plugins/change-password-froxlor/driver.php
new file mode 100644
index 000000000..0e26d32c3
--- /dev/null
+++ b/plugins/change-password-froxlor/driver.php
@@ -0,0 +1,107 @@
+oConfig = $oConfig;
+ $this->oLogger = $oLogger;
+ }
+
+ public static function isSupported() : bool
+ {
+ return \class_exists('PDO', false)
+ // The PHP extension PDO (mysql) must be installed to use this plugin
+ && \in_array('mysql', \PDO::getAvailableDrivers());
+ }
+
+ public static function configMapping() : array
+ {
+ return array(
+ \RainLoop\Plugins\Property::NewInstance('froxlor_dsn')->SetLabel('Froxlor PDO dsn')
+ ->SetDefaultValue('mysql:host=localhost;dbname=froxlor;charset=utf8'),
+ \RainLoop\Plugins\Property::NewInstance('froxlor_user')->SetLabel('User'),
+ \RainLoop\Plugins\Property::NewInstance('froxlor_password')->SetLabel('Password')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD),
+ \RainLoop\Plugins\Property::NewInstance('froxlor_allowed_emails')->SetLabel('Allowed emails')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
+ ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
+ ->SetDefaultValue('*')
+ );
+ }
+
+ public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
+ {
+ if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) {
+ return false;
+ }
+
+ try
+ {
+ if ($this->oLogger) {
+ $this->oLogger->Write('froxlor: Try to change password for '.$oAccount->Email());
+ }
+
+ $oPdo = new \PDO(
+ $this->oConfig->Get('plugin', 'froxlor_dsn', ''),
+ $this->oConfig->Get('plugin', 'froxlor_user', ''),
+ $this->oConfig->Get('plugin', 'froxlor_password', ''),
+ array(
+ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
+ )
+ );
+
+ $oStmt = $oPdo->prepare('SELECT password_enc, id FROM mail_users WHERE username = ? LIMIT 1');
+
+ if ($oStmt->execute(array($oAccount->IncLogin()))) {
+ $aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC);
+ if (!empty($aFetchResult['id'])) {
+ $sDbPassword = $aFetchResult['password_enc'];
+ $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
+ if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
+
+ $oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?');
+
+ return !!$oStmt->execute(array(
+ $this->cryptPassword($sNewPassword),
+ $aFetchResult['id']
+ ));
+ }
+ }
+ }
+ }
+ catch (\Exception $oException)
+ {
+ if ($this->oLogger) {
+ $this->oLogger->WriteException($oException);
+ }
+ }
+ return false;
+ }
+
+ private function cryptPassword(string $sPassword) : string
+ {
+ if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
+ $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
+ } elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) {
+ $sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
+ } else {
+ $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
+ }
+ return \crypt($sPassword, $sSalt);
+ }
+}
diff --git a/plugins/change-password-froxlor/index.php b/plugins/change-password-froxlor/index.php
new file mode 100644
index 000000000..9e0bbd160
--- /dev/null
+++ b/plugins/change-password-froxlor/index.php
@@ -0,0 +1,20 @@
+addHook('smtp.credentials', 'FilterSmtpCredentials');
+ $this->addHook('smtp.before-connect', 'FilterSmtpCredentials');
}
/**
* @param \RainLoop\Model\Account $oAccount
* @param array $aSmtpCredentials
*/
- public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials)
+ public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount,
+ \MailSo\Smtp\SmtpClient $oSmtpClient,
+ array &$aSmtpCredentials)
{
- if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials))
- {
- // Default:
- // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper();
- //
- // or write your custom php
- $aSmtpCredentials['Ehlo'] = 'localhost';
- }
+ // Default:
+ // $aSmtpCredentials['Ehlo'] = \MailSo\Smtp\SmtpClient::EhloHelper();
+ //
+ // or write your custom php
+ $aSmtpCredentials['Ehlo'] = 'localhost';
}
}
diff --git a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php b/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php
deleted file mode 100644
index c87ed2f61..000000000
--- a/plugins/contacts-suggestions-example/ContactsExampleSuggestions.php
+++ /dev/null
@@ -1,21 +0,0 @@
-Email(), ''),
- array('email@domain.com', 'name')
- );
-
- return $aResult;
- }
-}
diff --git a/plugins/contacts-suggestions-example/LICENSE b/plugins/contacts-suggestions-example/LICENSE
deleted file mode 100644
index 4aed64b3a..000000000
--- a/plugins/contacts-suggestions-example/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2015 RainLoop Team
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/contacts-suggestions-example/index.php b/plugins/contacts-suggestions-example/index.php
deleted file mode 100644
index 3e505c455..000000000
--- a/plugins/contacts-suggestions-example/index.php
+++ /dev/null
@@ -1,35 +0,0 @@
-addHook('main.fabrica', 'MainFabrica');
- }
-
- /**
- * @param string $sName
- * @param mixed $mResult
- */
- public function MainFabrica($sName, &$mResult)
- {
- switch ($sName)
- {
- case 'suggestions':
-
- if (!\is_array($mResult))
- {
- $mResult = array();
- }
-
- include_once __DIR__.'/ContactsExampleSuggestions.php';
- $mResult[] = new ContactsExampleSuggestions();
- break;
- }
- }
-}
diff --git a/plugins/custom-admin-settings-tab/index.php b/plugins/custom-admin-settings-tab/index.php
deleted file mode 100644
index 306db50fa..000000000
--- a/plugins/custom-admin-settings-tab/index.php
+++ /dev/null
@@ -1,34 +0,0 @@
-UseLangs(true); // start use langs folder
-
- $this->addJs('js/CustomAdminSettings.js', true); // add js file
-
- $this->addJsonHook('JsonAdminGetData', 'JsonAdminGetData');
-
- $this->addTemplate('templates/PluginCustomAdminSettingsTab.html', true);
- }
-
- /**
- * @return array
- */
- public function JsonAdminGetData()
- {
- return $this->jsonResponse(__FUNCTION__, array(
- 'PHP' => phpversion()
- ));
- }
-}
-
diff --git a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js b/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js
deleted file mode 100644
index 20bd0d6f5..000000000
--- a/plugins/custom-admin-settings-tab/js/CustomAdminSettings.js
+++ /dev/null
@@ -1,44 +0,0 @@
-
-(function () {
-
- if (!window.rl)
- {
- return;
- }
-
- /**
- * @constructor
- */
- function CustomAdminSettings()
- {
- this.php = ko.observable('');
-
- this.loading = ko.observable(false);
- }
-
- CustomAdminSettings.prototype.onBuild = function () // special function
- {
- var self = this;
-
- this.loading(true);
-
- rl.pluginRemoteRequest((iError, oData) => {
-
- self.loading(false);
-
- if (!iError) {
- self.php(oData.Result.PHP || '');
- }
-
- if (rl.Enums.StorageResultType.Abort === iError) {
- // show abort
- }
-
- }, 'JsonAdminGetData');
-
- };
-
- rl.addSettingsViewModelForAdmin(CustomAdminSettings, 'PluginCustomAdminSettingsTab',
- 'SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN/TAB_NAME', 'custom');
-
-}());
diff --git a/plugins/custom-admin-settings-tab/langs/en.ini b/plugins/custom-admin-settings-tab/langs/en.ini
deleted file mode 100644
index 3d81ab706..000000000
--- a/plugins/custom-admin-settings-tab/langs/en.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN]
-TAB_NAME = "Custom Extension Tab"
-LEGEND_CUSTOM = "Custom Extension Tab (Example)"
-LABEL_PHP_VERSION = "PHP version"
diff --git a/plugins/custom-admin-settings-tab/langs/ru.ini b/plugins/custom-admin-settings-tab/langs/ru.ini
deleted file mode 100644
index 71fcee232..000000000
--- a/plugins/custom-admin-settings-tab/langs/ru.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN]
-TAB_NAME = "Плагин"
-LEGEND_CUSTOM = "Плагин (Пример)"
-LABEL_PHP_VERSION = "PHP версия"
\ No newline at end of file
diff --git a/plugins/custom-auth-example/LICENSE b/plugins/custom-auth-example/LICENSE
deleted file mode 100644
index 271342337..000000000
--- a/plugins/custom-auth-example/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2013 RainLoop Team
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/custom-auth-example/index.php b/plugins/custom-auth-example/index.php
deleted file mode 100644
index 242443500..000000000
--- a/plugins/custom-auth-example/index.php
+++ /dev/null
@@ -1,44 +0,0 @@
-addHook('login.credentials', 'FilterLoginCredentials');
- }
-
- /**
- * @param string $sEmail
- * @param string $sLogin
- * @param string $sPassword
- *
- * @throws \RainLoop\Exceptions\ClientException
- */
- public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
- {
- // Your custom php logic
- // You may change login credentials
- if ('demo@snappymail.eu' === $sEmail)
- {
- $sEmail = 'user@snappymail.eu';
- $sLogin = 'user@snappymail.eu';
- $sPassword = 'super-puper-password';
- }
- else
- {
- // or throw auth exeption
- throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError);
- // or
- throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed);
- // or
- throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed);
- }
- }
-}
diff --git a/plugins/custom-settings-tab/LICENSE b/plugins/custom-settings-tab/LICENSE
deleted file mode 100644
index 4aed64b3a..000000000
--- a/plugins/custom-settings-tab/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2015 RainLoop Team
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/custom-settings-tab/index.php b/plugins/custom-settings-tab/index.php
deleted file mode 100644
index cee8ecf12..000000000
--- a/plugins/custom-settings-tab/index.php
+++ /dev/null
@@ -1,62 +0,0 @@
-UseLangs(true); // start use langs folder
-
- $this->addJs('js/CustomUserSettings.js'); // add js file
-
- $this->addJsonHook('JsonGetCustomUserData', 'JsonGetCustomUserData');
- $this->addJsonHook('JsonSaveCustomUserData', 'JsonSaveCustomUserData');
-
- $this->addTemplate('templates/PluginCustomSettingsTab.html');
- }
-
- /**
- * @return array
- */
- public function JsonGetCustomUserData()
- {
- $aSettings = $this->getUserSettings();
-
- $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : '';
- $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : '';
-
- // or get user's data from your custom storage ( DB / LDAP / ... ).
-
- \sleep(1);
- return $this->jsonResponse(__FUNCTION__, array(
- 'UserFacebook' => $sUserFacebook,
- 'UserSkype' => $sUserSkype
- ));
- }
-
- /**
- * @return array
- */
- public function JsonSaveCustomUserData()
- {
- $sUserFacebook = $this->jsonParam('UserFacebook');
- $sUserSkype = $this->jsonParam('UserSkype');
-
- // or put user's data to your custom storage ( DB / LDAP / ... ).
-
- \sleep(1);
- return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings(array(
- 'UserFacebook' => $sUserFacebook,
- 'UserSkype' => $sUserSkype
- )));
- }
-
-}
-
diff --git a/plugins/custom-settings-tab/js/CustomUserSettings.js b/plugins/custom-settings-tab/js/CustomUserSettings.js
deleted file mode 100644
index 85b7f5241..000000000
--- a/plugins/custom-settings-tab/js/CustomUserSettings.js
+++ /dev/null
@@ -1,81 +0,0 @@
-
-(function () {
-
- if (!window.rl)
- {
- return;
- }
-
- /**
- * @constructor
- */
- function CustomUserSettings()
- {
- this.userSkype = ko.observable('');
- this.userFacebook = ko.observable('');
-
- this.loading = ko.observable(false);
- this.saving = ko.observable(false);
-
- this.savingOrLoading = ko.computed(function () {
- return this.loading() || this.saving();
- }, this);
- }
-
- CustomUserSettings.prototype.customJsonSaveData = function ()
- {
- var self = this;
-
- if (this.saving())
- {
- return false;
- }
-
- this.saving(true);
-
- rl.pluginRemoteRequest((iError, oData) => {
-
- self.saving(false);
-
- if (!iError)
- {
- // true
- }
- else if (rl.Enums.StorageResultType.Abort === iError) {
- // show abort
- }
- else
- {
- // false
- }
-
- }, 'JsonSaveCustomUserData', {
- 'UserSkype': this.userSkype(),
- 'UserFacebook': this.userFacebook()
- });
- };
-
- CustomUserSettings.prototype.onBuild = function () // special function
- {
- var self = this;
-
- this.loading(true);
-
- rl.pluginRemoteRequest((iError, oData) => {
-
- self.loading(false);
-
- if (!iError)
- {
- self.userSkype(oData.Result.UserSkype || '');
- self.userFacebook(oData.Result.UserFacebook || '');
- }
-
- }, 'JsonGetCustomUserData');
-
- };
-
- rl.addSettingsViewModel(CustomUserSettings, 'PluginCustomSettingsTab',
- 'SETTINGS_CUSTOM_PLUGIN/TAB_NAME', 'custom');
-
-}());
diff --git a/plugins/custom-settings-tab/langs/en.ini b/plugins/custom-settings-tab/langs/en.ini
deleted file mode 100644
index cb517b757..000000000
--- a/plugins/custom-settings-tab/langs/en.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[SETTINGS_CUSTOM_PLUGIN]
-TAB_NAME = "Custom Extension"
-LEGEND_CUSTOM = "Custom Extension (Example)"
-LABEL_SKYPE = "Skype"
-LABEL_FACEBOOK = "Facebook"
-BUTTON_SAVE = "Save"
diff --git a/plugins/custom-settings-tab/langs/ru.ini b/plugins/custom-settings-tab/langs/ru.ini
deleted file mode 100644
index 6b275600c..000000000
--- a/plugins/custom-settings-tab/langs/ru.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[SETTINGS_CUSTOM_PLUGIN]
-TAB_NAME = "Плагин"
-LEGEND_CUSTOM = "Плагин (Пример)"
-LABEL_SKYPE = "Skype"
-LABEL_FACEBOOK = "Facebook"
-BUTTON_SAVE = "Сохранить"
\ No newline at end of file
diff --git a/plugins/custom-admin-settings-tab/LICENSE b/plugins/example/LICENSE
similarity index 100%
rename from plugins/custom-admin-settings-tab/LICENSE
rename to plugins/example/LICENSE
diff --git a/plugins/example/index.php b/plugins/example/index.php
index 40741b9cc..7bb6a2b68 100644
--- a/plugins/example/index.php
+++ b/plugins/example/index.php
@@ -31,6 +31,22 @@ class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addTemplate(string $sFile, bool $bAdminScope = false);
$this->addTemplateHook(string $sName, string $sPlace, string $sLocalTemplateName, bool $bPrepend = false);
*/
+
+// $this->addHook('login.credentials', 'FilterLoginCredentials');
+
+
+ $this->UseLangs(true); // start use langs folder
+
+ // User Settings tab
+ $this->addJs('js/ExampleUserSettings.js'); // add js file
+ $this->addJsonHook('JsonGetExampleUserData', 'JsonGetExampleUserData');
+ $this->addJsonHook('JsonSaveExampleUserData', 'JsonSaveExampleUserData');
+ $this->addTemplate('templates/ExampleUserSettingsTab.html');
+
+ // Admin Settings tab
+ $this->addJs('js/ExampleAdminSettings.js', true); // add js file
+ $this->addJsonHook('JsonAdminGetData', 'JsonAdminGetData');
+ $this->addTemplate('templates/ExampleAdminSettingsTab.html', true);
}
/**
@@ -61,6 +77,77 @@ class ExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
}
}
+ public function JsonAdminGetData()
+ {
+ return $this->jsonResponse(__FUNCTION__, array(
+ 'PHP' => \phpversion()
+ ));
+ }
+
+ /**
+ * @param string $sEmail
+ * @param string $sLogin
+ * @param string $sPassword
+ *
+ * @throws \RainLoop\Exceptions\ClientException
+ */
+ public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
+ {
+ // Your custom php logic
+ // You may change login credentials
+ if ('demo@snappymail.eu' === $sEmail)
+ {
+ $sEmail = 'user@snappymail.eu';
+ $sLogin = 'user@snappymail.eu';
+ $sPassword = 'super-duper-password';
+ }
+ else
+ {
+ // or throw auth exeption
+ throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AuthError);
+ // or
+ throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountNotAllowed);
+ // or
+ throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainNotAllowed);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function JsonGetExampleUserData()
+ {
+ $aSettings = $this->getUserSettings();
+
+ $sUserFacebook = isset($aSettings['UserFacebook']) ? $aSettings['UserFacebook'] : '';
+ $sUserSkype = isset($aSettings['UserSkype']) ? $aSettings['UserSkype'] : '';
+
+ // or get user's data from your custom storage ( DB / LDAP / ... ).
+
+ \sleep(1);
+ return $this->jsonResponse(__FUNCTION__, array(
+ 'UserFacebook' => $sUserFacebook,
+ 'UserSkype' => $sUserSkype
+ ));
+ }
+
+ /**
+ * @return array
+ */
+ public function JsonSaveExampleUserData()
+ {
+ $sUserFacebook = $this->jsonParam('UserFacebook');
+ $sUserSkype = $this->jsonParam('UserSkype');
+
+ // or put user's data to your custom storage ( DB / LDAP / ... ).
+
+ \sleep(1);
+ return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings(array(
+ 'UserFacebook' => $sUserFacebook,
+ 'UserSkype' => $sUserSkype
+ )));
+ }
+
/*
public function Config() : \RainLoop\Config\Plugin
public function Description() : string
diff --git a/plugins/example/js/ExampleAdminSettings.js b/plugins/example/js/ExampleAdminSettings.js
new file mode 100644
index 000000000..5a52ecfda
--- /dev/null
+++ b/plugins/example/js/ExampleAdminSettings.js
@@ -0,0 +1,37 @@
+
+(rl => { if (rl) {
+
+ class ExampleAdminSettings
+ {
+ constructor()
+ {
+ this.php = ko.observable('');
+ this.loading = ko.observable(false);
+ }
+
+ onBuild()
+ {
+ this.loading(true);
+
+ rl.pluginRemoteRequest((iError, oData) => {
+
+ this.loading(false);
+
+ if (iError) {
+ console.error({
+ iError: iError,
+ oData: oData
+ });
+ } else {
+ this.php(oData.Result.PHP || '');
+ }
+
+ }, 'JsonAdminGetData');
+
+ }
+ }
+
+ rl.addSettingsViewModelForAdmin(ExampleAdminSettings, 'ExampleAdminSettingsTab',
+ 'SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN/TAB_NAME', 'Example');
+
+}})(window.rl);
diff --git a/plugins/example/js/ExampleUserSettings.js b/plugins/example/js/ExampleUserSettings.js
new file mode 100644
index 000000000..839e9e84a
--- /dev/null
+++ b/plugins/example/js/ExampleUserSettings.js
@@ -0,0 +1,68 @@
+
+(rl => { if (rl) {
+
+ class ExampleUserSettings
+ {
+ constructor()
+ {
+ this.userSkype = ko.observable('');
+ this.userFacebook = ko.observable('');
+
+ this.loading = ko.observable(false);
+ this.saving = ko.observable(false);
+
+ this.savingOrLoading = ko.computed(() => {
+ return this.loading() || this.saving();
+ });
+ }
+
+ exampleJsonSaveData()
+ {
+ if (!this.saving()) {
+ this.saving(true);
+
+ rl.pluginRemoteRequest((iError, oData) => {
+
+ this.saving(false);
+
+ if (iError) {
+ console.error({
+ iError: iError,
+ oData: oData
+ });
+ } else {
+ console.dir({
+ iError: iError,
+ oData: oData
+ });
+ }
+
+ }, 'JsonSaveExampleUserData', {
+ 'UserSkype': this.userSkype(),
+ 'UserFacebook': this.userFacebook()
+ });
+ }
+ }
+
+ onBuild()
+ {
+ this.loading(true);
+
+ rl.pluginRemoteRequest((iError, oData) => {
+
+ this.loading(false);
+
+ if (!iError) {
+ self.userSkype(oData.Result.UserSkype || '');
+ self.userFacebook(oData.Result.UserFacebook || '');
+ }
+
+ }, 'JsonGetExampleUserData');
+
+ }
+ }
+
+ rl.addSettingsViewModel(ExampleUserSettings, 'ExampleUserSettingsTab',
+ 'SETTINGS_EXAMPLE_PLUGIN/TAB_NAME', 'Example');
+
+}})(window.rl);
diff --git a/plugins/example/langs/en.json b/plugins/example/langs/en.json
new file mode 100644
index 000000000..9c5f0c84c
--- /dev/null
+++ b/plugins/example/langs/en.json
@@ -0,0 +1,15 @@
+{
+ "SETTINGS_EXAMPLE_ADMIN_EXAMPLE_TAB_PLUGIN": {
+ "TAB_NAME": "Example Extension Tab",
+ "LEGEND_EXAMPLE": "Example Extension Tab (Example)",
+ "LABEL_PHP_VERSION": "PHP version"
+ },
+ "SETTINGS_EXAMPLE_PLUGIN": {
+ "TAB_NAME": "Example Extension",
+ "LEGEND_EXAMPLE": "Example Extension (Example)",
+ "LABEL_PHP_VERSION": "PHP version",
+ "LABEL_SKYPE": "Skype",
+ "LABEL_FACEBOOK": "Facebook",
+ "BUTTON_SAVE": "Save"
+ }
+}
diff --git a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html b/plugins/example/templates/ExampleAdminSettingsTab.html
similarity index 64%
rename from plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html
rename to plugins/example/templates/ExampleAdminSettingsTab.html
index cab9229a7..8fc98c5fc 100644
--- a/plugins/custom-admin-settings-tab/templates/PluginCustomAdminSettingsTab.html
+++ b/plugins/example/templates/ExampleAdminSettingsTab.html
@@ -1,17 +1,17 @@
\ No newline at end of file
+
diff --git a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html b/plugins/example/templates/ExampleUserSettingsTab.html
similarity index 66%
rename from plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html
rename to plugins/example/templates/ExampleUserSettingsTab.html
index 1b688168b..3bbfad069 100644
--- a/plugins/custom-settings-tab/templates/PluginCustomSettingsTab.html
+++ b/plugins/example/templates/ExampleUserSettingsTab.html
@@ -1,13 +1,13 @@