modoboa 2.4.11__py3-none-any.whl → 2.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. modoboa/admin/api/v2/serializers.py +14 -0
  2. modoboa/amavis/__init__.py +3 -0
  3. modoboa/amavis/app_settings.py +276 -0
  4. modoboa/amavis/apps.py +18 -0
  5. modoboa/amavis/checks/__init__.py +2 -0
  6. modoboa/amavis/checks/settings_checks.py +59 -0
  7. modoboa/amavis/dbrouter.py +35 -0
  8. modoboa/amavis/factories.py +164 -0
  9. modoboa/amavis/handlers.py +146 -0
  10. modoboa/amavis/lib.py +381 -0
  11. modoboa/amavis/management/__init__.py +0 -0
  12. modoboa/amavis/management/commands/__init__.py +0 -0
  13. modoboa/amavis/management/commands/amnotify.py +99 -0
  14. modoboa/amavis/management/commands/qcleanup.py +84 -0
  15. modoboa/amavis/migrations/0001_initial.py +340 -0
  16. modoboa/amavis/migrations/__init__.py +0 -0
  17. modoboa/amavis/models.py +226 -0
  18. modoboa/amavis/serializers.py +139 -0
  19. modoboa/amavis/sql_connector.py +240 -0
  20. modoboa/amavis/sql_email.py +66 -0
  21. modoboa/amavis/tasks.py +33 -0
  22. modoboa/amavis/templates/amavis/notifications/pending_requests.html +16 -0
  23. modoboa/amavis/tests/__init__.py +0 -0
  24. modoboa/amavis/tests/sa-learn +3 -0
  25. modoboa/amavis/tests/sample_messages/quarantined-input.txt +80 -0
  26. modoboa/amavis/tests/sample_messages/quarantined-output-plain_nolinks.txt +17 -0
  27. modoboa/amavis/tests/spamc +3 -0
  28. modoboa/amavis/tests/test_checks.py +25 -0
  29. modoboa/amavis/tests/test_handlers.py +214 -0
  30. modoboa/amavis/tests/test_lib.py +90 -0
  31. modoboa/amavis/tests/test_management_commands.py +45 -0
  32. modoboa/amavis/tests/test_sql_email.py +67 -0
  33. modoboa/amavis/tests/test_utils.py +19 -0
  34. modoboa/amavis/tests/test_viewsets.py +319 -0
  35. modoboa/amavis/urls.py +11 -0
  36. modoboa/amavis/utils.py +105 -0
  37. modoboa/amavis/viewsets.py +265 -0
  38. modoboa/core/api/v1/serializers.py +7 -5
  39. modoboa/core/api/v2/serializers.py +4 -2
  40. modoboa/core/api/v2/tests.py +16 -4
  41. modoboa/core/api/v2/urls.py +5 -5
  42. modoboa/core/api/v2/views.py +6 -2
  43. modoboa/core/api/v2/viewsets.py +24 -3
  44. modoboa/core/commands/deploy.py +3 -0
  45. modoboa/core/commands/templates/settings.py.tpl +12 -11
  46. modoboa/core/handlers.py +6 -2
  47. modoboa/core/management/commands/add_allowed_hosts.py +33 -0
  48. modoboa/core/management/commands/load_initial_data.py +10 -0
  49. modoboa/core/migrations/0025_rename_user_email_is_active_core_user_email_c0c03f_idx.py +23 -5
  50. modoboa/core/tests/test_core.py +24 -0
  51. modoboa/core/utils.py +3 -0
  52. modoboa/frontend_dist/assets/AccountAliasForm-C0oHHyZL.js +1 -0
  53. modoboa/frontend_dist/assets/AccountEditView-lgSJ2Se8.js +1 -0
  54. modoboa/frontend_dist/assets/AccountLayout-U386K8zy.js +1 -0
  55. modoboa/frontend_dist/assets/AccountPasswordSubForm-YsaE_cDx.js +1 -0
  56. modoboa/frontend_dist/assets/AccountView-1jfKFDwb.js +1 -0
  57. modoboa/frontend_dist/assets/AddressBook-CwN64Zls.js +1 -0
  58. modoboa/frontend_dist/assets/AdminLayout-Cxm1lggg.js +1 -0
  59. modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +1 -0
  60. modoboa/frontend_dist/assets/AlarmsView-Bcjsicac.js +1 -0
  61. modoboa/frontend_dist/assets/AliasEditView-k3rVt1tG.js +1 -0
  62. modoboa/frontend_dist/assets/{AliasRecipientForm-IOae6sjF.js → AliasRecipientForm-IehUzKok.js} +1 -1
  63. modoboa/frontend_dist/assets/AliasView-DMzA10eD.js +1 -0
  64. modoboa/frontend_dist/assets/AuditTrailView-5dnGX5El.js +1 -0
  65. modoboa/frontend_dist/assets/CalendarView-DZONeDA9.js +1 -0
  66. modoboa/frontend_dist/assets/{ChoiceField-DJ_c78Cm.js → ChoiceField-DnwXRkht.js} +1 -1
  67. modoboa/frontend_dist/assets/ComposeEmailForm-kghmfNuE.js +1 -0
  68. modoboa/frontend_dist/assets/ComposeEmailView-DLv3wk1k.js +1 -0
  69. modoboa/frontend_dist/assets/ConfirmDialog-CcPrCKuI.js +1 -0
  70. modoboa/frontend_dist/assets/{ConnectedLayout-Dvwmicnc.css → ConnectedLayout-Bxh21hcH.css} +1 -1
  71. modoboa/frontend_dist/assets/ConnectedLayout-CWlBK7Hf.js +1 -0
  72. modoboa/frontend_dist/assets/CreationForm-CW4lxnPg.js +1 -0
  73. modoboa/frontend_dist/assets/DashboardView-DXVZMbMo.js +1 -0
  74. modoboa/frontend_dist/assets/DomainAdminList-C3jcDDc3.js +1 -0
  75. modoboa/frontend_dist/assets/DomainEditView-ph8AaElX.js +1 -0
  76. modoboa/frontend_dist/assets/{DomainTransportForm-C2xo0Yd7.js → DomainTransportForm-NCz6Bl-h.js} +1 -1
  77. modoboa/frontend_dist/assets/DomainView-BgMSSuU-.js +5 -0
  78. modoboa/frontend_dist/assets/{DomainView-BDKoBFYr.css → DomainView-CCLYXPHx.css} +1 -1
  79. modoboa/frontend_dist/assets/DomainsView-CEEU9btK.js +1 -0
  80. modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +1 -0
  81. modoboa/frontend_dist/assets/EmailField-DeqDPm5j.js +1 -0
  82. modoboa/frontend_dist/assets/EmailView-DczVhVO0.js +1 -0
  83. modoboa/frontend_dist/assets/EmptyLayout-BXgcfMLH.js +1 -0
  84. modoboa/frontend_dist/assets/FiltersView-nJj_gSCx.js +1 -0
  85. modoboa/frontend_dist/assets/ForwardEmailView-Bgv3JQb6.js +1 -0
  86. modoboa/frontend_dist/assets/{HtmlEditor-CJ9umKeO.js → HtmlEditor-BWRdelVw.js} +1 -1
  87. modoboa/frontend_dist/assets/{IdentitiesView-0ziuQ5s-.css → IdentitiesView-DPrrRMS5.css} +1 -1
  88. modoboa/frontend_dist/assets/IdentitiesView-Dld9IloZ.js +1 -0
  89. modoboa/frontend_dist/assets/InformationView-BBWKSX8D.js +1 -0
  90. modoboa/frontend_dist/assets/InformationView-C9vvvQhJ.css +1 -0
  91. modoboa/frontend_dist/assets/{LoadingData-CYwX3Jpn.js → LoadingData-G57nJ_JV.js} +1 -1
  92. modoboa/frontend_dist/assets/{LoginCallbackView-E01qkKn0.js → LoginCallbackView-DjyE2SG_.js} +1 -1
  93. modoboa/frontend_dist/assets/{LoginView-Cy4uFV9h.js → LoginView-CqCCXYLo.js} +1 -1
  94. modoboa/frontend_dist/assets/{MailboxView-B-aI4XBq.css → MailboxView-CfStlWhk.css} +1 -1
  95. modoboa/frontend_dist/assets/MailboxView-DRrs9eLO.js +1 -0
  96. modoboa/frontend_dist/assets/MenuItems-BqIZW5av.js +1 -0
  97. modoboa/frontend_dist/assets/MessageView-D_6tx_gd.js +1 -0
  98. modoboa/frontend_dist/assets/MessagesView-BH7JIR03.js +1 -0
  99. modoboa/frontend_dist/assets/MigrationsView-Cv_So9T-.js +1 -0
  100. modoboa/frontend_dist/assets/{ParametersForm-BZM0QSvg.js → ParametersForm-3qXttTuQ.js} +1 -1
  101. modoboa/frontend_dist/assets/ParametersView-3Ns04cpQ.js +1 -0
  102. modoboa/frontend_dist/assets/ParametersView-B5B5Dt6K.js +1 -0
  103. modoboa/frontend_dist/assets/ProviderEditView-zh7CY832.js +1 -0
  104. modoboa/frontend_dist/assets/ProviderGeneralForm-BQU7t3ma.js +1 -0
  105. modoboa/frontend_dist/assets/ProvidersView-CoF_ZkZA.js +1 -0
  106. modoboa/frontend_dist/assets/QuarantineLayout-CYBsrbJM.js +1 -0
  107. modoboa/frontend_dist/assets/QuarantineView-D4gOE4EQ.css +1 -0
  108. modoboa/frontend_dist/assets/QuarantineView-DNvpoycb.js +1 -0
  109. modoboa/frontend_dist/assets/ReplyEmailView-D1XTcglu.js +1 -0
  110. modoboa/frontend_dist/assets/ResourcesForm-BW8rUGgZ.js +1 -0
  111. modoboa/frontend_dist/assets/SelfServiceLayout-DfDHiYeX.js +1 -0
  112. modoboa/frontend_dist/assets/{SettingsView-BxLJBFY0.js → SettingsView-gQiJ2NVb.js} +2 -2
  113. modoboa/frontend_dist/assets/StatisticsView-DYalet_q.js +1 -0
  114. modoboa/frontend_dist/assets/TimeSerieChart-BZ2htbFk.js +1 -0
  115. modoboa/frontend_dist/assets/UserLayout-zUtHi-z-.js +1 -0
  116. modoboa/frontend_dist/assets/VAlert-4r6LxKtg.js +1 -0
  117. modoboa/frontend_dist/assets/VApp-CX_C7AUN.js +1 -0
  118. modoboa/frontend_dist/assets/VAutocomplete-DNKmBvyZ.js +1 -0
  119. modoboa/frontend_dist/assets/VAvatar-DbuoZWmf.js +1 -0
  120. modoboa/frontend_dist/assets/VBadge-BQrRJ9S0.css +1 -0
  121. modoboa/frontend_dist/assets/VBadge-Bv2nvUmC.js +1 -0
  122. modoboa/frontend_dist/assets/VCard-DzjUT5OP.js +1 -0
  123. modoboa/frontend_dist/assets/VCheckbox-dr7UFjl4.js +1 -0
  124. modoboa/frontend_dist/assets/{VCheckboxBtn-Dt810gWf.js → VCheckboxBtn-CpFdBnTv.js} +1 -1
  125. modoboa/frontend_dist/assets/VChip-CaQvfmkw.js +1 -0
  126. modoboa/frontend_dist/assets/VColorPicker-ByGpCW5O.js +1 -0
  127. modoboa/frontend_dist/assets/{VContainer-DvTbsotR.js → VContainer-74Dnn8Ux.js} +1 -1
  128. modoboa/frontend_dist/assets/VDataTable-CL7yHvG7.js +1 -0
  129. modoboa/frontend_dist/assets/VDataTableServer-BqvNcIdw.js +1 -0
  130. modoboa/frontend_dist/assets/VDataTableVirtual--KsOP8i6.js +1 -0
  131. modoboa/frontend_dist/assets/{VDialog-Bk6EWNhz.js → VDialog-DmTGCGR0.js} +1 -1
  132. modoboa/frontend_dist/assets/VExpansionPanels-B7sSTCwd.js +1 -0
  133. modoboa/frontend_dist/assets/VFileInput-SULIc6F3.js +1 -0
  134. modoboa/frontend_dist/assets/VForm-DsRLc-sa.js +1 -0
  135. modoboa/frontend_dist/assets/VInput-CcxkaOXT.css +1 -0
  136. modoboa/frontend_dist/assets/VInput-DVKUObZe.js +1 -0
  137. modoboa/frontend_dist/assets/VMenu-nv0XOgg0.js +1 -0
  138. modoboa/frontend_dist/assets/VPicker-DnDSWJHJ.js +1 -0
  139. modoboa/frontend_dist/assets/VProgressCircular-qK6p5X_Y.js +1 -0
  140. modoboa/frontend_dist/assets/VRadioGroup-CbiPLy0t.js +1 -0
  141. modoboa/frontend_dist/assets/{VRow-BF35mT1S.js → VRow-DJ0NB63-.js} +1 -1
  142. modoboa/frontend_dist/assets/VSelect-CxCFMHyF.js +1 -0
  143. modoboa/frontend_dist/assets/VSelectionControl-C-6A4us5.js +1 -0
  144. modoboa/frontend_dist/assets/VSheet-DI6SxLnG.js +1 -0
  145. modoboa/frontend_dist/assets/VSpacer-CoJVmx8k.js +1 -0
  146. modoboa/frontend_dist/assets/VSwitch-DPnjPQuU.js +1 -0
  147. modoboa/frontend_dist/assets/VTable-ldTxgQPW.js +1 -0
  148. modoboa/frontend_dist/assets/VTabs-aS8WSL9I.js +1 -0
  149. modoboa/frontend_dist/assets/VTextField-BzBVKKob.css +1 -0
  150. modoboa/frontend_dist/assets/VTextField-DKbr4H5w.js +1 -0
  151. modoboa/frontend_dist/assets/VTextarea-BttkFsM4.js +1 -0
  152. modoboa/frontend_dist/assets/VToolbar-BxX3W2kR.js +1 -0
  153. modoboa/frontend_dist/assets/VWindowItem-Cvqvdegd.js +1 -0
  154. modoboa/frontend_dist/assets/WebmailLayout-BT2k6U7q.js +1 -0
  155. modoboa/frontend_dist/assets/accounts-CC2F0a0c.js +1 -0
  156. modoboa/frontend_dist/assets/{admin-o-HRGnmT.js → admin-CHCHFGI6.js} +1 -1
  157. modoboa/frontend_dist/assets/{aliases-DDVeehyg.js → aliases-C9bUD4Ws.js} +1 -1
  158. modoboa/frontend_dist/assets/amavis-BhzV4rgf.js +1 -0
  159. modoboa/frontend_dist/assets/amavis-DCVJxuui.js +1 -0
  160. modoboa/frontend_dist/assets/{contacts-C84DY-Q1.js → contacts-Dxz6eWpf.js} +1 -1
  161. modoboa/frontend_dist/assets/{domains-Bgn4ixHL.js → domains-C2cornvL.js} +1 -1
  162. modoboa/frontend_dist/assets/{domains.store-DTE-V7Y1.js → domains.store-BLKRipG8.js} +1 -1
  163. modoboa/frontend_dist/assets/{filter-CnffiQAW.js → filter-rmxrcjKk.js} +1 -1
  164. modoboa/frontend_dist/assets/forwardRefs-CpzzjgpX.js +1 -0
  165. modoboa/frontend_dist/assets/global.store-DndbMXYb.js +1 -0
  166. modoboa/frontend_dist/assets/{importExport-BlQYb0NO.js → importExport-C3uqrcok.js} +1 -1
  167. modoboa/frontend_dist/assets/index-LhNzkzAh.js +984 -0
  168. modoboa/frontend_dist/assets/layout-DbjDe3Wl.js +1 -0
  169. modoboa/frontend_dist/assets/{layout.store-DkjrAoXt.js → layout.store-Vq5mvIp7.js} +1 -1
  170. modoboa/frontend_dist/assets/{logos-q8SEyAa4.js → logos-Bvcy0usu.js} +1 -1
  171. modoboa/frontend_dist/assets/{logs-B7IJ7LBa.js → logs-BuItINky.js} +1 -1
  172. modoboa/frontend_dist/assets/{parameters-A6iBEYQq.js → parameters-C8IYEP7q.js} +1 -1
  173. modoboa/frontend_dist/assets/{parameters.store-BiXS4_6w.js → parameters.store-1cwSP2JP.js} +1 -1
  174. modoboa/frontend_dist/assets/permissions-DQjAcO9S.js +1 -0
  175. modoboa/frontend_dist/assets/{ssrBoot-AzTdjPjk.js → ssrBoot-BxIQ9ccA.js} +1 -1
  176. modoboa/frontend_dist/assets/{tag-BnSYRTcD.js → tag-3cfI1_f7.js} +1 -1
  177. modoboa/frontend_dist/assets/transports-D4Jk4-AP.js +1 -0
  178. modoboa/frontend_dist/assets/{webmail-CSH_3l6R.js → webmail-B2IUjaxM.js} +1 -1
  179. modoboa/frontend_dist/index.html +1 -1
  180. modoboa/lib/email_utils.py +2 -2
  181. modoboa/lib/permissions.py +7 -0
  182. modoboa/lib/test_runners.py +29 -0
  183. modoboa/lib/tests/__init__.py +5 -1
  184. modoboa/locale/br/LC_MESSAGES/django.po +87 -75
  185. modoboa/locale/cs/LC_MESSAGES/django.po +82 -74
  186. modoboa/locale/cs_CZ/LC_MESSAGES/django.po +145 -121
  187. modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
  188. modoboa/locale/de/LC_MESSAGES/django.po +339 -651
  189. modoboa/locale/de_DE/LC_MESSAGES/django.po +87 -75
  190. modoboa/locale/el_GR/LC_MESSAGES/django.po +160 -135
  191. modoboa/locale/en/LC_MESSAGES/django.po +82 -74
  192. modoboa/locale/es/LC_MESSAGES/django.po +158 -131
  193. modoboa/locale/es_MX/LC_MESSAGES/django.po +82 -74
  194. modoboa/locale/fi/LC_MESSAGES/django.po +87 -75
  195. modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
  196. modoboa/locale/fr/LC_MESSAGES/django.po +469 -201
  197. modoboa/locale/hu/LC_MESSAGES/django.po +82 -74
  198. modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
  199. modoboa/locale/it/LC_MESSAGES/django.po +148 -122
  200. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  201. modoboa/locale/ja_JP/LC_MESSAGES/django.po +80 -72
  202. modoboa/locale/ka/LC_MESSAGES/django.po +82 -74
  203. modoboa/locale/nl_NL/LC_MESSAGES/django.po +160 -132
  204. modoboa/locale/no/LC_MESSAGES/django.po +82 -74
  205. modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  206. modoboa/locale/pl_PL/LC_MESSAGES/django.po +172 -149
  207. modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  208. modoboa/locale/pt_BR/LC_MESSAGES/django.po +172 -144
  209. modoboa/locale/pt_PT/LC_MESSAGES/django.po +135 -112
  210. modoboa/locale/ro_RO/LC_MESSAGES/django.po +87 -75
  211. modoboa/locale/ru/LC_MESSAGES/django.po +142 -118
  212. modoboa/locale/si/LC_MESSAGES/django.po +82 -74
  213. modoboa/locale/sk/LC_MESSAGES/django.po +82 -74
  214. modoboa/locale/sk_SK/LC_MESSAGES/django.po +84 -76
  215. modoboa/locale/sl_SI/LC_MESSAGES/django.po +90 -76
  216. modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
  217. modoboa/locale/sv/LC_MESSAGES/django.po +172 -139
  218. modoboa/locale/tr/LC_MESSAGES/django.po +87 -75
  219. modoboa/locale/tr_TR/LC_MESSAGES/django.po +84 -74
  220. modoboa/locale/uk/LC_MESSAGES/django.po +82 -74
  221. modoboa/locale/zh/LC_MESSAGES/django.po +82 -74
  222. modoboa/locale/zh_CN/LC_MESSAGES/django.po +82 -74
  223. modoboa/locale/zh_TW/LC_MESSAGES/django.po +87 -75
  224. modoboa/parameters/api/v2/tests.py +2 -2
  225. modoboa/parameters/api/v2/viewsets.py +2 -0
  226. modoboa/policyd/tests.py +2 -0
  227. modoboa/urls_api_v2.py +6 -0
  228. modoboa/webmail/lib/imaputils.py +2 -2
  229. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/METADATA +6 -4
  230. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/RECORD +235 -185
  231. modoboa/frontend_dist/assets/AccountAliasForm-BV6KvTu6.js +0 -1
  232. modoboa/frontend_dist/assets/AccountEditView-DDOFyfBD.js +0 -1
  233. modoboa/frontend_dist/assets/AccountLayout-rX51xgxT.js +0 -1
  234. modoboa/frontend_dist/assets/AccountPasswordSubForm-D9S6LaeH.js +0 -1
  235. modoboa/frontend_dist/assets/AccountView-cmvaZNq3.js +0 -1
  236. modoboa/frontend_dist/assets/AddressBook-DCJxL8SU.js +0 -1
  237. modoboa/frontend_dist/assets/AdminLayout-r0wfG2lO.js +0 -1
  238. modoboa/frontend_dist/assets/AlarmsView-Bheey-gp.css +0 -1
  239. modoboa/frontend_dist/assets/AlarmsView-C0bqC4PA.js +0 -1
  240. modoboa/frontend_dist/assets/AliasEditView-DVoWoCGY.js +0 -1
  241. modoboa/frontend_dist/assets/AliasView-DrONZXOh.js +0 -1
  242. modoboa/frontend_dist/assets/AuditTrailView-OTkoZaMU.js +0 -1
  243. modoboa/frontend_dist/assets/CalendarView-CqF4_Ui9.js +0 -1
  244. modoboa/frontend_dist/assets/ComposeEmailForm-DO5_GB3e.js +0 -1
  245. modoboa/frontend_dist/assets/ComposeEmailView-A91HCBsN.js +0 -1
  246. modoboa/frontend_dist/assets/ConfirmDialog-BBcgdAnO.js +0 -1
  247. modoboa/frontend_dist/assets/ConnectedLayout-1oRW-Rql.js +0 -1
  248. modoboa/frontend_dist/assets/CreationForm-71YJbjsA.js +0 -1
  249. modoboa/frontend_dist/assets/DashboardView-CdLpSfUl.js +0 -1
  250. modoboa/frontend_dist/assets/DomainAdminList-BjC4KsqI.js +0 -1
  251. modoboa/frontend_dist/assets/DomainEditView-CQjKwYxl.js +0 -1
  252. modoboa/frontend_dist/assets/DomainView-BhhuZI_N.js +0 -5
  253. modoboa/frontend_dist/assets/DomainsView-Cft4BP8Z.js +0 -1
  254. modoboa/frontend_dist/assets/DomainsView-DasJ0NdZ.css +0 -1
  255. modoboa/frontend_dist/assets/EmailField-C8umy0EU.js +0 -1
  256. modoboa/frontend_dist/assets/EmailView-ki7uEQPD.js +0 -1
  257. modoboa/frontend_dist/assets/EmptyLayout-DaA1XH9n.js +0 -1
  258. modoboa/frontend_dist/assets/FiltersView-FYFZxG4B.js +0 -1
  259. modoboa/frontend_dist/assets/ForwardEmailView-cUbnSYCF.js +0 -1
  260. modoboa/frontend_dist/assets/IdentitiesView-njNo8N5n.js +0 -1
  261. modoboa/frontend_dist/assets/InformationView-D1h38POt.js +0 -1
  262. modoboa/frontend_dist/assets/InformationView-U5Ww-Sx1.css +0 -1
  263. modoboa/frontend_dist/assets/MailboxView-IlrLWm_H.js +0 -1
  264. modoboa/frontend_dist/assets/MenuItems-BAtHWzAE.js +0 -1
  265. modoboa/frontend_dist/assets/MessagesView-OSpjixFq.js +0 -1
  266. modoboa/frontend_dist/assets/MigrationsView-DKNOsVzF.js +0 -1
  267. modoboa/frontend_dist/assets/ParametersView-C4bXASiq.js +0 -1
  268. modoboa/frontend_dist/assets/ParametersView-CYXgNmc1.js +0 -1
  269. modoboa/frontend_dist/assets/ProviderEditView-CyxCWTST.js +0 -1
  270. modoboa/frontend_dist/assets/ProviderGeneralForm-BYPjNHqB.js +0 -1
  271. modoboa/frontend_dist/assets/ProvidersView-CxrMkRyk.js +0 -1
  272. modoboa/frontend_dist/assets/ReplyEmailView-Dkw9-N26.js +0 -1
  273. modoboa/frontend_dist/assets/ResourcesForm-CuUvrOdY.js +0 -1
  274. modoboa/frontend_dist/assets/StatisticsView-BN7QsZMT.js +0 -1
  275. modoboa/frontend_dist/assets/TimeSerieChart-BMN8BeFZ.js +0 -1
  276. modoboa/frontend_dist/assets/UserLayout-B6-JQg4F.js +0 -1
  277. modoboa/frontend_dist/assets/VAlert-DIQTrRif.js +0 -1
  278. modoboa/frontend_dist/assets/VApp-CpkYA7js.js +0 -1
  279. modoboa/frontend_dist/assets/VAutocomplete-C4IpXyl8.js +0 -1
  280. modoboa/frontend_dist/assets/VAvatar-Lpb-Dion.js +0 -1
  281. modoboa/frontend_dist/assets/VCard-er_isjE_.js +0 -1
  282. modoboa/frontend_dist/assets/VCheckbox-D-u8JXP1.js +0 -1
  283. modoboa/frontend_dist/assets/VChip-B4iSpj8_.js +0 -1
  284. modoboa/frontend_dist/assets/VColorPicker-BAjGDsXv.js +0 -1
  285. modoboa/frontend_dist/assets/VDataTable-4JRjbtgF.js +0 -1
  286. modoboa/frontend_dist/assets/VDataTableServer-tIDT1m3-.js +0 -1
  287. modoboa/frontend_dist/assets/VDataTableVirtual-BlnO18u_.js +0 -1
  288. modoboa/frontend_dist/assets/VExpansionPanels-CwGtXDhr.js +0 -1
  289. modoboa/frontend_dist/assets/VFileInput-D1_7ZkO_.js +0 -1
  290. modoboa/frontend_dist/assets/VForm-DAkW4nfy.js +0 -1
  291. modoboa/frontend_dist/assets/VMenu-BPFJwj2f.js +0 -1
  292. modoboa/frontend_dist/assets/VPicker-CfT82M8N.js +0 -1
  293. modoboa/frontend_dist/assets/VProgressCircular-w75-3ogi.js +0 -1
  294. modoboa/frontend_dist/assets/VRadioGroup-0j6DNC_k.js +0 -1
  295. modoboa/frontend_dist/assets/VSelect-Cs4ARbAS.js +0 -1
  296. modoboa/frontend_dist/assets/VSelectionControl-Dg-XyRRS.js +0 -1
  297. modoboa/frontend_dist/assets/VSheet-Btq_Mu4s.js +0 -1
  298. modoboa/frontend_dist/assets/VSpacer-C7xukQmu.js +0 -1
  299. modoboa/frontend_dist/assets/VSwitch-Cs1NQrmk.js +0 -1
  300. modoboa/frontend_dist/assets/VTable-CNz2SGk4.js +0 -1
  301. modoboa/frontend_dist/assets/VTabs-B1fyVn4M.js +0 -1
  302. modoboa/frontend_dist/assets/VTextField-BdyvgvkG.js +0 -1
  303. modoboa/frontend_dist/assets/VTextField-C-J20yj_.css +0 -1
  304. modoboa/frontend_dist/assets/VTextarea-DnOMpe0Q.js +0 -1
  305. modoboa/frontend_dist/assets/VToolbar-BiCiBxBJ.js +0 -1
  306. modoboa/frontend_dist/assets/VWindowItem-ChWm_kz3.js +0 -1
  307. modoboa/frontend_dist/assets/WebmailLayout-o4uEkp9e.js +0 -1
  308. modoboa/frontend_dist/assets/forwardRefs-Dvjn_Xq4.js +0 -1
  309. modoboa/frontend_dist/assets/global.store-BaiD63EN.js +0 -1
  310. modoboa/frontend_dist/assets/index-I1VDlN4g.js +0 -984
  311. modoboa/frontend_dist/assets/layout-D8ZJPiJ_.js +0 -1
  312. modoboa/frontend_dist/assets/permissions-CITHLHVg.js +0 -1
  313. modoboa/frontend_dist/assets/transports-Dz7c6kIy.js +0 -1
  314. {modoboa-2.4.11.data → modoboa-2.5.1.data}/scripts/modoboa-admin.py +0 -0
  315. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/WHEEL +0 -0
  316. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/entry_points.txt +0 -0
  317. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/licenses/LICENSE +0 -0
  318. {modoboa-2.4.11.dist-info → modoboa-2.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,265 @@
1
+ """Amavis viewsets."""
2
+
3
+ from django.http import Http404
4
+ from django.shortcuts import get_object_or_404
5
+ from django.utils.translation import gettext as _
6
+
7
+ import django_rq
8
+ from rest_framework import filters, mixins, permissions, response, viewsets
9
+ from rest_framework.decorators import action
10
+
11
+ from modoboa.amavis.sql_connector import SQLconnector
12
+ from modoboa.lib.paginator import Paginator
13
+ from modoboa.lib.permissions import CanViewDomain
14
+
15
+ from modoboa.admin import models as admin_models
16
+ from modoboa.amavis import models, serializers, tasks
17
+ from modoboa.amavis.lib import (
18
+ AMrelease,
19
+ manual_learning_enabled,
20
+ SelfServiceAuthentication,
21
+ )
22
+ from modoboa.amavis.sql_email import SQLemail
23
+ from modoboa.amavis.utils import smart_str
24
+ from modoboa.parameters import tools as param_tools
25
+
26
+
27
+ SELFSERVICE_ACTIONS = ["retrieve", "headers", "delete", "release"]
28
+
29
+
30
+ def get_user_valid_addresses(user):
31
+ """Retrieve all valid addresses of a user."""
32
+ valid_addresses = []
33
+ if user.role == "SimpleUsers":
34
+ valid_addresses.append(user.email)
35
+ try:
36
+ mb = admin_models.Mailbox.objects.get(user=user)
37
+ except admin_models.Mailbox.DoesNotExist:
38
+ pass
39
+ else:
40
+ valid_addresses += mb.alias_addresses
41
+ return valid_addresses
42
+
43
+
44
+ class QuarantineViewSet(viewsets.GenericViewSet):
45
+
46
+ filter_backends = (filters.OrderingFilter,)
47
+ ordering_fields = [
48
+ "datetime",
49
+ "from_address",
50
+ "score",
51
+ "subject",
52
+ "to_address",
53
+ "type",
54
+ ]
55
+ permission_classes = (permissions.IsAuthenticated,)
56
+
57
+ def get_serializer_class(self):
58
+ if self.action == "retrieve":
59
+ return serializers.MessageSerializer
60
+ if self.action == "headers":
61
+ return serializers.MessageHeadersSerializer
62
+ if self.action == "mark_selection":
63
+ return serializers.MarkMessageSelectionSerializer
64
+ if self.action in ["release_selection", "delete_selection"]:
65
+ return serializers.MessageSelectionSerializer
66
+ if self.action in ["delete", "release"]:
67
+ return serializers.MessageIdentifierSerializer
68
+ return serializers.PaginatedMessageListSerializer
69
+
70
+ def get_authenticators(self):
71
+ result = [auth() for auth in self.authentication_classes]
72
+ if self.request:
73
+ action = self.action_map.get(self.request.method.lower())
74
+ if action in SELFSERVICE_ACTIONS:
75
+ result = [SelfServiceAuthentication()] + result
76
+ return result
77
+
78
+ def get_permissions(self):
79
+ if self.request.auth == "selfservice" and self.action in SELFSERVICE_ACTIONS:
80
+ return []
81
+ return super().get_permissions()
82
+
83
+ def list(self, request):
84
+ ordering = request.GET.get("ordering")
85
+ connector = SQLconnector(user=request.user, ordering=ordering)
86
+ try:
87
+ page_size = int(request.GET.get("page_size"))
88
+ except (TypeError, ValueError):
89
+ page_size = request.user.parameters.get_value("messages_per_page")
90
+ total = connector.messages_count(request)
91
+ paginator = Paginator(total, page_size)
92
+ page_num = int(request.GET.get("page", 1))
93
+ page = paginator.getpage(page_num)
94
+ if not page:
95
+ serializer = self.get_serializer(
96
+ {
97
+ "count": 0,
98
+ "first_index": 0,
99
+ "last_index": 0,
100
+ "prev_page": None,
101
+ "next_page": None,
102
+ "results": [],
103
+ }
104
+ )
105
+ return response.Response(serializer.data)
106
+ email_list = connector.fetch(page.id_start, page.id_stop)
107
+ serializer = self.get_serializer(
108
+ {
109
+ "count": total,
110
+ "first_index": page_num * page_size,
111
+ "last_index": (page_num * page_size) + len(email_list),
112
+ "prev_page": page.previous_page_number if page.has_previous else None,
113
+ "next_page": page.next_page_number if page.has_next else None,
114
+ "results": email_list,
115
+ }
116
+ )
117
+ return response.Response(serializer.data)
118
+
119
+ def retrieve(self, request, pk):
120
+ rcpt = request.GET.get("rcpt")
121
+ if rcpt is None:
122
+ return response.Response({"error": _("Invalid request")}, status=400)
123
+ if request.user:
124
+ if request.user.email == rcpt:
125
+ SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
126
+ elif hasattr(request.user, "mailbox"):
127
+ mb = request.user.mailbox
128
+ if rcpt == mb.full_address or rcpt in mb.alias_addresses:
129
+ SQLconnector().set_msgrcpt_status(rcpt, pk, "V")
130
+ mail = SQLemail(pk.encode("ascii"), dformat="plain")
131
+ serializer = self.get_serializer(mail)
132
+ return response.Response(serializer.data)
133
+
134
+ @action(methods=["get"], detail=True)
135
+ def headers(self, request, pk):
136
+ email = SQLemail(pk.encode("ascii"))
137
+ headers = []
138
+ for name in email.msg.keys():
139
+ headers.append({"name": name, "value": email.get_header(email.msg, name)})
140
+ serializer = self.get_serializer({"headers": headers})
141
+ return response.Response(serializer.data)
142
+
143
+ def _release_selection(self, request, selection):
144
+ connector = SQLconnector()
145
+ valid_addresses = None
146
+ if request.user:
147
+ valid_addresses = get_user_valid_addresses(request.user)
148
+ msgrcpts = []
149
+ for item in selection:
150
+ if valid_addresses and item["rcpt"] not in valid_addresses:
151
+ continue
152
+ msgrcpts += [
153
+ (
154
+ item["mailid"],
155
+ connector.get_recipient_message(item["rcpt"], item["mailid"]),
156
+ )
157
+ ]
158
+ if (
159
+ not request.user or request.user.role == "SimpleUsers"
160
+ ) and not param_tools.get_global_parameter("user_can_release"):
161
+ for i, msgrcpt in msgrcpts:
162
+ connector.set_msgrcpt_status(smart_str(msgrcpt.rid.email), i, "p")
163
+ return response.Response({"status": "pending"})
164
+
165
+ amr = AMrelease()
166
+ error = None
167
+ for mid, rcpt in msgrcpts:
168
+ # we can't use the .mail relation on rcpt because it leads to
169
+ # an error on Postgres (memoryview pickle error).
170
+ mail = models.Msgs.objects.get(pk=mid.encode("ascii"))
171
+ result = amr.sendreq(mid, mail.secret_id, rcpt.rid.email)
172
+ if result:
173
+ connector.set_msgrcpt_status(smart_str(rcpt.rid.email), mid, "R")
174
+ else:
175
+ error = result
176
+ break
177
+
178
+ if error:
179
+ return response.Response({"status": error})
180
+
181
+ return response.Response({"status": "released"})
182
+
183
+ @action(methods=["post"], detail=False)
184
+ def release_selection(self, request):
185
+ serializer = self.get_serializer(data=request.data)
186
+ serializer.is_valid(raise_exception=True)
187
+ return self._release_selection(request, serializer.validated_data["selection"])
188
+
189
+ @action(methods=["post"], detail=True)
190
+ def release(self, request, pk):
191
+ serializer = self.get_serializer(data=request.data)
192
+ serializer.is_valid(raise_exception=True)
193
+ return self._release_selection(request, [serializer.validated_data])
194
+
195
+ def _delete_selection(self, request, selection):
196
+ connector = SQLconnector()
197
+ valid_addresses = None
198
+ if request.user:
199
+ valid_addresses = get_user_valid_addresses(request.user)
200
+ for item in selection:
201
+ if valid_addresses and item["rcpt"] not in valid_addresses:
202
+ continue
203
+ connector.set_msgrcpt_status(item["rcpt"], item["mailid"], "D")
204
+ return response.Response(status=204)
205
+
206
+ @action(methods=["post"], detail=False)
207
+ def delete_selection(self, request):
208
+ serializer = self.get_serializer(data=request.data)
209
+ serializer.is_valid(raise_exception=True)
210
+ return self._delete_selection(request, serializer.validated_data["selection"])
211
+
212
+ @action(methods=["post"], detail=True)
213
+ def delete(self, request, pk):
214
+ serializer = self.get_serializer(data=request.data)
215
+ serializer.is_valid(raise_exception=True)
216
+ return self._delete_selection(request, [serializer.validated_data])
217
+
218
+ @action(methods=["post"], detail=False)
219
+ def mark_selection(self, request):
220
+ serializer = self.get_serializer(data=request.data)
221
+ serializer.is_valid(raise_exception=True)
222
+ if not manual_learning_enabled(request.user):
223
+ return response.Response({"status": "ok"})
224
+ recipient_db = serializer.validated_data.get("database")
225
+ if not recipient_db:
226
+ recipient_db = "user" if request.user.role == "SimpleUsers" else "global"
227
+ queue = django_rq.get_queue("modoboa")
228
+ queue.enqueue(
229
+ tasks.manual_learning,
230
+ request.user.pk,
231
+ serializer.validated_data["type"],
232
+ serializer.validated_data["selection"],
233
+ recipient_db,
234
+ )
235
+ return response.Response(status=204)
236
+
237
+
238
+ class PolicyViewSet(
239
+ mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet
240
+ ):
241
+
242
+ permission_classes = (permissions.IsAuthenticated, CanViewDomain)
243
+ serializer_class = serializers.PolicySerializer
244
+
245
+ def get_queryset(self):
246
+ domains = [
247
+ f"@{name}"
248
+ for name in admin_models.Domain.objects.get_for_admin(
249
+ self.request.user
250
+ ).values_list("name", flat=True)
251
+ ]
252
+ return models.Policy.objects.filter(users__email__in=domains)
253
+
254
+ def get_object(self):
255
+ """Return the object the view is displaying."""
256
+ domain = get_object_or_404(admin_models.Domain, pk=self.kwargs["pk"])
257
+ queryset = self.filter_queryset(self.get_queryset())
258
+ obj = queryset.filter(users__email=f"@{domain.name}").first()
259
+ if obj is None:
260
+ raise Http404
261
+
262
+ # May raise a permission denied
263
+ self.check_object_permissions(self.request, obj)
264
+
265
+ return obj
@@ -83,10 +83,12 @@ class CheckPasswordTFASerializer(serializers.Serializer):
83
83
  def validate_password(self, value):
84
84
  user = self.context["user"]
85
85
  if not user.check_password(value):
86
- 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
@@ -664,9 +664,11 @@ class NotificationSerializer(serializers.Serializer):
664
664
  """Serializer used to render a notification."""
665
665
 
666
666
  id = serializers.CharField()
667
- url = serializers.CharField(required=False)
668
667
  text = serializers.CharField()
669
- level = serializers.CharField()
668
+ color = serializers.CharField()
669
+ target = serializers.CharField()
670
+ url = serializers.CharField(required=False)
671
+ counter = serializers.IntegerField(required=False)
670
672
 
671
673
 
672
674
  class ModoboaApplicationSerializer(serializers.Serializer):
@@ -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=[
@@ -32,11 +32,6 @@ urlpatterns += [
32
32
  views.ComponentsInformationAPIView.as_view(),
33
33
  name="components_information",
34
34
  ),
35
- path(
36
- "admin/notifications/",
37
- views.NotificationsAPIView.as_view(),
38
- name="notifications",
39
- ),
40
35
  path(
41
36
  "admin/news_feed/",
42
37
  views.NewsFeedAPIView.as_view(),
@@ -48,5 +43,10 @@ urlpatterns += [
48
43
  name="statistics",
49
44
  ),
50
45
  path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
46
+ path(
47
+ "notifications/",
48
+ views.NotificationsAPIView.as_view(),
49
+ name="notifications",
50
+ ),
51
51
  path("theme/", views.ThemeAPIView.as_view(), name="theme"),
52
52
  ]
@@ -142,7 +142,9 @@ class PasswordResetConfirmView(APIView):
142
142
  class ComponentsInformationAPIView(APIView):
143
143
  """Retrieve information about installed components."""
144
144
 
145
- permission_classes = [permissions.IsAuthenticated, IsSuperUser]
145
+ permission_classes = [
146
+ permissions.IsAuthenticated,
147
+ ]
146
148
  throttle_classes = [UserLesserDdosUser]
147
149
 
148
150
  @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
@@ -155,7 +157,9 @@ class ComponentsInformationAPIView(APIView):
155
157
  class NotificationsAPIView(APIView):
156
158
  """Return list of active notifications."""
157
159
 
158
- permission_classes = [permissions.IsAuthenticated, IsSuperUser]
160
+ permission_classes = [
161
+ permissions.IsAuthenticated,
162
+ ]
159
163
  throttle_classes = [UserLesserDdosUser]
160
164
 
161
165
  @extend_schema(responses=serializers.NotificationSerializer(many=True))
@@ -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 += [
@@ -218,6 +218,7 @@ class DeployCommand(Command):
218
218
  allowed_host = "localhost"
219
219
  extra_settings = []
220
220
  extensions = parsed_args.extensions
221
+ amavis_enabled = False
221
222
  if extensions:
222
223
  if "all" in extensions:
223
224
  extensions = self._get_extension_list()
@@ -233,6 +234,7 @@ class DeployCommand(Command):
233
234
  exec_cmd(cmd, capture_output=False)
234
235
  extra_settings = self.find_extra_settings(extensions)
235
236
  extensions = [extension[1] for extension in extensions]
237
+ amavis_enabled = "modoboa.amavis" in extensions
236
238
 
237
239
  mod = __import__(parsed_args.name, globals(), locals(), [smart_str("settings")])
238
240
  tpl = self._render_template(
@@ -247,6 +249,7 @@ class DeployCommand(Command):
247
249
  "devmode": parsed_args.devel,
248
250
  "extensions": extensions,
249
251
  "extra_settings": extra_settings,
252
+ "amavis_enabled": amavis_enabled,
250
253
  },
251
254
  )
252
255
  with open(f"{path}/settings.py", "w") as fp:
@@ -2,10 +2,10 @@
2
2
  Django settings for {{ name }} project.
3
3
 
4
4
  For more information on this file, see
5
- https://docs.djangoproject.com/en/4.2/topics/settings/
5
+ https://docs.djangoproject.com/en/dev/topics/settings/
6
6
 
7
7
  For the full list of settings and their values, see
8
- https://docs.djangoproject.com/en/2.2/ref/settings/
8
+ https://docs.djangoproject.com/en/dev/ref/settings/
9
9
  """
10
10
 
11
11
  from logging.handlers import SysLogHandler
@@ -17,9 +17,6 @@ env = environ.Env()
17
17
  BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
18
18
  environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
19
19
 
20
- # Quick-start development settings - unsuitable for production
21
- # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
22
-
23
20
  # SECURITY WARNING: keep the secret key used in production secret!
24
21
  SECRET_KEY = '{{ secret_key }}'
25
22
 
@@ -157,14 +154,14 @@ WSGI_APPLICATION = '{{ name }}.wsgi.application'
157
154
 
158
155
 
159
156
  # Database
160
- # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
157
+ # https://docs.djangoproject.com/en/dev/ref/settings/#databases
161
158
 
162
159
  DATABASES = {
163
160
  {% for conn in db_connections.values %}{{ conn|safe }}{% endfor %}
164
161
  }
165
162
 
166
163
  # Internationalization
167
- # https://docs.djangoproject.com/en/4.2/topics/i18n/
164
+ # https://docs.djangoproject.com/en/dev/topics/i18n/
168
165
 
169
166
  LANGUAGE_CODE = '{{ lang }}'
170
167
 
@@ -177,12 +174,12 @@ USE_L10N = True
177
174
  USE_TZ = True
178
175
 
179
176
  # Default primary key field type
180
- # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
177
+ # https://docs.djangoproject.com/en/dev/ref/settings/#default-auto-field
181
178
 
182
179
  DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
183
180
 
184
181
  # Static files (CSS, JavaScript, Images)
185
- # https://docs.djangoproject.com/en/2.2/howto/static-files/
182
+ # https://docs.djangoproject.com/en/dev/howto/static-files/
186
183
 
187
184
  STATIC_URL = '/sitestatic/'
188
185
  STATIC_ROOT = os.path.join(BASE_DIR, 'sitestatic')
@@ -295,7 +292,7 @@ CACHES = {
295
292
 
296
293
 
297
294
  # Password validation
298
- # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
295
+ # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
299
296
 
300
297
  AUTH_PASSWORD_VALIDATORS = [
301
298
  {
@@ -384,7 +381,11 @@ SILENCED_SYSTEM_CHECKS = [
384
381
  ]
385
382
 
386
383
  PHONENUMBER_DB_FORMAT = 'INTERNATIONAL'
387
-
384
+ {% if amavis_enabled %}
385
+ # Amavis settings
386
+ DATABASE_ROUTERS = ["modoboa.amavis.dbrouter.AmavisRouter"]
387
+ AMAVIS_DEFAULT_DATABASE_ENCODING = "LATIN1" # or any value matching your database config
388
+ {% endif %}
388
389
  # Load settings from extensions
389
390
  {% for extension in extra_settings %}
390
391
  try:
modoboa/core/handlers.py CHANGED
@@ -125,7 +125,9 @@ def check_for_new_versions(sender, include_all: bool, **kwargs) -> list:
125
125
  {
126
126
  "id": "newversionavailable",
127
127
  "text": _("One or more updates are available"),
128
- "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()
@@ -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
  ]
@@ -186,6 +186,30 @@ class ManagementCommandsTestCase(SimpleModoTestCase):
186
186
  with self.assertRaises(models.User.DoesNotExist):
187
187
  account.refresh_from_db()
188
188
 
189
+ def test_add_allowed_hosts(self):
190
+ with self.assertRaises(management.CommandError):
191
+ management.call_command(
192
+ "add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
193
+ )
194
+ management.call_command("load_initial_data")
195
+ management.call_command(
196
+ "add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
197
+ )
198
+ app_model = get_application_model()
199
+ app = app_model.objects.filter(name="modoboa_frontend").first()
200
+ self.assertIn("app1.domain.tld", app.redirect_uris)
201
+ self.assertIn("app2.domain.tld", app.redirect_uris)
202
+ self.assertIn("app1.domain.tld", app.post_logout_redirect_uris)
203
+ self.assertIn("app2.domain.tld", app.post_logout_redirect_uris)
204
+
205
+ # Check if same hostname is not added more than once
206
+ uri_count = len(app.redirect_uris.split(" "))
207
+ management.call_command(
208
+ "add_allowed_hosts", "app1.domain.tld", "app2.domain.tld"
209
+ )
210
+ app.refresh_from_db()
211
+ self.assertEqual(len(app.redirect_uris.split(" ")), uri_count)
212
+
189
213
 
190
214
  class APICommunicationTestCase(ModoTestCase):
191
215
  """Check communication with the API."""