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,164 @@
1
+ """Amavis factories."""
2
+
3
+ import datetime
4
+ import time
5
+
6
+ import factory
7
+
8
+ from . import models
9
+ from .utils import smart_bytes
10
+
11
+ SPAM_BODY = """X-Envelope-To: <{rcpt}>
12
+ X-Envelope-To-Blocked: <{rcpt}>
13
+ X-Quarantine-ID: <nq6ekd4wtXZg>
14
+ X-Spam-Flag: YES
15
+ X-Spam-Score: 1000.985
16
+ X-Spam-Level: ****************************************************************
17
+ X-Spam-Status: Yes, score=1000.985 tag=2 tag2=6.31 kill=6.31
18
+ tests=[ALL_TRUSTED=-1, GTUBE=1000, PYZOR_CHECK=1.985]
19
+ autolearn=no autolearn_force=no
20
+ Received: from demo.modoboa.org ([127.0.0.1])
21
+ by localhost (demo.modoboa.org [127.0.0.1]) (amavisd-new, port 10024)
22
+ with ESMTP id nq6ekd4wtXZg for <user@demo.local>;
23
+ Thu, 9 Nov 2017 15:59:52 +0100 (CET)
24
+ Received: from demo.modoboa.org (localhost [127.0.0.1])
25
+ by demo.modoboa.org (Postfix) with ESMTP
26
+ for <user@demo.local>; Thu, 9 Nov 2017 15:59:52 +0100 (CET)
27
+ Content-Type: text/plain; charset="utf-8"
28
+ MIME-Version: 1.0
29
+ Content-Transfer-Encoding: quoted-printable
30
+ Subject: Sample message
31
+ From: {sender}
32
+ To: {rcpt}
33
+ Message-ID: <151023959268.5550.5713670714483771838@demo.modoboa.org>
34
+ Date: Thu, 09 Nov 2017 15:59:52 +0100
35
+
36
+ This is the GTUBE, the
37
+ Generic
38
+ Test for
39
+ Unsolicited
40
+ Bulk
41
+ Email
42
+
43
+ If your spam filter supports it, the GTUBE provides a test by which you
44
+ can verify that the filter is installed correctly and is detecting incoming
45
+ spam. You can send yourself a test mail containing the following string of
46
+ characters (in upper case and with no white spaces and line breaks):
47
+
48
+ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
49
+
50
+ You should send this test mail from an account outside of your network.
51
+ """
52
+
53
+ VIRUS_BODY = r"""Subject: Virus Test Message (EICAR)
54
+ MIME-Version: 1.0
55
+ Content-Type: multipart/mixed; boundary="huq684BweRXVnRxX"
56
+ Content-Disposition: inline
57
+ Date: Sun, 06 Nov 2011 10:08:18 -0800
58
+
59
+
60
+ --huq684BweRXVnRxX
61
+ Content-Type: text/plain; charset=us-ascii
62
+ Content-Disposition: inline
63
+
64
+
65
+ This is a virus test message. It contains an attached file 'eicar.com',
66
+ which contains the EICAR virus <http://eicar.org/86-0-Intended-use.html>
67
+ test pattern.
68
+
69
+
70
+ --huq684BweRXVnRxX
71
+ Content-Type: application/x-msdos-program
72
+ Content-Disposition: attachment; filename="eicar.com"
73
+ Content-Transfer-Encoding: quoted-printable
74
+
75
+ X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*=0A
76
+ --huq684BweRXVnRxX--
77
+ """
78
+
79
+
80
+ class MaddrFactory(factory.django.DjangoModelFactory):
81
+ """Factory for Maddr."""
82
+
83
+ class Meta:
84
+ model = models.Maddr
85
+ django_get_or_create = ("email",)
86
+
87
+ id = factory.Sequence(lambda n: n) # NOQA:A003
88
+ email = factory.Sequence(lambda n: f"user_{n}@domain.test")
89
+ domain = "test.domain"
90
+
91
+
92
+ class MsgsFactory(factory.django.DjangoModelFactory):
93
+ """Factory for Mailaddr."""
94
+
95
+ class Meta:
96
+ model = models.Msgs
97
+
98
+ mail_id = factory.Sequence(lambda n: f"mailid{n}".encode("ascii"))
99
+ secret_id = factory.Sequence(lambda n: smart_bytes(f"id{n}"))
100
+ sid = factory.SubFactory(MaddrFactory)
101
+ client_addr = "127.0.0.1"
102
+ originating = "Y"
103
+ dsn_sent = "N"
104
+ subject = factory.Sequence(lambda n: f"Test message {n}")
105
+ time_num = factory.LazyAttribute(lambda o: int(time.time()))
106
+ time_iso = factory.LazyAttribute(
107
+ lambda o: datetime.datetime.fromtimestamp(o.time_num).isoformat()
108
+ )
109
+ size = 100
110
+
111
+
112
+ class MsgrcptFactory(factory.django.DjangoModelFactory):
113
+ """Factory for Msgrcpt."""
114
+
115
+ class Meta:
116
+ model = models.Msgrcpt
117
+
118
+ rseqnum = 1
119
+ is_local = "Y"
120
+ bl = "N"
121
+ wl = "N"
122
+ mail = factory.SubFactory(MsgsFactory)
123
+ rid = factory.SubFactory(MaddrFactory)
124
+
125
+
126
+ class QuarantineFactory(factory.django.DjangoModelFactory):
127
+ """Factory for Quarantine."""
128
+
129
+ class Meta:
130
+ model = models.Quarantine
131
+
132
+ chunk_ind = 1
133
+ mail = factory.SubFactory(MsgsFactory)
134
+
135
+
136
+ def create_quarantined_msg(rcpt, sender, rs, body, **kwargs):
137
+ """Create a quarantined msg."""
138
+ msgrcpt = MsgrcptFactory(
139
+ rs=rs,
140
+ rid__email=rcpt,
141
+ rid__domain="com.test", # FIXME
142
+ mail__sid__email=smart_bytes(sender),
143
+ mail__sid__domain="", # FIXME
144
+ **kwargs,
145
+ )
146
+ QuarantineFactory(
147
+ mail=msgrcpt.mail,
148
+ mail_text=smart_bytes(SPAM_BODY.format(rcpt=rcpt, sender=sender)),
149
+ )
150
+ return msgrcpt
151
+
152
+
153
+ def create_spam(rcpt, sender="spam@evil.corp", rs=" "):
154
+ """Create a spam."""
155
+ body = SPAM_BODY.format(rcpt=rcpt, sender=sender)
156
+ body += "fóó bár"
157
+ return create_quarantined_msg(
158
+ rcpt, sender, rs, body, bspam_level=999.0, content="S"
159
+ )
160
+
161
+
162
+ def create_virus(rcpt, sender="virus@evil.corp", rs=" "):
163
+ """Create a virus."""
164
+ return create_quarantined_msg(rcpt, sender, rs, VIRUS_BODY, content="V")
@@ -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