Merge remote-tracking branch 'upstream/master' into passlib
| @ -1,23 +1,59 @@ | ||||
| .select2-search--inline .select2-search__field:focus { | ||||
|   border: none; | ||||
| /* mailu logo */ | ||||
| .mailu-logo { | ||||
|   opacity: .8; | ||||
| } | ||||
| .bg-mailu-logo { | ||||
|   background-color: #2980b9!important; | ||||
| } | ||||
| 
 | ||||
| .sidebar h4 { | ||||
|   padding-left: 5px; | ||||
|   padding-right: 5px; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
| /* user image */ | ||||
| .div-circle { | ||||
|   position: relative; | ||||
|   width: 2.1rem; | ||||
|   height: 2.1rem; | ||||
|   opacity: .8; | ||||
|   background-color: white; | ||||
|   border-radius: 50%; | ||||
| } | ||||
| .div-circle > i { | ||||
|   display: block; | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   transform: translate(-50%, -50%) | ||||
| } | ||||
| 
 | ||||
| .sidebar-collapse .sidebar h4 { | ||||
|   display: none !important; | ||||
| /* nice round preformatted configuration display */ | ||||
| .pre-config { | ||||
|   padding: 9px; | ||||
|   margin: 0; | ||||
|   white-space: pre-wrap; | ||||
|   word-wrap: anywhere; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| 
 | ||||
| .logo a { | ||||
|   color: #fff; | ||||
| /* fieldset */ | ||||
| legend { | ||||
|   font-size: inherit; | ||||
| } | ||||
| fieldset:disabled :not(legend) label { | ||||
|   opacity: .5; | ||||
| } | ||||
| fieldset:disabled .form-control:disabled { | ||||
|   color: gray; | ||||
| } | ||||
| 
 | ||||
| .sidebar-toggle { | ||||
|   padding: unset !important; | ||||
| /* fix animation for icons in menu text */ | ||||
| .sidebar .nav-link p i { | ||||
|   transition: margin-left .3s linear,opacity .3s ease,visibility .3s ease; | ||||
| } | ||||
| 
 | ||||
| /* fix select2 text color */ | ||||
| .select2-container--default .select2-selection--multiple .select2-selection__choice { | ||||
|   color: black; | ||||
| } | ||||
| 
 | ||||
| /* range input spacing */ | ||||
| .input-group-text { | ||||
|   margin-right: 1em; | ||||
| } | ||||
|  | ||||
| @ -1,17 +1,79 @@ | ||||
| require('./app.css'); | ||||
| 
 | ||||
| import 'admin-lte/plugins/select2/js/select2.js'; | ||||
| import 'admin-lte/plugins/datatables/jquery.dataTables.js'; | ||||
| import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.js'; | ||||
| import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.js'; | ||||
| import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.js'; | ||||
| import logo from './mailu.png'; | ||||
| import modules from "./*.json"; | ||||
| 
 | ||||
| jQuery("document").ready(function() { | ||||
|     jQuery(".mailselect").select2({ | ||||
| // TODO: conditionally (or lazy) load select2 and dataTable
 | ||||
| $('document').ready(function() { | ||||
| 
 | ||||
|     // intercept anchors with data-clicked attribute and open alternate location instead
 | ||||
|     $('[data-clicked]').click(function(e) { | ||||
|         e.preventDefault(); | ||||
|         window.location.href = $(this).data('clicked'); | ||||
|     }); | ||||
| 
 | ||||
|     // use post for language selection
 | ||||
|     $('#mailu-languages > a').click(function(e) { | ||||
|         e.preventDefault(); | ||||
|         $.post({ | ||||
|             url: $(this).attr('href'), | ||||
|             success: function() { | ||||
|                 window.location = window.location.href; | ||||
|             }, | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     // allow en-/disabling of inputs in fieldset with checkbox in legend
 | ||||
|     $('fieldset legend input[type=checkbox]').change(function() { | ||||
|         var fieldset = $(this).parents('fieldset'); | ||||
|         if (this.checked) { | ||||
|             fieldset.removeAttr('disabled'); | ||||
|             fieldset.find('input,textarea').not(this).removeAttr('disabled'); | ||||
|         } else { | ||||
|             fieldset.attr('disabled', ''); | ||||
|             fieldset.find('input,textarea').not(this).attr('disabled', ''); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // display of range input value
 | ||||
|     $('input[type=range]').each(function() { | ||||
|         var value_element = $('#'+this.id+'_value'); | ||||
|         if (value_element.length) { | ||||
|             value_element = $(value_element[0]); | ||||
|             var infinity = $(this).data('infinity'); | ||||
|             var step = $(this).attr('step'); | ||||
|             $(this).on('input', function() { | ||||
|                 var num = (infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2); | ||||
|                 if (num.endsWith('.00')) num = num.substr(0, num.length - 3); | ||||
|                 value_element.text(num); | ||||
|             }).trigger('input'); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // init select2
 | ||||
|     $('.mailselect').select2({ | ||||
|         tags: true, | ||||
|         tokenSeparators: [',', ' '] | ||||
|         tokenSeparators: [',', ' '], | ||||
|     }); | ||||
|     jQuery(".dataTable").DataTable({ | ||||
|         "responsive": true, | ||||
| 
 | ||||
|     // init dataTable
 | ||||
|     var d = $(document.documentElement); | ||||
|     $('.dataTable').DataTable({ | ||||
|         'responsive': true, | ||||
|         language: { | ||||
|             url: d.data('static') + d.attr('lang') + '.json', | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     // init clipboard.js
 | ||||
|     new ClipboardJS('.btn-clip'); | ||||
| 
 | ||||
|     // disable login if not possible
 | ||||
|     var l = $('#login_needs_https'); | ||||
|     if (l.length && window.location.protocol != 'https:') { | ||||
|         l.removeClass("d-none"); | ||||
|         $('form :input').prop('disabled', true); | ||||
|     } | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| After Width: | Height: | Size: 4.8 KiB | 
| @ -1,22 +1,24 @@ | ||||
| // jQuery
 | ||||
| import jQuery from 'jquery'; | ||||
| import 'admin-lte/plugins/select2/css/select2.css'; | ||||
| 
 | ||||
| // bootstrap
 | ||||
| // import 'bootstrap/less/bootstrap.less';
 | ||||
| // import 'bootstrap';
 | ||||
| 
 | ||||
| // FontAwesome
 | ||||
| import 'admin-lte/plugins/fontawesome-free/css/fontawesome.css'; | ||||
| import 'admin-lte/plugins/fontawesome-free/css/regular.css'; | ||||
| import 'admin-lte/plugins/fontawesome-free/css/solid.css'; | ||||
| 
 | ||||
| // AdminLTE
 | ||||
| import 'admin-lte/plugins/jquery/jquery.min.js'; | ||||
| import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js'; | ||||
| import 'admin-lte/build/scss/adminlte.scss'; | ||||
| import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.css'; | ||||
| import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.css'; | ||||
| import 'admin-lte/plugins/bootstrap/js/bootstrap.js'; | ||||
| import 'admin-lte/build/js/AdminLTE.js'; | ||||
| import 'admin-lte/build/js/Layout.js'; | ||||
| import 'admin-lte/build/js/ControlSidebar.js'; | ||||
| import 'admin-lte/build/js/PushMenu.js'; | ||||
| 
 | ||||
| // fontawesome plugin
 | ||||
| import 'admin-lte/plugins/fontawesome-free/css/all.min.css'; | ||||
| 
 | ||||
| // select2 plugin
 | ||||
| import 'admin-lte/plugins/select2/css/select2.min.css'; | ||||
| import 'admin-lte/plugins/select2/js/select2.min.js'; | ||||
| 
 | ||||
| // dataTables plugin
 | ||||
| import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css'; | ||||
| import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.min.css'; | ||||
| import 'admin-lte/plugins/datatables/jquery.dataTables.min.js'; | ||||
| import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js'; | ||||
| import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.min.js'; | ||||
| import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.min.js'; | ||||
| 
 | ||||
| // clipboard.js
 | ||||
| import 'clipboard/dist/clipboard.min.js'; | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| __all__ = [ | ||||
|     'auth', 'postfix', 'dovecot', 'fetch' | ||||
|     'auth', 'postfix', 'dovecot', 'fetch', 'rspamd' | ||||
| ] | ||||
|  | ||||
| @ -0,0 +1,27 @@ | ||||
| from mailu import models | ||||
| from mailu.internal import internal | ||||
| 
 | ||||
| import flask | ||||
| 
 | ||||
| def vault_error(*messages, status=404): | ||||
|     return flask.make_response(flask.jsonify({'errors':messages}), status) | ||||
| 
 | ||||
| # rspamd key format: | ||||
| # {"selectors":[{"pubkey":"...","domain":"...","valid_start":TS,"valid_end":TS,"key":"...","selector":"...","bits":...,"alg":"..."}]} | ||||
| 
 | ||||
| # hashicorp vault answer format: | ||||
| # {"request_id":"...","lease_id":"","renewable":false,"lease_duration":2764800,"data":{...see above...},"wrap_info":null,"warnings":null,"auth":null} | ||||
| 
 | ||||
| @internal.route("/rspamd/vault/v1/dkim/<domain_name>", methods=['GET']) | ||||
| def rspamd_dkim_key(domain_name): | ||||
|     selectors = [] | ||||
|     if domain := models.Domain.query.get(domain_name): | ||||
|         if key := domain.dkim_key: | ||||
|             selectors.append( | ||||
|                 { | ||||
|                     'domain'  : domain.name, | ||||
|                     'key'     : key.decode('utf8'), | ||||
|                     'selector': flask.current_app.config.get('DKIM_SELECTOR', 'dkim'), | ||||
|                 } | ||||
|             ) | ||||
|     return flask.jsonify({'data': {'selectors': selectors}}) | ||||
| @ -0,0 +1,5 @@ | ||||
| from flask import Blueprint | ||||
| 
 | ||||
| sso = Blueprint('sso', __name__, static_folder=None, template_folder='templates') | ||||
| 
 | ||||
| from mailu.sso.views import * | ||||
| @ -0,0 +1,11 @@ | ||||
| from wtforms import validators, fields | ||||
| from flask_babel import lazy_gettext as _ | ||||
| import flask_wtf | ||||
| 
 | ||||
| class LoginForm(flask_wtf.FlaskForm): | ||||
|     class Meta: | ||||
|         csrf = False | ||||
|     email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()]) | ||||
|     pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) | ||||
|     submitAdmin = fields.SubmitField(_('Sign in')) | ||||
|     submitWebmail = fields.SubmitField(_('Sign in')) | ||||
| @ -0,0 +1,86 @@ | ||||
| {%- import "macros.html" as macros %} | ||||
| {%- import "bootstrap/utils.html" as utils %} | ||||
| <!doctype html> | ||||
| <html lang="{{ session['language'] }}" data-static="/static/"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}"> | ||||
|     <meta http-equiv="x-ua-compatible" content="ie=edge"> | ||||
|     <title>Mailu-Admin | {{ config["SITENAME"] }}</title> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='vendor.css') }}"> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}"> | ||||
|   </head> | ||||
|   <body class="hold-transition sidebar-mini layout-fixed"> | ||||
|     <div class="wrapper"> | ||||
|       <nav class="main-header navbar navbar-expand navbar-white navbar-light"> | ||||
|         <ul class="navbar-nav"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|           {%- for page, url in path %} | ||||
|             {%- if loop.index > 1 %} | ||||
|             <i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i> | ||||
|             {%- endif %} | ||||
|             {%- if url %} | ||||
|             <a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a> | ||||
|             {%- else %} | ||||
|             <span class="nav-link d-inline-block">{{ page }}</span> | ||||
|             {%- endif %} | ||||
|           {%- endfor %} | ||||
|           </li> | ||||
|         </ul> | ||||
|         <ul class="navbar-nav ml-auto"> | ||||
|           <li class="nav-item dropdown"> | ||||
|             <a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false"> | ||||
|               <i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span> | ||||
|               <span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a> | ||||
|             <div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages"> | ||||
|             {%- for locale in config.translations.values() %} | ||||
|               <a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('sso.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a> | ||||
|             {%- endfor %} | ||||
|             </div> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </nav> | ||||
|       <aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4"> | ||||
|         <a class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}> | ||||
|           <img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else '/static/mailu.png' }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3"> | ||||
|           <span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span> | ||||
|         </a> | ||||
|         {%- include "sidebar_sso.html" %} | ||||
|       </aside> | ||||
|       <div class="content-wrapper text-sm"> | ||||
|         <section class="content-header"> | ||||
|           <div class="container-fluid"> | ||||
|             <div class="row mb-2"> | ||||
|               <div class="col-sm-6"> | ||||
|                 <h1 class="m-0">{%- block title %}{%- endblock %}</h1> | ||||
|                 <small>{% block subtitle %}{% endblock %}</small> | ||||
|               </div> | ||||
|               <div class="col-sm-6"> | ||||
|                 {%- block main_action %}{%- endblock %} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </section> | ||||
|         <div class="content"> | ||||
|           {{ utils.flashed_messages(container=False, default_category='success') }} | ||||
|           {%- block content %}{%- endblock %} | ||||
|         </div> | ||||
|       </div> | ||||
|       <footer class="main-footer"> | ||||
|         Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span> | ||||
|         using <a href="https://flask.palletsprojects.com/">Flask</a> | ||||
|         and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>. | ||||
|         <span class="fa-pull-right"> | ||||
|           <i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span> | ||||
|           on <a href="https://github.com/Mailu/Mailu">Github</a> | ||||
|         </span> | ||||
|       </footer> | ||||
|     </div> | ||||
|     <script src="{{ url_for('static', filename='vendor.js') }}"></script> | ||||
|     <script src="{{ url_for('static', filename='app.js') }}"></script> | ||||
|   </body> | ||||
| </html> | ||||
| @ -0,0 +1,11 @@ | ||||
| {%- extends "base_sso.html" %} | ||||
| 
 | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ macros.form_field(form.email) }} | ||||
|   {{ macros.form_field(form.pw) }} | ||||
|   {{ macros.form_fields(fields, label=False, class="btn btn-default") }} | ||||
| </form> | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
| @ -0,0 +1,5 @@ | ||||
| {%- extends "form_sso.html" %} | ||||
| 
 | ||||
| {%- block title %} | ||||
| {% trans %}Sign in{% endtrans %} | ||||
| {%- endblock %} | ||||
| @ -0,0 +1,55 @@ | ||||
| <div class="sidebar text-sm"> | ||||
|   <nav class="mt-2"> | ||||
|     <ul class="nav nav-pills nav-sidebar flex-column" role="menu"> | ||||
|     <li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li> | ||||
|     {%- if config['ADMIN'] %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('ui.client') }}" class="nav-link"> | ||||
|           <i class="nav-icon fa fa-laptop"></i> | ||||
|           <p class="text">{% trans %}Client setup{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|     {%- endif %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer"> | ||||
|           <i class="nav-icon fa fa-globe"></i> | ||||
|           <p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-life-ring"></i> | ||||
|           <p class="text">{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p> | ||||
|         </a> | ||||
|       </li> | ||||
|     {#- | ||||
|       Domain self-registration is only available when | ||||
|        - Admin is available | ||||
|        - Domain Self-registration is enabled | ||||
|        - The current user is not logged on | ||||
|     #} | ||||
|     {%- if config['DOMAIN_REGISTRATION'] and not current_user.is_authenticated and config['ADMIN'] %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('ui.domain_signup') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-plus-square"></i> | ||||
|           <p class="text">{% trans %}Register a domain{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|     {%- endif %} | ||||
|     {#- | ||||
|       User self-registration is only available when | ||||
|        - Admin is available | ||||
|        - Self-registration is enabled | ||||
|        - The current user is not logged on | ||||
|     #} | ||||
|     {%- if not current_user.is_authenticated and signup_domains and config['ADMIN'] %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('ui.user_signup') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-user-plus"></i> | ||||
|           <p class="text">{% trans %}Sign up{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|     {%- endif %} | ||||
|     </ul> | ||||
|   </nav> | ||||
| </div> | ||||
| @ -0,0 +1,3 @@ | ||||
| __all__ = [ | ||||
|     'base', 'languages' | ||||
| ] | ||||
| @ -0,0 +1,57 @@ | ||||
| from werkzeug.utils import redirect | ||||
| from mailu import models, utils | ||||
| from mailu.sso import sso, forms | ||||
| from mailu.ui import access | ||||
| 
 | ||||
| from flask import current_app as app | ||||
| import flask | ||||
| import flask_login | ||||
| 
 | ||||
| @sso.route('/login', methods=['GET', 'POST']) | ||||
| def login(): | ||||
|     client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) | ||||
|     form = forms.LoginForm() | ||||
|     form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin' | ||||
|     form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail' | ||||
| 
 | ||||
|     fields = [] | ||||
|     if str(app.config["ADMIN"]).upper() != "FALSE": | ||||
|         fields.append(form.submitAdmin) | ||||
|     if str(app.config["WEBMAIL"]).upper() != "NONE": | ||||
|         fields.append(form.submitWebmail) | ||||
|     fields = [fields] | ||||
| 
 | ||||
|     if form.validate_on_submit(): | ||||
|         if form.submitAdmin.data: | ||||
|             destination = app.config['WEB_ADMIN'] | ||||
|         elif form.submitWebmail.data: | ||||
|             destination = app.config['WEB_WEBMAIL'] | ||||
|         device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit')) | ||||
|         username = form.email.data | ||||
|         if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip): | ||||
|             flask.flash('Too many attempts from your IP (rate-limit)', 'error') | ||||
|             return flask.render_template('login.html', form=form) | ||||
|         if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username): | ||||
|             flask.flash('Too many attempts for this user (rate-limit)', 'error') | ||||
|             return flask.render_template('login.html', form=form) | ||||
|         user = models.User.login(username, form.pw.data) | ||||
|         if user: | ||||
|             flask.session.regenerate() | ||||
|             flask_login.login_user(user) | ||||
|             response = flask.redirect(destination) | ||||
|             response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True) | ||||
|             flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.') | ||||
|             return response | ||||
|         else: | ||||
|             utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip) | ||||
|             flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.') | ||||
|             flask.flash('Wrong e-mail or password', 'error') | ||||
|     return flask.render_template('login.html', form=form, fields=fields) | ||||
| 
 | ||||
| @sso.route('/logout', methods=['GET']) | ||||
| @access.authenticated | ||||
| def logout(): | ||||
|     flask_login.logout_user() | ||||
|     flask.session.destroy() | ||||
|     return flask.redirect(flask.url_for('.login')) | ||||
| 
 | ||||
| @ -0,0 +1,7 @@ | ||||
| from mailu.sso import sso | ||||
| import flask | ||||
| 
 | ||||
| @sso.route('/language/<language>', methods=['POST']) | ||||
| def set_language(language=None): | ||||
|     flask.session['language'] = language | ||||
|     return flask.Response(status=200) | ||||
| @ -1,6 +1,6 @@ | ||||
| from flask import Blueprint | ||||
| 
 | ||||
| 
 | ||||
| ui = Blueprint('ui', __name__, static_folder='static', template_folder='templates') | ||||
| ui = Blueprint('ui', __name__, static_folder=None, template_folder='templates') | ||||
| 
 | ||||
| from mailu.ui.views import * | ||||
|  | ||||
| @ -1,15 +1,15 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Add a global administrator{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.admin, class_='mailselect') }} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "alias/create.html" %} | ||||
| {%- extends "alias/create.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Edit alias{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ alias }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "form.html" %} | ||||
| {%- extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Create alternative domain{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,36 +1,38 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Alternative domain list{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain.name }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block main_action %} | ||||
| {%- block main_action %} | ||||
| <a class="btn btn-primary float-right" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.table() %} | ||||
| {%- block content %} | ||||
| {%- call macros.table() %} | ||||
| <thead> | ||||
|   <tr> | ||||
|     <th>{% trans %}Actions{% endtrans %}</th> | ||||
|     <th>{% trans %}Name{% endtrans %}</th> | ||||
|     <th>{% trans %}Created{% endtrans %}</th> | ||||
|     <th>{% trans %}Last edit{% endtrans %}</th> | ||||
|   </tr> | ||||
| </thead> | ||||
| <tbody> | ||||
|   {% for alternative in domain.alternatives %} | ||||
|   {%- for alternative in domain.alternatives %} | ||||
|   <tr> | ||||
|     <td> | ||||
|       <a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> | ||||
|     </td> | ||||
|     <td>{{ alternative }}</td> | ||||
|     <td>{{ alternative.created_at }}</td> | ||||
|     <td>{{ alternative.created_at | format_date }}</td> | ||||
|     <td>{{ alternative.updated_at | format_date }}</td> | ||||
|   </tr> | ||||
|   {% endfor %} | ||||
|   {%- endfor %} | ||||
| </tbody> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,16 +1,16 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Public announcement{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.announcement_subject) }} | ||||
|   {{ macros.form_field(form.announcement_body, rows=10) }} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -0,0 +1,15 @@ | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {%- block title %} | ||||
| {% trans %}Antispam{% endtrans %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {%- block subtitle %} | ||||
| {% trans %}RSPAMD status page{% endtrans %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {%- block content %} | ||||
| <div class="embed-responsive embed-responsive-1by1"> | ||||
|   <iframe class="embed-responsive-item" src="{{ config["WEB_ADMIN"] }}/antispam/"></iframe> | ||||
| </div> | ||||
| {%- endblock %} | ||||
| @ -1,68 +1,86 @@ | ||||
| {% import "macros.html" as macros %} | ||||
| {% import "bootstrap/utils.html" as utils %} | ||||
| {%- import "macros.html" as macros %} | ||||
| {%- import "bootstrap/utils.html" as utils %} | ||||
| <!doctype html> | ||||
| <html> | ||||
| <html lang="{{ session['language'] }}" data-static="/static/"> | ||||
|   <head> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <link rel="stylesheet" href="{{ url_for('.static', filename='vendor.css') }}"> | ||||
|     <link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}"> | ||||
|     <title>Mailu-Admin - {{ config["SITENAME"] }}</title> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}"> | ||||
|     <meta http-equiv="x-ua-compatible" content="ie=edge"> | ||||
|     <title>Mailu-Admin | {{ config["SITENAME"] }}</title> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='vendor.css') }}"> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}"> | ||||
|   </head> | ||||
|   <body class="hold-transition sidebar-mini layout-fixed"> | ||||
|     <div class="wrapper"> | ||||
|       <nav class="main-header navbar navbar-expand navbar-white navbar-light"> | ||||
|         <ul class="navbar-nav"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a> | ||||
|             <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|           {%- for page, url in path %} | ||||
|             {%- if loop.index > 1 %} | ||||
|             <i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i> | ||||
|             {%- endif %} | ||||
|             {%- if url %} | ||||
|             <a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a> | ||||
|             {%- else %} | ||||
|             <span class="nav-link d-inline-block">{{ page }}</span> | ||||
|             {%- endif %} | ||||
|           {%- endfor %} | ||||
|           </li> | ||||
|         </ul> | ||||
|         <ul class="navbar-nav ml-auto"> | ||||
|           <li class="nav-item dropdown"> | ||||
|             <a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">{{ session['language'] }}</a> | ||||
|             <div class="dropdown-menu dropdown-menu-right p-0"> | ||||
|                 {% for language in session['available_languages'] %} | ||||
|                     <a class="dropdown-item {% if language == session['language'] %}active{% endif %} " href="{{ url_for('.set_language', language=language) }}">{{ language }}</a> | ||||
|                 {% endfor %} | ||||
|             <a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false"> | ||||
|               <i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span> | ||||
|               <span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a> | ||||
|             <div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages"> | ||||
|             {%- for locale in config.translations.values() %} | ||||
|               <a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a> | ||||
|             {%- endfor %} | ||||
|             </div> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </nav> | ||||
|       <aside class="main-sidebar sidebar-dark-primary"> | ||||
|         <a href="{{ config["WEB_ADMIN"] }}" class="brand-link"> | ||||
|       <aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4"> | ||||
|         <a href="{{ url_for('.domain_list' if current_user.manager_of or current_user.global_admin else '.user_settings') }}" class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}> | ||||
|           <img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else url_for('static', filename='mailu.png') }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3"> | ||||
|           <span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span> | ||||
|         </a> | ||||
|         {% block sidebar %} | ||||
|         {% include "sidebar.html" %} | ||||
|         {% endblock %} | ||||
|         {%- include "sidebar.html" %} | ||||
|       </aside> | ||||
|       <div class="content-wrapper"> | ||||
|       <div class="content-wrapper text-sm"> | ||||
|         <section class="content-header"> | ||||
|           <div class="container-fluid"> | ||||
|             <div class="row mb-2"> | ||||
|               <div class="col-sm-6"> | ||||
|                 <h1 class="m-0">{% block title %}{% endblock %}</h1> | ||||
|                 <h1 class="m-0">{%- block title %}{%- endblock %}</h1> | ||||
|                 <small>{% block subtitle %}{% endblock %}</small> | ||||
|               </div> | ||||
|               <div class="col-sm-6"> | ||||
|                 {% block main_action %} | ||||
|                 {% endblock %} | ||||
|                 {%- block main_action %}{%- endblock %} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </section> | ||||
| 
 | ||||
|         <div class="content"> | ||||
|           {{ utils.flashed_messages(container=False) }} | ||||
|           {% block content %}{% endblock %} | ||||
|           {{ utils.flashed_messages(container=False, default_category='success') }} | ||||
|           {%- block content %}{%- endblock %} | ||||
|         </div> | ||||
|       </div> | ||||
|       <footer class="main-footer"> | ||||
|         Built with <i class="fa fa-heart"></i> using <a class="white-text" href="http://flask.pocoo.org/">Flask</a> and | ||||
|         <a class="white-text" href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a> | ||||
|         <span class="pull-right"><i class="fa fa-code-fork"></i>on <a class="white-text" href="https://github.com/Mailu/Mailu">Github</a></a></span> | ||||
|         Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span> | ||||
|         using <a href="https://flask.palletsprojects.com/">Flask</a> | ||||
|         and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>. | ||||
|         <span class="fa-pull-right"> | ||||
|           <i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span> | ||||
|           on <a href="https://github.com/Mailu/Mailu">Github</a> | ||||
|         </span> | ||||
|       </footer> | ||||
|     </div> | ||||
|     <script src="{{ url_for('.static', filename='vendor.js') }}"></script> | ||||
|     <script src="{{ url_for('.static', filename='app.js') }}"></script> | ||||
|     <script src="{{ url_for('static', filename='vendor.js') }}"></script> | ||||
|     <script src="{{ url_for('static', filename='app.js') }}"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| @ -1,16 +1,16 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Confirm action{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ action }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card(theme="warning") %} | ||||
| {%- block content %} | ||||
| {%- call macros.card(theme="warning") %} | ||||
| <p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p> | ||||
| {{ macros.form(form) }} | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Docker error{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ action }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p> | ||||
| <pre>{{ error }}</pre> | ||||
| {% endblock %} | ||||
| <pre class="pre-config border bg-light">{{ error }}</pre> | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,21 +1,20 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}New domain{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.name) }} | ||||
|   {{ macros.form_fields((form.max_users, form.max_aliases)) }} | ||||
|   {{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000, | ||||
|       prepend='<span class="input-group-text"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>', | ||||
|       oninput='$("#quota").text(this.value == 0  ? "∞" : this.value/1000000000);') }} | ||||
|   {{ macros.form_field(form.max_quota_bytes, step=10**9, max=50*10**9, data_infinity="true", | ||||
|       prepend='<span class="input-group-text"><span id="max_quota_bytes_value"></span> GB</span>') }} | ||||
|   {{ macros.form_field(form.signup_enabled) }} | ||||
|   {{ macros.form_field(form.comment) }} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,66 +1,71 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Domain details{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain.name }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block main_action %} | ||||
| {% if current_user.global_admin %} | ||||
| {%- block main_action %} | ||||
| {%- if current_user.global_admin %} | ||||
| <a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}"> | ||||
|   {% if domain.dkim_publickey %} | ||||
|   {%- if domain.dkim_publickey %} | ||||
|   {% trans %}Regenerate keys{% endtrans %} | ||||
|   {% else %} | ||||
|   {%- else %} | ||||
|   {% trans %}Generate keys{% endtrans %} | ||||
|   {% endif %} | ||||
|   {%- endif %} | ||||
| </a> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
| {%- endif %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.table(datatable=False) %} | ||||
| {% set hostname = config["HOSTNAMES"].split(",")[0] %} | ||||
| {%- block content %} | ||||
| {%- call macros.table(datatable=False) %} | ||||
| <tr> | ||||
|   <th>{% trans %}Domain name{% endtrans %}</th> | ||||
|   <td>{{ domain.name }}</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle' if domain.check_mx() else 'fa-exclamation-circle' }}"></i></th> | ||||
|   <td><pre>{{ domain.name }}. 600 IN MX 10 {{ hostname }}.</pre></td> | ||||
|   <th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle text-success' if domain.check_mx() else 'fa-exclamation-circle text-danger' }}"></i></th> | ||||
|   <td>{{ macros.clip("dns_mx") }}<pre id="dns_mx" class="pre-config border bg-light">{{ domain.dns_mx }}</pre></td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <th>{% trans %}DNS SPF entries{% endtrans %}</th> | ||||
|   <td><pre> | ||||
| {{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ hostname }} -all"</pre></td> | ||||
|   <td>{{ macros.clip("dns_spf") }}<pre id="dns_spf" class="pre-config border bg-light">{{ domain.dns_spf }}</pre> | ||||
|   </td> | ||||
| </tr> | ||||
| {% if domain.dkim_publickey %} | ||||
| {%- if domain.dkim_publickey %} | ||||
| <tr> | ||||
|   <th>{% trans %}DKIM public key{% endtrans %}</th> | ||||
|   <td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ domain.dkim_publickey }}</pre></td> | ||||
|   <td>{{ macros.clip("dkim_key") }}<pre id="dkim_key" class="pre-config border bg-light">{{ domain.dkim_publickey }}</pre></td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <th>{% trans %}DNS DKIM entry{% endtrans %}</th> | ||||
|   <td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ config["DKIM_SELECTOR"] }}._domainkey.{{ domain.name }}. 600 IN TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"</pre></td> | ||||
|   <td>{{ macros.clip("dns_dkim") }}<pre id="dns_dkim" class="pre-config border bg-light">{{ domain.dns_dkim }}</pre></td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <th>{% trans %}DNS DMARC entry{% endtrans %}</th> | ||||
|   <td><pre>_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject;{% if config["DMARC_RUA"] %} rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }};{% endif %}{% if config["DMARC_RUF"] %} ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }};{% endif %} adkim=s; aspf=s"</pre></td> | ||||
|   <td> | ||||
|     {{ macros.clip("dns_dmarc") }}<pre id="dns_dmarc" class="pre-config border bg-light">{{ domain.dns_dmarc }}</pre> | ||||
|     {{ macros.clip("dns_dmarc_report") }}<pre id="dns_dmarc_report" class="pre-config border bg-light">{{ domain.dns_dmarc_report }}</pre> | ||||
|   </td> | ||||
| </tr> | ||||
| {% endif %} | ||||
| {%- endif %} | ||||
| {%- set tlsa_record=domain.dns_tlsa %} | ||||
| {%- if tlsa_record %} | ||||
| <tr> | ||||
|   <th>{% trans %}DNS TLSA entry{% endtrans %}</br><span class="text-secondary text-xs font-weight-normal">Let's Encrypt</br>ISRG Root X1</span></th> | ||||
|   <td>{{ macros.clip("dns_tlsa") }}<pre id="dns_tlsa" class="pre-config border bg-light">{{ tlsa_record }}</pre></td> | ||||
| </tr> | ||||
| {%- endif %} | ||||
| <tr> | ||||
|   <th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th> | ||||
|   <td> | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_submission._tcp.{{ domain.name }}. 600 IN SRV 1 1 587 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_imap._tcp.{{ domain.name }}. 600 IN SRV 100 1 143 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_pop3._tcp.{{ domain.name }}. 600 IN SRV 100 1 110 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
| {% if config["TLS_FLAVOR"] != "notls" %} | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_submissions._tcp.{{ domain.name }}. 600 IN SRV 10 1 465 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_imaps._tcp.{{ domain.name }}. 600 IN SRV 10 1 993 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
|     <pre style="white-space: pre-wrap; word-wrap: break-word;">_pop3s._tcp.{{ domain.name }}. 600 IN SRV 10 1 995 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> | ||||
| {% endif %}</td> | ||||
|   <td>{{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light"> | ||||
| {%- for line in domain.dns_autoconfig %} | ||||
| {{ line }} | ||||
| {%- endfor -%} | ||||
|   </pre></td> | ||||
| </tr> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "domain/create.html" %} | ||||
| {%- extends "domain/create.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Edit domain{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,31 +1,31 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Add a fetched account{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {% call macros.card(title="Remote server") %} | ||||
|   {%- call macros.card(title="Remote server") %} | ||||
|   {{ macros.form_field(form.protocol) }} | ||||
|   {{ macros.form_fields((form.host, form.port)) }} | ||||
|   {{ macros.form_field(form.tls) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {% call macros.card(title="Authentication") %} | ||||
|   {%- call macros.card(title="Authentication") %} | ||||
|   {{ macros.form_field(form.username) }} | ||||
|   {{ macros.form_field(form.password) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {% call macros.card(title="Settings") %} | ||||
|   {%- call macros.card(title="Settings") %} | ||||
|   {{ macros.form_field(form.keep) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "fetch/create.html" %} | ||||
| {%- extends "fetch/create.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Update a fetched account{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| {{ macros.form(form) }} | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| {% extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {% trans %}Sign in{% endtrans %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {% trans %}to access the administration tools{% endtrans %} | ||||
| {% endblock %} | ||||
| @ -1,104 +1,133 @@ | ||||
| {% macro form_errors(form) %} | ||||
|   {% if form.errors %} | ||||
|     {% for fieldname, errors in form.errors.items() %} | ||||
|       {% if bootstrap_is_hidden_field(form[fieldname]) %} | ||||
|         {% for error in errors %} | ||||
| {%- macro form_errors(form) %} | ||||
|   {%- if form.errors %} | ||||
|     {%- for fieldname, errors in form.errors.items() %} | ||||
|       {%- if bootstrap_is_hidden_field(form[fieldname]) %} | ||||
|         {%- for error in errors %} | ||||
|           <p class="error">{{error}}</p> | ||||
|         {% endfor %} | ||||
|       {% endif %} | ||||
|     {% endfor %} | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
|         {%- endfor %} | ||||
|       {%- endif %} | ||||
|     {%- endfor %} | ||||
|   {%- endif %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro form_field_errors(field) %} | ||||
|   {% if field.errors %} | ||||
|     {% for error in field.errors %} | ||||
| {%- macro form_field_errors(field) %} | ||||
|   {%- if field.errors %} | ||||
|     {%- for error in field.errors %} | ||||
|       <p class="help-block inline">{{ error }}</p> | ||||
|     {% endfor %} | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
|     {%- endfor %} | ||||
|   {%- endif %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro form_fields(fields, prepend='', append='', label=True) %} | ||||
|   {% set width = (12 / fields|length)|int %} | ||||
| {%- macro form_fields(fields, prepend='', append='', label=True) %} | ||||
|   {%- set width = (12 / fields|length)|int %} | ||||
|   <div class="form-group"> | ||||
|     <div class="row"> | ||||
|       {% for field in fields %} | ||||
|       {%- for field in fields %} | ||||
|       <div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}"> | ||||
|         {%- if field.__class__.__name__ == 'list' %} | ||||
|           {%- for subfield in field %} | ||||
|             {{ form_individual_field(subfield, prepend=prepend, append=append, label=label, **kwargs) }} | ||||
|           {%- endfor %} | ||||
|         {%- else %} | ||||
|           {{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }} | ||||
|         {%- endif %} | ||||
|       </div> | ||||
|       {% endfor %} | ||||
|       {%- endfor %} | ||||
|     </div> | ||||
|   </div> | ||||
| {% endmacro %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro form_individual_field(field, prepend='', append='', label=True, class_="") %} | ||||
|   {% if field.type == "BooleanField" %} | ||||
|     {{ field(**kwargs) }}<span>  </span> | ||||
|     {{ field.label if label else '' }} | ||||
|   {% else %} | ||||
| {%- macro form_individual_field(field, prepend='', append='', label=True, class_="") %} | ||||
|   {%- if field.type == "BooleanField" %} | ||||
|     {{ field(**kwargs) }}<span>  </span>{{ field.label if label else '' }} | ||||
|   {%- else %} | ||||
|     {{ field.label if label else '' }}{{ form_field_errors(field) }} | ||||
|       {% if prepend %}<div class="input-group-prepend">{% endif %} | ||||
|       {% if append %}<div class="input-group-append">{% endif %} | ||||
|         {{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }} | ||||
|       {% if prepend or append %}</div>{% endif %} | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
|     {%- if prepend %}<div class="input-group-prepend">{%- elif append %}<div class="input-group-append">{%- endif %} | ||||
|       {{ prepend|safe }}{{ field(class_=("form-control " + class_) if class_ else "form-control", **kwargs) }}{{ append|safe }} | ||||
|     {%- if prepend or append %}</div>{%- endif %} | ||||
|   {%- endif %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro form_field(field) %} | ||||
|   {% if field.type == 'SubmitField' %} | ||||
|   {{ form_fields((field,), label=False, class="btn btn-default", **kwargs) }} | ||||
|   {% else %} | ||||
|   {{ form_fields((field,), **kwargs) }} | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
| {%- macro form_field(field) %} | ||||
|   {%- if field.type == 'SubmitField' %} | ||||
|   {{- form_fields((field,), label=False, class="btn btn-default", **kwargs) }} | ||||
|   {%- else %} | ||||
|   {{- form_fields((field,), **kwargs) }} | ||||
|   {%- endif %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro form(form) %} | ||||
| {%- macro form(form) %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {% for field in form %} | ||||
|     {% if  bootstrap_is_hidden_field(field) %} | ||||
|   {%- for field in form %} | ||||
|     {%- if bootstrap_is_hidden_field(field) %} | ||||
|       {{ field() }} | ||||
|     {% else %} | ||||
|     {%- else %} | ||||
|       {{ form_field(field) }} | ||||
|     {% endif %} | ||||
|   {% endfor %} | ||||
|     {%- endif %} | ||||
|   {%- endfor %} | ||||
| </form> | ||||
| {% endmacro %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro card(title=None, theme="primary", header=True) %} | ||||
| {%- macro card(title=None, theme="primary", header=True) %} | ||||
| <div class="row"> | ||||
|   <div class="col-lg-12"> | ||||
|     <div class="card card-outline card-{{ theme }}"> | ||||
|       {% if header %} | ||||
|       {%- if header %} | ||||
|       <div class="card-header border-0"> | ||||
|         {% if title %} | ||||
|         {%- if title %} | ||||
|         <h3 class="card-title">{{ title }}</h3> | ||||
|         {% endif %} | ||||
|         {%- endif %} | ||||
|       </div> | ||||
|       {% endif %} | ||||
|       {%- endif %} | ||||
|       <div class="card-body"> | ||||
|        {{ caller() }} | ||||
|        {{- caller() }} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| {% endmacro %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {% macro table(title=None, theme="primary", datatable=True) %} | ||||
| {%- macro table(title=None, theme="primary", datatable=True) %} | ||||
| <div class="row"> | ||||
|   <div class="col-lg-12"> | ||||
|     <div class="card card-outline card-{{ theme }}"> | ||||
|       {%- if title %} | ||||
|       <div class="card-header border-0"> | ||||
|         {% if title %} | ||||
|         <h3 class="card-title">{{ title }}</h3> | ||||
|         {% endif %} | ||||
|       </div> | ||||
|       {%- endif %} | ||||
|       <div class="card-body"> | ||||
|         <table class="table table-bordered{% if datatable %} dataTable{% endif %}"> | ||||
|           {{ caller() }} | ||||
|           {{- caller() }} | ||||
|         </table> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| {% endmacro %} | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {%- macro fieldset(title=None, field=None, enabled=None, fields=None) %} | ||||
| {%- if field or title %} | ||||
| <fieldset{% if not enabled %} disabled{% endif %}> | ||||
| {%- if field %} | ||||
| <legend>{{ form_individual_field(field) }}</legend> | ||||
| {%- else %} | ||||
| <legend>{{ title }}</legend> | ||||
| {%- endif %} | ||||
| {%- endif %} | ||||
| {{- caller() }} | ||||
| {%- if fields %} | ||||
|   {%- set kwargs = {"enabled" if enabled else "disabled": ""} %} | ||||
|   {%- for field in fields %} | ||||
|     {{ form_field(field, **kwargs) }} | ||||
|   {%- endfor %} | ||||
| {%- endif %} | ||||
| </fieldset> | ||||
| {%- endmacro %} | ||||
| 
 | ||||
| {%- macro clip(target, title=_("copy to clipboard"), icon="copy", color="primary", action="copy") %} | ||||
| <button class="btn btn-{{ color }} btn-xs btn-clip float-right ml-2 mt-1" data-clipboard-action="{{ action }}" data-clipboard-target="#{{ target }}"> | ||||
| <i class="fas fa-{{ icon }}" title="{{ title }}" aria-expanded="false"></i><span class="sr-only">{{ title }}</span> | ||||
| </button> | ||||
| {%- endmacro %} | ||||
|  | ||||
| @ -1,19 +1,19 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Add a manager{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.manager, class_='mailselect') }} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| {% extends "form.html" %} | ||||
| {%- extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}New relay domain{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "form.html" %} | ||||
| {%- extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Edit relayd domain{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ relay }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,144 +1,156 @@ | ||||
| <div class="sidebar"> | ||||
|   {% if current_user.is_authenticated %} | ||||
| <div class="sidebar text-sm"> | ||||
|   {%- if current_user.is_authenticated %} | ||||
|   <div class="user-panel mt-3 pb-3 mb-3 d-flex"> | ||||
|     <div class="image"> | ||||
|       <div class="div-circle elevation-2"><i class="fa fa-user text-lg text-dark"></i></div> | ||||
|     </div> | ||||
|     <div class="info"> | ||||
|       <span class="text-center text-primary">{{ current_user }}</span> | ||||
|       <a href="{{ url_for('.user_settings') }}" class="d-block">{{ current_user }}</a> | ||||
|     </div> | ||||
|   </div> | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {%- endif %} | ||||
|   <nav class="mt-2"> | ||||
|     <ul class="nav nav-pills nav-sidebar flex-column" role="menu"> | ||||
|       {% if current_user.is_authenticated %} | ||||
|       <li class="nav-header">{% trans %}My account{% endtrans %}</li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.user_settings') }}" class="nav-link"> | ||||
|       {%- if current_user.is_authenticated %} | ||||
|       <li class="nav-header text-uppercase text-primary" role="none">{% trans %}My account{% endtrans %}</li> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.user_settings') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-wrench"></i> | ||||
|           <p class="text">{% trans %}Settings{% endtrans %}</p> | ||||
|           <p>{% trans %}Settings{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.user_password') }}" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-lock"></i> | ||||
|           <p class="text">{% trans %}Update password{% endtrans %}</p> | ||||
|           <p>{% trans %}Update password{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.user_reply') }}" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.user_reply') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-plane"></i> | ||||
|           <p class="text">{% trans %}Auto-reply{% endtrans %}</p> | ||||
|           <p>{% trans %}Auto-reply{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.fetch_list') }}" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.fetch_list') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fas fa-download"></i> | ||||
|           <p class="text">{% trans %}Fetched accounts{% endtrans %}</p> | ||||
|           <p>{% trans %}Fetched accounts{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.token_list') }}" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.token_list') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fas fa-ticket-alt"></i> | ||||
|           <p class="text">{% trans %}Authentication tokens{% endtrans %}</p> | ||||
|           <p>{% trans %}Authentication tokens{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
| 
 | ||||
|       {% if current_user.manager_of or current_user.global_admin %} | ||||
|       <li class="nav-header">{% trans %}Administration{% endtrans %}</li> | ||||
|       {% endif %} | ||||
|       {% if current_user.global_admin %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.announcement') }}" class="nav-link"> | ||||
|           <i class="nav-icon fa fa-bullhorn"></i> | ||||
|           <p class="text">{% trans %}Announcement{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.admin_list') }}" class="nav-link"> | ||||
|           <i class="nav-icon fa fa-user"></i> | ||||
|           <p class="text">{% trans %}Administrators{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.relay_list') }}" class="nav-link"> | ||||
|           <i class="nav-icon fa fa-reply-all"></i> | ||||
|           <p class="text">{% trans %}Relayed domains{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ config["WEB_ADMIN"] }}/antispam/" target="_blank" class="nav-link"> | ||||
|         <i class="nav-icon fas fa-trash-alt"></i> | ||||
|         <p class="text">{% trans %}Antispam{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% endif %} | ||||
|       {% if current_user.manager_of or current_user.global_admin %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.domain_list') }}" class="nav-link"> | ||||
|           <i class="nav-icon fa fa-envelope"></i> | ||||
|           <p class="text">{% trans %}Mail domains{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% endif %} | ||||
|       {% endif %} | ||||
| 
 | ||||
|       <li class="nav-header">{% trans %}Go to{% endtrans %}</li> | ||||
|       {% if config["WEBMAIL"] != "none" %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link"> | ||||
|         <i class="nav-icon far fa-envelope"></i> | ||||
|         <p class="text">{% trans %}Webmail{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% endif %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.client') }}" class="nav-link"> | ||||
|       {%- if current_user.is_authenticated %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.client') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-laptop"></i> | ||||
|           <p class="text">{% trans %}Client setup{% endtrans %}</p> | ||||
|           <p>{% trans %}Client setup{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link"> | ||||
|       {%- endif %} | ||||
| 
 | ||||
|       {%- if current_user.manager_of or current_user.global_admin %} | ||||
|       <li class="nav-header text-uppercase text-primary" role="none">{% trans %}Administration{% endtrans %}</li> | ||||
|       {%- endif %} | ||||
|       {%- if current_user.global_admin %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.announcement') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-bullhorn"></i> | ||||
|           <p>{% trans %}Announcement{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.admin_list') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-user"></i> | ||||
|           <p>{% trans %}Administrators{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.relay_list') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-reply-all"></i> | ||||
|           <p>{% trans %}Relayed domains{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ config["WEB_ADMIN"] }}/antispam/" data-clicked="{{ url_for('.antispam') }}" target="_blank" class="nav-link" role="menuitem"> | ||||
|         <i class="nav-icon fas fa-trash-alt"></i> | ||||
|         <p>{% trans %}Antispam{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {%- endif %} | ||||
|       {%- if current_user.manager_of or current_user.global_admin %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.domain_list') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-envelope"></i> | ||||
|           <p>{% trans %}Mail domains{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {%- endif %} | ||||
|       {%- endif %} | ||||
| 
 | ||||
|       <li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li> | ||||
|       {%- if config["WEBMAIL"] != "none" and current_user.is_authenticated %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link" role="menuitem"> | ||||
|         <i class="nav-icon far fa-envelope"></i> | ||||
|         <p>{% trans %}Webmail{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {%- endif %} | ||||
|       {%- if not current_user.is_authenticated %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.client') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-laptop"></i> | ||||
|           <p>{% trans %}Client setup{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {%- endif %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer"> | ||||
|         <i class="nav-icon fa fa-globe"></i> | ||||
|         <p class="text">{% trans %}Website{% endtrans %}</p> | ||||
|         <p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p> | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="nav-item"> | ||||
|         <a href="https://mailu.io" target="_blank" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem" rel="noreferrer"> | ||||
|           <i class="nav-icon fa fa-life-ring"></i> | ||||
|           <p class="text">{% trans %}Help{% endtrans %}</p> | ||||
|           <p>{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% if config['DOMAIN_REGISTRATION'] %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.domain_signup') }}" class="nav-link"> | ||||
|       {%- if config['DOMAIN_REGISTRATION'] %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.domain_signup') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-plus-square"></i> | ||||
|           <p class="text">{% trans %}Register a domain{% endtrans %}</p> | ||||
|           <p>{% trans %}Register a domain{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% endif %} | ||||
|       {% if current_user.is_authenticated %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.logout') }}" class="nav-link"> | ||||
|       {%- endif %} | ||||
|       {%- if current_user.is_authenticated %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('sso.logout') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fas fa-sign-out-alt"></i> | ||||
|           <p class="text">{% trans %}Sign out{% endtrans %}</p> | ||||
|           <p>{% trans %}Sign out{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% else %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.login') }}" class="nav-link"> | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('sso.login') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fas fa-sign-in-alt"></i> | ||||
|           <p class="text">{% trans %}Sign in{% endtrans %}</p> | ||||
|           <p>{% trans %}Sign in{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% if signup_domains %} | ||||
|       <li class="nav-item"> | ||||
|         <a href="{{ url_for('.user_signup') }}" class="nav-link"> | ||||
|       {%- if signup_domains %} | ||||
|       <li class="nav-item" role="none"> | ||||
|         <a href="{{ url_for('.user_signup') }}" class="nav-link" role="menuitem"> | ||||
|           <i class="nav-icon fa fa-user-plus"></i> | ||||
|           <p class="text">{% trans %}Sign up{% endtrans %}</p> | ||||
|           <p>{% trans %}Sign up{% endtrans %}</p> | ||||
|         </a> | ||||
|       </li> | ||||
|       {% endif %} | ||||
|       {% endif %} | ||||
|       {%- endif %} | ||||
|       {%- endif %} | ||||
|     </ul> | ||||
|   </nav> | ||||
| </div> | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "form.html" %} | ||||
| {%- extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Create an authentication token{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,38 +1,40 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Authentication tokens{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block main_action %} | ||||
| {%- block main_action %} | ||||
| <a class="btn btn-primary float-right" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.table() %} | ||||
| {%- block content %} | ||||
| {%- call macros.table() %} | ||||
| <thead> | ||||
|   <tr> | ||||
|     <th>{% trans %}Actions{% endtrans %}</th> | ||||
|     <th>{% trans %}Comment{% endtrans %}</th> | ||||
|     <th>{% trans %}Authorized IP{% endtrans %}</th> | ||||
|     <th>{% trans %}Created{% endtrans %}</th> | ||||
|     <th>{% trans %}Last edit{% endtrans %}</th> | ||||
|   </tr> | ||||
| </thead> | ||||
| <tbody> | ||||
|   {% for token in user.tokens %} | ||||
|   {%- for token in user.tokens %} | ||||
|   <tr> | ||||
|     <td> | ||||
|       <a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> | ||||
|     </td> | ||||
|     <td>{{ token.comment }}</td> | ||||
|     <td>{{ token.ip or "any" }}</td> | ||||
|     <td>{{ token.created_at }}</td> | ||||
|     <td>{{ token.created_at | format_date }}</td> | ||||
|     <td>{{ token.updated_at | format_date }}</td> | ||||
|   </tr> | ||||
|   {% endfor %} | ||||
|   {%- endfor %} | ||||
| </tbody> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,33 +1,32 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}New user{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain.name }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
| 
 | ||||
|   {% call macros.card(_("General")) %} | ||||
|   {%- call macros.card(_("General")) %} | ||||
|   {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }} | ||||
|   {{ macros.form_fields((form.pw, form.pw2)) }} | ||||
|   {{ macros.form_field(form.displayed_name) }} | ||||
|   {{ macros.form_field(form.comment) }} | ||||
|   {{ macros.form_field(form.enabled) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {% call macros.card(_("Features and quotas"), theme="success") %} | ||||
|   {{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50000000000), | ||||
|       prepend='<span class="input-group-text"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>', | ||||
|       oninput='$("#quota").text(this.value == 0  ? "∞" : this.value/1000000000);') }} | ||||
|   {%- call macros.card(_("Features and quotas"), theme="success") %} | ||||
|   {{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50*10**9), data_infinity="true", | ||||
|       prepend='<span class="input-group-text"><span id="quota_bytes_value"></span> GB</span>') }} | ||||
|   {{ macros.form_field(form.enable_imap) }} | ||||
|   {{ macros.form_field(form.enable_pop) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "user/create.html" %} | ||||
| {%- extends "user/create.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Edit user{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,25 +0,0 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {% trans %}Forward emails{% endtrans %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.forward_enabled, | ||||
|       onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')} | ||||
|                 else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }} | ||||
|   {{ macros.form_field(form.forward_keep, | ||||
|       **{("enabled" if user.forward_enabled else "disabled"): ""}) }} | ||||
|   {{ macros.form_field(form.forward_destination, | ||||
|       **{("enabled" if user.forward_enabled else "disabled"): ""}) }} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "form.html" %} | ||||
| {%- extends "form.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Password update{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,30 +1,23 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Automatic reply{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.card() %} | ||||
| {%- block content %} | ||||
| {%- call macros.card() %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {{ macros.form_field(form.reply_enabled, | ||||
|       onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')} | ||||
|                 else{$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').attr('readonly', '')}") }} | ||||
|   {{ macros.form_field(form.reply_subject, | ||||
|       **{("rw" if user.reply_enabled else "readonly"): ""}) }} | ||||
|     {{ macros.form_field(form.reply_body, rows=10, | ||||
|         **{("rw" if user.reply_enabled else "readonly"): ""}) }} | ||||
|   {{ macros.form_field(form.reply_enddate, | ||||
|         **{("rw" if user.reply_enabled else "readonly"): ""}) }} | ||||
|   {{ macros.form_field(form.reply_startdate, | ||||
|         **{("rw" if user.reply_enabled else "readonly"): ""}) }} | ||||
| 
 | ||||
|   {%- call macros.fieldset( | ||||
|       field=form.reply_enabled, | ||||
|       enabled=user.reply_enabled, | ||||
|       fields=[form.reply_subject, form.reply_body, form.reply_enddate, form.reply_startdate]) %} | ||||
|   {%- endcall %} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,38 +1,36 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}User settings{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ user }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
| 
 | ||||
|   {% call macros.card(title=_("Displayed name")) %} | ||||
|   {%- call macros.card(title=_("Displayed name")) %} | ||||
|   {{ macros.form_field(form.displayed_name) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {% call macros.card(title=_("Antispam")) %} | ||||
|   {{ macros.form_field(form.spam_enabled) }} | ||||
|   {%- call macros.card(title=_("Antispam")) %} | ||||
|   {%- call macros.fieldset(field=form.spam_enabled, enabled=user.spam_enabled) %} | ||||
|   {{ macros.form_field(form.spam_threshold, step=1, max=100, | ||||
|       prepend='<span class="input-group-text"><span id="threshold">'+form.spam_threshold.data.__str__()+'</span> / 100</span>', | ||||
|       oninput='$("#threshold").text(this.value);') }} | ||||
|   {% endcall %} | ||||
|      prepend='<span class="input-group-text"><span id="spam_threshold_value"></span> / 100</span>') }} | ||||
|   {%- endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {% call macros.card(title=_("Auto-forward")) %} | ||||
|   {{ macros.form_field(form.forward_enabled, | ||||
|       onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')} | ||||
|                 else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }} | ||||
|   {{ macros.form_field(form.forward_keep, | ||||
|       **{("enabled" if user.forward_enabled else "disabled"): ""}) }} | ||||
|   {{ macros.form_field(form.forward_destination, | ||||
|       **{("enabled" if user.forward_enabled else "disabled"): ""}) }} | ||||
|   {% endcall %} | ||||
|   {%- call macros.card(title=_("Auto-forward")) %} | ||||
|   {%- call macros.fieldset( | ||||
|       field=form.forward_enabled, | ||||
|       enabled=user.forward_enabled, | ||||
|       fields=[form.forward_keep, form.forward_destination]) %} | ||||
|   {%- endcall %} | ||||
|   {%- endcall %} | ||||
| 
 | ||||
|   {{ macros.form_field(form.submit) }} | ||||
| </form> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,23 +1,23 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Sign up{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {{ domain }} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <form class="form" method="post" role="form"> | ||||
|   {{ form.hidden_tag() }} | ||||
|   {% call macros.card() %} | ||||
|   {%- call macros.card() %} | ||||
|   {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }} | ||||
|   {{ macros.form_fields((form.pw, form.pw2)) }} | ||||
|   {% if form.captcha %} | ||||
|   {%- if form.captcha %} | ||||
|     {{ macros.form_field(form.captcha) }} | ||||
|   {% endif %} | ||||
|   {%- endif %} | ||||
|   {{ macros.form_field(form.submit) }} | ||||
|   {% endcall %} | ||||
|   {%- endcall %} | ||||
| </form> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,26 +1,26 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block title %} | ||||
| {%- block title %} | ||||
| {% trans %}Sign up{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block subtitle %} | ||||
| {%- block subtitle %} | ||||
| {% trans %}pick a domain for the new account{% endtrans %} | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {% call macros.table() %} | ||||
| {%- block content %} | ||||
| {%- call macros.table() %} | ||||
| <tr> | ||||
|   <th>{% trans %}Domain{% endtrans %}</th> | ||||
|   <th>{% trans %}Available slots{% endtrans %}</th> | ||||
|   <th>{% trans %}Quota{% endtrans %}</th> | ||||
| </tr> | ||||
| {% for domain_name, domain in available_domains.items() %} | ||||
| {%- for domain_name, domain in available_domains.items() %} | ||||
| <tr> | ||||
|   <td><a href="{{ url_for('.user_signup', domain_name=domain_name) }}">{{ domain_name }}</a></td> | ||||
|   <td>{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td> | ||||
|   <td>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td> | ||||
| </tr> | ||||
| {% endfor %} | ||||
| {% endcall %} | ||||
| {% endblock %} | ||||
| {%- endfor %} | ||||
| {%- endcall %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| {% extends "base.html" %} | ||||
| {%- extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
| {%- block content %} | ||||
| <div class="alert alert-warning" role="alert">{% trans %}We are still working on this feature!{% endtrans %}</div> | ||||
| {% endblock %} | ||||
| {%- endblock %} | ||||
|  | ||||
| @ -1,56 +1,75 @@ | ||||
| alembic==1.0.10 | ||||
| asn1crypto==0.24.0 | ||||
| Babel==2.6.0 | ||||
| bcrypt==3.1.6 | ||||
| alembic==1.7.4 | ||||
| appdirs==1.4.4 | ||||
| Babel==2.9.1 | ||||
| bcrypt==3.2.0 | ||||
| blinker==1.4 | ||||
| cffi==1.12.3 | ||||
| Click==7.0 | ||||
| cryptography==3.4.7 | ||||
| decorator==4.4.0 | ||||
| dnspython==1.16.0 | ||||
| dominate==2.3.5 | ||||
| Flask==1.0.2 | ||||
| Flask-Babel==0.12.2 | ||||
| CacheControl==0.12.9 | ||||
| certifi==2021.10.8 | ||||
| cffi==1.15.0 | ||||
| chardet==4.0.0 | ||||
| click==8.0.3 | ||||
| colorama==0.4.4 | ||||
| contextlib2==21.6.0 | ||||
| cryptography==35.0.0 | ||||
| decorator==5.1.0 | ||||
| # distlib==0.3.1 | ||||
| # distro==1.5.0 | ||||
| dnspython==2.1.0 | ||||
| dominate==2.6.0 | ||||
| email-validator==1.1.3 | ||||
| Flask==2.0.2 | ||||
| Flask-Babel==2.0.0 | ||||
| Flask-Bootstrap==3.3.7.1 | ||||
| Flask-DebugToolbar==0.10.1 | ||||
| Flask-Limiter==1.0.1 | ||||
| Flask-Login==0.4.1 | ||||
| Flask-DebugToolbar==0.11.0 | ||||
| Flask-Limiter==1.4 | ||||
| Flask-Login==0.5.0 | ||||
| flask-marshmallow==0.14.0 | ||||
| Flask-Migrate==2.4.0 | ||||
| Flask-Migrate==3.1.0 | ||||
| Flask-Script==2.0.6 | ||||
| Flask-SQLAlchemy==2.4.0 | ||||
| Flask-WTF==0.14.2 | ||||
| gunicorn==19.9.0 | ||||
| idna==2.8 | ||||
| infinity==1.4 | ||||
| intervals==0.8.1 | ||||
| itsdangerous==1.1.0 | ||||
| Jinja2==2.11.3 | ||||
| limits==1.3 | ||||
| Mako==1.0.9 | ||||
| MarkupSafe==1.1.1 | ||||
| mysqlclient==1.4.2.post1 | ||||
| marshmallow==3.10.0 | ||||
| marshmallow-sqlalchemy==0.24.1 | ||||
| Flask-SQLAlchemy==2.5.1 | ||||
| Flask-WTF==0.15.1 | ||||
| greenlet==1.1.2 | ||||
| gunicorn==20.1.0 | ||||
| html5lib==1.1 | ||||
| idna==3.3 | ||||
| infinity==1.5 | ||||
| intervals==0.9.2 | ||||
| itsdangerous==2.0.1 | ||||
| Jinja2==3.0.2 | ||||
| limits==1.5.1 | ||||
| lockfile==0.12.2 | ||||
| Mako==1.1.5 | ||||
| MarkupSafe==2.0.1 | ||||
| marshmallow==3.14.0 | ||||
| marshmallow-sqlalchemy==0.26.1 | ||||
| msgpack==1.0.2 | ||||
| mysqlclient==2.0.3 | ||||
| ordered-set==4.0.2 | ||||
| # packaging==20.9 | ||||
| passlib==1.7.4 | ||||
| psycopg2==2.8.2 | ||||
| pycparser==2.19 | ||||
| Pygments==2.8.1 | ||||
| pyOpenSSL==20.0.1 | ||||
| python-dateutil==2.8.0 | ||||
| python-editor==1.0.4 | ||||
| pytz==2019.1 | ||||
| PyYAML==5.4.1 | ||||
| redis==3.2.1 | ||||
| #alpine3:12 provides six==1.15.0 | ||||
| #six==1.12.0 | ||||
| socrate==0.1.1 | ||||
| SQLAlchemy==1.3.3 | ||||
| # pep517==0.10.0 | ||||
| progress==1.6 | ||||
| psycopg2==2.9.1 | ||||
| pycparser==2.20 | ||||
| Pygments==2.10.0 | ||||
| pyOpenSSL==21.0.0 | ||||
| pyparsing==3.0.4 | ||||
| pytz==2021.3 | ||||
| PyYAML==6.0 | ||||
| redis==3.5.3 | ||||
| requests==2.26.0 | ||||
| retrying==1.3.3 | ||||
| # six==1.15.0 | ||||
| socrate==0.2.0 | ||||
| SQLAlchemy==1.4.26 | ||||
| srslib==0.1.4 | ||||
| tabulate==0.8.3 | ||||
| tenacity==5.0.4 | ||||
| validators==0.12.6 | ||||
| tabulate==0.8.9 | ||||
| tenacity==8.0.1 | ||||
| toml==0.10.2 | ||||
| urllib3==1.26.7 | ||||
| validators==0.18.2 | ||||
| visitor==0.1.3 | ||||
| Werkzeug==0.15.5 | ||||
| WTForms==2.2.1 | ||||
| WTForms-Components==0.10.4 | ||||
| webencodings==0.5.1 | ||||
| Werkzeug==2.0.2 | ||||
| WTForms==2.3.3 | ||||
| WTForms-Components==0.10.5 | ||||
|  | ||||
| @ -1,65 +1,76 @@ | ||||
| var path = require("path"); | ||||
| var webpack = require("webpack"); | ||||
| var css = require("mini-css-extract-plugin"); | ||||
| const path = require('path'); | ||||
| const webpack = require('webpack'); | ||||
| const css = require('mini-css-extract-plugin'); | ||||
| const mini = require('css-minimizer-webpack-plugin'); | ||||
| const terse = require('terser-webpack-plugin'); | ||||
| const compress = require('compression-webpack-plugin'); | ||||
| 
 | ||||
| module.exports = { | ||||
|     mode: "development", | ||||
|     mode: 'production', | ||||
|     entry: { | ||||
|         app: "./assets/app.js", | ||||
|         vendor: "./assets/vendor.js" | ||||
|         app: { | ||||
|             import: './assets/app.js', | ||||
|             dependOn: 'vendor', | ||||
|         }, | ||||
|         vendor: './assets/vendor.js', | ||||
|     }, | ||||
|     output: { | ||||
|         path: path.resolve(__dirname, "static/"), | ||||
|         filename: "[name].js" | ||||
|         path: path.resolve(__dirname, 'static/'), | ||||
|         filename: '[name].js', | ||||
|         assetModuleFilename: '[name][ext]', | ||||
|     }, | ||||
|     module: { | ||||
|         rules: [ | ||||
|             { | ||||
|                 test: /\.js$/, | ||||
|                 use: ['babel-loader'] | ||||
|                 use: ['babel-loader', 'import-glob'], | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.scss$/, | ||||
|                 use: [css.loader, 'css-loader', 'sass-loader'] | ||||
|                 test: /\.s?css$/i, | ||||
|                 use: [css.loader, 'css-loader', 'sass-loader'], | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.less$/, | ||||
|                 use: [css.loader, 'css-loader', 'less-loader'] | ||||
|                 test: /\.less$/i, | ||||
|                 use: [css.loader, 'css-loader', 'less-loader'], | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.css$/, | ||||
|                 use: [css.loader, 'css-loader'] | ||||
|                 test: /\.(json|png|svg|jpg|jpeg|gif)$/i, | ||||
|                 type: 'asset/resource', | ||||
|             }, | ||||
|             { | ||||
|                 // Exposes jQuery for use outside Webpack build
 | ||||
|                 test: require.resolve('jquery'), | ||||
|                 use: [{ | ||||
|                     loader: 'expose-loader', | ||||
|                     options: { | ||||
|                       exposes: [ | ||||
|                         { | ||||
|                           globalName: '$', | ||||
|                           override: true, | ||||
|                         }, | ||||
|                         { | ||||
|                           globalName: 'jQuery', | ||||
|                           override: true, | ||||
|                         }, | ||||
|                       ]  | ||||
|                     },                | ||||
|                 }] | ||||
|             } | ||||
|         ] | ||||
|         ], | ||||
|     }, | ||||
|     plugins: [ | ||||
|         new css({ | ||||
| 	          filename: "[name].css", | ||||
| 	          chunkFilename: "[id].css" | ||||
|             filename: '[name].css', | ||||
|             chunkFilename: '[id].css', | ||||
|         }), | ||||
|         new webpack.ProvidePlugin({ | ||||
|             $: "jquery", | ||||
|             jQuery: "jquery" | ||||
|         }) | ||||
|     ] | ||||
| } | ||||
| 
 | ||||
|             $: 'jquery', | ||||
|             jQuery: 'jquery', | ||||
|             ClipboardJS: 'clipboard', | ||||
|         }), | ||||
|         new compress({ | ||||
|             filename: '[path][base].gz', | ||||
|             algorithm: "gzip", | ||||
|             exclude: /\.(png|gif|jpe?g)$/, | ||||
|             threshold: 5120, | ||||
|             minRatio: 0.8, | ||||
|             deleteOriginalAssets: false, | ||||
|         }), | ||||
|     ], | ||||
|     optimization: { | ||||
|         minimize: true, | ||||
|         minimizer: [ | ||||
|             new terse(), | ||||
|             new mini({ | ||||
|                 minimizerOptions: { | ||||
|                     preset: [ | ||||
|                         'default', { | ||||
|                             discardComments: { removeAll: true }, | ||||
|                         }, | ||||
|                     ], | ||||
|                 }, | ||||
|             }), | ||||
|         ], | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.6 KiB | 
| Before Width: | Height: | Size: 978 B After Width: | Height: | Size: 924 B | 
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.8 KiB | 
| @ -0,0 +1,2 @@ | ||||
| User-agent: * | ||||
| Disallow: / | ||||
| @ -1,6 +1,6 @@ | ||||
| # This is an idle image to dynamically replace any component if disabled. | ||||
| 
 | ||||
| ARG DISTRO=alpine:3.14 | ||||
| ARG DISTRO=alpine:3.14.2 | ||||
| FROM $DISTRO | ||||
| 
 | ||||
| CMD sleep 1000000d | ||||
|  | ||||
| @ -0,0 +1,10 @@ | ||||
| path: "/tmp/mta-sts.socket" | ||||
| mode: 0600 | ||||
| shutdown_timeout: 20 | ||||
| cache: | ||||
|   type: internal | ||||
|   options: | ||||
|     cache_size: 10000 | ||||
| default_zone: | ||||
|   strict_testing: {{ 'true' if DEFER_ON_TLS_ERROR else 'false' }} | ||||
|   timeout: 4 | ||||
 Alexander Graf
						Alexander Graf