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
modoboa/amavis/models.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# This is an auto-generated Django model module.
|
|
2
|
+
# You'll have to do the following manually to clean this up:
|
|
3
|
+
# * Rearrange models' order
|
|
4
|
+
# * Make sure each model has one field with primary_key=True
|
|
5
|
+
# Feel free to rename the models, but don't rename db_table values
|
|
6
|
+
# or field names.
|
|
7
|
+
#
|
|
8
|
+
# Also note: You'll have to insert the output of
|
|
9
|
+
# 'django-admin.py sqlcustom [appname]'into your database.
|
|
10
|
+
#
|
|
11
|
+
# Original Amavis version : 2.6.2
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from django.db import models
|
|
16
|
+
from django.utils.translation import gettext_lazy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Maddr(models.Model):
|
|
20
|
+
partition_tag = models.IntegerField(default=0)
|
|
21
|
+
id = models.BigIntegerField(primary_key=True) # NOQA:A003
|
|
22
|
+
email = models.CharField(max_length=255)
|
|
23
|
+
domain = models.CharField(max_length=255)
|
|
24
|
+
|
|
25
|
+
class Meta:
|
|
26
|
+
db_table = "maddr"
|
|
27
|
+
unique_together = [("partition_tag", "email")]
|
|
28
|
+
managed = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Mailaddr(models.Model):
|
|
32
|
+
id = models.IntegerField(primary_key=True) # NOQA:A003
|
|
33
|
+
priority = models.IntegerField()
|
|
34
|
+
email = models.CharField(unique=True, max_length=255)
|
|
35
|
+
|
|
36
|
+
class Meta:
|
|
37
|
+
db_table = "mailaddr"
|
|
38
|
+
managed = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Msgs(models.Model):
|
|
42
|
+
partition_tag = models.IntegerField(default=0)
|
|
43
|
+
mail_id = models.BinaryField(max_length=16, primary_key=True)
|
|
44
|
+
secret_id = models.BinaryField()
|
|
45
|
+
am_id = models.CharField(max_length=60)
|
|
46
|
+
time_num = models.IntegerField()
|
|
47
|
+
time_iso = models.CharField(max_length=48)
|
|
48
|
+
sid = models.ForeignKey(Maddr, db_column="sid", on_delete=models.CASCADE)
|
|
49
|
+
policy = models.CharField(max_length=765, blank=True)
|
|
50
|
+
client_addr = models.CharField(max_length=765, blank=True)
|
|
51
|
+
size = models.IntegerField()
|
|
52
|
+
originating = models.CharField(max_length=3)
|
|
53
|
+
content = models.CharField(max_length=1, blank=True)
|
|
54
|
+
quar_type = models.CharField(max_length=1, blank=True)
|
|
55
|
+
quar_loc = models.CharField(max_length=255, blank=True)
|
|
56
|
+
dsn_sent = models.CharField(max_length=3, blank=True)
|
|
57
|
+
spam_level = models.FloatField(null=True, blank=True)
|
|
58
|
+
message_id = models.CharField(max_length=765, blank=True)
|
|
59
|
+
from_addr = models.CharField(max_length=765, blank=True)
|
|
60
|
+
subject = models.CharField(max_length=765, blank=True)
|
|
61
|
+
host = models.CharField(max_length=765)
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
db_table = "msgs"
|
|
65
|
+
managed = False
|
|
66
|
+
unique_together = ("partition_tag", "mail_id")
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def time_datetime(self) -> datetime:
|
|
70
|
+
return datetime.fromtimestamp(self.time_num)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Msgrcpt(models.Model):
|
|
74
|
+
partition_tag = models.IntegerField(default=0)
|
|
75
|
+
mail = models.ForeignKey(Msgs, primary_key=True, on_delete=models.CASCADE)
|
|
76
|
+
rid = models.ForeignKey(Maddr, db_column="rid", on_delete=models.CASCADE)
|
|
77
|
+
rseqnum = models.IntegerField(default=0)
|
|
78
|
+
is_local = models.CharField(max_length=3)
|
|
79
|
+
content = models.CharField(max_length=3)
|
|
80
|
+
ds = models.CharField(max_length=3)
|
|
81
|
+
rs = models.CharField(max_length=3)
|
|
82
|
+
bl = models.CharField(max_length=3, blank=True)
|
|
83
|
+
wl = models.CharField(max_length=3, blank=True)
|
|
84
|
+
bspam_level = models.FloatField(null=True, blank=True)
|
|
85
|
+
smtp_resp = models.CharField(max_length=765, blank=True)
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
db_table = "msgrcpt"
|
|
89
|
+
managed = False
|
|
90
|
+
unique_together = ("partition_tag", "mail", "rseqnum")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Policy(models.Model):
|
|
94
|
+
policy_name = models.CharField(max_length=32, blank=True)
|
|
95
|
+
virus_lover = models.CharField(max_length=3, blank=True, null=True)
|
|
96
|
+
spam_lover = models.CharField(max_length=3, blank=True, null=True)
|
|
97
|
+
unchecked_lover = models.CharField(max_length=3, blank=True, null=True)
|
|
98
|
+
banned_files_lover = models.CharField(max_length=3, blank=True, null=True)
|
|
99
|
+
bad_header_lover = models.CharField(max_length=3, blank=True, null=True)
|
|
100
|
+
bypass_virus_checks = models.CharField(
|
|
101
|
+
gettext_lazy("Virus filter"),
|
|
102
|
+
default="",
|
|
103
|
+
null=True,
|
|
104
|
+
choices=(
|
|
105
|
+
("N", gettext_lazy("yes")),
|
|
106
|
+
("Y", gettext_lazy("no")),
|
|
107
|
+
("", gettext_lazy("default")),
|
|
108
|
+
),
|
|
109
|
+
max_length=3,
|
|
110
|
+
help_text=gettext_lazy(
|
|
111
|
+
"Bypass virus checks or not. Choose 'default' to use global " "settings."
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
bypass_spam_checks = models.CharField(
|
|
115
|
+
gettext_lazy("Spam filter"),
|
|
116
|
+
default="",
|
|
117
|
+
null=True,
|
|
118
|
+
choices=(
|
|
119
|
+
("N", gettext_lazy("yes")),
|
|
120
|
+
("Y", gettext_lazy("no")),
|
|
121
|
+
("", gettext_lazy("default")),
|
|
122
|
+
),
|
|
123
|
+
max_length=3,
|
|
124
|
+
help_text=gettext_lazy(
|
|
125
|
+
"Bypass spam checks or not. Choose 'default' to use global " "settings."
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
bypass_banned_checks = models.CharField(
|
|
129
|
+
gettext_lazy("Banned filter"),
|
|
130
|
+
default="",
|
|
131
|
+
null=True,
|
|
132
|
+
choices=(
|
|
133
|
+
("N", gettext_lazy("yes")),
|
|
134
|
+
("Y", gettext_lazy("no")),
|
|
135
|
+
("", gettext_lazy("default")),
|
|
136
|
+
),
|
|
137
|
+
max_length=3,
|
|
138
|
+
help_text=gettext_lazy(
|
|
139
|
+
"Bypass banned checks or not. Choose 'default' to use global " "settings."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
bypass_header_checks = models.CharField(max_length=3, blank=True, null=True)
|
|
143
|
+
virus_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
144
|
+
spam_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
145
|
+
banned_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
146
|
+
unchecked_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
147
|
+
bad_header_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
148
|
+
clean_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
149
|
+
archive_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
|
|
150
|
+
spam_tag_level = models.FloatField(null=True, blank=True)
|
|
151
|
+
spam_tag2_level = models.FloatField(null=True, blank=True)
|
|
152
|
+
spam_tag3_level = models.FloatField(null=True, blank=True)
|
|
153
|
+
spam_kill_level = models.FloatField(null=True, blank=True)
|
|
154
|
+
spam_dsn_cutoff_level = models.FloatField(null=True, blank=True)
|
|
155
|
+
spam_quarantine_cutoff_level = models.FloatField(null=True, blank=True)
|
|
156
|
+
addr_extension_virus = models.CharField(max_length=192, blank=True, null=True)
|
|
157
|
+
addr_extension_spam = models.CharField(max_length=192, blank=True, null=True)
|
|
158
|
+
addr_extension_banned = models.CharField(max_length=192, blank=True, null=True)
|
|
159
|
+
addr_extension_bad_header = models.CharField(max_length=192, blank=True, null=True)
|
|
160
|
+
warnvirusrecip = models.CharField(max_length=3, blank=True, null=True)
|
|
161
|
+
warnbannedrecip = models.CharField(max_length=3, blank=True, null=True)
|
|
162
|
+
warnbadhrecip = models.CharField(max_length=3, blank=True, null=True)
|
|
163
|
+
newvirus_admin = models.CharField(max_length=192, blank=True, null=True)
|
|
164
|
+
virus_admin = models.CharField(max_length=192, blank=True, null=True)
|
|
165
|
+
banned_admin = models.CharField(max_length=192, blank=True, null=True)
|
|
166
|
+
bad_header_admin = models.CharField(max_length=192, blank=True, null=True)
|
|
167
|
+
spam_admin = models.CharField(max_length=192, blank=True, null=True)
|
|
168
|
+
spam_subject_tag = models.CharField(max_length=192, blank=True, null=True)
|
|
169
|
+
spam_subject_tag2 = models.CharField(
|
|
170
|
+
gettext_lazy("Spam marker"),
|
|
171
|
+
default=None,
|
|
172
|
+
max_length=192,
|
|
173
|
+
blank=True,
|
|
174
|
+
null=True,
|
|
175
|
+
help_text=gettext_lazy(
|
|
176
|
+
"Modify spam subject using the specified text. "
|
|
177
|
+
"Choose 'default' to use global settings."
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
spam_subject_tag3 = models.CharField(max_length=192, blank=True, null=True)
|
|
181
|
+
message_size_limit = models.IntegerField(null=True, blank=True)
|
|
182
|
+
banned_rulenames = models.CharField(max_length=192, blank=True, null=True)
|
|
183
|
+
disclaimer_options = models.CharField(max_length=192, blank=True, null=True)
|
|
184
|
+
forward_method = models.CharField(max_length=192, blank=True, null=True)
|
|
185
|
+
sa_userconf = models.CharField(max_length=192, blank=True, null=True)
|
|
186
|
+
sa_username = models.CharField(max_length=192, blank=True, null=True)
|
|
187
|
+
|
|
188
|
+
class Meta:
|
|
189
|
+
db_table = "policy"
|
|
190
|
+
managed = False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Quarantine(models.Model):
|
|
194
|
+
partition_tag = models.IntegerField(default=0)
|
|
195
|
+
mail = models.ForeignKey(Msgs, primary_key=True, on_delete=models.CASCADE)
|
|
196
|
+
chunk_ind = models.IntegerField()
|
|
197
|
+
mail_text = models.BinaryField()
|
|
198
|
+
|
|
199
|
+
class Meta:
|
|
200
|
+
db_table = "quarantine"
|
|
201
|
+
managed = False
|
|
202
|
+
ordering = ["-mail__time_num"]
|
|
203
|
+
unique_together = ("partition_tag", "mail", "chunk_ind")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Users(models.Model):
|
|
207
|
+
id = models.AutoField(primary_key=True) # NOQA:A003
|
|
208
|
+
priority = models.IntegerField()
|
|
209
|
+
policy = models.ForeignKey(Policy, on_delete=models.CASCADE)
|
|
210
|
+
email = models.CharField(unique=True, max_length=255)
|
|
211
|
+
fullname = models.CharField(max_length=765, blank=True)
|
|
212
|
+
|
|
213
|
+
class Meta:
|
|
214
|
+
db_table = "users"
|
|
215
|
+
managed = False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Wblist(models.Model):
|
|
219
|
+
rid = models.IntegerField(primary_key=True)
|
|
220
|
+
sid = models.IntegerField()
|
|
221
|
+
wb = models.CharField(max_length=30)
|
|
222
|
+
|
|
223
|
+
class Meta:
|
|
224
|
+
db_table = "wblist"
|
|
225
|
+
managed = False
|
|
226
|
+
unique_together = [("rid", "sid")]
|
|
@@ -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="")
|
|
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)
|