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",