diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
index 8d5f0c295..d3219bddc 100644
--- a/docker/Dockerfile.dev
+++ b/docker/Dockerfile.dev
@@ -25,7 +25,7 @@ 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 sudo \
+RUN apk add --no-cache --update openssl librrd libxslt libjpeg libressl git rrdtool libmagic sudo sqlite \
&& rm -rf /var/cache/apk/*
RUN mkdir /code
WORKDIR /code
diff --git a/frontend/src/components/account/AutoReplyForm.vue b/frontend/src/components/account/AutoReplyForm.vue
index ee0593123..c3bdef981 100644
--- a/frontend/src/components/account/AutoReplyForm.vue
+++ b/frontend/src/components/account/AutoReplyForm.vue
@@ -20,7 +20,11 @@
{
- busStore.displayNotification({ msg: $gettext('Auto-reply message updated') })
- loading.value = false
- }).catch(() => {
- loading.value = false
- })
+ if (form.value.fromdate === '') {
+ form.value.fromdate = null
+ }
+ if (form.value.untildate === '') {
+ form.value.untildate = null
+ }
+ accountApi
+ .setARMessage(form.value)
+ .then(() => {
+ busStore.displayNotification({
+ msg: $gettext('Auto-reply message updated'),
+ })
+ loading.value = false
+ })
+ .catch(() => {
+ loading.value = false
+ })
}
-accountApi.getARMessage().then(resp => {
+accountApi.getARMessage().then((resp) => {
form.value = resp.data
if (form.value.fromdate) {
form.value.fromdate = form.value.fromdate.slice(0, -4)
diff --git a/modoboa/autoreply/__init__.py b/modoboa/autoreply/__init__.py
new file mode 100644
index 000000000..346848e87
--- /dev/null
+++ b/modoboa/autoreply/__init__.py
@@ -0,0 +1,3 @@
+"""Away message editor for Modoboa."""
+
+default_app_config = "modoboa.autoreply.apps.AutoreplyConfig"
diff --git a/modoboa/postfix_autoreply/api/__init__.py b/modoboa/autoreply/api/__init__.py
similarity index 100%
rename from modoboa/postfix_autoreply/api/__init__.py
rename to modoboa/autoreply/api/__init__.py
diff --git a/modoboa/postfix_autoreply/api/v2/__init__.py b/modoboa/autoreply/api/v2/__init__.py
similarity index 100%
rename from modoboa/postfix_autoreply/api/v2/__init__.py
rename to modoboa/autoreply/api/v2/__init__.py
diff --git a/modoboa/postfix_autoreply/api/v2/serializers.py b/modoboa/autoreply/api/v2/serializers.py
similarity index 67%
rename from modoboa/postfix_autoreply/api/v2/serializers.py
rename to modoboa/autoreply/api/v2/serializers.py
index 5de9f763f..e2aeab258 100644
--- a/modoboa/postfix_autoreply/api/v2/serializers.py
+++ b/modoboa/autoreply/api/v2/serializers.py
@@ -5,14 +5,14 @@ from django.utils.translation import gettext as _
from rest_framework import serializers
-from modoboa.postfix_autoreply import models
+from modoboa.autoreply import models
-class PostfixAutoreplySettingsSerializer(serializers.Serializer):
+class AutoreplySettingsSerializer(serializers.Serializer):
"""A serializer for global parameters."""
# General
- autoreplies_timeout = serializers.IntegerField(default=86400)
+ tracking_period = serializers.IntegerField(default=7)
default_subject = serializers.CharField(default="I'm off")
default_content = serializers.CharField(
default="""I'm currently off. I'll answer as soon as I come back.
@@ -22,6 +22,11 @@ Best regards,
"""
)
+ def validate_tracking_period(self, value):
+ if value < 1:
+ raise serializers.ValidationError(_("Value can't be less than 1 day"))
+ return value
+
class ARMessageSerializer(serializers.ModelSerializer):
"""A serializer for ARmessage."""
@@ -46,6 +51,16 @@ class ARMessageSerializer(serializers.ModelSerializer):
)
return data
+ def create(self, validated_data):
+ instance = super().create(validated_data)
+ instance.manage_sieve_rule(self.context["request"])
+ return instance
+
+ def update(self, instance, validated_data):
+ instance = super().update(instance, validated_data)
+ instance.manage_sieve_rule(self.context["request"])
+ return instance
+
class AccountARMessageSerializer(ARMessageSerializer):
"""Dedicated serializer for account operations."""
diff --git a/modoboa/postfix_autoreply/api/v2/tests.py b/modoboa/autoreply/api/v2/tests.py
similarity index 96%
rename from modoboa/postfix_autoreply/api/v2/tests.py
rename to modoboa/autoreply/api/v2/tests.py
index 0fd554432..4a9fb292f 100644
--- a/modoboa/postfix_autoreply/api/v2/tests.py
+++ b/modoboa/autoreply/api/v2/tests.py
@@ -6,10 +6,11 @@ from dateutil.relativedelta import relativedelta
from modoboa.admin import factories as admin_factories
from modoboa.core.models import User
from modoboa.lib.tests import ModoAPITestCase
-from modoboa.postfix_autoreply import factories, models
+from modoboa.sievefilters.api.v2.tests import PatcherMixin
+from modoboa.autoreply import factories, models
-class ARMessageViewSetTestCase(ModoAPITestCase):
+class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
"""API test case."""
@classmethod
@@ -124,7 +125,7 @@ class ARMessageViewSetTestCase(ModoAPITestCase):
)
-class AccountARMessageViewSetTestCase(ModoAPITestCase):
+class AccountARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
"""API test case."""
@classmethod
diff --git a/modoboa/postfix_autoreply/api/v2/urls.py b/modoboa/autoreply/api/v2/urls.py
similarity index 100%
rename from modoboa/postfix_autoreply/api/v2/urls.py
rename to modoboa/autoreply/api/v2/urls.py
diff --git a/modoboa/postfix_autoreply/api/v2/viewsets.py b/modoboa/autoreply/api/v2/viewsets.py
similarity index 94%
rename from modoboa/postfix_autoreply/api/v2/viewsets.py
rename to modoboa/autoreply/api/v2/viewsets.py
index 923112483..f9d5aab10 100644
--- a/modoboa/postfix_autoreply/api/v2/viewsets.py
+++ b/modoboa/autoreply/api/v2/viewsets.py
@@ -9,7 +9,7 @@ from modoboa.admin import models as admin_models
from modoboa.lib.email_utils import split_mailbox
from modoboa.parameters import tools as param_tools
-from modoboa.postfix_autoreply import models
+from modoboa.autoreply import models
from . import serializers
@@ -76,7 +76,7 @@ class AccountARMessageViewSet(viewsets.ViewSet):
def armessage(self, request):
if not hasattr(request.user, "mailbox"):
return response.Response(status=404)
- params = dict(param_tools.get_global_parameters("postfix_autoreply"))
+ params = dict(param_tools.get_global_parameters("autoreply"))
armessage, created = models.ARmessage.objects.get_or_create(
mbox=self.request.user.mailbox,
defaults={
@@ -88,7 +88,7 @@ class AccountARMessageViewSet(viewsets.ViewSet):
serializer = serializers.AccountARMessageSerializer(armessage)
return response.Response(serializer.data)
serializer = serializers.AccountARMessageSerializer(
- armessage, data=request.data
+ armessage, data=request.data, context={"request": request}
)
serializer.is_valid(raise_exception=True)
serializer.save()
diff --git a/modoboa/postfix_autoreply/app_settings.py b/modoboa/autoreply/app_settings.py
similarity index 82%
rename from modoboa/postfix_autoreply/app_settings.py
rename to modoboa/autoreply/app_settings.py
index a4503e1ff..fa76ebb92 100644
--- a/modoboa/postfix_autoreply/app_settings.py
+++ b/modoboa/autoreply/app_settings.py
@@ -1,10 +1,10 @@
-"""Postfix autoreply forms."""
+"""Autoreply settings."""
import collections
from django.utils.translation import gettext_lazy as _
-POSTFIX_AUTOREPLY_PARAMETERS_STRUCT = collections.OrderedDict(
+AUTOREPLY_PARAMETERS_STRUCT = collections.OrderedDict(
[
(
"general",
@@ -13,11 +13,11 @@ POSTFIX_AUTOREPLY_PARAMETERS_STRUCT = collections.OrderedDict(
"params": collections.OrderedDict(
[
(
- "autoreplies_timeout",
+ "tracking_period",
{
- "label": _("Automatic reply timeout"),
+ "label": _("Period between two auto-replies"),
"help_text": _(
- "Timeout in seconds between two auto-replies to the same recipient"
+ "Number of days to wait before sending a new auto-reply to the same recipient"
),
},
),
diff --git a/modoboa/postfix_autoreply/apps.py b/modoboa/autoreply/apps.py
similarity index 58%
rename from modoboa/postfix_autoreply/apps.py
rename to modoboa/autoreply/apps.py
index 8c288cbbf..48da4f013 100644
--- a/modoboa/postfix_autoreply/apps.py
+++ b/modoboa/autoreply/apps.py
@@ -11,20 +11,19 @@ def load_settings():
param_tools.registry.add2(
"global",
- "postfix_autoreply",
+ "autoreply",
gettext_lazy("Automatic replies"),
- app_settings.POSTFIX_AUTOREPLY_PARAMETERS_STRUCT,
- serializers.PostfixAutoreplySettingsSerializer,
+ app_settings.AUTOREPLY_PARAMETERS_STRUCT,
+ serializers.AutoreplySettingsSerializer,
False,
)
-class PostfixAutoreplyConfig(AppConfig):
+class AutoreplyConfig(AppConfig):
"""App configuration."""
- name = "modoboa.postfix_autoreply"
- verbose_name = "Auto-reply functionality using Postfix"
+ name = "modoboa.autoreply"
+ verbose_name = "Auto-reply functionality"
def ready(self):
load_settings()
- from . import handlers # NOQA:F401
diff --git a/modoboa/postfix_autoreply/factories.py b/modoboa/autoreply/factories.py
similarity index 100%
rename from modoboa/postfix_autoreply/factories.py
rename to modoboa/autoreply/factories.py
diff --git a/modoboa/postfix_autoreply/migrations/0001_initial.py b/modoboa/autoreply/migrations/0001_initial.py
similarity index 95%
rename from modoboa/postfix_autoreply/migrations/0001_initial.py
rename to modoboa/autoreply/migrations/0001_initial.py
index b2ac5ded3..7497a62b9 100644
--- a/modoboa/postfix_autoreply/migrations/0001_initial.py
+++ b/modoboa/autoreply/migrations/0001_initial.py
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import models, migrations
import django.utils.timezone
@@ -122,9 +119,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="arhistoric",
name="armessage",
- field=models.ForeignKey(
- to="postfix_autoreply.ARmessage", on_delete=models.CASCADE
- ),
+ field=models.ForeignKey(to="autoreply.ARmessage", on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterUniqueTogether(
diff --git a/modoboa/postfix_autoreply/migrations/0002_auto_20150728_1236.py b/modoboa/autoreply/migrations/0002_auto_20150728_1236.py
similarity index 75%
rename from modoboa/postfix_autoreply/migrations/0002_auto_20150728_1236.py
rename to modoboa/autoreply/migrations/0002_auto_20150728_1236.py
index 0c9a53e88..d8d144dd4 100644
--- a/modoboa/postfix_autoreply/migrations/0002_auto_20150728_1236.py
+++ b/modoboa/autoreply/migrations/0002_auto_20150728_1236.py
@@ -1,13 +1,10 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0001_initial"),
+ ("autoreply", "0001_initial"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0003_move_aliases.py b/modoboa/autoreply/migrations/0003_move_aliases.py
similarity index 84%
rename from modoboa/postfix_autoreply/migrations/0003_move_aliases.py
rename to modoboa/autoreply/migrations/0003_move_aliases.py
index aba8e5c53..b04ca5e86 100644
--- a/modoboa/postfix_autoreply/migrations/0003_move_aliases.py
+++ b/modoboa/autoreply/migrations/0003_move_aliases.py
@@ -1,12 +1,9 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import migrations
def move_aliases(apps, schema_editor):
"""Move exising aliases to the main table."""
- OldAlias = apps.get_model("postfix_autoreply", "Alias")
+ OldAlias = apps.get_model("autoreply", "Alias")
Alias = apps.get_model("admin", "Alias")
AliasRecipient = apps.get_model("admin", "AliasRecipient")
try:
@@ -31,7 +28,7 @@ def move_aliases(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0002_auto_20150728_1236"),
+ ("autoreply", "0002_auto_20150728_1236"),
]
operations = [migrations.RunPython(move_aliases)]
diff --git a/modoboa/postfix_autoreply/migrations/0004_delete_alias.py b/modoboa/autoreply/migrations/0004_delete_alias.py
similarity index 62%
rename from modoboa/postfix_autoreply/migrations/0004_delete_alias.py
rename to modoboa/autoreply/migrations/0004_delete_alias.py
index 86df2b121..5b95670bd 100644
--- a/modoboa/postfix_autoreply/migrations/0004_delete_alias.py
+++ b/modoboa/autoreply/migrations/0004_delete_alias.py
@@ -1,13 +1,10 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0003_move_aliases"),
+ ("autoreply", "0003_move_aliases"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py b/modoboa/autoreply/migrations/0005_auto_20151202_1623.py
similarity index 70%
rename from modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py
rename to modoboa/autoreply/migrations/0005_auto_20151202_1623.py
index 18873c578..c30f69e24 100644
--- a/modoboa/postfix_autoreply/migrations/0005_auto_20151202_1623.py
+++ b/modoboa/autoreply/migrations/0005_auto_20151202_1623.py
@@ -1,20 +1,15 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import migrations
def remove_useless_aliases(apps, schema_editor):
"""Remove aliases linked to disabled messages."""
- ARmessage = apps.get_model("postfix_autoreply", "ARmessage")
+ ARmessage = apps.get_model("autoreply", "ARmessage")
AliasRecipient = apps.get_model("admin", "AliasRecipient")
qset = ARmessage.objects.select_related("mbox", "mbox__domain").filter(
enabled=False
)
for armessage in qset:
- alr_address = "{0}@{1}@autoreply.{1}".format(
- armessage.mbox.address, armessage.mbox.domain
- )
+ alr_address = f"{armessage.mbox.address}@{armessage.mbox.domain}@autoreply.{armessage.mbox.domain}"
try:
alr = AliasRecipient.objects.get(address=alr_address)
except AliasRecipient.DoesNotExist:
@@ -28,7 +23,7 @@ def remove_useless_aliases(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0004_delete_alias"),
+ ("autoreply", "0004_delete_alias"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0006_auto_20160329_1501.py b/modoboa/autoreply/migrations/0006_auto_20160329_1501.py
similarity index 71%
rename from modoboa/postfix_autoreply/migrations/0006_auto_20160329_1501.py
rename to modoboa/autoreply/migrations/0006_auto_20160329_1501.py
index 4809cd439..2ca0f7f6d 100644
--- a/modoboa/postfix_autoreply/migrations/0006_auto_20160329_1501.py
+++ b/modoboa/autoreply/migrations/0006_auto_20160329_1501.py
@@ -1,13 +1,10 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0005_auto_20151202_1623"),
+ ("autoreply", "0005_auto_20151202_1623"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0007_auto_20180928_1423.py b/modoboa/autoreply/migrations/0007_auto_20180928_1423.py
similarity index 72%
rename from modoboa/postfix_autoreply/migrations/0007_auto_20180928_1423.py
rename to modoboa/autoreply/migrations/0007_auto_20180928_1423.py
index 2b7d039d1..0ed1e21b6 100644
--- a/modoboa/postfix_autoreply/migrations/0007_auto_20180928_1423.py
+++ b/modoboa/autoreply/migrations/0007_auto_20180928_1423.py
@@ -1,13 +1,9 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.15 on 2018-09-28 12:23
-from __future__ import unicode_literals
-
from django.db import migrations
def move_transport_entries(apps, schema_editor):
"""Move old transport entries to new model."""
- pf_Transport = apps.get_model("postfix_autoreply", "Transport")
+ pf_Transport = apps.get_model("autoreply", "Transport")
Transport = apps.get_model("transport", "Transport")
to_create = []
for old_transport in pf_Transport.objects.all():
@@ -23,7 +19,7 @@ class Migration(migrations.Migration):
dependencies = [
("transport", "0002_auto_20180928_1520"),
- ("postfix_autoreply", "0006_auto_20160329_1501"),
+ ("autoreply", "0006_auto_20160329_1501"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0008_delete_transport.py b/modoboa/autoreply/migrations/0008_delete_transport.py
similarity index 53%
rename from modoboa/postfix_autoreply/migrations/0008_delete_transport.py
rename to modoboa/autoreply/migrations/0008_delete_transport.py
index 4d0479c4f..62fff6e35 100644
--- a/modoboa/postfix_autoreply/migrations/0008_delete_transport.py
+++ b/modoboa/autoreply/migrations/0008_delete_transport.py
@@ -1,14 +1,10 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.11 on 2018-09-28 12:27
-from __future__ import unicode_literals
-
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0007_auto_20180928_1423"),
+ ("autoreply", "0007_auto_20180928_1423"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0009_auto_20220926_0913.py b/modoboa/autoreply/migrations/0009_auto_20220926_0913.py
similarity index 85%
rename from modoboa/postfix_autoreply/migrations/0009_auto_20220926_0913.py
rename to modoboa/autoreply/migrations/0009_auto_20220926_0913.py
index 84dc3eef0..ff449b414 100644
--- a/modoboa/postfix_autoreply/migrations/0009_auto_20220926_0913.py
+++ b/modoboa/autoreply/migrations/0009_auto_20220926_0913.py
@@ -1,12 +1,10 @@
-# Generated by Django 3.2.12 on 2022-09-26 07:13
-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0008_delete_transport"),
+ ("autoreply", "0008_delete_transport"),
]
operations = [
diff --git a/modoboa/postfix_autoreply/migrations/0010_auto_20230421_1825.py b/modoboa/autoreply/migrations/0010_auto_20230421_1825.py
similarity index 85%
rename from modoboa/postfix_autoreply/migrations/0010_auto_20230421_1825.py
rename to modoboa/autoreply/migrations/0010_auto_20230421_1825.py
index d8847d8b7..d43089366 100644
--- a/modoboa/postfix_autoreply/migrations/0010_auto_20230421_1825.py
+++ b/modoboa/autoreply/migrations/0010_auto_20230421_1825.py
@@ -1,12 +1,10 @@
-# Generated by Django 3.2.17 on 2023-04-21 16:25
-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("postfix_autoreply", "0009_auto_20220926_0913"),
+ ("autoreply", "0009_auto_20220926_0913"),
]
operations = [
diff --git a/modoboa/autoreply/migrations/0011_auto_20250609_0851.py b/modoboa/autoreply/migrations/0011_auto_20250609_0851.py
new file mode 100644
index 000000000..b35b2f9ce
--- /dev/null
+++ b/modoboa/autoreply/migrations/0011_auto_20250609_0851.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.22 on 2025-06-09 08:51
+
+import copy
+from django.db import migrations
+
+
+def cleanup_and_rename(apps, schema_editor):
+ # Delete transport entries
+ Transport = apps.get_model("transport", "Transport")
+ Transport.objects.filter(pattern__startswith="autoreply").delete()
+ # Delete aliases
+ AliasRecipient = apps.get_model("admin", "AliasRecipient")
+ AliasRecipient.objects.filter(address__contains="@autoreply.").delete()
+ # Rename parameters
+ LocalConfig = apps.get_model("core", "LocalConfig")
+ cfg = LocalConfig.objects.first()
+ if not cfg:
+ return
+ cfg._parameters["autoreply"] = copy.deepcopy(cfg._parameters["postfix_autoreply"])
+ del cfg._parameters["postfix_autoreply"]
+ cfg.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("autoreply", "0010_auto_20230421_1825"),
+ ]
+
+ operations = [migrations.RunPython(cleanup_and_rename, migrations.RunPython.noop)]
diff --git a/modoboa/autoreply/migrations/0012_delete_arhistoric.py b/modoboa/autoreply/migrations/0012_delete_arhistoric.py
new file mode 100644
index 000000000..55bf70348
--- /dev/null
+++ b/modoboa/autoreply/migrations/0012_delete_arhistoric.py
@@ -0,0 +1,16 @@
+# Generated by Django 4.2.22 on 2025-06-10 14:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("autoreply", "0011_auto_20250609_0851"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="ARhistoric",
+ ),
+ ]
diff --git a/modoboa/postfix_autoreply/management/__init__.py b/modoboa/autoreply/migrations/__init__.py
similarity index 100%
rename from modoboa/postfix_autoreply/management/__init__.py
rename to modoboa/autoreply/migrations/__init__.py
diff --git a/modoboa/autoreply/models.py b/modoboa/autoreply/models.py
new file mode 100644
index 000000000..2bf45ff8a
--- /dev/null
+++ b/modoboa/autoreply/models.py
@@ -0,0 +1,86 @@
+"""Postfix autoreply models."""
+
+from django.db import models
+from django.utils import timezone
+from django.utils.formats import localize
+from django.utils.translation import gettext_lazy as _
+
+from modoboa.admin.models import Mailbox
+from modoboa.sievefilters.lib import SieveClient
+
+
+class ARmessage(models.Model):
+ """Auto reply messages."""
+
+ mbox = models.ForeignKey(Mailbox, on_delete=models.CASCADE)
+ subject = models.CharField(
+ _("subject"),
+ max_length=255,
+ help_text=_("The subject that will appear in sent emails"),
+ )
+ content = models.TextField(
+ _("content"), help_text=_("The content that will appear in sent emails")
+ )
+ enabled = models.BooleanField(
+ _("enabled"), help_text=_("Activate/Deactivate your auto reply"), default=False
+ )
+ fromdate = models.DateTimeField(default=timezone.now)
+ untildate = models.DateTimeField(null=True, blank=True)
+
+ class Meta:
+ db_table = "postfix_autoreply_armessage"
+
+ def __str__(self):
+ return f"AR<{self.mbox}>: {self.enabled}"
+
+ def update_sieve_rule(self, request, fset):
+ context = {"name": self.mbox.user.fullname}
+ if not self.fromdate:
+ condition = [("Subject", ":matches", "*")]
+ else:
+ days = request.localconfig.parameters.get_value("tracking_period")
+ context["fromdate"] = localize(self.fromdate)
+ fromdate = self.fromdate.isoformat()
+ fromdate, tz = fromdate.split("+")
+ tz = f"+{tz.replace(':', '')}"
+ condition = [
+ ("currentdate", ":zone", tz, ":value", "ge", "iso8601", fromdate)
+ ]
+ if self.untildate:
+ context["untildate"] = localize(self.untildate)
+ condition.append(
+ (
+ "currentdate",
+ ":zone",
+ tz,
+ ":value",
+ "lt",
+ "iso8601",
+ self.untildate.isoformat().split("+")[0],
+ )
+ )
+ content = self.content % context
+ name = "autoreply"
+ action = [("vacation", ":subject", self.subject, ":days", days, content)]
+ if not fset.getfilter(name):
+ fset.addfilter(name, condition, action)
+ else:
+ fset.updatefilter(name, name, condition, action)
+ for filter in fset.filters:
+ if filter["name"] == name:
+ filter["description"] = (
+ "Autoreply rule generated by Modoboa, do not edit!"
+ )
+
+ def manage_sieve_rule(self, request):
+ client = SieveClient(user=request.user.username, password=str(request.auth))
+ active_script, scripts = client.listscripts()
+ if not active_script:
+ active_script = "DefaultScript"
+ client.pushscript(active_script, "# Empty script", True)
+ fset = client.getscript(active_script, format="fset")
+ if self.enabled:
+ self.update_sieve_rule(request, fset)
+ else:
+ fset.removefilter("autoreply")
+ client.pushscript(fset.name, str(fset))
diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl
index 7e5f66d29..90b3b6fe6 100644
--- a/modoboa/core/commands/templates/settings.py.tpl
+++ b/modoboa/core/commands/templates/settings.py.tpl
@@ -92,7 +92,7 @@ MODOBOA_APPS = (
'modoboa.pdfcredentials',
'modoboa.dmarc',
'modoboa.imap_migration',
- 'modoboa.postfix_autoreply',
+ 'modoboa.autoreply',
'modoboa.sievefilters',
'modoboa.contacts',
'modoboa.calendars',
diff --git a/modoboa/lib/tests/__init__.py b/modoboa/lib/tests/__init__.py
index c294c99b9..e1f202f0d 100644
--- a/modoboa/lib/tests/__init__.py
+++ b/modoboa/lib/tests/__init__.py
@@ -145,7 +145,7 @@ SETTINGS_SAMPLE = {
"max_sync_accounts": 2,
"create_folders": True,
},
- "postfix_autoreply": {
+ "autoreply": {
"autoreplies_timeout": 86400,
"default_subject": "I'm off",
"default_content": "I'm off",
diff --git a/modoboa/postfix_autoreply/__init__.py b/modoboa/postfix_autoreply/__init__.py
deleted file mode 100644
index d62a6f91a..000000000
--- a/modoboa/postfix_autoreply/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Away message editor for Modoboa (postfix compatible)."""
-
-from pkg_resources import DistributionNotFound, get_distribution
-
-try:
- __version__ = get_distribution(__name__).version
-except DistributionNotFound:
- # package is not installed
- pass
-
-default_app_config = "modoboa_postfix_autoreply.apps.PostfixAutoreplyConfig"
diff --git a/modoboa/postfix_autoreply/handlers.py b/modoboa/postfix_autoreply/handlers.py
deleted file mode 100644
index 972cfeaf8..000000000
--- a/modoboa/postfix_autoreply/handlers.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""Django signal handlers for modoboa_postfix_autoreply."""
-
-from django.db.models import signals
-from django.dispatch import receiver
-
-from modoboa.admin import models as admin_models
-from modoboa.transport import models as tr_models
-
-from . import models
-
-
-@receiver(signals.post_save, sender=admin_models.Domain)
-def manage_transport_entry(sender, instance, **kwargs):
- """Create or update a transport entry for this domain."""
- if kwargs.get("created"):
- tr_models.Transport.objects.get_or_create(
- pattern=f"autoreply.{instance}", service="autoreply"
- )
- return
- oldname = getattr(instance, "oldname", "None")
- if oldname is None or oldname == instance.name:
- return
- tr_models.Transport.objects.filter(pattern=f"autoreply.{oldname}").update(
- pattern=f"autoreply.{instance.name}"
- )
- qset = admin_models.AliasRecipient.objects.select_related(
- "alias", "r_mailbox"
- ).filter(
- alias__domain=instance, alias__internal=True, address__contains="@autoreply"
- )
- for alr in qset:
- alr.address = alr.address.replace(oldname, instance.name)
- alr.save()
-
-
-@receiver(signals.post_delete, sender=admin_models.Domain)
-def delete_transport_entry(sender, instance, **kwargs):
- """Delete a transport entry."""
- tr_models.Transport.objects.filter(pattern=f"autoreply.{instance}").delete()
-
-
-@receiver(signals.post_save, sender=admin_models.Mailbox)
-def rename_autoreply_alias(sender, instance, **kwargs):
- """Rename AR alias if needed."""
- old_address = getattr(instance, "old_full_address", None)
- if old_address is None or old_address == instance.full_address:
- return
- admin_models.AliasRecipient.objects.filter(
- address__contains=f"{old_address}@autoreply"
- ).update(address=f"{instance.full_address}@autoreply.{instance.domain}")
-
-
-@receiver(signals.post_delete, sender=admin_models.Mailbox)
-def delete_autoreply_alias(sender, instance, **kwargs):
- """Delete alias."""
- admin_models.AliasRecipient.objects.filter(
- address=f"{instance.full_address}@autoreply.{instance.domain}"
- ).delete()
-
-
-@receiver(signals.post_save, sender=models.ARmessage)
-def manage_autoreply_alias(sender, instance, **kwargs):
- """Create or delete the alias."""
- ar_alias_address = f"{instance.mbox.full_address}@autoreply.{instance.mbox.domain}"
- admin_models.Alias.objects.get(
- address=instance.mbox.full_address, domain=instance.mbox.domain, internal=True
- )
- alias, created = admin_models.Alias.objects.get_or_create(
- address=instance.mbox.full_address, domain=instance.mbox.domain, internal=True
- )
- if instance.enabled:
- admin_models.AliasRecipient.objects.get_or_create(
- alias=alias, address=ar_alias_address
- )
- else:
- admin_models.AliasRecipient.objects.filter(address=ar_alias_address).delete()
diff --git a/modoboa/postfix_autoreply/known_problems.py b/modoboa/postfix_autoreply/known_problems.py
deleted file mode 100644
index 843f99621..000000000
--- a/modoboa/postfix_autoreply/known_problems.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Know problems and associated fixes."""
-
-from modoboa.admin import models as admin_models
-from modoboa.admin.management.commands.subcommands import _repair
-from . import models
-
-
-@_repair.known_problem
-def ensure_autoreplies_recipents_are_valids(**options):
- """Sometime autoreply alias exists when ARmessage is not enabled."""
- deleted = 0
- qs = admin_models.AliasRecipient.objects.select_related("alias").filter(
- address__contains="@autoreply."
- )
- for alr in qs:
- address, domain = alr.alias.address.split("@")
- arqs = models.ARmessage.objects.filter(
- mbox__address=address, mbox__domain__name=domain
- )
- if not arqs.exists():
- _repair.log(f"Delete {alr} (AR does not exist)", **options)
- alr.delete()
- deleted += 1
- elif arqs.filter(enabled=False).exists():
- _repair.log(f"Delete {alr} (AR is disabled)", **options)
- alr.delete()
- deleted += 1
- if deleted:
- _repair.log(f"{deleted} alias recipient(s) deleted", **options)
diff --git a/modoboa/postfix_autoreply/management/commands/__init__.py b/modoboa/postfix_autoreply/management/commands/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/modoboa/postfix_autoreply/management/commands/autoreply.py b/modoboa/postfix_autoreply/management/commands/autoreply.py
deleted file mode 100644
index 43715f083..000000000
--- a/modoboa/postfix_autoreply/management/commands/autoreply.py
+++ /dev/null
@@ -1,205 +0,0 @@
-import datetime
-import email
-import email.header
-import fileinput
-from io import StringIO
-import logging
-import smtplib
-import sys
-from logging.handlers import SysLogHandler
-
-from django.core.mail import EmailMessage
-from django.core.management.base import BaseCommand
-from django.utils import timezone, translation
-from django.utils.encoding import smart_str
-from django.utils.formats import localize
-
-from modoboa.admin.models import Mailbox
-from modoboa.lib.email_utils import split_mailbox
-from modoboa.parameters import tools as param_tools
-from ...models import ARhistoric, ARmessage
-
-logger = logging.getLogger()
-
-
-def safe_subject(msg):
- """Clean message subject and return it."""
- decoded = email.header.decode_header(msg.get("Subject"))
- subject = ""
- for sub, charset in decoded:
- if isinstance(sub, str):
- subject += sub
- continue
- # charset can be None
- charset = charset if charset else "utf-8"
- try:
- subject += sub.decode(charset)
- except UnicodeDecodeError:
- pass
- return " ".join(subject.split())
-
-
-def send_autoreply(sender, mailbox, armessage, original_msg):
- """Send an autoreply message."""
- if armessage.fromdate > timezone.now():
- # Too soon, come back later
- return
-
- condition = armessage.untildate is not None and armessage.untildate < timezone.now()
- if condition:
- # ARmessage has expired, disable it
- armessage.enabled = False
- armessage.save(update_fields=["enabled"])
- return
-
- try:
- lastar = ARhistoric.objects.get(armessage=armessage.id, sender=sender)
- timeout = param_tools.get_global_parameter(
- "autoreplies_timeout", app="postfix_autoreply"
- )
- delta = datetime.timedelta(seconds=int(timeout))
- now = timezone.make_aware(
- datetime.datetime.now(), timezone.get_default_timezone()
- )
- if lastar.last_sent + delta > now:
- logger.debug(
- "no autoreply message sent because delta (%s) < timetout (%s)",
- delta,
- timeout,
- )
- return
-
- except ARhistoric.DoesNotExist:
- lastar = ARhistoric()
- lastar.armessage = armessage
- lastar.sender = sender
-
- headers = {"Auto-Submitted": "auto-replied", "Precedence": "bulk"}
- message_id = original_msg.get("Message-ID", "").strip("\n")
- if message_id:
- headers.update({"In-Reply-To": message_id, "References": message_id})
-
- subject = safe_subject(original_msg)
- with translation.override(mailbox.user.language):
- context = {
- "name": mailbox.user.fullname,
- "fromdate": localize(armessage.fromdate),
- "untildate": "",
- }
- if armessage.untildate:
- context.update({"untildate": localize(armessage.untildate)})
- content = armessage.content % context
- msg = EmailMessage(
- f"Auto: {armessage.subject} Re: {subject}",
- smart_str(content),
- mailbox.user.encoded_address,
- [sender],
- headers=headers,
- )
- try:
- msg.send()
- except smtplib.SMTPException as exp:
- logger.error("Failed to send autoreply message: %s", exp)
- sys.exit(1)
-
- logger.debug("autoreply message sent to %s", mailbox.user.encoded_address)
-
- lastar.last_sent = datetime.datetime.now()
- lastar.save()
-
-
-class Command(BaseCommand):
- """Command definition."""
-
- help = "Send autoreply emails" # NOQA:A003
-
- def add_arguments(self, parser):
- """Add extra arguments to command line."""
- parser.add_argument("--debug", action="store_true", dest="debug", default=False)
- parser.add_argument(
- "--syslog-socket-path", default="/dev/log", help="Path to syslog socket"
- )
- parser.add_argument("sender")
- parser.add_argument("recipient", nargs="+")
-
- def handle(self, *args, **options):
- try:
- handler = SysLogHandler(address=options["syslog_socket_path"])
- except OSError as ex:
- if ex.errno == 2:
- # try the default, localhost:514
- handler = SysLogHandler()
- else:
- raise
-
- logger.addHandler(handler)
- logger.setLevel(logging.ERROR)
- if options["debug"]:
- logger.setLevel(logging.DEBUG)
-
- logger.debug(
- "autoreply sender=%s recipient=%s",
- options["sender"],
- ",".join(options["recipient"]),
- )
-
- sender = smart_str(options["sender"])
-
- sender_localpart = split_mailbox(sender.lower())[0]
- if (
- (sender_localpart in ("mailer-daemon", "listserv", "majordomo"))
- or (sender_localpart.startswith("owner-"))
- or (sender_localpart.endswith("-request"))
- ):
- logger.debug("Skip auto reply, this mail comes from a mailing list")
- return
-
- content = StringIO()
- for line in fileinput.input([]):
- content.write(line)
- content.seek(0)
-
- original_msg = email.message_from_file(content)
-
- # Mailing list filter based on
- # https://tools.ietf.org/html/rfc5230#page-7
- ml_known_headers = [
- "List-Id",
- "List-Help",
- "List-Subscribe",
- "List-Unsubscribe",
- "List-Post",
- "List-Owner",
- "List-Archive",
- ]
- from_ml = False
- for header in ml_known_headers:
- if header in original_msg:
- from_ml = True
- break
- condition = (
- original_msg.get("Precedence") == "bulk"
- or original_msg.get("X-Mailer") == "PHPMailer"
- or from_ml
- )
- if condition:
- logger.debug("Skip auto reply, this mail comes from a mailing list")
- return
-
- recipients = [smart_str(rcpt) for rcpt in options["recipient"]]
- for fulladdress in recipients:
- address, domain = split_mailbox(fulladdress)
- try:
- mbox = Mailbox.objects.get(address=address, domain__name=domain)
- except Mailbox.DoesNotExist:
- msg = f"Unknown recipient {fulladdress}"
- logger.debug("autoreply %s", msg)
- continue
- try:
- armessage = ARmessage.objects.get(mbox=mbox.id, enabled=True)
- logger.debug("autoreply message found")
- except ARmessage.DoesNotExist:
- logger.debug("autoreply message not found")
- continue
-
- send_autoreply(sender, mbox, armessage, original_msg)
diff --git a/modoboa/postfix_autoreply/migrations/__init__.py b/modoboa/postfix_autoreply/migrations/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/modoboa/postfix_autoreply/models.py b/modoboa/postfix_autoreply/models.py
deleted file mode 100644
index 8feb0c875..000000000
--- a/modoboa/postfix_autoreply/models.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Postfix autoreply models."""
-
-from django.db import models
-from django.utils import timezone
-from django.utils.translation import gettext_lazy as _
-
-from modoboa.admin.models import Mailbox
-
-
-class ARmessage(models.Model):
- """Auto reply messages."""
-
- mbox = models.ForeignKey(Mailbox, on_delete=models.CASCADE)
- subject = models.CharField(
- _("subject"),
- max_length=255,
- help_text=_("The subject that will appear in sent emails"),
- )
- content = models.TextField(
- _("content"), help_text=_("The content that will appear in sent emails")
- )
- enabled = models.BooleanField(
- _("enabled"), help_text=_("Activate/Deactivate your auto reply"), default=False
- )
- fromdate = models.DateTimeField(default=timezone.now)
- untildate = models.DateTimeField(null=True, blank=True)
-
- class Meta:
- db_table = "postfix_autoreply_armessage"
-
- def __str__(self):
- return f"AR<{self.mbox}>: {self.enabled}"
-
-
-class ARhistoric(models.Model):
- """Auto reply historic."""
-
- armessage = models.ForeignKey(ARmessage, on_delete=models.CASCADE)
- last_sent = models.DateTimeField(auto_now=True)
- sender = models.CharField(max_length=254)
-
- class Meta:
- unique_together = ("armessage", "sender")
- db_table = "postfix_autoreply_arhistoric"
diff --git a/modoboa/postfix_autoreply/tests.py b/modoboa/postfix_autoreply/tests.py
deleted file mode 100644
index 03825fd0b..000000000
--- a/modoboa/postfix_autoreply/tests.py
+++ /dev/null
@@ -1,419 +0,0 @@
-"""modoboa-postfix-autoreply unit tests."""
-
-import datetime
-from io import StringIO
-import sys
-
-from dateutil.relativedelta import relativedelta
-
-from django.core import mail, management
-from django.urls import reverse
-from django.utils import timezone
-from django.utils.formats import date_format
-
-from modoboa.admin import factories as admin_factories, models as admin_models
-from modoboa.core.models import User
-from modoboa.lib.tests import ModoAPITestCase, ModoTestCase
-from modoboa.transport import models as tr_models
-
-from . import factories, models
-
-SIMPLE_EMAIL_CONTENT = """
-From: Homer Simpson
-Date: Wed, 15 Mar 2017 18:35:19 +0100
-Message-ID:
-Subject: Test
-To: user@test.com
-Content-Type: multipart/alternative; boundary=001a114420be4c231a054ac85e75
-
---001a114420be4c231a054ac85e75
-Content-Type: text/plain; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75
-Content-Type: text/html; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75--
-"""
-
-SIMPLE_EMAIL_CONTENT_WITH_BAD_HEADER = """
-From: Homer Simpson
-Date: Wed, 15 Mar 2017 18:35:19 +0100
-Message-ID:
-
-Subject: Test
-To: user@test.com
-Content-Type: multipart/alternative; boundary=001a114420be4c231a054ac85e75
-
---001a114420be4c231a054ac85e75
-Content-Type: text/plain; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75
-Content-Type: text/html; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75--
-"""
-
-ENCODED_EMAIL_SUBJECT = r"""
-From: Homer Simpson
-Date: Wed, 15 Mar 2017 18:35:19 +0100
-Message-ID:
-Subject: =?utf-8?q?A_m=C3=A9_non!!?=\n =?utf-8?q?_=C3=A7a_va_pas!!?=
-To: user@test.com
-Content-Type: multipart/alternative; boundary=001a114420be4c231a054ac85e75
-
---001a114420be4c231a054ac85e75
-Content-Type: text/plain; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75
-Content-Type: text/html; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75--
-"""
-
-EMAIL_FROM_ML_CONTENT = """
-From: Homer Simpson
-Date: Wed, 15 Mar 2017 18:35:19 +0100
-Message-ID:
-List-Id: "Test list"
-List-Archive:
-Subject: Test
-To: user@test.com
-Content-Type: multipart/alternative; boundary=001a114420be4c231a054ac85e75
-
---001a114420be4c231a054ac85e75
-Content-Type: text/plain; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75
-Content-Type: text/html; charset=UTF-8
-
-pouet
-
---001a114420be4c231a054ac85e75--
-"""
-
-
-class EventsTestCase(ModoAPITestCase):
-
- @classmethod
- def setUpTestData(cls): # NOQA:N802
- """Create test data."""
- super().setUpTestData()
- admin_factories.populate_database()
-
- def _create_armessage(self, account):
- values = {
- "mbox": account.mailbox.id,
- "subject": "I'm off",
- "content": "I'll back soon",
- "enabled": True,
- }
- response = self.client.post(reverse("v2:armessage-list"), values, format="json")
- self.assertEqual(response.status_code, 201)
- return response
-
- def test_domain_created_event(self):
- values = {
- "name": "domain.tld",
- "quota": 100,
- "default_mailbox_quota": 1,
- "type": "domain",
- }
- response = self.client.post(reverse("v2:domain-list"), values, format="json")
- self.assertEqual(response.status_code, 201)
- self.assertTrue(
- tr_models.Transport.objects.filter(pattern="autoreply.domain.tld").exists()
- )
-
- def test_domain_deleted_event(self):
- dom = admin_models.Domain.objects.get(name="test.com")
- response = self.client.post(
- reverse("v2:domain-delete", args=[dom.id]), {}, format="json"
- )
- self.assertEqual(response.status_code, 204)
- with self.assertRaises(tr_models.Transport.DoesNotExist):
- tr_models.Transport.objects.get(pattern="autoreply.test.com")
-
- def test_domain_modified_event(self):
- values = {
- "name": "test.fr",
- "quota": 100,
- "default_mailbox_quota": 1,
- "enabled": True,
- "type": "domain",
- }
- dom = admin_models.Domain.objects.get(name="test.com")
- response = self.client.put(
- reverse("v2:domain-detail", args=[dom.id]), values, format="json"
- )
- self.assertEqual(response.status_code, 200)
- self.assertTrue(
- tr_models.Transport.objects.filter(pattern="autoreply.test.fr").exists()
- )
- self.assertEqual(
- admin_models.Alias.objects.filter(domain=dom, internal=True).count(), 2
- )
- for alr in admin_models.AliasRecipient.objects.filter(
- address__contains="@test.fr"
- ):
- self.assertIn("autoreply.test.fr", alr.address)
-
- def test_armessage_postsave_event(self):
- values = {
- "username": "leon@test.com",
- "first_name": "Tester",
- "last_name": "Toto",
- "role": "SimpleUsers",
- "mailbox": {
- "use_domain_quota": True,
- },
- "is_active": True,
- "email": "leon@test.com",
- "enabled": True,
- "language": "en",
- }
- account = User.objects.get(username="user@test.com")
- armessage = self._create_armessage(account)
-
- # Rename account and check if alias is updated
- response = self.client.put(
- reverse("v2:account-detail", args=[account.id]), values, format="json"
- )
- self.assertEqual(response.status_code, 200)
- self.assertTrue(
- admin_models.AliasRecipient.objects.filter(
- alias__address="leon@test.com",
- alias__internal=True,
- address="leon@test.com@autoreply.test.com",
- ).exists()
- )
-
- values = {
- "mbox": account.mailbox.id,
- "subject": "I'm off",
- "content": "I'll back soon",
- "enabled": False,
- }
- self.client.put(
- reverse("v2:armessage-detail", args=[armessage.json()["id"]]),
- values,
- format="json",
- )
- self.assertFalse(
- admin_models.AliasRecipient.objects.filter(
- alias__address="leon@test.com",
- alias__internal=True,
- address="leon@test.com@autoreply.test.com",
- ).exists()
- )
-
- def test_mailbox_deleted_event(self):
- account = User.objects.get(username="user@test.com")
- self._create_armessage(account)
- response = self.client.post(
- reverse("v2:account-delete", args=[account.id]), {}, format="json"
- )
- self.assertEqual(response.status_code, 204)
- self.assertFalse(
- admin_models.AliasRecipient.objects.filter(
- address="user@test.com@autoreply.test.com"
- ).exists()
- )
- self.assertFalse(
- models.ARmessage.objects.filter(
- mbox__address="user", mbox__domain__name="test.com"
- ).exists()
- )
-
- def test_modify_mailbox_event(self):
- """Rename mailbox."""
- account = User.objects.get(username="user@test.com")
- factories.ARmessageFactory(mbox=account.mailbox)
- values = {
- "username": "leon@test.com",
- "first_name": "Tester",
- "last_name": "Toto",
- "role": "SimpleUsers",
- "mailbox": {
- "use_domain_quota": True,
- },
- "is_active": True,
- "email": "leon@test.com",
- "enabled": True,
- "language": "en",
- }
- self.client.put(
- reverse("v2:account-detail", args=[account.id]), values, format="json"
- )
- self.assertFalse(
- admin_models.AliasRecipient.objects.filter(
- alias__address="user@test.com",
- alias__internal=True,
- address="user@test.com@autoreply.test.com",
- ).exists()
- )
- self.assertTrue(
- admin_models.AliasRecipient.objects.filter(
- alias__address="leon@test.com",
- alias__internal=True,
- address="leon@test.com@autoreply.test.com",
- ).exists()
- )
-
-
-class RepairTestCase(ModoTestCase):
- """Check repair command."""
-
- @classmethod
- def setUpTestData(cls): # NOQA:N802
- """Create some data."""
- super().setUpTestData()
- admin_factories.populate_database()
-
- def test_management_command(self):
- """Check if problems are fixed."""
- mbox = admin_models.Mailbox.objects.get(user__username="user@test.com")
- alias = admin_models.Alias.objects.get(internal=True, address=mbox.full_address)
- arm = factories.ARmessageFactory(mbox=mbox, enabled=False)
- ar_address = f"{mbox.full_address}@autoreply.{mbox.domain.name}"
- admin_factories.AliasRecipientFactory(alias=alias, address=ar_address)
- management.call_command("modo", "repair", "--quiet")
- self.assertFalse(
- admin_models.AliasRecipient.objects.filter(address=ar_address).exists()
- )
- admin_factories.AliasRecipientFactory(alias=alias, address=ar_address)
- arm.delete()
- management.call_command("modo", "repair", "--quiet")
- self.assertFalse(
- admin_models.AliasRecipient.objects.filter(address=ar_address).exists()
- )
-
-
-class ManagementCommandTestCase(ModoTestCase):
- """Management command related tests."""
-
- @classmethod
- def setUpTestData(cls): # NOQA:N802
- """Create some data."""
- super().setUpTestData()
- admin_factories.populate_database()
- cls.account = User.objects.get(username="user@test.com")
-
- def setUp(self):
- """Replace stdin."""
- super().setUp()
- self.stdin = sys.stdin
- sys.stdin = StringIO(SIMPLE_EMAIL_CONTENT.strip())
- self.arm = factories.ARmessageFactory(mbox=self.account.mailbox)
-
- def tearDown(self):
- """Restore stdin."""
- sys.stdin = self.stdin
-
- def test_simple_case(self):
- """Check basic autoreply."""
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
- self.assertTrue(models.ARhistoric.objects.filter(armessage=self.arm).exists())
-
- # Try to send a new one
- sys.stdin.seek(0)
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
-
- def test_armessage_date_constraints(self):
- """Check date constraints."""
- account = User.objects.get(username="admin@test.com")
- arm = factories.ARmessageFactory(
- mbox=account.mailbox, fromdate=timezone.now() + relativedelta(days=1)
- )
- management.call_command("autoreply", "homer@simpson.com", "admin@test.com")
- self.assertEqual(len(mail.outbox), 0)
-
- arm.fromdate = timezone.now() - relativedelta(days=2)
- arm.untildate = timezone.now() - relativedelta(days=1)
- arm.save()
- sys.stdin.seek(0)
- management.call_command("autoreply", "homer@simpson.com", "admin@test.com")
- self.assertEqual(len(mail.outbox), 0)
- arm.refresh_from_db()
- self.assertFalse(arm.enabled)
-
- def test_recipient_does_not_exist(self):
- """Message received for non local recipient."""
- management.call_command("autoreply", "homer@simpson.com", "pouet@test.fr")
- self.assertEqual(len(mail.outbox), 0)
-
- def test_no_ar_message_defined(self):
- """No AR defined for local recipient."""
- management.call_command("autoreply", "homer@simpson.com", "admin@test.com")
- self.assertEqual(len(mail.outbox), 0)
-
- def test_encoded_message(self):
- """Message received with encoded header"""
- sys.stdin = StringIO(ENCODED_EMAIL_SUBJECT.strip())
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
-
- def test_message_from_ml(self):
- """Message received from a mailing list."""
- sys.stdin = StringIO(EMAIL_FROM_ML_CONTENT.strip())
- management.call_command("autoreply", "mailer-daemon@list.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 0)
-
- sys.stdin.seek(0)
- management.call_command("autoreply", "sender@list.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 0)
-
- def test_variable_substitution(self):
- """Check when message contains variables."""
- tz = timezone.get_current_timezone()
- self.arm.fromdate = datetime.datetime(2017, 12, 8, 14, 0, 0, 0).replace(
- tzinfo=tz
- )
- self.arm.content = "%(name)s %(fromdate)s %(untildate)s"
- self.arm.save()
- self.account.language = "fr"
- self.account.save(update_fields=["language"])
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
- self.assertEqual(
- "user@test.com 8 décembre 2017 14:00", mail.outbox[0].body.strip()
- )
-
- def test_untildate_substitution(self):
- """Check substitution of until date."""
- tz = timezone.get_current_timezone()
- self.arm.fromdate = datetime.datetime(2017, 12, 8, 14, 0, 0, 0).replace(
- tzinfo=tz
- )
- self.arm.untildate = timezone.now() + relativedelta(hours=1)
- self.arm.content = "%(name)s %(fromdate)s %(untildate)s"
- self.arm.save()
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
- self.assertEqual(
- "user@test.com Dec. 8, 2017, 2 p.m. {}".format(
- date_format(self.arm.untildate, "DATETIME_FORMAT")
- ),
- mail.outbox[0].body.strip(),
- )
-
- def test_message_with_bad_headers(self):
- """Message received with bad header (including newline)"""
- sys.stdin = StringIO(SIMPLE_EMAIL_CONTENT_WITH_BAD_HEADER.strip())
- management.call_command("autoreply", "homer@simpson.test", "user@test.com")
- self.assertEqual(len(mail.outbox), 1)
diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py
index a8ab5a0a1..f10cdfa1c 100644
--- a/modoboa/sievefilters/api/v2/serializers.py
+++ b/modoboa/sievefilters/api/v2/serializers.py
@@ -1,6 +1,6 @@
"""Sievefilters serializers."""
-from typing import List, Tuple
+from typing import List, Self, Tuple
from sievelib import commands
from sievelib.factory import Filter
@@ -61,9 +61,11 @@ class FilterSerializer(serializers.Serializer):
actions = ActionSerializer(many=True)
@staticmethod
- def from_filters(filters: List[Filter]) -> "FilterSerializer":
+ def from_filters(filters: List[Filter]) -> Self:
result = []
for fobj in filters:
+ if fobj["name"] == "autoreply":
+ continue
test = (
fobj["content"].children[0] if not fobj["enabled"] else fobj["content"]
)
diff --git a/modoboa/sievefilters/api/v2/tests.py b/modoboa/sievefilters/api/v2/tests.py
index 566172aa6..90a78e703 100644
--- a/modoboa/sievefilters/api/v2/tests.py
+++ b/modoboa/sievefilters/api/v2/tests.py
@@ -18,7 +18,23 @@ Application = get_application_model()
AccessToken = get_access_token_model()
-class FilterSetViewSetTestCase(ModoAPITestCase):
+class PatcherMixin:
+ """Mixin to patch external dependencies."""
+
+ def setUp(self):
+ super().setUp()
+ patcher = mock.patch("sievelib.managesieve.Client")
+ self.mock_client = patcher.start()
+ self.mock_client.return_value = mocks.ManagesieveClientMock()
+ self.addCleanup(patcher.stop)
+
+ patcher = mock.patch("imaplib.IMAP4")
+ self.mock_imap4 = patcher.start()
+ self.mock_imap4.return_value = IMAP4Mock()
+ self.addCleanup(patcher.stop)
+
+
+class FilterSetViewSetTestCase(PatcherMixin, ModoAPITestCase):
@classmethod
def setUpTestData(cls):
"""Create some users."""
@@ -42,19 +58,6 @@ class FilterSetViewSetTestCase(ModoAPITestCase):
application=cls.application,
)
- def setUp(self):
- """Connect with a simpler user."""
- super().setUp()
- patcher = mock.patch("sievelib.managesieve.Client")
- self.mock_client = patcher.start()
- self.mock_client.return_value = mocks.ManagesieveClientMock()
- self.addCleanup(patcher.stop)
-
- patcher = mock.patch("imaplib.IMAP4")
- self.mock_imap4 = patcher.start()
- self.mock_imap4.return_value = IMAP4Mock()
- self.addCleanup(patcher.stop)
-
def authenticate(self, username: str = "user@test.com"):
self.client.credentials(Authorization="Bearer " + self.access_token.token)
diff --git a/modoboa/sievefilters/mocks.py b/modoboa/sievefilters/mocks.py
index b3678e6f9..fad627252 100644
--- a/modoboa/sievefilters/mocks.py
+++ b/modoboa/sievefilters/mocks.py
@@ -61,7 +61,7 @@ class ManagesieveClientMock:
}
def connect(self, username, password, **kwargs):
- return username == "user@test.com"
+ return True
def logout(self):
return True
diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py
index 7432a38bd..fd1e0b964 100644
--- a/modoboa/urls_api_v2.py
+++ b/modoboa/urls_api_v2.py
@@ -18,7 +18,7 @@ urlpatterns = [
path("", include("modoboa.maillog.api.v2.urls")),
path("", include("modoboa.transport.api.v2.urls")),
path("", include("modoboa.pdfcredentials.api.v2.urls")),
- path("", include("modoboa.postfix_autoreply.api.v2.urls")),
+ path("", include("modoboa.autoreply.api.v2.urls")),
path("", include("modoboa.sievefilters.api.v2.urls")),
path("", include("modoboa.contacts.urls")),
path("", include("modoboa.calendars.urls")),
diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py
index 86426a62a..ab39e08b0 100644
--- a/test_project/test_project/settings.py
+++ b/test_project/test_project/settings.py
@@ -97,7 +97,7 @@ MODOBOA_APPS = (
"modoboa.pdfcredentials",
"modoboa.dmarc",
"modoboa.imap_migration",
- "modoboa.postfix_autoreply",
+ "modoboa.autoreply",
"modoboa.sievefilters",
# Modoboa extensions here.
"modoboa.contacts",