logo-128.png \
proxmox_logo.png
-CSSFILES = ext6-pmg.css
+CSSFILES = ext6-pmg.css ext6-pmg-mobile.css
all:
js/pmgmanagerlib.js:
make -C js pmgmanagerlib.js
-install: pmg-index.html.tt js/pmgmanagerlib.js
+js/pmgmanagerlib-mobile.js:
+ make -C js pmgmanagerlib-mobile.js
+
+install: pmg-index.html.tt js/pmgmanagerlib.js js/pmgmanagerlib-mobile.js
install -d -m 755 ${WWWCSSDIR}
install -d -m 755 ${WWWIMAGESDIR}
install -d -m 755 ${WWWJSDIR}
install -m 0644 pmg-index.html.tt ${WWWBASEDIR}
+ install -m 0644 pmg-mobile-index.html.tt ${WWWBASEDIR}
install -m 0644 js/pmgmanagerlib.js ${WWWJSDIR}
+ install -m 0644 js/pmgmanagerlib-mobile.js ${WWWJSDIR}
for i in ${IMAGES}; do install -m 0644 images/$$i ${WWWIMAGESDIR}; done
for i in ${CSSFILES}; do install -m 0644 css/$$i ${WWWCSSDIR}; done
--- /dev/null
+.item-title .item-header {
+ white-space: inherit;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.empty {
+ padding: 1em;
+ color: var(--f7-label-text-color);
+}
+
+img.logo {
+ padding: 0 10px;
+ vertical-align: middle;
+ height: 64px;
+}
+
+img.logo-navbar {
+ padding: 0 10px;
+ height: 32;
+}
+
+.settings-form {
+ position: absolute;
+ bottom: calc(var(--f7-fab-margin) + var(--f7-safe-area-bottom));
+ right: calc(var(--f7-fab-margin) + var(--f7-safe-area-right));
+ z-index: 1500;
+ width: 200px;
+ background-color: var(--f7-list-bg-color);
+}
+
+.button.subscription i.icon {
+ display: inline;
+}
+
+@media only screen and (max-width: 500px) {
+ .login-screen-title {
+ font-size: 6vw;
+ }
+}
+
+@media only screen and (min-width: 500px) {
+ .login-screen-title {
+ font-size: 32px;
+ }
+}
Section: perl
Priority: optional
Maintainer: Proxmox Support Team <support@proxmox.com>
-Build-Depends: debhelper (>= 9), perl (>= 5.10.0-19), libtemplate-perl
+Build-Depends: debhelper (>= 9),
+ libtemplate-perl,
+ perl (>= 5.10.0-19),
Standards-Version: 3.9.5
Homepage: http://www.proxmox.com
Package: pmg-gui
Architecture: all
-Depends: ${perl:Depends}, libtemplate-perl, libjs-extjs (>= 6.0.1), fonts-font-awesome, pmg-i18n, proxmox-widget-toolkit
+Depends: fonts-font-awesome,
+ libjs-extjs (>= 6.0.1),
+ libjs-framework7,
+ libtemplate-perl,
+ pmg-i18n,
+ proxmox-widget-toolkit,
+ ${perl:Depends},
Description: Proxmox Mail Gateway GUI
Graphical user interface for Proxmox Mail Gateway.
SpamContextMenu.js \
Application.js
+# caution: order is important
+MOBILESRC= \
+ mobile/component.js \
+ mobile/loginscreen.js \
+ mobile/mailview.js \
+ mobile/quarantineview.js \
+ mobile/utils.js \
+ mobile/app.js \
+
OnlineHelpInfo.js: /usr/bin/asciidoc-pmg
/usr/bin/asciidoc-pmg scan-extjs ${JSSRC} >$@.tmp
mv $@.tmp $@
cat OnlineHelpInfo.js ${JSSRC} >$@.tmp
mv $@.tmp $@
-all: pmgmanagerlib.js
+pmgmanagerlib-mobile.js: ${MOBILESRC}
+ cat ${MOBILESRC} >$@.tmp
+ mv $@.tmp $@
+
+all: pmgmanagerlib.js pmgmanagerlib-mobile.js
.PHONY: clean
clean:
find . -name '*~' -exec rm {} ';'
- rm -rf pmgmanagerlib.js OnlineHelpInfo.js
+ rm -rf pmgmanagerlib.js pmgmanagerlib-mobile.js OnlineHelpInfo.js
--- /dev/null
+var $$ = Dom7;
+var app = new Framework7({
+ root: '#app',
+ init: false,
+ name: 'Proxmox Mail Gateway',
+ routes: [
+ {
+ path: '/:path/:subpath?',
+ async: function(routeTo, routeFrom, resolve, reject) {
+ if (routeTo.params.path === 'mail') {
+ let mail = new MailView();
+ resolve({
+ template: mail.getTpl()
+ },{
+ context: {
+ mailid: routeTo.params.subpath
+ }
+ });
+ } else {
+ reject();
+ }
+ }
+ },
+ {
+ path: '/mail/:mailid/:action',
+ async: function(routeTo, routeFrom, resolve, reject) {
+ let action = routeTo.params.action;
+ let mailid = routeTo.params.mailid;
+ let confirmText = gettext('')
+ app.dialog.confirm(
+ `${action}: ${mailid}`,
+ gettext('Confirm'),
+ () => {
+ let loader = app.dialog.preloader();
+ app.request({
+ method: 'POST',
+ url: '/api2/json/quarantine/content/',
+ data: {
+ action: action,
+ id: mailid
+ },
+ headers: {
+ CSRFPreventionToken: Proxmox.CSRFPreventionToken
+ },
+ success: (data, status, xhr) => {
+ loader.close();
+ app.dialog.alert(
+ `Action '${action}' successful`,
+ gettext("Info"),
+ () => {
+ if (action === 'delete' ||
+ action === 'deliver')
+ {
+ // refresh the main list when a mail
+ // got deleted or delivered
+ app.ptr.refresh();
+ }
+ }
+ );
+ reject();
+ },
+ error: xhr => {
+ loader.close();
+ PMG.Utils.showError(xhr);
+ reject();
+ }
+ })
+ },
+ () => {
+ reject();
+ }
+ );
+ }
+ }
+ ]
+});
+
+let quarlist = new QuarantineView();
+
+app.init();
--- /dev/null
+class Component {
+ constructor(config = {}) {
+ var me = this;
+ me.config = config;
+ me.data = config.data || {};
+ me.tpl = me.config.tpl || '<div class="component"></div>';
+ }
+ getTpl() {
+ var me = this;
+ if (!me._compiledtpl) {
+ me._compiledtpl = Template7.compile(me.tpl);
+ }
+ return me._compiledtpl;
+ }
+ getEl(data) {
+ var me = this;
+ if (data === undefined && me._el) {
+ return me._el;
+ } else if (data !== undefined) {
+ me.data =data;
+ }
+ me._el = Dom7(me.getTpl()(me.data));
+ return me._el;
+ }
+}
+
--- /dev/null
+class LoginScreen extends Component {
+ constructor(config = {}) {
+ config.tpl = `
+ <div class="login-screen">
+ <div class="view">
+ <div class="page">
+ <div class="page-content login-screen-content">
+ <div class="login-screen-title">
+ <img class="logo" src="pve2/images/logo-128.png" />
+ Proxmox Mail Gateway
+ </div>
+ <form action="/api2/json/access/ticket" method="POST" class="form-ajax-submit">
+ <div class="list">
+ <ul>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">Username</div>
+ <div class="item-input-wrap">
+ <input type="text" name="username" placeholder="{{gettext 'Username'}}" required validate>
+ <span class="input-clear-button"></span>
+ </div>
+ </div>
+ </li>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">Password</div>
+ <div class="item-input-wrap">
+ <input type="password" name="password" placeholder="{{gettext 'Password'}}" required validate>
+ <span class="input-clear-button"></span>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <div class="list">
+ <ul>
+ <li>
+ <input type="submit" class="button" value='{{gettext "Log In"}}'>
+ </li>
+ </ul>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+ super(config);
+ var me = this;
+ me._screen = app.loginScreen.create({
+ content: me.getEl(),
+ });
+
+ let login = config.loginInfo;
+ me._form = me.getEl().find('form');
+
+ if (login.username && login.ticket) {
+ app.form.fillFromData(me._form, {
+ username: login.username,
+ password: login.ticket,
+ });
+ me._autoLogin = true;
+ } else if (PMG.Utils.authOK()) {
+ app.form.fillFromData(me._form, {
+ username: Proxmox.UserName,
+ password: decodeURIComponent(PMG.Utils.getCookie('PMGAuthCookie')),
+ });
+ me._autoLogin = true;
+ }
+ }
+ open(onLogin) {
+ var me = this;
+ return new Promise(function(resolve, reject) {
+ me._form.on('formajax:beforesend', (el, data, xhr) => {
+ me.loader = app.dialog.preloader();
+ });
+
+ me._form.on('formajax:success', (el, data, xhr) => {
+ let json;
+ try {
+ json = JSON.parse(xhr.responseText);
+ } catch (err) {
+ xhr.error = err;
+ PMG.Utils.showError(xhr);
+ return;
+ }
+
+ resolve(json);
+ });
+
+ me._form.on('formajax:error', (el, data, xhr) => {
+ me.loader.close();
+ PMG.Utils.showError(xhr);
+ });
+
+ if (me._autoLogin) {
+ delete me._autoLogin;
+ me._screen.on('open', () => {
+ me._form.trigger('submit');
+ })
+ }
+
+ me._screen.open();
+ });
+ }
+ close() {
+ var me = this;
+ if (me.loader) {
+ me.loader.close();
+ }
+ me._screen.close(false);
+ }
+}
+
--- /dev/null
+class MailView extends Component {
+ constructor(config = {}) {
+ config.tpl = `
+ <div class="page">
+ <div class="navbar sliding">
+ <div class="navbar-inner">
+ <div class="left">
+ <a href="#" class="link back">
+ <i class="icon icon-back"></i>
+ <span class="ios-only">{{gettext "Back"}}</span>
+ </a>
+ </div>
+ <div class="title">Preview</div>
+ </div>
+ </div>
+ <div class="fab fab-right-bottom">
+ <a href="#">
+ <i class="icon f7-icons ios-only">menu</i>
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">menu</i>
+ <i class="icon material-icons md-only">close</i>
+ </a>
+ <div class="fab-buttons fab-buttons-top">
+ <a href="/mail/{{mailid}}/blacklist" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">close</i>
+ </span>
+ <span class="fab-label">{{gettext "Blacklist"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/whitelist" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">check</i>
+ <i class="icon material-icons md-only">check</i>
+ </span>
+ <span class="fab-label">{{gettext "Whitelist"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/delete" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">trash</i>
+ <i class="icon material-icons md-only">delete</i>
+ </span>
+ <span class="fab-label">{{gettext "Delete"}}</span>
+ </a>
+ <a href="/mail/{{mailid}}/deliver" class="fab-label-button fab-close">
+ <span>
+ <i class="icon f7-icons ios-only">paper_plane</i>
+ <i class="icon material-icons md-only">send</i>
+ </span>
+ <span class="fab-label">{{gettext "Deliver"}}</span>
+ </a>
+ </div>
+ </div>
+ <div class="page-content">
+ <iframe frameborder=0 width="100%" height="100%" sandbox="allow-same-origin" src="/api2/htmlmail/quarantine/content?id={{mailid}}"></iframe>
+ </div>
+ </div>
+ `;
+ super(config);
+ }
+}
+
--- /dev/null
+class QuarantineView extends Component {
+ constructor(config = {}) {
+ config.tpl = config.tpl || `
+ <div class="view view-quarantine">
+ <div data-name="quarantine-list" class="page">
+ <div class="navbar">
+ <div class="navbar-inner">
+ <div class="left">
+ <img class="logo-navbar" style="padding: 0 10px" src="pve2/images/logo-128.png" height=32 />
+ </div>
+ <div class="title">Mail Gateway</div>
+ </div>
+ </div>
+ <div class="settings-form elevation-5 fab-morph-target">
+ <div class="block-title block-title-medium">{{gettext "Range"}}</div>
+ <div class="list no-hairlines-md">
+ <ul>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">{{gettext "From"}}</div>
+ <div class="item-input-wrap">
+ <input type="date" name="from" placeholder="from" required validate>
+ </div>
+ </div>
+ </li>
+ <li class="item-content item-input">
+ <div class="item-inner">
+ <div class="item-title item-label">{{gettext "To"}}</div>
+ <div class="item-input-wrap">
+ <input type="date" name="to" placeholder="to" required validate>
+ </div>
+ </div>
+ </li>
+ </ul>
+ <a class="button fab-close range-form">{{gettext "OK"}}</a>
+ </div>
+ </div>
+ <div class="fab fab-morph fab-right-bottom" data-morph-to=".settings-form">
+ <a href="#">
+ <i class="icon f7-icons ios-only">calendar</i>
+ <i class="icon material-icons md-only">date_range</i>
+ </a>
+ </div>
+ <div class="toolbar subscription toolbar-hidden toolbar-bottom">
+ <div class="toolbar-inner">
+ <a class="button subscription">
+ <i class="icon f7-icons ios-only color-yellow">alert</i>
+ <i class="icon material-icons md-only color-yellow">warning</i>
+ <span class="subscription-text">
+ {{gettext "No valid subscription"}}
+ </span>
+ </a>
+ </div>
+ </div>
+ <div class="page-content ptr-content">
+ <div class="ptr-preloader">
+ <div class="preloader"></div>
+ <div class="ptr-arrow"></div>
+ </div>
+ <div class="list virtual-list"></div>
+ </div>
+ </div>
+ </div>`;
+ config.itemTemplate = config.itemTemplate || `
+ <li class="swipeout">
+ <div class="swipeout-content">
+ <a href="/mail/{{id}}/" class="item-link item-content">
+ <div class="item-inner">
+ <div class="item-title">
+ <div class="item-header">{{escape from}}</div>
+ {{escape subject}}
+ </div>
+ <div class="item-after">Score: {{js "this.spamlevel || 0"}}</div>
+ </div>
+ </a>
+ </div>
+ <div class="swipeout-actions-left">
+ <a href="/mail/{{id}}/deliver" class="color-green swipeout-close">
+ <i class="icon f7-icons ios-only">paper_plane</i>
+ <i class="icon material-icons md-only">send</i>
+ {{gettext "Deliver"}}
+ </a>
+ <a href="/mail/{{id}}/whitelist" class="swipeout-close">
+ <i class="icon f7-icons ios-only">check</i>
+ <i class="icon material-icons md-only">check</i>
+ {{gettext "Whitelist"}}
+ </a>
+ </div>
+ <div class="swipeout-actions-right">
+ <a href="/mail/{{id}}/blacklist" class="color-orange swipeout-close">
+ <i class="icon f7-icons ios-only">close</i>
+ <i class="icon material-icons md-only">close</i>
+ {{gettext "Blacklist"}}
+ </a>
+ <a href="/mail/{{id}}/delete" class="color-red swipeout-close">
+ <i class="icon f7-icons ios-only">trash</i>
+ <i class="icon material-icons md-only">delete</i>
+ {{gettext "Delete"}}
+ </a>
+ </div>
+ </li>`;
+ config.dividerTemplate = config.dividerTemplate ||
+ '<li class="item-divider">{{group}}</li>';
+ super(config);
+
+ var me = this;
+
+ me._compiledItemTemplate = Template7.compile(me.config.itemTemplate);
+ me._compiledDividerTemplate = Template7.compile(me.config.dividerTemplate);
+ me.skelTpl = `
+ <li class="skeleton-text skeleton-effect-fade">
+ <a href="#" class="item-content item-link">
+ <div class="item-inner">
+ <div class="item-title">
+ <div class="item-header">_______________________</div>
+ ____ ______ __ _______ ____ _______ _______ ___
+ </div>
+ <div class="item-after">Score: 15</div>
+ </div>
+ </a>
+ </li>`;
+ me.skelDividerTpl = '<li class="item-divider skeleton-text">____-__-__</li>';
+ me.setEndtime(new Date());
+ let startdate = new Date();
+ startdate.setDate(startdate.getDate() - 7);
+ me.setStarttime(startdate);
+
+ // add to dom
+ $$(me.config.target || '#app').append(me.getEl());
+
+ $$(document).on('page:init', '.page[data-name=quarantine-list]', (e, page) => {
+ me.vList = app.virtualList.create({
+ el: '.virtual-list',
+ items: [],
+ renderItem: function(item) {
+ return me._renderItem(item);
+ },
+ emptyTemplate: '<div class="empty">No data in database</div>'
+ });
+
+ // setup pull to refresh
+ $$('.ptr-content').on('ptr:refresh', (e) => {
+ me.setItems([
+ { skel: true, divider: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true, divider: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ { skel: true },
+ ]);
+ me.load().then(data => {
+ me.setItems(data, {
+ sorter: {
+ property: 'time',
+ numeric: true,
+ direction: 'DESC'
+ },
+ grouperFn: (val) => PMG.Utils.unixToIso(val['time'])
+ });
+ }).catch(PMG.Utils.showError).then(() => {
+ e.detail();
+ });
+ });
+
+ // process query parameters
+ let { mail, action, date, username, ticket } = PMG.Utils.extractParams();
+ if (date) {
+ me.setStarttime(date);
+ }
+
+ // setup range form
+ $$('input[name=from]').val(PMG.Utils.unixToIso(me.starttime));
+ $$('input[name=to]').val(PMG.Utils.unixToIso(me.endtime));
+
+ $$('.fab').on('fab:close', () => {
+ let fromChanged = me.setStarttime($$('input[name=from]').val());
+ let toChanged = me.setEndtime($$('input[name=to]').val());
+ if (fromChanged || toChanged) {
+ app.ptr.refresh();
+ }
+ });
+
+ // check login
+
+ let loginInfo = { username, ticket };
+ let showPopup = (username && ticket) || !PMG.Utils.authOK();
+ me._loginScreen = new LoginScreen({ loginInfo });
+
+ me._loginScreen.open().then(data => {
+ me._loginScreen.close();
+ PMG.Utils.setLoginInfo(data);
+ return PMG.Utils.getSubscriptionInfo();
+ }).then(data => {
+ return PMG.Utils.checkSubscription(data, showPopup);
+ }).then(data => {
+ app.ptr.refresh();
+ if (mail) {
+ let url = "/mail/" + mail + "/" + (action || "");
+ me._view.router.navigate(url);
+ }
+ }).catch(PMG.Utils.showError);
+ });
+
+ me._view = app.views.create('.view-quarantine', {
+ main: me.config.mainView !== undefined ? me.config.mainView : true,
+ url: '/',
+ pushState: true,
+ pushStateAnimateOnLoad: true
+ });
+ }
+ setStarttime(starttime) {
+ var me = this;
+ let date = starttime;
+ if (!(starttime instanceof Date)) {
+ // we assume an ISO string
+ if (starttime == '') {
+ return;
+ }
+ date = new Date(PMG.Utils.isoToUnix(starttime)*1000);
+ }
+ // starttime is at beginning of date
+ date.setHours(0,0,0,0);
+ let result = Math.round(date.getTime()/1000);
+ if (result !== me.starttime) {
+ me.starttime = result;
+ return true;
+ }
+ return false
+ }
+ setEndtime(endtime) {
+ var me = this;
+ let date = endtime;
+ if (!(endtime instanceof Date)) {
+ if (endtime == '') {
+ return;
+ }
+ // we assume an ISO string
+ date = new Date(PMG.Utils.isoToUnix(endtime)*1000);
+ }
+ // endtime is at the end of the day
+ date.setHours(23, 59, 59);
+ let result = Math.round(date.getTime()/1000);
+ if (result !== me.endtime) {
+ me.endtime = result;
+ return true;
+ }
+ return false;
+ }
+ _renderItem(item) {
+ var me = this;
+
+ if(typeof item === 'object') {
+ if (item.skel) {
+ return item.divider? me.skelDividerTpl : me.skelTpl;
+ } else if (item.divider) {
+ return me._compiledDividerTemplate(item);
+ } else {
+ return me._compiledItemTemplate(item);
+ }
+ }
+
+ return item.toString();
+ }
+ setItems(items, options) {
+ var me = this;
+ if (options && options.sorter) {
+ if (options.sorter.sorterFn) {
+ items.sort(options.sorter.sorterFn);
+ } else {
+ let prop = options.sorter.property;
+ let numeric = options.sorter.numeric;
+ let dir = options.sorter.direction === "ASC" ? 1 : -1;
+ items.sort((a,b) => {
+ let result;
+
+ if (numeric) {
+ result = a[prop] - b[prop];
+ } else {
+ result = a[prop] === b[prop] ? 0 : (a[prop] < b[prop] ? 1 : -1);
+ }
+
+ return result * dir;
+ });
+ }
+ }
+ me.vList.replaceAllItems(items);
+ if (options && options.grouperFn) {
+ let lastgroup;
+ let offset = 0;
+ for (let i = 0; i+offset < items.length; i++) {
+ let item = items[i+offset];
+ let curgroup = options.grouperFn(item);
+ if (curgroup != lastgroup) {
+ me.vList.insertItemBefore(i+(offset++), {
+ divider: true,
+ group: curgroup
+ });
+ lastgroup = curgroup;
+ }
+ }
+ }
+ }
+ load() {
+ var me = this;
+ return new Promise(function(resolve, reject) {
+ app.request({
+ url: '/api2/json/quarantine/spam',
+ data: {
+ starttime: me.starttime,
+ endtime: me.endtime
+ },
+ dataType: 'json',
+ success: (response, status, xhr) => {
+ resolve(response.data);
+ },
+ error: xhr => {
+ reject(xhr);
+ }
+ });
+ });
+ }
+}
+
--- /dev/null
+Template7.registerHelper('gettext', function(value) {
+ return gettext(value);
+});
+
+var PMG = {
+ Utils: {
+ getCookie(name) {
+ let cookies = document.cookie.split(/;\s*/);
+ for (let i = 0; i < cookies.length; i++) {
+ let cookie = cookies[i].split('=');
+ if (cookie[0] === name && cookie.length > 1) {
+ return cookie[1];
+ }
+ }
+ return undefined;
+ },
+ setCookie(name, value, expires) {
+ value = encodeURIComponent(value);
+ let cookie = `${name}=${value}`;
+ if (expires) {
+ cookie += `; expires=${expires}`;
+ }
+ document.cookie = cookie;
+ },
+ deleteCookie(name) {
+ PMG.Utils.setCookie(name, "", "Thu, 01 Jan 1970 00:00:00 UTC");
+ },
+ authOK(options) {
+ var authCookie = PMG.Utils.getCookie('PMGAuthCookie') || "";
+ return (authCookie.substr(0,7) === 'PMGQUAR' && Proxmox.UserName !== '');
+ },
+ isoToUnix(iso) {
+ let fields = iso.split('-').map((field) => parseInt(field, 10));
+ // monthIndex starts at 0
+ let date = new Date(fields[0],fields[1]-1, fields[2]);
+ return Math.round(date.getTime()/1000);
+ },
+ unixToIso(unix) {
+ let date = new Date(unix*1000);
+ let year = date.getFullYear().toString();
+ let month = (date.getMonth()+1).toString().padStart(2, "0");
+ let day = date.getDate().toString().padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ },
+ showError(xhr) {
+ let statusText = "", errorText = "";
+ if (xhr instanceof Error) {
+ statusText = gettext("Error");
+ errorText = xhr.message;
+ } else if (xhr.error instanceof Error) {
+ statusText = gettext("Error");
+ errorText = xhr.error.message;
+ } else {
+ statusText = xhr.status.toString() + ' ' + xhr.statusText;
+ try {
+ let errorObj = JSON.parse(xhr.responseText);
+ if (errorObj.errors) {
+ let errors = Object.keys(errorObj.errors).map((key) => key + ": " + errorObj.errors[key]);
+ errorText = errors.join('<br>');
+ }
+ } catch (e) {
+ statusText = gettext("Error");
+ errorText = e.message;
+ }
+ }
+ app.toast.show({
+ text: `Error:<br>
+ ${statusText}<br>
+ ${errorText}
+ `,
+ closeButton: true,
+ destroyOnClose: true
+ });
+ },
+ extractParams() {
+ let queryObj = app.utils.parseUrlQuery(location.search);
+ let mail, action, date, username, ticket;
+ if (queryObj.ticket) {
+ let tocheck = decodeURIComponent(queryObj.ticket);
+ let match = tocheck.match(/^PMGQUAR:([^\s\:]+):/);
+ if (match) {
+ ticket = tocheck;
+ username = match[1];
+ }
+ delete queryObj.ticket;
+ }
+
+ if (queryObj.date) {
+ date =queryObj.date;
+ delete queryObj.date;
+ }
+
+ if (queryObj.cselect) {
+ mail = queryObj.cselect;
+ action = queryObj.action;
+ delete queryObj.cselect;
+ delete queryObj.action;
+ }
+
+ if (mail || action || date || ticket) {
+ let queryString = app.utils.serializeObject(queryObj);
+ window.history.replaceState(
+ window.history.state,
+ document.title,
+ location.pathname + (queryString? "?" + queryString : '')
+ );
+ }
+
+ return { mail, action, date, username, ticket };
+ },
+ setLoginInfo(result) {
+ PMG.Utils.setCookie('PMGAuthCookie', result.data.ticket);
+ Proxmox.CSRFPreventionToken = result.data.CSRFPreventionToken;
+ },
+ getSubscriptionInfo() {
+ return new Promise(function(resolve, reject) {
+ app.request({
+ url: '/api2/json/nodes/localhost/subscription',
+ dataType: 'json',
+ success: (result, status, xhr) => {
+ resolve(result.data);
+ },
+ error: (xhr, status) => {
+ reject(xhr);
+ }
+ });
+ });
+ },
+ checkSubscription(data, showPopup) {
+ return new Promise(function(resolve, reject) {
+ if (data.status !== 'Active') {
+ let url = data.url || 'https://wwww.proxmox.com';
+ let err = `You do not have a valid subscription for this server.
+ Please visit
+ <a target="_blank" href="${url}">www.proxmox.com</a>
+ to get a list of available options.`;
+ app.toolbar.show('.toolbar.subscription');
+ $$('.button.subscription').on('click', () => {
+ app.dialog.alert(
+ err,
+ gettext("No valid subscription"),
+ );
+ });
+ if (showPopup) {
+ app.dialog.alert(
+ err,
+ gettext("No valid subscription"),
+ () => {
+ resolve(data);
+ }
+ );
+ } else {
+ resolve();
+ }
+ } else {
+ app.toolbar.hide('.toolbar.subscription');
+ resolve();
+ }
+ });
+ }
+ }
+};
+
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="theme-color" content="#2196f3">
+ <title>Proxmox Mail Gateway - Quarantine</title>
+ <link rel="stylesheet" href="framework7/css/framework7.bundle.min.css">
+ <link rel="stylesheet" href="framework7/css/framework7-icons.css">
+ <link rel="stylesheet" href="framework7/css/material-icons.css">
+ <link rel="stylesheet" href="pve2/css/ext6-pmg-mobile.css">
+ [% IF langfile %]
+ <script type='text/javascript' src='/pve2/locale/pmg-lang-[% lang %].js'></script>
+ [% ELSE %]
+ <script type='text/javascript'> function gettext(buf) { return buf; } </script>
+ [%- END %]
+ <script type="text/javascript">
+ Proxmox = {
+ UserName: '[% username %]',
+ CSRFPreventionToken: '[% token %]'
+ };
+ </script>
+ </head>
+ <body>
+ <div id="app">
+ <div class="statusbar"></div>
+ </div>
+ [% IF debug %]
+ <script type="text/javascript" src="/framework7/js/framework7.bundle.js"></script>
+ [% ELSE %]
+ <script type="text/javascript" src="/framework7/js/framework7.bundle.min.js"></script>
+ [% END %]
+ <script type="text/javascript" src="/pve2/js/pmgmanagerlib-mobile.js"></script>
+ </body>
+</html>