Merge pull request #3107 from modoboa/doveadm-caching

Doveadm caching
This commit is contained in:
Antoine Nguyen
2023-12-20 14:56:48 +01:00
committed by GitHub
49 changed files with 6403 additions and 4757 deletions
+1
View File
@@ -33,3 +33,4 @@ test_project/modoboa.db
# Misc
.ash_history
modoboa.kdev4
.kateproject
+61
View File
@@ -131,6 +131,67 @@ Rebuild Virtual Environment
Specific instructions
*********************
2.2.3
=====
* Reduced the number of call to doveadm
You need to setup a new worker :
1. Create a new supervisord config (``/etc/supervisor/conf.d/modoboa-base-worker.conf`` by default) :
.. sourcecode:: ini
[program:modoboa-base-worker]
autostart=true
autorestart=true
command={%python env path%} {% manage.py instance path%} rqworker modoboa
directory={%modoboa home dir%}
user=%{modoboa user}
numprocs=1
stopsignal=TERM
``python env path`` : Python executable located in your virtual environment created for modoboa.
You will find it here ``/srv/modoboa/venv/bin/python`` by default.
``manage.py instance path``: Path to manage.py of your modoboa instance.
You will find it here : ``/srv/modoboa/instance/manage.py``by default.
``Modoboa home dir``: Home dir of the user running modooba.
You will find it here ``/srv/modoboa/`` by default.
``modoboa user`` : User managing dkim signing (modoboa by default).
You can help you with ``/etc/supervisor/conf.d/policyd.conf`` (by default).
Then restart supervisor. ``#> supervisorctl reread && supervisorctl update`` on Debian.
2. Update the RQ_QUEUES section bellow the ``#REDIS`` section:
.. sourcecode:: python
# RQ
RQ_QUEUES = {
...
},
'modoboa': {
'HOST': REDIS_HOST,
'PORT': REDIS_PORT,
'DB': 0,
},
}
3. Add the CACHE section bellow the ``# RQ`` one:
.. sourcecode:: python
# CACHE
CACHES = {
"default": {
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
"LOCATION": REDIS_URL
}
}
2.2.0
=====
+1 -1
View File
@@ -16,7 +16,7 @@ services:
DEBUG: "True"
REDIS_HOST: redis
command: >
sh -c "cd test_project; python3 manage.py rqworker dkim"
sh -c "cd test_project; python3 manage.py rqworker dkim modoboa"
volumes:
- .:/code
+4 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.9-alpine as base
FROM python:3.12-alpine as base
MAINTAINER Antoine Nguyen <tonio@ngyn.org>
ENV VIRTUAL_ENV=/opt/venv
@@ -17,8 +17,10 @@ RUN python -m pip install -U pip
RUN pip install -r requirements.txt -r test-requirements.txt
FROM base as run
COPY docker/doveadm /usr/local/bin
RUN chmod +x /usr/local/bin/doveadm
COPY --from=build $VIRTUAL_ENV $VIRTUAL_ENV
RUN apk add --no-cache --update openssl librrd libxslt libjpeg libressl git rrdtool libmagic \
RUN apk add --no-cache --update openssl librrd libxslt libjpeg libressl git rrdtool libmagic sudo \
&& rm -rf /var/cache/apk/*
RUN mkdir /code
WORKDIR /code
+4 -1
View File
@@ -1,7 +1,10 @@
FROM node:16.10-alpine
FROM node:16.10-alpine
ENV HOME=/home/node/
RUN apk add --no-cache --update gettext \
&& rm -rf /var/cache/apk/*
WORKDIR $HOME
RUN yarn install
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo "SHA1 SSHA512 SCRAM-SHA-256 BLF-CRYPT PLAIN HMAC-MD5 OTP SHA512 SHA DES-CRYPT CRYPT SSHA MD5-CRYPT PLAIN-MD4 PLAIN-MD5 SCRAM-SHA-1 SHA512-CRYPT CLEAR CLEARTEXT ARGON2I ARGON2ID SSHA256 MD5 PBKDF2 SHA256 CRAM-MD5 PLAIN-TRUNC SHA256-CRYPT SMD5 DIGEST-MD5 LDAP-MD5"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -16,6 +16,7 @@
"debounce": "^1.2.1",
"js-cookie": "^2.2.1",
"luxon": "^1.28.1",
"qrcode.vue": "~1.7.0",
"vee-validate": "^3.4.5",
"vue": "^2.6.14",
"vue-apexcharts": "^1.6.0",
@@ -23,8 +24,7 @@
"vue-gettext": "^2.1.12",
"vue-router": "^3.2.0",
"vuetify": "^2.6.10",
"vuex": "^3.4.0",
"qrcode.vue": "~1.7.0"
"vuex": "^3.4.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
+3 -3
View File
@@ -2517,9 +2517,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001407:
version "1.0.30001481"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz"
integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==
version "1.0.30001553"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz"
integrity sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0"
+2 -2
View File
@@ -75,6 +75,8 @@ class DomainAPITestCase(ModoAPITestCase):
@mock.patch("socket.getaddrinfo")
def test_create_domain_with_mx_check(self, mock_getaddrinfo, mock_query):
"""Check domain creation when MX check is activated."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
self.set_global_parameter("enable_admin_limits", False, app="limits")
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
self.set_global_parameter("domains_must_have_authorized_mx", True)
@@ -84,8 +86,6 @@ class DomainAPITestCase(ModoAPITestCase):
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
url = reverse("v1:domain-list")
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
response = self.client.post(
url, {"name": "no-mx.example.com", "quota": 0,
"default_mailbox_quota": 10}
+3 -2
View File
@@ -210,6 +210,9 @@ class DomainTestCase(ModoTestCase):
@mock.patch("socket.getaddrinfo")
def test_create_and_check_mx(self, mock_getaddrinfo, mock_query):
"""Check for authorized MX record."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
reseller = core_factories.UserFactory(
username="reseller", groups=("Resellers", ))
self.client.force_login(reseller)
@@ -218,8 +221,6 @@ class DomainTestCase(ModoTestCase):
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
self.set_global_parameter("domains_must_have_authorized_mx", True)
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
values = {
"name": "no-mx.example.com", "quota": 0,
"default_mailbox_quota": 0,
+2 -2
View File
@@ -83,14 +83,14 @@ domainalias; domalias1.com; domain1.com; True
@mock.patch("socket.getaddrinfo")
def test_domain_import_with_mx_check(self, mock_getaddrinfo, mock_query):
"""Check domain import when MX check is enabled."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
reseller = core_factories.UserFactory(
username="reseller", groups=("Resellers", ))
self.client.force_login(reseller)
self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1")
self.set_global_parameter("domains_must_have_authorized_mx", True)
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
f = ContentFile(
b"domain; test3.com; 100; 1; True", name="domains.csv")
resp = self.client.post(
+2 -1
View File
@@ -36,6 +36,7 @@ _DNE_MX_RECORD = MX("IN", "MX", 10, Name(
_MX_RECORDS = [_MX_RECORD_1]
_IP_SET_RECORDS = [RRset()]
_IPV4_RECORD_REDIS = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", int(settings.REDIS_PORT)))
_IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25))
_IPV4_RECORD_2 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", 25))
_IPV6_RECORD = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("2001:db8::1", 25))
@@ -69,7 +70,7 @@ _POSSIBLE_DNS_RESULTS_NO_MX = {
"mx3.example.com": [_A_RECORD]
}
_POSSIBLE_IP_RESULTS = {
settings.REDIS_HOST: socket.getaddrinfo(settings.REDIS_HOST, settings.REDIS_PORT),
settings.REDIS_HOST: [_IPV4_RECORD_REDIS],
"test3.com": [_IPV4_RECORD_2],
"mx2.example.com": [_IPV4_RECORD_2],
"bad-response.example.com": [_BAD_IP_RECORD],
+10 -12
View File
@@ -21,7 +21,9 @@ from django.utils.translation import gettext_lazy, gettext as _
from rest_framework import serializers, status
from rest_framework.exceptions import APIException
from modoboa.core import constants, models, sms_backends, app_settings, context_processors
from modoboa.core import (
constants, models, sms_backends, app_settings, context_processors
)
from modoboa.core.models import User
from modoboa.lib import fields as lib_fields, cryptutils
@@ -62,21 +64,15 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
default="local"
)
password_scheme = serializers.ChoiceField(
choices=[("sha512crypt", "sha512crypt"),
("sha256crypt", "sha256crypt"),
("blfcrypt", "bcrypt"),
("md5crypt", gettext_lazy("md5crypt (weak)")),
("sha256", gettext_lazy("sha256 (weak)")),
("md5", gettext_lazy("md5 (weak)")),
("crypt", gettext_lazy("crypt (weak)")),
("plain", gettext_lazy("plain (weak)"))],
default="sha512crypt"
choices=[("sha512crypt", "sha512crypt")],
required=False
)
rounds_number = serializers.IntegerField(default=70000)
update_scheme = serializers.BooleanField(default=True)
default_password = serializers.CharField(default="password")
default_password = serializers.CharField(default="ChangeMe1!")
random_password_length = serializers.IntegerField(min_value=8, default=8)
update_password_url = serializers.URLField(required=False, allow_blank=True)
update_password_url = serializers.URLField(
required=False, allow_blank=True)
password_recovery_msg = serializers.CharField(
required=False, allow_blank=True)
sms_password_recovery = serializers.BooleanField(default=False)
@@ -175,6 +171,8 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
**definition["attrs"])
# Choices serializer for default_top_redirection field
self.fields["default_top_redirection"].choices = app_settings.enabled_applications()
# Populate choices of password_scheme
self.fields["password_scheme"].choices = app_settings.get_password_scheme()
def validate_ldap_user_dn_template(self, value):
try:
+19
View File
@@ -1,18 +1,26 @@
"""Core API related tests."""
import copy
import getpass
import oath
from unittest import mock
from django.core.cache import cache
from django.test import override_settings
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django_rq import get_worker, get_queue
from modoboa.admin import factories
from modoboa.core import models
from modoboa.core.tests import utils
from modoboa.lib.tests import ModoAPITestCase
from rest_framework.authtoken.models import Token
DOVEADM_TEST_PATH = utils.get_doveadm_test_path()
DOVECOT_USER = getpass.getuser()
CORE_SETTINGS = {
"authentication_type": "local",
"password_scheme": "sha512crypt",
@@ -70,12 +78,23 @@ CORE_SETTINGS = {
class ParametersAPITestCase(ModoAPITestCase):
@override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH],
DOVECOT_USER=DOVECOT_USER)
def test_update(self):
cache.delete("password_scheme_choice")
queue = get_queue("modoboa", is_async=False)
queue.empty()
url = reverse("v2:parameter-detail", args=["core"])
data = copy.copy(CORE_SETTINGS)
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
get_worker("modoboa").work(burst=True)
self.assertIsNotNone(cache.get("password_scheme_choice"))
self.assertIn(('plain', 'plain (weak)'),
cache.get("password_scheme_choice")
)
# Modify SMS related settings
data["sms_password_recovery"] = True
resp = self.client.put(url, data, format="json")
+23 -7
View File
@@ -3,10 +3,14 @@
import collections
from django import forms
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import password_validation
from django.utils.translation import gettext as _, gettext_lazy
from django_rq import get_queue
from modoboa.core.jobs import job_retrieve_available_hashers
from modoboa.core.password_hashers import get_dovecot_schemes
from modoboa.core.password_hashers.base import PasswordHasher
from modoboa.lib import fields as lib_fields
@@ -31,6 +35,23 @@ def enabled_applications():
return sorted(result, key=lambda e: e[0])
def get_default_password_scheme():
available_schemes = get_dovecot_schemes()
return [
(hasher.name, gettext_lazy(hasher.label))
for hasher in PasswordHasher.get_password_hashers()
if hasher().scheme in available_schemes
]
def get_password_scheme():
available_schemes = cache.get("password_scheme_choice")
if available_schemes is None:
get_queue("modoboa").enqueue(job_retrieve_available_hashers)
return get_default_password_scheme()
return available_schemes
class GeneralParametersForm(param_forms.AdminParametersForm):
"""General parameters."""
@@ -73,7 +94,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
default_password = forms.CharField(
label=gettext_lazy("Default password"),
initial="password",
initial="ChangeMe1!",
help_text=gettext_lazy(
"Default password for automatically created accounts.")
)
@@ -535,12 +556,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm):
self._add_visibilty_rules(
sms_backends.get_all_backend_visibility_rules()
)
available_schemes = get_dovecot_schemes()
self.fields["password_scheme"].choices = [
(hasher.name, gettext_lazy(hasher.label))
for hasher in PasswordHasher.get_password_hashers()
if hasher().scheme in available_schemes
]
self.fields["password_scheme"].choices = get_password_scheme()
def _add_dynamic_fields(self):
new_fields = collections.OrderedDict()
@@ -262,8 +262,23 @@ RQ_QUEUES = {
'PORT': REDIS_PORT,
'DB': 0,
},
'modoboa': {
'HOST': REDIS_HOST,
'PORT': REDIS_PORT,
'DB': 0,
},
}
# CACHE
CACHES = {
"default": {
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
"LOCATION": REDIS_URL
}
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+9
View File
@@ -0,0 +1,9 @@
from rq import get_current_job
from modoboa.core.password_hashers import cache_available_password_hasher
def job_retrieve_available_hashers():
if get_current_job() is None:
return
cache_available_password_hasher()
+1 -2
View File
@@ -1,7 +1,6 @@
# Generated by Django 1.9.5 on 2016-06-15 18:12
from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields
class Migration(migrations.Migration):
@@ -17,7 +16,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('api_pk', models.PositiveIntegerField(null=True)),
('api_versions', jsonfield.fields.JSONField()),
('api_versions', models.JSONField()),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
@@ -1,6 +1,5 @@
# Generated by Django 1.9.5 on 2016-10-26 08:11
from django.db import migrations
import jsonfield.fields
from django.db import migrations, models
def set_parameter(store, parameter):
@@ -51,13 +50,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='localconfig',
name='_parameters',
field=jsonfield.fields.JSONField(default={}),
field=models.JSONField(default=dict),
preserve_default=False,
),
migrations.AddField(
model_name='user',
name='_parameters',
field=jsonfield.fields.JSONField(default={}),
field=models.JSONField(default=dict),
preserve_default=False,
),
migrations.RunPython(move_parameters, clear_parameters)
@@ -1,6 +1,5 @@
# Generated by Django 1.9.5 on 2016-11-04 21:17
from django.db import migrations
import jsonfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,11 +12,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='localconfig',
name='_parameters',
field=jsonfield.fields.JSONField(default={}),
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='user',
name='_parameters',
field=jsonfield.fields.JSONField(default={}),
field=models.JSONField(default=dict),
),
]
@@ -0,0 +1,49 @@
# Generated by Django 4.2.6 on 2023-10-24 17:14
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0025_rename_user_email_is_active_core_user_email_c0c03f_idx'),
]
operations = [
migrations.AlterField(
model_name='localconfig',
name='_parameters',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='localconfig',
name='api_versions',
field=models.JSONField(),
),
migrations.AlterField(
model_name='user',
name='_parameters',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='localconfig',
name='api_versions',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='localconfig',
name='_parameters',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='localconfig',
name='api_versions',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='user',
name='_parameters',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
]
+4 -4
View File
@@ -3,6 +3,7 @@
import re
from email.header import Header
from django.core.serializers.json import DjangoJSONEncoder
from django.conf import settings
from django.db import models
from django.urls import reverse
@@ -14,7 +15,6 @@ from django.contrib.auth.models import AbstractUser, Group
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
import jsonfield
from phonenumber_field.modelfields import PhoneNumberField
from reversion import revisions as reversion
@@ -74,7 +74,7 @@ class User(AbstractUser):
tfa_enabled = models.BooleanField(default=False)
_parameters = jsonfield.JSONField(default={})
_parameters = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
class Meta(object):
ordering = ["username"]
@@ -447,9 +447,9 @@ class LocalConfig(models.Model):
site = models.ForeignKey("sites.Site", on_delete=models.CASCADE)
# API results cache
api_versions = jsonfield.JSONField()
api_versions = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
_parameters = jsonfield.JSONField(default={})
_parameters = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
# Dovecot LDAP update
need_dovecot_update = models.BooleanField(default=False)
+14 -1
View File
@@ -2,6 +2,7 @@
Password hashers for Modoboa.
"""
from django.core.cache import cache
from django.conf import settings
from django.utils.encoding import smart_str
@@ -10,7 +11,7 @@ from modoboa.core.password_hashers.advanced import ( # NOQA:F401
ARGON2IDHasher, SSHAHasher
)
from modoboa.core.password_hashers.base import ( # NOQA:F401
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher, PasswordHasher
)
from modoboa.lib.sysutils import doveadm_cmd
@@ -55,3 +56,15 @@ def get_dovecot_schemes():
return ["{{{}}}".format(smart_str(scheme))
for scheme in schemes.split()]
def cache_available_password_hasher():
available_schemes = get_dovecot_schemes()
password_scheme_choice = [
(hasher.name, hasher.label)
for hasher in PasswordHasher.get_password_hashers()
if hasher().scheme in available_schemes
]
cache.set("password_scheme_choice",
password_scheme_choice,
2592000)
+2 -2
View File
@@ -233,12 +233,12 @@ class SettingsTestCase(ModoTestCase):
settings = SETTINGS_SAMPLE.copy()
response = self.client.post(url, settings, format="json")
self.assertEqual(response.status_code, 200)
settings["core-password_scheme"] = "sha512crypt"
settings["core-password_scheme"] = "magic_password_scheme"
response = self.client.post(url, settings, format="json")
self.assertEqual(response.status_code, 400)
compare(response.json(), {
"form_errors": {"password_scheme": ["Select a valid choice. \
sha512crypt is not one of the available choices."]},
magic_password_scheme is not one of the available choices."]},
"prefix": "core"
})
settings["core-password_scheme"] = "plain"
+4
View File
@@ -0,0 +1,4 @@
import os
def get_doveadm_test_path():
return "{}/doveadm".format(os.path.dirname(__file__))
+1 -3
View File
@@ -1,8 +1,6 @@
# Generated by Django 1.10.7 on 2017-11-24 11:16
from django.db import migrations, models
import jsonfield.fields
class Migration(migrations.Migration):
@@ -19,7 +17,7 @@ class Migration(migrations.Migration):
('pattern', models.CharField(max_length=254, unique=True, verbose_name='pattern')),
('service', models.CharField(max_length=30, verbose_name='service')),
('next_hop', models.CharField(blank=True, max_length=100, verbose_name='next hop')),
('_settings', jsonfield.fields.JSONField(default={})),
('_settings', models.JSONField(default=dict)),
],
options={
'ordering': ['pattern'],
@@ -0,0 +1,24 @@
# Generated by Django 4.2.6 on 2023-10-24 17:14
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transport', '0002_auto_20180928_1520'),
]
operations = [
migrations.AlterField(
model_name='transport',
name='_settings',
field=models.JSONField(default=dict),
),
migrations.AlterField(
model_name='transport',
name='_settings',
field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
]
+2 -2
View File
@@ -1,8 +1,8 @@
"""Transport models."""
import jsonfield
from reversion import revisions as reversion
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -17,7 +17,7 @@ class Transport(models.Model):
service = models.CharField(_("service"), max_length=30)
next_hop = models.CharField(_("next hop"), max_length=100, blank=True)
_settings = jsonfield.JSONField(default={})
_settings = models.JSONField(default=dict, encoder=DjangoJSONEncoder)
class Meta:
ordering = ["pattern"]
-1
View File
@@ -21,7 +21,6 @@ asgiref
dnspython==2.4.2
feedparser==6.0.11
gevent==23.9.1
jsonfield==3.1.0
Pillow # Optional dependency for django-ckeditor
progressbar33==2.4
python-dateutil
+14
View File
@@ -225,6 +225,8 @@ SPECTACULAR_SETTINGS = {
# MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")
# DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
DOVECOT_USER="root"
MODOBOA_API_URL = 'https://api.modoboa.org/1/'
@@ -243,6 +245,18 @@ RQ_QUEUES = {
'dkim': {
'URL': REDIS_URL,
},
'modoboa': {
'URL': REDIS_URL,
},
}
# CACHE
CACHES = {
"default": {
"BACKEND": 'django.core.cache.backends.redis.RedisCache',
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}"
}
}
# Password validation