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.
- modoboa/admin/api/v2/serializers.py +1 -0
- modoboa/admin/app_settings.py +12 -0
- modoboa/admin/{management/commands/subcommands/_mx.py → dns_checker.py} +41 -111
- modoboa/admin/jobs.py +86 -0
- modoboa/admin/management/commands/modo.py +0 -2
- modoboa/admin/tests/test_mailbox_operations.py +4 -4
- modoboa/admin/tests/test_mx.py +68 -56
- modoboa/amavis/jobs.py +11 -0
- modoboa/amavis/tests/test_jobs.py +18 -0
- modoboa/amavis/tests/test_viewsets.py +2 -3
- modoboa/autoconfig/templates/autoconfig/autoconfig.xml +2 -2
- modoboa/autoconfig/templates/autoconfig/autodiscover.xml +14 -0
- modoboa/autoconfig/tests.py +2 -1
- modoboa/autoconfig/views.py +11 -3
- modoboa/calendars/backends/caldav_.py +17 -13
- modoboa/calendars/jobs.py +7 -0
- modoboa/calendars/mocks.py +4 -1
- modoboa/calendars/serializers.py +14 -9
- modoboa/calendars/tests.py +10 -9
- modoboa/calendars/viewsets.py +3 -1
- modoboa/contacts/migrations/0008_addressbook_syncing.py +18 -0
- modoboa/contacts/models.py +1 -0
- modoboa/contacts/serializers.py +5 -2
- modoboa/contacts/tasks.py +9 -3
- modoboa/contacts/tests.py +32 -6
- modoboa/contacts/viewsets.py +7 -1
- modoboa/core/api/v2/serializers.py +0 -7
- modoboa/core/api/v2/tests.py +0 -10
- modoboa/core/app_settings.py +0 -22
- modoboa/core/commands/deploy.py +13 -0
- modoboa/core/commands/templates/cron_config.py.tpl +33 -0
- modoboa/core/commands/templates/settings.py.tpl +21 -0
- modoboa/core/jobs.py +34 -0
- modoboa/core/management/commands/load_initial_data.py +1 -1
- modoboa/core/password_hashers/base.py +4 -1
- modoboa/core/tests/test_core.py +0 -14
- modoboa/core/tests/test_jobs.py +40 -0
- modoboa/frontend_dist/assets/{AccountAliasForm-B-hTKSFr.js → AccountAliasForm-DO6DwfjE.js} +1 -1
- modoboa/frontend_dist/assets/{AccountEditView-LkP_qhf_.js → AccountEditView-CCuN9mGB.js} +1 -1
- modoboa/frontend_dist/assets/AccountLayout-Ge7fzuZg.js +1 -0
- modoboa/frontend_dist/assets/AccountPasswordSubForm-PkFPblkR.js +1 -0
- modoboa/frontend_dist/assets/AccountView-dgseekZ8.js +1 -0
- modoboa/frontend_dist/assets/AddressBook-DZWOHOJj.js +1 -0
- modoboa/frontend_dist/assets/AdminLayout-itB_mmH_.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-King6zb6.js +1 -0
- modoboa/frontend_dist/assets/{AliasEditView-BVjNJekt.js → AliasEditView-CTjrXYPf.js} +1 -1
- modoboa/frontend_dist/assets/AliasRecipientForm-CI7bXqVp.js +1 -0
- modoboa/frontend_dist/assets/{AliasView-DvvI6cV9.js → AliasView-BN13MNN_.js} +1 -1
- modoboa/frontend_dist/assets/AuditTrailView-C1jQwgoo.js +1 -0
- modoboa/frontend_dist/assets/CalendarView-juHVIHU5.css +1 -0
- modoboa/frontend_dist/assets/CalendarView-rRSzqxrH.js +1 -0
- modoboa/frontend_dist/assets/ChoiceField-DCr12shR.js +1 -0
- modoboa/frontend_dist/assets/ComposeEmailForm-BmtKwFb1.js +2 -0
- modoboa/frontend_dist/assets/ComposeEmailForm-CVNDl-Mq.css +1 -0
- modoboa/frontend_dist/assets/ComposeEmailView-JW22Phrb.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-CWpdwSQ6.js +1 -0
- modoboa/frontend_dist/assets/ConnectedLayout-BP8pO27H.js +1 -0
- modoboa/frontend_dist/assets/{ConnectedLayout-C6HNXWkp.css → ConnectedLayout-Ddpb_6yT.css} +1 -1
- modoboa/frontend_dist/assets/CreationForm-CzelJsVQ.js +1 -0
- modoboa/frontend_dist/assets/DashboardView-BBkBodVj.js +1 -0
- modoboa/frontend_dist/assets/{DomainAdminList-BO8_4Cwt.js → DomainAdminList-DD7p3i6F.js} +1 -1
- modoboa/frontend_dist/assets/{DomainEditView-BXylM4aB.js → DomainEditView-DoIoNYC4.js} +1 -1
- modoboa/frontend_dist/assets/{DomainTransportForm-Ccw5BXyM.js → DomainTransportForm-CbiJF9z5.js} +1 -1
- modoboa/frontend_dist/assets/{DomainView-Bd5XQI-1.js → DomainView-BXvznFYz.js} +3 -3
- modoboa/frontend_dist/assets/DomainsView-ffYjiffp.js +1 -0
- modoboa/frontend_dist/assets/EmailField-KhhJYA4D.js +1 -0
- modoboa/frontend_dist/assets/EmailSchedulingForm-CQL5Vfdr.js +1 -0
- modoboa/frontend_dist/assets/EmailView-Bq2bHZBO.js +1 -0
- modoboa/frontend_dist/assets/EmptyLayout-NrTftp18.js +1 -0
- modoboa/frontend_dist/assets/FiltersView-SOQy0U_3.js +1 -0
- modoboa/frontend_dist/assets/ForwardEmailView-kEPDjWUw.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-DNn3CY-w.js → HtmlEditor-CyBl5wj2.js} +15 -15
- modoboa/frontend_dist/assets/IdentitiesView-CZFf4oR9.js +1 -0
- modoboa/frontend_dist/assets/InformationView-CAtneAqM.js +1 -0
- modoboa/frontend_dist/assets/{LoadingData-CnQ_aVqa.js → LoadingData-C2txD49L.js} +1 -1
- modoboa/frontend_dist/assets/{LoginCallbackView-Nh_gmh8e.js → LoginCallbackView-DAvty3CI.js} +1 -1
- modoboa/frontend_dist/assets/{LoginView-BtC9BHuJ.js → LoginView-DHYITkn4.js} +1 -1
- modoboa/frontend_dist/assets/MailboxView-BNg2v7mi.css +1 -0
- modoboa/frontend_dist/assets/MailboxView-DHg5A78D.js +5 -0
- modoboa/frontend_dist/assets/MenuItems-C9p70bSC.js +1 -0
- modoboa/frontend_dist/assets/{MessageView-C1Goz-hY.js → MessageView-CrrEJpNI.js} +1 -1
- modoboa/frontend_dist/assets/MessagesView-CyecRd4I.js +1 -0
- modoboa/frontend_dist/assets/MigrationsView-DXGKKz2h.js +1 -0
- modoboa/frontend_dist/assets/{ParametersForm-BegEnkQM.js → ParametersForm-DFKYkPAs.js} +1 -1
- modoboa/frontend_dist/assets/ParametersView-B1wT5oyt.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-CfvVzKea.js +1 -0
- modoboa/frontend_dist/assets/ProviderEditView-De-8ggBp.js +1 -0
- modoboa/frontend_dist/assets/{ProviderGeneralForm-BAELnXh6.js → ProviderGeneralForm-DO-IpJMQ.js} +1 -1
- modoboa/frontend_dist/assets/ProvidersView-Bhk7sCz4.js +1 -0
- modoboa/frontend_dist/assets/QuarantineLayout-C1YszNmP.js +1 -0
- modoboa/frontend_dist/assets/QuarantineView-cElD_rS-.js +1 -0
- modoboa/frontend_dist/assets/ReplyEmailView-BvuOZdLl.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-FWxrmVwo.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-JHRvfvQf.js +1 -0
- modoboa/frontend_dist/assets/SettingsView-SIsKAXtQ.css +1 -0
- modoboa/frontend_dist/assets/SettingsView-s2l2Xl1L.js +6 -0
- modoboa/frontend_dist/assets/StatisticsView-FiidWvad.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-CdqUHiO_.js +1 -0
- modoboa/frontend_dist/assets/TimeSerieChart-nLIFGI0y.css +1 -0
- modoboa/frontend_dist/assets/UserLayout-DyvI8duf.js +1 -0
- modoboa/frontend_dist/assets/{VAlert-Buv8Z5G4.js → VAlert-MDbeolOo.js} +1 -1
- modoboa/frontend_dist/assets/VApp-DM1KLQfQ.js +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-C9NL5_uo.css +1 -0
- modoboa/frontend_dist/assets/VAutocomplete-CQsWYWNX.js +1 -0
- modoboa/frontend_dist/assets/VAvatar-CXV_FqpP.js +1 -0
- modoboa/frontend_dist/assets/VBadge-7tDx7aI3.js +1 -0
- modoboa/frontend_dist/assets/{VCard-D7AwBs4w.js → VCard-CBtX8JF-.js} +1 -1
- modoboa/frontend_dist/assets/VCheckbox-zY1MApOy.js +1 -0
- modoboa/frontend_dist/assets/{VCheckboxBtn-B5evxN9K.js → VCheckboxBtn-B0mIT3E0.js} +1 -1
- modoboa/frontend_dist/assets/VColorPicker-BbCHvk6K.js +1 -0
- modoboa/frontend_dist/assets/{VColorPicker-B_lVDaYR.css → VColorPicker-C9m8L-6U.css} +1 -1
- modoboa/frontend_dist/assets/{VContainer-DY9021j4.js → VContainer-Cn-vKB3s.js} +1 -1
- modoboa/frontend_dist/assets/VDataTable-CNT9KOSp.js +1 -0
- modoboa/frontend_dist/assets/VDataTableServer-_yn4Ry6U.js +1 -0
- modoboa/frontend_dist/assets/VDataTableVirtual-Csxh3Gp6.js +1 -0
- modoboa/frontend_dist/assets/VDatePicker-_OUDqShN.css +1 -0
- modoboa/frontend_dist/assets/VDatePicker-iFu13xIP.js +2 -0
- modoboa/frontend_dist/assets/VDialog-lKtBdqnp.js +1 -0
- modoboa/frontend_dist/assets/VExpansionPanels-CaIvk9iF.js +1 -0
- modoboa/frontend_dist/assets/VFileInput-C7J_qVmk.js +1 -0
- modoboa/frontend_dist/assets/VForm-fdQ5d-CH.js +1 -0
- modoboa/frontend_dist/assets/VInput-BwHvhzAe.js +1 -0
- modoboa/frontend_dist/assets/VMenu-BpmJf4X2.js +1 -0
- modoboa/frontend_dist/assets/VMenu-C5A_5Hs5.css +1 -0
- modoboa/frontend_dist/assets/VPicker-B7cB3kJg.css +1 -0
- modoboa/frontend_dist/assets/VPicker-BZho70wU.js +1 -0
- modoboa/frontend_dist/assets/VProgressCircular-DRw_-iNj.js +1 -0
- modoboa/frontend_dist/assets/VRadioGroup--eiP5xtJ.js +1 -0
- modoboa/frontend_dist/assets/{VRow-DyK_Mj7R.js → VRow-83Qnr5iB.js} +1 -1
- modoboa/frontend_dist/assets/VSelect-BcmGFGif.js +1 -0
- modoboa/frontend_dist/assets/{VSelectionControl-Dkms9_P5.js → VSelectionControl-Cqi1xt-q.js} +1 -1
- modoboa/frontend_dist/assets/VSheet-C3MaHhtw.js +1 -0
- modoboa/frontend_dist/assets/VSpacer-CuSkdJZL.js +1 -0
- modoboa/frontend_dist/assets/VSwitch-XumUl685.js +1 -0
- modoboa/frontend_dist/assets/{VTable-D8ZwwC_B.js → VTable-f7wcr2AZ.js} +1 -1
- modoboa/frontend_dist/assets/VTabs-CDfdejXj.css +1 -0
- modoboa/frontend_dist/assets/VTabs-y4wNP4im.js +1 -0
- modoboa/frontend_dist/assets/VTextField-BPIvtrn4.js +1 -0
- modoboa/frontend_dist/assets/VTextField-DjbYGlzs.css +1 -0
- modoboa/frontend_dist/assets/VTextarea-B6bGYcC3.js +1 -0
- modoboa/frontend_dist/assets/VTextarea-f6vTjzFy.css +1 -0
- modoboa/frontend_dist/assets/VToolbar-BYDPtwf0.css +1 -0
- modoboa/frontend_dist/assets/VToolbar-CB6wwtYc.js +1 -0
- modoboa/frontend_dist/assets/VWindowItem-CDtLLEkg.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-CQQAolnl.css +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-tosQSHLS.js +1 -0
- modoboa/frontend_dist/assets/{accounts-BZBqbAmd.js → accounts-KUsk6LHW.js} +1 -1
- modoboa/frontend_dist/assets/{admin-uCrKjpln.js → admin-BW9cZW0P.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-CFwFkX45.js → aliases-Ge0hjIsH.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-DCy0U0TD.js → amavis-BbFeFfsk.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-B4ScfQsG.js → amavis-DtuzP_CS.js} +1 -1
- modoboa/frontend_dist/assets/{contacts-DKmh2q-4.js → contacts-DMJlQTe0.js} +1 -1
- modoboa/frontend_dist/assets/{domains-CDE4_LDF.js → domains-Du64lcXT.js} +1 -1
- modoboa/frontend_dist/assets/{domains.store-a9Y5UlDV.js → domains.store-1U61jeCV.js} +1 -1
- modoboa/frontend_dist/assets/events-BM3in65C.js +1 -0
- modoboa/frontend_dist/assets/filter-Dihm6o59.js +1 -0
- modoboa/frontend_dist/assets/importExport-HGcNGWOm.js +1 -0
- modoboa/frontend_dist/assets/{index-B9q1vO3K.css → index-B1EK3MQe.css} +1 -1
- modoboa/frontend_dist/assets/{index-Dwg6jPTX.js → index-Dv00bmw9.js} +1 -1
- modoboa/frontend_dist/assets/{index-BjibLozh.js → index-jui3edpn.js} +47 -41
- modoboa/frontend_dist/assets/{language.store-DGuI8jG0.js → language.store-OcfdXL_-.js} +1 -1
- modoboa/frontend_dist/assets/languages-CF8hxo7x.js +1 -0
- modoboa/frontend_dist/assets/{layout-5PvsmkGR.js → layout-DOO7TRTJ.js} +1 -1
- modoboa/frontend_dist/assets/{layout.store-DJVcXtxk.js → layout.store-C0g-piJn.js} +1 -1
- modoboa/frontend_dist/assets/{logos-CMmd0hOG.js → logos-Dz2Gzei-.js} +1 -1
- modoboa/frontend_dist/assets/{logs-D2GOydPZ.js → logs-CLm32Weu.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-DJdlq9sh.js → parameters-DMIAQ7cd.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-DH3pPlaT.js → parameters.store-DLnFzCwV.js} +1 -1
- modoboa/frontend_dist/assets/{permissions-CgFJFhCp.js → permissions-CrpE0b4w.js} +1 -1
- modoboa/frontend_dist/assets/{ssrBoot-ChTB-PYP.js → ssrBoot-B7cr7q9U.js} +1 -1
- modoboa/frontend_dist/assets/{tag-CvrHE14f.js → tag-WF93n81Q.js} +1 -1
- modoboa/frontend_dist/assets/{theme-DFzlhSh5.js → theme-_0oOYChG.js} +1 -1
- modoboa/frontend_dist/assets/transports-CS61syt-.js +1 -0
- modoboa/frontend_dist/assets/webmail-CYDXU0DS.js +1 -0
- modoboa/frontend_dist/assets/webmail.store-BvHCQSjM.js +1 -0
- modoboa/frontend_dist/index.html +2 -2
- modoboa/imap_migration/models.py +3 -3
- modoboa/imap_migration/templates/imap_migration/offlineimap.conf +2 -2
- modoboa/imap_migration/tests.py +21 -0
- modoboa/lib/cryptutils.py +2 -2
- modoboa/lib/sysutils.py +1 -1
- modoboa/lib/tests/__init__.py +0 -1
- modoboa/locale/br/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/br/LC_MESSAGES/django.po +301 -273
- modoboa/locale/cs/LC_MESSAGES/django.po +286 -269
- modoboa/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/cs_CZ/LC_MESSAGES/django.po +303 -275
- modoboa/locale/de/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de/LC_MESSAGES/django.po +297 -269
- modoboa/locale/de_DE/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/de_DE/LC_MESSAGES/django.po +303 -275
- modoboa/locale/el_GR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/el_GR/LC_MESSAGES/django.po +303 -275
- modoboa/locale/en/LC_MESSAGES/django.po +286 -269
- modoboa/locale/es/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/es/LC_MESSAGES/django.po +302 -274
- modoboa/locale/es_MX/LC_MESSAGES/django.po +286 -269
- modoboa/locale/fi/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fi/LC_MESSAGES/django.po +299 -271
- modoboa/locale/fr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/fr/LC_MESSAGES/django.po +288 -271
- modoboa/locale/hu/LC_MESSAGES/django.po +286 -269
- modoboa/locale/it/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/it/LC_MESSAGES/django.po +303 -275
- modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ja_JP/LC_MESSAGES/django.po +390 -413
- modoboa/locale/ka/LC_MESSAGES/django.po +286 -269
- modoboa/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/nl_NL/LC_MESSAGES/django.po +299 -271
- modoboa/locale/no/LC_MESSAGES/django.po +286 -269
- modoboa/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pl_PL/LC_MESSAGES/django.po +297 -269
- modoboa/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_BR/LC_MESSAGES/django.po +298 -270
- modoboa/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/pt_PT/LC_MESSAGES/django.po +303 -275
- modoboa/locale/ro_RO/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ro_RO/LC_MESSAGES/django.po +299 -271
- modoboa/locale/ru/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ru/LC_MESSAGES/django.po +993 -1096
- modoboa/locale/si/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sk/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sk_SK/LC_MESSAGES/django.po +286 -269
- modoboa/locale/sl_SI/LC_MESSAGES/django.po +288 -269
- modoboa/locale/sv/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/sv/LC_MESSAGES/django.po +297 -269
- modoboa/locale/tr/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/tr/LC_MESSAGES/django.po +301 -272
- modoboa/locale/tr_TR/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/tr_TR/LC_MESSAGES/django.po +291 -269
- modoboa/locale/uk/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh_CN/LC_MESSAGES/django.po +286 -269
- modoboa/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/zh_TW/LC_MESSAGES/django.po +295 -273
- modoboa/maillog/jobs.py +11 -0
- modoboa/maillog/tests/test_views.py +5 -4
- modoboa/parameters/api/v2/tests.py +1 -1
- modoboa/policyd/management/commands/policy_daemon.py +5 -1
- modoboa/policyd/tests.py +4 -2
- modoboa/rspamd/tests.py +20 -0
- modoboa/templates/registration/twofactor_code_verify.html +1 -1
- modoboa/webmail/app_settings.py +37 -0
- modoboa/webmail/constants.py +21 -1
- modoboa/webmail/factories.py +19 -0
- modoboa/webmail/jobs.py +27 -0
- modoboa/webmail/lib/__init__.py +0 -2
- modoboa/webmail/lib/imapemail.py +9 -11
- modoboa/webmail/lib/imapheader.py +25 -13
- modoboa/webmail/lib/imaputils.py +51 -13
- modoboa/webmail/lib/sendmail.py +88 -105
- modoboa/webmail/lib/utils.py +109 -0
- modoboa/webmail/migrations/0001_initial.py +90 -0
- modoboa/webmail/migrations/__init__.py +0 -0
- modoboa/webmail/mocks.py +12 -3
- modoboa/webmail/models.py +102 -0
- modoboa/webmail/serializers.py +84 -4
- modoboa/webmail/tests/data.py +21 -0
- modoboa/webmail/tests/test_lib_imaputils.py +33 -0
- modoboa/webmail/tests/test_viewsets.py +108 -0
- modoboa/webmail/urls.py +5 -0
- modoboa/webmail/viewsets.py +39 -9
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/METADATA +16 -13
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/RECORD +269 -252
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/WHEEL +1 -1
- modoboa/admin/management/commands/handle_mailbox_operations.py +0 -103
- modoboa/core/management/commands/cleanlogs.py +0 -53
- modoboa/frontend_dist/assets/AccountLayout-APeC9bsN.js +0 -1
- modoboa/frontend_dist/assets/AccountPasswordSubForm-D4vB2yQK.js +0 -1
- modoboa/frontend_dist/assets/AccountView-CloBP1BY.js +0 -1
- modoboa/frontend_dist/assets/AddressBook-C03MXg3y.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-DFmyKOo6.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-CFhH0I5d.js +0 -1
- modoboa/frontend_dist/assets/AliasRecipientForm-CZ_SvjtG.js +0 -1
- modoboa/frontend_dist/assets/AuditTrailView-Cyq25Jle.js +0 -1
- modoboa/frontend_dist/assets/CalendarView-C_dSXRp9.js +0 -1
- modoboa/frontend_dist/assets/ChoiceField-BPhUvoyr.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-D3yUbBJX.css +0 -1
- modoboa/frontend_dist/assets/ComposeEmailForm-Ns5SYs4J.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-DgUfiWEH.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-BMIg94-U.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-CSXdVJtV.js +0 -1
- modoboa/frontend_dist/assets/CreationForm-CaAZYATm.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-DErmyUoy.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-CC1EwRId.js +0 -1
- modoboa/frontend_dist/assets/EmailField-BFgga4zp.js +0 -1
- modoboa/frontend_dist/assets/EmailView-DPWTch_6.js +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-DAw7fAab.js +0 -1
- modoboa/frontend_dist/assets/FiltersView-Cg9lkLw0.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-Dd1sxgaP.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-CiHIQ9Qt.js +0 -1
- modoboa/frontend_dist/assets/InformationView-Cc0aBA5j.js +0 -1
- modoboa/frontend_dist/assets/MailboxView-DIVABvk5.css +0 -1
- modoboa/frontend_dist/assets/MailboxView-DplC0IXR.js +0 -5
- modoboa/frontend_dist/assets/MenuItems-efx2rqwT.js +0 -1
- modoboa/frontend_dist/assets/MessagesView-CesYYZ9f.js +0 -1
- modoboa/frontend_dist/assets/MigrationsView-DSBk8T0y.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-B8W_PAiz.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-Dsx2M_g9.js +0 -1
- modoboa/frontend_dist/assets/ProviderEditView-PEAr7MA1.js +0 -1
- modoboa/frontend_dist/assets/ProvidersView-CHvQf3Vv.js +0 -1
- modoboa/frontend_dist/assets/QuarantineLayout-BWEaBK48.js +0 -1
- modoboa/frontend_dist/assets/QuarantineView-BzjeDhTm.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-CwQ9jsce.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-noC3AXd9.js +0 -1
- modoboa/frontend_dist/assets/SelfServiceLayout-DQgRMu0L.js +0 -1
- modoboa/frontend_dist/assets/SettingsView-DJTdIeRK.css +0 -1
- modoboa/frontend_dist/assets/SettingsView-Dgwpfd8O.js +0 -6
- modoboa/frontend_dist/assets/StatisticsView-BZkVSh5o.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-BXJwhTMO.js +0 -1
- modoboa/frontend_dist/assets/TimeSerieChart-C3XHmlRd.css +0 -1
- modoboa/frontend_dist/assets/UserLayout-CWK9ICfE.js +0 -1
- modoboa/frontend_dist/assets/VApp-CiIBWB_Q.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-DcNqTIie.js +0 -1
- modoboa/frontend_dist/assets/VAutocomplete-hzGuLlUI.css +0 -1
- modoboa/frontend_dist/assets/VAvatar-BsHplcRP.js +0 -1
- modoboa/frontend_dist/assets/VBadge-CavP_E2g.js +0 -1
- modoboa/frontend_dist/assets/VCheckbox-CKW9lz5i.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-Bsrz8yif.js +0 -1
- modoboa/frontend_dist/assets/VDataTable-ocZo8ju0.js +0 -1
- modoboa/frontend_dist/assets/VDataTableServer-CJfpYXQW.js +0 -1
- modoboa/frontend_dist/assets/VDataTableVirtual-Cr8yw49k.js +0 -1
- modoboa/frontend_dist/assets/VDialog-CUMh3paI.js +0 -1
- modoboa/frontend_dist/assets/VExpansionPanels-CkwHiKsA.js +0 -1
- modoboa/frontend_dist/assets/VFileInput-D5qlksIG.js +0 -1
- modoboa/frontend_dist/assets/VForm-ClJU5x_v.js +0 -1
- modoboa/frontend_dist/assets/VInput-DhzSMFXh.js +0 -1
- modoboa/frontend_dist/assets/VMenu-B53a3gm6.js +0 -1
- modoboa/frontend_dist/assets/VMenu-BEipA1lw.css +0 -1
- modoboa/frontend_dist/assets/VPicker-B928ZVJQ.js +0 -1
- modoboa/frontend_dist/assets/VPicker-ClSXs6kv.css +0 -1
- modoboa/frontend_dist/assets/VProgressCircular-viI3jDXe.js +0 -1
- modoboa/frontend_dist/assets/VRadioGroup-C0bqFrYg.js +0 -1
- modoboa/frontend_dist/assets/VSelect-y0E5_vPn.js +0 -1
- modoboa/frontend_dist/assets/VSheet-DQkh_hsX.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-CBqB9uSt.js +0 -1
- modoboa/frontend_dist/assets/VSwitch-D4xdR9jz.js +0 -1
- modoboa/frontend_dist/assets/VTabs-D2c0KeF2.js +0 -1
- modoboa/frontend_dist/assets/VTabs-NzpINroH.css +0 -1
- modoboa/frontend_dist/assets/VTextField-Cow3HZvI.css +0 -1
- modoboa/frontend_dist/assets/VTextField-DvvH1ciR.js +0 -1
- modoboa/frontend_dist/assets/VTextarea-DHtXtzqP.js +0 -1
- modoboa/frontend_dist/assets/VTextarea-DyGjqrlm.css +0 -1
- modoboa/frontend_dist/assets/VToolbar-CB2GrZpA.css +0 -1
- modoboa/frontend_dist/assets/VToolbar-pvLQtmbU.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-DErNPSH8.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-BzW0LWYp.css +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-D7J-QJ-0.js +0 -1
- modoboa/frontend_dist/assets/filter-BVvMTmPG.js +0 -1
- modoboa/frontend_dist/assets/global.store-Bha4Z76j.js +0 -1
- modoboa/frontend_dist/assets/importExport-EgijWlC1.js +0 -1
- modoboa/frontend_dist/assets/languages-Cge6pECg.js +0 -1
- modoboa/frontend_dist/assets/transports-BUSYxSF4.js +0 -1
- modoboa/frontend_dist/assets/webmail-HVg3ZHC0.js +0 -1
- modoboa/frontend_dist/assets/webmail.store-D-d88s4w.js +0 -1
- {modoboa-2.6.5.data → modoboa-2.7.0.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/entry_points.txt +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/licenses/LICENSE +0 -0
- {modoboa-2.6.5.dist-info → modoboa-2.7.0.dist-info}/top_level.txt +0 -0
modoboa/webmail/lib/sendmail.py
CHANGED
|
@@ -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
|
|
18
|
-
from modoboa.webmail.
|
|
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()
|
modoboa/webmail/lib/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|