modoboa 2.6.4__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.
Files changed (364) hide show
  1. modoboa/admin/api/v2/serializers.py +1 -0
  2. modoboa/admin/app_settings.py +12 -0
  3. modoboa/admin/{management/commands/subcommands/_mx.py → dns_checker.py} +41 -111
  4. modoboa/admin/jobs.py +86 -0
  5. modoboa/admin/management/commands/modo.py +0 -2
  6. modoboa/admin/tests/test_mailbox_operations.py +4 -4
  7. modoboa/admin/tests/test_mx.py +68 -56
  8. modoboa/amavis/jobs.py +11 -0
  9. modoboa/amavis/tests/test_jobs.py +18 -0
  10. modoboa/amavis/tests/test_viewsets.py +2 -3
  11. modoboa/autoconfig/templates/autoconfig/autoconfig.xml +2 -2
  12. modoboa/autoconfig/templates/autoconfig/autodiscover.xml +14 -0
  13. modoboa/autoconfig/tests.py +2 -1
  14. modoboa/autoconfig/views.py +11 -3
  15. modoboa/autoreply/models.py +4 -4
  16. modoboa/calendars/backends/caldav_.py +17 -13
  17. modoboa/calendars/jobs.py +7 -0
  18. modoboa/calendars/mocks.py +4 -1
  19. modoboa/calendars/serializers.py +14 -9
  20. modoboa/calendars/tests.py +10 -9
  21. modoboa/calendars/viewsets.py +3 -1
  22. modoboa/contacts/migrations/0008_addressbook_syncing.py +18 -0
  23. modoboa/contacts/models.py +1 -0
  24. modoboa/contacts/serializers.py +5 -2
  25. modoboa/contacts/tasks.py +9 -3
  26. modoboa/contacts/tests.py +32 -6
  27. modoboa/contacts/viewsets.py +7 -1
  28. modoboa/core/api/v2/serializers.py +0 -7
  29. modoboa/core/api/v2/tests.py +0 -10
  30. modoboa/core/app_settings.py +0 -22
  31. modoboa/core/commands/deploy.py +13 -0
  32. modoboa/core/commands/templates/cron_config.py.tpl +33 -0
  33. modoboa/core/commands/templates/settings.py.tpl +21 -0
  34. modoboa/core/jobs.py +34 -0
  35. modoboa/core/management/commands/load_initial_data.py +1 -1
  36. modoboa/core/password_hashers/base.py +4 -1
  37. modoboa/core/tests/test_core.py +0 -14
  38. modoboa/core/tests/test_jobs.py +40 -0
  39. modoboa/frontend_dist/assets/{AccountAliasForm-tLHotdIR.js → AccountAliasForm-DO6DwfjE.js} +1 -1
  40. modoboa/frontend_dist/assets/{AccountEditView-DnQIIg47.js → AccountEditView-CCuN9mGB.js} +1 -1
  41. modoboa/frontend_dist/assets/AccountLayout-Ge7fzuZg.js +1 -0
  42. modoboa/frontend_dist/assets/AccountPasswordSubForm-PkFPblkR.js +1 -0
  43. modoboa/frontend_dist/assets/AccountView-dgseekZ8.js +1 -0
  44. modoboa/frontend_dist/assets/AddressBook-DZWOHOJj.js +1 -0
  45. modoboa/frontend_dist/assets/AdminLayout-itB_mmH_.js +1 -0
  46. modoboa/frontend_dist/assets/AlarmsView-King6zb6.js +1 -0
  47. modoboa/frontend_dist/assets/{AliasEditView-n75NEN_s.js → AliasEditView-CTjrXYPf.js} +1 -1
  48. modoboa/frontend_dist/assets/AliasRecipientForm-CI7bXqVp.js +1 -0
  49. modoboa/frontend_dist/assets/{AliasView-DFIvPeIm.js → AliasView-BN13MNN_.js} +1 -1
  50. modoboa/frontend_dist/assets/AuditTrailView-C1jQwgoo.js +1 -0
  51. modoboa/frontend_dist/assets/CalendarView-juHVIHU5.css +1 -0
  52. modoboa/frontend_dist/assets/CalendarView-rRSzqxrH.js +1 -0
  53. modoboa/frontend_dist/assets/ChoiceField-DCr12shR.js +1 -0
  54. modoboa/frontend_dist/assets/ComposeEmailForm-BmtKwFb1.js +2 -0
  55. modoboa/frontend_dist/assets/ComposeEmailForm-CVNDl-Mq.css +1 -0
  56. modoboa/frontend_dist/assets/ComposeEmailView-JW22Phrb.js +1 -0
  57. modoboa/frontend_dist/assets/ConfirmDialog-CWpdwSQ6.js +1 -0
  58. modoboa/frontend_dist/assets/ConnectedLayout-BP8pO27H.js +1 -0
  59. modoboa/frontend_dist/assets/{ConnectedLayout-C6HNXWkp.css → ConnectedLayout-Ddpb_6yT.css} +1 -1
  60. modoboa/frontend_dist/assets/CreationForm-CzelJsVQ.js +1 -0
  61. modoboa/frontend_dist/assets/DashboardView-BBkBodVj.js +1 -0
  62. modoboa/frontend_dist/assets/{DomainAdminList-BjtfbhRj.js → DomainAdminList-DD7p3i6F.js} +1 -1
  63. modoboa/frontend_dist/assets/{DomainEditView-Cq5Ea4RW.js → DomainEditView-DoIoNYC4.js} +1 -1
  64. modoboa/frontend_dist/assets/{DomainTransportForm-DAXJGNLA.js → DomainTransportForm-CbiJF9z5.js} +1 -1
  65. modoboa/frontend_dist/assets/{DomainView-BfpK7U14.js → DomainView-BXvznFYz.js} +3 -3
  66. modoboa/frontend_dist/assets/DomainsView-ffYjiffp.js +1 -0
  67. modoboa/frontend_dist/assets/EmailField-KhhJYA4D.js +1 -0
  68. modoboa/frontend_dist/assets/EmailSchedulingForm-CQL5Vfdr.js +1 -0
  69. modoboa/frontend_dist/assets/EmailView-Bq2bHZBO.js +1 -0
  70. modoboa/frontend_dist/assets/EmptyLayout-NrTftp18.js +1 -0
  71. modoboa/frontend_dist/assets/FiltersView-SOQy0U_3.js +1 -0
  72. modoboa/frontend_dist/assets/ForwardEmailView-kEPDjWUw.js +1 -0
  73. modoboa/frontend_dist/assets/{HtmlEditor-D753fIAJ.js → HtmlEditor-CyBl5wj2.js} +15 -15
  74. modoboa/frontend_dist/assets/IdentitiesView-CZFf4oR9.js +1 -0
  75. modoboa/frontend_dist/assets/InformationView-CAtneAqM.js +1 -0
  76. modoboa/frontend_dist/assets/{LoadingData-Tm56XNwj.js → LoadingData-C2txD49L.js} +1 -1
  77. modoboa/frontend_dist/assets/{LoginCallbackView-DjCHUgz5.js → LoginCallbackView-DAvty3CI.js} +1 -1
  78. modoboa/frontend_dist/assets/{LoginView-D0r4XBEl.js → LoginView-DHYITkn4.js} +1 -1
  79. modoboa/frontend_dist/assets/MailboxView-BNg2v7mi.css +1 -0
  80. modoboa/frontend_dist/assets/MailboxView-DHg5A78D.js +5 -0
  81. modoboa/frontend_dist/assets/MenuItems-C9p70bSC.js +1 -0
  82. modoboa/frontend_dist/assets/{MessageView-BWh9BqzS.js → MessageView-CrrEJpNI.js} +1 -1
  83. modoboa/frontend_dist/assets/MessagesView-CyecRd4I.js +1 -0
  84. modoboa/frontend_dist/assets/MigrationsView-DXGKKz2h.js +1 -0
  85. modoboa/frontend_dist/assets/{ParametersForm-y02-GM0r.js → ParametersForm-DFKYkPAs.js} +1 -1
  86. modoboa/frontend_dist/assets/ParametersView-B1wT5oyt.js +1 -0
  87. modoboa/frontend_dist/assets/ParametersView-CfvVzKea.js +1 -0
  88. modoboa/frontend_dist/assets/ProviderEditView-De-8ggBp.js +1 -0
  89. modoboa/frontend_dist/assets/ProviderGeneralForm-DO-IpJMQ.js +1 -0
  90. modoboa/frontend_dist/assets/ProvidersView-Bhk7sCz4.js +1 -0
  91. modoboa/frontend_dist/assets/QuarantineLayout-C1YszNmP.js +1 -0
  92. modoboa/frontend_dist/assets/QuarantineView-cElD_rS-.js +1 -0
  93. modoboa/frontend_dist/assets/ReplyEmailView-BvuOZdLl.js +1 -0
  94. modoboa/frontend_dist/assets/ResourcesForm-FWxrmVwo.js +1 -0
  95. modoboa/frontend_dist/assets/SelfServiceLayout-JHRvfvQf.js +1 -0
  96. modoboa/frontend_dist/assets/SettingsView-SIsKAXtQ.css +1 -0
  97. modoboa/frontend_dist/assets/SettingsView-s2l2Xl1L.js +6 -0
  98. modoboa/frontend_dist/assets/StatisticsView-FiidWvad.js +1 -0
  99. modoboa/frontend_dist/assets/TimeSerieChart-CdqUHiO_.js +1 -0
  100. modoboa/frontend_dist/assets/TimeSerieChart-nLIFGI0y.css +1 -0
  101. modoboa/frontend_dist/assets/UserLayout-DyvI8duf.js +1 -0
  102. modoboa/frontend_dist/assets/{VAlert-CNys_LUS.js → VAlert-MDbeolOo.js} +1 -1
  103. modoboa/frontend_dist/assets/VApp-DM1KLQfQ.js +1 -0
  104. modoboa/frontend_dist/assets/VAutocomplete-C9NL5_uo.css +1 -0
  105. modoboa/frontend_dist/assets/VAutocomplete-CQsWYWNX.js +1 -0
  106. modoboa/frontend_dist/assets/VAvatar-CXV_FqpP.js +1 -0
  107. modoboa/frontend_dist/assets/VBadge-7tDx7aI3.js +1 -0
  108. modoboa/frontend_dist/assets/{VCard-BoH6kjPa.js → VCard-CBtX8JF-.js} +1 -1
  109. modoboa/frontend_dist/assets/VCheckbox-zY1MApOy.js +1 -0
  110. modoboa/frontend_dist/assets/{VCheckboxBtn-Ccl786Rg.js → VCheckboxBtn-B0mIT3E0.js} +1 -1
  111. modoboa/frontend_dist/assets/VColorPicker-BbCHvk6K.js +1 -0
  112. modoboa/frontend_dist/assets/{VColorPicker-B_lVDaYR.css → VColorPicker-C9m8L-6U.css} +1 -1
  113. modoboa/frontend_dist/assets/{VContainer-DKMcKasC.js → VContainer-Cn-vKB3s.js} +1 -1
  114. modoboa/frontend_dist/assets/VDataTable-CNT9KOSp.js +1 -0
  115. modoboa/frontend_dist/assets/VDataTableServer-_yn4Ry6U.js +1 -0
  116. modoboa/frontend_dist/assets/VDataTableVirtual-Csxh3Gp6.js +1 -0
  117. modoboa/frontend_dist/assets/VDatePicker-_OUDqShN.css +1 -0
  118. modoboa/frontend_dist/assets/VDatePicker-iFu13xIP.js +2 -0
  119. modoboa/frontend_dist/assets/VDialog-lKtBdqnp.js +1 -0
  120. modoboa/frontend_dist/assets/VExpansionPanels-CaIvk9iF.js +1 -0
  121. modoboa/frontend_dist/assets/VFileInput-C7J_qVmk.js +1 -0
  122. modoboa/frontend_dist/assets/VForm-fdQ5d-CH.js +1 -0
  123. modoboa/frontend_dist/assets/VInput-BwHvhzAe.js +1 -0
  124. modoboa/frontend_dist/assets/VMenu-BpmJf4X2.js +1 -0
  125. modoboa/frontend_dist/assets/VMenu-C5A_5Hs5.css +1 -0
  126. modoboa/frontend_dist/assets/VPicker-B7cB3kJg.css +1 -0
  127. modoboa/frontend_dist/assets/VPicker-BZho70wU.js +1 -0
  128. modoboa/frontend_dist/assets/VProgressCircular-DRw_-iNj.js +1 -0
  129. modoboa/frontend_dist/assets/VRadioGroup--eiP5xtJ.js +1 -0
  130. modoboa/frontend_dist/assets/{VRow-CuVZGqBj.js → VRow-83Qnr5iB.js} +1 -1
  131. modoboa/frontend_dist/assets/VSelect-BcmGFGif.js +1 -0
  132. modoboa/frontend_dist/assets/{VSelectionControl-uKY9Zull.js → VSelectionControl-Cqi1xt-q.js} +1 -1
  133. modoboa/frontend_dist/assets/VSheet-C3MaHhtw.js +1 -0
  134. modoboa/frontend_dist/assets/VSpacer-CuSkdJZL.js +1 -0
  135. modoboa/frontend_dist/assets/VSwitch-XumUl685.js +1 -0
  136. modoboa/frontend_dist/assets/{VTable-gDsAUSLX.js → VTable-f7wcr2AZ.js} +1 -1
  137. modoboa/frontend_dist/assets/VTabs-CDfdejXj.css +1 -0
  138. modoboa/frontend_dist/assets/VTabs-y4wNP4im.js +1 -0
  139. modoboa/frontend_dist/assets/VTextField-BPIvtrn4.js +1 -0
  140. modoboa/frontend_dist/assets/VTextField-DjbYGlzs.css +1 -0
  141. modoboa/frontend_dist/assets/VTextarea-B6bGYcC3.js +1 -0
  142. modoboa/frontend_dist/assets/VTextarea-f6vTjzFy.css +1 -0
  143. modoboa/frontend_dist/assets/VToolbar-BYDPtwf0.css +1 -0
  144. modoboa/frontend_dist/assets/VToolbar-CB6wwtYc.js +1 -0
  145. modoboa/frontend_dist/assets/VWindowItem-CDtLLEkg.js +1 -0
  146. modoboa/frontend_dist/assets/WebmailLayout-CQQAolnl.css +1 -0
  147. modoboa/frontend_dist/assets/WebmailLayout-tosQSHLS.js +1 -0
  148. modoboa/frontend_dist/assets/{accounts-DqbBlaLK.js → accounts-KUsk6LHW.js} +1 -1
  149. modoboa/frontend_dist/assets/{admin-D36m_nXq.js → admin-BW9cZW0P.js} +1 -1
  150. modoboa/frontend_dist/assets/{aliases-BtyF8GC8.js → aliases-Ge0hjIsH.js} +1 -1
  151. modoboa/frontend_dist/assets/{amavis-DySmsCJQ.js → amavis-BbFeFfsk.js} +1 -1
  152. modoboa/frontend_dist/assets/{amavis-B4QhM5Fa.js → amavis-DtuzP_CS.js} +1 -1
  153. modoboa/frontend_dist/assets/{contacts-Df8ptuMn.js → contacts-DMJlQTe0.js} +1 -1
  154. modoboa/frontend_dist/assets/{domains-CSxtTvUh.js → domains-Du64lcXT.js} +1 -1
  155. modoboa/frontend_dist/assets/domains.store-1U61jeCV.js +1 -0
  156. modoboa/frontend_dist/assets/events-BM3in65C.js +1 -0
  157. modoboa/frontend_dist/assets/filter-Dihm6o59.js +1 -0
  158. modoboa/frontend_dist/assets/importExport-HGcNGWOm.js +1 -0
  159. modoboa/frontend_dist/assets/{index-B9q1vO3K.css → index-B1EK3MQe.css} +1 -1
  160. modoboa/frontend_dist/assets/{index-Bp9Fb67E.js → index-Dv00bmw9.js} +1 -1
  161. modoboa/frontend_dist/assets/index-jui3edpn.js +1001 -0
  162. modoboa/frontend_dist/assets/{language.store-_-zrrEiS.js → language.store-OcfdXL_-.js} +1 -1
  163. modoboa/frontend_dist/assets/languages-CF8hxo7x.js +1 -0
  164. modoboa/frontend_dist/assets/{layout-CjSP_k58.js → layout-DOO7TRTJ.js} +1 -1
  165. modoboa/frontend_dist/assets/{layout.store-CF5WRevH.js → layout.store-C0g-piJn.js} +1 -1
  166. modoboa/frontend_dist/assets/{logos-95XYS6uH.js → logos-Dz2Gzei-.js} +1 -1
  167. modoboa/frontend_dist/assets/{logs-CyU_PIDh.js → logs-CLm32Weu.js} +1 -1
  168. modoboa/frontend_dist/assets/{parameters-BxAXBXU1.js → parameters-DMIAQ7cd.js} +1 -1
  169. modoboa/frontend_dist/assets/{parameters.store-CATOSoQZ.js → parameters.store-DLnFzCwV.js} +1 -1
  170. modoboa/frontend_dist/assets/{permissions-cSkGqV3M.js → permissions-CrpE0b4w.js} +1 -1
  171. modoboa/frontend_dist/assets/{ssrBoot-05WtbG6H.js → ssrBoot-B7cr7q9U.js} +1 -1
  172. modoboa/frontend_dist/assets/{tag-BmV84V2s.js → tag-WF93n81Q.js} +1 -1
  173. modoboa/frontend_dist/assets/{theme-myV4ekXo.js → theme-_0oOYChG.js} +1 -1
  174. modoboa/frontend_dist/assets/transports-CS61syt-.js +1 -0
  175. modoboa/frontend_dist/assets/webmail-CYDXU0DS.js +1 -0
  176. modoboa/frontend_dist/assets/webmail.store-BvHCQSjM.js +1 -0
  177. modoboa/frontend_dist/index.html +2 -2
  178. modoboa/imap_migration/api/v2/serializers.py +13 -0
  179. modoboa/imap_migration/models.py +3 -3
  180. modoboa/imap_migration/templates/imap_migration/offlineimap.conf +2 -2
  181. modoboa/imap_migration/tests.py +28 -0
  182. modoboa/lib/cryptutils.py +2 -2
  183. modoboa/lib/sysutils.py +1 -1
  184. modoboa/lib/tests/__init__.py +0 -1
  185. modoboa/locale/br/LC_MESSAGES/django.mo +0 -0
  186. modoboa/locale/br/LC_MESSAGES/django.po +753 -317
  187. modoboa/locale/cs/LC_MESSAGES/django.po +671 -309
  188. modoboa/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  189. modoboa/locale/cs_CZ/LC_MESSAGES/django.po +801 -378
  190. modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
  191. modoboa/locale/de/LC_MESSAGES/django.po +1116 -455
  192. modoboa/locale/de_DE/LC_MESSAGES/django.mo +0 -0
  193. modoboa/locale/de_DE/LC_MESSAGES/django.po +745 -318
  194. modoboa/locale/el_GR/LC_MESSAGES/django.mo +0 -0
  195. modoboa/locale/el_GR/LC_MESSAGES/django.po +825 -393
  196. modoboa/locale/en/LC_MESSAGES/django.po +669 -309
  197. modoboa/locale/es/LC_MESSAGES/django.mo +0 -0
  198. modoboa/locale/es/LC_MESSAGES/django.po +810 -390
  199. modoboa/locale/es_MX/LC_MESSAGES/django.po +669 -309
  200. modoboa/locale/fi/LC_MESSAGES/django.mo +0 -0
  201. modoboa/locale/fi/LC_MESSAGES/django.po +756 -316
  202. modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
  203. modoboa/locale/fr/LC_MESSAGES/django.po +674 -313
  204. modoboa/locale/hu/LC_MESSAGES/django.po +669 -309
  205. modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
  206. modoboa/locale/it/LC_MESSAGES/django.po +807 -380
  207. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  208. modoboa/locale/ja_JP/LC_MESSAGES/django.po +951 -419
  209. modoboa/locale/ka/LC_MESSAGES/django.po +669 -309
  210. modoboa/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  211. modoboa/locale/nl_NL/LC_MESSAGES/django.po +807 -387
  212. modoboa/locale/no/LC_MESSAGES/django.po +669 -309
  213. modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  214. modoboa/locale/pl_PL/LC_MESSAGES/django.po +826 -396
  215. modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  216. modoboa/locale/pt_BR/LC_MESSAGES/django.po +830 -392
  217. modoboa/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  218. modoboa/locale/pt_PT/LC_MESSAGES/django.po +788 -371
  219. modoboa/locale/ro_RO/LC_MESSAGES/django.mo +0 -0
  220. modoboa/locale/ro_RO/LC_MESSAGES/django.po +763 -316
  221. modoboa/locale/ru/LC_MESSAGES/django.mo +0 -0
  222. modoboa/locale/ru/LC_MESSAGES/django.po +1336 -1046
  223. modoboa/locale/si/LC_MESSAGES/django.po +669 -309
  224. modoboa/locale/sk/LC_MESSAGES/django.po +671 -309
  225. modoboa/locale/sk_SK/LC_MESSAGES/django.po +671 -309
  226. modoboa/locale/sl_SI/LC_MESSAGES/django.po +695 -315
  227. modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
  228. modoboa/locale/sv/LC_MESSAGES/django.po +819 -387
  229. modoboa/locale/tr/LC_MESSAGES/django.mo +0 -0
  230. modoboa/locale/tr/LC_MESSAGES/django.po +737 -315
  231. modoboa/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
  232. modoboa/locale/tr_TR/LC_MESSAGES/django.po +711 -310
  233. modoboa/locale/uk/LC_MESSAGES/django.po +671 -309
  234. modoboa/locale/zh/LC_MESSAGES/django.po +668 -309
  235. modoboa/locale/zh_CN/LC_MESSAGES/django.po +668 -309
  236. modoboa/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  237. modoboa/locale/zh_TW/LC_MESSAGES/django.po +747 -316
  238. modoboa/maillog/jobs.py +11 -0
  239. modoboa/maillog/tests/test_views.py +5 -4
  240. modoboa/parameters/api/v2/tests.py +1 -1
  241. modoboa/policyd/management/commands/policy_daemon.py +5 -1
  242. modoboa/policyd/tests.py +4 -2
  243. modoboa/rspamd/tests.py +20 -0
  244. modoboa/templates/registration/twofactor_code_verify.html +1 -1
  245. modoboa/webmail/app_settings.py +37 -0
  246. modoboa/webmail/constants.py +21 -1
  247. modoboa/webmail/factories.py +19 -0
  248. modoboa/webmail/jobs.py +27 -0
  249. modoboa/webmail/lib/__init__.py +0 -2
  250. modoboa/webmail/lib/imapemail.py +9 -11
  251. modoboa/webmail/lib/imapheader.py +25 -13
  252. modoboa/webmail/lib/imaputils.py +51 -13
  253. modoboa/webmail/lib/sendmail.py +88 -105
  254. modoboa/webmail/lib/utils.py +109 -0
  255. modoboa/webmail/migrations/0001_initial.py +90 -0
  256. modoboa/webmail/migrations/__init__.py +0 -0
  257. modoboa/webmail/mocks.py +12 -3
  258. modoboa/webmail/models.py +102 -0
  259. modoboa/webmail/serializers.py +84 -4
  260. modoboa/webmail/tests/data.py +21 -0
  261. modoboa/webmail/tests/test_lib_imaputils.py +33 -0
  262. modoboa/webmail/tests/test_viewsets.py +108 -0
  263. modoboa/webmail/urls.py +5 -0
  264. modoboa/webmail/viewsets.py +39 -9
  265. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/METADATA +16 -13
  266. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/RECORD +271 -254
  267. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/WHEEL +1 -1
  268. modoboa/admin/management/commands/handle_mailbox_operations.py +0 -103
  269. modoboa/core/management/commands/cleanlogs.py +0 -53
  270. modoboa/frontend_dist/assets/AccountLayout-B2HptJzY.js +0 -1
  271. modoboa/frontend_dist/assets/AccountPasswordSubForm-DpADntAL.js +0 -1
  272. modoboa/frontend_dist/assets/AccountView-CJBGh9Oe.js +0 -1
  273. modoboa/frontend_dist/assets/AddressBook-CrFqzlzZ.js +0 -1
  274. modoboa/frontend_dist/assets/AdminLayout-BbCt7YxX.js +0 -1
  275. modoboa/frontend_dist/assets/AlarmsView-CIlt_IKQ.js +0 -1
  276. modoboa/frontend_dist/assets/AliasRecipientForm-jjj5yJJw.js +0 -1
  277. modoboa/frontend_dist/assets/AuditTrailView-sWnED3E9.js +0 -1
  278. modoboa/frontend_dist/assets/CalendarView-DSCBIvef.js +0 -1
  279. modoboa/frontend_dist/assets/ChoiceField-RDT6XcnX.js +0 -1
  280. modoboa/frontend_dist/assets/ComposeEmailForm-D3yUbBJX.css +0 -1
  281. modoboa/frontend_dist/assets/ComposeEmailForm-G3sXQAwI.js +0 -1
  282. modoboa/frontend_dist/assets/ComposeEmailView-Ce1CtFjD.js +0 -1
  283. modoboa/frontend_dist/assets/ConfirmDialog-Du7aqKbR.js +0 -1
  284. modoboa/frontend_dist/assets/ConnectedLayout-BHWaBAzH.js +0 -1
  285. modoboa/frontend_dist/assets/CreationForm-CjtILq0_.js +0 -1
  286. modoboa/frontend_dist/assets/DashboardView-DFNwusDZ.js +0 -1
  287. modoboa/frontend_dist/assets/DomainsView-CYxLy-3Y.js +0 -1
  288. modoboa/frontend_dist/assets/EmailField-DmPc4pyP.js +0 -1
  289. modoboa/frontend_dist/assets/EmailView-BE3vYZtJ.js +0 -1
  290. modoboa/frontend_dist/assets/EmptyLayout-DMwKNHfI.js +0 -1
  291. modoboa/frontend_dist/assets/FiltersView-Dps3_fAh.js +0 -1
  292. modoboa/frontend_dist/assets/ForwardEmailView-C0I00eXf.js +0 -1
  293. modoboa/frontend_dist/assets/IdentitiesView-CoGBVhZx.js +0 -1
  294. modoboa/frontend_dist/assets/InformationView-eB2ayeLD.js +0 -1
  295. modoboa/frontend_dist/assets/MailboxView-DIVABvk5.css +0 -1
  296. modoboa/frontend_dist/assets/MailboxView-DNokO2WZ.js +0 -5
  297. modoboa/frontend_dist/assets/MenuItems-dl38msRA.js +0 -1
  298. modoboa/frontend_dist/assets/MessagesView-BZ7NUOwo.js +0 -1
  299. modoboa/frontend_dist/assets/MigrationsView-DcOVgmcH.js +0 -1
  300. modoboa/frontend_dist/assets/ParametersView-B6abt-LH.js +0 -1
  301. modoboa/frontend_dist/assets/ParametersView-v0YS6WU4.js +0 -1
  302. modoboa/frontend_dist/assets/ProviderEditView-BaL9mKxJ.js +0 -1
  303. modoboa/frontend_dist/assets/ProviderGeneralForm-eoHMDO5v.js +0 -1
  304. modoboa/frontend_dist/assets/ProvidersView-Zu-G35u7.js +0 -1
  305. modoboa/frontend_dist/assets/QuarantineLayout-BHiiksnP.js +0 -1
  306. modoboa/frontend_dist/assets/QuarantineView-BOXSobRz.js +0 -1
  307. modoboa/frontend_dist/assets/ReplyEmailView-JmSiNWPy.js +0 -1
  308. modoboa/frontend_dist/assets/ResourcesForm-D0YBapCG.js +0 -1
  309. modoboa/frontend_dist/assets/SelfServiceLayout-DUmQzUDk.js +0 -1
  310. modoboa/frontend_dist/assets/SettingsView-DIvKGRBY.css +0 -1
  311. modoboa/frontend_dist/assets/SettingsView-DMuaKagM.js +0 -6
  312. modoboa/frontend_dist/assets/StatisticsView-CyY_CNcI.js +0 -1
  313. modoboa/frontend_dist/assets/TimeSerieChart-BTDBH33K.js +0 -1
  314. modoboa/frontend_dist/assets/TimeSerieChart-C3XHmlRd.css +0 -1
  315. modoboa/frontend_dist/assets/UserLayout-DwxcQr4L.js +0 -1
  316. modoboa/frontend_dist/assets/VApp-B0vRc3-6.js +0 -1
  317. modoboa/frontend_dist/assets/VAutocomplete-Huaz_-hf.js +0 -1
  318. modoboa/frontend_dist/assets/VAutocomplete-hzGuLlUI.css +0 -1
  319. modoboa/frontend_dist/assets/VAvatar-JdJfoK0C.js +0 -1
  320. modoboa/frontend_dist/assets/VBadge-Cg5iu5LG.js +0 -1
  321. modoboa/frontend_dist/assets/VCheckbox-CCLOJFKF.js +0 -1
  322. modoboa/frontend_dist/assets/VColorPicker-452uc3tY.js +0 -1
  323. modoboa/frontend_dist/assets/VDataTable-BkLLfcQK.js +0 -1
  324. modoboa/frontend_dist/assets/VDataTableServer-B_6fWBA3.js +0 -1
  325. modoboa/frontend_dist/assets/VDataTableVirtual-Ike7oqcC.js +0 -1
  326. modoboa/frontend_dist/assets/VDialog-DIUeAIpw.js +0 -1
  327. modoboa/frontend_dist/assets/VExpansionPanels-BeMgiS7A.js +0 -1
  328. modoboa/frontend_dist/assets/VFileInput-Dvj95FL5.js +0 -1
  329. modoboa/frontend_dist/assets/VForm-DkphQD3e.js +0 -1
  330. modoboa/frontend_dist/assets/VInput-CbqehdOu.js +0 -1
  331. modoboa/frontend_dist/assets/VMenu-B6SCtOwT.js +0 -1
  332. modoboa/frontend_dist/assets/VMenu-BEipA1lw.css +0 -1
  333. modoboa/frontend_dist/assets/VPicker-BUx70Wjg.js +0 -1
  334. modoboa/frontend_dist/assets/VPicker-ClSXs6kv.css +0 -1
  335. modoboa/frontend_dist/assets/VProgressCircular-DTDGzl2O.js +0 -1
  336. modoboa/frontend_dist/assets/VRadioGroup-CS9ULZ1c.js +0 -1
  337. modoboa/frontend_dist/assets/VSelect-D5IXDPEX.js +0 -1
  338. modoboa/frontend_dist/assets/VSheet-HeHw6g5_.js +0 -1
  339. modoboa/frontend_dist/assets/VSpacer-BUxSSJbH.js +0 -1
  340. modoboa/frontend_dist/assets/VSwitch-Dh_dserW.js +0 -1
  341. modoboa/frontend_dist/assets/VTabs-5XSICLQP.js +0 -1
  342. modoboa/frontend_dist/assets/VTabs-NzpINroH.css +0 -1
  343. modoboa/frontend_dist/assets/VTextField-Cow3HZvI.css +0 -1
  344. modoboa/frontend_dist/assets/VTextField-QXQFhKxm.js +0 -1
  345. modoboa/frontend_dist/assets/VTextarea-Bdy4UKmV.js +0 -1
  346. modoboa/frontend_dist/assets/VTextarea-DyGjqrlm.css +0 -1
  347. modoboa/frontend_dist/assets/VToolbar-CB2GrZpA.css +0 -1
  348. modoboa/frontend_dist/assets/VToolbar-CyrYSqJZ.js +0 -1
  349. modoboa/frontend_dist/assets/VWindowItem-kZpArwK1.js +0 -1
  350. modoboa/frontend_dist/assets/WebmailLayout-BzW0LWYp.css +0 -1
  351. modoboa/frontend_dist/assets/WebmailLayout-CNUHODnw.js +0 -1
  352. modoboa/frontend_dist/assets/domains.store-D4ZqCQ-O.js +0 -1
  353. modoboa/frontend_dist/assets/filter-DHKXX97M.js +0 -1
  354. modoboa/frontend_dist/assets/global.store-DYMmGKpn.js +0 -1
  355. modoboa/frontend_dist/assets/importExport-C-adpHJX.js +0 -1
  356. modoboa/frontend_dist/assets/index-B1bntsLR.js +0 -995
  357. modoboa/frontend_dist/assets/languages-CxjoT69j.js +0 -1
  358. modoboa/frontend_dist/assets/transports-ChdHV5hX.js +0 -1
  359. modoboa/frontend_dist/assets/webmail-B7MNMkv_.js +0 -1
  360. modoboa/frontend_dist/assets/webmail.store-Cm-h6hhE.js +0 -1
  361. {modoboa-2.6.4.data → modoboa-2.7.0.data}/scripts/modoboa-admin.py +0 -0
  362. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/entry_points.txt +0 -0
  363. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/licenses/LICENSE +0 -0
  364. {modoboa-2.6.4.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)
@@ -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 CheckMXRecords(BaseCommand):
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 = param_tools.get_global_parameter("valid_mxs")
37
+ valid_mxs = self.config["valid_mxs"]
43
38
  return [
44
- ipaddress.ip_network(smart_str(v.strip()))
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(smart_str(ip))
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 provider, results
65
+ return results
103
66
 
104
- def store_dnsbl_result(self, domain, provider, results, **options):
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(self, domain, alerts, subject, tpl, **options):
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
- emails = list(options["email"])
148
- if not options["skip_admin_emails"]:
149
- emails.extend(
150
- domain.admins.exclude(mailbox__isnull=True).values_list(
151
- "email", flat=True
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, **options):
161
- """Check that domain's MX record exist.
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, **options)
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 param_tools.get_global_parameter("enable_mx_checks"):
222
- self.check_valid_mx(domain, mx_list, **options)
187
+ if self.config["enable_mx_checks"]:
188
+ self.check_valid_mx(domain, mx_list)
223
189
 
224
- if param_tools.get_global_parameter("enable_spf_checks"):
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 param_tools.get_global_parameter("enable_dmarc_checks"):
195
+ if self.config["enable_dmarc_checks"]:
233
196
  dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "dmarc", ttl)
234
- if param_tools.get_global_parameter("enable_autoconfig_checks"):
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
- not param_tools.get_global_parameter("enable_dnsbl_checks")
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 job in jobs:
263
- if not job.successful():
264
- continue
265
- provider, results = job.value
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, **options)
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
- call_command("handle_mailbox_operations")
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
- call_command("handle_mailbox_operations")
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
- call_command("handle_mailbox_operations")
87
+ jobs.handle_mailbox_operations()
88
88
  self.assertFalse(models.MailboxOperation.objects.exists())
89
89
  self.assertFalse(os.path.exists(path))
@@ -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, management
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("gevent.socket.gethostbyname")
53
+ @mock.patch("socket.gethostbyname")
48
54
  @mock.patch("socket.getaddrinfo")
49
55
  @mock.patch.object(dns.resolver.Resolver, "resolve")
50
- def test_management_command(
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
- management.call_command("modo", "check_mx", "--no-dnsbl", "--ttl=0")
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
- management.call_command("modo", "check_mx", "--no-dnsbl", "--ttl=7200")
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
- management.call_command("modo", "check_mx", "--no-dnsbl")
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("gevent.socket.gethostbyname")
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
- management.call_command(
117
- "modo", "check_mx", "--domain", domain.name, "--no-dnsbl"
118
- )
103
+ with LogCapture("modoboa.dns"):
104
+ DNSChecker().run(domain)
119
105
  self.assertEqual(domain.alarms.count(), 1)
120
- management.call_command(
121
- "modo", "check_mx", "--domain", domain.name, "--no-dnsbl"
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
- get_domain_mx_list("does-not-exist.example.com")
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
- factories.DomainFactory(name="does-not-exist.example.com")
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("gevent.socket.gethostbyname")
222
+ @mock.patch("socket.gethostbyname")
228
223
  @mock.patch("socket.getaddrinfo")
229
224
  @mock.patch.object(dns.resolver.Resolver, "resolve")
230
- def test_management_command(
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
- management.call_command("modo", "check_mx")
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("gevent.socket.gethostbyname")
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
- management.call_command("modo", "check_mx", "--email", "user@example.test")
255
- self.assertEqual(len(mail.outbox), 2)
260
+ with LogCapture("modoboa.dns"):
261
+ DNSChecker().run(self.domain)
262
+ self.assertEqual(len(mail.outbox), 1)
256
263
 
257
- @mock.patch("gevent.socket.gethostbyname")
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
- management.call_command("modo", "check_mx", "--email", "user@example.test")
268
- self.assertEqual(len(mail.outbox), 1)
275
+ DNSChecker().run(self.domain)
276
+ self.assertEqual(len(mail.outbox), 0)
269
277
 
270
- @mock.patch("gevent.socket.gethostbyname")
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
- management.call_command("modo", "check_mx", "--no-dnsbl")
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("gevent.socket.gethostbyname")
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
- management.call_command("modo", "check_mx", "--no-dnsbl", "--ttl=0")
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)
modoboa/amavis/jobs.py ADDED
@@ -0,0 +1,11 @@
1
+ """Async jobs definition."""
2
+
3
+ from django.core.management import call_command
4
+
5
+
6
+ def amnotify():
7
+ call_command("amnotify")
8
+
9
+
10
+ def qcleanup():
11
+ call_command("qcleanup")