Merge branch 'master' into refacto/vuejs_interface

This commit is contained in:
Antoine Nguyen
2021-02-12 21:35:47 +01:00
33 changed files with 354 additions and 79 deletions
@@ -1,8 +1,16 @@
---
name: 'Bug Report'
about: 'Report a reproducible bug in the software.'
title: ''
labels: ''
assignees: ''
---
# Impacted versions
* OS Type: Debian/Ubuntu, Centos/Redhat, Arch, Gentoo, Slack, ...
* OS Version: Number or Name
* Database Type: PostgreSQL/MySQL
* Database Type: PostgreSQL / MySQL
* Database version: X.y
* Modoboa: X.X.x
* installer used: Yes/No ?
@@ -12,6 +20,13 @@
# Current behavior
<!--
Explain the behavior you're seeing that you think is a bug, and explain how you
think things should behave instead.
-->
# Expected behavior
# Video/Screenshot link (optional)
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Modoboa Support
url: https://github.com/modoboa/modoboa/discussions
about: Please ask and answer questions here.
- name: Sponsoring
ur: https://modoboa.org/en/sponsoring/
about: You love Modoboa and want to sponsor it
+3 -1
View File
@@ -66,7 +66,9 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get install postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev
sudo apt-get update -y \
&& sudo apt-get install -y \
postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r ldap-requirements.txt
+1
View File
@@ -13,6 +13,7 @@ RUN python3 -m venv $VIRTUAL_ENV
WORKDIR /tmp
COPY requirements.txt /tmp
COPY test-requirements.txt /tmp
RUN python -m pip install -U pip
RUN pip install -r requirements.txt -r test-requirements.txt
FROM base as run
+13 -2
View File
@@ -31,7 +31,7 @@ The *admin* application exposes several parameters, they are presented below:
|Authentication type |General |The backend used for |Local |
| | |authentication | |
+--------------------+---------------------+-------------------------------+--------------------+
|Default password |General |Scheme used to crypt |crypt |
|Default password |General |Scheme used to crypt |sha512crypt |
|scheme | |mailbox passwords | |
+--------------------+---------------------+-------------------------------+--------------------+
|Rounds |General |Number of rounds (only used by |70000 |
@@ -144,6 +144,13 @@ The *admin* application exposes several parameters, they are presented below:
| | | | |
+--------------------+---------------------+-------------------------------+--------------------+
.. warning::
If Dovecot is not running on the same host than Modoboa, you will have
to define which password schemes are supported. To do so, open the
:file:`settings.py` file and add a ``DOVECOT_SUPPORTED_SCHEMES``
variable with the output of the command: ``doveadm pw -l``.
.. note::
If you are not familiar with virtual domain hosting, you should
@@ -202,7 +209,11 @@ Host configuration
.. note::
This section is only relevant when Modoboa handles mailboxes
renaming and removal from the filesystem.
renaming and removal from the filesystem, which requires that
Dovecot is installed and running on this host. If it is installed
at a non-standard directory, paths to its binaries can be set in the
:file:`settings.py` file with the ``DOVECOT_LOOKUP_PATH`` and
``DOVEADM_LOOKUP_PATH`` variables.
To manipulate mailboxes on the filesystem, you must allow the user who
runs Modoboa to execute commands as the user who owns mailboxes.
+6
View File
@@ -24,6 +24,12 @@ setup, go to the next section.
.. _venv_for_dev:
Prerequisites For Development
=============================
If you encounter errors about missing tools, please ensure you have them installed with the foollowing:
$ sudo apt install librrd-dev
Prepare a virtual environment
=============================
+10 -2
View File
@@ -55,9 +55,14 @@ following system packages according to your distribution:
.. note::
Alternatively, you could rely on your distribution packages for the Modoboa
dependencies which require compilation - e.g. ``psycopg2`` - if the version
dependencies which require compilation - e.g. ``rrdtool`` - if the version
is compatible. In this case, you have to create your virtual environment
with the ``--system-site-packages`` option.
with the ``--system-site-packages`` option, and the required system
packages will be:
+---------------------------------------+
| python3-wheel python3-rrdtool rrdtool |
+---------------------------------------+
Then, install Modoboa by running:
@@ -184,6 +189,9 @@ you just have to run the following command:
``<mysql|postgres>://[user:pass@][host:port]/dbname`` **OR**
``sqlite:////full/path/to/your/database/file.sqlite``.
Fox example, if you were using postgres, you could setup your command like this:
``modoboa-admin.py deploy instance_name --collectstatic --domain example.com --dburl default:postgres://user:pass@[localhost]/modoboa``
The command will ask you a few questions, answer them and you're
done.
+7 -5
View File
@@ -34,7 +34,7 @@ from . import lib, models, serializers
summary="Create a new domain"
)
)
class DomainViewSet(viewsets.ModelViewSet):
class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""Domain viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
@@ -63,7 +63,8 @@ class DomainAliasFilterSet(dj_filters.FilterSet):
fields = ["domain"]
class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet):
class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
lib_viewsets.ExpandableModelViewSet):
"""ViewSet for DomainAlias."""
filter_backends = (dj_filters.DjangoFilterBackend, )
@@ -83,7 +84,7 @@ class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet):
return context
class AccountViewSet(viewsets.ModelViewSet):
class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""ViewSet for User/Mailbox."""
filter_backends = [filters.SearchFilter]
@@ -178,7 +179,7 @@ class AccountViewSet(viewsets.ModelViewSet):
return Response(body)
class AliasViewSet(viewsets.ModelViewSet):
class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""
create:
Create a new alias instance.
@@ -203,7 +204,8 @@ class AliasViewSet(viewsets.ModelViewSet):
return queryset
class SenderAddressViewSet(viewsets.ModelViewSet):
class SenderAddressViewSet(lib_viewsets.RevisionModelMixin,
viewsets.ModelViewSet):
"""View set for SenderAddress model."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
+1 -11
View File
@@ -147,14 +147,6 @@ class AdminParametersForm(param_forms.AdminParametersForm):
)
)
mailboxes_owner = forms.CharField(
label=ugettext_lazy("Mailboxes owner"),
initial="vmail",
help_text=ugettext_lazy(
"The UNIX account who owns mailboxes on the filesystem"
)
)
default_mailbox_quota = forms.IntegerField(
label=ugettext_lazy("Default mailbox quota"),
initial=0,
@@ -200,8 +192,7 @@ class AdminParametersForm(param_forms.AdminParametersForm):
# Visibility rules
visibility_rules = {
"valid_mxs": "enable_mx_checks=True",
"domains_must_have_authorized_mx": "enable_mx_checks=True",
"mailboxes_owner": "handle_mailboxes=True",
"domains_must_have_authorized_mx": "enable_mx_checks=True"
}
def __init__(self, *args, **kwargs):
@@ -236,7 +227,6 @@ class AdminParametersForm(param_forms.AdminParametersForm):
hide_fields = True
if hide_fields:
del self.fields["handle_mailboxes"]
del self.fields["mailboxes_owner"]
def clean_default_domain_quota(self):
"""Ensure quota is a positive integer."""
+2 -3
View File
@@ -206,9 +206,8 @@ class AccountFormGeneral(forms.ModelForm):
account.role = self.cleaned_data["role"]
if hasattr(account, "mailbox"):
# Update forward status according to account status
models.Alias.objects.filter(
address=account.email, internal=False).update(
enabled=account.is_active)
models.Alias.objects.filter(address=account.email).update(
enabled=account.is_active)
return account
+2
View File
@@ -198,6 +198,8 @@ def get_dns_records(name, typ, resolver=None):
logger.warning(
_("DNS resolution timeout, unable to query %s at the moment") %
name, exc_info=e)
except dns.name.NameTooLong as e:
logger.error(_("DNS name is too long: %s" % name), exc_info=e)
else:
return dns_answers
return None
@@ -0,0 +1,18 @@
# Generated by Django 2.2.12 on 2020-12-04 08:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin', '0017_alarm'),
]
operations = [
migrations.AlterField(
model_name='alarm',
name='title',
field=models.TextField(),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 2.2.17 on 2021-01-28 13:40
from django.db import migrations
def update_disabled_accounts_aliases(apps, schema_editor):
User = apps.get_model('core', 'User')
Alias = apps.get_model('admin', 'Alias')
disabled_accounts = User.objects.filter(
mailbox__isnull=False, is_active=False)
Alias.objects.filter(
address__in=list(disabled_accounts.values_list('email', flat=True)),
enabled=True,
).update(enabled=False)
class Migration(migrations.Migration):
dependencies = [
('admin', '0018_auto_20201204_0935'),
('core', '0022_user_tfa_enabled'),
]
operations = [
migrations.RunPython(update_disabled_accounts_aliases),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2021-01-30 22:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin', '0019_update_disabled_accounts_aliases'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(help_text='The domain name', max_length=253, unique=True, verbose_name='name'),
),
]
+1 -1
View File
@@ -33,7 +33,7 @@ class Alarm(models.Model):
db_index=True
)
title = models.CharField(max_length=150)
title = models.TextField()
internal_name = models.CharField(max_length=120)
objects = models.Manager.from_queryset(AlarmQuerySet)()
+1 -1
View File
@@ -23,7 +23,7 @@ from . import mixins
class Domain(mixins.MessageLimitMixin, AdminObject):
"""Mail domain."""
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
name = models.CharField(ugettext_lazy("name"), max_length=253, unique=True,
help_text=ugettext_lazy("The domain name"))
quota = models.PositiveIntegerField(
default=0,
+1 -11
View File
@@ -1,8 +1,5 @@
"""Models related to mailboxes management."""
import os
import pwd
from reversion import revisions as reversion
from django.contrib.contenttypes.models import ContentType
@@ -153,14 +150,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject):
if not admin_params.get("handle_mailboxes"):
return None
if self.__mail_home is None:
curuser = pwd.getpwuid(os.getuid()).pw_name
mbowner = admin_params["mailboxes_owner"]
options = {}
if curuser != mbowner:
options["sudo_user"] = mbowner
code, output = doveadm_cmd(
"user -f home %s" % self.full_address, **options
)
code, output = doveadm_cmd("user -f home %s" % self.full_address)
if code:
raise lib_exceptions.InternalError(
_("Failed to retrieve mailbox location (%s)") % output)
+7 -1
View File
@@ -119,8 +119,10 @@ class AccountTestCase(ModoTestCase):
account = User.objects.get(username=values["username"])
self.assertEqual(account.language, "fr")
def test_forward_updated_on_edit(self):
def test_aliases_update_on_disable(self):
"""Check if aliases are updated when account is disabled."""
account = User.objects.get(username="user@test.com")
internal = models.Alias.objects.get(address=account.email, internal=True)
forward = models.Alias.objects.create(address=account.email, internal=False)
values = {
"username": "user@test.com",
@@ -133,12 +135,16 @@ class AccountTestCase(ModoTestCase):
self.ajax_post(
reverse("admin:account_change", args=[account.id]), values
)
internal.refresh_from_db()
self.assertFalse(internal.enabled)
forward.refresh_from_db()
self.assertFalse(forward.enabled)
values["is_active"] = True
self.ajax_post(
reverse("admin:account_change", args=[account.id]), values
)
internal.refresh_from_db()
self.assertTrue(internal.enabled)
forward.refresh_from_db()
self.assertTrue(forward.enabled)
+4 -2
View File
@@ -10,6 +10,7 @@ from django.test import override_settings
from django.urls import reverse
from rest_framework.authtoken.models import Token
from reversion.models import Version
from modoboa.admin import models as admin_models
from modoboa.core import factories as core_factories, models as core_models
@@ -51,8 +52,9 @@ class DomainAPITestCase(ModoAPITestCase):
url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10}
)
self.assertEqual(response.status_code, 201)
self.assertTrue(
models.Domain.objects.filter(name="test3.com").exists())
domain = models.Domain.objects.get(name="test3.com")
self.assertEqual(Version.objects.get_for_object(domain).count(), 1)
response = self.client.post(url, {})
self.assertEqual(response.status_code, 400)
self.assertIn("name", response.data)
+6 -3
View File
@@ -51,9 +51,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
password_scheme = forms.ChoiceField(
label=ugettext_lazy("Default password scheme"),
choices=[(hasher.name, ugettext_lazy(hasher.label))
for hasher in PasswordHasher.get_password_hashers()
if hasher().scheme in get_dovecot_schemes()],
choices=[],
initial="sha512crypt",
help_text=ugettext_lazy("Scheme used to crypt mailbox passwords"),
)
@@ -530,6 +528,11 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
self._add_visibilty_rules(
sms_backends.get_all_backend_visibility_rules()
)
self.fields["password_scheme"].choices = [
(hasher.name, ugettext_lazy(hasher.label))
for hasher in PasswordHasher.get_password_hashers()
if hasher().scheme in get_dovecot_schemes()
]
def _add_dynamic_fields(self):
new_fields = OrderedDict()
@@ -114,15 +114,15 @@ MIDDLEWARE = (
)
AUTHENTICATION_BACKENDS = (
# 'modoboa.lib.authbackends.LDAPBackend',
# 'modoboa.lib.authbackends.SMTPBackend',
#'modoboa.lib.authbackends.LDAPBackend',
#'modoboa.lib.authbackends.SMTPBackend',
'django.contrib.auth.backends.ModelBackend',
)
# SMTP authentication
# AUTH_SMTP_SERVER_ADDRESS = 'localhost'
# AUTH_SMTP_SERVER_PORT = 25
# AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
#AUTH_SMTP_SERVER_ADDRESS = 'localhost'
#AUTH_SMTP_SERVER_PORT = 25
#AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
TEMPLATES = [
@@ -205,9 +205,17 @@ SPECTACULAR_SETTINGS = {
}
# Modoboa settings
# MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
# DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
#MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
# Path to Dovecot binaries in case of a non-standard installation
#DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
#DOVEADM_LOOKUP_PATH = ('/path/to/doveadm', )
# List of supported schemes if doveadm is not available, given by: doveadm pw -l
#DOVECOT_SUPPORTED_SCHEMES = 'SHA512-CRYPT SHA256-CRYPT'
# DOVECOT_USER = 'vmail'
MODOBOA_API_URL = 'https://api.modoboa.org/1/'
+21 -12
View File
@@ -2,6 +2,7 @@
Password hashers for Modoboa.
"""
from django.conf import settings
from django.utils.encoding import smart_text
from modoboa.core.password_hashers.advanced import ( # NOQA:F401
@@ -32,17 +33,25 @@ def get_password_hasher(scheme):
def get_dovecot_schemes():
"""Return schemes supported by dovecot"""
supported_schemes = None
try:
_, schemes = doveadm_cmd("pw -l")
except OSError:
pass
else:
supported_schemes = ["{{{}}}".format(smart_text(scheme))
for scheme in schemes.split()]
"""Return schemes supported by Dovecot.
if not supported_schemes:
supported_schemes = ['{PLAIN}']
It first try to get supported schemes fron the settings, then from the
doveadm output, and fallback to {MD5-CRYPT} and {PLAIN} if the command
is not found.
return supported_schemes
:return: A list of supported '{SCHEME}'
"""
schemes = getattr(settings, "DOVECOT_SUPPORTED_SCHEMES", None)
default_schemes = "MD5-CRYPT PLAIN"
if not schemes:
try:
retcode, schemes = doveadm_cmd("pw -l")
except OSError:
schemes = default_schemes
else:
if retcode:
schemes = default_schemes
return ["{{{}}}".format(smart_text(scheme))
for scheme in schemes.split()]
+25 -4
View File
@@ -19,9 +19,9 @@ from modoboa.core.password_hashers import (
from modoboa.lib.tests import NO_SMTP, ModoTestCase
from .. import factories, models
DOVEADM_TEST_PATH = "{}/doveadm".format(os.path.dirname(__file__))
@override_settings(
DOVEADM_LOOKUP_PATH=["{}/doveadm".format(os.path.dirname(__file__))])
class AuthenticationTestCase(ModoTestCase):
"""Validate authentication scenarios."""
@@ -89,6 +89,7 @@ class AuthenticationTestCase(ModoTestCase):
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.endswith(reverse("core:dashboard")))
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH])
def test_password_schemes(self):
"""Validate password scheme changes."""
username = "user@test.com"
@@ -130,6 +131,7 @@ class AuthenticationTestCase(ModoTestCase):
self.assertTrue(user.password.startswith(pw_hash.scheme))
@skipUnless(argon2, "argon2-cffi not installed")
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH])
def test_password_argon2_parameter_change(self):
"""Validate hash parameter update on login works with argon2."""
username = "user@test.com"
@@ -169,8 +171,16 @@ class AuthenticationTestCase(ModoTestCase):
self.assertEqual(parameters.memory_cost, 1000)
self.assertEqual(parameters.parallelism, 2)
def test_supported_schemes(self):
"""Validate dovecot supported schemes."""
@mock.patch("modoboa.lib.sysutils.exec_cmd")
def test_dovecot_supported_schemes(self, exec_cmd_mock):
"""Validate Dovecot supported schemes with fake command output."""
exec_cmd_mock.return_value = (
0,
"MD5 MD5-CRYPT SHA SHA1 SHA256 SHA512 SMD5 SSHA SSHA256 SSHA512 "
"PLAIN CLEAR CLEARTEXT PLAIN-TRUNC CRAM-MD5 SCRAM-SHA-1 HMAC-MD5 "
"DIGEST-MD5 PLAIN-MD4 PLAIN-MD5 LDAP-MD5 LANMAN NTLM OTP SKEY RPA "
"PBKDF2 CRYPT SHA256-CRYPT SHA512-CRYPT"
)
supported_schemes = get_dovecot_schemes()
self.assertEqual(supported_schemes,
["{MD5}",
@@ -204,6 +214,17 @@ class AuthenticationTestCase(ModoTestCase):
"{SHA256-CRYPT}",
"{SHA512-CRYPT}"])
@override_settings(DOVECOT_SUPPORTED_SCHEMES="SHA1 SHA512-CRYPT")
def test_dovecot_supported_schemes_from_settings(self):
"""Validate dovecot supported schemes from the settings."""
supported_schemes = get_dovecot_schemes()
self.assertEqual(supported_schemes, ["{SHA1}", "{SHA512-CRYPT}"])
def test_dovecot_default_schemes(self):
"""Check default scheme if doveadm is not found."""
supported_schemes = get_dovecot_schemes()
self.assertEqual(supported_schemes, ["{MD5-CRYPT}", "{PLAIN}"])
class PasswordResetTestCase(ModoTestCase):
"""Test password reset service."""
@@ -0,0 +1,38 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# 李 正 <364109980@qq.com>, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-06 13:15+0200\n"
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
"Last-Translator: 李 正 <364109980@qq.com>, 2021\n"
"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: ldapsync/lib.py:84
msgid "Failed to create LDAP account: {}"
msgstr "无法创建LDAP帐户: {}"
#: ldapsync/lib.py:124
msgid "Failed to update LDAP account: {}"
msgstr "无法更新LDAP帐户: {}"
#: ldapsync/lib.py:138
msgid "Failed to delete LDAP account: {}"
msgstr "无法删除LDAP帐户: {}"
#: ldapsync/lib.py:149
msgid "Failed to disable LDAP account: {}"
msgstr "无法关闭LDAP帐户: {}"
+7 -4
View File
@@ -5,6 +5,7 @@ This module extra functions/shortcuts to communicate with the system
import inspect
import os
import pwd
import re
import subprocess
@@ -42,8 +43,7 @@ def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs):
return process.returncode, output
def doveadm_cmd(params, sudo_user=None, pinput=None,
capture_output=True, **kwargs):
def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs):
"""Execute doveadm command.
Run doveadm command using the current user. Set :keyword:`sudo_user` if
@@ -69,14 +69,17 @@ def doveadm_cmd(params, sudo_user=None, pinput=None,
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
dpath = fpath
break
dovecot_user = getattr(settings, "DOVECOT_USER", "vmail")
curuser = pwd.getpwuid(os.getuid()).pw_name
sudo_user = dovecot_user if curuser != dovecot_user else None
if dpath:
return exec_cmd("{} {}".format(dpath, params),
sudo_user=sudo_user,
pinput=pinput,
capture_output=capture_output,
**kwargs)
else:
raise OSError("doveadm command not found")
raise OSError("doveadm command not found")
def guess_extension_name():
+9
View File
@@ -1,6 +1,7 @@
"""ViewSet related mixins and tools."""
from rest_framework import viewsets
from reversion.views import RevisionMixin
class ExpandableSerializerMixin:
@@ -32,3 +33,11 @@ class ExpandableModelViewSet(ExpandableSerializerMixin,
"""
pass
class RevisionModelMixin(RevisionMixin):
"""
A mixin to wrap every request in a revision block.
"""
pass
+12 -3
View File
@@ -5,15 +5,16 @@
#
# Translators:
# Antoine Nguyen <tonio@ngyn.org>, 2020
# Nicolas John, 2020
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-09 16:48+0100\n"
"POT-Creation-Date: 2020-11-05 15:47+0100\n"
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>, 2020\n"
"Last-Translator: Nicolas John, 2020\n"
"Language-Team: German (https://www.transifex.com/tonio/teams/13749/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -41,7 +42,7 @@ msgstr "Warnung"
msgid "Cancel"
msgstr "Abbrechen"
#: static/js/global.js:342
#: static/js/global.js:347
msgid "Internal error"
msgstr "Interner Fehler"
@@ -56,3 +57,11 @@ msgstr "Suche"
#: static/js/searchbar.js:28
msgid "Search..."
msgstr "Suchen..."
#: static/js/twocols_nav.js:170
msgid "Are you sure?"
msgstr "Sind Sie sicher?"
#: static/js/twocols_nav.js:171
msgid "This will invalidate your registered application."
msgstr "Dadurch wird Ihre registrierte Anwendung ungültig."
@@ -0,0 +1,66 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# 李 正 <364109980@qq.com>, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-05 15:47+0100\n"
"PO-Revision-Date: 2020-05-11 18:45+0000\n"
"Last-Translator: 李 正 <364109980@qq.com>, 2021\n"
"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: bower_components/jquery-ui/jquery-ui.min.js:8
msgid "'"
msgstr "'"
#: bower_components/swagger-ui/dist/swagger-ui-bundle.js:133
msgid "operation"
msgstr "操作"
#: core/static/core/js/settings.js:16
msgid "No more log entry to show"
msgstr "没有更多的日志可以显示"
#: static/js/confirmation.js:44
msgid "Warning"
msgstr "警告"
#: static/js/confirmation.js:59
msgid "Cancel"
msgstr "取消"
#: static/js/global.js:347
msgid "Internal error"
msgstr "内部错误"
#: static/js/listing.js:21
msgid "No more entry to show"
msgstr "没有更多的条目可以显示"
#: static/js/listing.js:46
msgid "Search"
msgstr "查找"
#: static/js/searchbar.js:28
msgid "Search..."
msgstr "查找..."
#: static/js/twocols_nav.js:170
msgid "Are you sure?"
msgstr "你确定吗?"
#: static/js/twocols_nav.js:171
msgid "This will invalidate your registered application."
msgstr "这会使您的注册申请无效。"
+2 -1
View File
@@ -4,10 +4,11 @@ from rest_framework import viewsets
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
from modoboa.admin import models as admin_models
from modoboa.lib.viewsets import RevisionModelMixin
from . import serializers
class RelayDomainViewSet(viewsets.ModelViewSet):
class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet):
"""RelayDomain viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
+1 -1
View File
@@ -1 +1 @@
mysqlclient<2.0.2
mysqlclient<2.0.4
+2 -2
View File
@@ -2,7 +2,7 @@ django>=2.2,<2.3
django-braces==1.14.0
django-ckeditor==6.0.0
django-phonenumber-field[phonenumbers]==5.0.0
django-reversion==3.0.8
django-reversion==3.0.9
django-xforwardedfor-middleware==2.0
django-webpack-loader
django-otp==1.0.2
@@ -35,7 +35,7 @@ chardet
ovh
oath
redis
rrdtool>=0.1.11
rrdtool>=0.1.10
qrcode
aiosmtplib
+2
View File
@@ -21,3 +21,5 @@ max-line-length = 80
[flake8]
exclude = migrations
max-line-length = 80
per-file-ignores =
modoboa/core/commands/templates/settings.py.tpl: E265
+1 -1
View File
@@ -1,6 +1,6 @@
argon2-cffi>=16.1.0
factory-boy<=2.12.0
httmock==1.4.0
testfixtures==6.15.0
testfixtures==6.17.1
tox
asynctest