modoboa 2.4.10__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/v1/serializers.py +1 -0
- modoboa/admin/api/v2/serializers.py +15 -1
- modoboa/admin/api/v2/tests.py +11 -1
- modoboa/admin/lib.py +1 -1
- modoboa/admin/models/mailbox.py +15 -13
- modoboa/admin/models/mxrecord.py +4 -0
- modoboa/admin/tests/test_import_.py +11 -11
- 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 +13 -2
- modoboa/core/api/v2/tests.py +34 -4
- modoboa/core/api/v2/urls.py +10 -5
- modoboa/core/api/v2/views.py +23 -2
- modoboa/core/api/v2/viewsets.py +24 -3
- modoboa/core/app_settings.py +11 -0
- modoboa/core/fido2_auth.py +1 -2
- modoboa/core/handlers.py +6 -2
- modoboa/core/management/commands/add_allowed_hosts.py +33 -0
- modoboa/core/management/commands/cleanlogs.py +9 -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 +29 -0
- modoboa/core/utils.py +6 -0
- modoboa/dnstools/api/v2/serializers.py +9 -11
- 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-DVZXWaUX.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-7eU7c_rI.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-BaJZ3BeR.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/{DashboardView-BLlMi6Qb.css → DashboardView-gwwVAPvt.css} +1 -1
- modoboa/frontend_dist/assets/DomainAdminList-DVn9x0rB.js +1 -0
- modoboa/frontend_dist/assets/DomainEditView-nAoL64D_.js +1 -0
- modoboa/frontend_dist/assets/{DomainTransportForm-DPnPGBOp.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-uM4AtIGi.js → HtmlEditor-Bh4c689R.js} +1 -1
- modoboa/frontend_dist/assets/IdentitiesView-BXAuU1YX.js +1 -0
- modoboa/frontend_dist/assets/{IdentitiesView-jmuItyMZ.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-CVD2Aen8.js → LoadingData-CdVvm4FI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-sWzBke1g.js → LoginCallbackView-B9hAH4MI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-wmN73W0f.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-BCeQljir.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-9iNcDhkI.js +6 -0
- 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-j7di4leN.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-B46caNs1.js +1 -0
- 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-CZqM2Ofu.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-C_Ydf6yr.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-DewTk2H8.js +1 -0
- modoboa/frontend_dist/assets/{aliases-c3n-dCV_.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-Bqckz8sr.js → contacts-BjghrPqZ.js} +1 -1
- modoboa/frontend_dist/assets/{domains-nBMR-fRf.js → domains-BSawReeu.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-ChZgLcqP.js → domains.store-D-vWCEIK.js} +1 -1
- modoboa/frontend_dist/assets/{filter-D7NrAf6a.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-Dn9vYw7T.js → importExport-DzoL4Mvc.js} +1 -1
- modoboa/frontend_dist/assets/index-BImkz5Jx.js +984 -0
- modoboa/frontend_dist/assets/index-DuzUMVLM.js +1 -0
- modoboa/frontend_dist/assets/layout-C5FyYCHK.js +1 -0
- modoboa/frontend_dist/assets/{layout.store-BxBoBlgf.js → layout.store-NXWtFIwL.js} +1 -1
- modoboa/frontend_dist/assets/logos-BswdveCV.js +1 -0
- modoboa/frontend_dist/assets/{logs-DrTzylW7.js → logs-6CbtfaZS.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-Lgiqp7aw.js → parameters-aSQiR7kN.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-C9k9DuTj.js → parameters.store-CzQqVatx.js} +1 -1
- modoboa/frontend_dist/assets/permissions-DNoefz-n.js +1 -0
- modoboa/frontend_dist/assets/{ssrBoot-BsxW6uW4.js → ssrBoot-CKUX4kcb.js} +1 -1
- modoboa/frontend_dist/assets/{tag-CFK9dzMJ.js → tag-B_yWNNJD.js} +1 -1
- modoboa/frontend_dist/assets/transports-BDNB9wR5.js +1 -0
- modoboa/frontend_dist/assets/{webmail-KrD8ZhNM.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/redis.py +1 -5
- 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 +201 -334
- 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.10.dist-info → modoboa-2.5.0.dist-info}/METADATA +6 -4
- {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/RECORD +244 -193
- modoboa/frontend_dist/assets/AccountAliasForm-DVXatAhB.js +0 -1
- modoboa/frontend_dist/assets/AccountEditView-DmvQjxpx.js +0 -1
- modoboa/frontend_dist/assets/AccountLayout-OGtZvlHR.js +0 -1
- modoboa/frontend_dist/assets/AccountPasswordSubForm-g3IEGrgM.js +0 -1
- modoboa/frontend_dist/assets/AccountView-DsxYqr3k.js +0 -1
- modoboa/frontend_dist/assets/AddressBook-3RoKiKon.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-CWfn8zaQ.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-Bheey-gp.css +0 -1
- modoboa/frontend_dist/assets/AlarmsView-D3Mh8ntf.js +0 -1
- modoboa/frontend_dist/assets/AliasEditView-C15eUZ11.js +0 -1
- modoboa/frontend_dist/assets/AliasView-Cyvc5vMb.js +0 -1
- modoboa/frontend_dist/assets/AuditTrailView-LI2XuLLn.js +0 -1
- modoboa/frontend_dist/assets/CalendarView-BpOlPh3f.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-CNfI7ept.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-B866Xsrc.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-BvqxQsD1.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-BQ3ug6Jh.js +0 -1
- modoboa/frontend_dist/assets/CreationForm-C9Kh05ax.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-Cw-gIcuB.js +0 -1
- modoboa/frontend_dist/assets/DomainAdminList-CsWUNKVk.js +0 -1
- modoboa/frontend_dist/assets/DomainEditView-DocxeOeW.js +0 -1
- modoboa/frontend_dist/assets/DomainView-Djc_0PsF.js +0 -5
- modoboa/frontend_dist/assets/DomainsView-B-Lxru7P.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-DasJ0NdZ.css +0 -1
- modoboa/frontend_dist/assets/EmailField-BEKxuYni.js +0 -1
- modoboa/frontend_dist/assets/EmailView-BsR1Wes5.js +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-BzPFOeLU.js +0 -1
- modoboa/frontend_dist/assets/FiltersView-5rmpC5cC.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-D7MbetcT.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-BqjD9Lue.js +0 -1
- modoboa/frontend_dist/assets/InformationView-CghcvPn2.js +0 -1
- modoboa/frontend_dist/assets/InformationView-U5Ww-Sx1.css +0 -1
- modoboa/frontend_dist/assets/MailboxView-T_p-_ZtJ.js +0 -1
- modoboa/frontend_dist/assets/MenuItems-kHCMzR5E.js +0 -1
- modoboa/frontend_dist/assets/MessagesView-Cerv3xsy.js +0 -1
- modoboa/frontend_dist/assets/MigrationsView-7kjqPyYU.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-DspBxVMV.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-Dy0H5ep1.js +0 -1
- modoboa/frontend_dist/assets/ProviderEditView-DDLMOylC.js +0 -1
- modoboa/frontend_dist/assets/ProviderGeneralForm-BwOSKNHK.js +0 -1
- modoboa/frontend_dist/assets/ProvidersView-C99UD0WB.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-DAPBHldd.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-D87PHeH0.js +0 -1
- modoboa/frontend_dist/assets/SettingsView-OxDo9wNd.js +0 -6
- modoboa/frontend_dist/assets/StatisticsView-CM__Eqku.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-C6j0uYR_.js +0 -1
- modoboa/frontend_dist/assets/UserLayout-CckCGnPS.js +0 -1
- modoboa/frontend_dist/assets/VAlert-B7mzOJIO.js +0 -1
- modoboa/frontend_dist/assets/VApp-BKxnjOoi.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-Cc4_tcl1.js +0 -1
- modoboa/frontend_dist/assets/VAvatar-BAgTUIvX.js +0 -1
- modoboa/frontend_dist/assets/VCard-DFWiFORP.js +0 -1
- modoboa/frontend_dist/assets/VCheckbox-CKsH_vq3.js +0 -1
- modoboa/frontend_dist/assets/VChip-nZ0uhY7t.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-Os2aeP6J.js +0 -1
- modoboa/frontend_dist/assets/VContainer-Btam4lk2.js +0 -1
- modoboa/frontend_dist/assets/VDataTable-D_0_xJTl.js +0 -1
- modoboa/frontend_dist/assets/VDataTableServer-BWTt4Mzi.js +0 -1
- modoboa/frontend_dist/assets/VDataTableVirtual-BwVmkt4u.js +0 -1
- modoboa/frontend_dist/assets/VExpansionPanels-B5D6GOa3.js +0 -1
- modoboa/frontend_dist/assets/VFileInput-Cv9DIPki.js +0 -1
- modoboa/frontend_dist/assets/VForm-CpoZf60D.js +0 -1
- modoboa/frontend_dist/assets/VMenu-B_dVqOmo.js +0 -1
- modoboa/frontend_dist/assets/VPicker-CXkIGEze.js +0 -1
- modoboa/frontend_dist/assets/VProgressCircular-CrEXxs7k.js +0 -1
- modoboa/frontend_dist/assets/VRadioGroup-D8ypjYOO.js +0 -1
- modoboa/frontend_dist/assets/VSelect-CS51PDEt.js +0 -1
- modoboa/frontend_dist/assets/VSelectionControl-DiOqtY38.js +0 -1
- modoboa/frontend_dist/assets/VSheet-5VVWtHvs.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-C6PZ3X24.js +0 -1
- modoboa/frontend_dist/assets/VSwitch-Chg5o-Cp.js +0 -1
- modoboa/frontend_dist/assets/VTable-KsiZ3cBz.js +0 -1
- modoboa/frontend_dist/assets/VTabs-tNrJIYO0.js +0 -1
- modoboa/frontend_dist/assets/VTextField-C-J20yj_.css +0 -1
- modoboa/frontend_dist/assets/VTextField-CLwRV0Cb.js +0 -1
- modoboa/frontend_dist/assets/VTextarea-Di8jbl8m.js +0 -1
- modoboa/frontend_dist/assets/VToolbar-CGwhgdmI.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-wWSFAGI-.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-DsYThBrz.js +0 -1
- modoboa/frontend_dist/assets/admin-CJVLMHh9.js +0 -1
- modoboa/frontend_dist/assets/forwardRefs-Cpc3YYl6.js +0 -1
- modoboa/frontend_dist/assets/global.store-DUP26-A5.js +0 -1
- modoboa/frontend_dist/assets/index-DV9Li2cg.js +0 -852
- modoboa/frontend_dist/assets/index-DzL89N4E.js +0 -1
- modoboa/frontend_dist/assets/layout-CHO37cA6.js +0 -1
- modoboa/frontend_dist/assets/permissions-mL5hLHcW.js +0 -1
- modoboa/frontend_dist/assets/transports-TO08iTXJ.js +0 -1
- {modoboa-2.4.10.data → modoboa-2.5.0.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/WHEEL +0 -0
- {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/entry_points.txt +0 -0
- {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Amavis factories."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import factory
|
|
7
|
+
|
|
8
|
+
from . import models
|
|
9
|
+
from .utils import smart_bytes
|
|
10
|
+
|
|
11
|
+
SPAM_BODY = """X-Envelope-To: <{rcpt}>
|
|
12
|
+
X-Envelope-To-Blocked: <{rcpt}>
|
|
13
|
+
X-Quarantine-ID: <nq6ekd4wtXZg>
|
|
14
|
+
X-Spam-Flag: YES
|
|
15
|
+
X-Spam-Score: 1000.985
|
|
16
|
+
X-Spam-Level: ****************************************************************
|
|
17
|
+
X-Spam-Status: Yes, score=1000.985 tag=2 tag2=6.31 kill=6.31
|
|
18
|
+
tests=[ALL_TRUSTED=-1, GTUBE=1000, PYZOR_CHECK=1.985]
|
|
19
|
+
autolearn=no autolearn_force=no
|
|
20
|
+
Received: from demo.modoboa.org ([127.0.0.1])
|
|
21
|
+
by localhost (demo.modoboa.org [127.0.0.1]) (amavisd-new, port 10024)
|
|
22
|
+
with ESMTP id nq6ekd4wtXZg for <user@demo.local>;
|
|
23
|
+
Thu, 9 Nov 2017 15:59:52 +0100 (CET)
|
|
24
|
+
Received: from demo.modoboa.org (localhost [127.0.0.1])
|
|
25
|
+
by demo.modoboa.org (Postfix) with ESMTP
|
|
26
|
+
for <user@demo.local>; Thu, 9 Nov 2017 15:59:52 +0100 (CET)
|
|
27
|
+
Content-Type: text/plain; charset="utf-8"
|
|
28
|
+
MIME-Version: 1.0
|
|
29
|
+
Content-Transfer-Encoding: quoted-printable
|
|
30
|
+
Subject: Sample message
|
|
31
|
+
From: {sender}
|
|
32
|
+
To: {rcpt}
|
|
33
|
+
Message-ID: <151023959268.5550.5713670714483771838@demo.modoboa.org>
|
|
34
|
+
Date: Thu, 09 Nov 2017 15:59:52 +0100
|
|
35
|
+
|
|
36
|
+
This is the GTUBE, the
|
|
37
|
+
Generic
|
|
38
|
+
Test for
|
|
39
|
+
Unsolicited
|
|
40
|
+
Bulk
|
|
41
|
+
Email
|
|
42
|
+
|
|
43
|
+
If your spam filter supports it, the GTUBE provides a test by which you
|
|
44
|
+
can verify that the filter is installed correctly and is detecting incoming
|
|
45
|
+
spam. You can send yourself a test mail containing the following string of
|
|
46
|
+
characters (in upper case and with no white spaces and line breaks):
|
|
47
|
+
|
|
48
|
+
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
|
|
49
|
+
|
|
50
|
+
You should send this test mail from an account outside of your network.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
VIRUS_BODY = r"""Subject: Virus Test Message (EICAR)
|
|
54
|
+
MIME-Version: 1.0
|
|
55
|
+
Content-Type: multipart/mixed; boundary="huq684BweRXVnRxX"
|
|
56
|
+
Content-Disposition: inline
|
|
57
|
+
Date: Sun, 06 Nov 2011 10:08:18 -0800
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
--huq684BweRXVnRxX
|
|
61
|
+
Content-Type: text/plain; charset=us-ascii
|
|
62
|
+
Content-Disposition: inline
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
This is a virus test message. It contains an attached file 'eicar.com',
|
|
66
|
+
which contains the EICAR virus <http://eicar.org/86-0-Intended-use.html>
|
|
67
|
+
test pattern.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
--huq684BweRXVnRxX
|
|
71
|
+
Content-Type: application/x-msdos-program
|
|
72
|
+
Content-Disposition: attachment; filename="eicar.com"
|
|
73
|
+
Content-Transfer-Encoding: quoted-printable
|
|
74
|
+
|
|
75
|
+
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*=0A
|
|
76
|
+
--huq684BweRXVnRxX--
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MaddrFactory(factory.django.DjangoModelFactory):
|
|
81
|
+
"""Factory for Maddr."""
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
model = models.Maddr
|
|
85
|
+
django_get_or_create = ("email",)
|
|
86
|
+
|
|
87
|
+
id = factory.Sequence(lambda n: n) # NOQA:A003
|
|
88
|
+
email = factory.Sequence(lambda n: f"user_{n}@domain.test")
|
|
89
|
+
domain = "test.domain"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MsgsFactory(factory.django.DjangoModelFactory):
|
|
93
|
+
"""Factory for Mailaddr."""
|
|
94
|
+
|
|
95
|
+
class Meta:
|
|
96
|
+
model = models.Msgs
|
|
97
|
+
|
|
98
|
+
mail_id = factory.Sequence(lambda n: f"mailid{n}".encode("ascii"))
|
|
99
|
+
secret_id = factory.Sequence(lambda n: smart_bytes(f"id{n}"))
|
|
100
|
+
sid = factory.SubFactory(MaddrFactory)
|
|
101
|
+
client_addr = "127.0.0.1"
|
|
102
|
+
originating = "Y"
|
|
103
|
+
dsn_sent = "N"
|
|
104
|
+
subject = factory.Sequence(lambda n: f"Test message {n}")
|
|
105
|
+
time_num = factory.LazyAttribute(lambda o: int(time.time()))
|
|
106
|
+
time_iso = factory.LazyAttribute(
|
|
107
|
+
lambda o: datetime.datetime.fromtimestamp(o.time_num).isoformat()
|
|
108
|
+
)
|
|
109
|
+
size = 100
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MsgrcptFactory(factory.django.DjangoModelFactory):
|
|
113
|
+
"""Factory for Msgrcpt."""
|
|
114
|
+
|
|
115
|
+
class Meta:
|
|
116
|
+
model = models.Msgrcpt
|
|
117
|
+
|
|
118
|
+
rseqnum = 1
|
|
119
|
+
is_local = "Y"
|
|
120
|
+
bl = "N"
|
|
121
|
+
wl = "N"
|
|
122
|
+
mail = factory.SubFactory(MsgsFactory)
|
|
123
|
+
rid = factory.SubFactory(MaddrFactory)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class QuarantineFactory(factory.django.DjangoModelFactory):
|
|
127
|
+
"""Factory for Quarantine."""
|
|
128
|
+
|
|
129
|
+
class Meta:
|
|
130
|
+
model = models.Quarantine
|
|
131
|
+
|
|
132
|
+
chunk_ind = 1
|
|
133
|
+
mail = factory.SubFactory(MsgsFactory)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_quarantined_msg(rcpt, sender, rs, body, **kwargs):
|
|
137
|
+
"""Create a quarantined msg."""
|
|
138
|
+
msgrcpt = MsgrcptFactory(
|
|
139
|
+
rs=rs,
|
|
140
|
+
rid__email=rcpt,
|
|
141
|
+
rid__domain="com.test", # FIXME
|
|
142
|
+
mail__sid__email=smart_bytes(sender),
|
|
143
|
+
mail__sid__domain="", # FIXME
|
|
144
|
+
**kwargs,
|
|
145
|
+
)
|
|
146
|
+
QuarantineFactory(
|
|
147
|
+
mail=msgrcpt.mail,
|
|
148
|
+
mail_text=smart_bytes(SPAM_BODY.format(rcpt=rcpt, sender=sender)),
|
|
149
|
+
)
|
|
150
|
+
return msgrcpt
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_spam(rcpt, sender="spam@evil.corp", rs=" "):
|
|
154
|
+
"""Create a spam."""
|
|
155
|
+
body = SPAM_BODY.format(rcpt=rcpt, sender=sender)
|
|
156
|
+
body += "fóó bár"
|
|
157
|
+
return create_quarantined_msg(
|
|
158
|
+
rcpt, sender, rs, body, bspam_level=999.0, content="S"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_virus(rcpt, sender="virus@evil.corp", rs=" "):
|
|
163
|
+
"""Create a virus."""
|
|
164
|
+
return create_quarantined_msg(rcpt, sender, rs, VIRUS_BODY, content="V")
|
|
@@ -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
|