modoboa 2.6.5__py3-none-any.whl → 2.7.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 (359) hide show
  1. modoboa/admin/api/v2/serializers.py +1 -0
  2. modoboa/admin/app_settings.py +12 -0
  3. modoboa/admin/{management/commands/subcommands/_mx.py → dns_checker.py} +41 -111
  4. modoboa/admin/jobs.py +86 -0
  5. modoboa/admin/management/commands/modo.py +0 -2
  6. modoboa/admin/tests/test_mailbox_operations.py +4 -4
  7. modoboa/admin/tests/test_mx.py +68 -56
  8. modoboa/amavis/jobs.py +11 -0
  9. modoboa/amavis/tests/test_jobs.py +18 -0
  10. modoboa/amavis/tests/test_viewsets.py +2 -3
  11. modoboa/autoconfig/templates/autoconfig/autoconfig.xml +2 -2
  12. modoboa/autoconfig/templates/autoconfig/autodiscover.xml +14 -0
  13. modoboa/autoconfig/tests.py +2 -1
  14. modoboa/autoconfig/views.py +11 -3
  15. modoboa/calendars/backends/caldav_.py +17 -13
  16. modoboa/calendars/jobs.py +7 -0
  17. modoboa/calendars/mocks.py +4 -1
  18. modoboa/calendars/serializers.py +14 -9
  19. modoboa/calendars/tests.py +10 -9
  20. modoboa/calendars/viewsets.py +3 -1
  21. modoboa/contacts/migrations/0008_addressbook_syncing.py +18 -0
  22. modoboa/contacts/models.py +1 -0
  23. modoboa/contacts/serializers.py +5 -2
  24. modoboa/contacts/tasks.py +9 -3
  25. modoboa/contacts/tests.py +32 -6
  26. modoboa/contacts/viewsets.py +7 -1
  27. modoboa/core/api/v2/serializers.py +0 -7
  28. modoboa/core/api/v2/tests.py +0 -10
  29. modoboa/core/app_settings.py +0 -22
  30. modoboa/core/commands/deploy.py +13 -0
  31. modoboa/core/commands/templates/cron_config.py.tpl +33 -0
  32. modoboa/core/commands/templates/settings.py.tpl +21 -0
  33. modoboa/core/jobs.py +34 -0
  34. modoboa/core/management/commands/load_initial_data.py +1 -1
  35. modoboa/core/password_hashers/base.py +4 -1
  36. modoboa/core/tests/test_core.py +0 -14
  37. modoboa/core/tests/test_jobs.py +40 -0
  38. modoboa/frontend_dist/assets/{AccountAliasForm-B-hTKSFr.js → AccountAliasForm-DO6DwfjE.js} +1 -1
  39. modoboa/frontend_dist/assets/{AccountEditView-LkP_qhf_.js → AccountEditView-CCuN9mGB.js} +1 -1
  40. modoboa/frontend_dist/assets/AccountLayout-Ge7fzuZg.js +1 -0
  41. modoboa/frontend_dist/assets/AccountPasswordSubForm-PkFPblkR.js +1 -0
  42. modoboa/frontend_dist/assets/AccountView-dgseekZ8.js +1 -0
  43. modoboa/frontend_dist/assets/AddressBook-DZWOHOJj.js +1 -0
  44. modoboa/frontend_dist/assets/AdminLayout-itB_mmH_.js +1 -0
  45. modoboa/frontend_dist/assets/AlarmsView-King6zb6.js +1 -0
  46. modoboa/frontend_dist/assets/{AliasEditView-BVjNJekt.js → AliasEditView-CTjrXYPf.js} +1 -1
  47. modoboa/frontend_dist/assets/AliasRecipientForm-CI7bXqVp.js +1 -0
  48. modoboa/frontend_dist/assets/{AliasView-DvvI6cV9.js → AliasView-BN13MNN_.js} +1 -1
  49. modoboa/frontend_dist/assets/AuditTrailView-C1jQwgoo.js +1 -0
  50. modoboa/frontend_dist/assets/CalendarView-juHVIHU5.css +1 -0
  51. modoboa/frontend_dist/assets/CalendarView-rRSzqxrH.js +1 -0
  52. modoboa/frontend_dist/assets/ChoiceField-DCr12shR.js +1 -0
  53. modoboa/frontend_dist/assets/ComposeEmailForm-BmtKwFb1.js +2 -0
  54. modoboa/frontend_dist/assets/ComposeEmailForm-CVNDl-Mq.css +1 -0
  55. modoboa/frontend_dist/assets/ComposeEmailView-JW22Phrb.js +1 -0
  56. modoboa/frontend_dist/assets/ConfirmDialog-CWpdwSQ6.js +1 -0
  57. modoboa/frontend_dist/assets/ConnectedLayout-BP8pO27H.js +1 -0
  58. modoboa/frontend_dist/assets/{ConnectedLayout-C6HNXWkp.css → ConnectedLayout-Ddpb_6yT.css} +1 -1
  59. modoboa/frontend_dist/assets/CreationForm-CzelJsVQ.js +1 -0
  60. modoboa/frontend_dist/assets/DashboardView-BBkBodVj.js +1 -0
  61. modoboa/frontend_dist/assets/{DomainAdminList-BO8_4Cwt.js → DomainAdminList-DD7p3i6F.js} +1 -1
  62. modoboa/frontend_dist/assets/{DomainEditView-BXylM4aB.js → DomainEditView-DoIoNYC4.js} +1 -1
  63. modoboa/frontend_dist/assets/{DomainTransportForm-Ccw5BXyM.js → DomainTransportForm-CbiJF9z5.js} +1 -1
  64. modoboa/frontend_dist/assets/{DomainView-Bd5XQI-1.js → DomainView-BXvznFYz.js} +3 -3
  65. modoboa/frontend_dist/assets/DomainsView-ffYjiffp.js +1 -0
  66. modoboa/frontend_dist/assets/EmailField-KhhJYA4D.js +1 -0
  67. modoboa/frontend_dist/assets/EmailSchedulingForm-CQL5Vfdr.js +1 -0
  68. modoboa/frontend_dist/assets/EmailView-Bq2bHZBO.js +1 -0
  69. modoboa/frontend_dist/assets/EmptyLayout-NrTftp18.js +1 -0
  70. modoboa/frontend_dist/assets/FiltersView-SOQy0U_3.js +1 -0
  71. modoboa/frontend_dist/assets/ForwardEmailView-kEPDjWUw.js +1 -0
  72. modoboa/frontend_dist/assets/{HtmlEditor-DNn3CY-w.js → HtmlEditor-CyBl5wj2.js} +15 -15
  73. modoboa/frontend_dist/assets/IdentitiesView-CZFf4oR9.js +1 -0
  74. modoboa/frontend_dist/assets/InformationView-CAtneAqM.js +1 -0
  75. modoboa/frontend_dist/assets/{LoadingData-CnQ_aVqa.js → LoadingData-C2txD49L.js} +1 -1
  76. modoboa/frontend_dist/assets/{LoginCallbackView-Nh_gmh8e.js → LoginCallbackView-DAvty3CI.js} +1 -1
  77. modoboa/frontend_dist/assets/{LoginView-BtC9BHuJ.js → LoginView-DHYITkn4.js} +1 -1
  78. modoboa/frontend_dist/assets/MailboxView-BNg2v7mi.css +1 -0
  79. modoboa/frontend_dist/assets/MailboxView-DHg5A78D.js +5 -0
  80. modoboa/frontend_dist/assets/MenuItems-C9p70bSC.js +1 -0
  81. modoboa/frontend_dist/assets/{MessageView-C1Goz-hY.js → MessageView-CrrEJpNI.js} +1 -1
  82. modoboa/frontend_dist/assets/MessagesView-CyecRd4I.js +1 -0
  83. modoboa/frontend_dist/assets/MigrationsView-DXGKKz2h.js +1 -0
  84. modoboa/frontend_dist/assets/{ParametersForm-BegEnkQM.js → ParametersForm-DFKYkPAs.js} +1 -1
  85. modoboa/frontend_dist/assets/ParametersView-B1wT5oyt.js +1 -0
  86. modoboa/frontend_dist/assets/ParametersView-CfvVzKea.js +1 -0
  87. modoboa/frontend_dist/assets/ProviderEditView-De-8ggBp.js +1 -0
  88. modoboa/frontend_dist/assets/{ProviderGeneralForm-BAELnXh6.js → ProviderGeneralForm-DO-IpJMQ.js} +1 -1
  89. modoboa/frontend_dist/assets/ProvidersView-Bhk7sCz4.js +1 -0
  90. modoboa/frontend_dist/assets/QuarantineLayout-C1YszNmP.js +1 -0
  91. modoboa/frontend_dist/assets/QuarantineView-cElD_rS-.js +1 -0
  92. modoboa/frontend_dist/assets/ReplyEmailView-BvuOZdLl.js +1 -0
  93. modoboa/frontend_dist/assets/ResourcesForm-FWxrmVwo.js +1 -0
  94. modoboa/frontend_dist/assets/SelfServiceLayout-JHRvfvQf.js +1 -0
  95. modoboa/frontend_dist/assets/SettingsView-SIsKAXtQ.css +1 -0
  96. modoboa/frontend_dist/assets/SettingsView-s2l2Xl1L.js +6 -0
  97. modoboa/frontend_dist/assets/StatisticsView-FiidWvad.js +1 -0
  98. modoboa/frontend_dist/assets/TimeSerieChart-CdqUHiO_.js +1 -0
  99. modoboa/frontend_dist/assets/TimeSerieChart-nLIFGI0y.css +1 -0
  100. modoboa/frontend_dist/assets/UserLayout-DyvI8duf.js +1 -0
  101. modoboa/frontend_dist/assets/{VAlert-Buv8Z5G4.js → VAlert-MDbeolOo.js} +1 -1
  102. modoboa/frontend_dist/assets/VApp-DM1KLQfQ.js +1 -0
  103. modoboa/frontend_dist/assets/VAutocomplete-C9NL5_uo.css +1 -0
  104. modoboa/frontend_dist/assets/VAutocomplete-CQsWYWNX.js +1 -0
  105. modoboa/frontend_dist/assets/VAvatar-CXV_FqpP.js +1 -0
  106. modoboa/frontend_dist/assets/VBadge-7tDx7aI3.js +1 -0
  107. modoboa/frontend_dist/assets/{VCard-D7AwBs4w.js → VCard-CBtX8JF-.js} +1 -1
  108. modoboa/frontend_dist/assets/VCheckbox-zY1MApOy.js +1 -0
  109. modoboa/frontend_dist/assets/{VCheckboxBtn-B5evxN9K.js → VCheckboxBtn-B0mIT3E0.js} +1 -1
  110. modoboa/frontend_dist/assets/VColorPicker-BbCHvk6K.js +1 -0
  111. modoboa/frontend_dist/assets/{VColorPicker-B_lVDaYR.css → VColorPicker-C9m8L-6U.css} +1 -1
  112. modoboa/frontend_dist/assets/{VContainer-DY9021j4.js → VContainer-Cn-vKB3s.js} +1 -1
  113. modoboa/frontend_dist/assets/VDataTable-CNT9KOSp.js +1 -0
  114. modoboa/frontend_dist/assets/VDataTableServer-_yn4Ry6U.js +1 -0
  115. modoboa/frontend_dist/assets/VDataTableVirtual-Csxh3Gp6.js +1 -0
  116. modoboa/frontend_dist/assets/VDatePicker-_OUDqShN.css +1 -0
  117. modoboa/frontend_dist/assets/VDatePicker-iFu13xIP.js +2 -0
  118. modoboa/frontend_dist/assets/VDialog-lKtBdqnp.js +1 -0
  119. modoboa/frontend_dist/assets/VExpansionPanels-CaIvk9iF.js +1 -0
  120. modoboa/frontend_dist/assets/VFileInput-C7J_qVmk.js +1 -0
  121. modoboa/frontend_dist/assets/VForm-fdQ5d-CH.js +1 -0
  122. modoboa/frontend_dist/assets/VInput-BwHvhzAe.js +1 -0
  123. modoboa/frontend_dist/assets/VMenu-BpmJf4X2.js +1 -0
  124. modoboa/frontend_dist/assets/VMenu-C5A_5Hs5.css +1 -0
  125. modoboa/frontend_dist/assets/VPicker-B7cB3kJg.css +1 -0
  126. modoboa/frontend_dist/assets/VPicker-BZho70wU.js +1 -0
  127. modoboa/frontend_dist/assets/VProgressCircular-DRw_-iNj.js +1 -0
  128. modoboa/frontend_dist/assets/VRadioGroup--eiP5xtJ.js +1 -0
  129. modoboa/frontend_dist/assets/{VRow-DyK_Mj7R.js → VRow-83Qnr5iB.js} +1 -1
  130. modoboa/frontend_dist/assets/VSelect-BcmGFGif.js +1 -0
  131. modoboa/frontend_dist/assets/{VSelectionControl-Dkms9_P5.js → VSelectionControl-Cqi1xt-q.js} +1 -1
  132. modoboa/frontend_dist/assets/VSheet-C3MaHhtw.js +1 -0
  133. modoboa/frontend_dist/assets/VSpacer-CuSkdJZL.js +1 -0
  134. modoboa/frontend_dist/assets/VSwitch-XumUl685.js +1 -0
  135. modoboa/frontend_dist/assets/{VTable-D8ZwwC_B.js → VTable-f7wcr2AZ.js} +1 -1
  136. modoboa/frontend_dist/assets/VTabs-CDfdejXj.css +1 -0
  137. modoboa/frontend_dist/assets/VTabs-y4wNP4im.js +1 -0
  138. modoboa/frontend_dist/assets/VTextField-BPIvtrn4.js +1 -0
  139. modoboa/frontend_dist/assets/VTextField-DjbYGlzs.css +1 -0
  140. modoboa/frontend_dist/assets/VTextarea-B6bGYcC3.js +1 -0
  141. modoboa/frontend_dist/assets/VTextarea-f6vTjzFy.css +1 -0
  142. modoboa/frontend_dist/assets/VToolbar-BYDPtwf0.css +1 -0
  143. modoboa/frontend_dist/assets/VToolbar-CB6wwtYc.js +1 -0
  144. modoboa/frontend_dist/assets/VWindowItem-CDtLLEkg.js +1 -0
  145. modoboa/frontend_dist/assets/WebmailLayout-CQQAolnl.css +1 -0
  146. modoboa/frontend_dist/assets/WebmailLayout-tosQSHLS.js +1 -0
  147. modoboa/frontend_dist/assets/{accounts-BZBqbAmd.js → accounts-KUsk6LHW.js} +1 -1
  148. modoboa/frontend_dist/assets/{admin-uCrKjpln.js → admin-BW9cZW0P.js} +1 -1
  149. modoboa/frontend_dist/assets/{aliases-CFwFkX45.js → aliases-Ge0hjIsH.js} +1 -1
  150. modoboa/frontend_dist/assets/{amavis-DCy0U0TD.js → amavis-BbFeFfsk.js} +1 -1
  151. modoboa/frontend_dist/assets/{amavis-B4ScfQsG.js → amavis-DtuzP_CS.js} +1 -1
  152. modoboa/frontend_dist/assets/{contacts-DKmh2q-4.js → contacts-DMJlQTe0.js} +1 -1
  153. modoboa/frontend_dist/assets/{domains-CDE4_LDF.js → domains-Du64lcXT.js} +1 -1
  154. modoboa/frontend_dist/assets/{domains.store-a9Y5UlDV.js → domains.store-1U61jeCV.js} +1 -1
  155. modoboa/frontend_dist/assets/events-BM3in65C.js +1 -0
  156. modoboa/frontend_dist/assets/filter-Dihm6o59.js +1 -0
  157. modoboa/frontend_dist/assets/importExport-HGcNGWOm.js +1 -0
  158. modoboa/frontend_dist/assets/{index-B9q1vO3K.css → index-B1EK3MQe.css} +1 -1
  159. modoboa/frontend_dist/assets/{index-Dwg6jPTX.js → index-Dv00bmw9.js} +1 -1
  160. modoboa/frontend_dist/assets/{index-BjibLozh.js → index-jui3edpn.js} +47 -41
  161. modoboa/frontend_dist/assets/{language.store-DGuI8jG0.js → language.store-OcfdXL_-.js} +1 -1
  162. modoboa/frontend_dist/assets/languages-CF8hxo7x.js +1 -0
  163. modoboa/frontend_dist/assets/{layout-5PvsmkGR.js → layout-DOO7TRTJ.js} +1 -1
  164. modoboa/frontend_dist/assets/{layout.store-DJVcXtxk.js → layout.store-C0g-piJn.js} +1 -1
  165. modoboa/frontend_dist/assets/{logos-CMmd0hOG.js → logos-Dz2Gzei-.js} +1 -1
  166. modoboa/frontend_dist/assets/{logs-D2GOydPZ.js → logs-CLm32Weu.js} +1 -1
  167. modoboa/frontend_dist/assets/{parameters-DJdlq9sh.js → parameters-DMIAQ7cd.js} +1 -1
  168. modoboa/frontend_dist/assets/{parameters.store-DH3pPlaT.js → parameters.store-DLnFzCwV.js} +1 -1
  169. modoboa/frontend_dist/assets/{permissions-CgFJFhCp.js → permissions-CrpE0b4w.js} +1 -1
  170. modoboa/frontend_dist/assets/{ssrBoot-ChTB-PYP.js → ssrBoot-B7cr7q9U.js} +1 -1
  171. modoboa/frontend_dist/assets/{tag-CvrHE14f.js → tag-WF93n81Q.js} +1 -1
  172. modoboa/frontend_dist/assets/{theme-DFzlhSh5.js → theme-_0oOYChG.js} +1 -1
  173. modoboa/frontend_dist/assets/transports-CS61syt-.js +1 -0
  174. modoboa/frontend_dist/assets/webmail-CYDXU0DS.js +1 -0
  175. modoboa/frontend_dist/assets/webmail.store-BvHCQSjM.js +1 -0
  176. modoboa/frontend_dist/index.html +2 -2
  177. modoboa/imap_migration/models.py +3 -3
  178. modoboa/imap_migration/templates/imap_migration/offlineimap.conf +2 -2
  179. modoboa/imap_migration/tests.py +21 -0
  180. modoboa/lib/cryptutils.py +2 -2
  181. modoboa/lib/sysutils.py +1 -1
  182. modoboa/lib/tests/__init__.py +0 -1
  183. modoboa/locale/br/LC_MESSAGES/django.mo +0 -0
  184. modoboa/locale/br/LC_MESSAGES/django.po +301 -273
  185. modoboa/locale/cs/LC_MESSAGES/django.po +286 -269
  186. modoboa/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  187. modoboa/locale/cs_CZ/LC_MESSAGES/django.po +303 -275
  188. modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
  189. modoboa/locale/de/LC_MESSAGES/django.po +297 -269
  190. modoboa/locale/de_DE/LC_MESSAGES/django.mo +0 -0
  191. modoboa/locale/de_DE/LC_MESSAGES/django.po +303 -275
  192. modoboa/locale/el_GR/LC_MESSAGES/django.mo +0 -0
  193. modoboa/locale/el_GR/LC_MESSAGES/django.po +303 -275
  194. modoboa/locale/en/LC_MESSAGES/django.po +286 -269
  195. modoboa/locale/es/LC_MESSAGES/django.mo +0 -0
  196. modoboa/locale/es/LC_MESSAGES/django.po +302 -274
  197. modoboa/locale/es_MX/LC_MESSAGES/django.po +286 -269
  198. modoboa/locale/fi/LC_MESSAGES/django.mo +0 -0
  199. modoboa/locale/fi/LC_MESSAGES/django.po +299 -271
  200. modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
  201. modoboa/locale/fr/LC_MESSAGES/django.po +288 -271
  202. modoboa/locale/hu/LC_MESSAGES/django.po +286 -269
  203. modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
  204. modoboa/locale/it/LC_MESSAGES/django.po +303 -275
  205. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  206. modoboa/locale/ja_JP/LC_MESSAGES/django.po +390 -413
  207. modoboa/locale/ka/LC_MESSAGES/django.po +286 -269
  208. modoboa/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  209. modoboa/locale/nl_NL/LC_MESSAGES/django.po +299 -271
  210. modoboa/locale/no/LC_MESSAGES/django.po +286 -269
  211. modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  212. modoboa/locale/pl_PL/LC_MESSAGES/django.po +297 -269
  213. modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  214. modoboa/locale/pt_BR/LC_MESSAGES/django.po +298 -270
  215. modoboa/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  216. modoboa/locale/pt_PT/LC_MESSAGES/django.po +303 -275
  217. modoboa/locale/ro_RO/LC_MESSAGES/django.mo +0 -0
  218. modoboa/locale/ro_RO/LC_MESSAGES/django.po +299 -271
  219. modoboa/locale/ru/LC_MESSAGES/django.mo +0 -0
  220. modoboa/locale/ru/LC_MESSAGES/django.po +993 -1096
  221. modoboa/locale/si/LC_MESSAGES/django.po +286 -269
  222. modoboa/locale/sk/LC_MESSAGES/django.po +286 -269
  223. modoboa/locale/sk_SK/LC_MESSAGES/django.po +286 -269
  224. modoboa/locale/sl_SI/LC_MESSAGES/django.po +288 -269
  225. modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
  226. modoboa/locale/sv/LC_MESSAGES/django.po +297 -269
  227. modoboa/locale/tr/LC_MESSAGES/django.mo +0 -0
  228. modoboa/locale/tr/LC_MESSAGES/django.po +301 -272
  229. modoboa/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
  230. modoboa/locale/tr_TR/LC_MESSAGES/django.po +291 -269
  231. modoboa/locale/uk/LC_MESSAGES/django.po +286 -269
  232. modoboa/locale/zh/LC_MESSAGES/django.po +286 -269
  233. modoboa/locale/zh_CN/LC_MESSAGES/django.po +286 -269
  234. modoboa/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  235. modoboa/locale/zh_TW/LC_MESSAGES/django.po +295 -273
  236. modoboa/maillog/jobs.py +11 -0
  237. modoboa/maillog/tests/test_views.py +5 -4
  238. modoboa/parameters/api/v2/tests.py +1 -1
  239. modoboa/policyd/management/commands/policy_daemon.py +5 -1
  240. modoboa/policyd/tests.py +4 -2
  241. modoboa/rspamd/tests.py +20 -0
  242. modoboa/templates/registration/twofactor_code_verify.html +1 -1
  243. modoboa/webmail/app_settings.py +37 -0
  244. modoboa/webmail/constants.py +21 -1
  245. modoboa/webmail/factories.py +19 -0
  246. modoboa/webmail/jobs.py +27 -0
  247. modoboa/webmail/lib/__init__.py +0 -2
  248. modoboa/webmail/lib/imapemail.py +9 -11
  249. modoboa/webmail/lib/imapheader.py +25 -13
  250. modoboa/webmail/lib/imaputils.py +51 -13
  251. modoboa/webmail/lib/sendmail.py +88 -105
  252. modoboa/webmail/lib/utils.py +109 -0
  253. modoboa/webmail/migrations/0001_initial.py +90 -0
  254. modoboa/webmail/migrations/__init__.py +0 -0
  255. modoboa/webmail/mocks.py +12 -3
  256. modoboa/webmail/models.py +102 -0
  257. modoboa/webmail/serializers.py +84 -4
  258. modoboa/webmail/tests/data.py +21 -0
  259. modoboa/webmail/tests/test_lib_imaputils.py +33 -0
  260. modoboa/webmail/tests/test_viewsets.py +108 -0
  261. modoboa/webmail/urls.py +5 -0
  262. modoboa/webmail/viewsets.py +39 -9
  263. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/METADATA +16 -13
  264. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/RECORD +269 -252
  265. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/WHEEL +1 -1
  266. modoboa/admin/management/commands/handle_mailbox_operations.py +0 -103
  267. modoboa/core/management/commands/cleanlogs.py +0 -53
  268. modoboa/frontend_dist/assets/AccountLayout-APeC9bsN.js +0 -1
  269. modoboa/frontend_dist/assets/AccountPasswordSubForm-D4vB2yQK.js +0 -1
  270. modoboa/frontend_dist/assets/AccountView-CloBP1BY.js +0 -1
  271. modoboa/frontend_dist/assets/AddressBook-C03MXg3y.js +0 -1
  272. modoboa/frontend_dist/assets/AdminLayout-DFmyKOo6.js +0 -1
  273. modoboa/frontend_dist/assets/AlarmsView-CFhH0I5d.js +0 -1
  274. modoboa/frontend_dist/assets/AliasRecipientForm-CZ_SvjtG.js +0 -1
  275. modoboa/frontend_dist/assets/AuditTrailView-Cyq25Jle.js +0 -1
  276. modoboa/frontend_dist/assets/CalendarView-C_dSXRp9.js +0 -1
  277. modoboa/frontend_dist/assets/ChoiceField-BPhUvoyr.js +0 -1
  278. modoboa/frontend_dist/assets/ComposeEmailForm-D3yUbBJX.css +0 -1
  279. modoboa/frontend_dist/assets/ComposeEmailForm-Ns5SYs4J.js +0 -1
  280. modoboa/frontend_dist/assets/ComposeEmailView-DgUfiWEH.js +0 -1
  281. modoboa/frontend_dist/assets/ConfirmDialog-BMIg94-U.js +0 -1
  282. modoboa/frontend_dist/assets/ConnectedLayout-CSXdVJtV.js +0 -1
  283. modoboa/frontend_dist/assets/CreationForm-CaAZYATm.js +0 -1
  284. modoboa/frontend_dist/assets/DashboardView-DErmyUoy.js +0 -1
  285. modoboa/frontend_dist/assets/DomainsView-CC1EwRId.js +0 -1
  286. modoboa/frontend_dist/assets/EmailField-BFgga4zp.js +0 -1
  287. modoboa/frontend_dist/assets/EmailView-DPWTch_6.js +0 -1
  288. modoboa/frontend_dist/assets/EmptyLayout-DAw7fAab.js +0 -1
  289. modoboa/frontend_dist/assets/FiltersView-Cg9lkLw0.js +0 -1
  290. modoboa/frontend_dist/assets/ForwardEmailView-Dd1sxgaP.js +0 -1
  291. modoboa/frontend_dist/assets/IdentitiesView-CiHIQ9Qt.js +0 -1
  292. modoboa/frontend_dist/assets/InformationView-Cc0aBA5j.js +0 -1
  293. modoboa/frontend_dist/assets/MailboxView-DIVABvk5.css +0 -1
  294. modoboa/frontend_dist/assets/MailboxView-DplC0IXR.js +0 -5
  295. modoboa/frontend_dist/assets/MenuItems-efx2rqwT.js +0 -1
  296. modoboa/frontend_dist/assets/MessagesView-CesYYZ9f.js +0 -1
  297. modoboa/frontend_dist/assets/MigrationsView-DSBk8T0y.js +0 -1
  298. modoboa/frontend_dist/assets/ParametersView-B8W_PAiz.js +0 -1
  299. modoboa/frontend_dist/assets/ParametersView-Dsx2M_g9.js +0 -1
  300. modoboa/frontend_dist/assets/ProviderEditView-PEAr7MA1.js +0 -1
  301. modoboa/frontend_dist/assets/ProvidersView-CHvQf3Vv.js +0 -1
  302. modoboa/frontend_dist/assets/QuarantineLayout-BWEaBK48.js +0 -1
  303. modoboa/frontend_dist/assets/QuarantineView-BzjeDhTm.js +0 -1
  304. modoboa/frontend_dist/assets/ReplyEmailView-CwQ9jsce.js +0 -1
  305. modoboa/frontend_dist/assets/ResourcesForm-noC3AXd9.js +0 -1
  306. modoboa/frontend_dist/assets/SelfServiceLayout-DQgRMu0L.js +0 -1
  307. modoboa/frontend_dist/assets/SettingsView-DJTdIeRK.css +0 -1
  308. modoboa/frontend_dist/assets/SettingsView-Dgwpfd8O.js +0 -6
  309. modoboa/frontend_dist/assets/StatisticsView-BZkVSh5o.js +0 -1
  310. modoboa/frontend_dist/assets/TimeSerieChart-BXJwhTMO.js +0 -1
  311. modoboa/frontend_dist/assets/TimeSerieChart-C3XHmlRd.css +0 -1
  312. modoboa/frontend_dist/assets/UserLayout-CWK9ICfE.js +0 -1
  313. modoboa/frontend_dist/assets/VApp-CiIBWB_Q.js +0 -1
  314. modoboa/frontend_dist/assets/VAutocomplete-DcNqTIie.js +0 -1
  315. modoboa/frontend_dist/assets/VAutocomplete-hzGuLlUI.css +0 -1
  316. modoboa/frontend_dist/assets/VAvatar-BsHplcRP.js +0 -1
  317. modoboa/frontend_dist/assets/VBadge-CavP_E2g.js +0 -1
  318. modoboa/frontend_dist/assets/VCheckbox-CKW9lz5i.js +0 -1
  319. modoboa/frontend_dist/assets/VColorPicker-Bsrz8yif.js +0 -1
  320. modoboa/frontend_dist/assets/VDataTable-ocZo8ju0.js +0 -1
  321. modoboa/frontend_dist/assets/VDataTableServer-CJfpYXQW.js +0 -1
  322. modoboa/frontend_dist/assets/VDataTableVirtual-Cr8yw49k.js +0 -1
  323. modoboa/frontend_dist/assets/VDialog-CUMh3paI.js +0 -1
  324. modoboa/frontend_dist/assets/VExpansionPanels-CkwHiKsA.js +0 -1
  325. modoboa/frontend_dist/assets/VFileInput-D5qlksIG.js +0 -1
  326. modoboa/frontend_dist/assets/VForm-ClJU5x_v.js +0 -1
  327. modoboa/frontend_dist/assets/VInput-DhzSMFXh.js +0 -1
  328. modoboa/frontend_dist/assets/VMenu-B53a3gm6.js +0 -1
  329. modoboa/frontend_dist/assets/VMenu-BEipA1lw.css +0 -1
  330. modoboa/frontend_dist/assets/VPicker-B928ZVJQ.js +0 -1
  331. modoboa/frontend_dist/assets/VPicker-ClSXs6kv.css +0 -1
  332. modoboa/frontend_dist/assets/VProgressCircular-viI3jDXe.js +0 -1
  333. modoboa/frontend_dist/assets/VRadioGroup-C0bqFrYg.js +0 -1
  334. modoboa/frontend_dist/assets/VSelect-y0E5_vPn.js +0 -1
  335. modoboa/frontend_dist/assets/VSheet-DQkh_hsX.js +0 -1
  336. modoboa/frontend_dist/assets/VSpacer-CBqB9uSt.js +0 -1
  337. modoboa/frontend_dist/assets/VSwitch-D4xdR9jz.js +0 -1
  338. modoboa/frontend_dist/assets/VTabs-D2c0KeF2.js +0 -1
  339. modoboa/frontend_dist/assets/VTabs-NzpINroH.css +0 -1
  340. modoboa/frontend_dist/assets/VTextField-Cow3HZvI.css +0 -1
  341. modoboa/frontend_dist/assets/VTextField-DvvH1ciR.js +0 -1
  342. modoboa/frontend_dist/assets/VTextarea-DHtXtzqP.js +0 -1
  343. modoboa/frontend_dist/assets/VTextarea-DyGjqrlm.css +0 -1
  344. modoboa/frontend_dist/assets/VToolbar-CB2GrZpA.css +0 -1
  345. modoboa/frontend_dist/assets/VToolbar-pvLQtmbU.js +0 -1
  346. modoboa/frontend_dist/assets/VWindowItem-DErNPSH8.js +0 -1
  347. modoboa/frontend_dist/assets/WebmailLayout-BzW0LWYp.css +0 -1
  348. modoboa/frontend_dist/assets/WebmailLayout-D7J-QJ-0.js +0 -1
  349. modoboa/frontend_dist/assets/filter-BVvMTmPG.js +0 -1
  350. modoboa/frontend_dist/assets/global.store-Bha4Z76j.js +0 -1
  351. modoboa/frontend_dist/assets/importExport-EgijWlC1.js +0 -1
  352. modoboa/frontend_dist/assets/languages-Cge6pECg.js +0 -1
  353. modoboa/frontend_dist/assets/transports-BUSYxSF4.js +0 -1
  354. modoboa/frontend_dist/assets/webmail-HVg3ZHC0.js +0 -1
  355. modoboa/frontend_dist/assets/webmail.store-D-d88s4w.js +0 -1
  356. {modoboa-2.6.5.data → modoboa-2.7.0.data}/scripts/modoboa-admin.py +0 -0
  357. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/entry_points.txt +0 -0
  358. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/licenses/LICENSE +0 -0
  359. {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/top_level.txt +0 -0
@@ -1,118 +1,16 @@
1
- from email.header import Header
2
- from email.mime.image import MIMEImage
3
- from importlib.metadata import version
4
- import os
5
-
6
1
  import smtplib
7
- from urllib.parse import unquote, urlparse
8
-
9
- import lxml
10
2
 
11
3
  from django.conf import settings
12
4
  from django.core import mail
13
- from django.core.mail import EmailMessage, EmailMultiAlternatives
14
5
 
15
- from modoboa.core import models as core_models
16
6
  from modoboa.parameters import tools as param_tools
17
- from modoboa.webmail.lib.attachments import create_mail_attachment
18
- from modoboa.webmail.lib.utils import html2plaintext
7
+ from modoboa.webmail import constants, models
8
+ from modoboa.webmail.exceptions import WebmailInternalError
9
+ from modoboa.webmail.lib.utils import create_message
19
10
 
20
11
  from . import get_imapconnector
21
12
 
22
13
 
23
- def make_body_images_inline(body: str) -> tuple[str, list]:
24
- """Look for images inside the body and make them inline.
25
-
26
- Before sending a message in HTML format, it is necessary to find
27
- all img tags contained in the body in order to rewrite them. For
28
- example, icons provided by CKeditor are stored on the server
29
- filesystem and not accessible from the outside. We must embark
30
- them as parts of the MIME message if we want recipients to
31
- display them correctly.
32
-
33
- :param body: the HTML body to parse
34
- """
35
- html = lxml.html.fromstring(body)
36
- parts = []
37
- for tag in html.iter("img"):
38
- src = tag.get("src")
39
- if src is None:
40
- continue
41
- o = urlparse(src)
42
- path = unquote(os.path.join(settings.BASE_DIR, o.path[1:]))
43
- if not os.path.exists(path):
44
- continue
45
- fname = os.path.basename(path)
46
- cid = f"{os.path.splitext(fname)[0]}@modoboa"
47
- tag.set("src", f"cid:{cid}")
48
- with open(path, "rb") as fp:
49
- part = MIMEImage(fp.read())
50
- part["Content-ID"] = f"<{cid}>"
51
- part.replace_header("Content-Type", f'{part["Content-Type"]}; name="{fname}"')
52
- part["Content-Disposition"] = "inline"
53
- parts.append(part)
54
- return lxml.html.tostring(html, encoding="unicode"), parts
55
-
56
-
57
- def html_msg(attributes: dict) -> EmailMultiAlternatives:
58
- """Create a multipart message.
59
-
60
- We attach two alternatives:
61
- * text/html
62
- * text/plain
63
- """
64
- body = attributes.get("body")
65
- if body:
66
- tbody = html2plaintext(body)
67
- body, images = make_body_images_inline(body)
68
- else:
69
- tbody = ""
70
- images = []
71
- msg = EmailMultiAlternatives()
72
- msg.body = tbody
73
- msg.attach_alternative(body, "text/html")
74
- for img in images:
75
- msg.attach(img)
76
- return msg
77
-
78
-
79
- def plain_msg(attributes: dict) -> EmailMessage:
80
- """Create a simple text message."""
81
- msg = EmailMessage()
82
- msg.body = attributes.get("body", "")
83
- return msg
84
-
85
-
86
- def format_sender_address(user: core_models.User, address: str) -> str:
87
- """Format address before message is sent."""
88
- if user.first_name != "" or user.last_name != "":
89
- return f'"{Header(user.fullname, "utf8")}" <{address}>'
90
- return address
91
-
92
-
93
- def create_message(user: core_models.User, attributes: dict, attachments: list):
94
- headers = {"User-Agent": "Modoboa {}".format(version("modoboa"))}
95
- origmsgid = attributes.get("in_reply_to")
96
- if origmsgid:
97
- headers.update({"References": origmsgid, "In-Reply-To": origmsgid})
98
- mode = user.parameters.get_value("editor")
99
- sender = format_sender_address(user, attributes["sender"])
100
- if mode == "html":
101
- msg = html_msg(attributes)
102
- else:
103
- msg = plain_msg(attributes)
104
- msg.from_email = sender
105
- msg.to = attributes["to"]
106
- msg.headers = headers
107
- for hdr in ["subject", "cc", "bcc"]:
108
- if hdr in attributes:
109
- setattr(msg, hdr, attributes[hdr])
110
-
111
- for attachment in attachments:
112
- msg.attach(create_mail_attachment(attachment))
113
- return msg
114
-
115
-
116
14
  def send_mail(request, attributes: dict, attachments: list) -> tuple[bool, str | None]:
117
15
  """
118
16
  Send a new email.
@@ -161,3 +59,88 @@ def send_mail(request, attributes: dict, attachments: list) -> tuple[bool, str |
161
59
  with get_imapconnector(request) as imapc:
162
60
  imapc.push_mail(sentfolder, msg.message())
163
61
  return True, None
62
+
63
+
64
+ def schedule_email(
65
+ request, attributes: dict, attachments: list
66
+ ) -> models.ScheduledMessage:
67
+ """Schedule a new email sending."""
68
+ scheduled_datetime = attributes["scheduled_datetime"].replace(
69
+ second=0, microsecond=0
70
+ )
71
+ sched_msg = models.ScheduledMessage(
72
+ account=request.user,
73
+ sender=attributes["sender"],
74
+ scheduled_datetime=scheduled_datetime,
75
+ to=",".join(attributes["to"]),
76
+ subject=attributes.get("subject", ""),
77
+ body=attributes.get("body", ""),
78
+ in_reply_to=attributes.get("in_reply_to", ""),
79
+ )
80
+ for attr in ["cc", "bcc"]:
81
+ if attr in attributes:
82
+ setattr(sched_msg, attr, ",".join(attributes[attr]))
83
+ sched_msg.save()
84
+
85
+ # Save a copy of this message into an IMAP mailbox
86
+ msg = sched_msg.to_email_message()
87
+ with get_imapconnector(request) as imapc:
88
+ try:
89
+ imapc.create_folder(constants.MAILBOX_NAME_SCHEDULED)
90
+ except WebmailInternalError:
91
+ pass
92
+ # TODO: deal with UID Validity
93
+ sched_msg.imap_uid = imapc.push_mail(
94
+ constants.MAILBOX_NAME_SCHEDULED, msg.message()
95
+ )
96
+ sched_msg.save()
97
+
98
+ for attachment in attachments:
99
+ mattachment = models.MessageAttachment(
100
+ message=sched_msg,
101
+ )
102
+ for header in ["content-type", "Content-Type"]:
103
+ if header in attachment:
104
+ mattachment.content_type = attachment[header]
105
+ break
106
+ if not mattachment.content_type:
107
+ continue
108
+
109
+ mattachment.file.name = attachment["tmpname"]
110
+ if "fname" in attachment:
111
+ mattachment.filename = attachment["fname"]
112
+ mattachment.save()
113
+
114
+ return sched_msg
115
+
116
+
117
+ def send_scheduled_message(sched_msg: models.ScheduledMessage) -> bool:
118
+ """Send a scheduled message using configured SMTP server."""
119
+ msg = sched_msg.to_email_message()
120
+ conf = dict(param_tools.get_global_parameters("webmail"))
121
+ options = {
122
+ "host": conf["scheduling_smtp_server"],
123
+ "port": conf["scheduling_smtp_port"],
124
+ }
125
+ if conf["scheduling_smtp_secured_mode"] == "ssl":
126
+ options.update({"use_ssl": True})
127
+ elif conf["scheduling_smtp_secured_mode"] == "starttls":
128
+ options.update({"use_tls": True})
129
+
130
+ try:
131
+ with mail.get_connection(**options) as connection:
132
+ msg.connection = connection
133
+ msg.send()
134
+ except (smtplib.SMTPResponseException, smtplib.SMTPRecipientsRefused) as inst:
135
+ if isinstance(inst, smtplib.SMTPRecipientsRefused):
136
+ error = ", ".join(
137
+ [f"{rcpt}: {error}" for rcpt, error in inst.recipients.items()]
138
+ )
139
+ else:
140
+ error = str(inst.smtp_error)
141
+ sched_msg.status = constants.SchedulingState.SEND_ERROR.value
142
+ sched_msg.error = error
143
+ sched_msg.save()
144
+ return False
145
+
146
+ return sched_msg.delete_imap_copy()
@@ -1,7 +1,19 @@
1
1
  """Misc. utilities."""
2
2
 
3
+ from email.header import Header
4
+ from email.mime.image import MIMEImage
5
+ from importlib.metadata import version
6
+ import os
7
+ from urllib.parse import unquote, urlparse
8
+
3
9
  import lxml
4
10
 
11
+ from django.conf import settings
12
+ from django.core.mail import EmailMessage, EmailMultiAlternatives
13
+
14
+ from modoboa.core import models as core_models
15
+ from modoboa.webmail.lib.attachments import create_mail_attachment
16
+
5
17
 
6
18
  def html2plaintext(content: str) -> str:
7
19
  """HTML to plain text translation.
@@ -43,3 +55,100 @@ def decode_payload(encoding, payload):
43
55
 
44
56
  return quopri.decodestring(payload)
45
57
  return payload
58
+
59
+
60
+ def make_body_images_inline(body: str) -> tuple[str, list]:
61
+ """Look for images inside the body and make them inline.
62
+
63
+ Before sending a message in HTML format, it is necessary to find
64
+ all img tags contained in the body in order to rewrite them. For
65
+ example, icons provided by CKeditor are stored on the server
66
+ filesystem and not accessible from the outside. We must embark
67
+ them as parts of the MIME message if we want recipients to
68
+ display them correctly.
69
+
70
+ :param body: the HTML body to parse
71
+ """
72
+ html = lxml.html.fromstring(body)
73
+ parts = []
74
+ for tag in html.iter("img"):
75
+ src = tag.get("src")
76
+ if src is None:
77
+ continue
78
+ o = urlparse(src)
79
+ path = unquote(os.path.join(settings.BASE_DIR, o.path[1:]))
80
+ if not os.path.exists(path):
81
+ continue
82
+ fname = os.path.basename(path)
83
+ cid = f"{os.path.splitext(fname)[0]}@modoboa"
84
+ tag.set("src", f"cid:{cid}")
85
+ with open(path, "rb") as fp:
86
+ part = MIMEImage(fp.read())
87
+ part["Content-ID"] = f"<{cid}>"
88
+ part.replace_header("Content-Type", f'{part["Content-Type"]}; name="{fname}"')
89
+ part["Content-Disposition"] = "inline"
90
+ parts.append(part)
91
+ return lxml.html.tostring(html, encoding="unicode"), parts
92
+
93
+
94
+ def html_msg(body: str) -> EmailMultiAlternatives:
95
+ """Create a multipart message.
96
+
97
+ We attach two alternatives:
98
+ * text/html
99
+ * text/plain
100
+ """
101
+ if body:
102
+ tbody = html2plaintext(body)
103
+ body, images = make_body_images_inline(body)
104
+ else:
105
+ tbody = ""
106
+ images = []
107
+ msg = EmailMultiAlternatives()
108
+ msg.body = tbody
109
+ msg.attach_alternative(body, "text/html")
110
+ for img in images:
111
+ msg.attach(img)
112
+ return msg
113
+
114
+
115
+ def plain_msg(body: str) -> EmailMessage:
116
+ """Create a simple text message."""
117
+ msg = EmailMessage()
118
+ msg.body = body
119
+ return msg
120
+
121
+
122
+ def format_sender_address(user: core_models.User, address: str) -> str:
123
+ """Format address before message is sent."""
124
+ if user.first_name != "" or user.last_name != "":
125
+ return f'"{Header(user.fullname, "utf8")}" <{address}>'
126
+ return address
127
+
128
+
129
+ def create_message(
130
+ user: core_models.User, attributes: dict, attachments: list
131
+ ) -> EmailMessage:
132
+ """Create an EmailMessage instance ready to be sent."""
133
+ extra_headers = {"User-Agent": "Modoboa {}".format(version("modoboa"))}
134
+ headers = {}
135
+ origmsgid = attributes.get("in_reply_to")
136
+ if origmsgid:
137
+ headers.update({"References": origmsgid, "In-Reply-To": origmsgid})
138
+ mode = user.parameters.get_value("editor")
139
+ sender = format_sender_address(user, attributes["sender"])
140
+ if mode == "html":
141
+ msg = html_msg(attributes.get("body", ""))
142
+ else:
143
+ msg = plain_msg(attributes.get("body", ""))
144
+ msg.from_email = sender
145
+ msg.to = attributes["to"]
146
+ msg.headers = headers
147
+ msg.extra_headers = extra_headers
148
+ for hdr in ["subject", "cc", "bcc"]:
149
+ if hdr in attributes:
150
+ setattr(msg, hdr, attributes[hdr])
151
+
152
+ for attachment in attachments:
153
+ msg.attach(create_mail_attachment(attachment))
154
+ return msg
@@ -0,0 +1,90 @@
1
+ # Generated by Django 5.2.10 on 2026-02-02 13:29
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="ScheduledMessage",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.AutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ("sender", models.CharField(max_length=255)),
30
+ ("subject", models.CharField(max_length=255)),
31
+ ("body", models.TextField()),
32
+ ("to", models.TextField()),
33
+ ("cc", models.TextField(blank=True, null=True)),
34
+ ("bcc", models.TextField(blank=True, null=True)),
35
+ (
36
+ "in_reply_to",
37
+ models.CharField(blank=True, max_length=200, null=True),
38
+ ),
39
+ ("scheduled_datetime", models.DateTimeField()),
40
+ ("imap_uid", models.IntegerField(blank=True, null=True)),
41
+ (
42
+ "status",
43
+ models.CharField(
44
+ choices=[
45
+ ("scheduled", "scheduled"),
46
+ ("sending", "sending"),
47
+ ("send_error", "send_error"),
48
+ ("move_error", "move_error"),
49
+ ],
50
+ db_index=True,
51
+ default="scheduled",
52
+ max_length=15,
53
+ ),
54
+ ),
55
+ ("error", models.CharField(blank=True, max_length=255, null=True)),
56
+ (
57
+ "account",
58
+ models.ForeignKey(
59
+ on_delete=django.db.models.deletion.CASCADE,
60
+ to=settings.AUTH_USER_MODEL,
61
+ ),
62
+ ),
63
+ ],
64
+ ),
65
+ migrations.CreateModel(
66
+ name="MessageAttachment",
67
+ fields=[
68
+ (
69
+ "id",
70
+ models.AutoField(
71
+ auto_created=True,
72
+ primary_key=True,
73
+ serialize=False,
74
+ verbose_name="ID",
75
+ ),
76
+ ),
77
+ ("file", models.FileField(upload_to="")),
78
+ ("content_type", models.CharField(max_length=150)),
79
+ ("filename", models.CharField(max_length=255)),
80
+ (
81
+ "message",
82
+ models.ForeignKey(
83
+ on_delete=django.db.models.deletion.CASCADE,
84
+ related_name="attachments",
85
+ to="webmail.scheduledmessage",
86
+ ),
87
+ ),
88
+ ],
89
+ ),
90
+ ]
File without changes
modoboa/webmail/mocks.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Mock objects."""
2
2
 
3
+ from modoboa.webmail import constants
3
4
  from modoboa.webmail.tests import data as tests_data
4
5
 
5
6
 
@@ -24,7 +25,7 @@ class IMAP4Mock:
24
25
  return "OK", None
25
26
 
26
27
  def append(self, *args, **kwargs):
27
- pass
28
+ return "OK", [b"[APPENDUID 1234 11] ..."] # noqa
28
29
 
29
30
  def create(self, name):
30
31
  return "OK", None
@@ -43,7 +44,10 @@ class IMAP4Mock:
43
44
  return "OK", [b"19"]
44
45
  elif command == "FETCH":
45
46
  uid = int(args[0])
46
- data = tests_data.BODYSTRUCTURE_SAMPLE_WITH_FLAGS
47
+ if constants.CUSTOM_HEADER_SCHEDULED_ID.upper() in args[1]:
48
+ data = tests_data.BODYSTRUCTURE_SAMPLE_SCHEDULED
49
+ else:
50
+ data = tests_data.BODYSTRUCTURE_SAMPLE_WITH_FLAGS
47
51
  if uid == 46931:
48
52
  if args[1] == "(BODYSTRUCTURE)":
49
53
  data = tests_data.BODYSTRUCTURE_ONLY_4
@@ -62,7 +66,12 @@ class IMAP4Mock:
62
66
  if args[1] == "(BODYSTRUCTURE)":
63
67
  data = tests_data.BODYSTRUCTURE_EMPTY_MAIL
64
68
  elif "HEADER.FIELDS" in args[1]:
65
- data = tests_data.BODYSTRUCTURE_EMPTY_MAIL_WITH_HEADERS
69
+ if constants.CUSTOM_HEADER_SCHEDULED_DATETIME.upper() in args[1]:
70
+ data = (
71
+ tests_data.BODYSTRUCTURE_EMPTY_MAIL_WITH_HEADERS_SCHEDULED
72
+ )
73
+ else:
74
+ data = tests_data.BODYSTRUCTURE_EMPTY_MAIL_WITH_HEADERS
66
75
  else:
67
76
  data = tests_data.EMPTY_BODY
68
77
  elif uid == 3444:
@@ -0,0 +1,102 @@
1
+ """
2
+ Webmail related models.
3
+ """
4
+
5
+ from django.core.mail import EmailMessage
6
+ from django.db import models
7
+
8
+ from modoboa.lib.sysutils import doveadm_cmd
9
+ from modoboa.webmail import constants
10
+ from modoboa.webmail.lib.utils import create_message
11
+
12
+
13
+ class ScheduledMessage(models.Model):
14
+ account = models.ForeignKey("core.User", on_delete=models.CASCADE)
15
+ sender = models.CharField(max_length=255)
16
+ subject = models.CharField(max_length=255)
17
+ body = models.TextField()
18
+ to = models.TextField()
19
+ cc = models.TextField(blank=True, null=True)
20
+ bcc = models.TextField(blank=True, null=True)
21
+ in_reply_to = models.CharField(max_length=200, blank=True, null=True)
22
+ scheduled_datetime = models.DateTimeField()
23
+ imap_uid = models.IntegerField(blank=True, null=True)
24
+ status = models.CharField(
25
+ choices=constants.EMAIL_SCHEDULING_STATES,
26
+ default=constants.SchedulingState.SCHEDULED.value,
27
+ max_length=15,
28
+ db_index=True,
29
+ )
30
+ error = models.CharField(max_length=255, null=True, blank=True)
31
+
32
+ def __str__(self):
33
+ return f"{self.subject} - {self.sender.username} - {self.scheduled_datetime}"
34
+
35
+ def to_dict(self) -> dict:
36
+ result = {
37
+ "in_reply_to": self.in_reply_to,
38
+ "sender": self.sender,
39
+ "to": self.to.split(","),
40
+ }
41
+ if self.subject:
42
+ result["subject"] = self.subject
43
+ if self.body:
44
+ result["body"] = self.body
45
+ for hdr in ["cc", "bcc"]:
46
+ if getattr(self, hdr):
47
+ result[hdr] = getattr(self, hdr).split(",")
48
+ return result
49
+
50
+ def delete_imap_copy(self) -> bool:
51
+ """Move IMAP message to Sent folder using doveadm."""
52
+ # TODO: use doveadm HTTP API when dovecot is not local
53
+ sent_folder = self.account.parameters.get_value("sent_folder")
54
+ code, output = doveadm_cmd(
55
+ f"move -u {self.account.email} {sent_folder} "
56
+ f"mailbox {constants.MAILBOX_NAME_SCHEDULED} "
57
+ f"header {constants.CUSTOM_HEADER_SCHEDULED_ID} {self.id}"
58
+ )
59
+ if code:
60
+ self.status = constants.SchedulingState.MOVE_ERROR.value
61
+ self.error = output
62
+ self.save()
63
+ return False
64
+
65
+ # Try to delete mailbox when empty
66
+ doveadm_cmd(
67
+ f"mailbox delete -u {self.account.email} -s -e "
68
+ f"{constants.MAILBOX_NAME_SCHEDULED}"
69
+ )
70
+
71
+ return True
72
+
73
+ def to_email_message(self) -> EmailMessage:
74
+ """Convert this scheduled message to an EmailMessage instance."""
75
+ attachments = [attachment.to_dict() for attachment in self.attachments.all()]
76
+ result = create_message(self.account, self.to_dict(), attachments)
77
+ result.extra_headers.update(
78
+ {
79
+ constants.CUSTOM_HEADER_SCHEDULED_ID: self.id,
80
+ constants.CUSTOM_HEADER_SCHEDULED_DATETIME: self.scheduled_datetime.isoformat(),
81
+ }
82
+ )
83
+ return result
84
+
85
+
86
+ class MessageAttachment(models.Model):
87
+ message = models.ForeignKey(
88
+ ScheduledMessage, on_delete=models.CASCADE, related_name="attachments"
89
+ )
90
+ file = models.FileField()
91
+ content_type = models.CharField(max_length=150)
92
+ filename = models.CharField(max_length=255)
93
+
94
+ def __str__(self):
95
+ return f"Attachment for {self.message.subject}"
96
+
97
+ def to_dict(self) -> dict:
98
+ return {
99
+ "content-type": self.content_type,
100
+ "tmpname": self.file.name,
101
+ "fname": self.filename,
102
+ }