modoboa 2.6.5__py3-none-any.whl → 2.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modoboa/admin/api/v2/serializers.py +1 -0
- modoboa/admin/app_settings.py +12 -0
- modoboa/admin/{management/commands/subcommands/_mx.py → dns_checker.py} +41 -111
- modoboa/admin/jobs.py +86 -0
- modoboa/admin/management/commands/modo.py +0 -2
- modoboa/admin/tests/test_mailbox_operations.py +4 -4
- modoboa/admin/tests/test_mx.py +68 -56
- modoboa/amavis/jobs.py +11 -0
- modoboa/amavis/tests/test_jobs.py +18 -0
- modoboa/amavis/tests/test_viewsets.py +2 -3
- modoboa/autoconfig/templates/autoconfig/autoconfig.xml +2 -2
- modoboa/autoconfig/templates/autoconfig/autodiscover.xml +14 -0
- modoboa/autoconfig/tests.py +2 -1
- modoboa/autoconfig/views.py +11 -3
- modoboa/calendars/backends/caldav_.py +17 -13
- modoboa/calendars/jobs.py +7 -0
- modoboa/calendars/mocks.py +4 -1
- modoboa/calendars/serializers.py +14 -9
- modoboa/calendars/tests.py +10 -9
- modoboa/calendars/viewsets.py +3 -1
- modoboa/contacts/migrations/0008_addressbook_syncing.py +18 -0
- modoboa/contacts/models.py +1 -0
- modoboa/contacts/serializers.py +5 -2
- modoboa/contacts/tasks.py +9 -3
- modoboa/contacts/tests.py +32 -6
- modoboa/contacts/viewsets.py +7 -1
- modoboa/core/api/v2/serializers.py +0 -7
- modoboa/core/api/v2/tests.py +0 -10
- modoboa/core/app_settings.py +0 -22
- modoboa/core/commands/deploy.py +13 -0
- modoboa/core/commands/templates/cron_config.py.tpl +33 -0
- modoboa/core/commands/templates/settings.py.tpl +21 -0
- modoboa/core/jobs.py +34 -0
- modoboa/core/management/commands/load_initial_data.py +1 -1
- modoboa/core/password_hashers/base.py +4 -1
- modoboa/core/tests/test_core.py +0 -14
- modoboa/core/tests/test_jobs.py +40 -0
- modoboa/frontend_dist/assets/{AccountAliasForm-B-hTKSFr.js → AccountAliasForm-DO6DwfjE.js} +1 -1
- modoboa/frontend_dist/assets/{AccountEditView-LkP_qhf_.js → AccountEditView-CCuN9mGB.js} +1 -1
- modoboa/frontend_dist/assets/AccountLayout-Ge7fzuZg.js +1 -0
- modoboa/frontend_dist/assets/AccountPasswordSubForm-PkFPblkR.js +1 -0
- modoboa/frontend_dist/assets/AccountView-dgseekZ8.js +1 -0
- modoboa/frontend_dist/assets/AddressBook-DZWOHOJj.js +1 -0
- modoboa/frontend_dist/assets/AdminLayout-itB_mmH_.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-King6zb6.js +1 -0
- modoboa/frontend_dist/assets/{AliasEditView-BVjNJekt.js → AliasEditView-CTjrXYPf.js} +1 -1
- modoboa/frontend_dist/assets/AliasRecipientForm-CI7bXqVp.js +1 -0
- modoboa/frontend_dist/assets/{AliasView-DvvI6cV9.js → AliasView-BN13MNN_.js} +1 -1
- modoboa/frontend_dist/assets/AuditTrailView-C1jQwgoo.js +1 -0
- modoboa/frontend_dist/assets/CalendarView-juHVIHU5.css +1 -0
- modoboa/frontend_dist/assets/CalendarView-rRSzqxrH.js +1 -0
- modoboa/frontend_dist/assets/ChoiceField-DCr12shR.js +1 -0
- modoboa/frontend_dist/assets/ComposeEmailForm-BmtKwFb1.js +2 -0
- modoboa/frontend_dist/assets/ComposeEmailForm-CVNDl-Mq.css +1 -0
- modoboa/frontend_dist/assets/ComposeEmailView-JW22Phrb.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-CWpdwSQ6.js +1 -0
- modoboa/frontend_dist/assets/ConnectedLayout-BP8pO27H.js +1 -0
- modoboa/frontend_dist/assets/{ConnectedLayout-C6HNXWkp.css → ConnectedLayout-Ddpb_6yT.css} +1 -1
- modoboa/frontend_dist/assets/CreationForm-CzelJsVQ.js +1 -0
- modoboa/frontend_dist/assets/DashboardView-BBkBodVj.js +1 -0
- modoboa/frontend_dist/assets/{DomainAdminList-BO8_4Cwt.js → DomainAdminList-DD7p3i6F.js} +1 -1
- modoboa/frontend_dist/assets/{DomainEditView-BXylM4aB.js → DomainEditView-DoIoNYC4.js} +1 -1
- modoboa/frontend_dist/assets/{DomainTransportForm-Ccw5BXyM.js → DomainTransportForm-CbiJF9z5.js} +1 -1
- modoboa/frontend_dist/assets/{DomainView-Bd5XQI-1.js → DomainView-BXvznFYz.js} +3 -3
- modoboa/frontend_dist/assets/DomainsView-ffYjiffp.js +1 -0
- modoboa/frontend_dist/assets/EmailField-KhhJYA4D.js +1 -0
- modoboa/frontend_dist/assets/EmailSchedulingForm-CQL5Vfdr.js +1 -0
- modoboa/frontend_dist/assets/EmailView-Bq2bHZBO.js +1 -0
- modoboa/frontend_dist/assets/EmptyLayout-NrTftp18.js +1 -0
- modoboa/frontend_dist/assets/FiltersView-SOQy0U_3.js +1 -0
- modoboa/frontend_dist/assets/ForwardEmailView-kEPDjWUw.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-DNn3CY-w.js → HtmlEditor-CyBl5wj2.js} +15 -15
- modoboa/frontend_dist/assets/IdentitiesView-CZFf4oR9.js +1 -0
- modoboa/frontend_dist/assets/InformationView-CAtneAqM.js +1 -0
- modoboa/frontend_dist/assets/{LoadingData-CnQ_aVqa.js → LoadingData-C2txD49L.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-Nh_gmh8e.js → LoginCallbackView-DAvty3CI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-BtC9BHuJ.js → LoginView-DHYITkn4.js} +1 -1
- modoboa/frontend_dist/assets/MailboxView-BNg2v7mi.css +1 -0
- modoboa/frontend_dist/assets/MailboxView-DHg5A78D.js +5 -0
- modoboa/frontend_dist/assets/MenuItems-C9p70bSC.js +1 -0
- modoboa/frontend_dist/assets/{MessageView-C1Goz-hY.js → MessageView-CrrEJpNI.js} +1 -1
- modoboa/frontend_dist/assets/MessagesView-CyecRd4I.js +1 -0
- modoboa/frontend_dist/assets/MigrationsView-DXGKKz2h.js +1 -0
- modoboa/frontend_dist/assets/{ParametersForm-BegEnkQM.js → ParametersForm-DFKYkPAs.js} +1 -1
- modoboa/frontend_dist/assets/ParametersView-B1wT5oyt.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-CfvVzKea.js +1 -0
- modoboa/frontend_dist/assets/ProviderEditView-De-8ggBp.js +1 -0
- modoboa/frontend_dist/assets/{ProviderGeneralForm-BAELnXh6.js → ProviderGeneralForm-DO-IpJMQ.js} +1 -1
- modoboa/frontend_dist/assets/ProvidersView-Bhk7sCz4.js +1 -0
- modoboa/frontend_dist/assets/QuarantineLayout-C1YszNmP.js +1 -0
- modoboa/frontend_dist/assets/QuarantineView-cElD_rS-.js +1 -0
- modoboa/frontend_dist/assets/ReplyEmailView-BvuOZdLl.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-FWxrmVwo.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-JHRvfvQf.js +1 -0
- modoboa/frontend_dist/assets/SettingsView-SIsKAXtQ.css +1 -0
- modoboa/frontend_dist/assets/SettingsView-s2l2Xl1L.js +6 -0
- modoboa/frontend_dist/assets/StatisticsView-FiidWvad.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-CdqUHiO_.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-nLIFGI0y.css +1 -0
- modoboa/frontend_dist/assets/UserLayout-DyvI8duf.js +1 -0
- modoboa/frontend_dist/assets/{VAlert-Buv8Z5G4.js → VAlert-MDbeolOo.js} +1 -1
- modoboa/frontend_dist/assets/VApp-DM1KLQfQ.js +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-C9NL5_uo.css +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-CQsWYWNX.js +1 -0
- modoboa/frontend_dist/assets/VAvatar-CXV_FqpP.js +1 -0
- modoboa/frontend_dist/assets/VBadge-7tDx7aI3.js +1 -0
- modoboa/frontend_dist/assets/{VCard-D7AwBs4w.js → VCard-CBtX8JF-.js} +1 -1
- modoboa/frontend_dist/assets/VCheckbox-zY1MApOy.js +1 -0
- modoboa/frontend_dist/assets/{VCheckboxBtn-B5evxN9K.js → VCheckboxBtn-B0mIT3E0.js} +1 -1
- modoboa/frontend_dist/assets/VColorPicker-BbCHvk6K.js +1 -0
- modoboa/frontend_dist/assets/{VColorPicker-B_lVDaYR.css → VColorPicker-C9m8L-6U.css} +1 -1
- modoboa/frontend_dist/assets/{VContainer-DY9021j4.js → VContainer-Cn-vKB3s.js} +1 -1
- modoboa/frontend_dist/assets/VDataTable-CNT9KOSp.js +1 -0
- modoboa/frontend_dist/assets/VDataTableServer-_yn4Ry6U.js +1 -0
- modoboa/frontend_dist/assets/VDataTableVirtual-Csxh3Gp6.js +1 -0
- modoboa/frontend_dist/assets/VDatePicker-_OUDqShN.css +1 -0
- modoboa/frontend_dist/assets/VDatePicker-iFu13xIP.js +2 -0
- modoboa/frontend_dist/assets/VDialog-lKtBdqnp.js +1 -0
- modoboa/frontend_dist/assets/VExpansionPanels-CaIvk9iF.js +1 -0
- modoboa/frontend_dist/assets/VFileInput-C7J_qVmk.js +1 -0
- modoboa/frontend_dist/assets/VForm-fdQ5d-CH.js +1 -0
- modoboa/frontend_dist/assets/VInput-BwHvhzAe.js +1 -0
- modoboa/frontend_dist/assets/VMenu-BpmJf4X2.js +1 -0
- modoboa/frontend_dist/assets/VMenu-C5A_5Hs5.css +1 -0
- modoboa/frontend_dist/assets/VPicker-B7cB3kJg.css +1 -0
- modoboa/frontend_dist/assets/VPicker-BZho70wU.js +1 -0
- modoboa/frontend_dist/assets/VProgressCircular-DRw_-iNj.js +1 -0
- modoboa/frontend_dist/assets/VRadioGroup--eiP5xtJ.js +1 -0
- modoboa/frontend_dist/assets/{VRow-DyK_Mj7R.js → VRow-83Qnr5iB.js} +1 -1
- modoboa/frontend_dist/assets/VSelect-BcmGFGif.js +1 -0
- modoboa/frontend_dist/assets/{VSelectionControl-Dkms9_P5.js → VSelectionControl-Cqi1xt-q.js} +1 -1
- modoboa/frontend_dist/assets/VSheet-C3MaHhtw.js +1 -0
- modoboa/frontend_dist/assets/VSpacer-CuSkdJZL.js +1 -0
- modoboa/frontend_dist/assets/VSwitch-XumUl685.js +1 -0
- modoboa/frontend_dist/assets/{VTable-D8ZwwC_B.js → VTable-f7wcr2AZ.js} +1 -1
- modoboa/frontend_dist/assets/VTabs-CDfdejXj.css +1 -0
- modoboa/frontend_dist/assets/VTabs-y4wNP4im.js +1 -0
- modoboa/frontend_dist/assets/VTextField-BPIvtrn4.js +1 -0
- modoboa/frontend_dist/assets/VTextField-DjbYGlzs.css +1 -0
- modoboa/frontend_dist/assets/VTextarea-B6bGYcC3.js +1 -0
- modoboa/frontend_dist/assets/VTextarea-f6vTjzFy.css +1 -0
- modoboa/frontend_dist/assets/VToolbar-BYDPtwf0.css +1 -0
- modoboa/frontend_dist/assets/VToolbar-CB6wwtYc.js +1 -0
- modoboa/frontend_dist/assets/VWindowItem-CDtLLEkg.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-CQQAolnl.css +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-tosQSHLS.js +1 -0
- modoboa/frontend_dist/assets/{accounts-BZBqbAmd.js → accounts-KUsk6LHW.js} +1 -1
- modoboa/frontend_dist/assets/{admin-uCrKjpln.js → admin-BW9cZW0P.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-CFwFkX45.js → aliases-Ge0hjIsH.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-DCy0U0TD.js → amavis-BbFeFfsk.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-B4ScfQsG.js → amavis-DtuzP_CS.js} +1 -1
- modoboa/frontend_dist/assets/{contacts-DKmh2q-4.js → contacts-DMJlQTe0.js} +1 -1
- modoboa/frontend_dist/assets/{domains-CDE4_LDF.js → domains-Du64lcXT.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-a9Y5UlDV.js → domains.store-1U61jeCV.js} +1 -1
- modoboa/frontend_dist/assets/events-BM3in65C.js +1 -0
- modoboa/frontend_dist/assets/filter-Dihm6o59.js +1 -0
- modoboa/frontend_dist/assets/importExport-HGcNGWOm.js +1 -0
- modoboa/frontend_dist/assets/{index-B9q1vO3K.css → index-B1EK3MQe.css} +1 -1
- modoboa/frontend_dist/assets/{index-Dwg6jPTX.js → index-Dv00bmw9.js} +1 -1
- modoboa/frontend_dist/assets/{index-BjibLozh.js → index-jui3edpn.js} +47 -41
- modoboa/frontend_dist/assets/{language.store-DGuI8jG0.js → language.store-OcfdXL_-.js} +1 -1
- modoboa/frontend_dist/assets/languages-CF8hxo7x.js +1 -0
- modoboa/frontend_dist/assets/{layout-5PvsmkGR.js → layout-DOO7TRTJ.js} +1 -1
- modoboa/frontend_dist/assets/{layout.store-DJVcXtxk.js → layout.store-C0g-piJn.js} +1 -1
- modoboa/frontend_dist/assets/{logos-CMmd0hOG.js → logos-Dz2Gzei-.js} +1 -1
- modoboa/frontend_dist/assets/{logs-D2GOydPZ.js → logs-CLm32Weu.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-DJdlq9sh.js → parameters-DMIAQ7cd.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-DH3pPlaT.js → parameters.store-DLnFzCwV.js} +1 -1
- modoboa/frontend_dist/assets/{permissions-CgFJFhCp.js → permissions-CrpE0b4w.js} +1 -1
- modoboa/frontend_dist/assets/{ssrBoot-ChTB-PYP.js → ssrBoot-B7cr7q9U.js} +1 -1
- modoboa/frontend_dist/assets/{tag-CvrHE14f.js → tag-WF93n81Q.js} +1 -1
- modoboa/frontend_dist/assets/{theme-DFzlhSh5.js → theme-_0oOYChG.js} +1 -1
- modoboa/frontend_dist/assets/transports-CS61syt-.js +1 -0
- modoboa/frontend_dist/assets/webmail-CYDXU0DS.js +1 -0
- modoboa/frontend_dist/assets/webmail.store-BvHCQSjM.js +1 -0
- modoboa/frontend_dist/index.html +2 -2
- modoboa/imap_migration/models.py +3 -3
- modoboa/imap_migration/templates/imap_migration/offlineimap.conf +2 -2
- modoboa/imap_migration/tests.py +21 -0
- modoboa/lib/cryptutils.py +2 -2
- modoboa/lib/sysutils.py +1 -1
- modoboa/lib/tests/__init__.py +0 -1
- modoboa/locale/br/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/br/LC_MESSAGES/django.po +301 -273
- modoboa/locale/cs/LC_MESSAGES/django.po +286 -269
- modoboa/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/cs_CZ/LC_MESSAGES/django.po +303 -275
- modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de/LC_MESSAGES/django.po +297 -269
- modoboa/locale/de_DE/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de_DE/LC_MESSAGES/django.po +303 -275
- modoboa/locale/el_GR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/el_GR/LC_MESSAGES/django.po +303 -275
- modoboa/locale/en/LC_MESSAGES/django.po +286 -269
- modoboa/locale/es/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/es/LC_MESSAGES/django.po +302 -274
- modoboa/locale/es_MX/LC_MESSAGES/django.po +286 -269
- modoboa/locale/fi/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fi/LC_MESSAGES/django.po +299 -271
- modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fr/LC_MESSAGES/django.po +288 -271
- modoboa/locale/hu/LC_MESSAGES/django.po +286 -269
- modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/it/LC_MESSAGES/django.po +303 -275
- modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ja_JP/LC_MESSAGES/django.po +390 -413
- modoboa/locale/ka/LC_MESSAGES/django.po +286 -269
- modoboa/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/nl_NL/LC_MESSAGES/django.po +299 -271
- modoboa/locale/no/LC_MESSAGES/django.po +286 -269
- modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pl_PL/LC_MESSAGES/django.po +297 -269
- modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_BR/LC_MESSAGES/django.po +298 -270
- modoboa/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_PT/LC_MESSAGES/django.po +303 -275
- modoboa/locale/ro_RO/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ro_RO/LC_MESSAGES/django.po +299 -271
- modoboa/locale/ru/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ru/LC_MESSAGES/django.po +993 -1096
- modoboa/locale/si/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sk/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sk_SK/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sl_SI/LC_MESSAGES/django.po +288 -269
- modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/sv/LC_MESSAGES/django.po +297 -269
- modoboa/locale/tr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/tr/LC_MESSAGES/django.po +301 -272
- modoboa/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/tr_TR/LC_MESSAGES/django.po +291 -269
- modoboa/locale/uk/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh_CN/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/zh_TW/LC_MESSAGES/django.po +295 -273
- modoboa/maillog/jobs.py +11 -0
- modoboa/maillog/tests/test_views.py +5 -4
- modoboa/parameters/api/v2/tests.py +1 -1
- modoboa/policyd/management/commands/policy_daemon.py +5 -1
- modoboa/policyd/tests.py +4 -2
- modoboa/rspamd/tests.py +20 -0
- modoboa/templates/registration/twofactor_code_verify.html +1 -1
- modoboa/webmail/app_settings.py +37 -0
- modoboa/webmail/constants.py +21 -1
- modoboa/webmail/factories.py +19 -0
- modoboa/webmail/jobs.py +27 -0
- modoboa/webmail/lib/__init__.py +0 -2
- modoboa/webmail/lib/imapemail.py +9 -11
- modoboa/webmail/lib/imapheader.py +25 -13
- modoboa/webmail/lib/imaputils.py +51 -13
- modoboa/webmail/lib/sendmail.py +88 -105
- modoboa/webmail/lib/utils.py +109 -0
- modoboa/webmail/migrations/0001_initial.py +90 -0
- modoboa/webmail/migrations/__init__.py +0 -0
- modoboa/webmail/mocks.py +12 -3
- modoboa/webmail/models.py +102 -0
- modoboa/webmail/serializers.py +84 -4
- modoboa/webmail/tests/data.py +21 -0
- modoboa/webmail/tests/test_lib_imaputils.py +33 -0
- modoboa/webmail/tests/test_viewsets.py +108 -0
- modoboa/webmail/urls.py +5 -0
- modoboa/webmail/viewsets.py +39 -9
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/METADATA +16 -13
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/RECORD +269 -252
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/WHEEL +1 -1
- modoboa/admin/management/commands/handle_mailbox_operations.py +0 -103
- modoboa/core/management/commands/cleanlogs.py +0 -53
- modoboa/frontend_dist/assets/AccountLayout-APeC9bsN.js +0 -1
- modoboa/frontend_dist/assets/AccountPasswordSubForm-D4vB2yQK.js +0 -1
- modoboa/frontend_dist/assets/AccountView-CloBP1BY.js +0 -1
- modoboa/frontend_dist/assets/AddressBook-C03MXg3y.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-DFmyKOo6.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-CFhH0I5d.js +0 -1
- modoboa/frontend_dist/assets/AliasRecipientForm-CZ_SvjtG.js +0 -1
- modoboa/frontend_dist/assets/AuditTrailView-Cyq25Jle.js +0 -1
- modoboa/frontend_dist/assets/CalendarView-C_dSXRp9.js +0 -1
- modoboa/frontend_dist/assets/ChoiceField-BPhUvoyr.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-D3yUbBJX.css +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-Ns5SYs4J.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-DgUfiWEH.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-BMIg94-U.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-CSXdVJtV.js +0 -1
- modoboa/frontend_dist/assets/CreationForm-CaAZYATm.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-DErmyUoy.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-CC1EwRId.js +0 -1
- modoboa/frontend_dist/assets/EmailField-BFgga4zp.js +0 -1
- modoboa/frontend_dist/assets/EmailView-DPWTch_6.js +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-DAw7fAab.js +0 -1
- modoboa/frontend_dist/assets/FiltersView-Cg9lkLw0.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-Dd1sxgaP.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-CiHIQ9Qt.js +0 -1
- modoboa/frontend_dist/assets/InformationView-Cc0aBA5j.js +0 -1
- modoboa/frontend_dist/assets/MailboxView-DIVABvk5.css +0 -1
- modoboa/frontend_dist/assets/MailboxView-DplC0IXR.js +0 -5
- modoboa/frontend_dist/assets/MenuItems-efx2rqwT.js +0 -1
- modoboa/frontend_dist/assets/MessagesView-CesYYZ9f.js +0 -1
- modoboa/frontend_dist/assets/MigrationsView-DSBk8T0y.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-B8W_PAiz.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-Dsx2M_g9.js +0 -1
- modoboa/frontend_dist/assets/ProviderEditView-PEAr7MA1.js +0 -1
- modoboa/frontend_dist/assets/ProvidersView-CHvQf3Vv.js +0 -1
- modoboa/frontend_dist/assets/QuarantineLayout-BWEaBK48.js +0 -1
- modoboa/frontend_dist/assets/QuarantineView-BzjeDhTm.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-CwQ9jsce.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-noC3AXd9.js +0 -1
- modoboa/frontend_dist/assets/SelfServiceLayout-DQgRMu0L.js +0 -1
- modoboa/frontend_dist/assets/SettingsView-DJTdIeRK.css +0 -1
- modoboa/frontend_dist/assets/SettingsView-Dgwpfd8O.js +0 -6
- modoboa/frontend_dist/assets/StatisticsView-BZkVSh5o.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-BXJwhTMO.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-C3XHmlRd.css +0 -1
- modoboa/frontend_dist/assets/UserLayout-CWK9ICfE.js +0 -1
- modoboa/frontend_dist/assets/VApp-CiIBWB_Q.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-DcNqTIie.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-hzGuLlUI.css +0 -1
- modoboa/frontend_dist/assets/VAvatar-BsHplcRP.js +0 -1
- modoboa/frontend_dist/assets/VBadge-CavP_E2g.js +0 -1
- modoboa/frontend_dist/assets/VCheckbox-CKW9lz5i.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-Bsrz8yif.js +0 -1
- modoboa/frontend_dist/assets/VDataTable-ocZo8ju0.js +0 -1
- modoboa/frontend_dist/assets/VDataTableServer-CJfpYXQW.js +0 -1
- modoboa/frontend_dist/assets/VDataTableVirtual-Cr8yw49k.js +0 -1
- modoboa/frontend_dist/assets/VDialog-CUMh3paI.js +0 -1
- modoboa/frontend_dist/assets/VExpansionPanels-CkwHiKsA.js +0 -1
- modoboa/frontend_dist/assets/VFileInput-D5qlksIG.js +0 -1
- modoboa/frontend_dist/assets/VForm-ClJU5x_v.js +0 -1
- modoboa/frontend_dist/assets/VInput-DhzSMFXh.js +0 -1
- modoboa/frontend_dist/assets/VMenu-B53a3gm6.js +0 -1
- modoboa/frontend_dist/assets/VMenu-BEipA1lw.css +0 -1
- modoboa/frontend_dist/assets/VPicker-B928ZVJQ.js +0 -1
- modoboa/frontend_dist/assets/VPicker-ClSXs6kv.css +0 -1
- modoboa/frontend_dist/assets/VProgressCircular-viI3jDXe.js +0 -1
- modoboa/frontend_dist/assets/VRadioGroup-C0bqFrYg.js +0 -1
- modoboa/frontend_dist/assets/VSelect-y0E5_vPn.js +0 -1
- modoboa/frontend_dist/assets/VSheet-DQkh_hsX.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-CBqB9uSt.js +0 -1
- modoboa/frontend_dist/assets/VSwitch-D4xdR9jz.js +0 -1
- modoboa/frontend_dist/assets/VTabs-D2c0KeF2.js +0 -1
- modoboa/frontend_dist/assets/VTabs-NzpINroH.css +0 -1
- modoboa/frontend_dist/assets/VTextField-Cow3HZvI.css +0 -1
- modoboa/frontend_dist/assets/VTextField-DvvH1ciR.js +0 -1
- modoboa/frontend_dist/assets/VTextarea-DHtXtzqP.js +0 -1
- modoboa/frontend_dist/assets/VTextarea-DyGjqrlm.css +0 -1
- modoboa/frontend_dist/assets/VToolbar-CB2GrZpA.css +0 -1
- modoboa/frontend_dist/assets/VToolbar-pvLQtmbU.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-DErNPSH8.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-BzW0LWYp.css +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-D7J-QJ-0.js +0 -1
- modoboa/frontend_dist/assets/filter-BVvMTmPG.js +0 -1
- modoboa/frontend_dist/assets/global.store-Bha4Z76j.js +0 -1
- modoboa/frontend_dist/assets/importExport-EgijWlC1.js +0 -1
- modoboa/frontend_dist/assets/languages-Cge6pECg.js +0 -1
- modoboa/frontend_dist/assets/transports-BUSYxSF4.js +0 -1
- modoboa/frontend_dist/assets/webmail-HVg3ZHC0.js +0 -1
- modoboa/frontend_dist/assets/webmail.store-D-d88s4w.js +0 -1
- {modoboa-2.6.5.data → modoboa-2.7.0.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/entry_points.txt +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/top_level.txt +0 -0
|
@@ -223,6 +223,7 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
|
|
|
223
223
|
enable_dkim_checks = serializers.BooleanField(default=True)
|
|
224
224
|
enable_dmarc_checks = serializers.BooleanField(default=True)
|
|
225
225
|
enable_autoconfig_checks = serializers.BooleanField(default=True)
|
|
226
|
+
enable_dns_notifications = serializers.BooleanField(default=True)
|
|
226
227
|
custom_dns_server = serializers.IPAddressField(allow_blank=True, allow_null=True)
|
|
227
228
|
enable_dnsbl_checks = serializers.BooleanField(default=True)
|
|
228
229
|
dkim_keys_storage_dir = serializers.CharField(allow_blank=True, allow_null=True)
|
modoboa/admin/app_settings.py
CHANGED
|
@@ -110,6 +110,18 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
|
|
|
110
110
|
),
|
|
111
111
|
},
|
|
112
112
|
),
|
|
113
|
+
(
|
|
114
|
+
"enable_dns_notifications",
|
|
115
|
+
{
|
|
116
|
+
"label": gettext_lazy(
|
|
117
|
+
"Notify domain admins about DNS issues"
|
|
118
|
+
),
|
|
119
|
+
"help_text": gettext_lazy(
|
|
120
|
+
"Send a notification by email to domain administrators "
|
|
121
|
+
"when DNS issues are encountered"
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
),
|
|
113
125
|
(
|
|
114
126
|
"custom_dns_server",
|
|
115
127
|
{
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
"""Management command to check defined domains."""
|
|
2
|
-
|
|
3
1
|
import ipaddress
|
|
4
|
-
|
|
5
|
-
import gevent
|
|
6
|
-
from gevent import socket
|
|
2
|
+
import socket
|
|
7
3
|
|
|
8
4
|
from django.conf import settings
|
|
9
5
|
from django.core.mail import EmailMessage
|
|
10
|
-
from django.core.management.base import BaseCommand
|
|
11
6
|
from django.template.loader import render_to_string
|
|
12
7
|
from django.utils import timezone
|
|
13
|
-
from django.utils.encoding import smart_str
|
|
14
8
|
from django.utils.functional import cached_property
|
|
15
9
|
from django.utils.translation import gettext as _
|
|
16
10
|
|
|
@@ -19,10 +13,7 @@ from modoboa.dnstools import models as dns_models
|
|
|
19
13
|
from modoboa.parameters import tools as param_tools
|
|
20
14
|
|
|
21
15
|
|
|
22
|
-
class
|
|
23
|
-
"""Command class."""
|
|
24
|
-
|
|
25
|
-
help = "Check defined domains." # NOQA:A003
|
|
16
|
+
class DNSChecker:
|
|
26
17
|
|
|
27
18
|
@cached_property
|
|
28
19
|
def providers(self):
|
|
@@ -36,52 +27,24 @@ class CheckMXRecords(BaseCommand):
|
|
|
36
27
|
"""Return sender address for notifications."""
|
|
37
28
|
return param_tools.get_global_parameter("sender_address", app="core")
|
|
38
29
|
|
|
30
|
+
@cached_property
|
|
31
|
+
def config(self) -> dict:
|
|
32
|
+
return dict(param_tools.get_global_parameters("admin"))
|
|
33
|
+
|
|
39
34
|
@cached_property
|
|
40
35
|
def valid_mxs(self):
|
|
41
36
|
"""Return valid MXs set in admin."""
|
|
42
|
-
valid_mxs =
|
|
37
|
+
valid_mxs = self.config["valid_mxs"]
|
|
43
38
|
return [
|
|
44
|
-
ipaddress.ip_network(
|
|
45
|
-
for v in valid_mxs.split()
|
|
46
|
-
if v.strip()
|
|
39
|
+
ipaddress.ip_network(str(v.strip())) for v in valid_mxs.split() if v.strip()
|
|
47
40
|
]
|
|
48
41
|
|
|
49
|
-
def add_arguments(self, parser):
|
|
50
|
-
"""Add extra arguments to command."""
|
|
51
|
-
parser.add_argument(
|
|
52
|
-
"--no-dnsbl", action="store_true", default=False, help="Skip DNSBL queries."
|
|
53
|
-
)
|
|
54
|
-
parser.add_argument(
|
|
55
|
-
"--email",
|
|
56
|
-
type=str,
|
|
57
|
-
action="append",
|
|
58
|
-
default=[],
|
|
59
|
-
help="One or more email to notify",
|
|
60
|
-
)
|
|
61
|
-
parser.add_argument(
|
|
62
|
-
"--skip-admin-emails",
|
|
63
|
-
action="store_true",
|
|
64
|
-
default=False,
|
|
65
|
-
help="Skip domain's admins email notification.",
|
|
66
|
-
)
|
|
67
|
-
parser.add_argument(
|
|
68
|
-
"--domain",
|
|
69
|
-
type=str,
|
|
70
|
-
action="append",
|
|
71
|
-
default=[],
|
|
72
|
-
help="Domain name or id to update.",
|
|
73
|
-
)
|
|
74
|
-
parser.add_argument(
|
|
75
|
-
"--timeout", type=int, default=3, help="Timeout used for queries."
|
|
76
|
-
)
|
|
77
|
-
parser.add_argument("--ttl", type=int, default=7200, help="TTL for dns query.")
|
|
78
|
-
|
|
79
42
|
def query_dnsbl(self, mx_list, provider):
|
|
80
43
|
"""Check given IP against given DNSBL provider."""
|
|
81
44
|
results = {}
|
|
82
45
|
for ip, mxs in mx_list.items():
|
|
83
46
|
try:
|
|
84
|
-
ip = ipaddress.ip_address(
|
|
47
|
+
ip = ipaddress.ip_address(str(ip))
|
|
85
48
|
except ValueError:
|
|
86
49
|
continue
|
|
87
50
|
else:
|
|
@@ -99,9 +62,9 @@ class CheckMXRecords(BaseCommand):
|
|
|
99
62
|
result = False
|
|
100
63
|
for mx in mxs:
|
|
101
64
|
results[mx] = result
|
|
102
|
-
return
|
|
65
|
+
return results
|
|
103
66
|
|
|
104
|
-
def store_dnsbl_result(self, domain, provider, results
|
|
67
|
+
def store_dnsbl_result(self, domain, provider, results):
|
|
105
68
|
"""Store DNSBL provider results for domain.
|
|
106
69
|
|
|
107
70
|
Return a list of alerts.
|
|
@@ -142,23 +105,24 @@ class CheckMXRecords(BaseCommand):
|
|
|
142
105
|
models.DNSBLResult.objects.bulk_create(to_create)
|
|
143
106
|
return alerts
|
|
144
107
|
|
|
145
|
-
def send_alert_notifications(
|
|
108
|
+
def send_alert_notifications(
|
|
109
|
+
self, domain: models.Domain, alerts: list, subject: str, tpl: str
|
|
110
|
+
) -> None:
|
|
146
111
|
"""Send email notifications about given alerts."""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
)
|
|
112
|
+
if not self.config["enable_dns_notifications"]:
|
|
113
|
+
return
|
|
114
|
+
emails = domain.admins.exclude(mailbox__isnull=True).values_list(
|
|
115
|
+
"email", flat=True
|
|
116
|
+
)
|
|
154
117
|
if not len(emails):
|
|
155
118
|
return
|
|
156
119
|
content = render_to_string(tpl, {"domain": domain, "alerts": alerts})
|
|
157
120
|
msg = EmailMessage(subject, content.strip(), self.sender, emails)
|
|
158
121
|
msg.send()
|
|
159
122
|
|
|
160
|
-
def check_valid_mx(self, domain, mx_list
|
|
161
|
-
"""
|
|
123
|
+
def check_valid_mx(self, domain: models.Domain, mx_list: list) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Check that domain's MX record exist.
|
|
162
126
|
|
|
163
127
|
If `valid_mx` is provided, retrieved MX records must be
|
|
164
128
|
contained in it.
|
|
@@ -212,26 +176,25 @@ class CheckMXRecords(BaseCommand):
|
|
|
212
176
|
return
|
|
213
177
|
subject = _("[modoboa] MX issue(s) for domain {}").format(domain.name)
|
|
214
178
|
tpl = "admin/notifications/domain_invalid_mx.html"
|
|
215
|
-
self.send_alert_notifications(domain, alerts, subject, tpl
|
|
179
|
+
self.send_alert_notifications(domain, alerts, subject, tpl)
|
|
180
|
+
|
|
181
|
+
def run(self, domain: models.Domain, ttl: int = 7200):
|
|
182
|
+
# Remove deprecated records first
|
|
183
|
+
domain.dnsblresult_set.exclude(provider__in=self.providers).delete()
|
|
216
184
|
|
|
217
|
-
def check_domain(self, domain, timeout=3, ttl=7200, **options):
|
|
218
|
-
"""Check specified domain."""
|
|
219
185
|
mx_list = list(models.MXRecord.objects.get_or_create_for_domain(domain, ttl))
|
|
220
186
|
|
|
221
|
-
if
|
|
222
|
-
self.check_valid_mx(domain, mx_list
|
|
187
|
+
if self.config["enable_mx_checks"]:
|
|
188
|
+
self.check_valid_mx(domain, mx_list)
|
|
223
189
|
|
|
224
|
-
if
|
|
190
|
+
if self.config["enable_spf_checks"]:
|
|
225
191
|
dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "spf", ttl)
|
|
226
|
-
condition =
|
|
227
|
-
param_tools.get_global_parameter("enable_dkim_checks")
|
|
228
|
-
and domain.dkim_public_key
|
|
229
|
-
)
|
|
192
|
+
condition = self.config["enable_dkim_checks"] and domain.dkim_public_key
|
|
230
193
|
if condition:
|
|
231
194
|
dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "dkim", ttl)
|
|
232
|
-
if
|
|
195
|
+
if self.config["enable_dmarc_checks"]:
|
|
233
196
|
dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "dmarc", ttl)
|
|
234
|
-
if
|
|
197
|
+
if self.config["enable_autoconfig_checks"]:
|
|
235
198
|
dns_models.DNSRecord.objects.get_or_create_for_domain(
|
|
236
199
|
domain, "autoconfig", ttl
|
|
237
200
|
)
|
|
@@ -239,11 +202,8 @@ class CheckMXRecords(BaseCommand):
|
|
|
239
202
|
domain, "autodiscover", ttl
|
|
240
203
|
)
|
|
241
204
|
|
|
242
|
-
condition =
|
|
243
|
-
|
|
244
|
-
or options["no_dnsbl"] is True
|
|
245
|
-
)
|
|
246
|
-
if condition or not mx_list:
|
|
205
|
+
condition = not self.config["enable_dnsbl_checks"] or not mx_list
|
|
206
|
+
if condition:
|
|
247
207
|
return
|
|
248
208
|
|
|
249
209
|
mx_by_ip = {}
|
|
@@ -253,44 +213,14 @@ class CheckMXRecords(BaseCommand):
|
|
|
253
213
|
elif mx not in mx_by_ip[mx.address]:
|
|
254
214
|
mx_by_ip[mx.address].append(mx)
|
|
255
215
|
|
|
256
|
-
jobs = [
|
|
257
|
-
gevent.spawn(self.query_dnsbl, mx_by_ip, provider)
|
|
258
|
-
for provider in self.providers
|
|
259
|
-
]
|
|
260
|
-
gevent.joinall(jobs, timeout)
|
|
261
216
|
alerts = []
|
|
262
|
-
for
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
alerts += self.store_dnsbl_result(domain, provider, results, **options)
|
|
217
|
+
for provider in self.providers:
|
|
218
|
+
results = self.query_dnsbl(mx_by_ip, provider)
|
|
219
|
+
alerts += self.store_dnsbl_result(domain, provider, results)
|
|
220
|
+
|
|
267
221
|
if not alerts:
|
|
268
222
|
return
|
|
223
|
+
|
|
269
224
|
subject = _("[modoboa] DNSBL issue(s) for domain {}").format(domain.name)
|
|
270
225
|
tpl = "admin/notifications/domain_in_dnsbl.html"
|
|
271
|
-
self.send_alert_notifications(domain, alerts, subject, tpl
|
|
272
|
-
|
|
273
|
-
def handle(self, *args, **options):
|
|
274
|
-
"""Command entry point."""
|
|
275
|
-
# Remove deprecated records first
|
|
276
|
-
models.DNSBLResult.objects.exclude(provider__in=self.providers).delete()
|
|
277
|
-
|
|
278
|
-
if options["domain"]:
|
|
279
|
-
domains = []
|
|
280
|
-
for domain in options["domain"]:
|
|
281
|
-
try:
|
|
282
|
-
if domain.isdigit():
|
|
283
|
-
domains.append(models.Domain.objects.get(pk=domain))
|
|
284
|
-
else:
|
|
285
|
-
domains.append(models.Domain.objects.get(name=domain))
|
|
286
|
-
except models.Domain.DoesNotExist:
|
|
287
|
-
pass
|
|
288
|
-
else:
|
|
289
|
-
domains = models.Domain.objects.filter(enabled=True, enable_dns_checks=True)
|
|
290
|
-
|
|
291
|
-
options.pop("domain")
|
|
292
|
-
|
|
293
|
-
for domain in domains:
|
|
294
|
-
if domain.uses_a_reserved_tld:
|
|
295
|
-
continue
|
|
296
|
-
self.check_domain(domain, **options)
|
|
226
|
+
self.send_alert_notifications(domain, alerts, subject, tpl)
|
modoboa/admin/jobs.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Async jobs definition."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from django.db.models import F
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
|
|
10
|
+
import django_rq
|
|
11
|
+
|
|
12
|
+
from modoboa.admin import models
|
|
13
|
+
from modoboa.admin.app_settings import load_admin_settings
|
|
14
|
+
from modoboa.admin.dns_checker import DNSChecker
|
|
15
|
+
from modoboa.lib.sysutils import exec_cmd
|
|
16
|
+
from modoboa.parameters import tools as param_tools
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("modoboa.jobs")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rename_mailbox(operation):
|
|
22
|
+
"""Rename the mailbox folder through a RQ Job."""
|
|
23
|
+
if not os.path.exists(operation.argument):
|
|
24
|
+
logger.error(f"Failed to rename {operation.argument}, folder not found")
|
|
25
|
+
operation.delete()
|
|
26
|
+
return
|
|
27
|
+
new_mail_home = operation.mailbox.mail_home
|
|
28
|
+
dirname = os.path.dirname(new_mail_home)
|
|
29
|
+
if not os.path.exists(dirname):
|
|
30
|
+
try:
|
|
31
|
+
os.makedirs(dirname)
|
|
32
|
+
except OSError as e:
|
|
33
|
+
reason = str(e).decode("utf-8")
|
|
34
|
+
logger.critical(
|
|
35
|
+
f"renaming of {operation.argument} to {new_mail_home} failed (reason: {reason})"
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
code, output = exec_cmd(f"mv {operation.argument} {new_mail_home}")
|
|
39
|
+
if code:
|
|
40
|
+
logger.critical(f"Renaming of {new_mail_home} failed (reason: {output})")
|
|
41
|
+
return
|
|
42
|
+
operation.delete()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def delete_mailbox(operation):
|
|
46
|
+
"""Delete the mailbox folder through a RQ Job."""
|
|
47
|
+
if not os.path.exists(operation.argument):
|
|
48
|
+
logger.error(f"Failed to delete {operation.argument}, folder not found")
|
|
49
|
+
operation.delete()
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
def onerror(function, path, excinfo):
|
|
53
|
+
"""Handle errors."""
|
|
54
|
+
logger.critical(f"delete failed (reason: {excinfo})")
|
|
55
|
+
operation.delete()
|
|
56
|
+
|
|
57
|
+
shutil.rmtree(operation.argument, False, onerror)
|
|
58
|
+
operation.delete()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def handle_mailbox_operations():
|
|
62
|
+
load_admin_settings()
|
|
63
|
+
if not param_tools.get_global_parameter("handle_mailboxes"):
|
|
64
|
+
return
|
|
65
|
+
for ope in models.MailboxOperation.objects.all():
|
|
66
|
+
if ope.type == "rename":
|
|
67
|
+
rename_mailbox(ope)
|
|
68
|
+
elif ope.type == "delete":
|
|
69
|
+
delete_mailbox(ope)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def launch_domain_dns_checks(domain_id: int):
|
|
73
|
+
domain = models.Domain.objects.get(id=domain_id)
|
|
74
|
+
DNSChecker().run(domain)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handle_dns_checks():
|
|
78
|
+
"""Launch DNS checks for every possible domain."""
|
|
79
|
+
minute = timezone.now().minute
|
|
80
|
+
queue = django_rq.get_queue("modoboa")
|
|
81
|
+
for domain in models.Domain.objects.annotate(slot=F("id") % 60).filter(
|
|
82
|
+
enable_dns_checks=True, slot=minute
|
|
83
|
+
):
|
|
84
|
+
if domain.uses_a_reserved_tld:
|
|
85
|
+
continue
|
|
86
|
+
queue.enqueue(launch_domain_dns_checks, domain.id)
|
|
@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand
|
|
|
5
5
|
from .subcommands._export import ExportCommand
|
|
6
6
|
from .subcommands._import import ImportCommand
|
|
7
7
|
from .subcommands._manage_dkim_keys import ManageDKIMKeys
|
|
8
|
-
from .subcommands._mx import CheckMXRecords
|
|
9
8
|
from .subcommands._repair import Repair
|
|
10
9
|
|
|
11
10
|
|
|
@@ -20,7 +19,6 @@ class Command(BaseCommand):
|
|
|
20
19
|
subcommands = {
|
|
21
20
|
"export": ExportCommand,
|
|
22
21
|
"import": ImportCommand,
|
|
23
|
-
"check_mx": CheckMXRecords,
|
|
24
22
|
"manage_dkim_keys": ManageDKIMKeys,
|
|
25
23
|
"repair": Repair,
|
|
26
24
|
}
|
|
@@ -4,10 +4,10 @@ import os
|
|
|
4
4
|
import shutil
|
|
5
5
|
from unittest import mock
|
|
6
6
|
|
|
7
|
-
from django.core.management import call_command
|
|
8
7
|
from django.test import override_settings
|
|
9
8
|
from django.urls import reverse
|
|
10
9
|
|
|
10
|
+
from modoboa.admin import jobs
|
|
11
11
|
from modoboa.lib.tests import ModoAPITestCase
|
|
12
12
|
from .. import factories, models
|
|
13
13
|
|
|
@@ -45,7 +45,7 @@ class MailboxOperationTestCase(ModoAPITestCase):
|
|
|
45
45
|
self.client.post(
|
|
46
46
|
reverse("v2:account-delete", args=[mb.user.pk]), {}, format="json"
|
|
47
47
|
)
|
|
48
|
-
|
|
48
|
+
jobs.handle_mailbox_operations()
|
|
49
49
|
self.assertFalse(models.MailboxOperation.objects.exists())
|
|
50
50
|
self.assertFalse(os.path.exists(mb.mail_home))
|
|
51
51
|
|
|
@@ -71,7 +71,7 @@ class MailboxOperationTestCase(ModoAPITestCase):
|
|
|
71
71
|
self.assertEqual(response.status_code, 200)
|
|
72
72
|
path = f"{self.workdir}/test.com/admin2"
|
|
73
73
|
mail_home_mock.__get__ = mock.Mock(return_value=path)
|
|
74
|
-
|
|
74
|
+
jobs.handle_mailbox_operations()
|
|
75
75
|
self.assertFalse(models.MailboxOperation.objects.exists())
|
|
76
76
|
self.assertTrue(os.path.exists(mb.mail_home))
|
|
77
77
|
|
|
@@ -84,6 +84,6 @@ class MailboxOperationTestCase(ModoAPITestCase):
|
|
|
84
84
|
self.client.post(
|
|
85
85
|
reverse("v2:domain-delete", args=[domain.pk]), {}, format="json"
|
|
86
86
|
)
|
|
87
|
-
|
|
87
|
+
jobs.handle_mailbox_operations()
|
|
88
88
|
self.assertFalse(models.MailboxOperation.objects.exists())
|
|
89
89
|
self.assertFalse(os.path.exists(path))
|
modoboa/admin/tests/test_mx.py
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
from unittest import mock
|
|
4
4
|
|
|
5
5
|
import dns.resolver
|
|
6
|
+
from freezegun import freeze_time
|
|
7
|
+
from rq import SimpleWorker
|
|
6
8
|
from testfixtures import LogCapture
|
|
7
9
|
|
|
8
|
-
from django.core import mail
|
|
10
|
+
from django.core import mail
|
|
9
11
|
from django.test import override_settings
|
|
10
12
|
from django.utils.translation import gettext as _
|
|
11
13
|
|
|
14
|
+
import django_rq
|
|
15
|
+
|
|
16
|
+
from modoboa.admin import jobs
|
|
17
|
+
from modoboa.admin.dns_checker import DNSChecker
|
|
12
18
|
from modoboa.core import models as core_models, factories as core_factories
|
|
13
19
|
from modoboa.lib.tests import ModoTestCase
|
|
14
20
|
from . import utils
|
|
@@ -44,59 +50,38 @@ class MXTestCase(ModoTestCase):
|
|
|
44
50
|
cls.localconfig.save()
|
|
45
51
|
models.MXRecord.objects.all().delete()
|
|
46
52
|
|
|
47
|
-
@mock.patch("
|
|
53
|
+
@mock.patch("socket.gethostbyname")
|
|
48
54
|
@mock.patch("socket.getaddrinfo")
|
|
49
55
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
50
|
-
def
|
|
51
|
-
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname
|
|
52
|
-
):
|
|
56
|
+
def test_dns_checker(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
|
|
53
57
|
"""Check that command works fine."""
|
|
54
58
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
55
59
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
56
60
|
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
61
|
+
self.set_global_parameter("enable_dnsbl_checks", False)
|
|
62
|
+
|
|
57
63
|
self.assertEqual(models.MXRecord.objects.count(), 0)
|
|
58
|
-
|
|
64
|
+
with LogCapture("modoboa.dns"):
|
|
65
|
+
DNSChecker().run(self.domain, ttl=0)
|
|
59
66
|
self.assertTrue(models.MXRecord.objects.filter(domain=self.domain).exists())
|
|
60
67
|
|
|
61
68
|
# we passed a ttl to 0. this will reset the cache this time
|
|
62
69
|
qs = models.MXRecord.objects.filter(domain=self.domain)
|
|
63
70
|
id_ = qs[0].id
|
|
64
|
-
|
|
71
|
+
with LogCapture("modoboa.dns"):
|
|
72
|
+
DNSChecker().run(self.domain)
|
|
65
73
|
qs = models.MXRecord.objects.filter(domain=self.domain)
|
|
66
74
|
self.assertNotEqual(id_, qs[0].id)
|
|
67
75
|
id_ = qs[0].id
|
|
68
76
|
|
|
69
77
|
# assume that mxrecords ids are the same. means that we taking care of
|
|
70
78
|
# ttl
|
|
71
|
-
|
|
79
|
+
with LogCapture("modoboa.dns"):
|
|
80
|
+
DNSChecker().run(self.domain)
|
|
72
81
|
qs = models.MXRecord.objects.filter(domain=self.domain)
|
|
73
82
|
self.assertEqual(id_, qs[0].id)
|
|
74
83
|
|
|
75
|
-
@mock.patch("
|
|
76
|
-
@mock.patch("socket.getaddrinfo")
|
|
77
|
-
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
78
|
-
def test_single_domain_update(
|
|
79
|
-
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname
|
|
80
|
-
):
|
|
81
|
-
"""Update only one domain."""
|
|
82
|
-
mock_query.side_effect = utils.mock_dns_query_result
|
|
83
|
-
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
84
|
-
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
85
|
-
management.call_command("modo", "check_mx", "--domain", self.domain.name)
|
|
86
|
-
self.assertTrue(models.MXRecord.objects.filter(domain=self.domain).exists())
|
|
87
|
-
self.assertFalse(
|
|
88
|
-
models.MXRecord.objects.filter(domain=self.bad_domain).exists()
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
management.call_command("modo", "check_mx", "--domain", str(self.bad_domain.pk))
|
|
92
|
-
self.assertFalse(
|
|
93
|
-
models.MXRecord.objects.filter(domain=self.bad_domain).exists()
|
|
94
|
-
)
|
|
95
|
-
self.assertEqual(len(mail.outbox), 1)
|
|
96
|
-
|
|
97
|
-
management.call_command("modo", "check_mx", "--domain", "toto.com")
|
|
98
|
-
|
|
99
|
-
@mock.patch("gevent.socket.gethostbyname")
|
|
84
|
+
@mock.patch("socket.gethostbyname")
|
|
100
85
|
@mock.patch("socket.getaddrinfo")
|
|
101
86
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
102
87
|
def test_invalid_mx(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
|
|
@@ -104,6 +89,8 @@ class MXTestCase(ModoTestCase):
|
|
|
104
89
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
105
90
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
106
91
|
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
92
|
+
self.set_global_parameter("enable_dnsbl_checks", False)
|
|
93
|
+
|
|
107
94
|
domain = factories.DomainFactory(name="invalid-mx.com")
|
|
108
95
|
# Add domain admin with mailbox
|
|
109
96
|
mb = factories.MailboxFactory(
|
|
@@ -113,13 +100,11 @@ class MXTestCase(ModoTestCase):
|
|
|
113
100
|
user__groups=("DomainAdmins",),
|
|
114
101
|
)
|
|
115
102
|
domain.add_admin(mb.user)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
103
|
+
with LogCapture("modoboa.dns"):
|
|
104
|
+
DNSChecker().run(domain)
|
|
119
105
|
self.assertEqual(domain.alarms.count(), 1)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
106
|
+
with LogCapture("modoboa.dns"):
|
|
107
|
+
DNSChecker().run(domain)
|
|
123
108
|
self.assertEqual(domain.alarms.count(), 1)
|
|
124
109
|
self.assertEqual(len(mail.outbox), 1)
|
|
125
110
|
|
|
@@ -130,7 +115,8 @@ class MXTestCase(ModoTestCase):
|
|
|
130
115
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
131
116
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
132
117
|
self.set_global_parameter("custom_dns_server", "123.45.67.89")
|
|
133
|
-
|
|
118
|
+
with LogCapture("modoboa.dns"):
|
|
119
|
+
get_domain_mx_list("does-not-exist.example.com")
|
|
134
120
|
|
|
135
121
|
@mock.patch("ipaddress.ip_address")
|
|
136
122
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
@@ -215,7 +201,16 @@ class DNSBLTestCase(ModoTestCase):
|
|
|
215
201
|
"""Create some data."""
|
|
216
202
|
super().setUpTestData()
|
|
217
203
|
cls.domain = factories.DomainFactory(name="modoboa.org")
|
|
218
|
-
|
|
204
|
+
# Add domain admin with mailbox
|
|
205
|
+
mb = factories.MailboxFactory(
|
|
206
|
+
address="admin",
|
|
207
|
+
domain=cls.domain,
|
|
208
|
+
user__username="admin@modoboa.org",
|
|
209
|
+
user__groups=("DomainAdmins",),
|
|
210
|
+
)
|
|
211
|
+
cls.domain.add_admin(mb.user)
|
|
212
|
+
|
|
213
|
+
cls.domain4 = factories.DomainFactory(name="does-not-exist.example.com")
|
|
219
214
|
cls.domain2 = factories.DomainFactory(
|
|
220
215
|
name="test.localhost"
|
|
221
216
|
) # Should not be checked
|
|
@@ -224,18 +219,29 @@ class DNSBLTestCase(ModoTestCase):
|
|
|
224
219
|
)
|
|
225
220
|
models.DNSBLResult.objects.all().delete()
|
|
226
221
|
|
|
227
|
-
@mock.patch("
|
|
222
|
+
@mock.patch("socket.gethostbyname")
|
|
228
223
|
@mock.patch("socket.getaddrinfo")
|
|
229
224
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
230
|
-
def
|
|
231
|
-
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname
|
|
232
|
-
):
|
|
225
|
+
def test_job(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
|
|
233
226
|
"""Check that command works fine."""
|
|
234
227
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
235
228
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
236
229
|
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
237
230
|
self.assertEqual(models.DNSBLResult.objects.count(), 0)
|
|
238
|
-
|
|
231
|
+
|
|
232
|
+
with freeze_time(f"2026-01-12 14:{self.domain.id % 60:02}"):
|
|
233
|
+
jobs.handle_dns_checks()
|
|
234
|
+
with freeze_time(f"2026-01-12 14:{self.domain2.id % 60:02}"):
|
|
235
|
+
jobs.handle_dns_checks()
|
|
236
|
+
with freeze_time(f"2026-01-12 14:{self.domain3.id % 60:02}"):
|
|
237
|
+
jobs.handle_dns_checks()
|
|
238
|
+
with freeze_time(f"2026-01-12 14:{self.domain4.id % 60:02}"):
|
|
239
|
+
jobs.handle_dns_checks()
|
|
240
|
+
|
|
241
|
+
queue = django_rq.get_queue("modoboa")
|
|
242
|
+
worker = SimpleWorker([queue], connection=queue.connection)
|
|
243
|
+
worker.work(burst=True)
|
|
244
|
+
|
|
239
245
|
self.assertTrue(models.DNSBLResult.objects.filter(domain=self.domain).exists())
|
|
240
246
|
self.assertFalse(
|
|
241
247
|
models.DNSBLResult.objects.filter(domain=self.domain3).exists()
|
|
@@ -243,7 +249,7 @@ class DNSBLTestCase(ModoTestCase):
|
|
|
243
249
|
self.assertFalse(self.domain.uses_a_reserved_tld)
|
|
244
250
|
self.assertTrue(self.domain2.uses_a_reserved_tld)
|
|
245
251
|
|
|
246
|
-
@mock.patch("
|
|
252
|
+
@mock.patch("socket.gethostbyname")
|
|
247
253
|
@mock.patch("socket.getaddrinfo")
|
|
248
254
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
249
255
|
def test_notifications(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname):
|
|
@@ -251,23 +257,25 @@ class DNSBLTestCase(ModoTestCase):
|
|
|
251
257
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
252
258
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
253
259
|
mock_g_gethostbyname.return_value = "127.0.0.4"
|
|
254
|
-
|
|
255
|
-
|
|
260
|
+
with LogCapture("modoboa.dns"):
|
|
261
|
+
DNSChecker().run(self.domain)
|
|
262
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
256
263
|
|
|
257
|
-
@mock.patch("
|
|
264
|
+
@mock.patch("socket.gethostbyname")
|
|
258
265
|
@mock.patch("socket.getaddrinfo")
|
|
259
266
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
260
267
|
def test_notifications_wrong_dnsbl_response(
|
|
261
268
|
self, mock_query, mock_getaddrinfo, mock_g_gethostbyname
|
|
262
269
|
):
|
|
263
270
|
"""Check notifications."""
|
|
271
|
+
self.set_global_parameter("enable_dnsbl_checks", True)
|
|
264
272
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
265
273
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
266
274
|
mock_g_gethostbyname.return_value = "127.255.255.254" # <--Spamhaus response when querying from an open resolver
|
|
267
|
-
|
|
268
|
-
self.assertEqual(len(mail.outbox),
|
|
275
|
+
DNSChecker().run(self.domain)
|
|
276
|
+
self.assertEqual(len(mail.outbox), 0)
|
|
269
277
|
|
|
270
|
-
@mock.patch("
|
|
278
|
+
@mock.patch("socket.gethostbyname")
|
|
271
279
|
@mock.patch("socket.getaddrinfo")
|
|
272
280
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
273
281
|
def test_management_command_no_dnsbl(
|
|
@@ -277,8 +285,10 @@ class DNSBLTestCase(ModoTestCase):
|
|
|
277
285
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
278
286
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
279
287
|
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
288
|
+
self.set_global_parameter("enable_dnsbl_checks", False)
|
|
280
289
|
self.assertEqual(models.DNSBLResult.objects.count(), 0)
|
|
281
|
-
|
|
290
|
+
with LogCapture("modoboa.dns"):
|
|
291
|
+
DNSChecker().run(self.domain)
|
|
282
292
|
self.assertFalse(models.DNSBLResult.objects.filter(domain=self.domain).exists())
|
|
283
293
|
|
|
284
294
|
|
|
@@ -291,7 +301,7 @@ class DNSChecksTestCase(ModoTestCase):
|
|
|
291
301
|
super().setUpTestData()
|
|
292
302
|
cls.domain = factories.DomainFactory(name="dns-checks.com")
|
|
293
303
|
|
|
294
|
-
@mock.patch("
|
|
304
|
+
@mock.patch("socket.gethostbyname")
|
|
295
305
|
@mock.patch("socket.getaddrinfo")
|
|
296
306
|
@mock.patch.object(dns.resolver.Resolver, "resolve")
|
|
297
307
|
def test_management_command(
|
|
@@ -301,12 +311,14 @@ class DNSChecksTestCase(ModoTestCase):
|
|
|
301
311
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
302
312
|
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
|
303
313
|
mock_g_gethostbyname.return_value = "1.2.3.4"
|
|
314
|
+
self.set_global_parameter("enable_dnsbl_checks", False)
|
|
304
315
|
|
|
305
316
|
self.domain.enable_dkim = True
|
|
306
317
|
self.domain.dkim_public_key = "XXXXX"
|
|
307
318
|
self.domain.save(update_fields=["enable_dkim", "dkim_public_key"])
|
|
308
319
|
|
|
309
|
-
|
|
320
|
+
with LogCapture("modoboa.dns"):
|
|
321
|
+
DNSChecker().run(self.domain, ttl=0)
|
|
310
322
|
|
|
311
323
|
self.assertIsNot(self.domain.spf_record, None)
|
|
312
324
|
self.assertIsNot(self.domain.dkim_record, None)
|