modoboa 2.6.4__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 (364) 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/autoreply/models.py +4 -4
  16. modoboa/calendars/backends/caldav_.py +17 -13
  17. modoboa/calendars/jobs.py +7 -0
  18. modoboa/calendars/mocks.py +4 -1
  19. modoboa/calendars/serializers.py +14 -9
  20. modoboa/calendars/tests.py +10 -9
  21. modoboa/calendars/viewsets.py +3 -1
  22. modoboa/contacts/migrations/0008_addressbook_syncing.py +18 -0
  23. modoboa/contacts/models.py +1 -0
  24. modoboa/contacts/serializers.py +5 -2
  25. modoboa/contacts/tasks.py +9 -3
  26. modoboa/contacts/tests.py +32 -6
  27. modoboa/contacts/viewsets.py +7 -1
  28. modoboa/core/api/v2/serializers.py +0 -7
  29. modoboa/core/api/v2/tests.py +0 -10
  30. modoboa/core/app_settings.py +0 -22
  31. modoboa/core/commands/deploy.py +13 -0
  32. modoboa/core/commands/templates/cron_config.py.tpl +33 -0
  33. modoboa/core/commands/templates/settings.py.tpl +21 -0
  34. modoboa/core/jobs.py +34 -0
  35. modoboa/core/management/commands/load_initial_data.py +1 -1
  36. modoboa/core/password_hashers/base.py +4 -1
  37. modoboa/core/tests/test_core.py +0 -14
  38. modoboa/core/tests/test_jobs.py +40 -0
  39. modoboa/frontend_dist/assets/{AccountAliasForm-tLHotdIR.js → AccountAliasForm-DO6DwfjE.js} +1 -1
  40. modoboa/frontend_dist/assets/{AccountEditView-DnQIIg47.js → AccountEditView-CCuN9mGB.js} +1 -1
  41. modoboa/frontend_dist/assets/AccountLayout-Ge7fzuZg.js +1 -0
  42. modoboa/frontend_dist/assets/AccountPasswordSubForm-PkFPblkR.js +1 -0
  43. modoboa/frontend_dist/assets/AccountView-dgseekZ8.js +1 -0
  44. modoboa/frontend_dist/assets/AddressBook-DZWOHOJj.js +1 -0
  45. modoboa/frontend_dist/assets/AdminLayout-itB_mmH_.js +1 -0
  46. modoboa/frontend_dist/assets/AlarmsView-King6zb6.js +1 -0
  47. modoboa/frontend_dist/assets/{AliasEditView-n75NEN_s.js → AliasEditView-CTjrXYPf.js} +1 -1
  48. modoboa/frontend_dist/assets/AliasRecipientForm-CI7bXqVp.js +1 -0
  49. modoboa/frontend_dist/assets/{AliasView-DFIvPeIm.js → AliasView-BN13MNN_.js} +1 -1
  50. modoboa/frontend_dist/assets/AuditTrailView-C1jQwgoo.js +1 -0
  51. modoboa/frontend_dist/assets/CalendarView-juHVIHU5.css +1 -0
  52. modoboa/frontend_dist/assets/CalendarView-rRSzqxrH.js +1 -0
  53. modoboa/frontend_dist/assets/ChoiceField-DCr12shR.js +1 -0
  54. modoboa/frontend_dist/assets/ComposeEmailForm-BmtKwFb1.js +2 -0
  55. modoboa/frontend_dist/assets/ComposeEmailForm-CVNDl-Mq.css +1 -0
  56. modoboa/frontend_dist/assets/ComposeEmailView-JW22Phrb.js +1 -0
  57. modoboa/frontend_dist/assets/ConfirmDialog-CWpdwSQ6.js +1 -0
  58. modoboa/frontend_dist/assets/ConnectedLayout-BP8pO27H.js +1 -0
  59. modoboa/frontend_dist/assets/{ConnectedLayout-C6HNXWkp.css → ConnectedLayout-Ddpb_6yT.css} +1 -1
  60. modoboa/frontend_dist/assets/CreationForm-CzelJsVQ.js +1 -0
  61. modoboa/frontend_dist/assets/DashboardView-BBkBodVj.js +1 -0
  62. modoboa/frontend_dist/assets/{DomainAdminList-BjtfbhRj.js → DomainAdminList-DD7p3i6F.js} +1 -1
  63. modoboa/frontend_dist/assets/{DomainEditView-Cq5Ea4RW.js → DomainEditView-DoIoNYC4.js} +1 -1
  64. modoboa/frontend_dist/assets/{DomainTransportForm-DAXJGNLA.js → DomainTransportForm-CbiJF9z5.js} +1 -1
  65. modoboa/frontend_dist/assets/{DomainView-BfpK7U14.js → DomainView-BXvznFYz.js} +3 -3
  66. modoboa/frontend_dist/assets/DomainsView-ffYjiffp.js +1 -0
  67. modoboa/frontend_dist/assets/EmailField-KhhJYA4D.js +1 -0
  68. modoboa/frontend_dist/assets/EmailSchedulingForm-CQL5Vfdr.js +1 -0
  69. modoboa/frontend_dist/assets/EmailView-Bq2bHZBO.js +1 -0
  70. modoboa/frontend_dist/assets/EmptyLayout-NrTftp18.js +1 -0
  71. modoboa/frontend_dist/assets/FiltersView-SOQy0U_3.js +1 -0
  72. modoboa/frontend_dist/assets/ForwardEmailView-kEPDjWUw.js +1 -0
  73. modoboa/frontend_dist/assets/{HtmlEditor-D753fIAJ.js → HtmlEditor-CyBl5wj2.js} +15 -15
  74. modoboa/frontend_dist/assets/IdentitiesView-CZFf4oR9.js +1 -0
  75. modoboa/frontend_dist/assets/InformationView-CAtneAqM.js +1 -0
  76. modoboa/frontend_dist/assets/{LoadingData-Tm56XNwj.js → LoadingData-C2txD49L.js} +1 -1
  77. modoboa/frontend_dist/assets/{LoginCallbackView-DjCHUgz5.js → LoginCallbackView-DAvty3CI.js} +1 -1
  78. modoboa/frontend_dist/assets/{LoginView-D0r4XBEl.js → LoginView-DHYITkn4.js} +1 -1
  79. modoboa/frontend_dist/assets/MailboxView-BNg2v7mi.css +1 -0
  80. modoboa/frontend_dist/assets/MailboxView-DHg5A78D.js +5 -0
  81. modoboa/frontend_dist/assets/MenuItems-C9p70bSC.js +1 -0
  82. modoboa/frontend_dist/assets/{MessageView-BWh9BqzS.js → MessageView-CrrEJpNI.js} +1 -1
  83. modoboa/frontend_dist/assets/MessagesView-CyecRd4I.js +1 -0
  84. modoboa/frontend_dist/assets/MigrationsView-DXGKKz2h.js +1 -0
  85. modoboa/frontend_dist/assets/{ParametersForm-y02-GM0r.js → ParametersForm-DFKYkPAs.js} +1 -1
  86. modoboa/frontend_dist/assets/ParametersView-B1wT5oyt.js +1 -0
  87. modoboa/frontend_dist/assets/ParametersView-CfvVzKea.js +1 -0
  88. modoboa/frontend_dist/assets/ProviderEditView-De-8ggBp.js +1 -0
  89. modoboa/frontend_dist/assets/ProviderGeneralForm-DO-IpJMQ.js +1 -0
  90. modoboa/frontend_dist/assets/ProvidersView-Bhk7sCz4.js +1 -0
  91. modoboa/frontend_dist/assets/QuarantineLayout-C1YszNmP.js +1 -0
  92. modoboa/frontend_dist/assets/QuarantineView-cElD_rS-.js +1 -0
  93. modoboa/frontend_dist/assets/ReplyEmailView-BvuOZdLl.js +1 -0
  94. modoboa/frontend_dist/assets/ResourcesForm-FWxrmVwo.js +1 -0
  95. modoboa/frontend_dist/assets/SelfServiceLayout-JHRvfvQf.js +1 -0
  96. modoboa/frontend_dist/assets/SettingsView-SIsKAXtQ.css +1 -0
  97. modoboa/frontend_dist/assets/SettingsView-s2l2Xl1L.js +6 -0
  98. modoboa/frontend_dist/assets/StatisticsView-FiidWvad.js +1 -0
  99. modoboa/frontend_dist/assets/TimeSerieChart-CdqUHiO_.js +1 -0
  100. modoboa/frontend_dist/assets/TimeSerieChart-nLIFGI0y.css +1 -0
  101. modoboa/frontend_dist/assets/UserLayout-DyvI8duf.js +1 -0
  102. modoboa/frontend_dist/assets/{VAlert-CNys_LUS.js → VAlert-MDbeolOo.js} +1 -1
  103. modoboa/frontend_dist/assets/VApp-DM1KLQfQ.js +1 -0
  104. modoboa/frontend_dist/assets/VAutocomplete-C9NL5_uo.css +1 -0
  105. modoboa/frontend_dist/assets/VAutocomplete-CQsWYWNX.js +1 -0
  106. modoboa/frontend_dist/assets/VAvatar-CXV_FqpP.js +1 -0
  107. modoboa/frontend_dist/assets/VBadge-7tDx7aI3.js +1 -0
  108. modoboa/frontend_dist/assets/{VCard-BoH6kjPa.js → VCard-CBtX8JF-.js} +1 -1
  109. modoboa/frontend_dist/assets/VCheckbox-zY1MApOy.js +1 -0
  110. modoboa/frontend_dist/assets/{VCheckboxBtn-Ccl786Rg.js → VCheckboxBtn-B0mIT3E0.js} +1 -1
  111. modoboa/frontend_dist/assets/VColorPicker-BbCHvk6K.js +1 -0
  112. modoboa/frontend_dist/assets/{VColorPicker-B_lVDaYR.css → VColorPicker-C9m8L-6U.css} +1 -1
  113. modoboa/frontend_dist/assets/{VContainer-DKMcKasC.js → VContainer-Cn-vKB3s.js} +1 -1
  114. modoboa/frontend_dist/assets/VDataTable-CNT9KOSp.js +1 -0
  115. modoboa/frontend_dist/assets/VDataTableServer-_yn4Ry6U.js +1 -0
  116. modoboa/frontend_dist/assets/VDataTableVirtual-Csxh3Gp6.js +1 -0
  117. modoboa/frontend_dist/assets/VDatePicker-_OUDqShN.css +1 -0
  118. modoboa/frontend_dist/assets/VDatePicker-iFu13xIP.js +2 -0
  119. modoboa/frontend_dist/assets/VDialog-lKtBdqnp.js +1 -0
  120. modoboa/frontend_dist/assets/VExpansionPanels-CaIvk9iF.js +1 -0
  121. modoboa/frontend_dist/assets/VFileInput-C7J_qVmk.js +1 -0
  122. modoboa/frontend_dist/assets/VForm-fdQ5d-CH.js +1 -0
  123. modoboa/frontend_dist/assets/VInput-BwHvhzAe.js +1 -0
  124. modoboa/frontend_dist/assets/VMenu-BpmJf4X2.js +1 -0
  125. modoboa/frontend_dist/assets/VMenu-C5A_5Hs5.css +1 -0
  126. modoboa/frontend_dist/assets/VPicker-B7cB3kJg.css +1 -0
  127. modoboa/frontend_dist/assets/VPicker-BZho70wU.js +1 -0
  128. modoboa/frontend_dist/assets/VProgressCircular-DRw_-iNj.js +1 -0
  129. modoboa/frontend_dist/assets/VRadioGroup--eiP5xtJ.js +1 -0
  130. modoboa/frontend_dist/assets/{VRow-CuVZGqBj.js → VRow-83Qnr5iB.js} +1 -1
  131. modoboa/frontend_dist/assets/VSelect-BcmGFGif.js +1 -0
  132. modoboa/frontend_dist/assets/{VSelectionControl-uKY9Zull.js → VSelectionControl-Cqi1xt-q.js} +1 -1
  133. modoboa/frontend_dist/assets/VSheet-C3MaHhtw.js +1 -0
  134. modoboa/frontend_dist/assets/VSpacer-CuSkdJZL.js +1 -0
  135. modoboa/frontend_dist/assets/VSwitch-XumUl685.js +1 -0
  136. modoboa/frontend_dist/assets/{VTable-gDsAUSLX.js → VTable-f7wcr2AZ.js} +1 -1
  137. modoboa/frontend_dist/assets/VTabs-CDfdejXj.css +1 -0
  138. modoboa/frontend_dist/assets/VTabs-y4wNP4im.js +1 -0
  139. modoboa/frontend_dist/assets/VTextField-BPIvtrn4.js +1 -0
  140. modoboa/frontend_dist/assets/VTextField-DjbYGlzs.css +1 -0
  141. modoboa/frontend_dist/assets/VTextarea-B6bGYcC3.js +1 -0
  142. modoboa/frontend_dist/assets/VTextarea-f6vTjzFy.css +1 -0
  143. modoboa/frontend_dist/assets/VToolbar-BYDPtwf0.css +1 -0
  144. modoboa/frontend_dist/assets/VToolbar-CB6wwtYc.js +1 -0
  145. modoboa/frontend_dist/assets/VWindowItem-CDtLLEkg.js +1 -0
  146. modoboa/frontend_dist/assets/WebmailLayout-CQQAolnl.css +1 -0
  147. modoboa/frontend_dist/assets/WebmailLayout-tosQSHLS.js +1 -0
  148. modoboa/frontend_dist/assets/{accounts-DqbBlaLK.js → accounts-KUsk6LHW.js} +1 -1
  149. modoboa/frontend_dist/assets/{admin-D36m_nXq.js → admin-BW9cZW0P.js} +1 -1
  150. modoboa/frontend_dist/assets/{aliases-BtyF8GC8.js → aliases-Ge0hjIsH.js} +1 -1
  151. modoboa/frontend_dist/assets/{amavis-DySmsCJQ.js → amavis-BbFeFfsk.js} +1 -1
  152. modoboa/frontend_dist/assets/{amavis-B4QhM5Fa.js → amavis-DtuzP_CS.js} +1 -1
  153. modoboa/frontend_dist/assets/{contacts-Df8ptuMn.js → contacts-DMJlQTe0.js} +1 -1
  154. modoboa/frontend_dist/assets/{domains-CSxtTvUh.js → domains-Du64lcXT.js} +1 -1
  155. modoboa/frontend_dist/assets/domains.store-1U61jeCV.js +1 -0
  156. modoboa/frontend_dist/assets/events-BM3in65C.js +1 -0
  157. modoboa/frontend_dist/assets/filter-Dihm6o59.js +1 -0
  158. modoboa/frontend_dist/assets/importExport-HGcNGWOm.js +1 -0
  159. modoboa/frontend_dist/assets/{index-B9q1vO3K.css → index-B1EK3MQe.css} +1 -1
  160. modoboa/frontend_dist/assets/{index-Bp9Fb67E.js → index-Dv00bmw9.js} +1 -1
  161. modoboa/frontend_dist/assets/index-jui3edpn.js +1001 -0
  162. modoboa/frontend_dist/assets/{language.store-_-zrrEiS.js → language.store-OcfdXL_-.js} +1 -1
  163. modoboa/frontend_dist/assets/languages-CF8hxo7x.js +1 -0
  164. modoboa/frontend_dist/assets/{layout-CjSP_k58.js → layout-DOO7TRTJ.js} +1 -1
  165. modoboa/frontend_dist/assets/{layout.store-CF5WRevH.js → layout.store-C0g-piJn.js} +1 -1
  166. modoboa/frontend_dist/assets/{logos-95XYS6uH.js → logos-Dz2Gzei-.js} +1 -1
  167. modoboa/frontend_dist/assets/{logs-CyU_PIDh.js → logs-CLm32Weu.js} +1 -1
  168. modoboa/frontend_dist/assets/{parameters-BxAXBXU1.js → parameters-DMIAQ7cd.js} +1 -1
  169. modoboa/frontend_dist/assets/{parameters.store-CATOSoQZ.js → parameters.store-DLnFzCwV.js} +1 -1
  170. modoboa/frontend_dist/assets/{permissions-cSkGqV3M.js → permissions-CrpE0b4w.js} +1 -1
  171. modoboa/frontend_dist/assets/{ssrBoot-05WtbG6H.js → ssrBoot-B7cr7q9U.js} +1 -1
  172. modoboa/frontend_dist/assets/{tag-BmV84V2s.js → tag-WF93n81Q.js} +1 -1
  173. modoboa/frontend_dist/assets/{theme-myV4ekXo.js → theme-_0oOYChG.js} +1 -1
  174. modoboa/frontend_dist/assets/transports-CS61syt-.js +1 -0
  175. modoboa/frontend_dist/assets/webmail-CYDXU0DS.js +1 -0
  176. modoboa/frontend_dist/assets/webmail.store-BvHCQSjM.js +1 -0
  177. modoboa/frontend_dist/index.html +2 -2
  178. modoboa/imap_migration/api/v2/serializers.py +13 -0
  179. modoboa/imap_migration/models.py +3 -3
  180. modoboa/imap_migration/templates/imap_migration/offlineimap.conf +2 -2
  181. modoboa/imap_migration/tests.py +28 -0
  182. modoboa/lib/cryptutils.py +2 -2
  183. modoboa/lib/sysutils.py +1 -1
  184. modoboa/lib/tests/__init__.py +0 -1
  185. modoboa/locale/br/LC_MESSAGES/django.mo +0 -0
  186. modoboa/locale/br/LC_MESSAGES/django.po +753 -317
  187. modoboa/locale/cs/LC_MESSAGES/django.po +671 -309
  188. modoboa/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  189. modoboa/locale/cs_CZ/LC_MESSAGES/django.po +801 -378
  190. modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
  191. modoboa/locale/de/LC_MESSAGES/django.po +1116 -455
  192. modoboa/locale/de_DE/LC_MESSAGES/django.mo +0 -0
  193. modoboa/locale/de_DE/LC_MESSAGES/django.po +745 -318
  194. modoboa/locale/el_GR/LC_MESSAGES/django.mo +0 -0
  195. modoboa/locale/el_GR/LC_MESSAGES/django.po +825 -393
  196. modoboa/locale/en/LC_MESSAGES/django.po +669 -309
  197. modoboa/locale/es/LC_MESSAGES/django.mo +0 -0
  198. modoboa/locale/es/LC_MESSAGES/django.po +810 -390
  199. modoboa/locale/es_MX/LC_MESSAGES/django.po +669 -309
  200. modoboa/locale/fi/LC_MESSAGES/django.mo +0 -0
  201. modoboa/locale/fi/LC_MESSAGES/django.po +756 -316
  202. modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
  203. modoboa/locale/fr/LC_MESSAGES/django.po +674 -313
  204. modoboa/locale/hu/LC_MESSAGES/django.po +669 -309
  205. modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
  206. modoboa/locale/it/LC_MESSAGES/django.po +807 -380
  207. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  208. modoboa/locale/ja_JP/LC_MESSAGES/django.po +951 -419
  209. modoboa/locale/ka/LC_MESSAGES/django.po +669 -309
  210. modoboa/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  211. modoboa/locale/nl_NL/LC_MESSAGES/django.po +807 -387
  212. modoboa/locale/no/LC_MESSAGES/django.po +669 -309
  213. modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  214. modoboa/locale/pl_PL/LC_MESSAGES/django.po +826 -396
  215. modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  216. modoboa/locale/pt_BR/LC_MESSAGES/django.po +830 -392
  217. modoboa/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  218. modoboa/locale/pt_PT/LC_MESSAGES/django.po +788 -371
  219. modoboa/locale/ro_RO/LC_MESSAGES/django.mo +0 -0
  220. modoboa/locale/ro_RO/LC_MESSAGES/django.po +763 -316
  221. modoboa/locale/ru/LC_MESSAGES/django.mo +0 -0
  222. modoboa/locale/ru/LC_MESSAGES/django.po +1336 -1046
  223. modoboa/locale/si/LC_MESSAGES/django.po +669 -309
  224. modoboa/locale/sk/LC_MESSAGES/django.po +671 -309
  225. modoboa/locale/sk_SK/LC_MESSAGES/django.po +671 -309
  226. modoboa/locale/sl_SI/LC_MESSAGES/django.po +695 -315
  227. modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
  228. modoboa/locale/sv/LC_MESSAGES/django.po +819 -387
  229. modoboa/locale/tr/LC_MESSAGES/django.mo +0 -0
  230. modoboa/locale/tr/LC_MESSAGES/django.po +737 -315
  231. modoboa/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
  232. modoboa/locale/tr_TR/LC_MESSAGES/django.po +711 -310
  233. modoboa/locale/uk/LC_MESSAGES/django.po +671 -309
  234. modoboa/locale/zh/LC_MESSAGES/django.po +668 -309
  235. modoboa/locale/zh_CN/LC_MESSAGES/django.po +668 -309
  236. modoboa/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  237. modoboa/locale/zh_TW/LC_MESSAGES/django.po +747 -316
  238. modoboa/maillog/jobs.py +11 -0
  239. modoboa/maillog/tests/test_views.py +5 -4
  240. modoboa/parameters/api/v2/tests.py +1 -1
  241. modoboa/policyd/management/commands/policy_daemon.py +5 -1
  242. modoboa/policyd/tests.py +4 -2
  243. modoboa/rspamd/tests.py +20 -0
  244. modoboa/templates/registration/twofactor_code_verify.html +1 -1
  245. modoboa/webmail/app_settings.py +37 -0
  246. modoboa/webmail/constants.py +21 -1
  247. modoboa/webmail/factories.py +19 -0
  248. modoboa/webmail/jobs.py +27 -0
  249. modoboa/webmail/lib/__init__.py +0 -2
  250. modoboa/webmail/lib/imapemail.py +9 -11
  251. modoboa/webmail/lib/imapheader.py +25 -13
  252. modoboa/webmail/lib/imaputils.py +51 -13
  253. modoboa/webmail/lib/sendmail.py +88 -105
  254. modoboa/webmail/lib/utils.py +109 -0
  255. modoboa/webmail/migrations/0001_initial.py +90 -0
  256. modoboa/webmail/migrations/__init__.py +0 -0
  257. modoboa/webmail/mocks.py +12 -3
  258. modoboa/webmail/models.py +102 -0
  259. modoboa/webmail/serializers.py +84 -4
  260. modoboa/webmail/tests/data.py +21 -0
  261. modoboa/webmail/tests/test_lib_imaputils.py +33 -0
  262. modoboa/webmail/tests/test_viewsets.py +108 -0
  263. modoboa/webmail/urls.py +5 -0
  264. modoboa/webmail/viewsets.py +39 -9
  265. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/METADATA +16 -13
  266. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/RECORD +271 -254
  267. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/WHEEL +1 -1
  268. modoboa/admin/management/commands/handle_mailbox_operations.py +0 -103
  269. modoboa/core/management/commands/cleanlogs.py +0 -53
  270. modoboa/frontend_dist/assets/AccountLayout-B2HptJzY.js +0 -1
  271. modoboa/frontend_dist/assets/AccountPasswordSubForm-DpADntAL.js +0 -1
  272. modoboa/frontend_dist/assets/AccountView-CJBGh9Oe.js +0 -1
  273. modoboa/frontend_dist/assets/AddressBook-CrFqzlzZ.js +0 -1
  274. modoboa/frontend_dist/assets/AdminLayout-BbCt7YxX.js +0 -1
  275. modoboa/frontend_dist/assets/AlarmsView-CIlt_IKQ.js +0 -1
  276. modoboa/frontend_dist/assets/AliasRecipientForm-jjj5yJJw.js +0 -1
  277. modoboa/frontend_dist/assets/AuditTrailView-sWnED3E9.js +0 -1
  278. modoboa/frontend_dist/assets/CalendarView-DSCBIvef.js +0 -1
  279. modoboa/frontend_dist/assets/ChoiceField-RDT6XcnX.js +0 -1
  280. modoboa/frontend_dist/assets/ComposeEmailForm-D3yUbBJX.css +0 -1
  281. modoboa/frontend_dist/assets/ComposeEmailForm-G3sXQAwI.js +0 -1
  282. modoboa/frontend_dist/assets/ComposeEmailView-Ce1CtFjD.js +0 -1
  283. modoboa/frontend_dist/assets/ConfirmDialog-Du7aqKbR.js +0 -1
  284. modoboa/frontend_dist/assets/ConnectedLayout-BHWaBAzH.js +0 -1
  285. modoboa/frontend_dist/assets/CreationForm-CjtILq0_.js +0 -1
  286. modoboa/frontend_dist/assets/DashboardView-DFNwusDZ.js +0 -1
  287. modoboa/frontend_dist/assets/DomainsView-CYxLy-3Y.js +0 -1
  288. modoboa/frontend_dist/assets/EmailField-DmPc4pyP.js +0 -1
  289. modoboa/frontend_dist/assets/EmailView-BE3vYZtJ.js +0 -1
  290. modoboa/frontend_dist/assets/EmptyLayout-DMwKNHfI.js +0 -1
  291. modoboa/frontend_dist/assets/FiltersView-Dps3_fAh.js +0 -1
  292. modoboa/frontend_dist/assets/ForwardEmailView-C0I00eXf.js +0 -1
  293. modoboa/frontend_dist/assets/IdentitiesView-CoGBVhZx.js +0 -1
  294. modoboa/frontend_dist/assets/InformationView-eB2ayeLD.js +0 -1
  295. modoboa/frontend_dist/assets/MailboxView-DIVABvk5.css +0 -1
  296. modoboa/frontend_dist/assets/MailboxView-DNokO2WZ.js +0 -5
  297. modoboa/frontend_dist/assets/MenuItems-dl38msRA.js +0 -1
  298. modoboa/frontend_dist/assets/MessagesView-BZ7NUOwo.js +0 -1
  299. modoboa/frontend_dist/assets/MigrationsView-DcOVgmcH.js +0 -1
  300. modoboa/frontend_dist/assets/ParametersView-B6abt-LH.js +0 -1
  301. modoboa/frontend_dist/assets/ParametersView-v0YS6WU4.js +0 -1
  302. modoboa/frontend_dist/assets/ProviderEditView-BaL9mKxJ.js +0 -1
  303. modoboa/frontend_dist/assets/ProviderGeneralForm-eoHMDO5v.js +0 -1
  304. modoboa/frontend_dist/assets/ProvidersView-Zu-G35u7.js +0 -1
  305. modoboa/frontend_dist/assets/QuarantineLayout-BHiiksnP.js +0 -1
  306. modoboa/frontend_dist/assets/QuarantineView-BOXSobRz.js +0 -1
  307. modoboa/frontend_dist/assets/ReplyEmailView-JmSiNWPy.js +0 -1
  308. modoboa/frontend_dist/assets/ResourcesForm-D0YBapCG.js +0 -1
  309. modoboa/frontend_dist/assets/SelfServiceLayout-DUmQzUDk.js +0 -1
  310. modoboa/frontend_dist/assets/SettingsView-DIvKGRBY.css +0 -1
  311. modoboa/frontend_dist/assets/SettingsView-DMuaKagM.js +0 -6
  312. modoboa/frontend_dist/assets/StatisticsView-CyY_CNcI.js +0 -1
  313. modoboa/frontend_dist/assets/TimeSerieChart-BTDBH33K.js +0 -1
  314. modoboa/frontend_dist/assets/TimeSerieChart-C3XHmlRd.css +0 -1
  315. modoboa/frontend_dist/assets/UserLayout-DwxcQr4L.js +0 -1
  316. modoboa/frontend_dist/assets/VApp-B0vRc3-6.js +0 -1
  317. modoboa/frontend_dist/assets/VAutocomplete-Huaz_-hf.js +0 -1
  318. modoboa/frontend_dist/assets/VAutocomplete-hzGuLlUI.css +0 -1
  319. modoboa/frontend_dist/assets/VAvatar-JdJfoK0C.js +0 -1
  320. modoboa/frontend_dist/assets/VBadge-Cg5iu5LG.js +0 -1
  321. modoboa/frontend_dist/assets/VCheckbox-CCLOJFKF.js +0 -1
  322. modoboa/frontend_dist/assets/VColorPicker-452uc3tY.js +0 -1
  323. modoboa/frontend_dist/assets/VDataTable-BkLLfcQK.js +0 -1
  324. modoboa/frontend_dist/assets/VDataTableServer-B_6fWBA3.js +0 -1
  325. modoboa/frontend_dist/assets/VDataTableVirtual-Ike7oqcC.js +0 -1
  326. modoboa/frontend_dist/assets/VDialog-DIUeAIpw.js +0 -1
  327. modoboa/frontend_dist/assets/VExpansionPanels-BeMgiS7A.js +0 -1
  328. modoboa/frontend_dist/assets/VFileInput-Dvj95FL5.js +0 -1
  329. modoboa/frontend_dist/assets/VForm-DkphQD3e.js +0 -1
  330. modoboa/frontend_dist/assets/VInput-CbqehdOu.js +0 -1
  331. modoboa/frontend_dist/assets/VMenu-B6SCtOwT.js +0 -1
  332. modoboa/frontend_dist/assets/VMenu-BEipA1lw.css +0 -1
  333. modoboa/frontend_dist/assets/VPicker-BUx70Wjg.js +0 -1
  334. modoboa/frontend_dist/assets/VPicker-ClSXs6kv.css +0 -1
  335. modoboa/frontend_dist/assets/VProgressCircular-DTDGzl2O.js +0 -1
  336. modoboa/frontend_dist/assets/VRadioGroup-CS9ULZ1c.js +0 -1
  337. modoboa/frontend_dist/assets/VSelect-D5IXDPEX.js +0 -1
  338. modoboa/frontend_dist/assets/VSheet-HeHw6g5_.js +0 -1
  339. modoboa/frontend_dist/assets/VSpacer-BUxSSJbH.js +0 -1
  340. modoboa/frontend_dist/assets/VSwitch-Dh_dserW.js +0 -1
  341. modoboa/frontend_dist/assets/VTabs-5XSICLQP.js +0 -1
  342. modoboa/frontend_dist/assets/VTabs-NzpINroH.css +0 -1
  343. modoboa/frontend_dist/assets/VTextField-Cow3HZvI.css +0 -1
  344. modoboa/frontend_dist/assets/VTextField-QXQFhKxm.js +0 -1
  345. modoboa/frontend_dist/assets/VTextarea-Bdy4UKmV.js +0 -1
  346. modoboa/frontend_dist/assets/VTextarea-DyGjqrlm.css +0 -1
  347. modoboa/frontend_dist/assets/VToolbar-CB2GrZpA.css +0 -1
  348. modoboa/frontend_dist/assets/VToolbar-CyrYSqJZ.js +0 -1
  349. modoboa/frontend_dist/assets/VWindowItem-kZpArwK1.js +0 -1
  350. modoboa/frontend_dist/assets/WebmailLayout-BzW0LWYp.css +0 -1
  351. modoboa/frontend_dist/assets/WebmailLayout-CNUHODnw.js +0 -1
  352. modoboa/frontend_dist/assets/domains.store-D4ZqCQ-O.js +0 -1
  353. modoboa/frontend_dist/assets/filter-DHKXX97M.js +0 -1
  354. modoboa/frontend_dist/assets/global.store-DYMmGKpn.js +0 -1
  355. modoboa/frontend_dist/assets/importExport-C-adpHJX.js +0 -1
  356. modoboa/frontend_dist/assets/index-B1bntsLR.js +0 -995
  357. modoboa/frontend_dist/assets/languages-CxjoT69j.js +0 -1
  358. modoboa/frontend_dist/assets/transports-ChdHV5hX.js +0 -1
  359. modoboa/frontend_dist/assets/webmail-B7MNMkv_.js +0 -1
  360. modoboa/frontend_dist/assets/webmail.store-Cm-h6hhE.js +0 -1
  361. {modoboa-2.6.4.data → modoboa-2.7.0.data}/scripts/modoboa-admin.py +0 -0
  362. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/entry_points.txt +0 -0
  363. {modoboa-2.6.4.dist-info → modoboa-2.7.0.dist-info}/licenses/LICENSE +0 -0
  364. {modoboa-2.6.4.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
+ }