modoboa 2.4.10__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. modoboa/admin/api/v1/serializers.py +1 -0
  2. modoboa/admin/api/v2/serializers.py +15 -1
  3. modoboa/admin/api/v2/tests.py +11 -1
  4. modoboa/admin/lib.py +1 -1
  5. modoboa/admin/models/mailbox.py +15 -13
  6. modoboa/admin/models/mxrecord.py +4 -0
  7. modoboa/admin/tests/test_import_.py +11 -11
  8. modoboa/amavis/__init__.py +3 -0
  9. modoboa/amavis/app_settings.py +276 -0
  10. modoboa/amavis/apps.py +18 -0
  11. modoboa/amavis/checks/__init__.py +2 -0
  12. modoboa/amavis/checks/settings_checks.py +59 -0
  13. modoboa/amavis/dbrouter.py +35 -0
  14. modoboa/amavis/factories.py +164 -0
  15. modoboa/amavis/handlers.py +146 -0
  16. modoboa/amavis/lib.py +381 -0
  17. modoboa/amavis/management/__init__.py +0 -0
  18. modoboa/amavis/management/commands/__init__.py +0 -0
  19. modoboa/amavis/management/commands/amnotify.py +99 -0
  20. modoboa/amavis/management/commands/qcleanup.py +84 -0
  21. modoboa/amavis/migrations/0001_initial.py +340 -0
  22. modoboa/amavis/migrations/__init__.py +0 -0
  23. modoboa/amavis/models.py +226 -0
  24. modoboa/amavis/serializers.py +139 -0
  25. modoboa/amavis/sql_connector.py +240 -0
  26. modoboa/amavis/sql_email.py +66 -0
  27. modoboa/amavis/tasks.py +33 -0
  28. modoboa/amavis/templates/amavis/notifications/pending_requests.html +16 -0
  29. modoboa/amavis/tests/__init__.py +0 -0
  30. modoboa/amavis/tests/sa-learn +3 -0
  31. modoboa/amavis/tests/sample_messages/quarantined-input.txt +80 -0
  32. modoboa/amavis/tests/sample_messages/quarantined-output-plain_nolinks.txt +17 -0
  33. modoboa/amavis/tests/spamc +3 -0
  34. modoboa/amavis/tests/test_checks.py +25 -0
  35. modoboa/amavis/tests/test_handlers.py +214 -0
  36. modoboa/amavis/tests/test_lib.py +90 -0
  37. modoboa/amavis/tests/test_management_commands.py +45 -0
  38. modoboa/amavis/tests/test_sql_email.py +67 -0
  39. modoboa/amavis/tests/test_utils.py +19 -0
  40. modoboa/amavis/tests/test_viewsets.py +319 -0
  41. modoboa/amavis/urls.py +11 -0
  42. modoboa/amavis/utils.py +105 -0
  43. modoboa/amavis/viewsets.py +265 -0
  44. modoboa/core/api/v1/serializers.py +7 -5
  45. modoboa/core/api/v2/serializers.py +13 -2
  46. modoboa/core/api/v2/tests.py +34 -4
  47. modoboa/core/api/v2/urls.py +10 -5
  48. modoboa/core/api/v2/views.py +23 -2
  49. modoboa/core/api/v2/viewsets.py +24 -3
  50. modoboa/core/app_settings.py +11 -0
  51. modoboa/core/fido2_auth.py +1 -2
  52. modoboa/core/handlers.py +6 -2
  53. modoboa/core/management/commands/add_allowed_hosts.py +33 -0
  54. modoboa/core/management/commands/cleanlogs.py +9 -0
  55. modoboa/core/management/commands/load_initial_data.py +10 -0
  56. modoboa/core/migrations/0025_rename_user_email_is_active_core_user_email_c0c03f_idx.py +23 -5
  57. modoboa/core/tests/test_core.py +29 -0
  58. modoboa/core/utils.py +6 -0
  59. modoboa/dnstools/api/v2/serializers.py +9 -11
  60. modoboa/frontend_dist/assets/AccountAliasForm-BuSy_1n9.js +1 -0
  61. modoboa/frontend_dist/assets/AccountEditView-qdJmLM_e.js +1 -0
  62. modoboa/frontend_dist/assets/AccountLayout-DrN7vHsX.js +1 -0
  63. modoboa/frontend_dist/assets/AccountPasswordSubForm-DZGt_Xgq.js +1 -0
  64. modoboa/frontend_dist/assets/AccountView-CO65y0vZ.js +1 -0
  65. modoboa/frontend_dist/assets/AddressBook-BZNUlhek.js +1 -0
  66. modoboa/frontend_dist/assets/AdminLayout-CTNhuwTw.js +1 -0
  67. modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +1 -0
  68. modoboa/frontend_dist/assets/AlarmsView-DN_JIw9g.js +1 -0
  69. modoboa/frontend_dist/assets/AliasEditView-DjpPUTp9.js +1 -0
  70. modoboa/frontend_dist/assets/{AliasRecipientForm-DVZXWaUX.js → AliasRecipientForm-B1Y8wFdP.js} +1 -1
  71. modoboa/frontend_dist/assets/AliasView-GOJ5lyQH.js +1 -0
  72. modoboa/frontend_dist/assets/AuditTrailView-fbXmq70e.js +1 -0
  73. modoboa/frontend_dist/assets/CalendarView-LlQQNEPL.js +1 -0
  74. modoboa/frontend_dist/assets/{ChoiceField-7eU7c_rI.js → ChoiceField-B3ReQHVe.js} +1 -1
  75. modoboa/frontend_dist/assets/ComposeEmailForm-Bs1fZXAL.js +1 -0
  76. modoboa/frontend_dist/assets/ComposeEmailView-s3LMl3pO.js +1 -0
  77. modoboa/frontend_dist/assets/ConfirmDialog-DY_kUHLG.js +1 -0
  78. modoboa/frontend_dist/assets/{ConnectedLayout-BaJZ3BeR.css → ConnectedLayout-Bxh21hcH.css} +1 -1
  79. modoboa/frontend_dist/assets/ConnectedLayout-UWjiYBNw.js +1 -0
  80. modoboa/frontend_dist/assets/CreationForm-ORg3fazt.js +1 -0
  81. modoboa/frontend_dist/assets/DashboardView-Dplk9itS.js +1 -0
  82. modoboa/frontend_dist/assets/{DashboardView-BLlMi6Qb.css → DashboardView-gwwVAPvt.css} +1 -1
  83. modoboa/frontend_dist/assets/DomainAdminList-DVn9x0rB.js +1 -0
  84. modoboa/frontend_dist/assets/DomainEditView-nAoL64D_.js +1 -0
  85. modoboa/frontend_dist/assets/{DomainTransportForm-DPnPGBOp.js → DomainTransportForm-CA-DNUxX.js} +1 -1
  86. modoboa/frontend_dist/assets/{DomainView-BDKoBFYr.css → DomainView-CCLYXPHx.css} +1 -1
  87. modoboa/frontend_dist/assets/DomainView-CdXPpwJG.js +5 -0
  88. modoboa/frontend_dist/assets/DomainsView-B_59gowf.js +1 -0
  89. modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +1 -0
  90. modoboa/frontend_dist/assets/EmailField-CwcwI5xW.js +1 -0
  91. modoboa/frontend_dist/assets/EmailView-BshxcfAK.js +1 -0
  92. modoboa/frontend_dist/assets/EmptyLayout-DFfhnhLi.js +1 -0
  93. modoboa/frontend_dist/assets/FiltersView-Cf20MSTK.js +1 -0
  94. modoboa/frontend_dist/assets/ForwardEmailView-CZG062os.js +1 -0
  95. modoboa/frontend_dist/assets/{HtmlEditor-uM4AtIGi.js → HtmlEditor-Bh4c689R.js} +1 -1
  96. modoboa/frontend_dist/assets/IdentitiesView-BXAuU1YX.js +1 -0
  97. modoboa/frontend_dist/assets/{IdentitiesView-jmuItyMZ.css → IdentitiesView-DPrrRMS5.css} +1 -1
  98. modoboa/frontend_dist/assets/InformationView-C9vvvQhJ.css +1 -0
  99. modoboa/frontend_dist/assets/InformationView-Cn5FZW7H.js +1 -0
  100. modoboa/frontend_dist/assets/{LoadingData-CVD2Aen8.js → LoadingData-CdVvm4FI.js} +1 -1
  101. modoboa/frontend_dist/assets/{LoginCallbackView-sWzBke1g.js → LoginCallbackView-B9hAH4MI.js} +1 -1
  102. modoboa/frontend_dist/assets/{LoginView-wmN73W0f.js → LoginView-tHIR4Adc.js} +1 -1
  103. modoboa/frontend_dist/assets/MailboxView-Bugu2vhg.js +1 -0
  104. modoboa/frontend_dist/assets/MenuItems-PXjiG-fs.js +1 -0
  105. modoboa/frontend_dist/assets/MessageView-Cy4STShm.js +1 -0
  106. modoboa/frontend_dist/assets/MessagesView-DdkuEgfX.js +1 -0
  107. modoboa/frontend_dist/assets/MigrationsView-CidSEjCF.js +1 -0
  108. modoboa/frontend_dist/assets/{ParametersForm-BCeQljir.js → ParametersForm-CAv4SH-E.js} +1 -1
  109. modoboa/frontend_dist/assets/ParametersView-CX7Ffemw.js +1 -0
  110. modoboa/frontend_dist/assets/ParametersView-CrbNcmV3.js +1 -0
  111. modoboa/frontend_dist/assets/ProviderEditView-CrltAQXl.js +1 -0
  112. modoboa/frontend_dist/assets/ProviderGeneralForm-BYAzVnXM.js +1 -0
  113. modoboa/frontend_dist/assets/ProvidersView-osjIY4Ex.js +1 -0
  114. modoboa/frontend_dist/assets/QuarantineLayout-B8EcU9vS.js +1 -0
  115. modoboa/frontend_dist/assets/QuarantineView-D4gOE4EQ.css +1 -0
  116. modoboa/frontend_dist/assets/QuarantineView-D8Qg0MXA.js +1 -0
  117. modoboa/frontend_dist/assets/ReplyEmailView-BABPqWhd.js +1 -0
  118. modoboa/frontend_dist/assets/ResourcesForm-OaqdRYVs.js +1 -0
  119. modoboa/frontend_dist/assets/SelfServiceLayout-d277YTGR.js +1 -0
  120. modoboa/frontend_dist/assets/SettingsView-9iNcDhkI.js +6 -0
  121. modoboa/frontend_dist/assets/StatisticsView-cHsPyGkL.js +1 -0
  122. modoboa/frontend_dist/assets/TimeSerieChart--V83dcJ9.js +1 -0
  123. modoboa/frontend_dist/assets/UserLayout-B3sBiTcZ.js +1 -0
  124. modoboa/frontend_dist/assets/VAlert-BuaaYN2h.js +1 -0
  125. modoboa/frontend_dist/assets/VApp-CKP-6zGP.js +1 -0
  126. modoboa/frontend_dist/assets/VAutocomplete-Dwv6_Rzq.js +1 -0
  127. modoboa/frontend_dist/assets/VAvatar-Cmga0vj6.js +1 -0
  128. modoboa/frontend_dist/assets/VBadge-BQrRJ9S0.css +1 -0
  129. modoboa/frontend_dist/assets/VBadge-CixeK87a.js +1 -0
  130. modoboa/frontend_dist/assets/VCard-CxH9DWoK.js +1 -0
  131. modoboa/frontend_dist/assets/VCheckbox-62GOpvvP.js +1 -0
  132. modoboa/frontend_dist/assets/{VCheckboxBtn-j7di4leN.js → VCheckboxBtn-DMoNtKT8.js} +1 -1
  133. modoboa/frontend_dist/assets/VChip-D_styETR.js +1 -0
  134. modoboa/frontend_dist/assets/VColorPicker-BHscBGQV.js +1 -0
  135. modoboa/frontend_dist/assets/VContainer-B46caNs1.js +1 -0
  136. modoboa/frontend_dist/assets/VDataTable-Bh8NbVSx.js +1 -0
  137. modoboa/frontend_dist/assets/VDataTableServer-BDR5hOmo.js +1 -0
  138. modoboa/frontend_dist/assets/VDataTableVirtual-BOQlNtIG.js +1 -0
  139. modoboa/frontend_dist/assets/{VDialog-CZqM2Ofu.js → VDialog-BcTg7w6P.js} +1 -1
  140. modoboa/frontend_dist/assets/VExpansionPanels-BmH5Jl2Z.js +1 -0
  141. modoboa/frontend_dist/assets/VFileInput-BC4yAygd.js +1 -0
  142. modoboa/frontend_dist/assets/VForm-D5iPGkde.js +1 -0
  143. modoboa/frontend_dist/assets/VInput-CcxkaOXT.css +1 -0
  144. modoboa/frontend_dist/assets/VInput-CoDJzvaW.js +1 -0
  145. modoboa/frontend_dist/assets/VMenu-gUG70-zD.js +1 -0
  146. modoboa/frontend_dist/assets/VPicker-BXuKT3zB.js +1 -0
  147. modoboa/frontend_dist/assets/VProgressCircular-BtOPiGCg.js +1 -0
  148. modoboa/frontend_dist/assets/VRadioGroup-DIFZKSn-.js +1 -0
  149. modoboa/frontend_dist/assets/{VRow-C_Ydf6yr.js → VRow-ozg66L7j.js} +1 -1
  150. modoboa/frontend_dist/assets/VSelect-C3RjAa45.js +1 -0
  151. modoboa/frontend_dist/assets/VSelectionControl-zyz-fJvC.js +1 -0
  152. modoboa/frontend_dist/assets/VSheet-BNx2X4Mk.js +1 -0
  153. modoboa/frontend_dist/assets/VSpacer-DinPiXs9.js +1 -0
  154. modoboa/frontend_dist/assets/VSwitch-DwxdeAEq.js +1 -0
  155. modoboa/frontend_dist/assets/VTable-DaLxa4FO.js +1 -0
  156. modoboa/frontend_dist/assets/VTabs-BP0Hgsgm.js +1 -0
  157. modoboa/frontend_dist/assets/VTextField-BzBVKKob.css +1 -0
  158. modoboa/frontend_dist/assets/VTextField-XoGTj1KG.js +1 -0
  159. modoboa/frontend_dist/assets/VTextarea-wBlRMIv_.js +1 -0
  160. modoboa/frontend_dist/assets/VToolbar-CFZfqeOr.js +1 -0
  161. modoboa/frontend_dist/assets/VWindowItem-BB7ETW3b.js +1 -0
  162. modoboa/frontend_dist/assets/WebmailLayout-_Hk1XhVq.js +1 -0
  163. modoboa/frontend_dist/assets/accounts-DUzbx6k8.js +1 -0
  164. modoboa/frontend_dist/assets/admin-DewTk2H8.js +1 -0
  165. modoboa/frontend_dist/assets/{aliases-c3n-dCV_.js → aliases-4sXmjwXp.js} +1 -1
  166. modoboa/frontend_dist/assets/amavis-CC0li7_T.js +1 -0
  167. modoboa/frontend_dist/assets/amavis-DK8SHE6o.js +1 -0
  168. modoboa/frontend_dist/assets/{contacts-Bqckz8sr.js → contacts-BjghrPqZ.js} +1 -1
  169. modoboa/frontend_dist/assets/{domains-nBMR-fRf.js → domains-BSawReeu.js} +1 -1
  170. modoboa/frontend_dist/assets/{domains.store-ChZgLcqP.js → domains.store-D-vWCEIK.js} +1 -1
  171. modoboa/frontend_dist/assets/{filter-D7NrAf6a.js → filter-C82FUCw_.js} +1 -1
  172. modoboa/frontend_dist/assets/forwardRefs-cvcnlhoK.js +1 -0
  173. modoboa/frontend_dist/assets/global.store-DbkcI5o2.js +1 -0
  174. modoboa/frontend_dist/assets/{importExport-Dn9vYw7T.js → importExport-DzoL4Mvc.js} +1 -1
  175. modoboa/frontend_dist/assets/index-BImkz5Jx.js +984 -0
  176. modoboa/frontend_dist/assets/index-DuzUMVLM.js +1 -0
  177. modoboa/frontend_dist/assets/layout-C5FyYCHK.js +1 -0
  178. modoboa/frontend_dist/assets/{layout.store-BxBoBlgf.js → layout.store-NXWtFIwL.js} +1 -1
  179. modoboa/frontend_dist/assets/logos-BswdveCV.js +1 -0
  180. modoboa/frontend_dist/assets/{logs-DrTzylW7.js → logs-6CbtfaZS.js} +1 -1
  181. modoboa/frontend_dist/assets/{parameters-Lgiqp7aw.js → parameters-aSQiR7kN.js} +1 -1
  182. modoboa/frontend_dist/assets/{parameters.store-C9k9DuTj.js → parameters.store-CzQqVatx.js} +1 -1
  183. modoboa/frontend_dist/assets/permissions-DNoefz-n.js +1 -0
  184. modoboa/frontend_dist/assets/{ssrBoot-BsxW6uW4.js → ssrBoot-CKUX4kcb.js} +1 -1
  185. modoboa/frontend_dist/assets/{tag-CFK9dzMJ.js → tag-B_yWNNJD.js} +1 -1
  186. modoboa/frontend_dist/assets/transports-BDNB9wR5.js +1 -0
  187. modoboa/frontend_dist/assets/{webmail-KrD8ZhNM.js → webmail-CdU6CD9b.js} +1 -1
  188. modoboa/frontend_dist/index.html +1 -1
  189. modoboa/lib/email_utils.py +2 -2
  190. modoboa/lib/permissions.py +7 -0
  191. modoboa/lib/redis.py +1 -5
  192. modoboa/lib/test_runners.py +29 -0
  193. modoboa/lib/tests/__init__.py +5 -1
  194. modoboa/locale/br/LC_MESSAGES/django.po +87 -75
  195. modoboa/locale/cs/LC_MESSAGES/django.po +82 -74
  196. modoboa/locale/cs_CZ/LC_MESSAGES/django.po +145 -121
  197. modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
  198. modoboa/locale/de/LC_MESSAGES/django.po +339 -651
  199. modoboa/locale/de_DE/LC_MESSAGES/django.po +87 -75
  200. modoboa/locale/el_GR/LC_MESSAGES/django.po +160 -135
  201. modoboa/locale/en/LC_MESSAGES/django.po +82 -74
  202. modoboa/locale/es/LC_MESSAGES/django.po +158 -131
  203. modoboa/locale/es_MX/LC_MESSAGES/django.po +82 -74
  204. modoboa/locale/fi/LC_MESSAGES/django.po +87 -75
  205. modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
  206. modoboa/locale/fr/LC_MESSAGES/django.po +469 -201
  207. modoboa/locale/hu/LC_MESSAGES/django.po +82 -74
  208. modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
  209. modoboa/locale/it/LC_MESSAGES/django.po +148 -122
  210. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  211. modoboa/locale/ja_JP/LC_MESSAGES/django.po +201 -334
  212. modoboa/locale/ka/LC_MESSAGES/django.po +82 -74
  213. modoboa/locale/nl_NL/LC_MESSAGES/django.po +160 -132
  214. modoboa/locale/no/LC_MESSAGES/django.po +82 -74
  215. modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  216. modoboa/locale/pl_PL/LC_MESSAGES/django.po +172 -149
  217. modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  218. modoboa/locale/pt_BR/LC_MESSAGES/django.po +172 -144
  219. modoboa/locale/pt_PT/LC_MESSAGES/django.po +135 -112
  220. modoboa/locale/ro_RO/LC_MESSAGES/django.po +87 -75
  221. modoboa/locale/ru/LC_MESSAGES/django.po +142 -118
  222. modoboa/locale/si/LC_MESSAGES/django.po +82 -74
  223. modoboa/locale/sk/LC_MESSAGES/django.po +82 -74
  224. modoboa/locale/sk_SK/LC_MESSAGES/django.po +84 -76
  225. modoboa/locale/sl_SI/LC_MESSAGES/django.po +90 -76
  226. modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
  227. modoboa/locale/sv/LC_MESSAGES/django.po +172 -139
  228. modoboa/locale/tr/LC_MESSAGES/django.po +87 -75
  229. modoboa/locale/tr_TR/LC_MESSAGES/django.po +84 -74
  230. modoboa/locale/uk/LC_MESSAGES/django.po +82 -74
  231. modoboa/locale/zh/LC_MESSAGES/django.po +82 -74
  232. modoboa/locale/zh_CN/LC_MESSAGES/django.po +82 -74
  233. modoboa/locale/zh_TW/LC_MESSAGES/django.po +87 -75
  234. modoboa/parameters/api/v2/tests.py +2 -2
  235. modoboa/parameters/api/v2/viewsets.py +2 -0
  236. modoboa/policyd/tests.py +2 -0
  237. modoboa/urls_api_v2.py +6 -0
  238. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/METADATA +6 -4
  239. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/RECORD +244 -193
  240. modoboa/frontend_dist/assets/AccountAliasForm-DVXatAhB.js +0 -1
  241. modoboa/frontend_dist/assets/AccountEditView-DmvQjxpx.js +0 -1
  242. modoboa/frontend_dist/assets/AccountLayout-OGtZvlHR.js +0 -1
  243. modoboa/frontend_dist/assets/AccountPasswordSubForm-g3IEGrgM.js +0 -1
  244. modoboa/frontend_dist/assets/AccountView-DsxYqr3k.js +0 -1
  245. modoboa/frontend_dist/assets/AddressBook-3RoKiKon.js +0 -1
  246. modoboa/frontend_dist/assets/AdminLayout-CWfn8zaQ.js +0 -1
  247. modoboa/frontend_dist/assets/AlarmsView-Bheey-gp.css +0 -1
  248. modoboa/frontend_dist/assets/AlarmsView-D3Mh8ntf.js +0 -1
  249. modoboa/frontend_dist/assets/AliasEditView-C15eUZ11.js +0 -1
  250. modoboa/frontend_dist/assets/AliasView-Cyvc5vMb.js +0 -1
  251. modoboa/frontend_dist/assets/AuditTrailView-LI2XuLLn.js +0 -1
  252. modoboa/frontend_dist/assets/CalendarView-BpOlPh3f.js +0 -1
  253. modoboa/frontend_dist/assets/ComposeEmailForm-CNfI7ept.js +0 -1
  254. modoboa/frontend_dist/assets/ComposeEmailView-B866Xsrc.js +0 -1
  255. modoboa/frontend_dist/assets/ConfirmDialog-BvqxQsD1.js +0 -1
  256. modoboa/frontend_dist/assets/ConnectedLayout-BQ3ug6Jh.js +0 -1
  257. modoboa/frontend_dist/assets/CreationForm-C9Kh05ax.js +0 -1
  258. modoboa/frontend_dist/assets/DashboardView-Cw-gIcuB.js +0 -1
  259. modoboa/frontend_dist/assets/DomainAdminList-CsWUNKVk.js +0 -1
  260. modoboa/frontend_dist/assets/DomainEditView-DocxeOeW.js +0 -1
  261. modoboa/frontend_dist/assets/DomainView-Djc_0PsF.js +0 -5
  262. modoboa/frontend_dist/assets/DomainsView-B-Lxru7P.js +0 -1
  263. modoboa/frontend_dist/assets/DomainsView-DasJ0NdZ.css +0 -1
  264. modoboa/frontend_dist/assets/EmailField-BEKxuYni.js +0 -1
  265. modoboa/frontend_dist/assets/EmailView-BsR1Wes5.js +0 -1
  266. modoboa/frontend_dist/assets/EmptyLayout-BzPFOeLU.js +0 -1
  267. modoboa/frontend_dist/assets/FiltersView-5rmpC5cC.js +0 -1
  268. modoboa/frontend_dist/assets/ForwardEmailView-D7MbetcT.js +0 -1
  269. modoboa/frontend_dist/assets/IdentitiesView-BqjD9Lue.js +0 -1
  270. modoboa/frontend_dist/assets/InformationView-CghcvPn2.js +0 -1
  271. modoboa/frontend_dist/assets/InformationView-U5Ww-Sx1.css +0 -1
  272. modoboa/frontend_dist/assets/MailboxView-T_p-_ZtJ.js +0 -1
  273. modoboa/frontend_dist/assets/MenuItems-kHCMzR5E.js +0 -1
  274. modoboa/frontend_dist/assets/MessagesView-Cerv3xsy.js +0 -1
  275. modoboa/frontend_dist/assets/MigrationsView-7kjqPyYU.js +0 -1
  276. modoboa/frontend_dist/assets/ParametersView-DspBxVMV.js +0 -1
  277. modoboa/frontend_dist/assets/ParametersView-Dy0H5ep1.js +0 -1
  278. modoboa/frontend_dist/assets/ProviderEditView-DDLMOylC.js +0 -1
  279. modoboa/frontend_dist/assets/ProviderGeneralForm-BwOSKNHK.js +0 -1
  280. modoboa/frontend_dist/assets/ProvidersView-C99UD0WB.js +0 -1
  281. modoboa/frontend_dist/assets/ReplyEmailView-DAPBHldd.js +0 -1
  282. modoboa/frontend_dist/assets/ResourcesForm-D87PHeH0.js +0 -1
  283. modoboa/frontend_dist/assets/SettingsView-OxDo9wNd.js +0 -6
  284. modoboa/frontend_dist/assets/StatisticsView-CM__Eqku.js +0 -1
  285. modoboa/frontend_dist/assets/TimeSerieChart-C6j0uYR_.js +0 -1
  286. modoboa/frontend_dist/assets/UserLayout-CckCGnPS.js +0 -1
  287. modoboa/frontend_dist/assets/VAlert-B7mzOJIO.js +0 -1
  288. modoboa/frontend_dist/assets/VApp-BKxnjOoi.js +0 -1
  289. modoboa/frontend_dist/assets/VAutocomplete-Cc4_tcl1.js +0 -1
  290. modoboa/frontend_dist/assets/VAvatar-BAgTUIvX.js +0 -1
  291. modoboa/frontend_dist/assets/VCard-DFWiFORP.js +0 -1
  292. modoboa/frontend_dist/assets/VCheckbox-CKsH_vq3.js +0 -1
  293. modoboa/frontend_dist/assets/VChip-nZ0uhY7t.js +0 -1
  294. modoboa/frontend_dist/assets/VColorPicker-Os2aeP6J.js +0 -1
  295. modoboa/frontend_dist/assets/VContainer-Btam4lk2.js +0 -1
  296. modoboa/frontend_dist/assets/VDataTable-D_0_xJTl.js +0 -1
  297. modoboa/frontend_dist/assets/VDataTableServer-BWTt4Mzi.js +0 -1
  298. modoboa/frontend_dist/assets/VDataTableVirtual-BwVmkt4u.js +0 -1
  299. modoboa/frontend_dist/assets/VExpansionPanels-B5D6GOa3.js +0 -1
  300. modoboa/frontend_dist/assets/VFileInput-Cv9DIPki.js +0 -1
  301. modoboa/frontend_dist/assets/VForm-CpoZf60D.js +0 -1
  302. modoboa/frontend_dist/assets/VMenu-B_dVqOmo.js +0 -1
  303. modoboa/frontend_dist/assets/VPicker-CXkIGEze.js +0 -1
  304. modoboa/frontend_dist/assets/VProgressCircular-CrEXxs7k.js +0 -1
  305. modoboa/frontend_dist/assets/VRadioGroup-D8ypjYOO.js +0 -1
  306. modoboa/frontend_dist/assets/VSelect-CS51PDEt.js +0 -1
  307. modoboa/frontend_dist/assets/VSelectionControl-DiOqtY38.js +0 -1
  308. modoboa/frontend_dist/assets/VSheet-5VVWtHvs.js +0 -1
  309. modoboa/frontend_dist/assets/VSpacer-C6PZ3X24.js +0 -1
  310. modoboa/frontend_dist/assets/VSwitch-Chg5o-Cp.js +0 -1
  311. modoboa/frontend_dist/assets/VTable-KsiZ3cBz.js +0 -1
  312. modoboa/frontend_dist/assets/VTabs-tNrJIYO0.js +0 -1
  313. modoboa/frontend_dist/assets/VTextField-C-J20yj_.css +0 -1
  314. modoboa/frontend_dist/assets/VTextField-CLwRV0Cb.js +0 -1
  315. modoboa/frontend_dist/assets/VTextarea-Di8jbl8m.js +0 -1
  316. modoboa/frontend_dist/assets/VToolbar-CGwhgdmI.js +0 -1
  317. modoboa/frontend_dist/assets/VWindowItem-wWSFAGI-.js +0 -1
  318. modoboa/frontend_dist/assets/WebmailLayout-DsYThBrz.js +0 -1
  319. modoboa/frontend_dist/assets/admin-CJVLMHh9.js +0 -1
  320. modoboa/frontend_dist/assets/forwardRefs-Cpc3YYl6.js +0 -1
  321. modoboa/frontend_dist/assets/global.store-DUP26-A5.js +0 -1
  322. modoboa/frontend_dist/assets/index-DV9Li2cg.js +0 -852
  323. modoboa/frontend_dist/assets/index-DzL89N4E.js +0 -1
  324. modoboa/frontend_dist/assets/layout-CHO37cA6.js +0 -1
  325. modoboa/frontend_dist/assets/permissions-mL5hLHcW.js +0 -1
  326. modoboa/frontend_dist/assets/transports-TO08iTXJ.js +0 -1
  327. {modoboa-2.4.10.data → modoboa-2.5.0.data}/scripts/modoboa-admin.py +0 -0
  328. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/WHEEL +0 -0
  329. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/entry_points.txt +0 -0
  330. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/licenses/LICENSE +0 -0
  331. {modoboa-2.4.10.dist-info → modoboa-2.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,265 @@
1
+ """Amavis viewsets."""
2
+
3
+ from django.http import Http404
4
+ from django.shortcuts import get_object_or_404
5
+ from django.utils.translation import gettext as _
6
+
7
+ import django_rq
8
+ from rest_framework import filters, mixins, permissions, response, viewsets
9
+ from rest_framework.decorators import action
10
+
11
+ from modoboa.amavis.sql_connector import SQLconnector
12
+ from modoboa.lib.paginator import Paginator
13
+ from modoboa.lib.permissions import CanViewDomain
14
+
15
+ from modoboa.admin import models as admin_models
16
+ from modoboa.amavis import models, serializers, tasks
17
+ from modoboa.amavis.lib import (
18
+ AMrelease,
19
+ manual_learning_enabled,
20
+ SelfServiceAuthentication,
21
+ )
22
+ from modoboa.amavis.sql_email import SQLemail
23
+ from modoboa.amavis.utils import smart_str
24
+ from modoboa.parameters import tools as param_tools
25
+
26
+
27
+ SELFSERVICE_ACTIONS = ["retrieve", "headers", "delete", "release"]
28
+
29
+
30
+ def get_user_valid_addresses(user):
31
+ """Retrieve all valid addresses of a user."""
32
+ valid_addresses = []
33
+ if user.role == "SimpleUsers":
34
+ valid_addresses.append(user.email)
35
+ try:
36
+ mb = admin_models.Mailbox.objects.get(user=user)
37
+ except admin_models.Mailbox.DoesNotExist:
38
+ pass
39
+ else:
40
+ valid_addresses += mb.alias_addresses
41
+ return valid_addresses
42
+
43
+
44
+ class QuarantineViewSet(viewsets.GenericViewSet):
45
+
46
+ filter_backends = (filters.OrderingFilter,)
47
+ ordering_fields = [
48
+ "datetime",
49
+ "from_address",
50
+ "score",
51
+ "subject",
52
+ "to_address",
53
+ "type",
54
+ ]
55
+ permission_classes = (permissions.IsAuthenticated,)
56
+
57
+ def get_serializer_class(self):
58
+ if self.action == "retrieve":
59
+ return serializers.MessageSerializer
60
+ if self.action == "headers":
61
+ return serializers.MessageHeadersSerializer
62
+ if self.action == "mark_selection":
63
+ return serializers.MarkMessageSelectionSerializer
64
+ if self.action in ["release_selection", "delete_selection"]:
65
+ return serializers.MessageSelectionSerializer
66
+ if self.action in ["delete", "release"]:
67
+ return serializers.MessageIdentifierSerializer
68
+ return serializers.PaginatedMessageListSerializer
69
+
70
+ def get_authenticators(self):
71
+ result = [auth() for auth in self.authentication_classes]
72
+ if self.request:
73
+ action = self.action_map.get(self.request.method.lower())
74
+ if action in SELFSERVICE_ACTIONS:
75
+ result = [SelfServiceAuthentication()] + result
76
+ return result
77
+
78
+ def get_permissions(self):
79
+ if self.request.auth == "selfservice" and self.action in SELFSERVICE_ACTIONS:
80
+ return []
81
+ return super().get_permissions()
82
+
83
+ def list(self, request):
84
+ ordering = request.GET.get("ordering")
85
+ connector = SQLconnector(user=request.user, ordering=ordering)
86
+ try:
87
+ page_size = int(request.GET.get("page_size"))
88
+ except (TypeError, ValueError):
89
+ page_size = request.user.parameters.get_value("messages_per_page")
90
+ total = connector.messages_count(request)
91
+ paginator = Paginator(total, page_size)
92
+ page_num = int(request.GET.get("page", 1))
93
+ page = paginator.getpage(page_num)
94
+ if not page:
95
+ serializer = self.get_serializer(
96
+ {
97
+ "count": 0,
98
+ "first_index": 0,
99
+ "last_index": 0,
100
+ "prev_page": None,
101
+ "next_page": None,
102
+ "results": [],
103
+ }
104
+ )
105
+ return response.Response(serializer.data)
106
+ email_list = connector.fetch(page.id_start, page.id_stop)
107
+ serializer = self.get_serializer(
108
+ {
109
+ "count": total,
110
+ "first_index": page_num * page_size,
111
+ "last_index": (page_num * page_size) + len(email_list),
112
+ "prev_page": page.previous_page_number if page.has_previous else None,
113
+ "next_page": page.next_page_number if page.has_next else None,
114
+ "results": email_list,
115
+ }
116
+ )
117
+ return response.Response(serializer.data)
118
+
119
+ def retrieve(self, request, pk):
120
+ rcpt = request.GET.get("rcpt")
121
+ if rcpt is None:
122
+ return response.Response({"error": _("Invalid request")}, status=400)
123
+ if request.user:
124
+ if request.user.email == rcpt:
125
+ SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
126
+ elif hasattr(request.user, "mailbox"):
127
+ mb = request.user.mailbox
128
+ if rcpt == mb.full_address or rcpt in mb.alias_addresses:
129
+ SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
130
+ mail = SQLemail(pk.encode("ascii"), dformat="plain")
131
+ serializer = self.get_serializer(mail)
132
+ return response.Response(serializer.data)
133
+
134
+ @action(methods=["get"], detail=True)
135
+ def headers(self, request, pk):
136
+ email = SQLemail(pk.encode("ascii"))
137
+ headers = []
138
+ for name in email.msg.keys():
139
+ headers.append({"name": name, "value": email.get_header(email.msg, name)})
140
+ serializer = self.get_serializer({"headers": headers})
141
+ return response.Response(serializer.data)
142
+
143
+ def _release_selection(self, request, selection):
144
+ connector = SQLconnector()
145
+ valid_addresses = None
146
+ if request.user:
147
+ valid_addresses = get_user_valid_addresses(request.user)
148
+ msgrcpts = []
149
+ for item in selection:
150
+ if valid_addresses and item["rcpt"] not in valid_addresses:
151
+ continue
152
+ msgrcpts += [
153
+ (
154
+ item["mailid"],
155
+ connector.get_recipient_message(item["rcpt"], item["mailid"]),
156
+ )
157
+ ]
158
+ if (
159
+ not request.user or request.user.role == "SimpleUsers"
160
+ ) and not param_tools.get_global_parameter("user_can_release"):
161
+ for i, msgrcpt in msgrcpts:
162
+ connector.set_msgrcpt_status(smart_str(msgrcpt.rid.email), i, "p")
163
+ return response.Response({"status": "pending"})
164
+
165
+ amr = AMrelease()
166
+ error = None
167
+ for mid, rcpt in msgrcpts:
168
+ # we can't use the .mail relation on rcpt because it leads to
169
+ # an error on Postgres (memoryview pickle error).
170
+ mail = models.Msgs.objects.get(pk=mid.encode("ascii"))
171
+ result = amr.sendreq(mid, mail.secret_id, rcpt.rid.email)
172
+ if result:
173
+ connector.set_msgrcpt_status(smart_str(rcpt.rid.email), mid, "R")
174
+ else:
175
+ error = result
176
+ break
177
+
178
+ if error:
179
+ return response.Response({"status": error})
180
+
181
+ return response.Response({"status": "released"})
182
+
183
+ @action(methods=["post"], detail=False)
184
+ def release_selection(self, request):
185
+ serializer = self.get_serializer(data=request.data)
186
+ serializer.is_valid(raise_exception=True)
187
+ return self._release_selection(request, serializer.validated_data["selection"])
188
+
189
+ @action(methods=["post"], detail=True)
190
+ def release(self, request, pk):
191
+ serializer = self.get_serializer(data=request.data)
192
+ serializer.is_valid(raise_exception=True)
193
+ return self._release_selection(request, [serializer.validated_data])
194
+
195
+ def _delete_selection(self, request, selection):
196
+ connector = SQLconnector()
197
+ valid_addresses = None
198
+ if request.user:
199
+ valid_addresses = get_user_valid_addresses(request.user)
200
+ for item in selection:
201
+ if valid_addresses and item["rcpt"] not in valid_addresses:
202
+ continue
203
+ connector.set_msgrcpt_status(item["rcpt"], item["mailid"], "D")
204
+ return response.Response(status=204)
205
+
206
+ @action(methods=["post"], detail=False)
207
+ def delete_selection(self, request):
208
+ serializer = self.get_serializer(data=request.data)
209
+ serializer.is_valid(raise_exception=True)
210
+ return self._delete_selection(request, serializer.validated_data["selection"])
211
+
212
+ @action(methods=["post"], detail=True)
213
+ def delete(self, request, pk):
214
+ serializer = self.get_serializer(data=request.data)
215
+ serializer.is_valid(raise_exception=True)
216
+ return self._delete_selection(request, [serializer.validated_data])
217
+
218
+ @action(methods=["post"], detail=False)
219
+ def mark_selection(self, request):
220
+ serializer = self.get_serializer(data=request.data)
221
+ serializer.is_valid(raise_exception=True)
222
+ if not manual_learning_enabled(request.user):
223
+ return response.Response({"status": "ok"})
224
+ recipient_db = serializer.validated_data.get("database")
225
+ if not recipient_db:
226
+ recipient_db = "user" if request.user.role == "SimpleUsers" else "global"
227
+ queue = django_rq.get_queue("modoboa")
228
+ queue.enqueue(
229
+ tasks.manual_learning,
230
+ request.user.pk,
231
+ serializer.validated_data["type"],
232
+ serializer.validated_data["selection"],
233
+ recipient_db,
234
+ )
235
+ return response.Response(status=204)
236
+
237
+
238
+ class PolicyViewSet(
239
+ mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet
240
+ ):
241
+
242
+ permission_classes = (permissions.IsAuthenticated, CanViewDomain)
243
+ serializer_class = serializers.PolicySerializer
244
+
245
+ def get_queryset(self):
246
+ domains = [
247
+ f"@{name}"
248
+ for name in admin_models.Domain.objects.get_for_admin(
249
+ self.request.user
250
+ ).values_list("name", flat=True)
251
+ ]
252
+ return models.Policy.objects.filter(users__email__in=domains)
253
+
254
+ def get_object(self):
255
+ """Return the object the view is displaying."""
256
+ domain = get_object_or_404(admin_models.Domain, pk=self.kwargs["pk"])
257
+ queryset = self.filter_queryset(self.get_queryset())
258
+ obj = queryset.filter(users__email=f"@{domain.name}").first()
259
+ if obj is None:
260
+ raise Http404
261
+
262
+ # May raise a permission denied
263
+ self.check_object_permissions(self.request, obj)
264
+
265
+ return obj
@@ -83,10 +83,12 @@ class CheckPasswordTFASerializer(serializers.Serializer):
83
83
  def validate_password(self, value):
84
84
  user = self.context["user"]
85
85
  if not user.check_password(value):
86
- logger.warning(
87
- _("Failed TFA settings editing attempt from '%s' as user '%s'"),
88
- self.context["remote_addr"],
89
- escape(user.username),
90
- )
86
+ msg = _(
87
+ "Failed TFA settings editing attempt from '%(addr)s' as user '%(user)s'"
88
+ ) % {
89
+ "addr": self.context["remote_addr"],
90
+ "user": escape(user.username),
91
+ }
92
+ logger.warning(msg)
91
93
  raise serializers.ValidationError(_("Invalid password"))
92
94
  return value
@@ -188,6 +188,7 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
188
188
  inactive_account_threshold = serializers.IntegerField(default=30)
189
189
  top_notifications_check_interval = serializers.IntegerField(default=30)
190
190
  log_maximum_age = serializers.IntegerField(default=365)
191
+ message_history_maximum_age = serializers.IntegerField(default=180)
191
192
  items_per_page = serializers.IntegerField(default=30)
192
193
  default_top_redirection = serializers.ChoiceField(
193
194
  default="user", choices=[("user", _("User profile"))], required=False
@@ -663,9 +664,11 @@ class NotificationSerializer(serializers.Serializer):
663
664
  """Serializer used to render a notification."""
664
665
 
665
666
  id = serializers.CharField()
666
- url = serializers.CharField(required=False)
667
667
  text = serializers.CharField()
668
- level = serializers.CharField()
668
+ color = serializers.CharField()
669
+ target = serializers.CharField()
670
+ url = serializers.CharField(required=False)
671
+ counter = serializers.IntegerField(required=False)
669
672
 
670
673
 
671
674
  class ModoboaApplicationSerializer(serializers.Serializer):
@@ -691,3 +694,11 @@ class ThemeSerializer(serializers.Serializer):
691
694
  theme_primary_color_light = serializers.CharField(default="#3688F9")
692
695
  theme_secondary_color = serializers.CharField(default="#F18429")
693
696
  theme_label_color = serializers.CharField(default="#616161")
697
+
698
+
699
+ class StatisticsSerializer(serializers.Serializer):
700
+
701
+ domain_count = serializers.IntegerField()
702
+ domain_alias_count = serializers.IntegerField()
703
+ account_count = serializers.IntegerField()
704
+ alias_count = serializers.IntegerField()
@@ -196,6 +196,18 @@ class AccountViewSetTestCase(ModoAPITestCase):
196
196
  me = resp.json()
197
197
  self.assertEqual(me["username"], "admin")
198
198
 
199
+ def test_update_me(self):
200
+ url = reverse("v2:account-me")
201
+ data = {
202
+ "first_name": "First name",
203
+ "secondary_email": "toto@iti.com",
204
+ "language": "fr",
205
+ }
206
+ resp = self.client.put(url, data, format="json")
207
+ self.assertEqual(resp.status_code, 200)
208
+ me = resp.json()
209
+ self.assertEqual(me["secondary_email"], data["secondary_email"])
210
+
199
211
  def test_me_password(self, password_ko="Toto1234", password_ok="password"):
200
212
  url = reverse("v2:account-check-me-password")
201
213
  resp = self.client.post(url, {"password": password_ko}, format="json")
@@ -271,22 +283,22 @@ class AccountViewSetTestCase(ModoAPITestCase):
271
283
  url = reverse("v2:account-available-applications")
272
284
  resp = self.client.get(url)
273
285
  self.assertEqual(resp.status_code, 200)
274
- # admin -> only 1 app.
275
- self.assertEqual(len(resp.json()), 1)
286
+ # admin -> only 2 apps.
287
+ self.assertEqual(len(resp.json()), 2)
276
288
 
277
289
  # Domain admin with mailbox
278
290
  dadmin = models.User.objects.get(username="admin@test.com")
279
291
  self.authenticate_user(dadmin)
280
292
  resp = self.client.get(url)
281
293
  self.assertEqual(resp.status_code, 200)
282
- self.assertEqual(len(resp.json()), 4)
294
+ self.assertEqual(len(resp.json()), 5)
283
295
 
284
296
  # Simple user
285
297
  user = models.User.objects.get(username="user@test.com")
286
298
  self.authenticate_user(user)
287
299
  resp = self.client.get(url)
288
300
  self.assertEqual(resp.status_code, 200)
289
- self.assertEqual(len(resp.json()), 3)
301
+ self.assertEqual(len(resp.json()), 4)
290
302
 
291
303
  @override_settings(
292
304
  MODOBOA_APPS=[
@@ -658,3 +670,21 @@ class NewsFeedAPIViewTestCase(ModoAPITestCase):
658
670
  response = self.client.get(url)
659
671
  self.assertEqual(response.status_code, 200)
660
672
  self.assertIn("modoboa", response.json()[0]["link"])
673
+
674
+
675
+ class StatisticsAPIViewTestCase(ModoAPITestCase):
676
+
677
+ @classmethod
678
+ def setUpTestData(cls):
679
+ """Create test data."""
680
+ super().setUpTestData()
681
+ factories.populate_database()
682
+
683
+ def test_get_statistics(self):
684
+ url = reverse("v2:statistics")
685
+ response = self.client.get(url)
686
+ self.assertEqual(response.status_code, 200)
687
+ stats = response.json()
688
+ self.assertEqual(stats["domain_count"], 2)
689
+ self.assertEqual(stats["account_count"], 5)
690
+ self.assertEqual(stats["alias_count"], 3)
@@ -32,16 +32,21 @@ urlpatterns += [
32
32
  views.ComponentsInformationAPIView.as_view(),
33
33
  name="components_information",
34
34
  ),
35
- path(
36
- "admin/notifications/",
37
- views.NotificationsAPIView.as_view(),
38
- name="notifications",
39
- ),
40
35
  path(
41
36
  "admin/news_feed/",
42
37
  views.NewsFeedAPIView.as_view(),
43
38
  name="news-feed",
44
39
  ),
40
+ path(
41
+ "admin/statistics/",
42
+ views.StatisticsAPIView.as_view(),
43
+ name="statistics",
44
+ ),
45
45
  path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
46
+ path(
47
+ "notifications/",
48
+ views.NotificationsAPIView.as_view(),
49
+ name="notifications",
50
+ ),
46
51
  path("theme/", views.ThemeAPIView.as_view(), name="theme"),
47
52
  ]
@@ -14,7 +14,8 @@ from drf_spectacular.utils import extend_schema
14
14
  from rest_framework import permissions, response
15
15
  from rest_framework.views import APIView
16
16
 
17
- from modoboa.core import signals
17
+ from modoboa.admin import models as admin_models
18
+ from modoboa.core import models, signals
18
19
  from modoboa.core.utils import check_for_updates, get_capabilities
19
20
  from modoboa.lib.permissions import IsSuperUser, IsPrivilegedUser
20
21
  from modoboa.lib.throttle import (
@@ -141,7 +142,9 @@ class PasswordResetConfirmView(APIView):
141
142
  class ComponentsInformationAPIView(APIView):
142
143
  """Retrieve information about installed components."""
143
144
 
144
- permission_classes = [permissions.IsAuthenticated, IsSuperUser]
145
+ permission_classes = [
146
+ permissions.IsAuthenticated,
147
+ ]
145
148
  throttle_classes = [UserLesserDdosUser]
146
149
 
147
150
  @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
@@ -226,3 +229,21 @@ class NewsFeedAPIView(APIView):
226
229
 
227
230
  serializer = serializers.NewsFeedEntrySerializer(entries, many=True)
228
231
  return response.Response(serializer.data)
232
+
233
+
234
+ class StatisticsAPIView(APIView):
235
+ """Return some statistics about this modoboa instance."""
236
+
237
+ permission_classes = [permissions.IsAuthenticated, IsSuperUser]
238
+ throttle_classes = [UserLesserDdosUser]
239
+
240
+ @extend_schema(responses=serializers.StatisticsSerializer())
241
+ def get(self, request, *args, **kwargs):
242
+ data = {
243
+ "domain_count": admin_models.Domain.objects.count(),
244
+ "domain_alias_count": admin_models.DomainAlias.objects.count(),
245
+ "account_count": models.User.objects.count(),
246
+ "alias_count": admin_models.Alias.objects.filter(internal=False).count(),
247
+ }
248
+ serializer = serializers.StatisticsSerializer(data)
249
+ return response.Response(serializer.data)
@@ -48,11 +48,22 @@ def create_static_tokens(request):
48
48
  class AccountViewSet(core_v1_viewsets.AccountViewSet):
49
49
  """Account viewset."""
50
50
 
51
- @extend_schema(responses=admin_v2_serializers.AccountMeSerializer)
52
- @action(methods=["get"], detail=False)
51
+ @extend_schema(
52
+ responses=admin_v2_serializers.AccountMeSerializer,
53
+ request=admin_v2_serializers.AccountMeUpdateSerializer,
54
+ )
55
+ @action(methods=["get", "put"], detail=False)
53
56
  def me(self, request):
54
57
  """Return information about connected user."""
55
- serializer = admin_v1_serializers.AccountSerializer(request.user)
58
+ if request.method == "PUT":
59
+ serializer = admin_v2_serializers.AccountMeUpdateSerializer(
60
+ data=request.data, instance=request.user
61
+ )
62
+ serializer.is_valid(raise_exception=True)
63
+ instance = serializer.save()
64
+ serializer = admin_v1_serializers.AccountSerializer(instance)
65
+ else:
66
+ serializer = admin_v1_serializers.AccountSerializer(request.user)
56
67
  return response.Response(serializer.data)
57
68
 
58
69
  @action(
@@ -166,6 +177,16 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
166
177
  "url": "/admin",
167
178
  }
168
179
  )
180
+ if "modoboa.amavis" in settings.MODOBOA_APPS:
181
+ apps.append(
182
+ {
183
+ "name": "amavis",
184
+ "label": _("Quarantine"),
185
+ "icon": "mdi-server-security",
186
+ "description": _("Amavis quarantine"),
187
+ "url": "/user/quarantine",
188
+ }
189
+ )
169
190
  if hasattr(request.user, "mailbox"):
170
191
  if "modoboa.contacts" in settings.MODOBOA_APPS:
171
192
  apps += [
@@ -720,6 +720,17 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
720
720
  ),
721
721
  },
722
722
  ),
723
+ (
724
+ "message_history_maximum_age",
725
+ {
726
+ "label": gettext_lazy(
727
+ "Retention time in message history"
728
+ ),
729
+ "help_text": gettext_lazy(
730
+ "Retention time (in days) of a message in the message history section"
731
+ ),
732
+ },
733
+ ),
723
734
  (
724
735
  "items_per_page",
725
736
  {
@@ -34,10 +34,9 @@ def begin_registration(request):
34
34
  ),
35
35
  list(get_creds_from_user(request.user.pk).values()),
36
36
  user_verification=UserVerificationRequirement.DISCOURAGED,
37
- extensions={"credentialProtectionPolicy": "userVerificationOptional"},
38
37
  )
39
38
  request.session["fido2_state"] = state
40
- return options
39
+ return dict(options)
41
40
 
42
41
 
43
42
  def end_registration(request):
modoboa/core/handlers.py CHANGED
@@ -125,7 +125,9 @@ def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
125
125
  {
126
126
  "id": "newversionavailable",
127
127
  "text": _("One or more updates are available"),
128
- "level": "info",
128
+ "color": "info",
129
+ "url": "/admin/information",
130
+ "target": "admin",
129
131
  }
130
132
  ]
131
133
  elif include_all:
@@ -137,7 +139,9 @@ def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
137
139
  "id": "deprecatedpasswordscheme",
138
140
  "text": _("You are still using a deprecated password scheme (%s)")
139
141
  % hasher.name,
140
- "level": "warning",
142
+ "color": "warning",
143
+ "url": "/admin/information",
144
+ "target": "admin",
141
145
  }
142
146
  ]
143
147
  return result
@@ -0,0 +1,33 @@
1
+ from django.core.management.base import BaseCommand, CommandError
2
+
3
+ from oauth2_provider.models import get_application_model
4
+
5
+
6
+ class Command(BaseCommand):
7
+ """Command class."""
8
+
9
+ help = "Add new allowed hosts to frontend Oauth2 application."
10
+
11
+ def add_arguments(self, parser):
12
+ parser.add_argument("hostnames", type=str, nargs="+")
13
+
14
+ def handle(self, *args, **options):
15
+ app_model = get_application_model()
16
+ app = app_model.objects.filter(name="modoboa_frontend").first()
17
+ if not app:
18
+ raise CommandError(
19
+ "Application modoboa_frontend not found. "
20
+ "Make sure you ran load_initial_data first."
21
+ )
22
+ redirect_uris = app.redirect_uris.split(" ")
23
+ post_logout_redirect_uris = app.post_logout_redirect_uris.split(" ")
24
+ for hostname in options["hostnames"]:
25
+ uri = f"https://{hostname}/login/logged"
26
+ if uri not in redirect_uris:
27
+ redirect_uris.append(uri)
28
+ uri = f"https://{hostname}"
29
+ if uri not in post_logout_redirect_uris:
30
+ post_logout_redirect_uris.append(uri)
31
+ app.redirect_uris = " ".join(redirect_uris)
32
+ app.post_logout_redirect_uris = " ".join(post_logout_redirect_uris)
33
+ app.save()
@@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand
5
5
  from django.utils import timezone
6
6
 
7
7
  from modoboa.core.models import Log
8
+ from modoboa.maillog.models import Maillog
8
9
  from modoboa.parameters import tools as param_tools
9
10
 
10
11
 
@@ -41,4 +42,12 @@ class Command(BaseCommand):
41
42
  self.__vprint(f"Deleting logs older than {log_maximum_age} days...")
42
43
  limit = timezone.now() - datetime.timedelta(log_maximum_age)
43
44
  Log.objects.filter(date_created__lt=limit).delete()
45
+ message_history_maximum_age = param_tools.get_global_parameter(
46
+ "message_history_maximum_age"
47
+ )
48
+ self.__vprint(
49
+ f"Deleting messages in history older than {message_history_maximum_age} days..."
50
+ )
51
+ limit = timezone.now() - datetime.timedelta(message_history_maximum_age)
52
+ Maillog.objects.filter(date__lt=limit).delete()
44
53
  self.__vprint("Done.")
@@ -192,3 +192,13 @@ class Command(BaseCommand):
192
192
  }}
193
193
  """
194
194
  )
195
+
196
+
197
+ # ADD SIGNAL FOR THAT
198
+ # def load_initial_data(self):
199
+ # """Create records for existing domains and co."""
200
+ # for dom in Domain.objects.all():
201
+ # policy = create_user_and_policy("@{0}".format(dom.name))
202
+ # for domalias in dom.domainalias_set.all():
203
+ # domalias_pattern = "@{0}".format(domalias.name)
204
+ # create_user_and_use_policy(domalias_pattern, policy)
@@ -3,15 +3,33 @@
3
3
  from django.db import migrations
4
4
 
5
5
 
6
+ class RenameIndexIfExists(migrations.RenameIndex):
7
+
8
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
9
+ from_model = from_state.apps.get_model(app_label, self.model_name)
10
+ columns = [
11
+ from_model._meta.get_field(field).column for field in ["email", "is_active"]
12
+ ]
13
+ matching_index_name = schema_editor._constraint_names(
14
+ from_model,
15
+ column_names=columns,
16
+ index=True,
17
+ unique=False,
18
+ )
19
+ if len(matching_index_name) != 1:
20
+ return
21
+ super().database_forwards(app_label, schema_editor, from_state, to_state)
22
+
23
+
6
24
  class Migration(migrations.Migration):
7
25
  dependencies = [
8
26
  ("core", "0024_alter_user_language"),
9
27
  ]
10
28
 
11
29
  operations = [
12
- # migrations.RenameIndex(
13
- # model_name="user",
14
- # new_name="core_user_email_c0c03f_idx",
15
- # old_fields=("email", "is_active"),
16
- # ),
30
+ RenameIndexIfExists(
31
+ model_name="user",
32
+ new_name="core_user_email_c0c03f_idx",
33
+ old_fields=("email", "is_active"),
34
+ ),
17
35
  ]