modoboa 2.4.11__py3-none-any.whl → 2.5.0__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 +3 -1
- modoboa/core/api/v2/viewsets.py +24 -3
- 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-BuSy_1n9.js +1 -0
- modoboa/frontend_dist/assets/AccountEditView-qdJmLM_e.js +1 -0
- modoboa/frontend_dist/assets/AccountLayout-DrN7vHsX.js +1 -0
- modoboa/frontend_dist/assets/AccountPasswordSubForm-DZGt_Xgq.js +1 -0
- modoboa/frontend_dist/assets/AccountView-CO65y0vZ.js +1 -0
- modoboa/frontend_dist/assets/AddressBook-BZNUlhek.js +1 -0
- modoboa/frontend_dist/assets/AdminLayout-CTNhuwTw.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +1 -0
- modoboa/frontend_dist/assets/AlarmsView-DN_JIw9g.js +1 -0
- modoboa/frontend_dist/assets/AliasEditView-DjpPUTp9.js +1 -0
- modoboa/frontend_dist/assets/{AliasRecipientForm-IOae6sjF.js → AliasRecipientForm-B1Y8wFdP.js} +1 -1
- modoboa/frontend_dist/assets/AliasView-GOJ5lyQH.js +1 -0
- modoboa/frontend_dist/assets/AuditTrailView-fbXmq70e.js +1 -0
- modoboa/frontend_dist/assets/CalendarView-LlQQNEPL.js +1 -0
- modoboa/frontend_dist/assets/{ChoiceField-DJ_c78Cm.js → ChoiceField-B3ReQHVe.js} +1 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-Bs1fZXAL.js +1 -0
- modoboa/frontend_dist/assets/ComposeEmailView-s3LMl3pO.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-DY_kUHLG.js +1 -0
- modoboa/frontend_dist/assets/{ConnectedLayout-Dvwmicnc.css → ConnectedLayout-Bxh21hcH.css} +1 -1
- modoboa/frontend_dist/assets/ConnectedLayout-UWjiYBNw.js +1 -0
- modoboa/frontend_dist/assets/CreationForm-ORg3fazt.js +1 -0
- modoboa/frontend_dist/assets/DashboardView-Dplk9itS.js +1 -0
- modoboa/frontend_dist/assets/DomainAdminList-DVn9x0rB.js +1 -0
- modoboa/frontend_dist/assets/DomainEditView-nAoL64D_.js +1 -0
- modoboa/frontend_dist/assets/{DomainTransportForm-C2xo0Yd7.js → DomainTransportForm-CA-DNUxX.js} +1 -1
- modoboa/frontend_dist/assets/{DomainView-BDKoBFYr.css → DomainView-CCLYXPHx.css} +1 -1
- modoboa/frontend_dist/assets/DomainView-CdXPpwJG.js +5 -0
- modoboa/frontend_dist/assets/DomainsView-B_59gowf.js +1 -0
- modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +1 -0
- modoboa/frontend_dist/assets/EmailField-CwcwI5xW.js +1 -0
- modoboa/frontend_dist/assets/EmailView-BshxcfAK.js +1 -0
- modoboa/frontend_dist/assets/EmptyLayout-DFfhnhLi.js +1 -0
- modoboa/frontend_dist/assets/FiltersView-Cf20MSTK.js +1 -0
- modoboa/frontend_dist/assets/ForwardEmailView-CZG062os.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-CJ9umKeO.js → HtmlEditor-Bh4c689R.js} +1 -1
- modoboa/frontend_dist/assets/IdentitiesView-BXAuU1YX.js +1 -0
- modoboa/frontend_dist/assets/{IdentitiesView-0ziuQ5s-.css → IdentitiesView-DPrrRMS5.css} +1 -1
- modoboa/frontend_dist/assets/InformationView-C9vvvQhJ.css +1 -0
- modoboa/frontend_dist/assets/InformationView-Cn5FZW7H.js +1 -0
- modoboa/frontend_dist/assets/{LoadingData-CYwX3Jpn.js → LoadingData-CdVvm4FI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-E01qkKn0.js → LoginCallbackView-B9hAH4MI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-Cy4uFV9h.js → LoginView-tHIR4Adc.js} +1 -1
- modoboa/frontend_dist/assets/MailboxView-Bugu2vhg.js +1 -0
- modoboa/frontend_dist/assets/MenuItems-PXjiG-fs.js +1 -0
- modoboa/frontend_dist/assets/MessageView-Cy4STShm.js +1 -0
- modoboa/frontend_dist/assets/MessagesView-DdkuEgfX.js +1 -0
- modoboa/frontend_dist/assets/MigrationsView-CidSEjCF.js +1 -0
- modoboa/frontend_dist/assets/{ParametersForm-BZM0QSvg.js → ParametersForm-CAv4SH-E.js} +1 -1
- modoboa/frontend_dist/assets/ParametersView-CX7Ffemw.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-CrbNcmV3.js +1 -0
- modoboa/frontend_dist/assets/ProviderEditView-CrltAQXl.js +1 -0
- modoboa/frontend_dist/assets/ProviderGeneralForm-BYAzVnXM.js +1 -0
- modoboa/frontend_dist/assets/ProvidersView-osjIY4Ex.js +1 -0
- modoboa/frontend_dist/assets/QuarantineLayout-B8EcU9vS.js +1 -0
- modoboa/frontend_dist/assets/QuarantineView-D4gOE4EQ.css +1 -0
- modoboa/frontend_dist/assets/QuarantineView-D8Qg0MXA.js +1 -0
- modoboa/frontend_dist/assets/ReplyEmailView-BABPqWhd.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-OaqdRYVs.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-d277YTGR.js +1 -0
- modoboa/frontend_dist/assets/{SettingsView-BxLJBFY0.js → SettingsView-9iNcDhkI.js} +2 -2
- modoboa/frontend_dist/assets/StatisticsView-cHsPyGkL.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart--V83dcJ9.js +1 -0
- modoboa/frontend_dist/assets/UserLayout-B3sBiTcZ.js +1 -0
- modoboa/frontend_dist/assets/VAlert-BuaaYN2h.js +1 -0
- modoboa/frontend_dist/assets/VApp-CKP-6zGP.js +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-Dwv6_Rzq.js +1 -0
- modoboa/frontend_dist/assets/VAvatar-Cmga0vj6.js +1 -0
- modoboa/frontend_dist/assets/VBadge-BQrRJ9S0.css +1 -0
- modoboa/frontend_dist/assets/VBadge-CixeK87a.js +1 -0
- modoboa/frontend_dist/assets/VCard-CxH9DWoK.js +1 -0
- modoboa/frontend_dist/assets/VCheckbox-62GOpvvP.js +1 -0
- modoboa/frontend_dist/assets/{VCheckboxBtn-Dt810gWf.js → VCheckboxBtn-DMoNtKT8.js} +1 -1
- modoboa/frontend_dist/assets/VChip-D_styETR.js +1 -0
- modoboa/frontend_dist/assets/VColorPicker-BHscBGQV.js +1 -0
- modoboa/frontend_dist/assets/{VContainer-DvTbsotR.js → VContainer-B46caNs1.js} +1 -1
- modoboa/frontend_dist/assets/VDataTable-Bh8NbVSx.js +1 -0
- modoboa/frontend_dist/assets/VDataTableServer-BDR5hOmo.js +1 -0
- modoboa/frontend_dist/assets/VDataTableVirtual-BOQlNtIG.js +1 -0
- modoboa/frontend_dist/assets/{VDialog-Bk6EWNhz.js → VDialog-BcTg7w6P.js} +1 -1
- modoboa/frontend_dist/assets/VExpansionPanels-BmH5Jl2Z.js +1 -0
- modoboa/frontend_dist/assets/VFileInput-BC4yAygd.js +1 -0
- modoboa/frontend_dist/assets/VForm-D5iPGkde.js +1 -0
- modoboa/frontend_dist/assets/VInput-CcxkaOXT.css +1 -0
- modoboa/frontend_dist/assets/VInput-CoDJzvaW.js +1 -0
- modoboa/frontend_dist/assets/VMenu-gUG70-zD.js +1 -0
- modoboa/frontend_dist/assets/VPicker-BXuKT3zB.js +1 -0
- modoboa/frontend_dist/assets/VProgressCircular-BtOPiGCg.js +1 -0
- modoboa/frontend_dist/assets/VRadioGroup-DIFZKSn-.js +1 -0
- modoboa/frontend_dist/assets/{VRow-BF35mT1S.js → VRow-ozg66L7j.js} +1 -1
- modoboa/frontend_dist/assets/VSelect-C3RjAa45.js +1 -0
- modoboa/frontend_dist/assets/VSelectionControl-zyz-fJvC.js +1 -0
- modoboa/frontend_dist/assets/VSheet-BNx2X4Mk.js +1 -0
- modoboa/frontend_dist/assets/VSpacer-DinPiXs9.js +1 -0
- modoboa/frontend_dist/assets/VSwitch-DwxdeAEq.js +1 -0
- modoboa/frontend_dist/assets/VTable-DaLxa4FO.js +1 -0
- modoboa/frontend_dist/assets/VTabs-BP0Hgsgm.js +1 -0
- modoboa/frontend_dist/assets/VTextField-BzBVKKob.css +1 -0
- modoboa/frontend_dist/assets/VTextField-XoGTj1KG.js +1 -0
- modoboa/frontend_dist/assets/VTextarea-wBlRMIv_.js +1 -0
- modoboa/frontend_dist/assets/VToolbar-CFZfqeOr.js +1 -0
- modoboa/frontend_dist/assets/VWindowItem-BB7ETW3b.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-_Hk1XhVq.js +1 -0
- modoboa/frontend_dist/assets/accounts-DUzbx6k8.js +1 -0
- modoboa/frontend_dist/assets/{admin-o-HRGnmT.js → admin-DewTk2H8.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-DDVeehyg.js → aliases-4sXmjwXp.js} +1 -1
- modoboa/frontend_dist/assets/amavis-CC0li7_T.js +1 -0
- modoboa/frontend_dist/assets/amavis-DK8SHE6o.js +1 -0
- modoboa/frontend_dist/assets/{contacts-C84DY-Q1.js → contacts-BjghrPqZ.js} +1 -1
- modoboa/frontend_dist/assets/{domains-Bgn4ixHL.js → domains-BSawReeu.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-DTE-V7Y1.js → domains.store-D-vWCEIK.js} +1 -1
- modoboa/frontend_dist/assets/{filter-CnffiQAW.js → filter-C82FUCw_.js} +1 -1
- modoboa/frontend_dist/assets/forwardRefs-cvcnlhoK.js +1 -0
- modoboa/frontend_dist/assets/global.store-DbkcI5o2.js +1 -0
- modoboa/frontend_dist/assets/{importExport-BlQYb0NO.js → importExport-DzoL4Mvc.js} +1 -1
- modoboa/frontend_dist/assets/index-BImkz5Jx.js +984 -0
- modoboa/frontend_dist/assets/layout-C5FyYCHK.js +1 -0
- modoboa/frontend_dist/assets/{layout.store-DkjrAoXt.js → layout.store-NXWtFIwL.js} +1 -1
- modoboa/frontend_dist/assets/{logos-q8SEyAa4.js → logos-BswdveCV.js} +1 -1
- modoboa/frontend_dist/assets/{logs-B7IJ7LBa.js → logs-6CbtfaZS.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-A6iBEYQq.js → parameters-aSQiR7kN.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-BiXS4_6w.js → parameters.store-CzQqVatx.js} +1 -1
- modoboa/frontend_dist/assets/permissions-DNoefz-n.js +1 -0
- modoboa/frontend_dist/assets/{ssrBoot-AzTdjPjk.js → ssrBoot-CKUX4kcb.js} +1 -1
- modoboa/frontend_dist/assets/{tag-BnSYRTcD.js → tag-B_yWNNJD.js} +1 -1
- modoboa/frontend_dist/assets/transports-BDNB9wR5.js +1 -0
- modoboa/frontend_dist/assets/{webmail-CSH_3l6R.js → webmail-CdU6CD9b.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-2.4.11.dist-info → modoboa-2.5.0.dist-info}/METADATA +6 -4
- {modoboa-2.4.11.dist-info → modoboa-2.5.0.dist-info}/RECORD +231 -181
- 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.0.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.0.dist-info}/WHEEL +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.0.dist-info}/entry_points.txt +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.0.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Amavis handlers."""
|
|
2
|
+
|
|
3
|
+
from django.db.models import signals
|
|
4
|
+
from django.dispatch import receiver
|
|
5
|
+
from django.utils.translation import gettext as _
|
|
6
|
+
|
|
7
|
+
from modoboa.admin import models as admin_models
|
|
8
|
+
from modoboa.core import signals as core_signals
|
|
9
|
+
from modoboa.lib import signals as lib_signals
|
|
10
|
+
from modoboa.parameters import tools as param_tools
|
|
11
|
+
from .lib import (
|
|
12
|
+
create_user_and_policy,
|
|
13
|
+
create_user_and_use_policy,
|
|
14
|
+
delete_user,
|
|
15
|
+
delete_user_and_policy,
|
|
16
|
+
update_user_and_policy,
|
|
17
|
+
)
|
|
18
|
+
from .models import Policy, Users
|
|
19
|
+
from .sql_connector import SQLconnector
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@receiver(signals.post_save, sender=admin_models.Domain)
|
|
23
|
+
def manage_domain_policy(sender, instance, **kwargs):
|
|
24
|
+
"""Create user and policy when a domain is added."""
|
|
25
|
+
if kwargs.get("created"):
|
|
26
|
+
create_user_and_policy(f"@{instance.name}")
|
|
27
|
+
else:
|
|
28
|
+
update_user_and_policy(f"@{instance.oldname}", f"@{instance.name}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@receiver(signals.pre_delete, sender=admin_models.Domain)
|
|
32
|
+
def on_domain_deleted(sender, instance, **kwargs):
|
|
33
|
+
"""Delete user and policy for domain."""
|
|
34
|
+
delete_user_and_policy(f"@{instance.name}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@receiver(signals.post_save, sender=admin_models.DomainAlias)
|
|
38
|
+
def on_domain_alias_created(sender, instance, **kwargs):
|
|
39
|
+
"""Create user and use domain policy for domain alias."""
|
|
40
|
+
if not kwargs.get("created"):
|
|
41
|
+
return
|
|
42
|
+
create_user_and_use_policy(f"@{instance.name}", f"@{instance.target.name}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@receiver(signals.pre_delete, sender=admin_models.DomainAlias)
|
|
46
|
+
def on_domain_alias_deleted(sender, instance, **kwargs):
|
|
47
|
+
"""Delete user for domain alias."""
|
|
48
|
+
delete_user(f"@{instance.name}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@receiver(signals.post_save, sender=admin_models.Mailbox)
|
|
52
|
+
def on_mailbox_modified(sender, instance, **kwargs):
|
|
53
|
+
"""Update amavis records if address has changed."""
|
|
54
|
+
condition = (
|
|
55
|
+
not param_tools.get_global_parameter("manual_learning")
|
|
56
|
+
or not hasattr(instance, "old_full_address")
|
|
57
|
+
or instance.full_address == instance.old_full_address
|
|
58
|
+
)
|
|
59
|
+
if condition:
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
user = Users.objects.select_related("policy").get(
|
|
63
|
+
email=instance.old_full_address
|
|
64
|
+
)
|
|
65
|
+
except Users.DoesNotExist:
|
|
66
|
+
return
|
|
67
|
+
full_address = instance.full_address
|
|
68
|
+
user.email = full_address
|
|
69
|
+
user.policy.policy_name = full_address[:32]
|
|
70
|
+
user.policy.sa_username = full_address
|
|
71
|
+
user.policy.save()
|
|
72
|
+
user.save()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@receiver(signals.pre_delete, sender=admin_models.Mailbox)
|
|
76
|
+
def on_mailbox_deleted(sender, instance, **kwargs):
|
|
77
|
+
"""Clean amavis database when a mailbox is removed."""
|
|
78
|
+
if not param_tools.get_global_parameter("manual_learning"):
|
|
79
|
+
return
|
|
80
|
+
delete_user_and_policy(f"@{instance.full_address}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@receiver(signals.post_save, sender=admin_models.AliasRecipient)
|
|
84
|
+
def on_aliasrecipient_created(sender, instance, **kwargs):
|
|
85
|
+
"""Create amavis record for the new alias recipient.
|
|
86
|
+
|
|
87
|
+
FIXME: how to deal with distibution lists ?
|
|
88
|
+
"""
|
|
89
|
+
conf = dict(param_tools.get_global_parameters("amavis"))
|
|
90
|
+
condition = (
|
|
91
|
+
not conf["manual_learning"]
|
|
92
|
+
or not conf["user_level_learning"]
|
|
93
|
+
or not instance.r_mailbox
|
|
94
|
+
or instance.alias.type != "alias"
|
|
95
|
+
)
|
|
96
|
+
if condition:
|
|
97
|
+
return
|
|
98
|
+
policy = Policy.objects.filter(policy_name=instance.r_mailbox.full_address).first()
|
|
99
|
+
if policy:
|
|
100
|
+
# Use mailbox policy for this new alias. We update or create
|
|
101
|
+
# to handle the case where an account is being replaced by an
|
|
102
|
+
# alias (when it is disabled).
|
|
103
|
+
email = instance.alias.address
|
|
104
|
+
Users.objects.update_or_create(
|
|
105
|
+
email=email, defaults={"policy": policy, "fullname": email, "priority": 7}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@receiver(signals.pre_delete, sender=admin_models.Alias)
|
|
110
|
+
def on_mailboxalias_deleted(sender, instance, **kwargs):
|
|
111
|
+
"""Clean amavis database when an alias is removed."""
|
|
112
|
+
if not param_tools.get_global_parameter("manual_learning"):
|
|
113
|
+
return
|
|
114
|
+
if instance.address.startswith("@"):
|
|
115
|
+
# Catchall alias, do not remove domain entry accidentally...
|
|
116
|
+
return
|
|
117
|
+
aliases = [instance.address]
|
|
118
|
+
Users.objects.filter(email__in=aliases).delete()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@receiver(core_signals.get_top_notifications)
|
|
122
|
+
def check_for_pending_requests(sender, include_all, **kwargs):
|
|
123
|
+
"""Check if release requests are pending."""
|
|
124
|
+
request = lib_signals.get_request()
|
|
125
|
+
condition = (
|
|
126
|
+
param_tools.get_global_parameter("user_can_release")
|
|
127
|
+
or request.user.role == "SimpleUsers"
|
|
128
|
+
)
|
|
129
|
+
if condition:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
nbrequests = SQLconnector(user=request.user).get_pending_requests()
|
|
133
|
+
if not nbrequests:
|
|
134
|
+
return [{"id": "nbrequests", "counter": 0}] if include_all else []
|
|
135
|
+
|
|
136
|
+
url = "/user/quarantine?requests=1"
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
"id": "nbrequests",
|
|
140
|
+
"url": url,
|
|
141
|
+
"text": _("Pending requests"),
|
|
142
|
+
"counter": nbrequests,
|
|
143
|
+
"color": "error",
|
|
144
|
+
"target": "all",
|
|
145
|
+
}
|
|
146
|
+
]
|
modoboa/amavis/lib.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import socket
|
|
4
|
+
import struct
|
|
5
|
+
from email.utils import parseaddr
|
|
6
|
+
|
|
7
|
+
import idna
|
|
8
|
+
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.utils.translation import gettext as _
|
|
11
|
+
|
|
12
|
+
from rest_framework import authentication, exceptions
|
|
13
|
+
|
|
14
|
+
from modoboa.admin import models as admin_models
|
|
15
|
+
from modoboa.lib.email_utils import split_address, split_local_part, split_mailbox
|
|
16
|
+
from modoboa.lib.exceptions import InternalError
|
|
17
|
+
from modoboa.lib.sysutils import exec_cmd
|
|
18
|
+
from modoboa.parameters import tools as param_tools
|
|
19
|
+
from .models import Msgrcpt, Policy, Users
|
|
20
|
+
from .utils import smart_bytes, smart_str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SelfServiceAuthentication(authentication.BaseAuthentication):
|
|
24
|
+
|
|
25
|
+
def authenticate(self, request):
|
|
26
|
+
from .sql_connector import SQLconnector
|
|
27
|
+
|
|
28
|
+
mail_id = request._request.resolver_match.kwargs.get("pk")
|
|
29
|
+
if request.method == "GET":
|
|
30
|
+
rcpt = request.GET.get("rcpt")
|
|
31
|
+
secret_id = request.GET.get("secret_id")
|
|
32
|
+
else:
|
|
33
|
+
rcpt = request.data.get("rcpt")
|
|
34
|
+
secret_id = request.data.get("secret_id")
|
|
35
|
+
if not mail_id or not rcpt or not secret_id:
|
|
36
|
+
return None
|
|
37
|
+
connector = SQLconnector()
|
|
38
|
+
try:
|
|
39
|
+
msgrcpt = connector.get_recipient_message(rcpt, mail_id)
|
|
40
|
+
except Msgrcpt.DoesNotExist:
|
|
41
|
+
raise exceptions.AuthenticationFailed("Invalid credentials") from None
|
|
42
|
+
if secret_id != smart_str(msgrcpt.mail.secret_id):
|
|
43
|
+
raise exceptions.AuthenticationFailed("Invalid credentials")
|
|
44
|
+
return (None, "selfservice")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AMrelease:
|
|
48
|
+
def __init__(self):
|
|
49
|
+
conf = dict(param_tools.get_global_parameters("amavis"))
|
|
50
|
+
try:
|
|
51
|
+
if conf["am_pdp_mode"] == "inet":
|
|
52
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
53
|
+
self.sock.connect((conf["am_pdp_host"], conf["am_pdp_port"]))
|
|
54
|
+
else:
|
|
55
|
+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
56
|
+
self.sock.connect(conf["am_pdp_socket"])
|
|
57
|
+
except OSError as err:
|
|
58
|
+
raise InternalError(
|
|
59
|
+
_("Connection to amavis failed: %s") % str(err)
|
|
60
|
+
) from None
|
|
61
|
+
|
|
62
|
+
def decode(self, answer):
|
|
63
|
+
def repl(match):
|
|
64
|
+
return struct.pack("B", int(match.group(0)[1:], 16))
|
|
65
|
+
|
|
66
|
+
return re.sub(rb"%([0-9a-fA-F]{2})", repl, answer)
|
|
67
|
+
|
|
68
|
+
def __del__(self):
|
|
69
|
+
self.sock.close()
|
|
70
|
+
|
|
71
|
+
def sendreq(self, mailid, secretid, recipient, *others):
|
|
72
|
+
self.sock.send(
|
|
73
|
+
smart_bytes(
|
|
74
|
+
f"""request=release
|
|
75
|
+
mail_id={smart_str(mailid)}
|
|
76
|
+
secret_id={smart_str(secretid)}
|
|
77
|
+
quar_type=Q
|
|
78
|
+
recipient={smart_str(recipient)}
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
answer = self.sock.recv(1024)
|
|
84
|
+
answer = self.decode(answer)
|
|
85
|
+
if re.search(rb"250 [\d\.]+ Ok", answer):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SpamassassinClient:
|
|
91
|
+
"""A stupid spamassassin client."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, user, recipient_db):
|
|
94
|
+
"""Constructor."""
|
|
95
|
+
conf = dict(param_tools.get_global_parameters("amavis"))
|
|
96
|
+
self._sa_is_local = conf["sa_is_local"]
|
|
97
|
+
self._default_username = conf["default_user"]
|
|
98
|
+
self._recipient_db = recipient_db
|
|
99
|
+
self._setup_cache = {}
|
|
100
|
+
self._username_cache = []
|
|
101
|
+
if user.role == "SimpleUsers":
|
|
102
|
+
if conf["user_level_learning"]:
|
|
103
|
+
self._username = user.email
|
|
104
|
+
else:
|
|
105
|
+
self._username = None
|
|
106
|
+
self.error = None
|
|
107
|
+
if self._sa_is_local:
|
|
108
|
+
self._learn_cmd = self._find_binary("sa-learn")
|
|
109
|
+
self._learn_cmd += " --{0} --no-sync -u {1}"
|
|
110
|
+
self._learn_cmd_kwargs = {}
|
|
111
|
+
self._expected_exit_codes = [0]
|
|
112
|
+
self._sync_cmd = self._find_binary("sa-learn")
|
|
113
|
+
self._sync_cmd += " -u {0} --sync"
|
|
114
|
+
else:
|
|
115
|
+
self._learn_cmd = self._find_binary("spamc")
|
|
116
|
+
self._learn_cmd += " -d {} -p {}".format(
|
|
117
|
+
conf["spamd_address"], conf["spamd_port"]
|
|
118
|
+
)
|
|
119
|
+
self._learn_cmd += " -L {0} -u {1}"
|
|
120
|
+
self._learn_cmd_kwargs = {}
|
|
121
|
+
self._expected_exit_codes = [5, 6]
|
|
122
|
+
|
|
123
|
+
def _find_binary(self, name):
|
|
124
|
+
"""Find path to binary."""
|
|
125
|
+
code, output = exec_cmd(f"which {name}")
|
|
126
|
+
if not code:
|
|
127
|
+
return smart_str(output).strip()
|
|
128
|
+
known_paths = getattr(settings, "SA_LOOKUP_PATH", ("/usr/bin",))
|
|
129
|
+
for path in known_paths:
|
|
130
|
+
bpath = os.path.join(path, name)
|
|
131
|
+
if os.path.isfile(bpath) and os.access(bpath, os.X_OK):
|
|
132
|
+
return bpath
|
|
133
|
+
raise InternalError(_("Failed to find {} binary").format(name))
|
|
134
|
+
|
|
135
|
+
def _get_mailbox_from_rcpt(self, rcpt):
|
|
136
|
+
"""Retrieve a mailbox from a recipient address."""
|
|
137
|
+
local_part, domname, extension = split_mailbox(rcpt, return_extension=True)
|
|
138
|
+
try:
|
|
139
|
+
mailbox = admin_models.Mailbox.objects.select_related("domain").get(
|
|
140
|
+
address=local_part, domain__name=domname
|
|
141
|
+
)
|
|
142
|
+
except admin_models.Mailbox.DoesNotExist:
|
|
143
|
+
alias = admin_models.Alias.objects.filter(
|
|
144
|
+
address=f"{local_part}@{domname}",
|
|
145
|
+
aliasrecipient__r_mailbox__isnull=False,
|
|
146
|
+
).first()
|
|
147
|
+
if not alias:
|
|
148
|
+
raise InternalError(_("No recipient found")) from None
|
|
149
|
+
if alias.type != "alias":
|
|
150
|
+
return None
|
|
151
|
+
mailbox = alias.aliasrecipient_set.filter(r_mailbox__isnull=False).first()
|
|
152
|
+
return mailbox
|
|
153
|
+
|
|
154
|
+
def _get_domain_from_rcpt(self, rcpt):
|
|
155
|
+
"""Retrieve a domain from a recipient address."""
|
|
156
|
+
local_part, domname = split_mailbox(rcpt)
|
|
157
|
+
domain = admin_models.Domain.objects.filter(name=domname).first()
|
|
158
|
+
if not domain:
|
|
159
|
+
raise InternalError(_("Local domain not found"))
|
|
160
|
+
return domain
|
|
161
|
+
|
|
162
|
+
def _learn(self, rcpt, msg, mtype):
|
|
163
|
+
"""Internal method to call the learning command."""
|
|
164
|
+
if self._username is None:
|
|
165
|
+
if self._recipient_db == "global":
|
|
166
|
+
username = self._default_username
|
|
167
|
+
elif self._recipient_db == "domain":
|
|
168
|
+
domain = self._get_domain_from_rcpt(rcpt)
|
|
169
|
+
username = domain.name
|
|
170
|
+
condition = (
|
|
171
|
+
username not in self._setup_cache
|
|
172
|
+
and setup_manual_learning_for_domain(domain)
|
|
173
|
+
)
|
|
174
|
+
if condition:
|
|
175
|
+
self._setup_cache[username] = True
|
|
176
|
+
else:
|
|
177
|
+
mbox = self._get_mailbox_from_rcpt(rcpt)
|
|
178
|
+
if mbox is None:
|
|
179
|
+
username = self._default_username
|
|
180
|
+
else:
|
|
181
|
+
if isinstance(mbox, admin_models.Mailbox):
|
|
182
|
+
username = mbox.full_address
|
|
183
|
+
elif isinstance(mbox, admin_models.AliasRecipient):
|
|
184
|
+
username = mbox.address
|
|
185
|
+
else:
|
|
186
|
+
username = None
|
|
187
|
+
condition = (
|
|
188
|
+
username is not None
|
|
189
|
+
and username not in self._setup_cache
|
|
190
|
+
and setup_manual_learning_for_mbox(mbox)
|
|
191
|
+
)
|
|
192
|
+
if condition:
|
|
193
|
+
self._setup_cache[username] = True
|
|
194
|
+
else:
|
|
195
|
+
username = self._username
|
|
196
|
+
if username not in self._setup_cache:
|
|
197
|
+
mbox = self._get_mailbox_from_rcpt(username)
|
|
198
|
+
if mbox and setup_manual_learning_for_mbox(mbox):
|
|
199
|
+
self._setup_cache[username] = True
|
|
200
|
+
if username not in self._username_cache:
|
|
201
|
+
self._username_cache.append(username)
|
|
202
|
+
cmd = self._learn_cmd.format(mtype, username)
|
|
203
|
+
code, output = exec_cmd(cmd, pinput=smart_bytes(msg), **self._learn_cmd_kwargs)
|
|
204
|
+
if code in self._expected_exit_codes:
|
|
205
|
+
return True
|
|
206
|
+
self.error = smart_str(output)
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
def learn_spam(self, rcpt, msg):
|
|
210
|
+
"""Learn new spam."""
|
|
211
|
+
return self._learn(rcpt, msg, "spam")
|
|
212
|
+
|
|
213
|
+
def learn_ham(self, rcpt, msg):
|
|
214
|
+
"""Learn new ham."""
|
|
215
|
+
return self._learn(rcpt, msg, "ham")
|
|
216
|
+
|
|
217
|
+
def done(self):
|
|
218
|
+
"""Call this method at the end of the processing."""
|
|
219
|
+
if self._sa_is_local:
|
|
220
|
+
for username in self._username_cache:
|
|
221
|
+
cmd = self._sync_cmd.format(username)
|
|
222
|
+
exec_cmd(cmd, **self._learn_cmd_kwargs)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def create_user_and_policy(name, priority=7):
|
|
226
|
+
"""Create records.
|
|
227
|
+
|
|
228
|
+
Create two records (a user and a policy) using :keyword:`name` as
|
|
229
|
+
an identifier.
|
|
230
|
+
|
|
231
|
+
:param str name: name
|
|
232
|
+
:return: the new ``Policy`` object
|
|
233
|
+
"""
|
|
234
|
+
policy, _ = Policy.objects.get_or_create(policy_name=name[:32])
|
|
235
|
+
if not Users.objects.filter(email=name).exists():
|
|
236
|
+
Users.objects.create(
|
|
237
|
+
email=name, fullname=name, priority=priority, policy=policy
|
|
238
|
+
)
|
|
239
|
+
return policy
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def create_user_and_use_policy(name, policy, priority=7):
|
|
243
|
+
"""Create a *users* record and use an existing policy.
|
|
244
|
+
|
|
245
|
+
:param str name: user record name
|
|
246
|
+
:param str policy: string or Policy instance
|
|
247
|
+
"""
|
|
248
|
+
if isinstance(policy, str):
|
|
249
|
+
policy = Policy.objects.get(policy_name=policy[:32])
|
|
250
|
+
Users.objects.get_or_create(
|
|
251
|
+
email=name, fullname=name, priority=priority, policy=policy
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def update_user_and_policy(oldname, newname):
|
|
256
|
+
"""Update records.
|
|
257
|
+
|
|
258
|
+
:param str oldname: old name
|
|
259
|
+
:param str newname: new name
|
|
260
|
+
"""
|
|
261
|
+
if oldname == newname:
|
|
262
|
+
return
|
|
263
|
+
u = Users.objects.get(email=oldname)
|
|
264
|
+
u.email = newname
|
|
265
|
+
u.fullname = newname
|
|
266
|
+
u.policy.policy_name = newname[:32]
|
|
267
|
+
u.policy.save(update_fields=["policy_name"])
|
|
268
|
+
u.save()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def delete_user_and_policy(name):
|
|
272
|
+
"""Delete records.
|
|
273
|
+
|
|
274
|
+
:param str name: identifier
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
u = Users.objects.get(email=name)
|
|
278
|
+
except Users.DoesNotExist:
|
|
279
|
+
return
|
|
280
|
+
u.policy.delete()
|
|
281
|
+
u.delete()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def delete_user(name):
|
|
285
|
+
"""Delete a *users* record.
|
|
286
|
+
|
|
287
|
+
:param str name: user record name
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
Users.objects.get(email=name).delete()
|
|
291
|
+
except Users.DoesNotExist:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def manual_learning_enabled(user):
|
|
296
|
+
"""Check if manual learning is enabled or not.
|
|
297
|
+
|
|
298
|
+
Also check for :kw:`user` if necessary.
|
|
299
|
+
|
|
300
|
+
:return: True if learning is enabled, False otherwise.
|
|
301
|
+
"""
|
|
302
|
+
conf = dict(param_tools.get_global_parameters("amavis"))
|
|
303
|
+
if not conf["manual_learning"]:
|
|
304
|
+
return False
|
|
305
|
+
if user.role != "SuperAdmins":
|
|
306
|
+
if user.has_perm("admin.view_domain"):
|
|
307
|
+
manual_learning = (
|
|
308
|
+
conf["domain_level_learning"] or conf["user_level_learning"]
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
manual_learning = conf["user_level_learning"]
|
|
312
|
+
return manual_learning
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def setup_manual_learning_for_domain(domain):
|
|
317
|
+
"""Setup manual learning if necessary.
|
|
318
|
+
|
|
319
|
+
:return: True if learning has been setup, False otherwise
|
|
320
|
+
"""
|
|
321
|
+
if Policy.objects.filter(sa_username=domain.name).exists():
|
|
322
|
+
return False
|
|
323
|
+
policy = Policy.objects.get(policy_name=f"@{domain.name[:32]}")
|
|
324
|
+
policy.sa_username = domain.name
|
|
325
|
+
policy.save()
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def setup_manual_learning_for_mbox(mbox):
|
|
330
|
+
"""Setup manual learning if necessary.
|
|
331
|
+
|
|
332
|
+
:return: True if learning has been setup, False otherwise
|
|
333
|
+
"""
|
|
334
|
+
result = False
|
|
335
|
+
if isinstance(mbox, admin_models.AliasRecipient) and mbox.r_mailbox is not None:
|
|
336
|
+
mbox = mbox.r_mailbox
|
|
337
|
+
if isinstance(mbox, admin_models.Mailbox):
|
|
338
|
+
pname = mbox.full_address[:32]
|
|
339
|
+
if not Policy.objects.filter(policy_name=pname).exists():
|
|
340
|
+
policy = create_user_and_policy(pname)
|
|
341
|
+
policy.sa_username = mbox.full_address
|
|
342
|
+
policy.save()
|
|
343
|
+
for alias in mbox.alias_addresses:
|
|
344
|
+
create_user_and_use_policy(alias, policy)
|
|
345
|
+
result = True
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def make_query_args(address, exact_extension=True, wildcard=None, domain_search=False):
|
|
350
|
+
assert isinstance(address, str), "address should be of type str"
|
|
351
|
+
conf = dict(param_tools.get_global_parameters("amavis"))
|
|
352
|
+
local_part, domain = split_address(address)
|
|
353
|
+
if not conf["localpart_is_case_sensitive"]:
|
|
354
|
+
local_part = local_part.lower()
|
|
355
|
+
if domain:
|
|
356
|
+
domain = domain.lstrip("@").rstrip(".")
|
|
357
|
+
domain = domain.lower()
|
|
358
|
+
orig_domain = domain
|
|
359
|
+
domain = idna.encode(domain, uts46=True).decode("ascii")
|
|
360
|
+
delimiter = conf["recipient_delimiter"]
|
|
361
|
+
local_part, extension = split_local_part(local_part, delimiter=delimiter)
|
|
362
|
+
query_args = []
|
|
363
|
+
if conf["localpart_is_case_sensitive"] or (domain and domain != orig_domain):
|
|
364
|
+
query_args.append(address)
|
|
365
|
+
if extension:
|
|
366
|
+
query_args.append(f"{local_part}{delimiter}{extension}@{domain}")
|
|
367
|
+
if delimiter and not exact_extension and wildcard:
|
|
368
|
+
query_args.append(f"{local_part}{delimiter}{wildcard}@{domain}")
|
|
369
|
+
query_args.append(f"{local_part}@{domain}")
|
|
370
|
+
if domain_search:
|
|
371
|
+
query_args.append(f"@{domain}")
|
|
372
|
+
query_args.append("@.")
|
|
373
|
+
|
|
374
|
+
return query_args
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def cleanup_email_address(address):
|
|
378
|
+
address = parseaddr(address)
|
|
379
|
+
if address[0]:
|
|
380
|
+
return f"{address[0]} <{address[1]}>"
|
|
381
|
+
return address[1]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from django.contrib.sites import models as sites_models
|
|
2
|
+
from django.core import mail
|
|
3
|
+
from django.core.management.base import BaseCommand
|
|
4
|
+
from django.template.loader import render_to_string
|
|
5
|
+
from django.utils.translation import gettext as _
|
|
6
|
+
|
|
7
|
+
from modoboa.admin.models import Domain
|
|
8
|
+
from modoboa.core.models import User
|
|
9
|
+
from modoboa.parameters import tools as param_tools
|
|
10
|
+
from ...models import Msgrcpt
|
|
11
|
+
from ...sql_connector import SQLconnector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Command(BaseCommand):
|
|
15
|
+
help = "Amavis notification tool" # NOQA:A003
|
|
16
|
+
|
|
17
|
+
sender = None
|
|
18
|
+
baseurl = None
|
|
19
|
+
listingurl = None
|
|
20
|
+
|
|
21
|
+
def add_arguments(self, parser):
|
|
22
|
+
"""Add extra arguments to command line."""
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--smtp_host",
|
|
25
|
+
type=str,
|
|
26
|
+
default="localhost",
|
|
27
|
+
help="The address of the SMTP server used to send notifications",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--smtp_port",
|
|
31
|
+
type=int,
|
|
32
|
+
default=25,
|
|
33
|
+
help=(
|
|
34
|
+
"The listening port of the SMTP server used to send " "notifications"
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--verbose", action="store_true", help="Activate verbose mode"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def handle(self, *args, **options):
|
|
42
|
+
self.options = options
|
|
43
|
+
self.notify_admins_pending_requests()
|
|
44
|
+
|
|
45
|
+
def _build_message(self, rcpt, total, reqs):
|
|
46
|
+
"""Build new EmailMessage instance."""
|
|
47
|
+
if self.options["verbose"]:
|
|
48
|
+
print(f"Sending notification to {rcpt}")
|
|
49
|
+
context = {
|
|
50
|
+
"total": total,
|
|
51
|
+
"requests": reqs,
|
|
52
|
+
"baseurl": self.baseurl,
|
|
53
|
+
"listingurl": self.listingurl,
|
|
54
|
+
}
|
|
55
|
+
content = render_to_string(
|
|
56
|
+
"amavis/notifications/pending_requests.html", context
|
|
57
|
+
)
|
|
58
|
+
msg = mail.EmailMessage(
|
|
59
|
+
_("[modoboa] Pending release requests"), content, self.sender, [rcpt]
|
|
60
|
+
)
|
|
61
|
+
return msg
|
|
62
|
+
|
|
63
|
+
def notify_admins_pending_requests(self):
|
|
64
|
+
self.sender = param_tools.get_global_parameter(
|
|
65
|
+
"notifications_sender", app="amavis"
|
|
66
|
+
)
|
|
67
|
+
self.baseurl = f"https://{sites_models.Site.objects.get_current().domain}"
|
|
68
|
+
self.listingurl = f"{self.baseurl}/user/quarantine?requests=1"
|
|
69
|
+
messages = []
|
|
70
|
+
# Check domain administators first.
|
|
71
|
+
for da in User.objects.filter(groups__name="DomainAdmins"):
|
|
72
|
+
if not hasattr(da, "mailbox"):
|
|
73
|
+
continue
|
|
74
|
+
rcpt = da.mailbox.full_address
|
|
75
|
+
reqs = SQLconnector().get_domains_pending_requests(
|
|
76
|
+
Domain.objects.get_for_admin(da).values_list("name", flat=True)
|
|
77
|
+
)
|
|
78
|
+
total = reqs.count()
|
|
79
|
+
reqs = reqs.all()[:10]
|
|
80
|
+
if reqs.count():
|
|
81
|
+
messages.append(self._build_message(rcpt, total, reqs))
|
|
82
|
+
|
|
83
|
+
# Then super administators.
|
|
84
|
+
reqs = Msgrcpt.objects.filter(rs="p")
|
|
85
|
+
total = reqs.count()
|
|
86
|
+
if total:
|
|
87
|
+
reqs = reqs.all()[:10]
|
|
88
|
+
for su in User.objects.filter(is_superuser=True):
|
|
89
|
+
if not hasattr(su, "mailbox"):
|
|
90
|
+
continue
|
|
91
|
+
rcpt = su.mailbox.full_address
|
|
92
|
+
messages.append(self._build_message(rcpt, total, reqs))
|
|
93
|
+
|
|
94
|
+
# Finally, send emails.
|
|
95
|
+
if not len(messages):
|
|
96
|
+
return
|
|
97
|
+
kwargs = {"host": self.options["smtp_host"], "port": self.options["smtp_port"]}
|
|
98
|
+
with mail.get_connection(**kwargs) as connection:
|
|
99
|
+
connection.send_messages(messages)
|