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,226 @@
1
+ # This is an auto-generated Django model module.
2
+ # You'll have to do the following manually to clean this up:
3
+ # * Rearrange models' order
4
+ # * Make sure each model has one field with primary_key=True
5
+ # Feel free to rename the models, but don't rename db_table values
6
+ # or field names.
7
+ #
8
+ # Also note: You'll have to insert the output of
9
+ # 'django-admin.py sqlcustom [appname]'into your database.
10
+ #
11
+ # Original Amavis version : 2.6.2
12
+
13
+ from datetime import datetime
14
+
15
+ from django.db import models
16
+ from django.utils.translation import gettext_lazy
17
+
18
+
19
+ class Maddr(models.Model):
20
+ partition_tag = models.IntegerField(default=0)
21
+ id = models.BigIntegerField(primary_key=True) # NOQA:A003
22
+ email = models.CharField(max_length=255)
23
+ domain = models.CharField(max_length=255)
24
+
25
+ class Meta:
26
+ db_table = "maddr"
27
+ unique_together = [("partition_tag", "email")]
28
+ managed = False
29
+
30
+
31
+ class Mailaddr(models.Model):
32
+ id = models.IntegerField(primary_key=True) # NOQA:A003
33
+ priority = models.IntegerField()
34
+ email = models.CharField(unique=True, max_length=255)
35
+
36
+ class Meta:
37
+ db_table = "mailaddr"
38
+ managed = False
39
+
40
+
41
+ class Msgs(models.Model):
42
+ partition_tag = models.IntegerField(default=0)
43
+ mail_id = models.BinaryField(max_length=16, primary_key=True)
44
+ secret_id = models.BinaryField()
45
+ am_id = models.CharField(max_length=60)
46
+ time_num = models.IntegerField()
47
+ time_iso = models.CharField(max_length=48)
48
+ sid = models.ForeignKey(Maddr, db_column="sid", on_delete=models.CASCADE)
49
+ policy = models.CharField(max_length=765, blank=True)
50
+ client_addr = models.CharField(max_length=765, blank=True)
51
+ size = models.IntegerField()
52
+ originating = models.CharField(max_length=3)
53
+ content = models.CharField(max_length=1, blank=True)
54
+ quar_type = models.CharField(max_length=1, blank=True)
55
+ quar_loc = models.CharField(max_length=255, blank=True)
56
+ dsn_sent = models.CharField(max_length=3, blank=True)
57
+ spam_level = models.FloatField(null=True, blank=True)
58
+ message_id = models.CharField(max_length=765, blank=True)
59
+ from_addr = models.CharField(max_length=765, blank=True)
60
+ subject = models.CharField(max_length=765, blank=True)
61
+ host = models.CharField(max_length=765)
62
+
63
+ class Meta:
64
+ db_table = "msgs"
65
+ managed = False
66
+ unique_together = ("partition_tag", "mail_id")
67
+
68
+ @property
69
+ def time_datetime(self) -> datetime:
70
+ return datetime.fromtimestamp(self.time_num)
71
+
72
+
73
+ class Msgrcpt(models.Model):
74
+ partition_tag = models.IntegerField(default=0)
75
+ mail = models.ForeignKey(Msgs, primary_key=True, on_delete=models.CASCADE)
76
+ rid = models.ForeignKey(Maddr, db_column="rid", on_delete=models.CASCADE)
77
+ rseqnum = models.IntegerField(default=0)
78
+ is_local = models.CharField(max_length=3)
79
+ content = models.CharField(max_length=3)
80
+ ds = models.CharField(max_length=3)
81
+ rs = models.CharField(max_length=3)
82
+ bl = models.CharField(max_length=3, blank=True)
83
+ wl = models.CharField(max_length=3, blank=True)
84
+ bspam_level = models.FloatField(null=True, blank=True)
85
+ smtp_resp = models.CharField(max_length=765, blank=True)
86
+
87
+ class Meta:
88
+ db_table = "msgrcpt"
89
+ managed = False
90
+ unique_together = ("partition_tag", "mail", "rseqnum")
91
+
92
+
93
+ class Policy(models.Model):
94
+ policy_name = models.CharField(max_length=32, blank=True)
95
+ virus_lover = models.CharField(max_length=3, blank=True, null=True)
96
+ spam_lover = models.CharField(max_length=3, blank=True, null=True)
97
+ unchecked_lover = models.CharField(max_length=3, blank=True, null=True)
98
+ banned_files_lover = models.CharField(max_length=3, blank=True, null=True)
99
+ bad_header_lover = models.CharField(max_length=3, blank=True, null=True)
100
+ bypass_virus_checks = models.CharField(
101
+ gettext_lazy("Virus filter"),
102
+ default="",
103
+ null=True,
104
+ choices=(
105
+ ("N", gettext_lazy("yes")),
106
+ ("Y", gettext_lazy("no")),
107
+ ("", gettext_lazy("default")),
108
+ ),
109
+ max_length=3,
110
+ help_text=gettext_lazy(
111
+ "Bypass virus checks or not. Choose 'default' to use global " "settings."
112
+ ),
113
+ )
114
+ bypass_spam_checks = models.CharField(
115
+ gettext_lazy("Spam filter"),
116
+ default="",
117
+ null=True,
118
+ choices=(
119
+ ("N", gettext_lazy("yes")),
120
+ ("Y", gettext_lazy("no")),
121
+ ("", gettext_lazy("default")),
122
+ ),
123
+ max_length=3,
124
+ help_text=gettext_lazy(
125
+ "Bypass spam checks or not. Choose 'default' to use global " "settings."
126
+ ),
127
+ )
128
+ bypass_banned_checks = models.CharField(
129
+ gettext_lazy("Banned filter"),
130
+ default="",
131
+ null=True,
132
+ choices=(
133
+ ("N", gettext_lazy("yes")),
134
+ ("Y", gettext_lazy("no")),
135
+ ("", gettext_lazy("default")),
136
+ ),
137
+ max_length=3,
138
+ help_text=gettext_lazy(
139
+ "Bypass banned checks or not. Choose 'default' to use global " "settings."
140
+ ),
141
+ )
142
+ bypass_header_checks = models.CharField(max_length=3, blank=True, null=True)
143
+ virus_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
144
+ spam_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
145
+ banned_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
146
+ unchecked_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
147
+ bad_header_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
148
+ clean_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
149
+ archive_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
150
+ spam_tag_level = models.FloatField(null=True, blank=True)
151
+ spam_tag2_level = models.FloatField(null=True, blank=True)
152
+ spam_tag3_level = models.FloatField(null=True, blank=True)
153
+ spam_kill_level = models.FloatField(null=True, blank=True)
154
+ spam_dsn_cutoff_level = models.FloatField(null=True, blank=True)
155
+ spam_quarantine_cutoff_level = models.FloatField(null=True, blank=True)
156
+ addr_extension_virus = models.CharField(max_length=192, blank=True, null=True)
157
+ addr_extension_spam = models.CharField(max_length=192, blank=True, null=True)
158
+ addr_extension_banned = models.CharField(max_length=192, blank=True, null=True)
159
+ addr_extension_bad_header = models.CharField(max_length=192, blank=True, null=True)
160
+ warnvirusrecip = models.CharField(max_length=3, blank=True, null=True)
161
+ warnbannedrecip = models.CharField(max_length=3, blank=True, null=True)
162
+ warnbadhrecip = models.CharField(max_length=3, blank=True, null=True)
163
+ newvirus_admin = models.CharField(max_length=192, blank=True, null=True)
164
+ virus_admin = models.CharField(max_length=192, blank=True, null=True)
165
+ banned_admin = models.CharField(max_length=192, blank=True, null=True)
166
+ bad_header_admin = models.CharField(max_length=192, blank=True, null=True)
167
+ spam_admin = models.CharField(max_length=192, blank=True, null=True)
168
+ spam_subject_tag = models.CharField(max_length=192, blank=True, null=True)
169
+ spam_subject_tag2 = models.CharField(
170
+ gettext_lazy("Spam marker"),
171
+ default=None,
172
+ max_length=192,
173
+ blank=True,
174
+ null=True,
175
+ help_text=gettext_lazy(
176
+ "Modify spam subject using the specified text. "
177
+ "Choose 'default' to use global settings."
178
+ ),
179
+ )
180
+ spam_subject_tag3 = models.CharField(max_length=192, blank=True, null=True)
181
+ message_size_limit = models.IntegerField(null=True, blank=True)
182
+ banned_rulenames = models.CharField(max_length=192, blank=True, null=True)
183
+ disclaimer_options = models.CharField(max_length=192, blank=True, null=True)
184
+ forward_method = models.CharField(max_length=192, blank=True, null=True)
185
+ sa_userconf = models.CharField(max_length=192, blank=True, null=True)
186
+ sa_username = models.CharField(max_length=192, blank=True, null=True)
187
+
188
+ class Meta:
189
+ db_table = "policy"
190
+ managed = False
191
+
192
+
193
+ class Quarantine(models.Model):
194
+ partition_tag = models.IntegerField(default=0)
195
+ mail = models.ForeignKey(Msgs, primary_key=True, on_delete=models.CASCADE)
196
+ chunk_ind = models.IntegerField()
197
+ mail_text = models.BinaryField()
198
+
199
+ class Meta:
200
+ db_table = "quarantine"
201
+ managed = False
202
+ ordering = ["-mail__time_num"]
203
+ unique_together = ("partition_tag", "mail", "chunk_ind")
204
+
205
+
206
+ class Users(models.Model):
207
+ id = models.AutoField(primary_key=True) # NOQA:A003
208
+ priority = models.IntegerField()
209
+ policy = models.ForeignKey(Policy, on_delete=models.CASCADE)
210
+ email = models.CharField(unique=True, max_length=255)
211
+ fullname = models.CharField(max_length=765, blank=True)
212
+
213
+ class Meta:
214
+ db_table = "users"
215
+ managed = False
216
+
217
+
218
+ class Wblist(models.Model):
219
+ rid = models.IntegerField(primary_key=True)
220
+ sid = models.IntegerField()
221
+ wb = models.CharField(max_length=30)
222
+
223
+ class Meta:
224
+ db_table = "wblist"
225
+ managed = False
226
+ unique_together = [("rid", "sid")]
@@ -0,0 +1,139 @@
1
+ """Amavis serializers."""
2
+
3
+ from django.utils.translation import gettext as _
4
+
5
+ from rest_framework import serializers
6
+
7
+ from modoboa.amavis import models
8
+
9
+
10
+ class GlobalParametersSerializer(serializers.Serializer):
11
+
12
+ localpart_is_case_sensitive = serializers.BooleanField(default=False)
13
+ recipient_delimiter = serializers.CharField(default="")
14
+ max_messages_age = serializers.IntegerField(default=14)
15
+ released_msgs_cleanup = serializers.BooleanField(default=False)
16
+ am_pdp_mode = serializers.ChoiceField(
17
+ choices=[("inet", "inet"), ("unix", "unix")],
18
+ default="unix",
19
+ )
20
+ am_pdp_host = serializers.CharField(default="localhost", required=False)
21
+ am_pdp_port = serializers.IntegerField(default=9998, required=False)
22
+ am_pdp_socket = serializers.CharField(
23
+ default="/var/amavis/amavisd.sock", required=False
24
+ )
25
+ user_can_release = serializers.BooleanField(default=False)
26
+ self_service = serializers.BooleanField(default=False)
27
+ notifications_sender = serializers.EmailField(default="noreply@domain.tld")
28
+ manual_learning = serializers.BooleanField(default=True)
29
+ sa_is_local = serializers.BooleanField(default=True)
30
+ default_user = serializers.CharField(default="amavis", required=False)
31
+ spamd_address = serializers.CharField(default="127.0.0.1", required=False)
32
+ spamd_port = serializers.IntegerField(default=783, required=False)
33
+ domain_level_learning = serializers.BooleanField(default=False)
34
+ user_level_learning = serializers.BooleanField(default=False)
35
+
36
+ def validate(self, data):
37
+ am_pdp_mode = data.get("am_pdp_mode")
38
+ errors = {}
39
+ error = _("This field is required")
40
+ if am_pdp_mode == "inet":
41
+ for name in ["am_pdp_host", "am_pdp_port"]:
42
+ if name not in data:
43
+ errors[name] = error
44
+ else:
45
+ name = "am_pdp_socket"
46
+ if name not in data:
47
+ errors[name] = error
48
+ manual_learning = data.get("manual_learning")
49
+ if manual_learning:
50
+ name = "default_user"
51
+ if name not in data:
52
+ errors[name] = error
53
+ sa_is_local = data.get("sa_is_local")
54
+ if sa_is_local:
55
+ for name in ["spamd_address", "spamd_port"]:
56
+ if name not in data:
57
+ errors[name] = error
58
+ if errors:
59
+ raise serializers.ValidationError(errors)
60
+ return data
61
+
62
+
63
+ class UserPreferencesSerializer(serializers.Serializer):
64
+
65
+ messages_per_page = serializers.IntegerField(default=40)
66
+
67
+
68
+ class HeaderSerializer(serializers.Serializer):
69
+
70
+ name = serializers.CharField()
71
+ value = serializers.CharField()
72
+
73
+
74
+ class MessageHeadersSerializer(serializers.Serializer):
75
+
76
+ headers = HeaderSerializer(many=True)
77
+
78
+
79
+ class MessageSerializer(serializers.Serializer):
80
+
81
+ qtype = serializers.CharField()
82
+ qreason = serializers.CharField()
83
+ headers = HeaderSerializer(many=True)
84
+ body = serializers.CharField()
85
+
86
+
87
+ class CompactMessageSerializer(serializers.Serializer):
88
+
89
+ mailid = serializers.CharField()
90
+ type = serializers.CharField()
91
+ status = serializers.CharField()
92
+ score = serializers.CharField()
93
+ to_address = serializers.CharField()
94
+ from_address = serializers.CharField()
95
+ subject = serializers.CharField()
96
+ datetime = serializers.DateTimeField()
97
+ style = serializers.CharField(required=False)
98
+
99
+
100
+ class PaginatedMessageListSerializer(serializers.Serializer):
101
+
102
+ count = serializers.IntegerField()
103
+ first_index = serializers.IntegerField()
104
+ last_index = serializers.IntegerField()
105
+ prev_page = serializers.IntegerField()
106
+ next_page = serializers.IntegerField()
107
+ results = CompactMessageSerializer(many=True)
108
+
109
+
110
+ class MessageIdentifierSerializer(serializers.Serializer):
111
+
112
+ mailid = serializers.CharField()
113
+ rcpt = serializers.CharField()
114
+ secret_id = serializers.CharField(required=False)
115
+
116
+
117
+ class MessageSelectionSerializer(serializers.Serializer):
118
+
119
+ selection = MessageIdentifierSerializer(many=True)
120
+
121
+
122
+ class MarkMessageSelectionSerializer(MessageSelectionSerializer):
123
+
124
+ type = serializers.CharField()
125
+ database = serializers.CharField(required=False)
126
+
127
+
128
+ class PolicySerializer(serializers.ModelSerializer):
129
+
130
+ class Meta:
131
+ model = models.Policy
132
+ fields = (
133
+ "bypass_virus_checks",
134
+ "bypass_spam_checks",
135
+ "bypass_banned_checks",
136
+ "spam_tag2_level",
137
+ "spam_kill_level",
138
+ "spam_subject_tag2",
139
+ )
@@ -0,0 +1,240 @@
1
+ """SQL connector module."""
2
+
3
+ import datetime
4
+
5
+ from django.db.models import Q
6
+
7
+ from modoboa.admin.models import Domain
8
+ from modoboa.lib.email_utils import decode
9
+
10
+ from .lib import cleanup_email_address, make_query_args
11
+ from .models import Maddr, Msgrcpt, Quarantine
12
+ from .utils import ConvertFrom, fix_utf8_encoding, smart_bytes, smart_str
13
+
14
+
15
+ def reverse_domain_names(domains):
16
+ """Return a list of reversed domain names."""
17
+ return [".".join(reversed(domain.split("."))) for domain in domains]
18
+
19
+
20
+ class SQLconnector:
21
+ """This class handles all database operations."""
22
+
23
+ ORDER_TRANSLATION_TABLE = {
24
+ "type": "content",
25
+ "score": "bspam_level",
26
+ "datetime": "mail__time_num",
27
+ "subject": "mail__subject",
28
+ "from_address": "mail__from_addr",
29
+ "to_address": "rid__email",
30
+ }
31
+
32
+ QUARANTINE_FIELDS = [
33
+ "content",
34
+ "bspam_level",
35
+ "rs",
36
+ "rid__email",
37
+ "mail__from_addr",
38
+ "mail__subject",
39
+ "mail__mail_id",
40
+ "mail__time_num",
41
+ ]
42
+
43
+ def __init__(self, user=None, ordering: str | None = None) -> None:
44
+ """Constructor."""
45
+ self.user = user
46
+ self.messages = None
47
+ self.ordering = ordering
48
+
49
+ self._messages_count = None
50
+ self._annotations = {}
51
+
52
+ def _exec(self, query, args):
53
+ """Execute a raw SQL query.
54
+
55
+ :param string query: query to execute
56
+ :param list args: a list of arguments to replace in :kw:`query`
57
+ """
58
+ from django.db import connections, transaction
59
+
60
+ with transaction.atomic():
61
+ cursor = connections["amavis"].cursor()
62
+ cursor.execute(query, args)
63
+
64
+ def _apply_msgrcpt_simpleuser_filter(self, flt):
65
+ """Apply specific filter for simple users."""
66
+ if "str_email" not in self._annotations:
67
+ self._annotations["str_email"] = ConvertFrom("rid__email")
68
+
69
+ rcpts = [self.user.email]
70
+ if hasattr(self.user, "mailbox"):
71
+ rcpts += self.user.mailbox.alias_addresses
72
+
73
+ query_rcpts = []
74
+ for rcpt in rcpts:
75
+ query_rcpts += make_query_args(rcpt, exact_extension=False, wildcard=".*")
76
+
77
+ re = f"({'|'.join(query_rcpts)})"
78
+ return flt & Q(str_email__regex=re)
79
+
80
+ def _apply_msgrcpt_filters(self, flt):
81
+ """Apply filters based on user's role."""
82
+ if self.user.role == "SimpleUsers":
83
+ flt = self._apply_msgrcpt_simpleuser_filter(flt)
84
+ elif not self.user.is_superuser:
85
+ doms = Domain.objects.get_for_admin(self.user).values_list(
86
+ "name", flat=True
87
+ )
88
+ flt &= Q(rid__domain__in=reverse_domain_names(doms))
89
+ return flt
90
+
91
+ def _get_quarantine_content(self, request):
92
+ """Fetch quarantine content.
93
+
94
+ Filters: rs, rid, content
95
+ """
96
+ flt = (
97
+ Q(rs__in=[" ", "V", "R", "p", "S", "H"])
98
+ if request.GET.get("viewrequests", "0") != "1"
99
+ else Q(rs="p")
100
+ )
101
+ flt = self._apply_msgrcpt_filters(flt)
102
+ search = request.GET.get("search")
103
+ if search:
104
+ criteria = request.GET.get("criteria", "both")
105
+ if criteria == "both":
106
+ criteria = "from_addr,subject,to"
107
+ search_flt = None
108
+ for crit in criteria.split(","):
109
+ if crit == "from_addr":
110
+ nfilter = Q(mail__from_addr__icontains=search)
111
+ elif crit == "subject":
112
+ nfilter = Q(mail__subject__icontains=search)
113
+ elif crit == "to":
114
+ if "str_email" not in self._annotations:
115
+ self._annotations["str_email"] = ConvertFrom("rid__email")
116
+ nfilter = Q(str_email__icontains=search)
117
+ else:
118
+ continue
119
+ search_flt = nfilter if search_flt is None else search_flt | nfilter
120
+ if search_flt:
121
+ flt &= search_flt
122
+
123
+ msgtype = request.GET.get("msgtype")
124
+ if msgtype is not None:
125
+ flt &= Q(content=msgtype)
126
+
127
+ flt &= Q(mail__in=Quarantine.objects.filter(chunk_ind=1).values("mail_id"))
128
+
129
+ return (
130
+ Msgrcpt.objects.annotate(**self._annotations)
131
+ .select_related("mail", "rid")
132
+ .filter(flt)
133
+ )
134
+
135
+ def messages_count(self, request) -> int:
136
+ """
137
+ Return the total number of messages living in the quarantine.
138
+
139
+ We also store the built queryset for a later use.
140
+ """
141
+ if self.user is None:
142
+ return None
143
+ if self._messages_count is None:
144
+ self.messages = self._get_quarantine_content(request)
145
+ self.messages = self.messages.values(*self.QUARANTINE_FIELDS)
146
+
147
+ order = self.ordering
148
+ if order is not None:
149
+ sign = ""
150
+ if order[0] == "-":
151
+ sign = "-"
152
+ order = order[1:]
153
+ order = self.ORDER_TRANSLATION_TABLE.get(order)
154
+ if order:
155
+ self.messages = self.messages.order_by(sign + order)
156
+
157
+ self._messages_count = len(self.messages)
158
+
159
+ return self._messages_count
160
+
161
+ def fetch(self, start=None, stop=None):
162
+ """Fetch a range of messages from the internal cache."""
163
+ emails = []
164
+ for qm in self.messages[start - 1 : stop]:
165
+ if qm["rs"] == "D":
166
+ continue
167
+ m = {
168
+ "from_address": cleanup_email_address(
169
+ fix_utf8_encoding(qm["mail__from_addr"])
170
+ ),
171
+ "to_address": smart_str(qm["rid__email"]),
172
+ "subject": fix_utf8_encoding(qm["mail__subject"]),
173
+ "mailid": smart_str(qm["mail__mail_id"]),
174
+ "datetime": datetime.datetime.fromtimestamp(qm["mail__time_num"]),
175
+ "type": qm["content"],
176
+ "score": qm["bspam_level"],
177
+ "status": qm["rs"],
178
+ }
179
+ if qm["rs"] in ["", " "]:
180
+ m["style"] = "unseen"
181
+ elif qm["rs"] == "p":
182
+ m["style"] = "pending"
183
+ emails.append(m)
184
+ return emails
185
+
186
+ def get_recipient_message(self, address, mailid):
187
+ """Retrieve a message for a given recipient."""
188
+ assert isinstance(address, str), "address should be of type str"
189
+
190
+ return Msgrcpt.objects.annotate(str_email=ConvertFrom("rid__email")).get(
191
+ mail=mailid.encode("ascii"), str_email=address
192
+ )
193
+
194
+ def set_msgrcpt_status(self, address, mailid: str, status):
195
+ """Change the status (rs field) of a message recipient.
196
+
197
+ :param string status: status
198
+ """
199
+ assert isinstance(address, str), "address should be of type str"
200
+ addr = Maddr.objects.annotate(str_email=ConvertFrom("email")).get(
201
+ str_email=address
202
+ )
203
+ self._exec(
204
+ "UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s",
205
+ [status, mailid.encode("ascii"), addr.id],
206
+ )
207
+
208
+ def get_domains_pending_requests(self, domains):
209
+ """Retrieve pending release requests for a list of domains."""
210
+ return Msgrcpt.objects.filter(
211
+ rs="p", rid__domain__in=reverse_domain_names(domains)
212
+ )
213
+
214
+ def get_pending_requests(self):
215
+ """Return the number of requests currently pending."""
216
+ rq = Q(rs="p")
217
+ if not self.user.is_superuser:
218
+ doms = Domain.objects.get_for_admin(self.user)
219
+ if not doms.exists():
220
+ return 0
221
+ doms_q = Q(
222
+ rid__domain__in=reverse_domain_names(
223
+ doms.values_list("name", flat=True)
224
+ )
225
+ )
226
+ rq &= doms_q
227
+ return Msgrcpt.objects.filter(rq).count()
228
+
229
+ def get_mail_content(self, mailid) -> str:
230
+ """Retrieve the content of a message."""
231
+ content_bytes = smart_bytes("").join(
232
+ [
233
+ smart_bytes(qmail.mail_text)
234
+ for qmail in Quarantine.objects.filter(mail=mailid)
235
+ ]
236
+ )
237
+ content = decode(
238
+ content_bytes, "utf-8", append_to_error=f"; mail_id={smart_str(mailid)}"
239
+ )
240
+ return content
@@ -0,0 +1,66 @@
1
+ """An email representation based on a database record."""
2
+
3
+ from html2text import HTML2Text
4
+
5
+ from django.template.loader import render_to_string
6
+
7
+ from modoboa.lib.email_utils import Email
8
+ from .sql_connector import SQLconnector
9
+ from .utils import fix_utf8_encoding, smart_str
10
+
11
+
12
+ class SQLemail(Email):
13
+ """The SQL version of the Email class."""
14
+
15
+ def __init__(self, *args, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ self.qtype = ""
18
+ self.qreason = ""
19
+
20
+ qreason = self.msg["X-Amavis-Alert"]
21
+ if qreason:
22
+ if "," in qreason:
23
+ self.qtype, qreason = qreason.split(",", 1)
24
+ elif qreason.startswith("BAD HEADER SECTION "):
25
+ # Workaround for amavis <= 2.8.0 :p
26
+ self.qtype = "BAD HEADER SECTION"
27
+ qreason = qreason[19:]
28
+
29
+ qreason = " ".join([x.strip() for x in qreason.splitlines()])
30
+ self.qreason = qreason
31
+
32
+ def _fetch_message(self) -> str:
33
+ return SQLconnector().get_mail_content(self.mailid)
34
+
35
+ @property
36
+ def body(self):
37
+ if self._body is None:
38
+ super().body # noqa
39
+ self._body = fix_utf8_encoding(self._body)
40
+
41
+ # if there's no plain text version available attempt to make one by
42
+ # sanitising the html version. The output isn't always pretty but it
43
+ # is readable, better than a blank screen and helps the user decide
44
+ # if the message is spam or ham.
45
+ if (
46
+ self.dformat == "plain"
47
+ and not self.contents["plain"]
48
+ and self.contents["html"]
49
+ ):
50
+ h = HTML2Text()
51
+ h.ignore_tables = True
52
+ h.images_to_alt = True
53
+ mail_text = h.handle(self.contents["html"])
54
+ self.contents["plain"] = self._post_process_plain(smart_str(mail_text))
55
+ self._body = self.viewmail_plain()
56
+ self._body = fix_utf8_encoding(self._body)
57
+
58
+ return self._body
59
+
60
+ def render_headers(self, **kwargs):
61
+ context = {
62
+ "qtype": self.qtype,
63
+ "qreason": self.qreason,
64
+ "headers": self.headers,
65
+ }
66
+ return render_to_string("modoboa_amavis/mailheaders.html", context)