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.
Files changed (202) hide show
  1. modoboa/admin/api/v2/serializers.py +6 -1
  2. modoboa/admin/api/v2/tests.py +16 -0
  3. modoboa/admin/api/v2/viewsets.py +11 -1
  4. modoboa/admin/app_settings.py +39 -0
  5. modoboa/admin/lib.py +8 -3
  6. modoboa/admin/models/alias.py +22 -0
  7. modoboa/admin/models/domain.py +1 -1
  8. modoboa/admin/models/mixins.py +3 -1
  9. modoboa/admin/tests/test_alias_targets.py +68 -0
  10. modoboa/admin/tests/test_mx.py +12 -12
  11. modoboa/autoconfig/__init__.py +0 -0
  12. modoboa/autoconfig/apps.py +6 -0
  13. modoboa/autoconfig/templates/autoconfig/autoconfig.xml +24 -0
  14. modoboa/autoconfig/templates/autoconfig/autodiscover.xml +27 -0
  15. modoboa/autoconfig/tests.py +38 -0
  16. modoboa/autoconfig/urls.py +21 -0
  17. modoboa/autoconfig/views.py +109 -0
  18. modoboa/autoreply/api/v2/tests.py +4 -4
  19. modoboa/autoreply/models.py +2 -2
  20. modoboa/calendars/tests.py +8 -8
  21. modoboa/contacts/tests.py +5 -7
  22. modoboa/core/commands/templates/settings.py.tpl +25 -2
  23. modoboa/core/models.py +1 -1
  24. modoboa/core/tests/test_ldap.py +2 -0
  25. modoboa/core/views/auth.py +27 -8
  26. modoboa/frontend_dist/assets/{AccountAliasForm-C0oHHyZL.js → AccountAliasForm-CqtTlaTr.js} +1 -1
  27. modoboa/frontend_dist/assets/{AccountEditView-lgSJ2Se8.js → AccountEditView-vbG3aNZy.js} +1 -1
  28. modoboa/frontend_dist/assets/AccountLayout-Dr0kIVYn.js +1 -0
  29. modoboa/frontend_dist/assets/{AccountPasswordSubForm-YsaE_cDx.js → AccountPasswordSubForm-KRg2Sc3j.js} +1 -1
  30. modoboa/frontend_dist/assets/{AccountView-1jfKFDwb.js → AccountView-ojD2hYFN.js} +1 -1
  31. modoboa/frontend_dist/assets/{AddressBook-CwN64Zls.js → AddressBook-UKBKm1mQ.js} +1 -1
  32. modoboa/frontend_dist/assets/AdminLayout-Dj4_-niL.js +1 -0
  33. modoboa/frontend_dist/assets/AlarmsView-CdHVE0Hh.js +1 -0
  34. modoboa/frontend_dist/assets/AlarmsView-Dal0VD8U.css +1 -0
  35. modoboa/frontend_dist/assets/AliasEditView-CYDSiX8l.css +1 -0
  36. modoboa/frontend_dist/assets/AliasEditView-DmAxOjKB.js +1 -0
  37. modoboa/frontend_dist/assets/AliasRecipientForm-BrTdirrM.js +1 -0
  38. modoboa/frontend_dist/assets/{AliasView-DMzA10eD.js → AliasView-Sr8AZcUK.js} +1 -1
  39. modoboa/frontend_dist/assets/{AuditTrailView-5dnGX5El.js → AuditTrailView-B0U4QZw5.js} +1 -1
  40. modoboa/frontend_dist/assets/{CalendarView-DZONeDA9.js → CalendarView-5ZhvN1XU.js} +1 -1
  41. modoboa/frontend_dist/assets/{ChoiceField-DnwXRkht.js → ChoiceField-APLYjZCH.js} +1 -1
  42. modoboa/frontend_dist/assets/{ComposeEmailForm-kghmfNuE.js → ComposeEmailForm-DkLwcddN.js} +1 -1
  43. modoboa/frontend_dist/assets/ComposeEmailView-Du9di9mX.js +1 -0
  44. modoboa/frontend_dist/assets/ConfirmDialog-CnWM982L.js +1 -0
  45. modoboa/frontend_dist/assets/ConnectedLayout-CiKHST8t.js +1 -0
  46. modoboa/frontend_dist/assets/{CreationForm-CW4lxnPg.js → CreationForm-BDQpHM1l.js} +1 -1
  47. modoboa/frontend_dist/assets/DashboardView-nIp1bBh1.js +1 -0
  48. modoboa/frontend_dist/assets/{DomainAdminList-C3jcDDc3.js → DomainAdminList-CzY3elnc.js} +1 -1
  49. modoboa/frontend_dist/assets/{DomainEditView-ph8AaElX.js → DomainEditView-B_vQgqL-.js} +1 -1
  50. modoboa/frontend_dist/assets/{DomainTransportForm-NCz6Bl-h.js → DomainTransportForm-HOsP56gy.js} +1 -1
  51. modoboa/frontend_dist/assets/{DomainView-BgMSSuU-.js → DomainView-CPDvROg8.js} +3 -3
  52. modoboa/frontend_dist/assets/DomainsView-WlF6IHNs.css +1 -0
  53. modoboa/frontend_dist/assets/DomainsView-haCsU12U.js +1 -0
  54. modoboa/frontend_dist/assets/{EmailField-DeqDPm5j.js → EmailField-Cjy8TitF.js} +1 -1
  55. modoboa/frontend_dist/assets/{EmailView-DczVhVO0.js → EmailView-dnVoHho7.js} +1 -1
  56. modoboa/frontend_dist/assets/EmptyLayout-Deh2_dQt.js +1 -0
  57. modoboa/frontend_dist/assets/{FiltersView-nJj_gSCx.js → FiltersView-Csn8N70V.js} +1 -1
  58. modoboa/frontend_dist/assets/ForwardEmailView-CUBLqaN5.js +1 -0
  59. modoboa/frontend_dist/assets/{HtmlEditor-BWRdelVw.js → HtmlEditor-CXZVsQx7.js} +1 -1
  60. modoboa/frontend_dist/assets/IdentitiesView-Cl2lbfkM.css +1 -0
  61. modoboa/frontend_dist/assets/IdentitiesView-lIu0dkk7.js +1 -0
  62. modoboa/frontend_dist/assets/{InformationView-BBWKSX8D.js → InformationView-DQqhhU9V.js} +1 -1
  63. modoboa/frontend_dist/assets/{LoadingData-G57nJ_JV.js → LoadingData-fbGWs7ji.js} +1 -1
  64. modoboa/frontend_dist/assets/LoginCallbackView-3Hv94z-r.js +1 -0
  65. modoboa/frontend_dist/assets/{LoginView-CqCCXYLo.js → LoginView-BUbinhV5.js} +1 -1
  66. modoboa/frontend_dist/assets/MailboxView-CYPFkxfT.js +1 -0
  67. modoboa/frontend_dist/assets/{MenuItems-BqIZW5av.js → MenuItems-Dg2GGDq6.js} +1 -1
  68. modoboa/frontend_dist/assets/{MessageView-D_6tx_gd.js → MessageView-Dz7CnObf.js} +1 -1
  69. modoboa/frontend_dist/assets/{MessagesView-BH7JIR03.js → MessagesView-BJAKsy9_.js} +1 -1
  70. modoboa/frontend_dist/assets/{MigrationsView-Cv_So9T-.js → MigrationsView-6k4E9C53.js} +1 -1
  71. modoboa/frontend_dist/assets/ParametersForm-Bfv6vx6R.js +1 -0
  72. modoboa/frontend_dist/assets/ParametersForm-t6-3GMDS.css +1 -0
  73. modoboa/frontend_dist/assets/ParametersView-BOIkKJtp.js +1 -0
  74. modoboa/frontend_dist/assets/ParametersView-BcyDgfqX.css +1 -0
  75. modoboa/frontend_dist/assets/ParametersView-BnQQOUOX.js +1 -0
  76. modoboa/frontend_dist/assets/{ProviderEditView-zh7CY832.js → ProviderEditView-DIc_KMp4.js} +1 -1
  77. modoboa/frontend_dist/assets/{ProviderGeneralForm-BQU7t3ma.js → ProviderGeneralForm-VpYAY2D4.js} +1 -1
  78. modoboa/frontend_dist/assets/{ProvidersView-CoF_ZkZA.js → ProvidersView-CgNIFDfD.js} +1 -1
  79. modoboa/frontend_dist/assets/QuarantineLayout-aTeFWNaK.js +1 -0
  80. modoboa/frontend_dist/assets/{QuarantineView-DNvpoycb.js → QuarantineView-n30IFBjf.js} +1 -1
  81. modoboa/frontend_dist/assets/ReplyEmailView-To3fx256.js +1 -0
  82. modoboa/frontend_dist/assets/ResourcesForm-Dy_SbQPT.js +1 -0
  83. modoboa/frontend_dist/assets/SelfServiceLayout-CsOvD1_L.js +1 -0
  84. modoboa/frontend_dist/assets/{SettingsView-gQiJ2NVb.js → SettingsView-oiWlLNP2.js} +2 -2
  85. modoboa/frontend_dist/assets/{StatisticsView-DYalet_q.js → StatisticsView-CTON4fgZ.js} +1 -1
  86. modoboa/frontend_dist/assets/{TimeSerieChart-BZ2htbFk.js → TimeSerieChart-DR0-ujj7.js} +1 -1
  87. modoboa/frontend_dist/assets/{TimeSerieChart-CxiwMzE8.css → TimeSerieChart-Dn6Wznn8.css} +1 -1
  88. modoboa/frontend_dist/assets/UserLayout-BBz6nSHf.js +1 -0
  89. modoboa/frontend_dist/assets/{VAlert-4r6LxKtg.js → VAlert-BWUQvs3i.js} +1 -1
  90. modoboa/frontend_dist/assets/{VApp-CX_C7AUN.js → VApp-1CFpPM0s.js} +1 -1
  91. modoboa/frontend_dist/assets/{VAutocomplete-DNKmBvyZ.js → VAutocomplete-vTx-9foZ.js} +1 -1
  92. modoboa/frontend_dist/assets/{VAvatar-DbuoZWmf.js → VAvatar-Wz1UmZ2b.js} +1 -1
  93. modoboa/frontend_dist/assets/{VBadge-Bv2nvUmC.js → VBadge-C08Ii4Nb.js} +1 -1
  94. modoboa/frontend_dist/assets/{VCard-DzjUT5OP.js → VCard-BrQzTMsm.js} +1 -1
  95. modoboa/frontend_dist/assets/{VCheckbox-dr7UFjl4.js → VCheckbox-IdHLHDAe.js} +1 -1
  96. modoboa/frontend_dist/assets/{VCheckboxBtn-CpFdBnTv.js → VCheckboxBtn-BMf4a9DP.js} +1 -1
  97. modoboa/frontend_dist/assets/{VChip-CaQvfmkw.js → VChip-B1E7xQW1.js} +1 -1
  98. modoboa/frontend_dist/assets/VColorPicker-CuNM9hII.js +1 -0
  99. modoboa/frontend_dist/assets/{VContainer-74Dnn8Ux.js → VContainer-f9kXL-37.js} +1 -1
  100. modoboa/frontend_dist/assets/{VDataTable-CL7yHvG7.js → VDataTable-CVJjOdhF.js} +1 -1
  101. modoboa/frontend_dist/assets/{VDataTableServer-BqvNcIdw.js → VDataTableServer-BSZUbic1.js} +1 -1
  102. modoboa/frontend_dist/assets/{VDataTableVirtual--KsOP8i6.js → VDataTableVirtual-Ceu9uaGm.js} +1 -1
  103. modoboa/frontend_dist/assets/{VDialog-DmTGCGR0.js → VDialog-BF9xR7BQ.js} +1 -1
  104. modoboa/frontend_dist/assets/{VExpansionPanels-B7sSTCwd.js → VExpansionPanels-Rq8tzNGV.js} +1 -1
  105. modoboa/frontend_dist/assets/{VFileInput-SULIc6F3.js → VFileInput-CXyx9q4a.js} +1 -1
  106. modoboa/frontend_dist/assets/{VForm-DsRLc-sa.js → VForm-RVvaatfV.js} +1 -1
  107. modoboa/frontend_dist/assets/{VInput-DVKUObZe.js → VInput-ssnfbpwr.js} +1 -1
  108. modoboa/frontend_dist/assets/{VMenu-nv0XOgg0.js → VMenu-BPG7b7dT.js} +1 -1
  109. modoboa/frontend_dist/assets/{VPicker-DnDSWJHJ.js → VPicker-Cv_7ON7Q.js} +1 -1
  110. modoboa/frontend_dist/assets/{VProgressCircular-qK6p5X_Y.js → VProgressCircular-Bdwj7JuI.js} +1 -1
  111. modoboa/frontend_dist/assets/{VRadioGroup-CbiPLy0t.js → VRadioGroup-BdEZyWNi.js} +1 -1
  112. modoboa/frontend_dist/assets/{VRow-DJ0NB63-.js → VRow-DGPh2Qmm.js} +1 -1
  113. modoboa/frontend_dist/assets/{VSelect-CxCFMHyF.js → VSelect-CzmrBwOO.js} +1 -1
  114. modoboa/frontend_dist/assets/{VSelectionControl-C-6A4us5.js → VSelectionControl-BIXXUgD4.js} +1 -1
  115. modoboa/frontend_dist/assets/{VSheet-DI6SxLnG.js → VSheet-C07oo1bg.js} +1 -1
  116. modoboa/frontend_dist/assets/VSpacer-Pa16wvUF.js +1 -0
  117. modoboa/frontend_dist/assets/{VSwitch-DPnjPQuU.js → VSwitch-C8Eq78Y3.js} +1 -1
  118. modoboa/frontend_dist/assets/{VTable-ldTxgQPW.js → VTable-C08Q6ok2.js} +1 -1
  119. modoboa/frontend_dist/assets/{VTabs-aS8WSL9I.js → VTabs-CaqxP8RF.js} +1 -1
  120. modoboa/frontend_dist/assets/{VTextField-DKbr4H5w.js → VTextField-BBIziqiv.js} +1 -1
  121. modoboa/frontend_dist/assets/{VTextarea-BttkFsM4.js → VTextarea-PURjBZRs.js} +1 -1
  122. modoboa/frontend_dist/assets/{VToolbar-BxX3W2kR.js → VToolbar-CILwTR5O.js} +1 -1
  123. modoboa/frontend_dist/assets/VWindowItem-l3wU5Kto.js +1 -0
  124. modoboa/frontend_dist/assets/WebmailLayout-uF8MMy0l.js +1 -0
  125. modoboa/frontend_dist/assets/{accounts-CC2F0a0c.js → accounts-DkNidbb0.js} +1 -1
  126. modoboa/frontend_dist/assets/{admin-CHCHFGI6.js → admin-C5XWZaxP.js} +1 -1
  127. modoboa/frontend_dist/assets/{aliases-C9bUD4Ws.js → aliases-eOyMwyZV.js} +1 -1
  128. modoboa/frontend_dist/assets/{amavis-DCVJxuui.js → amavis-BOgLfXLH.js} +1 -1
  129. modoboa/frontend_dist/assets/{amavis-BhzV4rgf.js → amavis-w7390k8i.js} +1 -1
  130. modoboa/frontend_dist/assets/{contacts-Dxz6eWpf.js → contacts-Ch-bKxJM.js} +1 -1
  131. modoboa/frontend_dist/assets/domains-DEEhRTFJ.js +1 -0
  132. modoboa/frontend_dist/assets/{domains.store-BLKRipG8.js → domains.store-BhRRATEd.js} +1 -1
  133. modoboa/frontend_dist/assets/{filter-rmxrcjKk.js → filter-CfvL3jIf.js} +1 -1
  134. modoboa/frontend_dist/assets/{forwardRefs-CpzzjgpX.js → forwardRefs-GE5Osi9V.js} +1 -1
  135. modoboa/frontend_dist/assets/{global.store-DndbMXYb.js → global.store-CxPrtY0_.js} +1 -1
  136. modoboa/frontend_dist/assets/importExport-C1_milrO.js +1 -0
  137. modoboa/frontend_dist/assets/index-D9OmnhdE.js +982 -0
  138. modoboa/frontend_dist/assets/{layout-DbjDe3Wl.js → layout-CuwY6Rle.js} +1 -1
  139. modoboa/frontend_dist/assets/{layout.store-Vq5mvIp7.js → layout.store-uEYOXtgD.js} +1 -1
  140. modoboa/frontend_dist/assets/{logos-Bvcy0usu.js → logos-BOVDV-my.js} +1 -1
  141. modoboa/frontend_dist/assets/{logs-BuItINky.js → logs-DT7l7Npx.js} +1 -1
  142. modoboa/frontend_dist/assets/{parameters-C8IYEP7q.js → parameters-DlTg3gyZ.js} +1 -1
  143. modoboa/frontend_dist/assets/{parameters.store-1cwSP2JP.js → parameters.store-eTs-B-pe.js} +1 -1
  144. modoboa/frontend_dist/assets/{permissions-DQjAcO9S.js → permissions-B-P8ks2E.js} +1 -1
  145. modoboa/frontend_dist/assets/{ssrBoot-BxIQ9ccA.js → ssrBoot-DaeAGyhc.js} +1 -1
  146. modoboa/frontend_dist/assets/{tag-3cfI1_f7.js → tag-jImTUiWx.js} +1 -1
  147. modoboa/frontend_dist/assets/theme-tqh89n3L.js +1 -0
  148. modoboa/frontend_dist/assets/transports-BnIcB3M9.js +1 -0
  149. modoboa/frontend_dist/assets/{webmail-B2IUjaxM.js → webmail-Dqo8FjAw.js} +1 -1
  150. modoboa/frontend_dist/index.html +1 -1
  151. modoboa/locale/ja_JP/LC_MESSAGES/django.mo +0 -0
  152. modoboa/locale/ja_JP/LC_MESSAGES/django.po +1 -1
  153. modoboa/relaydomains/tests.py +1 -1
  154. modoboa/static/css/offline.css +7 -20
  155. modoboa/templates/registration/base.html +18 -0
  156. modoboa/templates/registration/login.html +1 -1
  157. modoboa/urls.py +1 -5
  158. {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/METADATA +9 -9
  159. {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/RECORD +164 -156
  160. {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/licenses/LICENSE +1 -1
  161. modoboa/core/api/v1/tests.py +0 -26
  162. modoboa/frontend_dist/assets/AccountLayout-U386K8zy.js +0 -1
  163. modoboa/frontend_dist/assets/AdminLayout-Cxm1lggg.js +0 -1
  164. modoboa/frontend_dist/assets/AlarmsView-9yKGbmkC.css +0 -1
  165. modoboa/frontend_dist/assets/AlarmsView-Bcjsicac.js +0 -1
  166. modoboa/frontend_dist/assets/AliasEditView-Cx9410JP.css +0 -1
  167. modoboa/frontend_dist/assets/AliasEditView-k3rVt1tG.js +0 -1
  168. modoboa/frontend_dist/assets/AliasRecipientForm-IehUzKok.js +0 -1
  169. modoboa/frontend_dist/assets/ComposeEmailView-DLv3wk1k.js +0 -1
  170. modoboa/frontend_dist/assets/ConfirmDialog-CcPrCKuI.js +0 -1
  171. modoboa/frontend_dist/assets/ConnectedLayout-CWlBK7Hf.js +0 -1
  172. modoboa/frontend_dist/assets/DashboardView-DXVZMbMo.js +0 -1
  173. modoboa/frontend_dist/assets/DomainsView-CEEU9btK.js +0 -1
  174. modoboa/frontend_dist/assets/DomainsView-DZ-ss9bI.css +0 -1
  175. modoboa/frontend_dist/assets/EmptyLayout-BXgcfMLH.js +0 -1
  176. modoboa/frontend_dist/assets/ForwardEmailView-Bgv3JQb6.js +0 -1
  177. modoboa/frontend_dist/assets/IdentitiesView-DPrrRMS5.css +0 -1
  178. modoboa/frontend_dist/assets/IdentitiesView-Dld9IloZ.js +0 -1
  179. modoboa/frontend_dist/assets/LoginCallbackView-DjyE2SG_.js +0 -1
  180. modoboa/frontend_dist/assets/MailboxView-DRrs9eLO.js +0 -1
  181. modoboa/frontend_dist/assets/ParametersForm-3qXttTuQ.js +0 -1
  182. modoboa/frontend_dist/assets/ParametersForm-Dk-qwlRm.css +0 -1
  183. modoboa/frontend_dist/assets/ParametersView-3Ns04cpQ.js +0 -1
  184. modoboa/frontend_dist/assets/ParametersView-B5B5Dt6K.js +0 -1
  185. modoboa/frontend_dist/assets/ParametersView-DaKFNERg.css +0 -1
  186. modoboa/frontend_dist/assets/QuarantineLayout-CYBsrbJM.js +0 -1
  187. modoboa/frontend_dist/assets/ReplyEmailView-D1XTcglu.js +0 -1
  188. modoboa/frontend_dist/assets/ResourcesForm-BW8rUGgZ.js +0 -1
  189. modoboa/frontend_dist/assets/SelfServiceLayout-DfDHiYeX.js +0 -1
  190. modoboa/frontend_dist/assets/UserLayout-zUtHi-z-.js +0 -1
  191. modoboa/frontend_dist/assets/VColorPicker-ByGpCW5O.js +0 -1
  192. modoboa/frontend_dist/assets/VSpacer-CoJVmx8k.js +0 -1
  193. modoboa/frontend_dist/assets/VWindowItem-Cvqvdegd.js +0 -1
  194. modoboa/frontend_dist/assets/WebmailLayout-BT2k6U7q.js +0 -1
  195. modoboa/frontend_dist/assets/domains-C2cornvL.js +0 -1
  196. modoboa/frontend_dist/assets/importExport-C3uqrcok.js +0 -1
  197. modoboa/frontend_dist/assets/index-LhNzkzAh.js +0 -984
  198. modoboa/frontend_dist/assets/transports-D4Jk4-AP.js +0 -1
  199. {modoboa-2.5.1.data → modoboa-2.6.1.data}/scripts/modoboa-admin.py +0 -0
  200. {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/WHEEL +0 -0
  201. {modoboa-2.5.1.dist-info → modoboa-2.6.1.dist-info}/entry_points.txt +0 -0
  202. {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):
@@ -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
@@ -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
- for idt in lib.get_identities(request.user):
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
 
@@ -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(user, searchquery=None, idtfilter=None, grpfilter=None):
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.admin")
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.admin")
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:
@@ -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(
@@ -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
@@ -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")
@@ -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.admin") as log:
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.admin",
151
+ "modoboa.dns",
152
152
  "ERROR",
153
153
  _("No DNS record found for %s") % "does-not-exist.example.com",
154
154
  ),
155
- ("modoboa.admin", "ERROR", _("No MX record for %s") % "no-mx.example.com"),
156
- ("modoboa.admin", "ERROR", _("No working name servers found")),
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.admin",
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.admin",
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.admin",
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.admin",
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.admin",
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.admin") as log:
196
+ with LogCapture("modoboa.dns") as log:
197
197
  get_domain_mx_list("test3.com")
198
198
  log.check(
199
- ("modoboa.admin", "ERROR", _("No AAAA record for %s") % "mx3.example.com")
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.admin") as log2:
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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AutoconfigConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "modoboa.autoconfig"
@@ -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.force_login(admin)
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.force_login(self.account)
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.force_login(self.account)
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.force_login(self.account)
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)
@@ -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"] = (