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,265 @@
|
|
|
1
|
+
"""Amavis viewsets."""
|
|
2
|
+
|
|
3
|
+
from django.http import Http404
|
|
4
|
+
from django.shortcuts import get_object_or_404
|
|
5
|
+
from django.utils.translation import gettext as _
|
|
6
|
+
|
|
7
|
+
import django_rq
|
|
8
|
+
from rest_framework import filters, mixins, permissions, response, viewsets
|
|
9
|
+
from rest_framework.decorators import action
|
|
10
|
+
|
|
11
|
+
from modoboa.amavis.sql_connector import SQLconnector
|
|
12
|
+
from modoboa.lib.paginator import Paginator
|
|
13
|
+
from modoboa.lib.permissions import CanViewDomain
|
|
14
|
+
|
|
15
|
+
from modoboa.admin import models as admin_models
|
|
16
|
+
from modoboa.amavis import models, serializers, tasks
|
|
17
|
+
from modoboa.amavis.lib import (
|
|
18
|
+
AMrelease,
|
|
19
|
+
manual_learning_enabled,
|
|
20
|
+
SelfServiceAuthentication,
|
|
21
|
+
)
|
|
22
|
+
from modoboa.amavis.sql_email import SQLemail
|
|
23
|
+
from modoboa.amavis.utils import smart_str
|
|
24
|
+
from modoboa.parameters import tools as param_tools
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SELFSERVICE_ACTIONS = ["retrieve", "headers", "delete", "release"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_user_valid_addresses(user):
|
|
31
|
+
"""Retrieve all valid addresses of a user."""
|
|
32
|
+
valid_addresses = []
|
|
33
|
+
if user.role == "SimpleUsers":
|
|
34
|
+
valid_addresses.append(user.email)
|
|
35
|
+
try:
|
|
36
|
+
mb = admin_models.Mailbox.objects.get(user=user)
|
|
37
|
+
except admin_models.Mailbox.DoesNotExist:
|
|
38
|
+
pass
|
|
39
|
+
else:
|
|
40
|
+
valid_addresses += mb.alias_addresses
|
|
41
|
+
return valid_addresses
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class QuarantineViewSet(viewsets.GenericViewSet):
|
|
45
|
+
|
|
46
|
+
filter_backends = (filters.OrderingFilter,)
|
|
47
|
+
ordering_fields = [
|
|
48
|
+
"datetime",
|
|
49
|
+
"from_address",
|
|
50
|
+
"score",
|
|
51
|
+
"subject",
|
|
52
|
+
"to_address",
|
|
53
|
+
"type",
|
|
54
|
+
]
|
|
55
|
+
permission_classes = (permissions.IsAuthenticated,)
|
|
56
|
+
|
|
57
|
+
def get_serializer_class(self):
|
|
58
|
+
if self.action == "retrieve":
|
|
59
|
+
return serializers.MessageSerializer
|
|
60
|
+
if self.action == "headers":
|
|
61
|
+
return serializers.MessageHeadersSerializer
|
|
62
|
+
if self.action == "mark_selection":
|
|
63
|
+
return serializers.MarkMessageSelectionSerializer
|
|
64
|
+
if self.action in ["release_selection", "delete_selection"]:
|
|
65
|
+
return serializers.MessageSelectionSerializer
|
|
66
|
+
if self.action in ["delete", "release"]:
|
|
67
|
+
return serializers.MessageIdentifierSerializer
|
|
68
|
+
return serializers.PaginatedMessageListSerializer
|
|
69
|
+
|
|
70
|
+
def get_authenticators(self):
|
|
71
|
+
result = [auth() for auth in self.authentication_classes]
|
|
72
|
+
if self.request:
|
|
73
|
+
action = self.action_map.get(self.request.method.lower())
|
|
74
|
+
if action in SELFSERVICE_ACTIONS:
|
|
75
|
+
result = [SelfServiceAuthentication()] + result
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
def get_permissions(self):
|
|
79
|
+
if self.request.auth == "selfservice" and self.action in SELFSERVICE_ACTIONS:
|
|
80
|
+
return []
|
|
81
|
+
return super().get_permissions()
|
|
82
|
+
|
|
83
|
+
def list(self, request):
|
|
84
|
+
ordering = request.GET.get("ordering")
|
|
85
|
+
connector = SQLconnector(user=request.user, ordering=ordering)
|
|
86
|
+
try:
|
|
87
|
+
page_size = int(request.GET.get("page_size"))
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
page_size = request.user.parameters.get_value("messages_per_page")
|
|
90
|
+
total = connector.messages_count(request)
|
|
91
|
+
paginator = Paginator(total, page_size)
|
|
92
|
+
page_num = int(request.GET.get("page", 1))
|
|
93
|
+
page = paginator.getpage(page_num)
|
|
94
|
+
if not page:
|
|
95
|
+
serializer = self.get_serializer(
|
|
96
|
+
{
|
|
97
|
+
"count": 0,
|
|
98
|
+
"first_index": 0,
|
|
99
|
+
"last_index": 0,
|
|
100
|
+
"prev_page": None,
|
|
101
|
+
"next_page": None,
|
|
102
|
+
"results": [],
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
return response.Response(serializer.data)
|
|
106
|
+
email_list = connector.fetch(page.id_start, page.id_stop)
|
|
107
|
+
serializer = self.get_serializer(
|
|
108
|
+
{
|
|
109
|
+
"count": total,
|
|
110
|
+
"first_index": page_num * page_size,
|
|
111
|
+
"last_index": (page_num * page_size) + len(email_list),
|
|
112
|
+
"prev_page": page.previous_page_number if page.has_previous else None,
|
|
113
|
+
"next_page": page.next_page_number if page.has_next else None,
|
|
114
|
+
"results": email_list,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
return response.Response(serializer.data)
|
|
118
|
+
|
|
119
|
+
def retrieve(self, request, pk):
|
|
120
|
+
rcpt = request.GET.get("rcpt")
|
|
121
|
+
if rcpt is None:
|
|
122
|
+
return response.Response({"error": _("Invalid request")}, status=400)
|
|
123
|
+
if request.user:
|
|
124
|
+
if request.user.email == rcpt:
|
|
125
|
+
SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
|
|
126
|
+
elif hasattr(request.user, "mailbox"):
|
|
127
|
+
mb = request.user.mailbox
|
|
128
|
+
if rcpt == mb.full_address or rcpt in mb.alias_addresses:
|
|
129
|
+
SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
|
|
130
|
+
mail = SQLemail(pk.encode("ascii"), dformat="plain")
|
|
131
|
+
serializer = self.get_serializer(mail)
|
|
132
|
+
return response.Response(serializer.data)
|
|
133
|
+
|
|
134
|
+
@action(methods=["get"], detail=True)
|
|
135
|
+
def headers(self, request, pk):
|
|
136
|
+
email = SQLemail(pk.encode("ascii"))
|
|
137
|
+
headers = []
|
|
138
|
+
for name in email.msg.keys():
|
|
139
|
+
headers.append({"name": name, "value": email.get_header(email.msg, name)})
|
|
140
|
+
serializer = self.get_serializer({"headers": headers})
|
|
141
|
+
return response.Response(serializer.data)
|
|
142
|
+
|
|
143
|
+
def _release_selection(self, request, selection):
|
|
144
|
+
connector = SQLconnector()
|
|
145
|
+
valid_addresses = None
|
|
146
|
+
if request.user:
|
|
147
|
+
valid_addresses = get_user_valid_addresses(request.user)
|
|
148
|
+
msgrcpts = []
|
|
149
|
+
for item in selection:
|
|
150
|
+
if valid_addresses and item["rcpt"] not in valid_addresses:
|
|
151
|
+
continue
|
|
152
|
+
msgrcpts += [
|
|
153
|
+
(
|
|
154
|
+
item["mailid"],
|
|
155
|
+
connector.get_recipient_message(item["rcpt"], item["mailid"]),
|
|
156
|
+
)
|
|
157
|
+
]
|
|
158
|
+
if (
|
|
159
|
+
not request.user or request.user.role == "SimpleUsers"
|
|
160
|
+
) and not param_tools.get_global_parameter("user_can_release"):
|
|
161
|
+
for i, msgrcpt in msgrcpts:
|
|
162
|
+
connector.set_msgrcpt_status(smart_str(msgrcpt.rid.email), i, "p")
|
|
163
|
+
return response.Response({"status": "pending"})
|
|
164
|
+
|
|
165
|
+
amr = AMrelease()
|
|
166
|
+
error = None
|
|
167
|
+
for mid, rcpt in msgrcpts:
|
|
168
|
+
# we can't use the .mail relation on rcpt because it leads to
|
|
169
|
+
# an error on Postgres (memoryview pickle error).
|
|
170
|
+
mail = models.Msgs.objects.get(pk=mid.encode("ascii"))
|
|
171
|
+
result = amr.sendreq(mid, mail.secret_id, rcpt.rid.email)
|
|
172
|
+
if result:
|
|
173
|
+
connector.set_msgrcpt_status(smart_str(rcpt.rid.email), mid, "R")
|
|
174
|
+
else:
|
|
175
|
+
error = result
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if error:
|
|
179
|
+
return response.Response({"status": error})
|
|
180
|
+
|
|
181
|
+
return response.Response({"status": "released"})
|
|
182
|
+
|
|
183
|
+
@action(methods=["post"], detail=False)
|
|
184
|
+
def release_selection(self, request):
|
|
185
|
+
serializer = self.get_serializer(data=request.data)
|
|
186
|
+
serializer.is_valid(raise_exception=True)
|
|
187
|
+
return self._release_selection(request, serializer.validated_data["selection"])
|
|
188
|
+
|
|
189
|
+
@action(methods=["post"], detail=True)
|
|
190
|
+
def release(self, request, pk):
|
|
191
|
+
serializer = self.get_serializer(data=request.data)
|
|
192
|
+
serializer.is_valid(raise_exception=True)
|
|
193
|
+
return self._release_selection(request, [serializer.validated_data])
|
|
194
|
+
|
|
195
|
+
def _delete_selection(self, request, selection):
|
|
196
|
+
connector = SQLconnector()
|
|
197
|
+
valid_addresses = None
|
|
198
|
+
if request.user:
|
|
199
|
+
valid_addresses = get_user_valid_addresses(request.user)
|
|
200
|
+
for item in selection:
|
|
201
|
+
if valid_addresses and item["rcpt"] not in valid_addresses:
|
|
202
|
+
continue
|
|
203
|
+
connector.set_msgrcpt_status(item["rcpt"], item["mailid"], "D")
|
|
204
|
+
return response.Response(status=204)
|
|
205
|
+
|
|
206
|
+
@action(methods=["post"], detail=False)
|
|
207
|
+
def delete_selection(self, request):
|
|
208
|
+
serializer = self.get_serializer(data=request.data)
|
|
209
|
+
serializer.is_valid(raise_exception=True)
|
|
210
|
+
return self._delete_selection(request, serializer.validated_data["selection"])
|
|
211
|
+
|
|
212
|
+
@action(methods=["post"], detail=True)
|
|
213
|
+
def delete(self, request, pk):
|
|
214
|
+
serializer = self.get_serializer(data=request.data)
|
|
215
|
+
serializer.is_valid(raise_exception=True)
|
|
216
|
+
return self._delete_selection(request, [serializer.validated_data])
|
|
217
|
+
|
|
218
|
+
@action(methods=["post"], detail=False)
|
|
219
|
+
def mark_selection(self, request):
|
|
220
|
+
serializer = self.get_serializer(data=request.data)
|
|
221
|
+
serializer.is_valid(raise_exception=True)
|
|
222
|
+
if not manual_learning_enabled(request.user):
|
|
223
|
+
return response.Response({"status": "ok"})
|
|
224
|
+
recipient_db = serializer.validated_data.get("database")
|
|
225
|
+
if not recipient_db:
|
|
226
|
+
recipient_db = "user" if request.user.role == "SimpleUsers" else "global"
|
|
227
|
+
queue = django_rq.get_queue("modoboa")
|
|
228
|
+
queue.enqueue(
|
|
229
|
+
tasks.manual_learning,
|
|
230
|
+
request.user.pk,
|
|
231
|
+
serializer.validated_data["type"],
|
|
232
|
+
serializer.validated_data["selection"],
|
|
233
|
+
recipient_db,
|
|
234
|
+
)
|
|
235
|
+
return response.Response(status=204)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class PolicyViewSet(
|
|
239
|
+
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet
|
|
240
|
+
):
|
|
241
|
+
|
|
242
|
+
permission_classes = (permissions.IsAuthenticated, CanViewDomain)
|
|
243
|
+
serializer_class = serializers.PolicySerializer
|
|
244
|
+
|
|
245
|
+
def get_queryset(self):
|
|
246
|
+
domains = [
|
|
247
|
+
f"@{name}"
|
|
248
|
+
for name in admin_models.Domain.objects.get_for_admin(
|
|
249
|
+
self.request.user
|
|
250
|
+
).values_list("name", flat=True)
|
|
251
|
+
]
|
|
252
|
+
return models.Policy.objects.filter(users__email__in=domains)
|
|
253
|
+
|
|
254
|
+
def get_object(self):
|
|
255
|
+
"""Return the object the view is displaying."""
|
|
256
|
+
domain = get_object_or_404(admin_models.Domain, pk=self.kwargs["pk"])
|
|
257
|
+
queryset = self.filter_queryset(self.get_queryset())
|
|
258
|
+
obj = queryset.filter(users__email=f"@{domain.name}").first()
|
|
259
|
+
if obj is None:
|
|
260
|
+
raise Http404
|
|
261
|
+
|
|
262
|
+
# May raise a permission denied
|
|
263
|
+
self.check_object_permissions(self.request, obj)
|
|
264
|
+
|
|
265
|
+
return obj
|
|
@@ -83,10 +83,12 @@ class CheckPasswordTFASerializer(serializers.Serializer):
|
|
|
83
83
|
def validate_password(self, value):
|
|
84
84
|
user = self.context["user"]
|
|
85
85
|
if not user.check_password(value):
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
msg = _(
|
|
87
|
+
"Failed TFA settings editing attempt from '%(addr)s' as user '%(user)s'"
|
|
88
|
+
) % {
|
|
89
|
+
"addr": self.context["remote_addr"],
|
|
90
|
+
"user": escape(user.username),
|
|
91
|
+
}
|
|
92
|
+
logger.warning(msg)
|
|
91
93
|
raise serializers.ValidationError(_("Invalid password"))
|
|
92
94
|
return value
|
|
@@ -188,6 +188,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
|
|
|
188
188
|
inactive_account_threshold = serializers.IntegerField(default=30)
|
|
189
189
|
top_notifications_check_interval = serializers.IntegerField(default=30)
|
|
190
190
|
log_maximum_age = serializers.IntegerField(default=365)
|
|
191
|
+
message_history_maximum_age = serializers.IntegerField(default=180)
|
|
191
192
|
items_per_page = serializers.IntegerField(default=30)
|
|
192
193
|
default_top_redirection = serializers.ChoiceField(
|
|
193
194
|
default="user", choices=[("user", _("User profile"))], required=False
|
|
@@ -663,9 +664,11 @@ class NotificationSerializer(serializers.Serializer):
|
|
|
663
664
|
"""Serializer used to render a notification."""
|
|
664
665
|
|
|
665
666
|
id = serializers.CharField()
|
|
666
|
-
url = serializers.CharField(required=False)
|
|
667
667
|
text = serializers.CharField()
|
|
668
|
-
|
|
668
|
+
color = serializers.CharField()
|
|
669
|
+
target = serializers.CharField()
|
|
670
|
+
url = serializers.CharField(required=False)
|
|
671
|
+
counter = serializers.IntegerField(required=False)
|
|
669
672
|
|
|
670
673
|
|
|
671
674
|
class ModoboaApplicationSerializer(serializers.Serializer):
|
|
@@ -691,3 +694,11 @@ class ThemeSerializer(serializers.Serializer):
|
|
|
691
694
|
theme_primary_color_light = serializers.CharField(default="#3688F9")
|
|
692
695
|
theme_secondary_color = serializers.CharField(default="#F18429")
|
|
693
696
|
theme_label_color = serializers.CharField(default="#616161")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
class StatisticsSerializer(serializers.Serializer):
|
|
700
|
+
|
|
701
|
+
domain_count = serializers.IntegerField()
|
|
702
|
+
domain_alias_count = serializers.IntegerField()
|
|
703
|
+
account_count = serializers.IntegerField()
|
|
704
|
+
alias_count = serializers.IntegerField()
|
modoboa/core/api/v2/tests.py
CHANGED
|
@@ -196,6 +196,18 @@ class AccountViewSetTestCase(ModoAPITestCase):
|
|
|
196
196
|
me = resp.json()
|
|
197
197
|
self.assertEqual(me["username"], "admin")
|
|
198
198
|
|
|
199
|
+
def test_update_me(self):
|
|
200
|
+
url = reverse("v2:account-me")
|
|
201
|
+
data = {
|
|
202
|
+
"first_name": "First name",
|
|
203
|
+
"secondary_email": "toto@iti.com",
|
|
204
|
+
"language": "fr",
|
|
205
|
+
}
|
|
206
|
+
resp = self.client.put(url, data, format="json")
|
|
207
|
+
self.assertEqual(resp.status_code, 200)
|
|
208
|
+
me = resp.json()
|
|
209
|
+
self.assertEqual(me["secondary_email"], data["secondary_email"])
|
|
210
|
+
|
|
199
211
|
def test_me_password(self, password_ko="Toto1234", password_ok="password"):
|
|
200
212
|
url = reverse("v2:account-check-me-password")
|
|
201
213
|
resp = self.client.post(url, {"password": password_ko}, format="json")
|
|
@@ -271,22 +283,22 @@ class AccountViewSetTestCase(ModoAPITestCase):
|
|
|
271
283
|
url = reverse("v2:account-available-applications")
|
|
272
284
|
resp = self.client.get(url)
|
|
273
285
|
self.assertEqual(resp.status_code, 200)
|
|
274
|
-
# admin -> only
|
|
275
|
-
self.assertEqual(len(resp.json()),
|
|
286
|
+
# admin -> only 2 apps.
|
|
287
|
+
self.assertEqual(len(resp.json()), 2)
|
|
276
288
|
|
|
277
289
|
# Domain admin with mailbox
|
|
278
290
|
dadmin = models.User.objects.get(username="admin@test.com")
|
|
279
291
|
self.authenticate_user(dadmin)
|
|
280
292
|
resp = self.client.get(url)
|
|
281
293
|
self.assertEqual(resp.status_code, 200)
|
|
282
|
-
self.assertEqual(len(resp.json()),
|
|
294
|
+
self.assertEqual(len(resp.json()), 5)
|
|
283
295
|
|
|
284
296
|
# Simple user
|
|
285
297
|
user = models.User.objects.get(username="user@test.com")
|
|
286
298
|
self.authenticate_user(user)
|
|
287
299
|
resp = self.client.get(url)
|
|
288
300
|
self.assertEqual(resp.status_code, 200)
|
|
289
|
-
self.assertEqual(len(resp.json()),
|
|
301
|
+
self.assertEqual(len(resp.json()), 4)
|
|
290
302
|
|
|
291
303
|
@override_settings(
|
|
292
304
|
MODOBOA_APPS=[
|
|
@@ -658,3 +670,21 @@ class NewsFeedAPIViewTestCase(ModoAPITestCase):
|
|
|
658
670
|
response = self.client.get(url)
|
|
659
671
|
self.assertEqual(response.status_code, 200)
|
|
660
672
|
self.assertIn("modoboa", response.json()[0]["link"])
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class StatisticsAPIViewTestCase(ModoAPITestCase):
|
|
676
|
+
|
|
677
|
+
@classmethod
|
|
678
|
+
def setUpTestData(cls):
|
|
679
|
+
"""Create test data."""
|
|
680
|
+
super().setUpTestData()
|
|
681
|
+
factories.populate_database()
|
|
682
|
+
|
|
683
|
+
def test_get_statistics(self):
|
|
684
|
+
url = reverse("v2:statistics")
|
|
685
|
+
response = self.client.get(url)
|
|
686
|
+
self.assertEqual(response.status_code, 200)
|
|
687
|
+
stats = response.json()
|
|
688
|
+
self.assertEqual(stats["domain_count"], 2)
|
|
689
|
+
self.assertEqual(stats["account_count"], 5)
|
|
690
|
+
self.assertEqual(stats["alias_count"], 3)
|
modoboa/core/api/v2/urls.py
CHANGED
|
@@ -32,16 +32,21 @@ urlpatterns += [
|
|
|
32
32
|
views.ComponentsInformationAPIView.as_view(),
|
|
33
33
|
name="components_information",
|
|
34
34
|
),
|
|
35
|
-
path(
|
|
36
|
-
"admin/notifications/",
|
|
37
|
-
views.NotificationsAPIView.as_view(),
|
|
38
|
-
name="notifications",
|
|
39
|
-
),
|
|
40
35
|
path(
|
|
41
36
|
"admin/news_feed/",
|
|
42
37
|
views.NewsFeedAPIView.as_view(),
|
|
43
38
|
name="news-feed",
|
|
44
39
|
),
|
|
40
|
+
path(
|
|
41
|
+
"admin/statistics/",
|
|
42
|
+
views.StatisticsAPIView.as_view(),
|
|
43
|
+
name="statistics",
|
|
44
|
+
),
|
|
45
45
|
path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
|
|
46
|
+
path(
|
|
47
|
+
"notifications/",
|
|
48
|
+
views.NotificationsAPIView.as_view(),
|
|
49
|
+
name="notifications",
|
|
50
|
+
),
|
|
46
51
|
path("theme/", views.ThemeAPIView.as_view(), name="theme"),
|
|
47
52
|
]
|
modoboa/core/api/v2/views.py
CHANGED
|
@@ -14,7 +14,8 @@ from drf_spectacular.utils import extend_schema
|
|
|
14
14
|
from rest_framework import permissions, response
|
|
15
15
|
from rest_framework.views import APIView
|
|
16
16
|
|
|
17
|
-
from modoboa.
|
|
17
|
+
from modoboa.admin import models as admin_models
|
|
18
|
+
from modoboa.core import models, signals
|
|
18
19
|
from modoboa.core.utils import check_for_updates, get_capabilities
|
|
19
20
|
from modoboa.lib.permissions import IsSuperUser, IsPrivilegedUser
|
|
20
21
|
from modoboa.lib.throttle import (
|
|
@@ -141,7 +142,9 @@ class PasswordResetConfirmView(APIView):
|
|
|
141
142
|
class ComponentsInformationAPIView(APIView):
|
|
142
143
|
"""Retrieve information about installed components."""
|
|
143
144
|
|
|
144
|
-
permission_classes = [
|
|
145
|
+
permission_classes = [
|
|
146
|
+
permissions.IsAuthenticated,
|
|
147
|
+
]
|
|
145
148
|
throttle_classes = [UserLesserDdosUser]
|
|
146
149
|
|
|
147
150
|
@extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
|
|
@@ -226,3 +229,21 @@ class NewsFeedAPIView(APIView):
|
|
|
226
229
|
|
|
227
230
|
serializer = serializers.NewsFeedEntrySerializer(entries, many=True)
|
|
228
231
|
return response.Response(serializer.data)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class StatisticsAPIView(APIView):
|
|
235
|
+
"""Return some statistics about this modoboa instance."""
|
|
236
|
+
|
|
237
|
+
permission_classes = [permissions.IsAuthenticated, IsSuperUser]
|
|
238
|
+
throttle_classes = [UserLesserDdosUser]
|
|
239
|
+
|
|
240
|
+
@extend_schema(responses=serializers.StatisticsSerializer())
|
|
241
|
+
def get(self, request, *args, **kwargs):
|
|
242
|
+
data = {
|
|
243
|
+
"domain_count": admin_models.Domain.objects.count(),
|
|
244
|
+
"domain_alias_count": admin_models.DomainAlias.objects.count(),
|
|
245
|
+
"account_count": models.User.objects.count(),
|
|
246
|
+
"alias_count": admin_models.Alias.objects.filter(internal=False).count(),
|
|
247
|
+
}
|
|
248
|
+
serializer = serializers.StatisticsSerializer(data)
|
|
249
|
+
return response.Response(serializer.data)
|
modoboa/core/api/v2/viewsets.py
CHANGED
|
@@ -48,11 +48,22 @@ def create_static_tokens(request):
|
|
|
48
48
|
class AccountViewSet(core_v1_viewsets.AccountViewSet):
|
|
49
49
|
"""Account viewset."""
|
|
50
50
|
|
|
51
|
-
@extend_schema(
|
|
52
|
-
|
|
51
|
+
@extend_schema(
|
|
52
|
+
responses=admin_v2_serializers.AccountMeSerializer,
|
|
53
|
+
request=admin_v2_serializers.AccountMeUpdateSerializer,
|
|
54
|
+
)
|
|
55
|
+
@action(methods=["get", "put"], detail=False)
|
|
53
56
|
def me(self, request):
|
|
54
57
|
"""Return information about connected user."""
|
|
55
|
-
|
|
58
|
+
if request.method == "PUT":
|
|
59
|
+
serializer = admin_v2_serializers.AccountMeUpdateSerializer(
|
|
60
|
+
data=request.data, instance=request.user
|
|
61
|
+
)
|
|
62
|
+
serializer.is_valid(raise_exception=True)
|
|
63
|
+
instance = serializer.save()
|
|
64
|
+
serializer = admin_v1_serializers.AccountSerializer(instance)
|
|
65
|
+
else:
|
|
66
|
+
serializer = admin_v1_serializers.AccountSerializer(request.user)
|
|
56
67
|
return response.Response(serializer.data)
|
|
57
68
|
|
|
58
69
|
@action(
|
|
@@ -166,6 +177,16 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
|
|
|
166
177
|
"url": "/admin",
|
|
167
178
|
}
|
|
168
179
|
)
|
|
180
|
+
if "modoboa.amavis" in settings.MODOBOA_APPS:
|
|
181
|
+
apps.append(
|
|
182
|
+
{
|
|
183
|
+
"name": "amavis",
|
|
184
|
+
"label": _("Quarantine"),
|
|
185
|
+
"icon": "mdi-server-security",
|
|
186
|
+
"description": _("Amavis quarantine"),
|
|
187
|
+
"url": "/user/quarantine",
|
|
188
|
+
}
|
|
189
|
+
)
|
|
169
190
|
if hasattr(request.user, "mailbox"):
|
|
170
191
|
if "modoboa.contacts" in settings.MODOBOA_APPS:
|
|
171
192
|
apps += [
|
modoboa/core/app_settings.py
CHANGED
|
@@ -720,6 +720,17 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
|
|
|
720
720
|
),
|
|
721
721
|
},
|
|
722
722
|
),
|
|
723
|
+
(
|
|
724
|
+
"message_history_maximum_age",
|
|
725
|
+
{
|
|
726
|
+
"label": gettext_lazy(
|
|
727
|
+
"Retention time in message history"
|
|
728
|
+
),
|
|
729
|
+
"help_text": gettext_lazy(
|
|
730
|
+
"Retention time (in days) of a message in the message history section"
|
|
731
|
+
),
|
|
732
|
+
},
|
|
733
|
+
),
|
|
723
734
|
(
|
|
724
735
|
"items_per_page",
|
|
725
736
|
{
|
modoboa/core/fido2_auth.py
CHANGED
|
@@ -34,10 +34,9 @@ def begin_registration(request):
|
|
|
34
34
|
),
|
|
35
35
|
list(get_creds_from_user(request.user.pk).values()),
|
|
36
36
|
user_verification=UserVerificationRequirement.DISCOURAGED,
|
|
37
|
-
extensions={"credentialProtectionPolicy": "userVerificationOptional"},
|
|
38
37
|
)
|
|
39
38
|
request.session["fido2_state"] = state
|
|
40
|
-
return options
|
|
39
|
+
return dict(options)
|
|
41
40
|
|
|
42
41
|
|
|
43
42
|
def end_registration(request):
|
modoboa/core/handlers.py
CHANGED
|
@@ -125,7 +125,9 @@ def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
|
|
|
125
125
|
{
|
|
126
126
|
"id": "newversionavailable",
|
|
127
127
|
"text": _("One or more updates are available"),
|
|
128
|
-
"
|
|
128
|
+
"color": "info",
|
|
129
|
+
"url": "/admin/information",
|
|
130
|
+
"target": "admin",
|
|
129
131
|
}
|
|
130
132
|
]
|
|
131
133
|
elif include_all:
|
|
@@ -137,7 +139,9 @@ def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
|
|
|
137
139
|
"id": "deprecatedpasswordscheme",
|
|
138
140
|
"text": _("You are still using a deprecated password scheme (%s)")
|
|
139
141
|
% hasher.name,
|
|
140
|
-
"
|
|
142
|
+
"color": "warning",
|
|
143
|
+
"url": "/admin/information",
|
|
144
|
+
"target": "admin",
|
|
141
145
|
}
|
|
142
146
|
]
|
|
143
147
|
return result
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
2
|
+
|
|
3
|
+
from oauth2_provider.models import get_application_model
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(BaseCommand):
|
|
7
|
+
"""Command class."""
|
|
8
|
+
|
|
9
|
+
help = "Add new allowed hosts to frontend Oauth2 application."
|
|
10
|
+
|
|
11
|
+
def add_arguments(self, parser):
|
|
12
|
+
parser.add_argument("hostnames", type=str, nargs="+")
|
|
13
|
+
|
|
14
|
+
def handle(self, *args, **options):
|
|
15
|
+
app_model = get_application_model()
|
|
16
|
+
app = app_model.objects.filter(name="modoboa_frontend").first()
|
|
17
|
+
if not app:
|
|
18
|
+
raise CommandError(
|
|
19
|
+
"Application modoboa_frontend not found. "
|
|
20
|
+
"Make sure you ran load_initial_data first."
|
|
21
|
+
)
|
|
22
|
+
redirect_uris = app.redirect_uris.split(" ")
|
|
23
|
+
post_logout_redirect_uris = app.post_logout_redirect_uris.split(" ")
|
|
24
|
+
for hostname in options["hostnames"]:
|
|
25
|
+
uri = f"https://{hostname}/login/logged"
|
|
26
|
+
if uri not in redirect_uris:
|
|
27
|
+
redirect_uris.append(uri)
|
|
28
|
+
uri = f"https://{hostname}"
|
|
29
|
+
if uri not in post_logout_redirect_uris:
|
|
30
|
+
post_logout_redirect_uris.append(uri)
|
|
31
|
+
app.redirect_uris = " ".join(redirect_uris)
|
|
32
|
+
app.post_logout_redirect_uris = " ".join(post_logout_redirect_uris)
|
|
33
|
+
app.save()
|
|
@@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand
|
|
|
5
5
|
from django.utils import timezone
|
|
6
6
|
|
|
7
7
|
from modoboa.core.models import Log
|
|
8
|
+
from modoboa.maillog.models import Maillog
|
|
8
9
|
from modoboa.parameters import tools as param_tools
|
|
9
10
|
|
|
10
11
|
|
|
@@ -41,4 +42,12 @@ class Command(BaseCommand):
|
|
|
41
42
|
self.__vprint(f"Deleting logs older than {log_maximum_age} days...")
|
|
42
43
|
limit = timezone.now() - datetime.timedelta(log_maximum_age)
|
|
43
44
|
Log.objects.filter(date_created__lt=limit).delete()
|
|
45
|
+
message_history_maximum_age = param_tools.get_global_parameter(
|
|
46
|
+
"message_history_maximum_age"
|
|
47
|
+
)
|
|
48
|
+
self.__vprint(
|
|
49
|
+
f"Deleting messages in history older than {message_history_maximum_age} days..."
|
|
50
|
+
)
|
|
51
|
+
limit = timezone.now() - datetime.timedelta(message_history_maximum_age)
|
|
52
|
+
Maillog.objects.filter(date__lt=limit).delete()
|
|
44
53
|
self.__vprint("Done.")
|
|
@@ -192,3 +192,13 @@ class Command(BaseCommand):
|
|
|
192
192
|
}}
|
|
193
193
|
"""
|
|
194
194
|
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ADD SIGNAL FOR THAT
|
|
198
|
+
# def load_initial_data(self):
|
|
199
|
+
# """Create records for existing domains and co."""
|
|
200
|
+
# for dom in Domain.objects.all():
|
|
201
|
+
# policy = create_user_and_policy("@{0}".format(dom.name))
|
|
202
|
+
# for domalias in dom.domainalias_set.all():
|
|
203
|
+
# domalias_pattern = "@{0}".format(domalias.name)
|
|
204
|
+
# create_user_and_use_policy(domalias_pattern, policy)
|
|
@@ -3,15 +3,33 @@
|
|
|
3
3
|
from django.db import migrations
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
class RenameIndexIfExists(migrations.RenameIndex):
|
|
7
|
+
|
|
8
|
+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
|
9
|
+
from_model = from_state.apps.get_model(app_label, self.model_name)
|
|
10
|
+
columns = [
|
|
11
|
+
from_model._meta.get_field(field).column for field in ["email", "is_active"]
|
|
12
|
+
]
|
|
13
|
+
matching_index_name = schema_editor._constraint_names(
|
|
14
|
+
from_model,
|
|
15
|
+
column_names=columns,
|
|
16
|
+
index=True,
|
|
17
|
+
unique=False,
|
|
18
|
+
)
|
|
19
|
+
if len(matching_index_name) != 1:
|
|
20
|
+
return
|
|
21
|
+
super().database_forwards(app_label, schema_editor, from_state, to_state)
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
class Migration(migrations.Migration):
|
|
7
25
|
dependencies = [
|
|
8
26
|
("core", "0024_alter_user_language"),
|
|
9
27
|
]
|
|
10
28
|
|
|
11
29
|
operations = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
RenameIndexIfExists(
|
|
31
|
+
model_name="user",
|
|
32
|
+
new_name="core_user_email_c0c03f_idx",
|
|
33
|
+
old_fields=("email", "is_active"),
|
|
34
|
+
),
|
|
17
35
|
]
|