modoboa 2.5.1__py3-none-any.whl → 2.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modoboa/admin/api/v2/serializers.py +6 -1
- modoboa/admin/api/v2/tests.py +16 -0
- modoboa/admin/api/v2/viewsets.py +11 -1
- modoboa/admin/app_settings.py +39 -0
- modoboa/admin/lib.py +8 -3
- modoboa/admin/models/alias.py +22 -0
- modoboa/admin/models/domain.py +1 -1
- modoboa/admin/models/mixins.py +3 -1
- modoboa/admin/tests/test_alias_targets.py +68 -0
- modoboa/admin/tests/test_mx.py +12 -12
- modoboa/autoconfig/__init__.py +0 -0
- modoboa/autoconfig/apps.py +6 -0
- modoboa/autoconfig/templates/autoconfig/autoconfig.xml +24 -0
- modoboa/autoconfig/templates/autoconfig/autodiscover.xml +27 -0
- modoboa/autoconfig/tests.py +38 -0
- modoboa/autoconfig/urls.py +21 -0
- modoboa/autoconfig/views.py +109 -0
- modoboa/autoreply/api/v2/tests.py +4 -4
- modoboa/autoreply/models.py +2 -2
- modoboa/calendars/tests.py +8 -8
- modoboa/contacts/tests.py +5 -7
- modoboa/core/commands/templates/settings.py.tpl +25 -2
- modoboa/core/models.py +1 -1
- modoboa/core/tests/test_ldap.py +2 -0
- modoboa/core/views/auth.py +27 -8
- modoboa/frontend_dist/assets/{AccountAliasForm-C0oHHyZL.js → AccountAliasForm-CqtTlaTr.js} +1 -1
- modoboa/frontend_dist/assets/{AccountEditView-lgSJ2Se8.js → AccountEditView-vbG3aNZy.js} +1 -1
- modoboa/frontend_dist/assets/AccountLayout-Dr0kIVYn.js +1 -0
- modoboa/frontend_dist/assets/{AccountPasswordSubForm-YsaE_cDx.js → AccountPasswordSubForm-KRg2Sc3j.js} +1 -1
- modoboa/frontend_dist/assets/{AccountView-1jfKFDwb.js → AccountView-ojD2hYFN.js} +1 -1
- modoboa/frontend_dist/assets/{AddressBook-CwN64Zls.js → AddressBook-UKBKm1mQ.js} +1 -1
- modoboa/frontend_dist/assets/AdminLayout-Dj4_-niL.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-CdHVE0Hh.js +1 -0
- modoboa/frontend_dist/assets/AlarmsView-Dal0VD8U.css +1 -0
- modoboa/frontend_dist/assets/AliasEditView-CYDSiX8l.css +1 -0
- modoboa/frontend_dist/assets/AliasEditView-DmAxOjKB.js +1 -0
- modoboa/frontend_dist/assets/AliasRecipientForm-BrTdirrM.js +1 -0
- modoboa/frontend_dist/assets/{AliasView-DMzA10eD.js → AliasView-Sr8AZcUK.js} +1 -1
- modoboa/frontend_dist/assets/{AuditTrailView-5dnGX5El.js → AuditTrailView-B0U4QZw5.js} +1 -1
- modoboa/frontend_dist/assets/{CalendarView-DZONeDA9.js → CalendarView-5ZhvN1XU.js} +1 -1
- modoboa/frontend_dist/assets/{ChoiceField-DnwXRkht.js → ChoiceField-APLYjZCH.js} +1 -1
- modoboa/frontend_dist/assets/{ComposeEmailForm-kghmfNuE.js → ComposeEmailForm-DkLwcddN.js} +1 -1
- modoboa/frontend_dist/assets/ComposeEmailView-Du9di9mX.js +1 -0
- modoboa/frontend_dist/assets/ConfirmDialog-CnWM982L.js +1 -0
- modoboa/frontend_dist/assets/ConnectedLayout-CiKHST8t.js +1 -0
- modoboa/frontend_dist/assets/{CreationForm-CW4lxnPg.js → CreationForm-BDQpHM1l.js} +1 -1
- modoboa/frontend_dist/assets/DashboardView-nIp1bBh1.js +1 -0
- modoboa/frontend_dist/assets/{DomainAdminList-C3jcDDc3.js → DomainAdminList-CzY3elnc.js} +1 -1
- modoboa/frontend_dist/assets/{DomainEditView-ph8AaElX.js → DomainEditView-B_vQgqL-.js} +1 -1
- modoboa/frontend_dist/assets/{DomainTransportForm-NCz6Bl-h.js → DomainTransportForm-HOsP56gy.js} +1 -1
- modoboa/frontend_dist/assets/{DomainView-BgMSSuU-.js → DomainView-CPDvROg8.js} +3 -3
- modoboa/frontend_dist/assets/DomainsView-WlF6IHNs.css +1 -0
- modoboa/frontend_dist/assets/DomainsView-haCsU12U.js +1 -0
- modoboa/frontend_dist/assets/{EmailField-DeqDPm5j.js → EmailField-Cjy8TitF.js} +1 -1
- modoboa/frontend_dist/assets/{EmailView-DczVhVO0.js → EmailView-dnVoHho7.js} +1 -1
- modoboa/frontend_dist/assets/EmptyLayout-Deh2_dQt.js +1 -0
- modoboa/frontend_dist/assets/{FiltersView-nJj_gSCx.js → FiltersView-Csn8N70V.js} +1 -1
- modoboa/frontend_dist/assets/ForwardEmailView-CUBLqaN5.js +1 -0
- modoboa/frontend_dist/assets/{HtmlEditor-BWRdelVw.js → HtmlEditor-CXZVsQx7.js} +1 -1
- modoboa/frontend_dist/assets/IdentitiesView-Cl2lbfkM.css +1 -0
- modoboa/frontend_dist/assets/IdentitiesView-lIu0dkk7.js +1 -0
- modoboa/frontend_dist/assets/{InformationView-BBWKSX8D.js → InformationView-DQqhhU9V.js} +1 -1
- modoboa/frontend_dist/assets/{LoadingData-G57nJ_JV.js → LoadingData-fbGWs7ji.js} +1 -1
- modoboa/frontend_dist/assets/LoginCallbackView-3Hv94z-r.js +1 -0
- modoboa/frontend_dist/assets/{LoginView-CqCCXYLo.js → LoginView-BUbinhV5.js} +1 -1
- modoboa/frontend_dist/assets/MailboxView-CYPFkxfT.js +1 -0
- modoboa/frontend_dist/assets/{MenuItems-BqIZW5av.js → MenuItems-Dg2GGDq6.js} +1 -1
- modoboa/frontend_dist/assets/{MessageView-D_6tx_gd.js → MessageView-Dz7CnObf.js} +1 -1
- modoboa/frontend_dist/assets/{MessagesView-BH7JIR03.js → MessagesView-BJAKsy9_.js} +1 -1
- modoboa/frontend_dist/assets/{MigrationsView-Cv_So9T-.js → MigrationsView-6k4E9C53.js} +1 -1
- modoboa/frontend_dist/assets/ParametersForm-Bfv6vx6R.js +1 -0
- modoboa/frontend_dist/assets/ParametersForm-t6-3GMDS.css +1 -0
- modoboa/frontend_dist/assets/ParametersView-BOIkKJtp.js +1 -0
- modoboa/frontend_dist/assets/ParametersView-BcyDgfqX.css +1 -0
- modoboa/frontend_dist/assets/ParametersView-BnQQOUOX.js +1 -0
- modoboa/frontend_dist/assets/{ProviderEditView-zh7CY832.js → ProviderEditView-DIc_KMp4.js} +1 -1
- modoboa/frontend_dist/assets/{ProviderGeneralForm-BQU7t3ma.js → ProviderGeneralForm-VpYAY2D4.js} +1 -1
- modoboa/frontend_dist/assets/{ProvidersView-CoF_ZkZA.js → ProvidersView-CgNIFDfD.js} +1 -1
- modoboa/frontend_dist/assets/QuarantineLayout-aTeFWNaK.js +1 -0
- modoboa/frontend_dist/assets/{QuarantineView-DNvpoycb.js → QuarantineView-n30IFBjf.js} +1 -1
- modoboa/frontend_dist/assets/ReplyEmailView-To3fx256.js +1 -0
- modoboa/frontend_dist/assets/ResourcesForm-Dy_SbQPT.js +1 -0
- modoboa/frontend_dist/assets/SelfServiceLayout-CsOvD1_L.js +1 -0
- modoboa/frontend_dist/assets/{SettingsView-gQiJ2NVb.js → SettingsView-oiWlLNP2.js} +2 -2
- modoboa/frontend_dist/assets/{StatisticsView-DYalet_q.js → StatisticsView-CTON4fgZ.js} +1 -1
- modoboa/frontend_dist/assets/{TimeSerieChart-BZ2htbFk.js → TimeSerieChart-DR0-ujj7.js} +1 -1
- modoboa/frontend_dist/assets/{TimeSerieChart-CxiwMzE8.css → TimeSerieChart-Dn6Wznn8.css} +1 -1
- modoboa/frontend_dist/assets/UserLayout-BBz6nSHf.js +1 -0
- modoboa/frontend_dist/assets/{VAlert-4r6LxKtg.js → VAlert-BWUQvs3i.js} +1 -1
- modoboa/frontend_dist/assets/{VApp-CX_C7AUN.js → VApp-1CFpPM0s.js} +1 -1
- modoboa/frontend_dist/assets/{VAutocomplete-DNKmBvyZ.js → VAutocomplete-vTx-9foZ.js} +1 -1
- modoboa/frontend_dist/assets/{VAvatar-DbuoZWmf.js → VAvatar-Wz1UmZ2b.js} +1 -1
- modoboa/frontend_dist/assets/{VBadge-Bv2nvUmC.js → VBadge-C08Ii4Nb.js} +1 -1
- modoboa/frontend_dist/assets/{VCard-DzjUT5OP.js → VCard-BrQzTMsm.js} +1 -1
- modoboa/frontend_dist/assets/{VCheckbox-dr7UFjl4.js → VCheckbox-IdHLHDAe.js} +1 -1
- modoboa/frontend_dist/assets/{VCheckboxBtn-CpFdBnTv.js → VCheckboxBtn-BMf4a9DP.js} +1 -1
- modoboa/frontend_dist/assets/{VChip-CaQvfmkw.js → VChip-B1E7xQW1.js} +1 -1
- modoboa/frontend_dist/assets/VColorPicker-CuNM9hII.js +1 -0
- modoboa/frontend_dist/assets/{VContainer-74Dnn8Ux.js → VContainer-f9kXL-37.js} +1 -1
- modoboa/frontend_dist/assets/{VDataTable-CL7yHvG7.js → VDataTable-CVJjOdhF.js} +1 -1
- modoboa/frontend_dist/assets/{VDataTableServer-BqvNcIdw.js → VDataTableServer-BSZUbic1.js} +1 -1
- modoboa/frontend_dist/assets/{VDataTableVirtual--KsOP8i6.js → VDataTableVirtual-Ceu9uaGm.js} +1 -1
- modoboa/frontend_dist/assets/{VDialog-DmTGCGR0.js → VDialog-BF9xR7BQ.js} +1 -1
- modoboa/frontend_dist/assets/{VExpansionPanels-B7sSTCwd.js → VExpansionPanels-Rq8tzNGV.js} +1 -1
- modoboa/frontend_dist/assets/{VFileInput-SULIc6F3.js → VFileInput-CXyx9q4a.js} +1 -1
- modoboa/frontend_dist/assets/{VForm-DsRLc-sa.js → VForm-RVvaatfV.js} +1 -1
- modoboa/frontend_dist/assets/{VInput-DVKUObZe.js → VInput-ssnfbpwr.js} +1 -1
- modoboa/frontend_dist/assets/{VMenu-nv0XOgg0.js → VMenu-BPG7b7dT.js} +1 -1
- modoboa/frontend_dist/assets/{VPicker-DnDSWJHJ.js → VPicker-Cv_7ON7Q.js} +1 -1
- modoboa/frontend_dist/assets/{VProgressCircular-qK6p5X_Y.js → VProgressCircular-Bdwj7JuI.js} +1 -1
- modoboa/frontend_dist/assets/{VRadioGroup-CbiPLy0t.js → VRadioGroup-BdEZyWNi.js} +1 -1
- modoboa/frontend_dist/assets/{VRow-DJ0NB63-.js → VRow-DGPh2Qmm.js} +1 -1
- modoboa/frontend_dist/assets/{VSelect-CxCFMHyF.js → VSelect-CzmrBwOO.js} +1 -1
- modoboa/frontend_dist/assets/{VSelectionControl-C-6A4us5.js → VSelectionControl-BIXXUgD4.js} +1 -1
- modoboa/frontend_dist/assets/{VSheet-DI6SxLnG.js → VSheet-C07oo1bg.js} +1 -1
- modoboa/frontend_dist/assets/VSpacer-Pa16wvUF.js +1 -0
- modoboa/frontend_dist/assets/{VSwitch-DPnjPQuU.js → VSwitch-C8Eq78Y3.js} +1 -1
- modoboa/frontend_dist/assets/{VTable-ldTxgQPW.js → VTable-C08Q6ok2.js} +1 -1
- modoboa/frontend_dist/assets/{VTabs-aS8WSL9I.js → VTabs-CaqxP8RF.js} +1 -1
- modoboa/frontend_dist/assets/{VTextField-DKbr4H5w.js → VTextField-BBIziqiv.js} +1 -1
- modoboa/frontend_dist/assets/{VTextarea-BttkFsM4.js → VTextarea-PURjBZRs.js} +1 -1
- modoboa/frontend_dist/assets/{VToolbar-BxX3W2kR.js → VToolbar-CILwTR5O.js} +1 -1
- modoboa/frontend_dist/assets/VWindowItem-l3wU5Kto.js +1 -0
- modoboa/frontend_dist/assets/WebmailLayout-uF8MMy0l.js +1 -0
- modoboa/frontend_dist/assets/{accounts-CC2F0a0c.js → accounts-DkNidbb0.js} +1 -1
- modoboa/frontend_dist/assets/{admin-CHCHFGI6.js → admin-C5XWZaxP.js} +1 -1
- modoboa/frontend_dist/assets/{aliases-C9bUD4Ws.js → aliases-eOyMwyZV.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-DCVJxuui.js → amavis-BOgLfXLH.js} +1 -1
- modoboa/frontend_dist/assets/{amavis-BhzV4rgf.js → amavis-w7390k8i.js} +1 -1
- modoboa/frontend_dist/assets/{contacts-Dxz6eWpf.js → contacts-Ch-bKxJM.js} +1 -1
- modoboa/frontend_dist/assets/domains-DEEhRTFJ.js +1 -0
- modoboa/frontend_dist/assets/{domains.store-BLKRipG8.js → domains.store-BhRRATEd.js} +1 -1
- modoboa/frontend_dist/assets/{filter-rmxrcjKk.js → filter-CfvL3jIf.js} +1 -1
- modoboa/frontend_dist/assets/{forwardRefs-CpzzjgpX.js → forwardRefs-GE5Osi9V.js} +1 -1
- modoboa/frontend_dist/assets/{global.store-DndbMXYb.js → global.store-CxPrtY0_.js} +1 -1
- modoboa/frontend_dist/assets/importExport-C1_milrO.js +1 -0
- modoboa/frontend_dist/assets/index-D9OmnhdE.js +982 -0
- modoboa/frontend_dist/assets/{layout-DbjDe3Wl.js → layout-CuwY6Rle.js} +1 -1
- modoboa/frontend_dist/assets/{layout.store-Vq5mvIp7.js → layout.store-uEYOXtgD.js} +1 -1
- modoboa/frontend_dist/assets/{logos-Bvcy0usu.js → logos-BOVDV-my.js} +1 -1
- modoboa/frontend_dist/assets/{logs-BuItINky.js → logs-DT7l7Npx.js} +1 -1
- modoboa/frontend_dist/assets/{parameters-C8IYEP7q.js → parameters-DlTg3gyZ.js} +1 -1
- modoboa/frontend_dist/assets/{parameters.store-1cwSP2JP.js → parameters.store-eTs-B-pe.js} +1 -1
- modoboa/frontend_dist/assets/{permissions-DQjAcO9S.js → permissions-B-P8ks2E.js} +1 -1
- modoboa/frontend_dist/assets/{ssrBoot-BxIQ9ccA.js → ssrBoot-DaeAGyhc.js} +1 -1
- modoboa/frontend_dist/assets/{tag-3cfI1_f7.js → tag-jImTUiWx.js} +1 -1
- modoboa/frontend_dist/assets/theme-tqh89n3L.js +1 -0
- modoboa/frontend_dist/assets/transports-BnIcB3M9.js +1 -0
- modoboa/frontend_dist/assets/{webmail-B2IUjaxM.js → webmail-Dqo8FjAw.js} +1 -1
- modoboa/frontend_dist/index.html +1 -1
- modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
- modoboa/locale/ja_JP/LC_MESSAGES/django.po +1 -1
- modoboa/relaydomains/tests.py +1 -1
- modoboa/static/css/offline.css +7 -20
- modoboa/templates/registration/base.html +18 -0
- modoboa/templates/registration/login.html +1 -1
- modoboa/urls.py +1 -5
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/METADATA +9 -9
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/RECORD +164 -156
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/licenses/LICENSE +1 -1
- modoboa/core/api/v1/tests.py +0 -26
- modoboa/frontend_dist/assets/AccountLayout-U386K8zy.js +0 -1
- modoboa/frontend_dist/assets/AdminLayout-Cxm1lggg.js +0 -1
- modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +0 -1
- modoboa/frontend_dist/assets/AlarmsView-Bcjsicac.js +0 -1
- modoboa/frontend_dist/assets/AliasEditView-Cx9410JP.css +0 -1
- modoboa/frontend_dist/assets/AliasEditView-k3rVt1tG.js +0 -1
- modoboa/frontend_dist/assets/AliasRecipientForm-IehUzKok.js +0 -1
- modoboa/frontend_dist/assets/ComposeEmailView-DLv3wk1k.js +0 -1
- modoboa/frontend_dist/assets/ConfirmDialog-CcPrCKuI.js +0 -1
- modoboa/frontend_dist/assets/ConnectedLayout-CWlBK7Hf.js +0 -1
- modoboa/frontend_dist/assets/DashboardView-DXVZMbMo.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-CEEU9btK.js +0 -1
- modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +0 -1
- modoboa/frontend_dist/assets/EmptyLayout-BXgcfMLH.js +0 -1
- modoboa/frontend_dist/assets/ForwardEmailView-Bgv3JQb6.js +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-DPrrRMS5.css +0 -1
- modoboa/frontend_dist/assets/IdentitiesView-Dld9IloZ.js +0 -1
- modoboa/frontend_dist/assets/LoginCallbackView-DjyE2SG_.js +0 -1
- modoboa/frontend_dist/assets/MailboxView-DRrs9eLO.js +0 -1
- modoboa/frontend_dist/assets/ParametersForm-3qXttTuQ.js +0 -1
- modoboa/frontend_dist/assets/ParametersForm-Dk-qwlRm.css +0 -1
- modoboa/frontend_dist/assets/ParametersView-3Ns04cpQ.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-B5B5Dt6K.js +0 -1
- modoboa/frontend_dist/assets/ParametersView-DaKFNERg.css +0 -1
- modoboa/frontend_dist/assets/QuarantineLayout-CYBsrbJM.js +0 -1
- modoboa/frontend_dist/assets/ReplyEmailView-D1XTcglu.js +0 -1
- modoboa/frontend_dist/assets/ResourcesForm-BW8rUGgZ.js +0 -1
- modoboa/frontend_dist/assets/SelfServiceLayout-DfDHiYeX.js +0 -1
- modoboa/frontend_dist/assets/UserLayout-zUtHi-z-.js +0 -1
- modoboa/frontend_dist/assets/VColorPicker-ByGpCW5O.js +0 -1
- modoboa/frontend_dist/assets/VSpacer-CoJVmx8k.js +0 -1
- modoboa/frontend_dist/assets/VWindowItem-Cvqvdegd.js +0 -1
- modoboa/frontend_dist/assets/WebmailLayout-BT2k6U7q.js +0 -1
- modoboa/frontend_dist/assets/domains-C2cornvL.js +0 -1
- modoboa/frontend_dist/assets/importExport-C3uqrcok.js +0 -1
- modoboa/frontend_dist/assets/index-LhNzkzAh.js +0 -984
- modoboa/frontend_dist/assets/transports-D4Jk4-AP.js +0 -1
- {modoboa-2.5.1.data → modoboa-2.6.1.data}/scripts/modoboa-admin.py +0 -0
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/WHEEL +0 -0
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/entry_points.txt +0 -0
- {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/top_level.txt +0 -0
|
@@ -244,6 +244,11 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
|
|
|
244
244
|
auto_create_domain_and_mailbox = serializers.BooleanField(default=True)
|
|
245
245
|
create_alias_on_mbox_rename = serializers.BooleanField(default=False)
|
|
246
246
|
|
|
247
|
+
# Aliases settings
|
|
248
|
+
alias_can_target_any_domain = serializers.BooleanField(default=True)
|
|
249
|
+
alias_target_block_list = serializers.CharField(required=False, allow_null=True)
|
|
250
|
+
alias_target_allow_list = serializers.CharField(required=False, allow_null=True)
|
|
251
|
+
|
|
247
252
|
def validate_default_domain_quota(self, value):
|
|
248
253
|
"""Ensure quota is a positive integer."""
|
|
249
254
|
if value < 0:
|
|
@@ -745,7 +750,7 @@ class CSVImportSerializer(serializers.Serializer):
|
|
|
745
750
|
class CSVIdentityImportSerializer(CSVImportSerializer):
|
|
746
751
|
"""Custom serializer for identity import."""
|
|
747
752
|
|
|
748
|
-
crypt_passwords = serializers.BooleanField()
|
|
753
|
+
crypt_passwords = serializers.BooleanField(default=False)
|
|
749
754
|
|
|
750
755
|
|
|
751
756
|
class AlarmSerializer(serializers.ModelSerializer):
|
modoboa/admin/api/v2/tests.py
CHANGED
|
@@ -653,6 +653,22 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
|
|
|
653
653
|
received_content[0] = ",".join(admin_row)
|
|
654
654
|
self.assertCountEqual(expected_response.strip().split("\r\n"), received_content)
|
|
655
655
|
|
|
656
|
+
def test_export_accounts(self):
|
|
657
|
+
response = self.client.get(f"{reverse('v2:identities-export')}?type=account")
|
|
658
|
+
expected_response = "account,admin,,,,True,SuperAdmins,,\r\naccount,admin@test.com,{PLAIN}toto,,,True,DomainAdmins,admin@test.com,10,test.com\r\naccount,admin@test2.com,{PLAIN}toto,,,True,DomainAdmins,admin@test2.com,10,test2.com\r\naccount,user@test.com,{PLAIN}toto,,,True,SimpleUsers,user@test.com,10\r\naccount,user@test2.com,{PLAIN}toto,,,True,SimpleUsers,user@test2.com,10\r\n" # NOQA:E501
|
|
659
|
+
received_content = force_str(response.content.strip()).split("\r\n")
|
|
660
|
+
# Empty admin password because it is hashed using SHA512-CRYPT
|
|
661
|
+
admin_row = received_content[0].split(",")
|
|
662
|
+
admin_row[2] = ""
|
|
663
|
+
received_content[0] = ",".join(admin_row)
|
|
664
|
+
self.assertCountEqual(expected_response.strip().split("\r\n"), received_content)
|
|
665
|
+
|
|
666
|
+
def test_export_aliases(self):
|
|
667
|
+
response = self.client.get(f"{reverse('v2:identities-export')}?type=alias")
|
|
668
|
+
expected_response = "alias,alias@test.com,True,user@test.com\r\nalias,forward@test.com,True,user@external.com\r\nalias,postmaster@test.com,True,test@truc.fr,toto@titi.com\r\n" # NOQA:E501
|
|
669
|
+
received_content = force_str(response.content.strip()).split("\r\n")
|
|
670
|
+
self.assertCountEqual(expected_response.strip().split("\r\n"), received_content)
|
|
671
|
+
|
|
656
672
|
|
|
657
673
|
class AliasViewSetTestCase(ModoAPITestCase):
|
|
658
674
|
@classmethod
|
modoboa/admin/api/v2/viewsets.py
CHANGED
|
@@ -58,10 +58,19 @@ class DomainViewSet(
|
|
|
58
58
|
):
|
|
59
59
|
"""V2 viewset."""
|
|
60
60
|
|
|
61
|
+
filter_backends = (
|
|
62
|
+
filters.OrderingFilter,
|
|
63
|
+
filters.SearchFilter,
|
|
64
|
+
dj_filters.DjangoFilterBackend,
|
|
65
|
+
)
|
|
66
|
+
pagination_class = pagination.CustomPageNumberPagination
|
|
61
67
|
permission_classes = (
|
|
62
68
|
permissions.IsAuthenticated,
|
|
63
69
|
permissions.DjangoModelPermissions,
|
|
64
70
|
)
|
|
71
|
+
search_fields = [
|
|
72
|
+
"^name",
|
|
73
|
+
]
|
|
65
74
|
|
|
66
75
|
def get_queryset(self):
|
|
67
76
|
"""Filter queryset based on current user."""
|
|
@@ -240,7 +249,8 @@ class IdentityViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
|
|
|
240
249
|
def export(self, request, **kwargs):
|
|
241
250
|
"""Export accounts and aliases to CSV."""
|
|
242
251
|
result = []
|
|
243
|
-
|
|
252
|
+
idtfilter = request.GET.get("type")
|
|
253
|
+
for idt in lib.get_identities(request.user, idtfilter=idtfilter):
|
|
244
254
|
result.append(idt.to_csv_row())
|
|
245
255
|
return response.Response(result)
|
|
246
256
|
|
modoboa/admin/app_settings.py
CHANGED
|
@@ -239,6 +239,45 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
|
|
|
239
239
|
),
|
|
240
240
|
},
|
|
241
241
|
),
|
|
242
|
+
(
|
|
243
|
+
"aliases",
|
|
244
|
+
{
|
|
245
|
+
"label": gettext_lazy("Aliases"),
|
|
246
|
+
"params": collections.OrderedDict(
|
|
247
|
+
[
|
|
248
|
+
(
|
|
249
|
+
"alias_can_target_any_domain",
|
|
250
|
+
{
|
|
251
|
+
"label": gettext_lazy("Alias can target any domain"),
|
|
252
|
+
"help_text": gettext_lazy(
|
|
253
|
+
"Allow aliases to target any domain, not just the local domains."
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
),
|
|
257
|
+
(
|
|
258
|
+
"alias_target_block_list",
|
|
259
|
+
{
|
|
260
|
+
"label": gettext_lazy("Alias target block list"),
|
|
261
|
+
"help_text": gettext_lazy(
|
|
262
|
+
"A list of domains that aliases cannot target (comma separated)."
|
|
263
|
+
),
|
|
264
|
+
"display": "alias_can_target_any_domain=true",
|
|
265
|
+
},
|
|
266
|
+
),
|
|
267
|
+
(
|
|
268
|
+
"alias_target_allow_list",
|
|
269
|
+
{
|
|
270
|
+
"label": gettext_lazy("Alias target allow list"),
|
|
271
|
+
"help_text": gettext_lazy(
|
|
272
|
+
"A list of domains that aliases can target (comma separated)."
|
|
273
|
+
),
|
|
274
|
+
"display": "alias_can_target_any_domain=false",
|
|
275
|
+
},
|
|
276
|
+
),
|
|
277
|
+
]
|
|
278
|
+
),
|
|
279
|
+
},
|
|
280
|
+
),
|
|
242
281
|
]
|
|
243
282
|
)
|
|
244
283
|
|
modoboa/admin/lib.py
CHANGED
|
@@ -31,7 +31,12 @@ from . import signals
|
|
|
31
31
|
from .models import Alias, Domain, DomainAlias
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def get_identities(
|
|
34
|
+
def get_identities(
|
|
35
|
+
user: User,
|
|
36
|
+
searchquery: str | None = None,
|
|
37
|
+
idtfilter: str | None = None,
|
|
38
|
+
grpfilter: str | None = None,
|
|
39
|
+
) -> chain:
|
|
35
40
|
"""Return all the identities owned by a user.
|
|
36
41
|
|
|
37
42
|
:param user: the desired user
|
|
@@ -213,7 +218,7 @@ def get_authoritative_resolver(domain, resolver=None):
|
|
|
213
218
|
|
|
214
219
|
def get_dns_records(name, typ, resolver=None):
|
|
215
220
|
"""Retrieve DNS records for given name and type."""
|
|
216
|
-
logger = logging.getLogger("modoboa.
|
|
221
|
+
logger = logging.getLogger("modoboa.dns")
|
|
217
222
|
|
|
218
223
|
try:
|
|
219
224
|
resolver = get_authoritative_resolver(name, resolver=resolver)
|
|
@@ -242,7 +247,7 @@ def get_dns_records(name, typ, resolver=None):
|
|
|
242
247
|
def get_domain_mx_list(domain):
|
|
243
248
|
"""Return a list of MX IP address for domain."""
|
|
244
249
|
result = []
|
|
245
|
-
logger = logging.getLogger("modoboa.
|
|
250
|
+
logger = logging.getLogger("modoboa.dns")
|
|
246
251
|
resolver = get_dns_resolver()
|
|
247
252
|
dns_answers = get_dns_records(domain, "MX", resolver)
|
|
248
253
|
if dns_answers is None:
|
modoboa/admin/models/alias.py
CHANGED
|
@@ -21,6 +21,7 @@ from modoboa.lib.exceptions import (
|
|
|
21
21
|
ModoboaException,
|
|
22
22
|
AliasExists,
|
|
23
23
|
)
|
|
24
|
+
from modoboa.parameters import tools as param_tools
|
|
24
25
|
from .. import signals
|
|
25
26
|
from .base import AdminObject
|
|
26
27
|
from .domain import Domain
|
|
@@ -90,6 +91,25 @@ class AliasManager(models.Manager):
|
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def convert_to_list(raw_value: str) -> list[str]:
|
|
95
|
+
"""Convert input string to list of string."""
|
|
96
|
+
return [item.strip() for item in raw_value.split(",")]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_alias_target_allowed(domain: str) -> bool:
|
|
100
|
+
"""Check if alias target is allowed or not."""
|
|
101
|
+
params = dict(param_tools.get_global_parameters(app="admin"))
|
|
102
|
+
if params.get("alias_can_target_any_domain"):
|
|
103
|
+
blocked_domains = params.get("alias_target_block_list")
|
|
104
|
+
if blocked_domains and domain in convert_to_list(blocked_domains):
|
|
105
|
+
return False
|
|
106
|
+
else:
|
|
107
|
+
allowed_domains = params.get("alias_target_allow_list")
|
|
108
|
+
if allowed_domains and domain not in convert_to_list(allowed_domains):
|
|
109
|
+
return False
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
93
113
|
class Alias(AdminObject):
|
|
94
114
|
"""Mailbox alias."""
|
|
95
115
|
|
|
@@ -182,6 +202,8 @@ class Alias(AdminObject):
|
|
|
182
202
|
if domname is None:
|
|
183
203
|
raise BadRequest("{} {}".format(_("Invalid address"), address))
|
|
184
204
|
domain = Domain.objects.filter(name=domname).first()
|
|
205
|
+
if not domain and not is_alias_target_allowed(domname):
|
|
206
|
+
raise BadRequest(f"{_('Target domain not allowed')}: {domname}")
|
|
185
207
|
kwargs = {"address": address, "alias": self}
|
|
186
208
|
if (domain is not None) and (
|
|
187
209
|
any(
|
modoboa/admin/models/domain.py
CHANGED
|
@@ -202,7 +202,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject):
|
|
|
202
202
|
return self.dnsrecord_set.filter(type="autodiscover").first()
|
|
203
203
|
|
|
204
204
|
@cached_property
|
|
205
|
-
def allocated_quota(self):
|
|
205
|
+
def allocated_quota(self) -> int:
|
|
206
206
|
"""Return current quota allocation."""
|
|
207
207
|
if not self.quota:
|
|
208
208
|
return 0
|
modoboa/admin/models/mixins.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Admin model mixins."""
|
|
2
2
|
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
3
5
|
from modoboa.policyd.utils import get_message_counter
|
|
4
6
|
|
|
5
7
|
|
|
@@ -18,7 +20,7 @@ class MessageLimitMixin:
|
|
|
18
20
|
return self.message_limit - get_message_counter(self.message_counter_key)
|
|
19
21
|
|
|
20
22
|
@property
|
|
21
|
-
def sent_messages_in_percent(self):
|
|
23
|
+
def sent_messages_in_percent(self) -> Optional[int]: # noqa
|
|
22
24
|
"""Return number of sent messages as a percentage."""
|
|
23
25
|
if not self.message_limit:
|
|
24
26
|
return None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tests for alias target allow/block settings."""
|
|
2
|
+
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
|
|
5
|
+
from modoboa.admin import factories, models
|
|
6
|
+
from modoboa.lib.tests import ModoAPITestCase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AliasTargetSettingsTestCase(ModoAPITestCase):
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def setUpTestData(cls): # NOQA:N802
|
|
13
|
+
super().setUpTestData()
|
|
14
|
+
factories.populate_database()
|
|
15
|
+
|
|
16
|
+
def test_block_external_domain_when_any_allowed_with_block_list(self):
|
|
17
|
+
# alias_can_target_any_domain = True and block external domain
|
|
18
|
+
self.set_global_parameter("alias_can_target_any_domain", True, app="admin")
|
|
19
|
+
self.set_global_parameter("alias_target_block_list", "bad.com", app="admin")
|
|
20
|
+
|
|
21
|
+
values = {
|
|
22
|
+
"address": "list@test.com",
|
|
23
|
+
"recipients": ["user@bad.com"],
|
|
24
|
+
"enabled": True,
|
|
25
|
+
}
|
|
26
|
+
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
|
|
27
|
+
# Creation should fail
|
|
28
|
+
self.assertEqual(resp.status_code, 400)
|
|
29
|
+
self.assertEqual(resp.json()["error"], "Target domain not allowed: bad.com")
|
|
30
|
+
|
|
31
|
+
def test_allow_only_list_when_any_disallowed(self):
|
|
32
|
+
# alias_can_target_any_domain = False and allow list contains domain
|
|
33
|
+
self.set_global_parameter("alias_can_target_any_domain", False, app="admin")
|
|
34
|
+
self.set_global_parameter("alias_target_allow_list", "ok.com", app="admin")
|
|
35
|
+
|
|
36
|
+
# Check if local domain is still allowed
|
|
37
|
+
values = {
|
|
38
|
+
"address": "list2@test.com",
|
|
39
|
+
"recipients": ["user@test.com"],
|
|
40
|
+
"enabled": True,
|
|
41
|
+
}
|
|
42
|
+
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
|
|
43
|
+
self.assertEqual(resp.status_code, 201)
|
|
44
|
+
|
|
45
|
+
values = {
|
|
46
|
+
"address": "list3@test.com",
|
|
47
|
+
"recipients": ["user@ok.com"],
|
|
48
|
+
"enabled": True,
|
|
49
|
+
}
|
|
50
|
+
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
|
|
51
|
+
self.assertEqual(resp.status_code, 201)
|
|
52
|
+
alias = models.Alias.objects.get(address="list3@test.com", internal=False)
|
|
53
|
+
# Allowed external domain should remain external
|
|
54
|
+
self.assertTrue(
|
|
55
|
+
alias.aliasrecipient_set.filter(
|
|
56
|
+
address="user@ok.com", r_mailbox__isnull=True, r_alias__isnull=True
|
|
57
|
+
).exists()
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Now try a domain not in allow list, should stay external as well
|
|
61
|
+
values = {
|
|
62
|
+
"address": "list4@test.com",
|
|
63
|
+
"recipients": ["user@bad.com"],
|
|
64
|
+
"enabled": True,
|
|
65
|
+
}
|
|
66
|
+
resp = self.client.post(reverse("v2:alias-list"), values, format="json")
|
|
67
|
+
self.assertEqual(resp.status_code, 400)
|
|
68
|
+
self.assertEqual(resp.json()["error"], "Target domain not allowed: bad.com")
|
modoboa/admin/tests/test_mx.py
CHANGED
|
@@ -138,7 +138,7 @@ class MXTestCase(ModoTestCase):
|
|
|
138
138
|
"""Test to get error loggins from specific DNS server."""
|
|
139
139
|
mock_query.side_effect = utils.mock_dns_query_result
|
|
140
140
|
mock_ip_address.side_effect = utils.mock_ip_address_result
|
|
141
|
-
with LogCapture("modoboa.
|
|
141
|
+
with LogCapture("modoboa.dns") as log:
|
|
142
142
|
get_domain_mx_list("does-not-exist.example.com")
|
|
143
143
|
get_domain_mx_list("no-mx.example.com")
|
|
144
144
|
get_domain_mx_list("no-ns-servers.example.com")
|
|
@@ -148,36 +148,36 @@ class MXTestCase(ModoTestCase):
|
|
|
148
148
|
get_domain_mx_list("bad-response.example.com")
|
|
149
149
|
log.check(
|
|
150
150
|
(
|
|
151
|
-
"modoboa.
|
|
151
|
+
"modoboa.dns",
|
|
152
152
|
"ERROR",
|
|
153
153
|
_("No DNS record found for %s") % "does-not-exist.example.com",
|
|
154
154
|
),
|
|
155
|
-
("modoboa.
|
|
156
|
-
("modoboa.
|
|
155
|
+
("modoboa.dns", "ERROR", _("No MX record for %s") % "no-mx.example.com"),
|
|
156
|
+
("modoboa.dns", "ERROR", _("No working name servers found")),
|
|
157
157
|
(
|
|
158
|
-
"modoboa.
|
|
158
|
+
"modoboa.dns",
|
|
159
159
|
"WARNING",
|
|
160
160
|
_("DNS resolution timeout, unable to query %s at the moment")
|
|
161
161
|
% "timeout.example.com",
|
|
162
162
|
),
|
|
163
163
|
(
|
|
164
|
-
"modoboa.
|
|
164
|
+
"modoboa.dns",
|
|
165
165
|
"ERROR",
|
|
166
166
|
_("No DNS record found for %s") % "does-not-exist.example.com",
|
|
167
167
|
),
|
|
168
168
|
(
|
|
169
|
-
"modoboa.
|
|
169
|
+
"modoboa.dns",
|
|
170
170
|
"ERROR",
|
|
171
171
|
_("No DNS record found for %s") % "does-not-exist.example.com",
|
|
172
172
|
),
|
|
173
173
|
(
|
|
174
|
-
"modoboa.
|
|
174
|
+
"modoboa.dns",
|
|
175
175
|
"WARNING",
|
|
176
176
|
_("Invalid IP address format for %(domain)s; %(addr)s")
|
|
177
177
|
% {"domain": "bad-response.example.com", "addr": "000.0.0.0"},
|
|
178
178
|
),
|
|
179
179
|
(
|
|
180
|
-
"modoboa.
|
|
180
|
+
"modoboa.dns",
|
|
181
181
|
"WARNING",
|
|
182
182
|
_("Invalid IP address format for %(domain)s; %(addr)s")
|
|
183
183
|
% {"domain": "bad-response.example.com", "addr": "000.0.0.0"},
|
|
@@ -193,15 +193,15 @@ class MXTestCase(ModoTestCase):
|
|
|
193
193
|
localconfig = core_models.LocalConfig.objects.first()
|
|
194
194
|
localconfig.parameters.set_value("enable_ipv6_mx_checks", True, "admin")
|
|
195
195
|
localconfig.save()
|
|
196
|
-
with LogCapture("modoboa.
|
|
196
|
+
with LogCapture("modoboa.dns") as log:
|
|
197
197
|
get_domain_mx_list("test3.com")
|
|
198
198
|
log.check(
|
|
199
|
-
("modoboa.
|
|
199
|
+
("modoboa.dns", "ERROR", _("No AAAA record for %s") % "mx3.example.com")
|
|
200
200
|
)
|
|
201
201
|
localconfig = core_models.LocalConfig.objects.first()
|
|
202
202
|
localconfig.parameters.set_value("enable_ipv6_mx_checks", False, "admin")
|
|
203
203
|
localconfig.save()
|
|
204
|
-
with LogCapture("modoboa.
|
|
204
|
+
with LogCapture("modoboa.dns") as log2:
|
|
205
205
|
get_domain_mx_list("test3.com")
|
|
206
206
|
log2.check()
|
|
207
207
|
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<clientConfig version="1.1">
|
|
3
|
+
<emailProvider id="{{ domain }}">
|
|
4
|
+
<domain>{{ domain }}</domain>
|
|
5
|
+
<displayName>Modoboa</displayName>
|
|
6
|
+
<displayShortName>Mail</displayShortName>
|
|
7
|
+
|
|
8
|
+
<incomingServer type="imap">
|
|
9
|
+
<hostname>{{ connection_settings.imap.HOSTNAME }}</hostname>
|
|
10
|
+
<port>{{ connection_settings.imap.PORT }}</port>
|
|
11
|
+
<socketType>{{ connection_settings.imap.SOCKET_TYPE }}</socketType>
|
|
12
|
+
<authentication>plain</authentication>
|
|
13
|
+
<username>{{ emailaddress }}</username>
|
|
14
|
+
</incomingServer>
|
|
15
|
+
|
|
16
|
+
<outgoingServer type="smtp">
|
|
17
|
+
<hostname>{{ connection_settings.smtp.HOSTNAME }}</hostname>
|
|
18
|
+
<port>{{ connection_settings.smtp.PORT }}</port>
|
|
19
|
+
<socketType>{{ connection_settings.smtp.SOCKET_TYPE }}</socketType>
|
|
20
|
+
<authentication>plain</authentication>
|
|
21
|
+
<username>{{ emailaddress }}</username>
|
|
22
|
+
</outgoingServer>
|
|
23
|
+
</emailProvider>
|
|
24
|
+
</clientConfig>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
|
3
|
+
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
|
4
|
+
<Account>
|
|
5
|
+
<AccountType>email</AccountType>
|
|
6
|
+
<Action>settings</Action>
|
|
7
|
+
|
|
8
|
+
<Protocol>
|
|
9
|
+
<Type>IMAP</Type>
|
|
10
|
+
<Server>{{ connection_settings.imap.HOSTNAME }}</Server>
|
|
11
|
+
<Port>{{ connection_settings.imap.PORT }}</Port>
|
|
12
|
+
<SSL>true</SSL>
|
|
13
|
+
<LoginName>{{ emailaddress }}</LoginName>
|
|
14
|
+
</Protocol>
|
|
15
|
+
|
|
16
|
+
<Protocol>
|
|
17
|
+
<Type>SMTP</Type>
|
|
18
|
+
<Server>{{ connection_settings.smtp.HOSTNAME }}</Server>
|
|
19
|
+
<Port>{{ connection_settings.smtp.PORT }}</Port>
|
|
20
|
+
<SSL>false</SSL>
|
|
21
|
+
<TLS>true</TLS>
|
|
22
|
+
<LoginName>{{ emailaddress }}</LoginName>
|
|
23
|
+
</Protocol>
|
|
24
|
+
|
|
25
|
+
</Account>
|
|
26
|
+
</Response>
|
|
27
|
+
</Autodiscover>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from django.urls import reverse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ViewsTestCase(TestCase):
|
|
6
|
+
|
|
7
|
+
databases = "__all__"
|
|
8
|
+
|
|
9
|
+
def test_autoconfig(self):
|
|
10
|
+
url = reverse("autoconfig:autoconfig")
|
|
11
|
+
|
|
12
|
+
resp = self.client.get(url)
|
|
13
|
+
self.assertEqual(resp.status_code, 404)
|
|
14
|
+
|
|
15
|
+
resp = self.client.get(f"{url}?emailaddress=test@test.com")
|
|
16
|
+
self.assertEqual(resp.status_code, 200)
|
|
17
|
+
|
|
18
|
+
self.assertIn(b'<emailProvider id="test.com">', resp.content)
|
|
19
|
+
|
|
20
|
+
def test_autodiscover(self):
|
|
21
|
+
url = reverse("autoconfig:autodiscover")
|
|
22
|
+
|
|
23
|
+
resp = self.client.post(url)
|
|
24
|
+
self.assertEqual(resp.status_code, 404)
|
|
25
|
+
|
|
26
|
+
resp = self.client.post(url, {"EmailAddress": "test@test.com"})
|
|
27
|
+
self.assertEqual(resp.status_code, 200)
|
|
28
|
+
|
|
29
|
+
self.assertIn(b"<LoginName>test@test.com", resp.content)
|
|
30
|
+
|
|
31
|
+
def test_mobileconfig(self):
|
|
32
|
+
url = reverse("autoconfig:mobileconfig")
|
|
33
|
+
|
|
34
|
+
resp = self.client.get(url)
|
|
35
|
+
self.assertEqual(resp.status_code, 404)
|
|
36
|
+
|
|
37
|
+
resp = self.client.get(f"{url}?emailaddress=test@test.com")
|
|
38
|
+
self.assertEqual(resp.status_code, 200)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Autoconfig urls."""
|
|
2
|
+
|
|
3
|
+
from django.urls import path
|
|
4
|
+
|
|
5
|
+
from modoboa.autoconfig import views
|
|
6
|
+
|
|
7
|
+
app_name = "autoconfig"
|
|
8
|
+
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path(
|
|
11
|
+
"mail/config-v1.1.xml",
|
|
12
|
+
views.AutoConfigView.as_view(),
|
|
13
|
+
name="autoconfig",
|
|
14
|
+
),
|
|
15
|
+
path(
|
|
16
|
+
"autodiscover/autodiscover.xml",
|
|
17
|
+
views.AutoDiscoverView.as_view(),
|
|
18
|
+
name="autodiscover",
|
|
19
|
+
),
|
|
20
|
+
path("mobileconfig", views.MobileConfigView.as_view(), name="mobileconfig"),
|
|
21
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import plistlib
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.http import Http404, HttpResponse
|
|
6
|
+
from django.utils.decorators import method_decorator
|
|
7
|
+
from django.views import generic
|
|
8
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
9
|
+
|
|
10
|
+
from modoboa.lib.email_utils import split_address
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigBaseMixin:
|
|
14
|
+
|
|
15
|
+
content_type = "application/xml"
|
|
16
|
+
|
|
17
|
+
def get_common_context(self, emailaddress: str) -> dict:
|
|
18
|
+
local_part, domain = split_address(emailaddress)
|
|
19
|
+
return {
|
|
20
|
+
"emailaddress": emailaddress,
|
|
21
|
+
"domain": domain,
|
|
22
|
+
"connection_settings": settings.EMAIL_CLIENT_CONNECTION_SETTINGS,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AutoConfigView(ConfigBaseMixin, generic.TemplateView):
|
|
27
|
+
|
|
28
|
+
http_method_names = ["get"]
|
|
29
|
+
template_name = "autoconfig/autoconfig.xml"
|
|
30
|
+
|
|
31
|
+
def get_context_data(self, **kwargs):
|
|
32
|
+
emailaddress = self.request.GET.get("emailaddress")
|
|
33
|
+
if not emailaddress:
|
|
34
|
+
raise Http404
|
|
35
|
+
context = super().get_context_data(**kwargs)
|
|
36
|
+
context.update(self.get_common_context(emailaddress))
|
|
37
|
+
return context
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@method_decorator(csrf_exempt, name="dispatch")
|
|
41
|
+
class AutoDiscoverView(ConfigBaseMixin, generic.TemplateView):
|
|
42
|
+
|
|
43
|
+
http_method_names = ["post"]
|
|
44
|
+
template_name = "autoconfig/autodiscover.xml"
|
|
45
|
+
|
|
46
|
+
def get_context_data(self, **kwargs):
|
|
47
|
+
emailaddress = self.request.POST.get("EmailAddress")
|
|
48
|
+
if not emailaddress:
|
|
49
|
+
raise Http404
|
|
50
|
+
context = super().get_context_data(**kwargs)
|
|
51
|
+
context.update(self.get_common_context(emailaddress))
|
|
52
|
+
return context
|
|
53
|
+
|
|
54
|
+
def post(self, request, *args, **kwargs):
|
|
55
|
+
context = self.get_context_data(**kwargs)
|
|
56
|
+
return self.render_to_response(context)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MobileConfigView(generic.View):
|
|
60
|
+
|
|
61
|
+
http_method_names = ["get"]
|
|
62
|
+
|
|
63
|
+
def get(self, request, *args, **kwargs):
|
|
64
|
+
emailaddress = self.request.GET.get("emailaddress")
|
|
65
|
+
if not emailaddress:
|
|
66
|
+
raise Http404
|
|
67
|
+
local_part, domain = split_address(emailaddress)
|
|
68
|
+
parts = domain.split(".")
|
|
69
|
+
parts.reverse()
|
|
70
|
+
reverse_domain = ".".join(parts)
|
|
71
|
+
imap_settings = settings.EMAIL_CLIENT_CONNECTION_SETTINGS["imap"]
|
|
72
|
+
smtp_settings = settings.EMAIL_CLIENT_CONNECTION_SETTINGS["smtp"]
|
|
73
|
+
profile = {
|
|
74
|
+
"PayloadType": "Configuration",
|
|
75
|
+
"PayloadVersion": 1,
|
|
76
|
+
"PayloadIdentifier": f"{reverse_domain}.mailprofile",
|
|
77
|
+
"PayloadUUID": str(uuid.uuid4()),
|
|
78
|
+
"PayloadDisplayName": f"{domain} Mail Configuration",
|
|
79
|
+
"PayloadOrganization": domain,
|
|
80
|
+
"PayloadContent": [
|
|
81
|
+
{
|
|
82
|
+
"PayloadType": "com.apple.mail.managed",
|
|
83
|
+
"PayloadVersion": 1,
|
|
84
|
+
"PayloadIdentifier": f"{reverse_domain}.mailprofile.mail",
|
|
85
|
+
"PayloadUUID": str(uuid.uuid4()),
|
|
86
|
+
"PayloadDisplayName": "Mail",
|
|
87
|
+
"EmailAccountDescription": f"{domain} Mail",
|
|
88
|
+
"EmailAccountType": "EmailTypeIMAP",
|
|
89
|
+
"EmailAddress": emailaddress,
|
|
90
|
+
# incoming
|
|
91
|
+
"IncomingMailServerHostName": imap_settings["HOSTNAME"],
|
|
92
|
+
"IncomingMailServerPortNumber": imap_settings["PORT"],
|
|
93
|
+
"IncomingMailServerUseSSL": True,
|
|
94
|
+
"IncomingMailServerUsername": emailaddress,
|
|
95
|
+
# outgoing
|
|
96
|
+
"OutgoingMailServerHostName": smtp_settings["HOSTNAME"],
|
|
97
|
+
"OutgoingMailServerPortNumber": smtp_settings["PORT"],
|
|
98
|
+
"OutgoingMailServerUseSSL": True,
|
|
99
|
+
"OutgoingMailServerUsername": emailaddress,
|
|
100
|
+
"OutgoingPasswordSameAsIncomingPassword": True,
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
plist_bytes = plistlib.dumps(profile, fmt=plistlib.FMT_XML)
|
|
105
|
+
resp = HttpResponse(
|
|
106
|
+
plist_bytes, content_type="application/x-apple-aspen-config"
|
|
107
|
+
)
|
|
108
|
+
resp["Content-Disposition"] = 'attachment; filename="mail.mobileconfig"'
|
|
109
|
+
return resp
|
|
@@ -62,7 +62,7 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
|
|
|
62
62
|
def test_retrieve_armessage_domadmin(self):
|
|
63
63
|
admin = User.objects.get(username="admin@test.com")
|
|
64
64
|
self.client.logout()
|
|
65
|
-
self.client.
|
|
65
|
+
self.client.force_authenticate(admin)
|
|
66
66
|
url = reverse("api:armessage-list")
|
|
67
67
|
response = self.client.get(url)
|
|
68
68
|
self.assertEqual(response.status_code, 200)
|
|
@@ -74,7 +74,7 @@ class ARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
|
|
|
74
74
|
|
|
75
75
|
def test_retrieve_armessage_simpleuser(self):
|
|
76
76
|
self.client.logout()
|
|
77
|
-
self.client.
|
|
77
|
+
self.client.force_authenticate(self.account)
|
|
78
78
|
url = reverse("api:armessage-list")
|
|
79
79
|
response = self.client.get(url)
|
|
80
80
|
self.assertEqual(response.status_code, 200)
|
|
@@ -156,7 +156,7 @@ class AccountARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
|
|
|
156
156
|
response = self.client.get(url)
|
|
157
157
|
self.assertEqual(response.status_code, 403)
|
|
158
158
|
self.client.logout()
|
|
159
|
-
self.client.
|
|
159
|
+
self.client.force_authenticate(self.account)
|
|
160
160
|
self.assertFalse(
|
|
161
161
|
models.ARmessage.objects.filter(mbox=self.account.mailbox).exists()
|
|
162
162
|
)
|
|
@@ -168,7 +168,7 @@ class AccountARMessageViewSetTestCase(PatcherMixin, ModoAPITestCase):
|
|
|
168
168
|
|
|
169
169
|
def test_set_armessage(self):
|
|
170
170
|
self.client.logout()
|
|
171
|
-
self.client.
|
|
171
|
+
self.client.force_authenticate(self.account)
|
|
172
172
|
url = reverse("api:account_armessage-armessage")
|
|
173
173
|
fromdate = timezone.now()
|
|
174
174
|
todate = fromdate + relativedelta(days=4)
|
modoboa/autoreply/models.py
CHANGED
|
@@ -58,9 +58,9 @@ class ARmessage(models.Model):
|
|
|
58
58
|
name = "autoreply"
|
|
59
59
|
action = [("vacation", ":subject", self.subject, ":days", days, content)]
|
|
60
60
|
if not fset.getfilter(name):
|
|
61
|
-
fset.addfilter(name, condition, action)
|
|
61
|
+
fset.addfilter(name, condition, action, matchtype="allof")
|
|
62
62
|
else:
|
|
63
|
-
fset.updatefilter(name, name, condition, action)
|
|
63
|
+
fset.updatefilter(name, name, condition, action, matchtype="allof")
|
|
64
64
|
for filter in fset.filters:
|
|
65
65
|
if filter["name"] == name:
|
|
66
66
|
filter["description"] = (
|