modoboa 2.4.11__py3-none-any.whl → 2.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modoboa/admin/api/v2/serializers.py +14 -0
- modoboa/amavis/__init__.py +3 -0
- modoboa/amavis/app_settings.py +276 -0
- modoboa/amavis/apps.py +18 -0
- modoboa/amavis/checks/__init__.py +2 -0
- modoboa/amavis/checks/settings_checks.py +59 -0
- modoboa/amavis/dbrouter.py +35 -0
- modoboa/amavis/factories.py +164 -0
- modoboa/amavis/handlers.py +146 -0
- modoboa/amavis/lib.py +381 -0
- modoboa/amavis/management/__init__.py +0 -0
- modoboa/amavis/management/commands/__init__.py +0 -0
- modoboa/amavis/management/commands/amnotify.py +99 -0
- modoboa/amavis/management/commands/qcleanup.py +84 -0
- modoboa/amavis/migrations/0001_initial.py +340 -0
- modoboa/amavis/migrations/__init__.py +0 -0
- modoboa/amavis/models.py +226 -0
- modoboa/amavis/serializers.py +139 -0
- modoboa/amavis/sql_connector.py +240 -0
- modoboa/amavis/sql_email.py +66 -0
- modoboa/amavis/tasks.py +33 -0
- modoboa/amavis/templates/amavis/notifications/pending_requests.html +16 -0
- modoboa/amavis/tests/__init__.py +0 -0
- modoboa/amavis/tests/sa-learn +3 -0
- modoboa/amavis/tests/sample_messages/quarantined-input.txt +80 -0
- modoboa/amavis/tests/sample_messages/quarantined-output-plain_nolinks.txt +17 -0
- modoboa/amavis/tests/spamc +3 -0
- modoboa/amavis/tests/test_checks.py +25 -0
- modoboa/amavis/tests/test_handlers.py +214 -0
- modoboa/amavis/tests/test_lib.py +90 -0
- modoboa/amavis/tests/test_management_commands.py +45 -0
- modoboa/amavis/tests/test_sql_email.py +67 -0
- modoboa/amavis/tests/test_utils.py +19 -0
- modoboa/amavis/tests/test_viewsets.py +319 -0
- modoboa/amavis/urls.py +11 -0
- modoboa/amavis/utils.py +105 -0
- modoboa/amavis/viewsets.py +265 -0
- modoboa/core/api/v1/serializers.py +7 -5
- modoboa/core/api/v2/serializers.py +4 -2
- modoboa/core/api/v2/tests.py +16 -4
- modoboa/core/api/v2/urls.py +5 -5
- modoboa/core/api/v2/views.py +6 -2
- modoboa/core/api/v2/viewsets.py +24 -3
- modoboa/core/commands/deploy.py +3 -0
- modoboa/core/commands/templates/settings.py.tpl +12 -11
- modoboa/core/handlers.py +6 -2
- modoboa/core/management/commands/add_allowed_hosts.py +33 -0
- modoboa/core/management/commands/load_initial_data.py +10 -0
- modoboa/core/migrations/0025_rename_user_email_is_active_core_user_email_c0c03f_idx.py +23 -5
- modoboa/core/tests/test_core.py +24 -0
- modoboa/core/utils.py +3 -0
- modoboa/frontend_dist/assets/AccountAliasForm-C0oHHyZL.js +1 -0
- modoboa/frontend_dist/assets/AccountEditView-lgSJ2Se8.js +1 -0
- modoboa/frontend_dist/assets/AccountLayout-U386K8zy.js +1 -0
- modoboa/frontend_dist/assets/AccountPasswordSubForm-YsaE_cDx.js +1 -0
- modoboa/frontend_dist/assets/AccountView-1jfKFDwb.js +1 -0
- modoboa/frontend_dist/assets/AddressBook-CwN64Zls.js +1 -0
- modoboa/frontend_dist/assets/AdminLayout-Cxm1lggg.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +1 -0
- modoboa/frontend_dist/assets/AlarmsView-Bcjsicac.js +1 -0
- modoboa/frontend_dist/assets/AliasEditView-k3rVt1tG.js +1 -0
- modoboa/frontend_dist/assets/{AliasRecipientForm-IOae6sjF.js → AliasRecipientForm-IehUzKok.js} +1 -1
- modoboa/frontend_dist/assets/AliasView-DMzA10eD.js +1 -0
- modoboa/frontend_dist/assets/AuditTrailView-5dnGX5El.js +1 -0
- modoboa/frontend_dist/assets/CalendarView-DZONeDA9.js +1 -0
- modoboa/frontend_dist/assets/{ChoiceField-DJ_c78Cm.js → ChoiceField-DnwXRkht.js} +1 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-kghmfNuE.js +1 -0
- modoboa/frontend_dist/assets/ComposeEmailView-DLv3wk1k.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-CcPrCKuI.js +1 -0
- modoboa/frontend_dist/assets/{ConnectedLayout-Dvwmicnc.css → ConnectedLayout-Bxh21hcH.css} +1 -1
- modoboa/frontend_dist/assets/ConnectedLayout-CWlBK7Hf.js +1 -0
- modoboa/frontend_dist/assets/CreationForm-CW4lxnPg.js +1 -0
- modoboa/frontend_dist/assets/DashboardView-DXVZMbMo.js +1 -0
- modoboa/frontend_dist/assets/DomainAdminList-C3jcDDc3.js +1 -0
- modoboa/frontend_dist/assets/DomainEditView-ph8AaElX.js +1 -0
- modoboa/frontend_dist/assets/{DomainTransportForm-C2xo0Yd7.js → DomainTransportForm-NCz6Bl-h.js} +1 -1
- modoboa/frontend_dist/assets/DomainView-BgMSSuU-.js +5 -0
- modoboa/frontend_dist/assets/{DomainView-BDKoBFYr.css → DomainView-CCLYXPHx.css} +1 -1
- modoboa/frontend_dist/assets/DomainsView-CEEU9btK.js +1 -0
- modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +1 -0
- modoboa/frontend_dist/assets/EmailField-DeqDPm5j.js +1 -0
- modoboa/frontend_dist/assets/EmailView-DczVhVO0.js +1 -0
- modoboa/frontend_dist/assets/EmptyLayout-BXgcfMLH.js +1 -0
- modoboa/frontend_dist/assets/FiltersView-nJj_gSCx.js +1 -0
- modoboa/frontend_dist/assets/ForwardEmailView-Bgv3JQb6.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-CJ9umKeO.js → HtmlEditor-BWRdelVw.js} +1 -1
- modoboa/frontend_dist/assets/{IdentitiesView-0ziuQ5s-.css → IdentitiesView-DPrrRMS5.css} +1 -1
- modoboa/frontend_dist/assets/IdentitiesView-Dld9IloZ.js +1 -0
- modoboa/frontend_dist/assets/InformationView-BBWKSX8D.js +1 -0
- modoboa/frontend_dist/assets/InformationView-C9vvvQhJ.css +1 -0
- modoboa/frontend_dist/assets/{LoadingData-CYwX3Jpn.js → LoadingData-G57nJ_JV.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-E01qkKn0.js → LoginCallbackView-DjyE2SG_.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-Cy4uFV9h.js → LoginView-CqCCXYLo.js} +1 -1
- modoboa/frontend_dist/assets/{MailboxView-B-aI4XBq.css → MailboxView-CfStlWhk.css} +1 -1
- modoboa/frontend_dist/assets/MailboxView-DRrs9eLO.js +1 -0
- modoboa/frontend_dist/assets/MenuItems-BqIZW5av.js +1 -0
- modoboa/frontend_dist/assets/MessageView-D_6tx_gd.js +1 -0
- modoboa/frontend_dist/assets/MessagesView-BH7JIR03.js +1 -0
- modoboa/frontend_dist/assets/MigrationsView-Cv_So9T-.js +1 -0
- modoboa/frontend_dist/assets/{ParametersForm-BZM0QSvg.js → ParametersForm-3qXttTuQ.js} +1 -1
- modoboa/frontend_dist/assets/ParametersView-3Ns04cpQ.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-B5B5Dt6K.js +1 -0
- modoboa/frontend_dist/assets/ProviderEditView-zh7CY832.js +1 -0
- modoboa/frontend_dist/assets/ProviderGeneralForm-BQU7t3ma.js +1 -0
- modoboa/frontend_dist/assets/ProvidersView-CoF_ZkZA.js +1 -0
- modoboa/frontend_dist/assets/QuarantineLayout-CYBsrbJM.js +1 -0
- modoboa/frontend_dist/assets/QuarantineView-D4gOE4EQ.css +1 -0
- modoboa/frontend_dist/assets/QuarantineView-DNvpoycb.js +1 -0
- modoboa/frontend_dist/assets/ReplyEmailView-D1XTcglu.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-BW8rUGgZ.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-DfDHiYeX.js +1 -0
- modoboa/frontend_dist/assets/{SettingsView-BxLJBFY0.js → SettingsView-gQiJ2NVb.js} +2 -2
- modoboa/frontend_dist/assets/StatisticsView-DYalet_q.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-BZ2htbFk.js +1 -0
- modoboa/frontend_dist/assets/UserLayout-zUtHi-z-.js +1 -0
- modoboa/frontend_dist/assets/VAlert-4r6LxKtg.js +1 -0
- modoboa/frontend_dist/assets/VApp-CX_C7AUN.js +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-DNKmBvyZ.js +1 -0
- modoboa/frontend_dist/assets/VAvatar-DbuoZWmf.js +1 -0
- modoboa/frontend_dist/assets/VBadge-BQrRJ9S0.css +1 -0
- modoboa/frontend_dist/assets/VBadge-Bv2nvUmC.js +1 -0
- modoboa/frontend_dist/assets/VCard-DzjUT5OP.js +1 -0
- modoboa/frontend_dist/assets/VCheckbox-dr7UFjl4.js +1 -0
- modoboa/frontend_dist/assets/{VCheckboxBtn-Dt810gWf.js → VCheckboxBtn-CpFdBnTv.js} +1 -1
- modoboa/frontend_dist/assets/VChip-CaQvfmkw.js +1 -0
- modoboa/frontend_dist/assets/VColorPicker-ByGpCW5O.js +1 -0
- modoboa/frontend_dist/assets/{VContainer-DvTbsotR.js → VContainer-74Dnn8Ux.js} +1 -1
- modoboa/frontend_dist/assets/VDataTable-CL7yHvG7.js +1 -0
- modoboa/frontend_dist/assets/VDataTableServer-BqvNcIdw.js +1 -0
- modoboa/frontend_dist/assets/VDataTableVirtual--KsOP8i6.js +1 -0
- modoboa/frontend_dist/assets/{VDialog-Bk6EWNhz.js → VDialog-DmTGCGR0.js} +1 -1
- modoboa/frontend_dist/assets/VExpansionPanels-B7sSTCwd.js +1 -0
- modoboa/frontend_dist/assets/VFileInput-SULIc6F3.js +1 -0
- modoboa/frontend_dist/assets/VForm-DsRLc-sa.js +1 -0
- modoboa/frontend_dist/assets/VInput-CcxkaOXT.css +1 -0
- modoboa/frontend_dist/assets/VInput-DVKUObZe.js +1 -0
- modoboa/frontend_dist/assets/VMenu-nv0XOgg0.js +1 -0
- modoboa/frontend_dist/assets/VPicker-DnDSWJHJ.js +1 -0
- modoboa/frontend_dist/assets/VProgressCircular-qK6p5X_Y.js +1 -0
- modoboa/frontend_dist/assets/VRadioGroup-CbiPLy0t.js +1 -0
- modoboa/frontend_dist/assets/{VRow-BF35mT1S.js → VRow-DJ0NB63-.js} +1 -1
- modoboa/frontend_dist/assets/VSelect-CxCFMHyF.js +1 -0
- modoboa/frontend_dist/assets/VSelectionControl-C-6A4us5.js +1 -0
- modoboa/frontend_dist/assets/VSheet-DI6SxLnG.js +1 -0
- modoboa/frontend_dist/assets/VSpacer-CoJVmx8k.js +1 -0
- modoboa/frontend_dist/assets/VSwitch-DPnjPQuU.js +1 -0
- modoboa/frontend_dist/assets/VTable-ldTxgQPW.js +1 -0
- modoboa/frontend_dist/assets/VTabs-aS8WSL9I.js +1 -0
- modoboa/frontend_dist/assets/VTextField-BzBVKKob.css +1 -0
- modoboa/frontend_dist/assets/VTextField-DKbr4H5w.js +1 -0
- modoboa/frontend_dist/assets/VTextarea-BttkFsM4.js +1 -0
- modoboa/frontend_dist/assets/VToolbar-BxX3W2kR.js +1 -0
- modoboa/frontend_dist/assets/VWindowItem-Cvqvdegd.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-BT2k6U7q.js +1 -0
- modoboa/frontend_dist/assets/accounts-CC2F0a0c.js +1 -0
- modoboa/frontend_dist/assets/{admin-o-HRGnmT.js → admin-CHCHFGI6.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-DDVeehyg.js → aliases-C9bUD4Ws.js} +1 -1
- modoboa/frontend_dist/assets/amavis-BhzV4rgf.js +1 -0
- modoboa/frontend_dist/assets/amavis-DCVJxuui.js +1 -0
- modoboa/frontend_dist/assets/{contacts-C84DY-Q1.js → contacts-Dxz6eWpf.js} +1 -1
- modoboa/frontend_dist/assets/{domains-Bgn4ixHL.js → domains-C2cornvL.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-DTE-V7Y1.js → domains.store-BLKRipG8.js} +1 -1
- modoboa/frontend_dist/assets/{filter-CnffiQAW.js → filter-rmxrcjKk.js} +1 -1
- modoboa/frontend_dist/assets/forwardRefs-CpzzjgpX.js +1 -0
- modoboa/frontend_dist/assets/global.store-DndbMXYb.js +1 -0
- modoboa/frontend_dist/assets/{importExport-BlQYb0NO.js → importExport-C3uqrcok.js} +1 -1
- modoboa/frontend_dist/assets/index-LhNzkzAh.js +984 -0
- modoboa/frontend_dist/assets/layout-DbjDe3Wl.js +1 -0
- modoboa/frontend_dist/assets/{layout.store-DkjrAoXt.js → layout.store-Vq5mvIp7.js} +1 -1
- modoboa/frontend_dist/assets/{logos-q8SEyAa4.js → logos-Bvcy0usu.js} +1 -1
- modoboa/frontend_dist/assets/{logs-B7IJ7LBa.js → logs-BuItINky.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-A6iBEYQq.js → parameters-C8IYEP7q.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-BiXS4_6w.js → parameters.store-1cwSP2JP.js} +1 -1
- modoboa/frontend_dist/assets/permissions-DQjAcO9S.js +1 -0
- modoboa/frontend_dist/assets/{ssrBoot-AzTdjPjk.js → ssrBoot-BxIQ9ccA.js} +1 -1
- modoboa/frontend_dist/assets/{tag-BnSYRTcD.js → tag-3cfI1_f7.js} +1 -1
- modoboa/frontend_dist/assets/transports-D4Jk4-AP.js +1 -0
- modoboa/frontend_dist/assets/{webmail-CSH_3l6R.js → webmail-B2IUjaxM.js} +1 -1
- modoboa/frontend_dist/index.html +1 -1
- modoboa/lib/email_utils.py +2 -2
- modoboa/lib/permissions.py +7 -0
- modoboa/lib/test_runners.py +29 -0
- modoboa/lib/tests/__init__.py +5 -1
- modoboa/locale/br/LC_MESSAGES/django.po +87 -75
- modoboa/locale/cs/LC_MESSAGES/django.po +82 -74
- modoboa/locale/cs_CZ/LC_MESSAGES/django.po +145 -121
- modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de/LC_MESSAGES/django.po +339 -651
- modoboa/locale/de_DE/LC_MESSAGES/django.po +87 -75
- modoboa/locale/el_GR/LC_MESSAGES/django.po +160 -135
- modoboa/locale/en/LC_MESSAGES/django.po +82 -74
- modoboa/locale/es/LC_MESSAGES/django.po +158 -131
- modoboa/locale/es_MX/LC_MESSAGES/django.po +82 -74
- modoboa/locale/fi/LC_MESSAGES/django.po +87 -75
- modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fr/LC_MESSAGES/django.po +469 -201
- modoboa/locale/hu/LC_MESSAGES/django.po +82 -74
- modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/it/LC_MESSAGES/django.po +148 -122
- modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ja_JP/LC_MESSAGES/django.po +80 -72
- modoboa/locale/ka/LC_MESSAGES/django.po +82 -74
- modoboa/locale/nl_NL/LC_MESSAGES/django.po +160 -132
- modoboa/locale/no/LC_MESSAGES/django.po +82 -74
- modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pl_PL/LC_MESSAGES/django.po +172 -149
- modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_BR/LC_MESSAGES/django.po +172 -144
- modoboa/locale/pt_PT/LC_MESSAGES/django.po +135 -112
- modoboa/locale/ro_RO/LC_MESSAGES/django.po +87 -75
- modoboa/locale/ru/LC_MESSAGES/django.po +142 -118
- modoboa/locale/si/LC_MESSAGES/django.po +82 -74
- modoboa/locale/sk/LC_MESSAGES/django.po +82 -74
- modoboa/locale/sk_SK/LC_MESSAGES/django.po +84 -76
- modoboa/locale/sl_SI/LC_MESSAGES/django.po +90 -76
- modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/sv/LC_MESSAGES/django.po +172 -139
- modoboa/locale/tr/LC_MESSAGES/django.po +87 -75
- modoboa/locale/tr_TR/LC_MESSAGES/django.po +84 -74
- modoboa/locale/uk/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh_CN/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh_TW/LC_MESSAGES/django.po +87 -75
- modoboa/parameters/api/v2/tests.py +2 -2
- modoboa/parameters/api/v2/viewsets.py +2 -0
- modoboa/policyd/tests.py +2 -0
- modoboa/urls_api_v2.py +6 -0
- modoboa/webmail/lib/imaputils.py +2 -2
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/METADATA +6 -4
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/RECORD +235 -185
- modoboa/frontend_dist/assets/AccountAliasForm-BV6KvTu6.js +0 -1
- modoboa/frontend_dist/assets/AccountEditView-DDOFyfBD.js +0 -1
- modoboa/frontend_dist/assets/AccountLayout-rX51xgxT.js +0 -1
- modoboa/frontend_dist/assets/AccountPasswordSubForm-D9S6LaeH.js +0 -1
- modoboa/frontend_dist/assets/AccountView-cmvaZNq3.js +0 -1
- modoboa/frontend_dist/assets/AddressBook-DCJxL8SU.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-r0wfG2lO.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-Bheey-gp.css +0 -1
- modoboa/frontend_dist/assets/AlarmsView-C0bqC4PA.js +0 -1
- modoboa/frontend_dist/assets/AliasEditView-DVoWoCGY.js +0 -1
- modoboa/frontend_dist/assets/AliasView-DrONZXOh.js +0 -1
- modoboa/frontend_dist/assets/AuditTrailView-OTkoZaMU.js +0 -1
- modoboa/frontend_dist/assets/CalendarView-CqF4_Ui9.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-DO5_GB3e.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-A91HCBsN.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-BBcgdAnO.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-1oRW-Rql.js +0 -1
- modoboa/frontend_dist/assets/CreationForm-71YJbjsA.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-CdLpSfUl.js +0 -1
- modoboa/frontend_dist/assets/DomainAdminList-BjC4KsqI.js +0 -1
- modoboa/frontend_dist/assets/DomainEditView-CQjKwYxl.js +0 -1
- modoboa/frontend_dist/assets/DomainView-BhhuZI_N.js +0 -5
- modoboa/frontend_dist/assets/DomainsView-Cft4BP8Z.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-DasJ0NdZ.css +0 -1
- modoboa/frontend_dist/assets/EmailField-C8umy0EU.js +0 -1
- modoboa/frontend_dist/assets/EmailView-ki7uEQPD.js +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-DaA1XH9n.js +0 -1
- modoboa/frontend_dist/assets/FiltersView-FYFZxG4B.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-cUbnSYCF.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-njNo8N5n.js +0 -1
- modoboa/frontend_dist/assets/InformationView-D1h38POt.js +0 -1
- modoboa/frontend_dist/assets/InformationView-U5Ww-Sx1.css +0 -1
- modoboa/frontend_dist/assets/MailboxView-IlrLWm_H.js +0 -1
- modoboa/frontend_dist/assets/MenuItems-BAtHWzAE.js +0 -1
- modoboa/frontend_dist/assets/MessagesView-OSpjixFq.js +0 -1
- modoboa/frontend_dist/assets/MigrationsView-DKNOsVzF.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-C4bXASiq.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-CYXgNmc1.js +0 -1
- modoboa/frontend_dist/assets/ProviderEditView-CyxCWTST.js +0 -1
- modoboa/frontend_dist/assets/ProviderGeneralForm-BYPjNHqB.js +0 -1
- modoboa/frontend_dist/assets/ProvidersView-CxrMkRyk.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-Dkw9-N26.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-CuUvrOdY.js +0 -1
- modoboa/frontend_dist/assets/StatisticsView-BN7QsZMT.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-BMN8BeFZ.js +0 -1
- modoboa/frontend_dist/assets/UserLayout-B6-JQg4F.js +0 -1
- modoboa/frontend_dist/assets/VAlert-DIQTrRif.js +0 -1
- modoboa/frontend_dist/assets/VApp-CpkYA7js.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-C4IpXyl8.js +0 -1
- modoboa/frontend_dist/assets/VAvatar-Lpb-Dion.js +0 -1
- modoboa/frontend_dist/assets/VCard-er_isjE_.js +0 -1
- modoboa/frontend_dist/assets/VCheckbox-D-u8JXP1.js +0 -1
- modoboa/frontend_dist/assets/VChip-B4iSpj8_.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-BAjGDsXv.js +0 -1
- modoboa/frontend_dist/assets/VDataTable-4JRjbtgF.js +0 -1
- modoboa/frontend_dist/assets/VDataTableServer-tIDT1m3-.js +0 -1
- modoboa/frontend_dist/assets/VDataTableVirtual-BlnO18u_.js +0 -1
- modoboa/frontend_dist/assets/VExpansionPanels-CwGtXDhr.js +0 -1
- modoboa/frontend_dist/assets/VFileInput-D1_7ZkO_.js +0 -1
- modoboa/frontend_dist/assets/VForm-DAkW4nfy.js +0 -1
- modoboa/frontend_dist/assets/VMenu-BPFJwj2f.js +0 -1
- modoboa/frontend_dist/assets/VPicker-CfT82M8N.js +0 -1
- modoboa/frontend_dist/assets/VProgressCircular-w75-3ogi.js +0 -1
- modoboa/frontend_dist/assets/VRadioGroup-0j6DNC_k.js +0 -1
- modoboa/frontend_dist/assets/VSelect-Cs4ARbAS.js +0 -1
- modoboa/frontend_dist/assets/VSelectionControl-Dg-XyRRS.js +0 -1
- modoboa/frontend_dist/assets/VSheet-Btq_Mu4s.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-C7xukQmu.js +0 -1
- modoboa/frontend_dist/assets/VSwitch-Cs1NQrmk.js +0 -1
- modoboa/frontend_dist/assets/VTable-CNz2SGk4.js +0 -1
- modoboa/frontend_dist/assets/VTabs-B1fyVn4M.js +0 -1
- modoboa/frontend_dist/assets/VTextField-BdyvgvkG.js +0 -1
- modoboa/frontend_dist/assets/VTextField-C-J20yj_.css +0 -1
- modoboa/frontend_dist/assets/VTextarea-DnOMpe0Q.js +0 -1
- modoboa/frontend_dist/assets/VToolbar-BiCiBxBJ.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-ChWm_kz3.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-o4uEkp9e.js +0 -1
- modoboa/frontend_dist/assets/forwardRefs-Dvjn_Xq4.js +0 -1
- modoboa/frontend_dist/assets/global.store-BaiD63EN.js +0 -1
- modoboa/frontend_dist/assets/index-I1VDlN4g.js +0 -984
- modoboa/frontend_dist/assets/layout-D8ZJPiJ_.js +0 -1
- modoboa/frontend_dist/assets/permissions-CITHLHVg.js +0 -1
- modoboa/frontend_dist/assets/transports-Dz7c6kIy.js +0 -1
- {modoboa-2.4.11.data → modoboa-2.5.1.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/WHEEL +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/entry_points.txt +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Amavis serializers."""
|
|
2
|
+
|
|
3
|
+
from django.utils.translation import gettext as _
|
|
4
|
+
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
|
|
7
|
+
from modoboa.amavis import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GlobalParametersSerializer(serializers.Serializer):
|
|
11
|
+
|
|
12
|
+
localpart_is_case_sensitive = serializers.BooleanField(default=False)
|
|
13
|
+
recipient_delimiter = serializers.CharField(default="", allow_blank=True)
|
|
14
|
+
max_messages_age = serializers.IntegerField(default=14)
|
|
15
|
+
released_msgs_cleanup = serializers.BooleanField(default=False)
|
|
16
|
+
am_pdp_mode = serializers.ChoiceField(
|
|
17
|
+
choices=[("inet", "inet"), ("unix", "unix")],
|
|
18
|
+
default="unix",
|
|
19
|
+
)
|
|
20
|
+
am_pdp_host = serializers.CharField(default="localhost", required=False)
|
|
21
|
+
am_pdp_port = serializers.IntegerField(default=9998, required=False)
|
|
22
|
+
am_pdp_socket = serializers.CharField(
|
|
23
|
+
default="/var/amavis/amavisd.sock", required=False
|
|
24
|
+
)
|
|
25
|
+
user_can_release = serializers.BooleanField(default=False)
|
|
26
|
+
self_service = serializers.BooleanField(default=False)
|
|
27
|
+
notifications_sender = serializers.EmailField(default="noreply@domain.tld")
|
|
28
|
+
manual_learning = serializers.BooleanField(default=True)
|
|
29
|
+
sa_is_local = serializers.BooleanField(default=True)
|
|
30
|
+
default_user = serializers.CharField(default="amavis", required=False)
|
|
31
|
+
spamd_address = serializers.CharField(default="127.0.0.1", required=False)
|
|
32
|
+
spamd_port = serializers.IntegerField(default=783, required=False)
|
|
33
|
+
domain_level_learning = serializers.BooleanField(default=False)
|
|
34
|
+
user_level_learning = serializers.BooleanField(default=False)
|
|
35
|
+
|
|
36
|
+
def validate(self, data):
|
|
37
|
+
am_pdp_mode = data.get("am_pdp_mode")
|
|
38
|
+
errors = {}
|
|
39
|
+
error = _("This field is required")
|
|
40
|
+
if am_pdp_mode == "inet":
|
|
41
|
+
for name in ["am_pdp_host", "am_pdp_port"]:
|
|
42
|
+
if name not in data:
|
|
43
|
+
errors[name] = error
|
|
44
|
+
else:
|
|
45
|
+
name = "am_pdp_socket"
|
|
46
|
+
if name not in data:
|
|
47
|
+
errors[name] = error
|
|
48
|
+
manual_learning = data.get("manual_learning")
|
|
49
|
+
if manual_learning:
|
|
50
|
+
name = "default_user"
|
|
51
|
+
if name not in data:
|
|
52
|
+
errors[name] = error
|
|
53
|
+
sa_is_local = data.get("sa_is_local")
|
|
54
|
+
if sa_is_local:
|
|
55
|
+
for name in ["spamd_address", "spamd_port"]:
|
|
56
|
+
if name not in data:
|
|
57
|
+
errors[name] = error
|
|
58
|
+
if errors:
|
|
59
|
+
raise serializers.ValidationError(errors)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UserPreferencesSerializer(serializers.Serializer):
|
|
64
|
+
|
|
65
|
+
messages_per_page = serializers.IntegerField(default=40)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class HeaderSerializer(serializers.Serializer):
|
|
69
|
+
|
|
70
|
+
name = serializers.CharField()
|
|
71
|
+
value = serializers.CharField()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MessageHeadersSerializer(serializers.Serializer):
|
|
75
|
+
|
|
76
|
+
headers = HeaderSerializer(many=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MessageSerializer(serializers.Serializer):
|
|
80
|
+
|
|
81
|
+
qtype = serializers.CharField()
|
|
82
|
+
qreason = serializers.CharField()
|
|
83
|
+
headers = HeaderSerializer(many=True)
|
|
84
|
+
body = serializers.CharField()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CompactMessageSerializer(serializers.Serializer):
|
|
88
|
+
|
|
89
|
+
mailid = serializers.CharField()
|
|
90
|
+
type = serializers.CharField()
|
|
91
|
+
status = serializers.CharField()
|
|
92
|
+
score = serializers.CharField()
|
|
93
|
+
to_address = serializers.CharField()
|
|
94
|
+
from_address = serializers.CharField()
|
|
95
|
+
subject = serializers.CharField()
|
|
96
|
+
datetime = serializers.DateTimeField()
|
|
97
|
+
style = serializers.CharField(required=False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PaginatedMessageListSerializer(serializers.Serializer):
|
|
101
|
+
|
|
102
|
+
count = serializers.IntegerField()
|
|
103
|
+
first_index = serializers.IntegerField()
|
|
104
|
+
last_index = serializers.IntegerField()
|
|
105
|
+
prev_page = serializers.IntegerField()
|
|
106
|
+
next_page = serializers.IntegerField()
|
|
107
|
+
results = CompactMessageSerializer(many=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class MessageIdentifierSerializer(serializers.Serializer):
|
|
111
|
+
|
|
112
|
+
mailid = serializers.CharField()
|
|
113
|
+
rcpt = serializers.CharField()
|
|
114
|
+
secret_id = serializers.CharField(required=False)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class MessageSelectionSerializer(serializers.Serializer):
|
|
118
|
+
|
|
119
|
+
selection = MessageIdentifierSerializer(many=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MarkMessageSelectionSerializer(MessageSelectionSerializer):
|
|
123
|
+
|
|
124
|
+
type = serializers.CharField()
|
|
125
|
+
database = serializers.CharField(required=False)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PolicySerializer(serializers.ModelSerializer):
|
|
129
|
+
|
|
130
|
+
class Meta:
|
|
131
|
+
model = models.Policy
|
|
132
|
+
fields = (
|
|
133
|
+
"bypass_virus_checks",
|
|
134
|
+
"bypass_spam_checks",
|
|
135
|
+
"bypass_banned_checks",
|
|
136
|
+
"spam_tag2_level",
|
|
137
|
+
"spam_kill_level",
|
|
138
|
+
"spam_subject_tag2",
|
|
139
|
+
)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""SQL connector module."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
from django.db.models import Q
|
|
6
|
+
|
|
7
|
+
from modoboa.admin.models import Domain
|
|
8
|
+
from modoboa.lib.email_utils import decode
|
|
9
|
+
|
|
10
|
+
from .lib import cleanup_email_address, make_query_args
|
|
11
|
+
from .models import Maddr, Msgrcpt, Quarantine
|
|
12
|
+
from .utils import ConvertFrom, fix_utf8_encoding, smart_bytes, smart_str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reverse_domain_names(domains):
|
|
16
|
+
"""Return a list of reversed domain names."""
|
|
17
|
+
return [".".join(reversed(domain.split("."))) for domain in domains]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SQLconnector:
|
|
21
|
+
"""This class handles all database operations."""
|
|
22
|
+
|
|
23
|
+
ORDER_TRANSLATION_TABLE = {
|
|
24
|
+
"type": "content",
|
|
25
|
+
"score": "bspam_level",
|
|
26
|
+
"datetime": "mail__time_num",
|
|
27
|
+
"subject": "mail__subject",
|
|
28
|
+
"from_address": "mail__from_addr",
|
|
29
|
+
"to_address": "rid__email",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
QUARANTINE_FIELDS = [
|
|
33
|
+
"content",
|
|
34
|
+
"bspam_level",
|
|
35
|
+
"rs",
|
|
36
|
+
"rid__email",
|
|
37
|
+
"mail__from_addr",
|
|
38
|
+
"mail__subject",
|
|
39
|
+
"mail__mail_id",
|
|
40
|
+
"mail__time_num",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def __init__(self, user=None, ordering: str | None = None) -> None:
|
|
44
|
+
"""Constructor."""
|
|
45
|
+
self.user = user
|
|
46
|
+
self.messages = None
|
|
47
|
+
self.ordering = ordering
|
|
48
|
+
|
|
49
|
+
self._messages_count = None
|
|
50
|
+
self._annotations = {}
|
|
51
|
+
|
|
52
|
+
def _exec(self, query, args):
|
|
53
|
+
"""Execute a raw SQL query.
|
|
54
|
+
|
|
55
|
+
:param string query: query to execute
|
|
56
|
+
:param list args: a list of arguments to replace in :kw:`query`
|
|
57
|
+
"""
|
|
58
|
+
from django.db import connections, transaction
|
|
59
|
+
|
|
60
|
+
with transaction.atomic():
|
|
61
|
+
cursor = connections["amavis"].cursor()
|
|
62
|
+
cursor.execute(query, args)
|
|
63
|
+
|
|
64
|
+
def _apply_msgrcpt_simpleuser_filter(self, flt):
|
|
65
|
+
"""Apply specific filter for simple users."""
|
|
66
|
+
if "str_email" not in self._annotations:
|
|
67
|
+
self._annotations["str_email"] = ConvertFrom("rid__email")
|
|
68
|
+
|
|
69
|
+
rcpts = [self.user.email]
|
|
70
|
+
if hasattr(self.user, "mailbox"):
|
|
71
|
+
rcpts += self.user.mailbox.alias_addresses
|
|
72
|
+
|
|
73
|
+
query_rcpts = []
|
|
74
|
+
for rcpt in rcpts:
|
|
75
|
+
query_rcpts += make_query_args(rcpt, exact_extension=False, wildcard=".*")
|
|
76
|
+
|
|
77
|
+
re = f"({'|'.join(query_rcpts)})"
|
|
78
|
+
return flt & Q(str_email__regex=re)
|
|
79
|
+
|
|
80
|
+
def _apply_msgrcpt_filters(self, flt):
|
|
81
|
+
"""Apply filters based on user's role."""
|
|
82
|
+
if self.user.role == "SimpleUsers":
|
|
83
|
+
flt = self._apply_msgrcpt_simpleuser_filter(flt)
|
|
84
|
+
elif not self.user.is_superuser:
|
|
85
|
+
doms = Domain.objects.get_for_admin(self.user).values_list(
|
|
86
|
+
"name", flat=True
|
|
87
|
+
)
|
|
88
|
+
flt &= Q(rid__domain__in=reverse_domain_names(doms))
|
|
89
|
+
return flt
|
|
90
|
+
|
|
91
|
+
def _get_quarantine_content(self, request):
|
|
92
|
+
"""Fetch quarantine content.
|
|
93
|
+
|
|
94
|
+
Filters: rs, rid, content
|
|
95
|
+
"""
|
|
96
|
+
flt = (
|
|
97
|
+
Q(rs__in=[" ", "V", "R", "p", "S", "H"])
|
|
98
|
+
if request.GET.get("viewrequests", "0") != "1"
|
|
99
|
+
else Q(rs="p")
|
|
100
|
+
)
|
|
101
|
+
flt = self._apply_msgrcpt_filters(flt)
|
|
102
|
+
search = request.GET.get("search")
|
|
103
|
+
if search:
|
|
104
|
+
criteria = request.GET.get("criteria", "both")
|
|
105
|
+
if criteria == "both":
|
|
106
|
+
criteria = "from_addr,subject,to"
|
|
107
|
+
search_flt = None
|
|
108
|
+
for crit in criteria.split(","):
|
|
109
|
+
if crit == "from_addr":
|
|
110
|
+
nfilter = Q(mail__from_addr__icontains=search)
|
|
111
|
+
elif crit == "subject":
|
|
112
|
+
nfilter = Q(mail__subject__icontains=search)
|
|
113
|
+
elif crit == "to":
|
|
114
|
+
if "str_email" not in self._annotations:
|
|
115
|
+
self._annotations["str_email"] = ConvertFrom("rid__email")
|
|
116
|
+
nfilter = Q(str_email__icontains=search)
|
|
117
|
+
else:
|
|
118
|
+
continue
|
|
119
|
+
search_flt = nfilter if search_flt is None else search_flt | nfilter
|
|
120
|
+
if search_flt:
|
|
121
|
+
flt &= search_flt
|
|
122
|
+
|
|
123
|
+
msgtype = request.GET.get("msgtype")
|
|
124
|
+
if msgtype is not None:
|
|
125
|
+
flt &= Q(content=msgtype)
|
|
126
|
+
|
|
127
|
+
flt &= Q(mail__in=Quarantine.objects.filter(chunk_ind=1).values("mail_id"))
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
Msgrcpt.objects.annotate(**self._annotations)
|
|
131
|
+
.select_related("mail", "rid")
|
|
132
|
+
.filter(flt)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def messages_count(self, request) -> int:
|
|
136
|
+
"""
|
|
137
|
+
Return the total number of messages living in the quarantine.
|
|
138
|
+
|
|
139
|
+
We also store the built queryset for a later use.
|
|
140
|
+
"""
|
|
141
|
+
if self.user is None:
|
|
142
|
+
return None
|
|
143
|
+
if self._messages_count is None:
|
|
144
|
+
self.messages = self._get_quarantine_content(request)
|
|
145
|
+
self.messages = self.messages.values(*self.QUARANTINE_FIELDS)
|
|
146
|
+
|
|
147
|
+
order = self.ordering
|
|
148
|
+
if order is not None:
|
|
149
|
+
sign = ""
|
|
150
|
+
if order[0] == "-":
|
|
151
|
+
sign = "-"
|
|
152
|
+
order = order[1:]
|
|
153
|
+
order = self.ORDER_TRANSLATION_TABLE.get(order)
|
|
154
|
+
if order:
|
|
155
|
+
self.messages = self.messages.order_by(sign + order)
|
|
156
|
+
|
|
157
|
+
self._messages_count = len(self.messages)
|
|
158
|
+
|
|
159
|
+
return self._messages_count
|
|
160
|
+
|
|
161
|
+
def fetch(self, start=None, stop=None):
|
|
162
|
+
"""Fetch a range of messages from the internal cache."""
|
|
163
|
+
emails = []
|
|
164
|
+
for qm in self.messages[start - 1 : stop]:
|
|
165
|
+
if qm["rs"] == "D":
|
|
166
|
+
continue
|
|
167
|
+
m = {
|
|
168
|
+
"from_address": cleanup_email_address(
|
|
169
|
+
fix_utf8_encoding(qm["mail__from_addr"])
|
|
170
|
+
),
|
|
171
|
+
"to_address": smart_str(qm["rid__email"]),
|
|
172
|
+
"subject": fix_utf8_encoding(qm["mail__subject"]),
|
|
173
|
+
"mailid": smart_str(qm["mail__mail_id"]),
|
|
174
|
+
"datetime": datetime.datetime.fromtimestamp(qm["mail__time_num"]),
|
|
175
|
+
"type": qm["content"],
|
|
176
|
+
"score": qm["bspam_level"],
|
|
177
|
+
"status": qm["rs"],
|
|
178
|
+
}
|
|
179
|
+
if qm["rs"] in ["", " "]:
|
|
180
|
+
m["style"] = "unseen"
|
|
181
|
+
elif qm["rs"] == "p":
|
|
182
|
+
m["style"] = "pending"
|
|
183
|
+
emails.append(m)
|
|
184
|
+
return emails
|
|
185
|
+
|
|
186
|
+
def get_recipient_message(self, address, mailid):
|
|
187
|
+
"""Retrieve a message for a given recipient."""
|
|
188
|
+
assert isinstance(address, str), "address should be of type str"
|
|
189
|
+
|
|
190
|
+
return Msgrcpt.objects.annotate(str_email=ConvertFrom("rid__email")).get(
|
|
191
|
+
mail=mailid.encode("ascii"), str_email=address
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def set_msgrcpt_status(self, address, mailid: str, status):
|
|
195
|
+
"""Change the status (rs field) of a message recipient.
|
|
196
|
+
|
|
197
|
+
:param string status: status
|
|
198
|
+
"""
|
|
199
|
+
assert isinstance(address, str), "address should be of type str"
|
|
200
|
+
addr = Maddr.objects.annotate(str_email=ConvertFrom("email")).get(
|
|
201
|
+
str_email=address
|
|
202
|
+
)
|
|
203
|
+
self._exec(
|
|
204
|
+
"UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s",
|
|
205
|
+
[status, mailid.encode("ascii"), addr.id],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def get_domains_pending_requests(self, domains):
|
|
209
|
+
"""Retrieve pending release requests for a list of domains."""
|
|
210
|
+
return Msgrcpt.objects.filter(
|
|
211
|
+
rs="p", rid__domain__in=reverse_domain_names(domains)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def get_pending_requests(self):
|
|
215
|
+
"""Return the number of requests currently pending."""
|
|
216
|
+
rq = Q(rs="p")
|
|
217
|
+
if not self.user.is_superuser:
|
|
218
|
+
doms = Domain.objects.get_for_admin(self.user)
|
|
219
|
+
if not doms.exists():
|
|
220
|
+
return 0
|
|
221
|
+
doms_q = Q(
|
|
222
|
+
rid__domain__in=reverse_domain_names(
|
|
223
|
+
doms.values_list("name", flat=True)
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
rq &= doms_q
|
|
227
|
+
return Msgrcpt.objects.filter(rq).count()
|
|
228
|
+
|
|
229
|
+
def get_mail_content(self, mailid) -> str:
|
|
230
|
+
"""Retrieve the content of a message."""
|
|
231
|
+
content_bytes = smart_bytes("").join(
|
|
232
|
+
[
|
|
233
|
+
smart_bytes(qmail.mail_text)
|
|
234
|
+
for qmail in Quarantine.objects.filter(mail=mailid)
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
content = decode(
|
|
238
|
+
content_bytes, "utf-8", append_to_error=f"; mail_id={smart_str(mailid)}"
|
|
239
|
+
)
|
|
240
|
+
return content
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""An email representation based on a database record."""
|
|
2
|
+
|
|
3
|
+
from html2text import HTML2Text
|
|
4
|
+
|
|
5
|
+
from django.template.loader import render_to_string
|
|
6
|
+
|
|
7
|
+
from modoboa.lib.email_utils import Email
|
|
8
|
+
from .sql_connector import SQLconnector
|
|
9
|
+
from .utils import fix_utf8_encoding, smart_str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLemail(Email):
|
|
13
|
+
"""The SQL version of the Email class."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, **kwargs):
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
self.qtype = ""
|
|
18
|
+
self.qreason = ""
|
|
19
|
+
|
|
20
|
+
qreason = self.msg["X-Amavis-Alert"]
|
|
21
|
+
if qreason:
|
|
22
|
+
if "," in qreason:
|
|
23
|
+
self.qtype, qreason = qreason.split(",", 1)
|
|
24
|
+
elif qreason.startswith("BAD HEADER SECTION "):
|
|
25
|
+
# Workaround for amavis <= 2.8.0 :p
|
|
26
|
+
self.qtype = "BAD HEADER SECTION"
|
|
27
|
+
qreason = qreason[19:]
|
|
28
|
+
|
|
29
|
+
qreason = " ".join([x.strip() for x in qreason.splitlines()])
|
|
30
|
+
self.qreason = qreason
|
|
31
|
+
|
|
32
|
+
def _fetch_message(self) -> str:
|
|
33
|
+
return SQLconnector().get_mail_content(self.mailid)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def body(self):
|
|
37
|
+
if self._body is None:
|
|
38
|
+
super().body # noqa
|
|
39
|
+
self._body = fix_utf8_encoding(self._body)
|
|
40
|
+
|
|
41
|
+
# if there's no plain text version available attempt to make one by
|
|
42
|
+
# sanitising the html version. The output isn't always pretty but it
|
|
43
|
+
# is readable, better than a blank screen and helps the user decide
|
|
44
|
+
# if the message is spam or ham.
|
|
45
|
+
if (
|
|
46
|
+
self.dformat == "plain"
|
|
47
|
+
and not self.contents["plain"]
|
|
48
|
+
and self.contents["html"]
|
|
49
|
+
):
|
|
50
|
+
h = HTML2Text()
|
|
51
|
+
h.ignore_tables = True
|
|
52
|
+
h.images_to_alt = True
|
|
53
|
+
mail_text = h.handle(self.contents["html"])
|
|
54
|
+
self.contents["plain"] = self._post_process_plain(smart_str(mail_text))
|
|
55
|
+
self._body = self.viewmail_plain()
|
|
56
|
+
self._body = fix_utf8_encoding(self._body)
|
|
57
|
+
|
|
58
|
+
return self._body
|
|
59
|
+
|
|
60
|
+
def render_headers(self, **kwargs):
|
|
61
|
+
context = {
|
|
62
|
+
"qtype": self.qtype,
|
|
63
|
+
"qreason": self.qreason,
|
|
64
|
+
"headers": self.headers,
|
|
65
|
+
}
|
|
66
|
+
return render_to_string("modoboa_amavis/mailheaders.html", context)
|
modoboa/amavis/tasks.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Async tasks."""
|
|
2
|
+
|
|
3
|
+
# from django.utils.translation import ngettext
|
|
4
|
+
|
|
5
|
+
from modoboa.core import models as core_models
|
|
6
|
+
|
|
7
|
+
from .lib import SpamassassinClient
|
|
8
|
+
from .sql_connector import SQLconnector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def manual_learning(user_pk: int, mtype: str, selection: list[dict], recipient_db: str):
|
|
12
|
+
"""Task to trigger manual learning for given selection."""
|
|
13
|
+
user = core_models.User.objects.get(pk=user_pk)
|
|
14
|
+
connector = SQLconnector()
|
|
15
|
+
saclient = SpamassassinClient(user, recipient_db)
|
|
16
|
+
for item in selection:
|
|
17
|
+
content = connector.get_mail_content(item["mailid"].encode("ascii"))
|
|
18
|
+
result = (
|
|
19
|
+
saclient.learn_spam(item["rcpt"], content)
|
|
20
|
+
if mtype == "spam"
|
|
21
|
+
else saclient.learn_ham(item["rcpt"], content)
|
|
22
|
+
)
|
|
23
|
+
if not result:
|
|
24
|
+
break
|
|
25
|
+
connector.set_msgrcpt_status(item["rcpt"], item["mailid"], mtype[0].upper())
|
|
26
|
+
if saclient.error is None:
|
|
27
|
+
saclient.done()
|
|
28
|
+
|
|
29
|
+
# message = ngettext("%(count)d message processed successfully",
|
|
30
|
+
# "%(count)d messages processed successfully",
|
|
31
|
+
# len(selection)) % {"count": len(selection)}
|
|
32
|
+
# else:
|
|
33
|
+
# message = saclient.error
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{% load i18n %}
|
|
2
|
+
|
|
3
|
+
{% blocktranslate trimmed count counter=total %}
|
|
4
|
+
{{ counter }} release request is pending for action.
|
|
5
|
+
{% plural %}
|
|
6
|
+
{{ counter }} release requests are pending for action.
|
|
7
|
+
{% endblocktranslate %}
|
|
8
|
+
{% translate "Sketch:" %}
|
|
9
|
+
{% for msg in requests %}
|
|
10
|
+
{% translate "From:" %} {{ msg.mail.sid.email }}
|
|
11
|
+
{% translate "To:" %} {{ msg.rid.email }}
|
|
12
|
+
{% translate "Date:" %} {{ msg.mail.time_datetime|date:"DATETIME_FORMAT" }}
|
|
13
|
+
{% translate "Subject:" %} {{ msg.mail.subject }}
|
|
14
|
+
{% translate "Act on this message:" %} {{ baseurl }}/user/quarantine/{{ msg.mail.mail_id }}/{{ msg.rid.email }}
|
|
15
|
+
{% endfor %}
|
|
16
|
+
{% blocktranslate %}Please visit {{ listingurl }} for a full listing.{% endblocktranslate %}
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
X-Envelope-From: <it-edu@evil.example.net>
|
|
2
|
+
X-Envelope-To: <me@example.net>
|
|
3
|
+
X-Envelope-To-Blocked: <me@example.net>
|
|
4
|
+
X-Quarantine-ID: <nZkKQ_MUuyil>
|
|
5
|
+
X-Amavis-Alert: BAD HEADER SECTION, Non-encoded non-ASCII data (and not UTF-8)
|
|
6
|
+
(char 85 hex): Subject: I think I saw you in my dreams\x{85}
|
|
7
|
+
X-Spam-Flag: YES
|
|
8
|
+
X-Spam-Score: 8.178
|
|
9
|
+
X-Spam-Level: ********
|
|
10
|
+
X-Spam-Status: Yes, score=8.178 tag=-999 tag2=3.9 kill=5
|
|
11
|
+
tests=[HEADER_FROM_DIFFERENT_DOMAINS=0.25, HTML_IMAGE_ONLY_28=0.726,
|
|
12
|
+
HTML_MESSAGE=0.001, MIME_HTML_ONLY=1.105, RCVD_IN_MSPIKE_H2=-0.001,
|
|
13
|
+
TVD_PH_BODY_ACCOUNTS_PRE=0.001, T_REMOTE_IMAGE=0.01,
|
|
14
|
+
T_RP_MATCHES_RCVD=-0.01, URIBL_BLACK=1.7, URIBL_DBL_ABUSE_SPAM=2,
|
|
15
|
+
URI_WPADMIN=2.396] autolearn=no autolearn_force=no
|
|
16
|
+
Received: from mail.example.net ([127.0.0.1])
|
|
17
|
+
by mail.example.net (mail.example.net [127.0.0.1]) (amavisd-new, port 10024)
|
|
18
|
+
with LMTP id nZkKQ_MUuyil for <me@example.net>;
|
|
19
|
+
Sat, 16 Dec 2017 13:09:49 +0000 (GMT)
|
|
20
|
+
Received: from evil.example.net (mail.evil.example.net [10.0.0.1])
|
|
21
|
+
by mail.example.net (Postfix) with ESMTPS id 62B2E1AF12D
|
|
22
|
+
for <me@example.net>; Sat, 16 Dec 2017 13:09:49 +0000 (GMT)
|
|
23
|
+
Received: from it-edu by evil.example.net with local (Exim 4.82)
|
|
24
|
+
(envelope-from <it-edu@evil.example.net>)
|
|
25
|
+
id 1eQCDr-0005NE-IT
|
|
26
|
+
for me@example.net; Sat, 16 Dec 2017 16:09:47 +0300
|
|
27
|
+
To: me@example.net
|
|
28
|
+
Subject: =?UTF-8?Q?Account_Suspension_Notification_=21?=
|
|
29
|
+
X-PHP-Originating-Script: 520:maill.php
|
|
30
|
+
Date: Sat, 16 Dec 2017 16:09:47 +0300
|
|
31
|
+
From: Notice <Notice@fake.example.net>
|
|
32
|
+
Reply-To: Notice@fake.example.net
|
|
33
|
+
Message-ID: <6619a5295cefcf484a58944fc280eb90@evil.example.net>
|
|
34
|
+
X-Priority: 1
|
|
35
|
+
X-Mailer: PHPMailer 5.2.10 (https://github.com/PHPMailer/PHPMailer/)
|
|
36
|
+
MIME-Version: 1.0
|
|
37
|
+
Content-Type: text/html; charset=iso-8859-1
|
|
38
|
+
Content-Transfer-Encoding: 8bit
|
|
39
|
+
|
|
40
|
+
<!DOCTYPE html>
|
|
41
|
+
<html>
|
|
42
|
+
<HEAD><STYLE>
|
|
43
|
+
.cssage P
|
|
44
|
+
{
|
|
45
|
+
margin:0px;
|
|
46
|
+
padding:0px
|
|
47
|
+
}
|
|
48
|
+
body.cssage
|
|
49
|
+
{
|
|
50
|
+
font-size: 12pt;
|
|
51
|
+
font-family:Calibri
|
|
52
|
+
}
|
|
53
|
+
</STYLE>
|
|
54
|
+
|
|
55
|
+
</HEAD>
|
|
56
|
+
<BODY class="cssage">
|
|
57
|
+
<DIV dir="ltr"><FONT size=2 face="Arial, Helvetica, sans-serif"></FONT><BR>
|
|
58
|
+
<DIV>
|
|
59
|
+
<DIV>
|
|
60
|
+
<TABLE style="FONT-FAMILY: 'Times New Roman'" border=0 cellSpacing=0 cellPadding=0 width=600 align="center">
|
|
61
|
+
<TR class=header>
|
|
62
|
+
<TD vAlign=bottom><SPAN style="FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(159,159,159); FONT-SIZE: 12px" >This is an automated email, please do not reply</SPAN></TD>
|
|
63
|
+
<TD align=right><IMG style="WIDTH: 215px; HEIGHT: 66px" alt="" src="http://im55.gulfup.com/oLrUdE.jpg"></TD></TR>
|
|
64
|
+
<TR >
|
|
65
|
+
<TD height=14 vAlign=bottom colSpan=2><IMG src="https://www.payza.com/emails/images/bar.png" width="100%" height=3></TD></TR>
|
|
66
|
+
<TR>
|
|
67
|
+
<TD colSpan=2>
|
|
68
|
+
<H1 style="PADDING-BOTTOM: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(189,151,0); FONT-SIZE: 24px; FONT-WEIGHT: normal; PADDING-TOP: 10px">Dear Client</H1>
|
|
69
|
+
<P style="LINE-HEIGHT: 21px; FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(84,84,84); FONT-SIZE: 14px">We've noticed that some of your account information appears to be missing or incorrect
|
|
70
|
+
We need to verify your account information in order to continue using your Apple ID, Please Verify your account information by clicking on the link below<BR><BR><B><A style="COLOR: rgb(27,144,234)" href="http://chiikulab.com/wp-admin/images/Update/Apple2018%20V2.1/
|
|
71
|
+
" target=_blank>Click here to Verify your ID</A></B></P></TD></TR>
|
|
72
|
+
<TR>
|
|
73
|
+
<TD colSpan=2>
|
|
74
|
+
<DIV><SPAN style="FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(51,102,204); FONT-SIZE: 15px" ></SPAN><BR></DIV><br><SPAN style="FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(202,162,0); FONT-SIZE: 17px" class=ecxthanks>Thanks for choosing Apple,</SPAN><BR style="COLOR: rgb(202,162,0)"><SPAN style="FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(202,162,0); FONT-SIZE: 15px" >Apple Team</SPAN></TD></TR>
|
|
75
|
+
<TR >
|
|
76
|
+
<TD height=14 vAlign=bottom colSpan=2><IMG src="https://www.payza.com/emails/images/bar.png" width="100%" height=3></TD></TR>
|
|
77
|
+
<TR>
|
|
78
|
+
<TD colSpan=2>
|
|
79
|
+
<P style="PADDING-BOTTOM: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(84,84,84); FONT-SIZE: 12px; PADDING-TOP: 10px" >© 2017 Apple. All rights reserved.</P>
|
|
80
|
+
<P style="FONT-FAMILY: 'Trebuchet MS', Arial, Helvetica, sans-serif; COLOR: rgb(186,148,0); FONT-SIZE: 11px">Email ID: 163327</P></TD></TR></TABLE></DIV></DIV></DIV></BODY></html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<pre>This is an automated email, please do not reply
|
|
2
|
+
|
|
3
|
+
# Dear Client
|
|
4
|
+
|
|
5
|
+
We've noticed that some of your account information appears to be missing or
|
|
6
|
+
incorrect We need to verify your account information in order to continue
|
|
7
|
+
using your Apple ID, Please Verify your account information by clicking on the
|
|
8
|
+
link below
|
|
9
|
+
|
|
10
|
+
**Click here to Verify your ID**
|
|
11
|
+
|
|
12
|
+
Thanks for choosing Apple,
|
|
13
|
+
Apple Team
|
|
14
|
+
|
|
15
|
+
© 2017 Apple. All rights reserved.
|
|
16
|
+
|
|
17
|
+
Email ID: 163327</pre>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from django.test.utils import override_settings
|
|
3
|
+
|
|
4
|
+
from ..checks import settings_checks
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CheckSessionCookieSecureTest(TestCase):
|
|
8
|
+
|
|
9
|
+
databases = "__all__"
|
|
10
|
+
|
|
11
|
+
@override_settings(AMAVIS_DEFAULT_DATABASE_ENCODING="LATIN-1")
|
|
12
|
+
def test_amavis_database_encoding_incorrect(self):
|
|
13
|
+
"""
|
|
14
|
+
If AMAVIS_DEFAULT_DATABASE_ENCODING is incorrect provide one warning.
|
|
15
|
+
"""
|
|
16
|
+
self.assertEqual(
|
|
17
|
+
settings_checks.check_amavis_database_encoding(None), [settings_checks.W001]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@override_settings(AMAVIS_DEFAULT_DATABASE_ENCODING="UTF-8")
|
|
21
|
+
def test_amavis_database_encoding_correct(self):
|
|
22
|
+
"""
|
|
23
|
+
If AMAVIS_DEFAULT_DATABASE_ENCODING is correct, there's no warning.
|
|
24
|
+
"""
|
|
25
|
+
self.assertEqual(settings_checks.check_amavis_database_encoding(None), [])
|