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,146 @@
1
+ """Amavis handlers."""
2
+
3
+ from django.db.models import signals
4
+ from django.dispatch import receiver
5
+ from django.utils.translation import gettext as _
6
+
7
+ from modoboa.admin import models as admin_models
8
+ from modoboa.core import signals as core_signals
9
+ from modoboa.lib import signals as lib_signals
10
+ from modoboa.parameters import tools as param_tools
11
+ from .lib import (
12
+ create_user_and_policy,
13
+ create_user_and_use_policy,
14
+ delete_user,
15
+ delete_user_and_policy,
16
+ update_user_and_policy,
17
+ )
18
+ from .models import Policy, Users
19
+ from .sql_connector import SQLconnector
20
+
21
+
22
+ @receiver(signals.post_save, sender=admin_models.Domain)
23
+ def manage_domain_policy(sender, instance, **kwargs):
24
+ """Create user and policy when a domain is added."""
25
+ if kwargs.get("created"):
26
+ create_user_and_policy(f"@{instance.name}")
27
+ else:
28
+ update_user_and_policy(f"@{instance.oldname}", f"@{instance.name}")
29
+
30
+
31
+ @receiver(signals.pre_delete, sender=admin_models.Domain)
32
+ def on_domain_deleted(sender, instance, **kwargs):
33
+ """Delete user and policy for domain."""
34
+ delete_user_and_policy(f"@{instance.name}")
35
+
36
+
37
+ @receiver(signals.post_save, sender=admin_models.DomainAlias)
38
+ def on_domain_alias_created(sender, instance, **kwargs):
39
+ """Create user and use domain policy for domain alias."""
40
+ if not kwargs.get("created"):
41
+ return
42
+ create_user_and_use_policy(f"@{instance.name}", f"@{instance.target.name}")
43
+
44
+
45
+ @receiver(signals.pre_delete, sender=admin_models.DomainAlias)
46
+ def on_domain_alias_deleted(sender, instance, **kwargs):
47
+ """Delete user for domain alias."""
48
+ delete_user(f"@{instance.name}")
49
+
50
+
51
+ @receiver(signals.post_save, sender=admin_models.Mailbox)
52
+ def on_mailbox_modified(sender, instance, **kwargs):
53
+ """Update amavis records if address has changed."""
54
+ condition = (
55
+ not param_tools.get_global_parameter("manual_learning")
56
+ or not hasattr(instance, "old_full_address")
57
+ or instance.full_address == instance.old_full_address
58
+ )
59
+ if condition:
60
+ return
61
+ try:
62
+ user = Users.objects.select_related("policy").get(
63
+ email=instance.old_full_address
64
+ )
65
+ except Users.DoesNotExist:
66
+ return
67
+ full_address = instance.full_address
68
+ user.email = full_address
69
+ user.policy.policy_name = full_address[:32]
70
+ user.policy.sa_username = full_address
71
+ user.policy.save()
72
+ user.save()
73
+
74
+
75
+ @receiver(signals.pre_delete, sender=admin_models.Mailbox)
76
+ def on_mailbox_deleted(sender, instance, **kwargs):
77
+ """Clean amavis database when a mailbox is removed."""
78
+ if not param_tools.get_global_parameter("manual_learning"):
79
+ return
80
+ delete_user_and_policy(f"@{instance.full_address}")
81
+
82
+
83
+ @receiver(signals.post_save, sender=admin_models.AliasRecipient)
84
+ def on_aliasrecipient_created(sender, instance, **kwargs):
85
+ """Create amavis record for the new alias recipient.
86
+
87
+ FIXME: how to deal with distibution lists ?
88
+ """
89
+ conf = dict(param_tools.get_global_parameters("amavis"))
90
+ condition = (
91
+ not conf["manual_learning"]
92
+ or not conf["user_level_learning"]
93
+ or not instance.r_mailbox
94
+ or instance.alias.type != "alias"
95
+ )
96
+ if condition:
97
+ return
98
+ policy = Policy.objects.filter(policy_name=instance.r_mailbox.full_address).first()
99
+ if policy:
100
+ # Use mailbox policy for this new alias. We update or create
101
+ # to handle the case where an account is being replaced by an
102
+ # alias (when it is disabled).
103
+ email = instance.alias.address
104
+ Users.objects.update_or_create(
105
+ email=email, defaults={"policy": policy, "fullname": email, "priority": 7}
106
+ )
107
+
108
+
109
+ @receiver(signals.pre_delete, sender=admin_models.Alias)
110
+ def on_mailboxalias_deleted(sender, instance, **kwargs):
111
+ """Clean amavis database when an alias is removed."""
112
+ if not param_tools.get_global_parameter("manual_learning"):
113
+ return
114
+ if instance.address.startswith("@"):
115
+ # Catchall alias, do not remove domain entry accidentally...
116
+ return
117
+ aliases = [instance.address]
118
+ Users.objects.filter(email__in=aliases).delete()
119
+
120
+
121
+ @receiver(core_signals.get_top_notifications)
122
+ def check_for_pending_requests(sender, include_all, **kwargs):
123
+ """Check if release requests are pending."""
124
+ request = lib_signals.get_request()
125
+ condition = (
126
+ param_tools.get_global_parameter("user_can_release")
127
+ or request.user.role == "SimpleUsers"
128
+ )
129
+ if condition:
130
+ return []
131
+
132
+ nbrequests = SQLconnector(user=request.user).get_pending_requests()
133
+ if not nbrequests:
134
+ return [{"id": "nbrequests", "counter": 0}] if include_all else []
135
+
136
+ url = "/user/quarantine?requests=1"
137
+ return [
138
+ {
139
+ "id": "nbrequests",
140
+ "url": url,
141
+ "text": _("Pending requests"),
142
+ "counter": nbrequests,
143
+ "color": "error",
144
+ "target": "all",
145
+ }
146
+ ]
modoboa/amavis/lib.py ADDED
@@ -0,0 +1,381 @@
1
+ import os
2
+ import re
3
+ import socket
4
+ import struct
5
+ from email.utils import parseaddr
6
+
7
+ import idna
8
+
9
+ from django.conf import settings
10
+ from django.utils.translation import gettext as _
11
+
12
+ from rest_framework import authentication, exceptions
13
+
14
+ from modoboa.admin import models as admin_models
15
+ from modoboa.lib.email_utils import split_address, split_local_part, split_mailbox
16
+ from modoboa.lib.exceptions import InternalError
17
+ from modoboa.lib.sysutils import exec_cmd
18
+ from modoboa.parameters import tools as param_tools
19
+ from .models import Msgrcpt, Policy, Users
20
+ from .utils import smart_bytes, smart_str
21
+
22
+
23
+ class SelfServiceAuthentication(authentication.BaseAuthentication):
24
+
25
+ def authenticate(self, request):
26
+ from .sql_connector import SQLconnector
27
+
28
+ mail_id = request._request.resolver_match.kwargs.get("pk")
29
+ if request.method == "GET":
30
+ rcpt = request.GET.get("rcpt")
31
+ secret_id = request.GET.get("secret_id")
32
+ else:
33
+ rcpt = request.data.get("rcpt")
34
+ secret_id = request.data.get("secret_id")
35
+ if not mail_id or not rcpt or not secret_id:
36
+ return None
37
+ connector = SQLconnector()
38
+ try:
39
+ msgrcpt = connector.get_recipient_message(rcpt, mail_id)
40
+ except Msgrcpt.DoesNotExist:
41
+ raise exceptions.AuthenticationFailed("Invalid credentials") from None
42
+ if secret_id != smart_str(msgrcpt.mail.secret_id):
43
+ raise exceptions.AuthenticationFailed("Invalid credentials")
44
+ return (None, "selfservice")
45
+
46
+
47
+ class AMrelease:
48
+ def __init__(self):
49
+ conf = dict(param_tools.get_global_parameters("amavis"))
50
+ try:
51
+ if conf["am_pdp_mode"] == "inet":
52
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
53
+ self.sock.connect((conf["am_pdp_host"], conf["am_pdp_port"]))
54
+ else:
55
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
56
+ self.sock.connect(conf["am_pdp_socket"])
57
+ except OSError as err:
58
+ raise InternalError(
59
+ _("Connection to amavis failed: %s") % str(err)
60
+ ) from None
61
+
62
+ def decode(self, answer):
63
+ def repl(match):
64
+ return struct.pack("B", int(match.group(0)[1:], 16))
65
+
66
+ return re.sub(rb"%([0-9a-fA-F]{2})", repl, answer)
67
+
68
+ def __del__(self):
69
+ self.sock.close()
70
+
71
+ def sendreq(self, mailid, secretid, recipient, *others):
72
+ self.sock.send(
73
+ smart_bytes(
74
+ f"""request=release
75
+ mail_id={smart_str(mailid)}
76
+ secret_id={smart_str(secretid)}
77
+ quar_type=Q
78
+ recipient={smart_str(recipient)}
79
+
80
+ """
81
+ )
82
+ )
83
+ answer = self.sock.recv(1024)
84
+ answer = self.decode(answer)
85
+ if re.search(rb"250 [\d\.]+ Ok", answer):
86
+ return True
87
+ return False
88
+
89
+
90
+ class SpamassassinClient:
91
+ """A stupid spamassassin client."""
92
+
93
+ def __init__(self, user, recipient_db):
94
+ """Constructor."""
95
+ conf = dict(param_tools.get_global_parameters("amavis"))
96
+ self._sa_is_local = conf["sa_is_local"]
97
+ self._default_username = conf["default_user"]
98
+ self._recipient_db = recipient_db
99
+ self._setup_cache = {}
100
+ self._username_cache = []
101
+ if user.role == "SimpleUsers":
102
+ if conf["user_level_learning"]:
103
+ self._username = user.email
104
+ else:
105
+ self._username = None
106
+ self.error = None
107
+ if self._sa_is_local:
108
+ self._learn_cmd = self._find_binary("sa-learn")
109
+ self._learn_cmd += " --{0} --no-sync -u {1}"
110
+ self._learn_cmd_kwargs = {}
111
+ self._expected_exit_codes = [0]
112
+ self._sync_cmd = self._find_binary("sa-learn")
113
+ self._sync_cmd += " -u {0} --sync"
114
+ else:
115
+ self._learn_cmd = self._find_binary("spamc")
116
+ self._learn_cmd += " -d {} -p {}".format(
117
+ conf["spamd_address"], conf["spamd_port"]
118
+ )
119
+ self._learn_cmd += " -L {0} -u {1}"
120
+ self._learn_cmd_kwargs = {}
121
+ self._expected_exit_codes = [5, 6]
122
+
123
+ def _find_binary(self, name):
124
+ """Find path to binary."""
125
+ code, output = exec_cmd(f"which {name}")
126
+ if not code:
127
+ return smart_str(output).strip()
128
+ known_paths = getattr(settings, "SA_LOOKUP_PATH", ("/usr/bin",))
129
+ for path in known_paths:
130
+ bpath = os.path.join(path, name)
131
+ if os.path.isfile(bpath) and os.access(bpath, os.X_OK):
132
+ return bpath
133
+ raise InternalError(_("Failed to find {} binary").format(name))
134
+
135
+ def _get_mailbox_from_rcpt(self, rcpt):
136
+ """Retrieve a mailbox from a recipient address."""
137
+ local_part, domname, extension = split_mailbox(rcpt, return_extension=True)
138
+ try:
139
+ mailbox = admin_models.Mailbox.objects.select_related("domain").get(
140
+ address=local_part, domain__name=domname
141
+ )
142
+ except admin_models.Mailbox.DoesNotExist:
143
+ alias = admin_models.Alias.objects.filter(
144
+ address=f"{local_part}@{domname}",
145
+ aliasrecipient__r_mailbox__isnull=False,
146
+ ).first()
147
+ if not alias:
148
+ raise InternalError(_("No recipient found")) from None
149
+ if alias.type != "alias":
150
+ return None
151
+ mailbox = alias.aliasrecipient_set.filter(r_mailbox__isnull=False).first()
152
+ return mailbox
153
+
154
+ def _get_domain_from_rcpt(self, rcpt):
155
+ """Retrieve a domain from a recipient address."""
156
+ local_part, domname = split_mailbox(rcpt)
157
+ domain = admin_models.Domain.objects.filter(name=domname).first()
158
+ if not domain:
159
+ raise InternalError(_("Local domain not found"))
160
+ return domain
161
+
162
+ def _learn(self, rcpt, msg, mtype):
163
+ """Internal method to call the learning command."""
164
+ if self._username is None:
165
+ if self._recipient_db == "global":
166
+ username = self._default_username
167
+ elif self._recipient_db == "domain":
168
+ domain = self._get_domain_from_rcpt(rcpt)
169
+ username = domain.name
170
+ condition = (
171
+ username not in self._setup_cache
172
+ and setup_manual_learning_for_domain(domain)
173
+ )
174
+ if condition:
175
+ self._setup_cache[username] = True
176
+ else:
177
+ mbox = self._get_mailbox_from_rcpt(rcpt)
178
+ if mbox is None:
179
+ username = self._default_username
180
+ else:
181
+ if isinstance(mbox, admin_models.Mailbox):
182
+ username = mbox.full_address
183
+ elif isinstance(mbox, admin_models.AliasRecipient):
184
+ username = mbox.address
185
+ else:
186
+ username = None
187
+ condition = (
188
+ username is not None
189
+ and username not in self._setup_cache
190
+ and setup_manual_learning_for_mbox(mbox)
191
+ )
192
+ if condition:
193
+ self._setup_cache[username] = True
194
+ else:
195
+ username = self._username
196
+ if username not in self._setup_cache:
197
+ mbox = self._get_mailbox_from_rcpt(username)
198
+ if mbox and setup_manual_learning_for_mbox(mbox):
199
+ self._setup_cache[username] = True
200
+ if username not in self._username_cache:
201
+ self._username_cache.append(username)
202
+ cmd = self._learn_cmd.format(mtype, username)
203
+ code, output = exec_cmd(cmd, pinput=smart_bytes(msg), **self._learn_cmd_kwargs)
204
+ if code in self._expected_exit_codes:
205
+ return True
206
+ self.error = smart_str(output)
207
+ return False
208
+
209
+ def learn_spam(self, rcpt, msg):
210
+ """Learn new spam."""
211
+ return self._learn(rcpt, msg, "spam")
212
+
213
+ def learn_ham(self, rcpt, msg):
214
+ """Learn new ham."""
215
+ return self._learn(rcpt, msg, "ham")
216
+
217
+ def done(self):
218
+ """Call this method at the end of the processing."""
219
+ if self._sa_is_local:
220
+ for username in self._username_cache:
221
+ cmd = self._sync_cmd.format(username)
222
+ exec_cmd(cmd, **self._learn_cmd_kwargs)
223
+
224
+
225
+ def create_user_and_policy(name, priority=7):
226
+ """Create records.
227
+
228
+ Create two records (a user and a policy) using :keyword:`name` as
229
+ an identifier.
230
+
231
+ :param str name: name
232
+ :return: the new ``Policy`` object
233
+ """
234
+ policy, _ = Policy.objects.get_or_create(policy_name=name[:32])
235
+ if not Users.objects.filter(email=name).exists():
236
+ Users.objects.create(
237
+ email=name, fullname=name, priority=priority, policy=policy
238
+ )
239
+ return policy
240
+
241
+
242
+ def create_user_and_use_policy(name, policy, priority=7):
243
+ """Create a *users* record and use an existing policy.
244
+
245
+ :param str name: user record name
246
+ :param str policy: string or Policy instance
247
+ """
248
+ if isinstance(policy, str):
249
+ policy = Policy.objects.get(policy_name=policy[:32])
250
+ Users.objects.get_or_create(
251
+ email=name, fullname=name, priority=priority, policy=policy
252
+ )
253
+
254
+
255
+ def update_user_and_policy(oldname, newname):
256
+ """Update records.
257
+
258
+ :param str oldname: old name
259
+ :param str newname: new name
260
+ """
261
+ if oldname == newname:
262
+ return
263
+ u = Users.objects.get(email=oldname)
264
+ u.email = newname
265
+ u.fullname = newname
266
+ u.policy.policy_name = newname[:32]
267
+ u.policy.save(update_fields=["policy_name"])
268
+ u.save()
269
+
270
+
271
+ def delete_user_and_policy(name):
272
+ """Delete records.
273
+
274
+ :param str name: identifier
275
+ """
276
+ try:
277
+ u = Users.objects.get(email=name)
278
+ except Users.DoesNotExist:
279
+ return
280
+ u.policy.delete()
281
+ u.delete()
282
+
283
+
284
+ def delete_user(name):
285
+ """Delete a *users* record.
286
+
287
+ :param str name: user record name
288
+ """
289
+ try:
290
+ Users.objects.get(email=name).delete()
291
+ except Users.DoesNotExist:
292
+ pass
293
+
294
+
295
+ def manual_learning_enabled(user):
296
+ """Check if manual learning is enabled or not.
297
+
298
+ Also check for :kw:`user` if necessary.
299
+
300
+ :return: True if learning is enabled, False otherwise.
301
+ """
302
+ conf = dict(param_tools.get_global_parameters("amavis"))
303
+ if not conf["manual_learning"]:
304
+ return False
305
+ if user.role != "SuperAdmins":
306
+ if user.has_perm("admin.view_domain"):
307
+ manual_learning = (
308
+ conf["domain_level_learning"] or conf["user_level_learning"]
309
+ )
310
+ else:
311
+ manual_learning = conf["user_level_learning"]
312
+ return manual_learning
313
+ return True
314
+
315
+
316
+ def setup_manual_learning_for_domain(domain):
317
+ """Setup manual learning if necessary.
318
+
319
+ :return: True if learning has been setup, False otherwise
320
+ """
321
+ if Policy.objects.filter(sa_username=domain.name).exists():
322
+ return False
323
+ policy = Policy.objects.get(policy_name=f"@{domain.name[:32]}")
324
+ policy.sa_username = domain.name
325
+ policy.save()
326
+ return True
327
+
328
+
329
+ def setup_manual_learning_for_mbox(mbox):
330
+ """Setup manual learning if necessary.
331
+
332
+ :return: True if learning has been setup, False otherwise
333
+ """
334
+ result = False
335
+ if isinstance(mbox, admin_models.AliasRecipient) and mbox.r_mailbox is not None:
336
+ mbox = mbox.r_mailbox
337
+ if isinstance(mbox, admin_models.Mailbox):
338
+ pname = mbox.full_address[:32]
339
+ if not Policy.objects.filter(policy_name=pname).exists():
340
+ policy = create_user_and_policy(pname)
341
+ policy.sa_username = mbox.full_address
342
+ policy.save()
343
+ for alias in mbox.alias_addresses:
344
+ create_user_and_use_policy(alias, policy)
345
+ result = True
346
+ return result
347
+
348
+
349
+ def make_query_args(address, exact_extension=True, wildcard=None, domain_search=False):
350
+ assert isinstance(address, str), "address should be of type str"
351
+ conf = dict(param_tools.get_global_parameters("amavis"))
352
+ local_part, domain = split_address(address)
353
+ if not conf["localpart_is_case_sensitive"]:
354
+ local_part = local_part.lower()
355
+ if domain:
356
+ domain = domain.lstrip("@").rstrip(".")
357
+ domain = domain.lower()
358
+ orig_domain = domain
359
+ domain = idna.encode(domain, uts46=True).decode("ascii")
360
+ delimiter = conf["recipient_delimiter"]
361
+ local_part, extension = split_local_part(local_part, delimiter=delimiter)
362
+ query_args = []
363
+ if conf["localpart_is_case_sensitive"] or (domain and domain != orig_domain):
364
+ query_args.append(address)
365
+ if extension:
366
+ query_args.append(f"{local_part}{delimiter}{extension}@{domain}")
367
+ if delimiter and not exact_extension and wildcard:
368
+ query_args.append(f"{local_part}{delimiter}{wildcard}@{domain}")
369
+ query_args.append(f"{local_part}@{domain}")
370
+ if domain_search:
371
+ query_args.append(f"@{domain}")
372
+ query_args.append("@.")
373
+
374
+ return query_args
375
+
376
+
377
+ def cleanup_email_address(address):
378
+ address = parseaddr(address)
379
+ if address[0]:
380
+ return f"{address[0]} <{address[1]}>"
381
+ return address[1]
File without changes
File without changes
@@ -0,0 +1,99 @@
1
+ from django.contrib.sites import models as sites_models
2
+ from django.core import mail
3
+ from django.core.management.base import BaseCommand
4
+ from django.template.loader import render_to_string
5
+ from django.utils.translation import gettext as _
6
+
7
+ from modoboa.admin.models import Domain
8
+ from modoboa.core.models import User
9
+ from modoboa.parameters import tools as param_tools
10
+ from ...models import Msgrcpt
11
+ from ...sql_connector import SQLconnector
12
+
13
+
14
+ class Command(BaseCommand):
15
+ help = "Amavis notification tool" # NOQA:A003
16
+
17
+ sender = None
18
+ baseurl = None
19
+ listingurl = None
20
+
21
+ def add_arguments(self, parser):
22
+ """Add extra arguments to command line."""
23
+ parser.add_argument(
24
+ "--smtp_host",
25
+ type=str,
26
+ default="localhost",
27
+ help="The address of the SMTP server used to send notifications",
28
+ )
29
+ parser.add_argument(
30
+ "--smtp_port",
31
+ type=int,
32
+ default=25,
33
+ help=(
34
+ "The listening port of the SMTP server used to send " "notifications"
35
+ ),
36
+ )
37
+ parser.add_argument(
38
+ "--verbose", action="store_true", help="Activate verbose mode"
39
+ )
40
+
41
+ def handle(self, *args, **options):
42
+ self.options = options
43
+ self.notify_admins_pending_requests()
44
+
45
+ def _build_message(self, rcpt, total, reqs):
46
+ """Build new EmailMessage instance."""
47
+ if self.options["verbose"]:
48
+ print(f"Sending notification to {rcpt}")
49
+ context = {
50
+ "total": total,
51
+ "requests": reqs,
52
+ "baseurl": self.baseurl,
53
+ "listingurl": self.listingurl,
54
+ }
55
+ content = render_to_string(
56
+ "amavis/notifications/pending_requests.html", context
57
+ )
58
+ msg = mail.EmailMessage(
59
+ _("[modoboa] Pending release requests"), content, self.sender, [rcpt]
60
+ )
61
+ return msg
62
+
63
+ def notify_admins_pending_requests(self):
64
+ self.sender = param_tools.get_global_parameter(
65
+ "notifications_sender", app="amavis"
66
+ )
67
+ self.baseurl = f"https://{sites_models.Site.objects.get_current().domain}"
68
+ self.listingurl = f"{self.baseurl}/user/quarantine?requests=1"
69
+ messages = []
70
+ # Check domain administators first.
71
+ for da in User.objects.filter(groups__name="DomainAdmins"):
72
+ if not hasattr(da, "mailbox"):
73
+ continue
74
+ rcpt = da.mailbox.full_address
75
+ reqs = SQLconnector().get_domains_pending_requests(
76
+ Domain.objects.get_for_admin(da).values_list("name", flat=True)
77
+ )
78
+ total = reqs.count()
79
+ reqs = reqs.all()[:10]
80
+ if reqs.count():
81
+ messages.append(self._build_message(rcpt, total, reqs))
82
+
83
+ # Then super administators.
84
+ reqs = Msgrcpt.objects.filter(rs="p")
85
+ total = reqs.count()
86
+ if total:
87
+ reqs = reqs.all()[:10]
88
+ for su in User.objects.filter(is_superuser=True):
89
+ if not hasattr(su, "mailbox"):
90
+ continue
91
+ rcpt = su.mailbox.full_address
92
+ messages.append(self._build_message(rcpt, total, reqs))
93
+
94
+ # Finally, send emails.
95
+ if not len(messages):
96
+ return
97
+ kwargs = {"host": self.options["smtp_host"], "port": self.options["smtp_port"]}
98
+ with mail.get_connection(**kwargs) as connection:
99
+ connection.send_messages(messages)