modoboa 2.4.11__py3-none-any.whl → 2.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modoboa/admin/api/v2/serializers.py +14 -0
- modoboa/amavis/__init__.py +3 -0
- modoboa/amavis/app_settings.py +276 -0
- modoboa/amavis/apps.py +18 -0
- modoboa/amavis/checks/__init__.py +2 -0
- modoboa/amavis/checks/settings_checks.py +59 -0
- modoboa/amavis/dbrouter.py +35 -0
- modoboa/amavis/factories.py +164 -0
- modoboa/amavis/handlers.py +146 -0
- modoboa/amavis/lib.py +381 -0
- modoboa/amavis/management/__init__.py +0 -0
- modoboa/amavis/management/commands/__init__.py +0 -0
- modoboa/amavis/management/commands/amnotify.py +99 -0
- modoboa/amavis/management/commands/qcleanup.py +84 -0
- modoboa/amavis/migrations/0001_initial.py +340 -0
- modoboa/amavis/migrations/__init__.py +0 -0
- modoboa/amavis/models.py +226 -0
- modoboa/amavis/serializers.py +139 -0
- modoboa/amavis/sql_connector.py +240 -0
- modoboa/amavis/sql_email.py +66 -0
- modoboa/amavis/tasks.py +33 -0
- modoboa/amavis/templates/amavis/notifications/pending_requests.html +16 -0
- modoboa/amavis/tests/__init__.py +0 -0
- modoboa/amavis/tests/sa-learn +3 -0
- modoboa/amavis/tests/sample_messages/quarantined-input.txt +80 -0
- modoboa/amavis/tests/sample_messages/quarantined-output-plain_nolinks.txt +17 -0
- modoboa/amavis/tests/spamc +3 -0
- modoboa/amavis/tests/test_checks.py +25 -0
- modoboa/amavis/tests/test_handlers.py +214 -0
- modoboa/amavis/tests/test_lib.py +90 -0
- modoboa/amavis/tests/test_management_commands.py +45 -0
- modoboa/amavis/tests/test_sql_email.py +67 -0
- modoboa/amavis/tests/test_utils.py +19 -0
- modoboa/amavis/tests/test_viewsets.py +319 -0
- modoboa/amavis/urls.py +11 -0
- modoboa/amavis/utils.py +105 -0
- modoboa/amavis/viewsets.py +265 -0
- modoboa/core/api/v1/serializers.py +7 -5
- modoboa/core/api/v2/serializers.py +4 -2
- modoboa/core/api/v2/tests.py +16 -4
- modoboa/core/api/v2/urls.py +5 -5
- modoboa/core/api/v2/views.py +6 -2
- modoboa/core/api/v2/viewsets.py +24 -3
- modoboa/core/commands/deploy.py +3 -0
- modoboa/core/commands/templates/settings.py.tpl +12 -11
- modoboa/core/handlers.py +6 -2
- modoboa/core/management/commands/add_allowed_hosts.py +33 -0
- modoboa/core/management/commands/load_initial_data.py +10 -0
- modoboa/core/migrations/0025_rename_user_email_is_active_core_user_email_c0c03f_idx.py +23 -5
- modoboa/core/tests/test_core.py +24 -0
- modoboa/core/utils.py +3 -0
- modoboa/frontend_dist/assets/AccountAliasForm-C0oHHyZL.js +1 -0
- modoboa/frontend_dist/assets/AccountEditView-lgSJ2Se8.js +1 -0
- modoboa/frontend_dist/assets/AccountLayout-U386K8zy.js +1 -0
- modoboa/frontend_dist/assets/AccountPasswordSubForm-YsaE_cDx.js +1 -0
- modoboa/frontend_dist/assets/AccountView-1jfKFDwb.js +1 -0
- modoboa/frontend_dist/assets/AddressBook-CwN64Zls.js +1 -0
- modoboa/frontend_dist/assets/AdminLayout-Cxm1lggg.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +1 -0
- modoboa/frontend_dist/assets/AlarmsView-Bcjsicac.js +1 -0
- modoboa/frontend_dist/assets/AliasEditView-k3rVt1tG.js +1 -0
- modoboa/frontend_dist/assets/{AliasRecipientForm-IOae6sjF.js → AliasRecipientForm-IehUzKok.js} +1 -1
- modoboa/frontend_dist/assets/AliasView-DMzA10eD.js +1 -0
- modoboa/frontend_dist/assets/AuditTrailView-5dnGX5El.js +1 -0
- modoboa/frontend_dist/assets/CalendarView-DZONeDA9.js +1 -0
- modoboa/frontend_dist/assets/{ChoiceField-DJ_c78Cm.js → ChoiceField-DnwXRkht.js} +1 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-kghmfNuE.js +1 -0
- modoboa/frontend_dist/assets/ComposeEmailView-DLv3wk1k.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-CcPrCKuI.js +1 -0
- modoboa/frontend_dist/assets/{ConnectedLayout-Dvwmicnc.css → ConnectedLayout-Bxh21hcH.css} +1 -1
- modoboa/frontend_dist/assets/ConnectedLayout-CWlBK7Hf.js +1 -0
- modoboa/frontend_dist/assets/CreationForm-CW4lxnPg.js +1 -0
- modoboa/frontend_dist/assets/DashboardView-DXVZMbMo.js +1 -0
- modoboa/frontend_dist/assets/DomainAdminList-C3jcDDc3.js +1 -0
- modoboa/frontend_dist/assets/DomainEditView-ph8AaElX.js +1 -0
- modoboa/frontend_dist/assets/{DomainTransportForm-C2xo0Yd7.js → DomainTransportForm-NCz6Bl-h.js} +1 -1
- modoboa/frontend_dist/assets/DomainView-BgMSSuU-.js +5 -0
- modoboa/frontend_dist/assets/{DomainView-BDKoBFYr.css → DomainView-CCLYXPHx.css} +1 -1
- modoboa/frontend_dist/assets/DomainsView-CEEU9btK.js +1 -0
- modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +1 -0
- modoboa/frontend_dist/assets/EmailField-DeqDPm5j.js +1 -0
- modoboa/frontend_dist/assets/EmailView-DczVhVO0.js +1 -0
- modoboa/frontend_dist/assets/EmptyLayout-BXgcfMLH.js +1 -0
- modoboa/frontend_dist/assets/FiltersView-nJj_gSCx.js +1 -0
- modoboa/frontend_dist/assets/ForwardEmailView-Bgv3JQb6.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-CJ9umKeO.js → HtmlEditor-BWRdelVw.js} +1 -1
- modoboa/frontend_dist/assets/{IdentitiesView-0ziuQ5s-.css → IdentitiesView-DPrrRMS5.css} +1 -1
- modoboa/frontend_dist/assets/IdentitiesView-Dld9IloZ.js +1 -0
- modoboa/frontend_dist/assets/InformationView-BBWKSX8D.js +1 -0
- modoboa/frontend_dist/assets/InformationView-C9vvvQhJ.css +1 -0
- modoboa/frontend_dist/assets/{LoadingData-CYwX3Jpn.js → LoadingData-G57nJ_JV.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-E01qkKn0.js → LoginCallbackView-DjyE2SG_.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-Cy4uFV9h.js → LoginView-CqCCXYLo.js} +1 -1
- modoboa/frontend_dist/assets/{MailboxView-B-aI4XBq.css → MailboxView-CfStlWhk.css} +1 -1
- modoboa/frontend_dist/assets/MailboxView-DRrs9eLO.js +1 -0
- modoboa/frontend_dist/assets/MenuItems-BqIZW5av.js +1 -0
- modoboa/frontend_dist/assets/MessageView-D_6tx_gd.js +1 -0
- modoboa/frontend_dist/assets/MessagesView-BH7JIR03.js +1 -0
- modoboa/frontend_dist/assets/MigrationsView-Cv_So9T-.js +1 -0
- modoboa/frontend_dist/assets/{ParametersForm-BZM0QSvg.js → ParametersForm-3qXttTuQ.js} +1 -1
- modoboa/frontend_dist/assets/ParametersView-3Ns04cpQ.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-B5B5Dt6K.js +1 -0
- modoboa/frontend_dist/assets/ProviderEditView-zh7CY832.js +1 -0
- modoboa/frontend_dist/assets/ProviderGeneralForm-BQU7t3ma.js +1 -0
- modoboa/frontend_dist/assets/ProvidersView-CoF_ZkZA.js +1 -0
- modoboa/frontend_dist/assets/QuarantineLayout-CYBsrbJM.js +1 -0
- modoboa/frontend_dist/assets/QuarantineView-D4gOE4EQ.css +1 -0
- modoboa/frontend_dist/assets/QuarantineView-DNvpoycb.js +1 -0
- modoboa/frontend_dist/assets/ReplyEmailView-D1XTcglu.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-BW8rUGgZ.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-DfDHiYeX.js +1 -0
- modoboa/frontend_dist/assets/{SettingsView-BxLJBFY0.js → SettingsView-gQiJ2NVb.js} +2 -2
- modoboa/frontend_dist/assets/StatisticsView-DYalet_q.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-BZ2htbFk.js +1 -0
- modoboa/frontend_dist/assets/UserLayout-zUtHi-z-.js +1 -0
- modoboa/frontend_dist/assets/VAlert-4r6LxKtg.js +1 -0
- modoboa/frontend_dist/assets/VApp-CX_C7AUN.js +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-DNKmBvyZ.js +1 -0
- modoboa/frontend_dist/assets/VAvatar-DbuoZWmf.js +1 -0
- modoboa/frontend_dist/assets/VBadge-BQrRJ9S0.css +1 -0
- modoboa/frontend_dist/assets/VBadge-Bv2nvUmC.js +1 -0
- modoboa/frontend_dist/assets/VCard-DzjUT5OP.js +1 -0
- modoboa/frontend_dist/assets/VCheckbox-dr7UFjl4.js +1 -0
- modoboa/frontend_dist/assets/{VCheckboxBtn-Dt810gWf.js → VCheckboxBtn-CpFdBnTv.js} +1 -1
- modoboa/frontend_dist/assets/VChip-CaQvfmkw.js +1 -0
- modoboa/frontend_dist/assets/VColorPicker-ByGpCW5O.js +1 -0
- modoboa/frontend_dist/assets/{VContainer-DvTbsotR.js → VContainer-74Dnn8Ux.js} +1 -1
- modoboa/frontend_dist/assets/VDataTable-CL7yHvG7.js +1 -0
- modoboa/frontend_dist/assets/VDataTableServer-BqvNcIdw.js +1 -0
- modoboa/frontend_dist/assets/VDataTableVirtual--KsOP8i6.js +1 -0
- modoboa/frontend_dist/assets/{VDialog-Bk6EWNhz.js → VDialog-DmTGCGR0.js} +1 -1
- modoboa/frontend_dist/assets/VExpansionPanels-B7sSTCwd.js +1 -0
- modoboa/frontend_dist/assets/VFileInput-SULIc6F3.js +1 -0
- modoboa/frontend_dist/assets/VForm-DsRLc-sa.js +1 -0
- modoboa/frontend_dist/assets/VInput-CcxkaOXT.css +1 -0
- modoboa/frontend_dist/assets/VInput-DVKUObZe.js +1 -0
- modoboa/frontend_dist/assets/VMenu-nv0XOgg0.js +1 -0
- modoboa/frontend_dist/assets/VPicker-DnDSWJHJ.js +1 -0
- modoboa/frontend_dist/assets/VProgressCircular-qK6p5X_Y.js +1 -0
- modoboa/frontend_dist/assets/VRadioGroup-CbiPLy0t.js +1 -0
- modoboa/frontend_dist/assets/{VRow-BF35mT1S.js → VRow-DJ0NB63-.js} +1 -1
- modoboa/frontend_dist/assets/VSelect-CxCFMHyF.js +1 -0
- modoboa/frontend_dist/assets/VSelectionControl-C-6A4us5.js +1 -0
- modoboa/frontend_dist/assets/VSheet-DI6SxLnG.js +1 -0
- modoboa/frontend_dist/assets/VSpacer-CoJVmx8k.js +1 -0
- modoboa/frontend_dist/assets/VSwitch-DPnjPQuU.js +1 -0
- modoboa/frontend_dist/assets/VTable-ldTxgQPW.js +1 -0
- modoboa/frontend_dist/assets/VTabs-aS8WSL9I.js +1 -0
- modoboa/frontend_dist/assets/VTextField-BzBVKKob.css +1 -0
- modoboa/frontend_dist/assets/VTextField-DKbr4H5w.js +1 -0
- modoboa/frontend_dist/assets/VTextarea-BttkFsM4.js +1 -0
- modoboa/frontend_dist/assets/VToolbar-BxX3W2kR.js +1 -0
- modoboa/frontend_dist/assets/VWindowItem-Cvqvdegd.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-BT2k6U7q.js +1 -0
- modoboa/frontend_dist/assets/accounts-CC2F0a0c.js +1 -0
- modoboa/frontend_dist/assets/{admin-o-HRGnmT.js → admin-CHCHFGI6.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-DDVeehyg.js → aliases-C9bUD4Ws.js} +1 -1
- modoboa/frontend_dist/assets/amavis-BhzV4rgf.js +1 -0
- modoboa/frontend_dist/assets/amavis-DCVJxuui.js +1 -0
- modoboa/frontend_dist/assets/{contacts-C84DY-Q1.js → contacts-Dxz6eWpf.js} +1 -1
- modoboa/frontend_dist/assets/{domains-Bgn4ixHL.js → domains-C2cornvL.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-DTE-V7Y1.js → domains.store-BLKRipG8.js} +1 -1
- modoboa/frontend_dist/assets/{filter-CnffiQAW.js → filter-rmxrcjKk.js} +1 -1
- modoboa/frontend_dist/assets/forwardRefs-CpzzjgpX.js +1 -0
- modoboa/frontend_dist/assets/global.store-DndbMXYb.js +1 -0
- modoboa/frontend_dist/assets/{importExport-BlQYb0NO.js → importExport-C3uqrcok.js} +1 -1
- modoboa/frontend_dist/assets/index-LhNzkzAh.js +984 -0
- modoboa/frontend_dist/assets/layout-DbjDe3Wl.js +1 -0
- modoboa/frontend_dist/assets/{layout.store-DkjrAoXt.js → layout.store-Vq5mvIp7.js} +1 -1
- modoboa/frontend_dist/assets/{logos-q8SEyAa4.js → logos-Bvcy0usu.js} +1 -1
- modoboa/frontend_dist/assets/{logs-B7IJ7LBa.js → logs-BuItINky.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-A6iBEYQq.js → parameters-C8IYEP7q.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-BiXS4_6w.js → parameters.store-1cwSP2JP.js} +1 -1
- modoboa/frontend_dist/assets/permissions-DQjAcO9S.js +1 -0
- modoboa/frontend_dist/assets/{ssrBoot-AzTdjPjk.js → ssrBoot-BxIQ9ccA.js} +1 -1
- modoboa/frontend_dist/assets/{tag-BnSYRTcD.js → tag-3cfI1_f7.js} +1 -1
- modoboa/frontend_dist/assets/transports-D4Jk4-AP.js +1 -0
- modoboa/frontend_dist/assets/{webmail-CSH_3l6R.js → webmail-B2IUjaxM.js} +1 -1
- modoboa/frontend_dist/index.html +1 -1
- modoboa/lib/email_utils.py +2 -2
- modoboa/lib/permissions.py +7 -0
- modoboa/lib/test_runners.py +29 -0
- modoboa/lib/tests/__init__.py +5 -1
- modoboa/locale/br/LC_MESSAGES/django.po +87 -75
- modoboa/locale/cs/LC_MESSAGES/django.po +82 -74
- modoboa/locale/cs_CZ/LC_MESSAGES/django.po +145 -121
- modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de/LC_MESSAGES/django.po +339 -651
- modoboa/locale/de_DE/LC_MESSAGES/django.po +87 -75
- modoboa/locale/el_GR/LC_MESSAGES/django.po +160 -135
- modoboa/locale/en/LC_MESSAGES/django.po +82 -74
- modoboa/locale/es/LC_MESSAGES/django.po +158 -131
- modoboa/locale/es_MX/LC_MESSAGES/django.po +82 -74
- modoboa/locale/fi/LC_MESSAGES/django.po +87 -75
- modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fr/LC_MESSAGES/django.po +469 -201
- modoboa/locale/hu/LC_MESSAGES/django.po +82 -74
- modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/it/LC_MESSAGES/django.po +148 -122
- modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ja_JP/LC_MESSAGES/django.po +80 -72
- modoboa/locale/ka/LC_MESSAGES/django.po +82 -74
- modoboa/locale/nl_NL/LC_MESSAGES/django.po +160 -132
- modoboa/locale/no/LC_MESSAGES/django.po +82 -74
- modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pl_PL/LC_MESSAGES/django.po +172 -149
- modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_BR/LC_MESSAGES/django.po +172 -144
- modoboa/locale/pt_PT/LC_MESSAGES/django.po +135 -112
- modoboa/locale/ro_RO/LC_MESSAGES/django.po +87 -75
- modoboa/locale/ru/LC_MESSAGES/django.po +142 -118
- modoboa/locale/si/LC_MESSAGES/django.po +82 -74
- modoboa/locale/sk/LC_MESSAGES/django.po +82 -74
- modoboa/locale/sk_SK/LC_MESSAGES/django.po +84 -76
- modoboa/locale/sl_SI/LC_MESSAGES/django.po +90 -76
- modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/sv/LC_MESSAGES/django.po +172 -139
- modoboa/locale/tr/LC_MESSAGES/django.po +87 -75
- modoboa/locale/tr_TR/LC_MESSAGES/django.po +84 -74
- modoboa/locale/uk/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh_CN/LC_MESSAGES/django.po +82 -74
- modoboa/locale/zh_TW/LC_MESSAGES/django.po +87 -75
- modoboa/parameters/api/v2/tests.py +2 -2
- modoboa/parameters/api/v2/viewsets.py +2 -0
- modoboa/policyd/tests.py +2 -0
- modoboa/urls_api_v2.py +6 -0
- modoboa/webmail/lib/imaputils.py +2 -2
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/METADATA +6 -4
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/RECORD +235 -185
- modoboa/frontend_dist/assets/AccountAliasForm-BV6KvTu6.js +0 -1
- modoboa/frontend_dist/assets/AccountEditView-DDOFyfBD.js +0 -1
- modoboa/frontend_dist/assets/AccountLayout-rX51xgxT.js +0 -1
- modoboa/frontend_dist/assets/AccountPasswordSubForm-D9S6LaeH.js +0 -1
- modoboa/frontend_dist/assets/AccountView-cmvaZNq3.js +0 -1
- modoboa/frontend_dist/assets/AddressBook-DCJxL8SU.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-r0wfG2lO.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-Bheey-gp.css +0 -1
- modoboa/frontend_dist/assets/AlarmsView-C0bqC4PA.js +0 -1
- modoboa/frontend_dist/assets/AliasEditView-DVoWoCGY.js +0 -1
- modoboa/frontend_dist/assets/AliasView-DrONZXOh.js +0 -1
- modoboa/frontend_dist/assets/AuditTrailView-OTkoZaMU.js +0 -1
- modoboa/frontend_dist/assets/CalendarView-CqF4_Ui9.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-DO5_GB3e.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-A91HCBsN.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-BBcgdAnO.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-1oRW-Rql.js +0 -1
- modoboa/frontend_dist/assets/CreationForm-71YJbjsA.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-CdLpSfUl.js +0 -1
- modoboa/frontend_dist/assets/DomainAdminList-BjC4KsqI.js +0 -1
- modoboa/frontend_dist/assets/DomainEditView-CQjKwYxl.js +0 -1
- modoboa/frontend_dist/assets/DomainView-BhhuZI_N.js +0 -5
- modoboa/frontend_dist/assets/DomainsView-Cft4BP8Z.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-DasJ0NdZ.css +0 -1
- modoboa/frontend_dist/assets/EmailField-C8umy0EU.js +0 -1
- modoboa/frontend_dist/assets/EmailView-ki7uEQPD.js +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-DaA1XH9n.js +0 -1
- modoboa/frontend_dist/assets/FiltersView-FYFZxG4B.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-cUbnSYCF.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-njNo8N5n.js +0 -1
- modoboa/frontend_dist/assets/InformationView-D1h38POt.js +0 -1
- modoboa/frontend_dist/assets/InformationView-U5Ww-Sx1.css +0 -1
- modoboa/frontend_dist/assets/MailboxView-IlrLWm_H.js +0 -1
- modoboa/frontend_dist/assets/MenuItems-BAtHWzAE.js +0 -1
- modoboa/frontend_dist/assets/MessagesView-OSpjixFq.js +0 -1
- modoboa/frontend_dist/assets/MigrationsView-DKNOsVzF.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-C4bXASiq.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-CYXgNmc1.js +0 -1
- modoboa/frontend_dist/assets/ProviderEditView-CyxCWTST.js +0 -1
- modoboa/frontend_dist/assets/ProviderGeneralForm-BYPjNHqB.js +0 -1
- modoboa/frontend_dist/assets/ProvidersView-CxrMkRyk.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-Dkw9-N26.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-CuUvrOdY.js +0 -1
- modoboa/frontend_dist/assets/StatisticsView-BN7QsZMT.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-BMN8BeFZ.js +0 -1
- modoboa/frontend_dist/assets/UserLayout-B6-JQg4F.js +0 -1
- modoboa/frontend_dist/assets/VAlert-DIQTrRif.js +0 -1
- modoboa/frontend_dist/assets/VApp-CpkYA7js.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-C4IpXyl8.js +0 -1
- modoboa/frontend_dist/assets/VAvatar-Lpb-Dion.js +0 -1
- modoboa/frontend_dist/assets/VCard-er_isjE_.js +0 -1
- modoboa/frontend_dist/assets/VCheckbox-D-u8JXP1.js +0 -1
- modoboa/frontend_dist/assets/VChip-B4iSpj8_.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-BAjGDsXv.js +0 -1
- modoboa/frontend_dist/assets/VDataTable-4JRjbtgF.js +0 -1
- modoboa/frontend_dist/assets/VDataTableServer-tIDT1m3-.js +0 -1
- modoboa/frontend_dist/assets/VDataTableVirtual-BlnO18u_.js +0 -1
- modoboa/frontend_dist/assets/VExpansionPanels-CwGtXDhr.js +0 -1
- modoboa/frontend_dist/assets/VFileInput-D1_7ZkO_.js +0 -1
- modoboa/frontend_dist/assets/VForm-DAkW4nfy.js +0 -1
- modoboa/frontend_dist/assets/VMenu-BPFJwj2f.js +0 -1
- modoboa/frontend_dist/assets/VPicker-CfT82M8N.js +0 -1
- modoboa/frontend_dist/assets/VProgressCircular-w75-3ogi.js +0 -1
- modoboa/frontend_dist/assets/VRadioGroup-0j6DNC_k.js +0 -1
- modoboa/frontend_dist/assets/VSelect-Cs4ARbAS.js +0 -1
- modoboa/frontend_dist/assets/VSelectionControl-Dg-XyRRS.js +0 -1
- modoboa/frontend_dist/assets/VSheet-Btq_Mu4s.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-C7xukQmu.js +0 -1
- modoboa/frontend_dist/assets/VSwitch-Cs1NQrmk.js +0 -1
- modoboa/frontend_dist/assets/VTable-CNz2SGk4.js +0 -1
- modoboa/frontend_dist/assets/VTabs-B1fyVn4M.js +0 -1
- modoboa/frontend_dist/assets/VTextField-BdyvgvkG.js +0 -1
- modoboa/frontend_dist/assets/VTextField-C-J20yj_.css +0 -1
- modoboa/frontend_dist/assets/VTextarea-DnOMpe0Q.js +0 -1
- modoboa/frontend_dist/assets/VToolbar-BiCiBxBJ.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-ChWm_kz3.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-o4uEkp9e.js +0 -1
- modoboa/frontend_dist/assets/forwardRefs-Dvjn_Xq4.js +0 -1
- modoboa/frontend_dist/assets/global.store-BaiD63EN.js +0 -1
- modoboa/frontend_dist/assets/index-I1VDlN4g.js +0 -984
- modoboa/frontend_dist/assets/layout-D8ZJPiJ_.js +0 -1
- modoboa/frontend_dist/assets/permissions-CITHLHVg.js +0 -1
- modoboa/frontend_dist/assets/transports-Dz7c6kIy.js +0 -1
- {modoboa-2.4.11.data → modoboa-2.5.1.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/WHEEL +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/entry_points.txt +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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
|
|
@@ -664,9 +664,11 @@ class NotificationSerializer(serializers.Serializer):
|
|
|
664
664
|
"""Serializer used to render a notification."""
|
|
665
665
|
|
|
666
666
|
id = serializers.CharField()
|
|
667
|
-
url = serializers.CharField(required=False)
|
|
668
667
|
text = serializers.CharField()
|
|
669
|
-
|
|
668
|
+
color = serializers.CharField()
|
|
669
|
+
target = serializers.CharField()
|
|
670
|
+
url = serializers.CharField(required=False)
|
|
671
|
+
counter = serializers.IntegerField(required=False)
|
|
670
672
|
|
|
671
673
|
|
|
672
674
|
class ModoboaApplicationSerializer(serializers.Serializer):
|
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=[
|
modoboa/core/api/v2/urls.py
CHANGED
|
@@ -32,11 +32,6 @@ 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(),
|
|
@@ -48,5 +43,10 @@ urlpatterns += [
|
|
|
48
43
|
name="statistics",
|
|
49
44
|
),
|
|
50
45
|
path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
|
|
46
|
+
path(
|
|
47
|
+
"notifications/",
|
|
48
|
+
views.NotificationsAPIView.as_view(),
|
|
49
|
+
name="notifications",
|
|
50
|
+
),
|
|
51
51
|
path("theme/", views.ThemeAPIView.as_view(), name="theme"),
|
|
52
52
|
]
|
modoboa/core/api/v2/views.py
CHANGED
|
@@ -142,7 +142,9 @@ class PasswordResetConfirmView(APIView):
|
|
|
142
142
|
class ComponentsInformationAPIView(APIView):
|
|
143
143
|
"""Retrieve information about installed components."""
|
|
144
144
|
|
|
145
|
-
permission_classes = [
|
|
145
|
+
permission_classes = [
|
|
146
|
+
permissions.IsAuthenticated,
|
|
147
|
+
]
|
|
146
148
|
throttle_classes = [UserLesserDdosUser]
|
|
147
149
|
|
|
148
150
|
@extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
|
|
@@ -155,7 +157,9 @@ class ComponentsInformationAPIView(APIView):
|
|
|
155
157
|
class NotificationsAPIView(APIView):
|
|
156
158
|
"""Return list of active notifications."""
|
|
157
159
|
|
|
158
|
-
permission_classes = [
|
|
160
|
+
permission_classes = [
|
|
161
|
+
permissions.IsAuthenticated,
|
|
162
|
+
]
|
|
159
163
|
throttle_classes = [UserLesserDdosUser]
|
|
160
164
|
|
|
161
165
|
@extend_schema(responses=serializers.NotificationSerializer(many=True))
|
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/commands/deploy.py
CHANGED
|
@@ -218,6 +218,7 @@ class DeployCommand(Command):
|
|
|
218
218
|
allowed_host = "localhost"
|
|
219
219
|
extra_settings = []
|
|
220
220
|
extensions = parsed_args.extensions
|
|
221
|
+
amavis_enabled = False
|
|
221
222
|
if extensions:
|
|
222
223
|
if "all" in extensions:
|
|
223
224
|
extensions = self._get_extension_list()
|
|
@@ -233,6 +234,7 @@ class DeployCommand(Command):
|
|
|
233
234
|
exec_cmd(cmd, capture_output=False)
|
|
234
235
|
extra_settings = self.find_extra_settings(extensions)
|
|
235
236
|
extensions = [extension[1] for extension in extensions]
|
|
237
|
+
amavis_enabled = "modoboa.amavis" in extensions
|
|
236
238
|
|
|
237
239
|
mod = __import__(parsed_args.name, globals(), locals(), [smart_str("settings")])
|
|
238
240
|
tpl = self._render_template(
|
|
@@ -247,6 +249,7 @@ class DeployCommand(Command):
|
|
|
247
249
|
"devmode": parsed_args.devel,
|
|
248
250
|
"extensions": extensions,
|
|
249
251
|
"extra_settings": extra_settings,
|
|
252
|
+
"amavis_enabled": amavis_enabled,
|
|
250
253
|
},
|
|
251
254
|
)
|
|
252
255
|
with open(f"{path}/settings.py", "w") as fp:
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Django settings for {{ name }} project.
|
|
3
3
|
|
|
4
4
|
For more information on this file, see
|
|
5
|
-
https://docs.djangoproject.com/en/
|
|
5
|
+
https://docs.djangoproject.com/en/dev/topics/settings/
|
|
6
6
|
|
|
7
7
|
For the full list of settings and their values, see
|
|
8
|
-
https://docs.djangoproject.com/en/
|
|
8
|
+
https://docs.djangoproject.com/en/dev/ref/settings/
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from logging.handlers import SysLogHandler
|
|
@@ -17,9 +17,6 @@ env = environ.Env()
|
|
|
17
17
|
BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
|
|
18
18
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
|
19
19
|
|
|
20
|
-
# Quick-start development settings - unsuitable for production
|
|
21
|
-
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
|
22
|
-
|
|
23
20
|
# SECURITY WARNING: keep the secret key used in production secret!
|
|
24
21
|
SECRET_KEY = '{{ secret_key }}'
|
|
25
22
|
|
|
@@ -157,14 +154,14 @@ WSGI_APPLICATION = '{{ name }}.wsgi.application'
|
|
|
157
154
|
|
|
158
155
|
|
|
159
156
|
# Database
|
|
160
|
-
# https://docs.djangoproject.com/en/
|
|
157
|
+
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
|
161
158
|
|
|
162
159
|
DATABASES = {
|
|
163
160
|
{% for conn in db_connections.values %}{{ conn|safe }}{% endfor %}
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
# Internationalization
|
|
167
|
-
# https://docs.djangoproject.com/en/
|
|
164
|
+
# https://docs.djangoproject.com/en/dev/topics/i18n/
|
|
168
165
|
|
|
169
166
|
LANGUAGE_CODE = '{{ lang }}'
|
|
170
167
|
|
|
@@ -177,12 +174,12 @@ USE_L10N = True
|
|
|
177
174
|
USE_TZ = True
|
|
178
175
|
|
|
179
176
|
# Default primary key field type
|
|
180
|
-
# https://docs.djangoproject.com/en/
|
|
177
|
+
# https://docs.djangoproject.com/en/dev/ref/settings/#default-auto-field
|
|
181
178
|
|
|
182
179
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
|
183
180
|
|
|
184
181
|
# Static files (CSS, JavaScript, Images)
|
|
185
|
-
# https://docs.djangoproject.com/en/
|
|
182
|
+
# https://docs.djangoproject.com/en/dev/howto/static-files/
|
|
186
183
|
|
|
187
184
|
STATIC_URL = '/sitestatic/'
|
|
188
185
|
STATIC_ROOT = os.path.join(BASE_DIR, 'sitestatic')
|
|
@@ -295,7 +292,7 @@ CACHES = {
|
|
|
295
292
|
|
|
296
293
|
|
|
297
294
|
# Password validation
|
|
298
|
-
# https://docs.djangoproject.com/en/
|
|
295
|
+
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
|
|
299
296
|
|
|
300
297
|
AUTH_PASSWORD_VALIDATORS = [
|
|
301
298
|
{
|
|
@@ -384,7 +381,11 @@ SILENCED_SYSTEM_CHECKS = [
|
|
|
384
381
|
]
|
|
385
382
|
|
|
386
383
|
PHONENUMBER_DB_FORMAT = 'INTERNATIONAL'
|
|
387
|
-
|
|
384
|
+
{% if amavis_enabled %}
|
|
385
|
+
# Amavis settings
|
|
386
|
+
DATABASE_ROUTERS = ["modoboa.amavis.dbrouter.AmavisRouter"]
|
|
387
|
+
AMAVIS_DEFAULT_DATABASE_ENCODING = "LATIN1" # or any value matching your database config
|
|
388
|
+
{% endif %}
|
|
388
389
|
# Load settings from extensions
|
|
389
390
|
{% for extension in extra_settings %}
|
|
390
391
|
try:
|
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()
|
|
@@ -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
|
]
|
modoboa/core/tests/test_core.py
CHANGED
|
@@ -186,6 +186,30 @@ class ManagementCommandsTestCase(SimpleModoTestCase):
|
|
|
186
186
|
with self.assertRaises(models.User.DoesNotExist):
|
|
187
187
|
account.refresh_from_db()
|
|
188
188
|
|
|
189
|
+
def test_add_allowed_hosts(self):
|
|
190
|
+
with self.assertRaises(management.CommandError):
|
|
191
|
+
management.call_command(
|
|
192
|
+
"add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
|
|
193
|
+
)
|
|
194
|
+
management.call_command("load_initial_data")
|
|
195
|
+
management.call_command(
|
|
196
|
+
"add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
|
|
197
|
+
)
|
|
198
|
+
app_model = get_application_model()
|
|
199
|
+
app = app_model.objects.filter(name="modoboa_frontend").first()
|
|
200
|
+
self.assertIn("app1.domain.tld", app.redirect_uris)
|
|
201
|
+
self.assertIn("app2.domain.tld", app.redirect_uris)
|
|
202
|
+
self.assertIn("app1.domain.tld", app.post_logout_redirect_uris)
|
|
203
|
+
self.assertIn("app2.domain.tld", app.post_logout_redirect_uris)
|
|
204
|
+
|
|
205
|
+
# Check if same hostname is not added more than once
|
|
206
|
+
uri_count = len(app.redirect_uris.split(" "))
|
|
207
|
+
management.call_command(
|
|
208
|
+
"add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
|
|
209
|
+
)
|
|
210
|
+
app.refresh_from_db()
|
|
211
|
+
self.assertEqual(len(app.redirect_uris.split(" ")), uri_count)
|
|
212
|
+
|
|
189
213
|
|
|
190
214
|
class APICommunicationTestCase(ModoTestCase):
|
|
191
215
|
"""Check communication with the API."""
|