diff --git a/.gitignore b/.gitignore index 7372e6d4f..ab59b88fc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ modoboa.iml .tox .eggs/ modoboa_test +bin/instance .coverage ./build/ dist/ diff --git a/README.rst b/README.rst index d3e7fd69a..c553721e7 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ Modoboa is a mail hosting and management platform including a modern and simplified Web User Interface. It provides useful components such -as an administration panel or a webmail. +as an administration panel and webmail. Modoboa integrates with well known software such as `Postfix `_ or `Dovecot `_. A SQL @@ -53,7 +53,7 @@ If you want to try out Modoboa, check out our `demo installation `_ * Mailing list: join the `modoboa-users `_ group on Google Groups diff --git a/doc/manual_installation/dovecot.rst b/doc/manual_installation/dovecot.rst index 4d7cd6754..d9e8fa961 100644 --- a/doc/manual_installation/dovecot.rst +++ b/doc/manual_installation/dovecot.rst @@ -147,11 +147,22 @@ MySQL users default_pass_scheme = CRYPT - password_query = SELECT email AS user, password FROM core_user WHERE email='%Lu' and is_active=1 + password_query = \ + SELECT email AS user, password \ + FROM core_user u \ + INNER JOIN admin_mailbox mb ON u.id=mb.user_id \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + WHERE u.email='%L{user}' AND u.is_active=1 AND dom.enabled=1 - user_query = SELECT '/%Ld/%Ln' AS home, as uid, as gid, concat('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%Ln' AND dom.name='%Ld' + user_query = \ + SELECT '/%Ld/%Ln' AS home, \ + as uid, as gid, CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule \ + FROM admin_mailbox mb \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + INNER JOIN core_user u ON mb.user_id=u.id \ + WHERE mb.address='%L{username}' AND dom.name='%L{domain}' AND u.is_active=1 AND dom.enabled=1 - iterate_query = SELECT email AS user FROM core_user + iterate_query = SELECT email AS user FROM core_user WHERE is_active .. _dovecot_pg_queries: @@ -166,11 +177,22 @@ PostgreSQL users default_pass_scheme = CRYPT - password_query = SELECT email AS user, password FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE u.email='%Lu' AND u.is_active AND dom.enabled + password_query = \ + SELECT email AS user, password \ + FROM core_user u \ + INNER JOIN admin_mailbox mb ON u.id=mb.user_id \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + WHERE u.email='%L{user}' AND u.is_active AND dom.enabled - user_query = SELECT '/%Ld/%Ln' AS home, as uid, as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%Ln' AND dom.name='%Ld' + user_query = \ + SELECT '/%Ld/%Ln' AS home, \ + as uid, as gid, CONCAT('*:bytes=', mb.quota, 'M') AS quota_rule \ + FROM admin_mailbox mb \ + INNER JOIN admin_domain dom ON mb.domain_id=dom.id \ + INNER JOIN core_user u ON mb.user_id=u.id \ + WHERE mb.address='%L{username}' AND dom.name='%L{domain}' AND u.is_active AND dom.enabled - iterate_query = SELECT email AS user FROM core_user + iterate_query = SELECT email AS user FROM core_user WHERE is_active SQLite users ------------ diff --git a/doc/manual_installation/modoboa.rst b/doc/manual_installation/modoboa.rst index 156ace9a9..f78ec8aef 100644 --- a/doc/manual_installation/modoboa.rst +++ b/doc/manual_installation/modoboa.rst @@ -235,11 +235,23 @@ following content inside: 0 0 * * * modoboa $PYTHON $INSTANCE/manage.py clearsessions # Logs table cleanup 0 0 * * * modoboa $PYTHON $INSTANCE/manage.py cleanlogs + # Logs parsing + */15 * * * * root $PYTHON $INSTANCE/manage.py logparser &> /dev/null + 0 * * * * modoboa $PYTHON $INSTANCE/manage.py update_statistics # DNSBL checks */30 * * * * modoboa $PYTHON $INSTANCE/manage.py modo check_mx # Public API communication 0 * * * * modoboa $PYTHON $INSTANCE/manage.py communicate_with_public_api +.. hint:: **🥵 potential high load configuration** + + Please note that above crontab might not be ideal on high load systems. + If you receive a fairly high amount of emails per day, you may want to + run modoboas logparser tasks *once per night*. + + This has the down side that the statistic graph and message log within + the UI are updated once per day only. + .. _policy_daemon: Policy daemon diff --git a/doc/requirements.txt b/doc/requirements.txt index db1851ad6..6b1d35172 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1 +1 @@ -docutils<0.19 +docutils<0.20 diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index a0d16369d..9f5acf8f6 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -247,7 +247,7 @@ export default { created () { parameters.getApplications().then(response => { response.data.forEach(item => { - this.mainMenuItems[4].children.push({ + this.mainMenuItems[5].children.push({ text: item.label, to: { name: 'ParametersEdit', params: { app: item.name } } }) diff --git a/frontend/src/components/parameters/ParametersForm.vue b/frontend/src/components/parameters/ParametersForm.vue index ab2287024..108844d96 100644 --- a/frontend/src/components/parameters/ParametersForm.vue +++ b/frontend/src/components/parameters/ParametersForm.vue @@ -84,7 +84,8 @@ export default { active: 0, structure: [], parameters: {}, - formErrors: {} + formErrors: {}, + label: '' } }, computed: { @@ -93,15 +94,7 @@ export default { return result }, title () { - let result = this.$gettext('Settings: ') - if (this.$route.params.app === 'core') { - result += this.$gettext('General') - } else if (this.$route.params.app === 'admin') { - result += this.$gettext('Administration') - } else if (this.$route.params.app === 'limits') { - result += this.$gettext('Limitations') - } - return result + return this.$gettext('Settings: ' + this.label) } }, created () { @@ -118,7 +111,8 @@ export default { this.structure = response.data }) parameters.getApplication(app).then(response => { - this.parameters = response.data + this.parameters = response.data.params + this.label = response.data.label }) }, display (element) { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b7a14356c..8802f0629 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2415,9 +2415,9 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.16.6: node-releases "^1.1.71" buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-indexof@^1.0.0: version "1.1.1" @@ -8577,9 +8577,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== shelljs@^0.8.3: version "0.8.5" @@ -8711,9 +8711,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@^0.5.13, source-map-support@~0.5.12: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -9179,9 +9179,9 @@ terser-webpack-plugin@^1.4.3, terser-webpack-plugin@^1.4.4: worker-farm "^1.7.0" terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 1b763c0ad..5bf9543a2 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -94,6 +94,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): custom_dns_server = GenericIPAddressField( label=ugettext_lazy("Custom DNS server"), required=False, + initial="", help_text=ugettext_lazy( "Use a custom DNS server instead of local server configuration" ) @@ -341,7 +342,7 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ ) }), ("enable_autoconfig_checks", { - "label": ugettext_lazy("Enable SPF checks"), + "label": ugettext_lazy("Enable autoconfig checks"), "help_text": ugettext_lazy( "Check if every domain has a valid records for " "autoconfiguration" diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py index 1c28f2101..3e99e6d96 100644 --- a/modoboa/core/api/v2/serializers.py +++ b/modoboa/core/api/v2/serializers.py @@ -13,6 +13,7 @@ from modoboa.lib import fields as lib_fields from ... import constants from ... import models from ... import sms_backends +from ... import app_settings class CoreGlobalParametersSerializer(serializers.Serializer): @@ -44,7 +45,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer): required=False, allow_blank=True) sms_password_recovery = serializers.BooleanField(default=False) sms_provider = serializers.ChoiceField( - choices=constants.SMS_BACKENDS, required=False) + choices=constants.SMS_BACKENDS, required=False, allow_null=True) # LDAP settings ldap_server_address = serializers.CharField(default="localhost") @@ -106,7 +107,8 @@ class CoreGlobalParametersSerializer(serializers.Serializer): ) # Dashboard settings - rss_feed_url = serializers.URLField(allow_blank=True) + rss_feed_url = serializers.URLField( + allow_blank=True, required=False, allow_null=True) hide_features_widget = serializers.BooleanField(default=False) # Notification settings @@ -126,7 +128,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer): log_maximum_age = serializers.IntegerField(default=365) items_per_page = serializers.IntegerField(default=30) default_top_redirection = serializers.ChoiceField( - default="user", choices=[""]) + default="user", choices=[("user", _("User profile"))], required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -134,6 +136,8 @@ class CoreGlobalParametersSerializer(serializers.Serializer): for field, definition in sms_backend_fields.items(): self.fields[field] = definition["type"]( **definition["attrs"]) + # Choices serializer for default_top_redirection field + self.fields["default_top_redirection"].choices = app_settings.enabled_applications() def validate_ldap_user_dn_template(self, value): try: diff --git a/modoboa/core/api/v2/tests.py b/modoboa/core/api/v2/tests.py index df66e3e13..d8d06c75b 100644 --- a/modoboa/core/api/v2/tests.py +++ b/modoboa/core/api/v2/tests.py @@ -58,7 +58,7 @@ CORE_SETTINGS = { "top_notifications_check_interval": 30, "log_maximum_age": 365, "items_per_page": 30, - "default_top_redirection": "" + "default_top_redirection": "user" } diff --git a/modoboa/core/sms_backends/ovh.py b/modoboa/core/sms_backends/ovh.py index 6c9af68d5..f36f036c6 100644 --- a/modoboa/core/sms_backends/ovh.py +++ b/modoboa/core/sms_backends/ovh.py @@ -75,19 +75,25 @@ class OVHBackend(SMSBackend): "sms_ovh_application_key": { "type": serializers.CharField, "attrs": { - "required": False + "required": False, + "allow_blank": True, + "allow_null": True, } }, "sms_ovh_application_secret": { "type": serializers.CharField, "attrs": { - "required": False + "required": False, + "allow_blank": True, + "allow_null": True, } }, "sms_ovh_consumer_key": { "type": serializers.CharField, "attrs": { - "required": False + "required": False, + "allow_blank": True, + "allow_null": True, } } } diff --git a/modoboa/lib/email_utils.py b/modoboa/lib/email_utils.py index ab81ad1d5..d3b30b76e 100644 --- a/modoboa/lib/email_utils.py +++ b/modoboa/lib/email_utils.py @@ -253,8 +253,6 @@ class Email(object): ) mail_text = lxml.html.tostring( cleaner.clean_html(html), encoding="unicode") - with open("/tmp/output.txt", "w") as fp: - fp.write(mail_text) return smart_text(mail_text) def _map_cid(self, url): diff --git a/modoboa/locale/fr/LC_MESSAGES/django.po b/modoboa/locale/fr/LC_MESSAGES/django.po index 745856a20..9d5aa5c53 100644 --- a/modoboa/locale/fr/LC_MESSAGES/django.po +++ b/modoboa/locale/fr/LC_MESSAGES/django.po @@ -1,24 +1,26 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Antoine Nguyen , 2016-2017,2020 # Christophe CHAUVET , 2018 +# Icarus Flight, 2022 +# Jan-Philipp Sprung, 2021 # Jorg722, 2019 msgid "" msgstr "" "Project-Id-Version: Modoboa\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-11-05 15:36+0100\n" -"PO-Revision-Date: 2020-11-05 15:49+0100\n" -"Last-Translator: Antoine Nguyen \n" +"PO-Revision-Date: 2011-07-01 08:15+0000\n" +"Last-Translator: Icarus Flight, 2022\n" "Language-Team: French (http://www.transifex.com/tonio/modoboa/language/fr/)\n" -"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Language: fr\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: core/app_settings.py:26 msgid "User profile" @@ -46,8 +48,7 @@ msgstr "Méthode de chiffrement par défaut" #: core/app_settings.py:56 msgid "Scheme used to crypt mailbox passwords" -msgstr "" -"Méthode utilisée pour chiffrer les mots de passe des boites aux lettres" +msgstr "Méthode utilisée pour chiffrer les mots de passe des boites aux lettres" #: core/app_settings.py:60 msgid "Rounds" @@ -57,9 +58,7 @@ msgstr "Tours" msgid "" "Number of rounds to use (only used by sha256crypt and sha512crypt). Must be " "between 1000 and 999999999, inclusive." -msgstr "" -"Nombre de tours à utiliser (sha256crypt et sha512crypt uniquement). Doit " -"être compris entre 1000 et 999999999 inclue." +msgstr "Nombre de tours à utiliser (sha256crypt et sha512crypt uniquement). Doit être compris entre 1000 et 999999999 inclue." #: core/app_settings.py:69 msgid "Update password scheme at login" @@ -67,9 +66,7 @@ msgstr "Mettre à jour la méthode de chiffrement du mot de passe à la connexio #: core/app_settings.py:72 msgid "Update user password at login to use the default password scheme" -msgstr "" -"Mise à jour du mot de passe utilisateur lors d'une connexion pour utiliser " -"la méthode de chiffrement par défaut" +msgstr "Mise à jour du mot de passe utilisateur lors d'une connexion pour utiliser la méthode de chiffrement par défaut" #: core/app_settings.py:77 msgid "Default password" @@ -96,10 +93,7 @@ msgid "" "The URL of an external page where users will be able to update their " "password. It applies only to non local users, ie. those automatically " "created after a successful external authentication (LDAP, SMTP)." -msgstr "" -"L'URL d'une page externe où les utilisateurs pourront mettre à jour leur mot " -"de passe. Elle s'applique uniquement aux utilisateurs non locaux, c'est à " -"dire ceux créés après une authentification externe réussie (LDAP, SMTP)." +msgstr "L'URL d'une page externe où les utilisateurs pourront mettre à jour leur mot de passe. Elle s'applique uniquement aux utilisateurs non locaux, c'est à dire ceux créés après une authentification externe réussie (LDAP, SMTP)." #: core/app_settings.py:104 msgid "Password recovery announcement" @@ -107,9 +101,7 @@ msgstr "Annonce pour la récupération de mot de passe" #: core/app_settings.py:109 msgid "A temporary message that will be displayed on the reset password page." -msgstr "" -"Un message temporaire qui sera affiché sur la page de récupération de mot de " -"passe" +msgstr "Un message temporaire qui sera affiché sur la page de récupération de mot de passe" #: core/app_settings.py:115 msgid "Enable password recovery by SMS" @@ -117,9 +109,7 @@ msgstr "Activer la récupération de mot de passe par SMS" #: core/app_settings.py:118 msgid "Enable password recovery by SMS for users who filled a phone number." -msgstr "" -"Activer la récupération de mot de passe par SMS pour les utilisateurs ayant " -"renseigné un numéro de téléphone" +msgstr "Activer la récupération de mot de passe par SMS pour les utilisateurs ayant renseigné un numéro de téléphone" #: core/app_settings.py:124 msgid "SMS provider" @@ -127,8 +117,7 @@ msgstr "Fournisseur SMS" #: core/app_settings.py:127 msgid "Choose a provider to send password recovery SMS" -msgstr "" -"Choisir un fournisseur pour envoyer les SMS de récupération de mot de passe" +msgstr "Choisir un fournisseur pour envoyer les SMS de récupération de mot de passe" #: core/app_settings.py:133 msgid "LDAP settings" @@ -157,9 +146,7 @@ msgstr "Activer le serveur secondaire (secours)" #: core/app_settings.py:152 msgid "" "Enable a secondary LDAP server which will be used if the primary one fails" -msgstr "" -"Activer un serveur LDAP secondaire qui sera utilisé si le principal est en " -"échec" +msgstr "Activer un serveur LDAP secondaire qui sera utilisé si le principal est en échec" #: core/app_settings.py:158 msgid "Secondary server address" @@ -199,12 +186,9 @@ msgstr "Groupes administrateurs" #: core/app_settings.py:190 msgid "" -"Members of those LDAP Posix groups will be created as domain administrators. " -"Use ';' characters to separate groups." -msgstr "" -"Les membres de ces groupes LDAP (Posix) seront créés en tant " -"qu'administrateurs de domaine. Utiliser des caractères ';' pour séparer les " -"groupes." +"Members of those LDAP Posix groups will be created as domain administrators." +" Use ';' characters to separate groups." +msgstr "Les membres de ces groupes LDAP (Posix) seront créés en tant qu'administrateurs de domaine. Utiliser des caractères ';' pour séparer les groupes." #: core/app_settings.py:197 msgid "Group type" @@ -220,9 +204,7 @@ msgstr "Base de recherche des groupes" #: core/app_settings.py:209 msgid "The distinguished name of the search base used to find groups" -msgstr "" -"Le nom distinctif de la base de recherche utilisée pour rechercher les " -"groupes" +msgstr "Le nom distinctif de la base de recherche utilisée pour rechercher les groupes" #: core/app_settings.py:215 msgid "Password attribute" @@ -260,9 +242,7 @@ msgstr "ND de connexion" msgid "" "The distinguished name to use when binding to the LDAP server. Leave empty " "for an anonymous bind" -msgstr "" -"Le nom distinctif à utiliser pour les connections au serveur LDAP. Laisser " -"vide pour une connexion anonyme" +msgstr "Le nom distinctif à utiliser pour les connections au serveur LDAP. Laisser vide pour une connexion anonyme" #: core/app_settings.py:243 core/app_settings.py:297 msgid "Bind password" @@ -270,9 +250,7 @@ msgstr "Mot de passe de connexion" #: core/app_settings.py:246 core/app_settings.py:300 msgid "The password to use when binding to the LDAP server (with 'Bind DN')" -msgstr "" -"Le mot de passe à utiliser pour les connections au serveur LDAP (avec 'ND de " -"connexion')" +msgstr "Le mot de passe à utiliser pour les connections au serveur LDAP (avec 'ND de connexion')" #: core/app_settings.py:254 core/app_settings.py:346 msgid "Users search base" @@ -280,9 +258,7 @@ msgstr "Base de recherche des utilisateurs" #: core/app_settings.py:257 core/app_settings.py:349 msgid "The distinguished name of the search base used to find users" -msgstr "" -"Le nom distinctif de la base de recherche utilisée pour rechercher les " -"utilisateurs" +msgstr "Le nom distinctif de la base de recherche utilisée pour rechercher les utilisateurs" #: core/app_settings.py:263 core/app_settings.py:355 msgid "Search filter" @@ -292,9 +268,7 @@ msgstr "Filtre de recherche" msgid "" "An optional filter string (e.g. '(objectClass=person)'). In order to be " "valid, it must be enclosed in parentheses." -msgstr "" -"Un filtre de recherche optionel (ex. '(objectClass=person)'). Pour être " -"valide, il doit être entouré de parenthèses." +msgstr "Un filtre de recherche optionel (ex. '(objectClass=person)'). Pour être valide, il doit être entouré de parenthèses." #: core/app_settings.py:273 msgid "User DN template" @@ -305,9 +279,7 @@ msgstr "Modèle de nom distinctif utilisateur" msgid "" "The template used to construct a user's DN. It should contain one " "placeholder (ie. %(user)s)" -msgstr "" -"Le modèle utilisé pour construire le nom distinctif d'un utilisateur. Il " -"doit contenir un espace réservé (ex. %(user)s)" +msgstr "Le modèle utilisé pour construire le nom distinctif d'un utilisateur. Il doit contenir un espace réservé (ex. %(user)s)" #: core/app_settings.py:284 msgid "LDAP synchronization settings" @@ -320,9 +292,7 @@ msgstr "Activer l'export LDAP" #: core/app_settings.py:311 msgid "" "Enable automatic synchronization between local database and LDAP directory" -msgstr "" -"Activer la synchronisation automatique entre la base de données locale et " -"l'annuaire LDAP" +msgstr "Activer la synchronisation automatique entre la base de données locale et l'annuaire LDAP" #: core/app_settings.py:317 msgid "Delete remote LDAP account when local account is deleted" @@ -332,9 +302,7 @@ msgstr "Supprimer un compte LDAP distant lorsqu'un compte local est supprimé" msgid "" "Delete remote LDAP account when local account is deleted, otherwise it will " "be disabled." -msgstr "" -"Supprimer un compte LDAP distant lorsqu'un compte local supprimé, autrement " -"il sera désactivé." +msgstr "Supprimer un compte LDAP distant lorsqu'un compte local supprimé, autrement il sera désactivé." #: core/app_settings.py:327 msgid "Account DN template" @@ -345,9 +313,7 @@ msgstr "Modèle des DN de compte" msgid "" "The template used to construct an account's DN. It should contain one " "placeholder (ie. %(user)s)" -msgstr "" -"Le modèle utilisé pour construite un DN de compte (nom unique). Il doit " -"contenir un emplacement (c'est à dire 1%(user)s)" +msgstr "Le modèle utilisé pour construite un DN de compte (nom unique). Il doit contenir un emplacement (c'est à dire 1%(user)s)" #: core/app_settings.py:337 msgid "Enable import from LDAP" @@ -355,9 +321,7 @@ msgstr "Activer l'import LDAP" #: core/app_settings.py:340 msgid "Enable account synchronization from LDAP directory to local database" -msgstr "" -"Activer la synchronisation des comptes entre un annuaire LDAP et la base de " -"données locale" +msgstr "Activer la synchronisation des comptes entre un annuaire LDAP et la base de données locale" #: core/app_settings.py:365 msgid "Username attribute" @@ -365,8 +329,7 @@ msgstr "Attribut nom d'utilisateur" #: core/app_settings.py:368 msgid "The name of the LDAP attribute where the username can be found." -msgstr "" -"Le nom de l'attribut LDAP dans lequel le nom d'utilisateur peut être trouvé." +msgstr "Le nom de l'attribut LDAP dans lequel le nom d'utilisateur peut être trouvé." #: core/app_settings.py:373 msgid "Enable Dovecot LDAP sync" @@ -374,9 +337,7 @@ msgstr "Activer la synchronisation LDAP pour Dovecot" #: core/app_settings.py:376 msgid "LDAP authentication settings will be applied to Dovecot configuration." -msgstr "" -"Les paramètres d'authentification LDAP seront appliqués à la configuration " -"de Dovecot." +msgstr "Les paramètres d'authentification LDAP seront appliqués à la configuration de Dovecot." #: core/app_settings.py:382 msgid "Dovecot LDAP config file" @@ -385,9 +346,7 @@ msgstr "Fichier de configuration Dovecot pour LDAP" #: core/app_settings.py:386 msgid "" "Location of the configuration file which contains Dovecot LDAP settings." -msgstr "" -"Emplacement du fichier de configuration qui contient les paramètres LDAP de " -"Dovecot" +msgstr "Emplacement du fichier de configuration qui contient les paramètres LDAP de Dovecot" #: core/app_settings.py:391 core/templates/core/dashboard.html:5 msgid "Dashboard" @@ -399,9 +358,7 @@ msgstr "Flux RSS personnalisé" #: core/app_settings.py:397 msgid "Display custom RSS feed to resellers and domain administrators" -msgstr "" -"Afficher un flux RSS personnalisé aux revendeurs et administrateurs de " -"domaine" +msgstr "Afficher un flux RSS personnalisé aux revendeurs et administrateurs de domaine" #: core/app_settings.py:402 msgid "Hide features widget" @@ -409,9 +366,7 @@ msgstr "Masquer le widget des fonctionnalités" #: core/app_settings.py:405 msgid "Hide features widget for resellers and domain administrators" -msgstr "" -"Masquer le widget des fonctionnalités pour les revendeurs et les " -"administrateurs de domaine" +msgstr "Masquer le widget des fonctionnalités pour les revendeurs et les administrateurs de domaine" #: core/app_settings.py:409 msgid "Notifications" @@ -451,9 +406,7 @@ msgstr "Envoyer un email quand de nouvelles versions sont trouvées" #: core/app_settings.py:439 msgid "Send an email to notify admins about new versions" -msgstr "" -"Envoyer un email pour notifier les administrateurs à propos des nouvelles " -"versions" +msgstr "Envoyer un email pour notifier les administrateurs à propos des nouvelles versions" #: core/app_settings.py:443 msgid "Recipient" @@ -469,9 +422,7 @@ msgstr "Envoyer des statistiques" #: core/app_settings.py:454 msgid "Send statistics to Modoboa public API (counters and used extensions)" -msgstr "" -"Envoyer des statistiques à l'API publique de Modoboa (compteurs et " -"extensions utilisées)" +msgstr "Envoyer des statistiques à l'API publique de Modoboa (compteurs et extensions utilisées)" #: core/app_settings.py:458 msgid "Miscellaneous" @@ -483,11 +434,9 @@ msgstr "Seuil des comptes inactifs" #: core/app_settings.py:464 msgid "" -"An account with a last login date greater than this threshold (in days) will " -"be considered as inactive" -msgstr "" -"Un compte avec une date de dernière connexion antérieure à ce seuil sera " -"considéré comme inactif" +"An account with a last login date greater than this threshold (in days) will" +" be considered as inactive" +msgstr "Un compte avec une date de dernière connexion antérieure à ce seuil sera considéré comme inactif" #: core/app_settings.py:470 msgid "Top notifications check interval" @@ -519,8 +468,7 @@ msgstr "Redirection par défaut" #: core/app_settings.py:494 msgid "The default redirection used when no application is specified" -msgstr "" -"La redirection appliquée par défaut quand aucune application n'est spécifiée" +msgstr "La redirection appliquée par défaut quand aucune application n'est spécifiée" #: core/app_settings.py:548 core/app_settings.py:556 core/app_settings.py:564 msgid "Invalid syntax" @@ -543,10 +491,7 @@ msgid "" "You have USE_TZ set to False, this may result in issues during transitions " "between summer/winter time (ie the same local time occuring twice due to " "clock change)." -msgstr "" -"Vous avez défini USE_TZ à False, ce qui peut entraîner des problèmes lors " -"des transitions entre l'heure d'été et l'heure d'hiver (c.-à-d. La même " -"heure locale se produisant deux fois en raison d'un changement d'horloge)." +msgstr "Vous avez défini USE_TZ à False, ce qui peut entraîner des problèmes lors des transitions entre l'heure d'été et l'heure d'hiver (c.-à-d. La même heure locale se produisant deux fois en raison d'un changement d'horloge)." #: core/checks/settings_checks.py:9 msgid "Set `USE_TZ = True` in settings.py" @@ -622,7 +567,7 @@ msgstr "Code incorrect" #: core/forms.py:163 msgid "This code is invalid" -msgstr "Ce code est incorrect" +msgstr "Ce code n'est pas valide" #: core/handlers.py:36 msgid "added" @@ -700,8 +645,7 @@ msgstr "Une adresse e-mail alternative ou de secours." #: core/models.py:126 msgid "Failed to update password: LDAP module not installed" -msgstr "" -"Echec de la mise à jour du mot de passe : le module LDAP n'est pas installé" +msgstr "Echec de la mise à jour du mot de passe : le module LDAP n'est pas installé" #: core/models.py:155 msgid "account" @@ -734,38 +678,40 @@ msgid "Password must contain at least {} digit." msgid_plural "Password must contain at least {} digits." msgstr[0] "Le mot de passe doit contenir au moins {} chiffre." msgstr[1] "Le mot de passe doit contenir au moins {} chiffres." +msgstr[2] "Le mot de passe doit contenir au moins {} chiffres." #: core/password_validation.py:34 msgid "Password must contain at least {} lowercase letter." msgid_plural "Password must contain at least {} lowercase letters." msgstr[0] "Le mot de passe doit contenir au moins {} minuscule." msgstr[1] "Le mot de passe doit contenir au moins {} minuscules." +msgstr[2] "Le mot de passe doit contenir au moins {} minuscules." #: core/password_validation.py:45 msgid "Password must contain at least {} uppercase letter." msgid_plural "Password must contain at least {} uppercase letters." msgstr[0] "Le mot de passe doit contenir au moins {} majuscule." msgstr[1] "Le mot de passe doit contenir au moins {} majuscules." +msgstr[2] "Le mot de passe doit contenir au moins {} majuscules." #: core/password_validation.py:57 msgid "Password must contain at least {} special character." msgid_plural "Password must contain at least {} special characters." msgstr[0] "Le mot de passe doit contenir au moins {} caractère spécial." msgstr[1] "Le mot de passe doit contenir au moins {} caractères spéciaux." +msgstr[2] "Le mot de passe doit contenir au moins {} caractères spéciaux." #: core/password_validation.py:65 msgid "Your password must contain a combination of different character types." -msgstr "" -"Votre mot de passe doit contenir une combinaison de différents types de " -"caractère." +msgstr "Votre mot de passe doit contenir une combinaison de différents types de caractère." #: core/serializers.py:15 msgid "Invalid PIN code" -msgstr "Code PIN incorrect" +msgstr "Code PIN non valide" #: core/serializers.py:21 msgid "2FA is already enabled" -msgstr "La 2FA est déjà activée" +msgstr "2FA est déjà activf" #: core/sms_backends/ovh.py:19 msgid "API endpoint" @@ -835,9 +781,7 @@ msgstr "Dernières nouvelles" msgid "" "Visit the official " "weblog for more information." -msgstr "" -"Visitez le blog " -"officiel pour plus d'information." +msgstr "Visitez le blog officiel pour plus d'information." #: core/templates/core/api_access.html:3 msgid "API access" @@ -850,11 +794,9 @@ msgstr "Contrôlez votre accès à l'API de Modoboa" #: core/templates/core/api_access.html:7 #, python-format msgid "" -"A documentation of the API is available here." -msgstr "" -"Une documentation de l'API est disponible ici." +"A documentation of the API is available here." +msgstr "Une documentation de l'API est disponible ici." #: core/templates/core/api_access.html:14 msgid "API access token" @@ -884,8 +826,7 @@ msgstr "Une ou plusieurs mises à jour sont disponibles!" #: core/templates/core/information.html:6 msgid "Check the following list to find the component(s) that can be updated." -msgstr "" -"Consulter la liste suivante pour trouver le(s) composant(s) concerné(s)." +msgstr "Consulter la liste suivante pour trouver le(s) composant(s) concerné(s)." #: core/templates/core/information.html:10 msgid "Applications" @@ -951,12 +892,9 @@ msgid "" msgid_plural "" "\n" " The following updates are available:\n" -msgstr[0] "" -"\n" -"La mise à jour suivante est disponible :\n" -msgstr[1] "" -"\n" -"Les mises à jour suivantes sont disponibles :\n" +msgstr[0] "\nLa mise à jour suivante est disponible :\n" +msgstr[1] "\nLes mises à jour suivantes sont disponibles :\n" +msgstr[2] "\nLes mises à jour suivantes sont disponibles :\n" #: core/templates/core/parameters.html:2 core/templatetags/core_tags.py:71 msgid "Parameters" @@ -1010,13 +948,13 @@ msgstr "Sécurité" #: core/templates/core/user_security.html:6 msgid "Two-Factor authentication" -msgstr "Authentification à double facteur" +msgstr "Authentification à deux facteurs" #: core/templates/core/user_security.html:17 msgid "" -"Install a soft token authenticator like FreeOTP or Google Authenticator from " -"your application repository and use that app to scan this QR code." -msgstr "Installer une application de génération de mots de passe à usage unique comme FreeOTP ou Google Athenticator depuis votre magasin d'applications et utiliser le pour scanner ce code QR." +"Install a soft token authenticator like FreeOTP or Google Authenticator from" +" your application repository and use that app to scan this QR code." +msgstr "Installez une application d'authentification par jetons comme FreeOTP ou Google Authenticator à partir de votre appstore et utilisez-la pour scanner ce QR code." #: core/templates/core/user_security.html:25 msgid "Pin code" @@ -1024,12 +962,12 @@ msgstr "Code PIN" #: core/templates/core/user_security.html:28 msgid "Register" -msgstr "Enregistrer" +msgstr "Enregistrement" #: core/templates/core/user_security.html:33 msgid "" "Congratulations! Two-Factor Authentication is now enabled for your account." -msgstr "Félicitations ! L'authentification à double facteur est activée pour votre compte." +msgstr "Félicitations ! L'authentification à deux facteurs est maintenant activée pour votre compte." #: core/templates/core/user_security.html:38 #: core/templates/core/user_security.html:89 @@ -1038,34 +976,35 @@ msgid "" "access to your account, in case you lose your phone for example. Make sure " "to save them in a safe place, otherwise you won't be able to access your " "account anymore." -msgstr "Les codes de récuparation suivants peuvent être utilisés une fois chacun pour récupérer l'accès à votre compte, par exemple si vous perdez votre téléphone. Assurez vous de les sauvegarder dans un endroit sûr, autrement vous ne pourrez plus accéder à votre compte." +msgstr "Les codes de récupération suivants peuvent être utilisés une fois chacun pour vous permettre de récupérer un accès à votre compte, au cas où vous perdriez votre téléphone par exemple. Assurez-vous de les conserver dans un lieu sûr, autrement vous ne serez définitivement plus en mesure d'accéder à votre compte." #: core/templates/core/user_security.html:53 msgid "Two-Factor Authentication is enabled for your account." -msgstr "L'authentication à double facteur est activée pour votre compte." +msgstr "L'authentification à deux facteurs est activée pour votre compte." #: core/templates/core/user_security.html:58 msgid "Disable 2FA" -msgstr "Désactiver la 2FA" +msgstr "Désactiver authentification à 2 facteurs." #: core/templates/core/user_security.html:61 msgid "Reset recovery codes" -msgstr "Réinitialiser les codes de récupération" +msgstr "Réinitialiser les codes de récupération." #: core/templates/core/user_security.html:65 msgid "" "Two-Factor Authentication (2FA) is not yet activated for your account. " "Enabling this feature will increase your account's security." -msgstr "L'authentification à double facteur (2FA) n'est pas encore activée pour votre compte. Activer cette fonctionnalité améliorera la sécurité de votre compte." +msgstr "L'authentification à 2 facteurs n'est pas encore activée pour votre compte. Activer cette fonctionnalité renforcera la sécurité de votre compte." #: core/templates/core/user_security.html:73 msgid "Enable 2FA" -msgstr "Activer la 2FA" +msgstr "Activer authentification à 2 facteurs." #: core/templates/core/user_security.html:83 #: core/templates/core/user_security.html:99 -#: templates/common/generic_modal.html:6 templates/common/generic_modal.html:13 -#: templates/common/wizard_forms.html:6 templates/common/wizard_forms.html:27 +#: templates/common/generic_modal.html:6 +#: templates/common/generic_modal.html:13 templates/common/wizard_forms.html:6 +#: templates/common/wizard_forms.html:27 msgid "Close" msgstr "Fermer" @@ -1075,7 +1014,7 @@ msgstr "Nouveaux codes de récupération" #: core/templates/core/user_security.html:88 msgid "Here are your new recovery codes. " -msgstr "Voici vos nouveaux codes de récupération." +msgstr "Vous trouverez ici vos nouveaux codes de récupération." #: core/templatetags/core_tags.py:34 msgid "Modoboa" @@ -1100,14 +1039,12 @@ msgstr "Paramètres enregistrés" #: core/views/auth.py:61 #, python-format msgid "Password scheme mismatch. Updating %s password" -msgstr "" -"La méthode de chiffrement du mot de passe diffère. Mise à jour du mot de " -"passe de %s" +msgstr "La méthode de chiffrement du mot de passe diffère. Mise à jour du mot de passe de %s" #: core/views/auth.py:68 #, python-format msgid "Password hash parameter missmatch. Updating %s password" -msgstr "" +msgstr "Le paramètre de hash du mot de passe ne correspond pas. Mise à jour %s du mot de passe" #: core/views/auth.py:84 #, python-format @@ -1116,9 +1053,7 @@ msgstr "Utiliseur '%s' connecté avec succès" #: core/views/auth.py:92 msgid "Your username and password didn't match. Please try again." -msgstr "" -"Votre nom d'utilisateur et mot de passe ne correspondent pas. Veuillez " -"réessayer." +msgstr "Votre nom d'utilisateur et mot de passe ne correspondent pas. Veuillez réessayer." #: core/views/auth.py:127 msgid "User '{}' successfully logged out" @@ -1126,9 +1061,7 @@ msgstr "Utilisateur '{}' déconnecté avec succès" #: core/views/auth.py:172 core/views/auth.py:227 msgid "Please use the following code to recover your Modoboa password: {}" -msgstr "" -"Veuillez utiliser le code suivant pour récupérer votre mot de passe " -"Modoboa : {}" +msgstr "Veuillez utiliser le code suivant pour récupérer votre mot de passe Modoboa : {}" #: core/views/user.py:60 msgid "Profile updated" @@ -1144,7 +1077,7 @@ msgstr "Accès mis à jour" #: core/viewsets.py:52 msgid "2FA is not enabled" -msgstr "La 2FA n'est pas activée" +msgstr "Authentification 2 facteurs non activée" #: lib/db_utils.py:23 #, python-format @@ -1265,9 +1198,7 @@ msgstr "Mot de passe oublié ?" msgid "" "Your password has been set. You may go ahead and sign " "in now." -msgstr "" -"Votre mot de passe a été défini. Vous pouvez continuer et vous connecter dès maintenant." +msgstr "Votre mot de passe a été défini. Vous pouvez continuer et vous connecter dès maintenant." #: templates/registration/password_reset_confirm.html:7 msgid "Change password" @@ -1277,10 +1208,7 @@ msgstr "Changer le mot de passe" msgid "" "The password reset link was invalid, possibly because it has already been " "used. Please request a new password reset." -msgstr "" -"Le lien de réinitialisation du mot de passe était invalide, probablement " -"parce qu'il a déjà été utilisé. Merci de demander une nouvelle " -"réinitialisation de mot de passe." +msgstr "Le lien de réinitialisation du mot de passe était invalide, probablement parce qu'il a déjà été utilisé. Merci de demander une nouvelle réinitialisation de mot de passe." #: templates/registration/password_reset_confirm_code.html:20 msgid "Enter the code you've just received by SMS" @@ -1296,29 +1224,20 @@ msgid "" "exists with the email you entered. You should receive them shortly.

" "

If you don't receive an email, please make sure you've entered your " "primary address, and check your spam folder.

" -msgstr "" -"

Nous vous avons envoyé par email les instructions pour définir votre mot " -"de passe, pour peu qu'un compte avec l'email que vous avez renseigné existe. " -"Vous devriez les recevoir rapidement.

Si vous ne recevez pas d'email, " -"merci de vérifier que vous avez renseigné votre adresse principale et de " -"vérifier votre dossier spam.

" +msgstr "

Nous vous avons envoyé par email les instructions pour définir votre mot de passe, pour peu qu'un compte avec l'email que vous avez renseigné existe. Vous devriez les recevoir rapidement.

Si vous ne recevez pas d'email, merci de vérifier que vous avez renseigné votre adresse principale et de vérifier votre dossier spam.

" #: templates/registration/password_reset_email.html:4 #, python-format msgid "" "To initiate the password reset process for your %(username)s Modoboa " "account, click the link below:" -msgstr "" -"Pour initier la procédure de réinitialisation du mot de passe de votre " -"compte Modoboa %(username)s, cliquez sur le lien ci-dessous :" +msgstr "Pour initier la procédure de réinitialisation du mot de passe de votre compte Modoboa %(username)s, cliquez sur le lien ci-dessous :" #: templates/registration/password_reset_email.html:10 msgid "" "If clicking the link above doesn't work, please copy and paste the URL in a " "new browser window instead." -msgstr "" -"Si cliquer sur lien ci-dessous ne fonctionne pas, merci de copier / coller " -"cette URL dans une nouvelle fenêtre navigateur à la place." +msgstr "Si cliquer sur lien ci-dessous ne fonctionne pas, merci de copier / coller cette URL dans une nouvelle fenêtre navigateur à la place." #: templates/registration/password_reset_email.html:14 msgid "Sincerely, The Modoboa Team." diff --git a/modoboa/parameters/api/v2/serializers.py b/modoboa/parameters/api/v2/serializers.py index 8b6ada39b..662f4126a 100644 --- a/modoboa/parameters/api/v2/serializers.py +++ b/modoboa/parameters/api/v2/serializers.py @@ -34,3 +34,10 @@ class StructureSerializer(serializers.Serializer): name = serializers.CharField() display = serializers.CharField(help_text="Display rule") parameters = ParameterSerializer(many=True) + + +class AppParametersSerializer(serializers.Serializer): + """Serializer for application parameters.""" + + label = serializers.CharField() + params = serializers.DictField() diff --git a/modoboa/parameters/api/v2/viewsets.py b/modoboa/parameters/api/v2/viewsets.py index efefd838c..a6f802f6a 100644 --- a/modoboa/parameters/api/v2/viewsets.py +++ b/modoboa/parameters/api/v2/viewsets.py @@ -36,14 +36,19 @@ class ParametersViewSet(viewsets.ViewSet): description='A registered application name', type=str, required=True ), - ] + ], + responses=serializers.AppParametersSerializer ) def retrieve(self, request, pk: str): """Return all parameters for given app.""" parameters = request.localconfig.parameters.get_values_dict(pk) serializer = tools.registry.get_serializer_class("global", pk)( parameters) - return response.Response(serializer.data) + result = serializers.AppParametersSerializer({ + "label": tools.registry.get_label("global", pk), + "params": serializer.data + }) + return response.Response(result.data) @extend_schema( parameters=[ diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index f3a6fd5d6..cda90ba1e 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -63,6 +63,9 @@ class Registry(object): result = [{"name": key, "label": value["label"]} for key, value in self._registry2[level].items()] return result + + def get_label(self, level, app): + return self._registry2[level][app]["label"] def get_forms(self, level, *args, **kwargs): """Return form instances for all app of the given level.""" diff --git a/modoboa/policyd/management/commands/policy_daemon.py b/modoboa/policyd/management/commands/policy_daemon.py index f0c94edfa..2808a5086 100644 --- a/modoboa/policyd/management/commands/policy_daemon.py +++ b/modoboa/policyd/management/commands/policy_daemon.py @@ -34,7 +34,7 @@ class Command(BaseCommand): """Entry point.""" loop = asyncio.get_event_loop() coro = asyncio.start_server( - core.new_connection, options["host"], options["port"], loop=loop + core.new_connection, options["host"], options["port"] ) server = loop.run_until_complete(coro) diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index 45d8bae1c..d04b3e3e8 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -177,7 +177,7 @@ TwocolsNav.prototype = { startTFASetup: function (evt) { evt.preventDefault(); $.ajax({ - url: '/api/v1/account/tfa/setup/', + url: window.root_url + '/api/v1/account/tfa/setup/', type: 'post', contentType: 'application/json' }).done(function (resp) { @@ -191,7 +191,7 @@ TwocolsNav.prototype = { pin_code: $('#pin-code').val() }; $.ajax({ - url: '/api/v1/account/tfa/setup/check/', + url: window.root_url + '/api/v1/account/tfa/setup/check/', type: 'post', global: false, data: JSON.stringify(data), diff --git a/modoboa/templates/nlayout.html b/modoboa/templates/nlayout.html index 2c94c30a4..8a219c595 100644 --- a/modoboa/templates/nlayout.html +++ b/modoboa/templates/nlayout.html @@ -87,6 +87,7 @@