koishi-plugin-mail-manager 1.0.1 → 1.0.3

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 (45) hide show
  1. package/client/api.ts +4 -0
  2. package/client/components/AccountEditModal.vue +262 -106
  3. package/client/pages/AccountsView.vue +11 -18
  4. package/client/pages/MailsView.vue +215 -21
  5. package/client/pages/index.vue +8 -11
  6. package/dist/index.js +11 -4
  7. package/dist/style.css +1 -1
  8. package/lib/core.d.ts +3 -7
  9. package/lib/index.js +1203 -820
  10. package/package.json +6 -6
  11. package/readme.md +8 -1
  12. package/lib/api.d.ts +0 -8
  13. package/lib/cleanup.d.ts +0 -35
  14. package/lib/commands.d.ts +0 -9
  15. package/lib/config.d.ts +0 -16
  16. package/lib/core/accounts.d.ts +0 -15
  17. package/lib/core/forward.d.ts +0 -41
  18. package/lib/core/index.d.ts +0 -16
  19. package/lib/core/mails.d.ts +0 -56
  20. package/lib/core/rules.d.ts +0 -41
  21. package/lib/core/state.d.ts +0 -46
  22. package/lib/database.d.ts +0 -6
  23. package/lib/html2image.d.ts +0 -42
  24. package/lib/imap.d.ts +0 -198
  25. package/lib/index.d.ts +0 -14
  26. package/lib/logger.d.ts +0 -61
  27. package/lib/parser.d.ts +0 -77
  28. package/lib/providers/base.d.ts +0 -168
  29. package/lib/providers/gmail.d.ts +0 -48
  30. package/lib/providers/icloud.d.ts +0 -15
  31. package/lib/providers/index.d.ts +0 -37
  32. package/lib/providers/netease.d.ts +0 -19
  33. package/lib/providers/outlook.d.ts +0 -15
  34. package/lib/providers/qq.d.ts +0 -15
  35. package/lib/providers/yahoo.d.ts +0 -14
  36. package/lib/render.d.ts +0 -36
  37. package/lib/service.d.ts +0 -42
  38. package/lib/styles.d.ts +0 -16
  39. package/lib/types.d.ts +0 -547
  40. package/lib/utils/common.d.ts +0 -45
  41. package/lib/utils/constants.d.ts +0 -31
  42. package/lib/utils/crypto.d.ts +0 -74
  43. package/lib/utils/dns.d.ts +0 -180
  44. package/lib/utils/errors.d.ts +0 -63
  45. package/lib/utils/index.d.ts +0 -7
package/client/api.ts CHANGED
@@ -46,6 +46,10 @@ export const accountApi = {
46
46
  /** 测试连接 */
47
47
  test: (id: number) => call<ConnectionTestResult>('mail-manager/accounts/test', id),
48
48
 
49
+ /** 用当前表单配置测试连接(无需先保存账号) */
50
+ testTemp: (data: Partial<MailAccount>) =>
51
+ call<ConnectionTestResult>('mail-manager/accounts/test-temp', data),
52
+
49
53
  /** 连接账号 */
50
54
  connect: (id: number) => call<void>('mail-manager/accounts/connect', id),
51
55
 
@@ -5,138 +5,161 @@
5
5
  <span class="ml-modal-title">{{ isEditing ? '编辑账号' : '添加账号' }}</span>
6
6
  <button class="ml-modal-close" @click="closeModal"><Icon name="close" /></button>
7
7
  </div>
8
+
8
9
  <div class="ml-modal-body">
9
- <!-- 错误提示 -->
10
10
  <div v-if="formError" class="form-error">
11
11
  <Icon name="alert" /> {{ formError }}
12
12
  </div>
13
13
 
14
- <div class="ml-form-group">
15
- <label class="ml-label">名称 <span class="required">*</span></label>
16
- <input
17
- v-model="formData.name"
18
- class="ml-input"
19
- :class="{ 'has-error': !formData.name && formTouched.name }"
20
- placeholder="用于标识的名称(如:工作邮箱)"
21
- @blur="formTouched.name = true"
22
- />
23
- </div>
24
-
25
- <!-- 邮箱服务商快速选择 -->
26
- <div class="ml-form-group">
27
- <label class="ml-label">邮箱服务商</label>
28
- <Select
29
- v-model="selectedProvider"
30
- :options="providerOptions"
31
- placeholder="选择服务商自动填充配置"
32
- @change="onProviderChange"
33
- />
34
- </div>
35
-
36
- <div class="ml-form-group">
37
- <label class="ml-label">邮箱地址 <span class="required">*</span></label>
38
- <input
39
- v-model="formData.email"
40
- class="ml-input"
41
- :class="{ 'has-error': !formData.email && formTouched.email }"
42
- type="email"
43
- placeholder="example@mail.com"
44
- @blur="formTouched.email = true"
45
- @input="autoFillImapHost"
46
- />
47
- </div>
14
+ <div class="form-section">
15
+ <div class="section-title"><Icon name="user" /> 基本信息</div>
16
+ <div class="ml-form-group">
17
+ <label class="ml-label">名称 <span class="required">*</span></label>
18
+ <input
19
+ v-model="formData.name"
20
+ class="ml-input"
21
+ :class="{ 'has-error': !formData.name && formTouched.name }"
22
+ placeholder="用于标识的名称(如:工作邮箱)"
23
+ @blur="formTouched.name = true"
24
+ />
25
+ </div>
48
26
 
49
- <div class="ml-form-group">
50
- <label class="ml-label">密码/授权码 <span v-if="!isEditing" class="required">*</span></label>
51
- <div class="password-input-wrapper">
27
+ <div class="ml-form-group">
28
+ <label class="ml-label">邮箱地址 <span class="required">*</span></label>
52
29
  <input
53
- v-model="formData.password"
30
+ v-model="formData.email"
54
31
  class="ml-input"
55
- :class="{ 'has-error': !isEditing && !formData.password && formTouched.password }"
56
- :type="showPassword ? 'text' : 'password'"
57
- :placeholder="isEditing ? '留空则保持原密码不变' : '授权码(非登录密码)'"
58
- @blur="formTouched.password = true"
32
+ :class="{ 'has-error': !formData.email && formTouched.email }"
33
+ type="email"
34
+ placeholder="example@mail.com"
35
+ @blur="formTouched.email = true"
36
+ @input="autoFillImapHost"
59
37
  />
60
- <button class="toggle-password-btn" @click="showPassword = !showPassword" tabindex="-1">
61
- <Icon :name="showPassword ? 'eye' : 'eye-off'" />
62
- </button>
63
38
  </div>
64
- <!-- 授权码获取指引 -->
65
- <div v-if="selectedProviderInfo" class="auth-guide">
66
- <Icon name="info" />
67
- <span>{{ selectedProviderInfo.authGuide }}</span>
68
- <a v-if="selectedProviderInfo.helpUrl" :href="selectedProviderInfo.helpUrl" target="_blank" class="guide-link">
69
- 查看教程 <Icon name="external-link" />
70
- </a>
39
+
40
+ <div class="ml-form-group">
41
+ <label class="ml-label">邮箱服务商</label>
42
+ <Select
43
+ v-model="selectedProvider"
44
+ :options="providerOptions"
45
+ placeholder="选择服务商自动填充配置"
46
+ @change="onProviderChange"
47
+ />
71
48
  </div>
72
- <div v-else class="ml-help"><Icon name="lightbulb" /> 大部分邮箱需要使用授权码,而非登录密码</div>
73
49
  </div>
74
50
 
75
- <div class="ml-form-group">
76
- <label class="ml-label">代理服务器(可选)</label>
77
- <input
78
- v-model="formData.proxyUrl"
79
- class="ml-input"
80
- placeholder="如: socks5://127.0.0.1:6780"
81
- />
82
- <div v-if="selectedProviderInfo?.needsProxy" class="ml-help warning">
83
- <Icon name="alert" />
84
- {{ selectedProviderInfo.proxyHint || '此邮箱服务可能需要代理才能连接' }}
51
+ <div class="form-section">
52
+ <div class="section-title"><Icon name="network" /> 连接参数</div>
53
+ <div class="ml-form-group">
54
+ <label class="ml-label">IMAP 服务器 <span class="required">*</span></label>
55
+ <div class="server-input-row">
56
+ <input
57
+ v-model="formData.imapHost"
58
+ class="ml-input"
59
+ :class="{ 'has-error': !formData.imapHost && formTouched.imapHost }"
60
+ placeholder="imap.example.com"
61
+ @blur="formTouched.imapHost = true"
62
+ />
63
+ <Select
64
+ v-model="quickServerSelect"
65
+ :options="quickServerOptions"
66
+ placeholder="快速选择"
67
+ size="small"
68
+ @change="onQuickServerSelect"
69
+ />
70
+ </div>
71
+ </div>
72
+
73
+ <div class="ml-form-row">
74
+ <div class="ml-form-group port-group">
75
+ <label class="ml-label">端口</label>
76
+ <Select
77
+ v-model="formData.imapPort"
78
+ :options="portOptions"
79
+ placeholder="选择端口"
80
+ />
81
+ </div>
82
+
83
+ <div class="ml-form-group switch-group">
84
+ <label class="ml-label">TLS 加密</label>
85
+ <div class="switch-wrapper">
86
+ <label class="ml-switch">
87
+ <input v-model="formData.imapTls" type="checkbox" />
88
+ <span class="slider"></span>
89
+ </label>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="ml-form-group switch-group">
94
+ <label class="ml-label">启用</label>
95
+ <div class="switch-wrapper">
96
+ <label class="ml-switch">
97
+ <input v-model="formData.enabled" type="checkbox" />
98
+ <span class="slider"></span>
99
+ </label>
100
+ </div>
101
+ </div>
85
102
  </div>
86
- </div>
87
103
 
88
- <div class="ml-form-group">
89
- <label class="ml-label">IMAP 服务器 <span class="required">*</span></label>
90
- <div class="server-input-row">
104
+ <div class="ml-form-group">
105
+ <label class="ml-label">代理服务器(可选)</label>
91
106
  <input
92
- v-model="formData.imapHost"
107
+ v-model="formData.proxyUrl"
93
108
  class="ml-input"
94
- :class="{ 'has-error': !formData.imapHost && formTouched.imapHost }"
95
- placeholder="imap.example.com"
96
- @blur="formTouched.imapHost = true"
97
- />
98
- <Select
99
- v-model="quickServerSelect"
100
- :options="quickServerOptions"
101
- placeholder="快速选择"
102
- size="small"
103
- @change="onQuickServerSelect"
109
+ placeholder="如: socks5://127.0.0.1:6780"
104
110
  />
111
+ <div v-if="selectedProviderInfo?.needsProxy" class="ml-help warning">
112
+ <Icon name="alert" />
113
+ {{ selectedProviderInfo.proxyHint || '此邮箱服务可能需要代理才能连接' }}
114
+ </div>
105
115
  </div>
106
116
  </div>
107
117
 
108
- <div class="ml-form-row">
109
- <div class="ml-form-group port-group">
110
- <label class="ml-label">端口</label>
111
- <Select
112
- v-model="formData.imapPort"
113
- :options="portOptions"
114
- placeholder="选择端口"
115
- />
116
- </div>
118
+ <div class="form-section">
119
+ <div class="section-title"><Icon name="settings" /> 认证与校验</div>
120
+
117
121
  <div class="ml-form-group">
118
- <label class="ml-label">TLS 加密</label>
119
- <div class="switch-wrapper">
120
- <label class="ml-switch">
121
- <input v-model="formData.imapTls" type="checkbox" />
122
- <span class="slider"></span>
123
- </label>
122
+ <label class="ml-label">密码/授权码 <span v-if="!isEditing" class="required">*</span></label>
123
+ <div class="password-input-wrapper">
124
+ <input
125
+ v-model="formData.password"
126
+ class="ml-input"
127
+ :class="{ 'has-error': !isEditing && !formData.password && formTouched.password }"
128
+ :type="showPassword ? 'text' : 'password'"
129
+ :placeholder="isEditing ? '留空则保持原密码不变;测试连接需填写' : '授权码(非登录密码)'"
130
+ @blur="formTouched.password = true"
131
+ />
132
+ <button class="toggle-password-btn" @click="showPassword = !showPassword" tabindex="-1" type="button">
133
+ <Icon :name="showPassword ? 'eye' : 'eye-off'" />
134
+ </button>
124
135
  </div>
136
+
137
+ <div v-if="selectedProviderInfo" class="auth-guide">
138
+ <Icon name="info" />
139
+ <span>{{ selectedProviderInfo.authGuide }}</span>
140
+ <a v-if="selectedProviderInfo.helpUrl" :href="selectedProviderInfo.helpUrl" target="_blank" class="guide-link">
141
+ 查看教程 <Icon name="external-link" />
142
+ </a>
143
+ </div>
144
+ <div v-else class="ml-help"><Icon name="lightbulb" /> 大部分邮箱需要使用授权码,而非登录密码</div>
125
145
  </div>
126
- <div class="ml-form-group">
127
- <label class="ml-label">启用</label>
128
- <div class="switch-wrapper">
129
- <label class="ml-switch">
130
- <input v-model="formData.enabled" type="checkbox" />
131
- <span class="slider"></span>
132
- </label>
146
+
147
+ <div class="test-box" :class="{ success: testSuccess === true, failed: testSuccess === false }">
148
+ <div class="test-info">
149
+ <Icon :name="testSuccess === true ? 'check' : testSuccess === false ? 'alert-circle' : 'activity'" />
150
+ <span v-if="testSuccess === null">建议保存前先测试连接,避免无效配置反复重连。</span>
151
+ <span v-else>{{ testMessage }}</span>
133
152
  </div>
153
+ <button class="ml-btn" type="button" @click="testConnectionWithForm" :disabled="testing || !canTestConfig">
154
+ {{ testing ? '测试中...' : '测试连接' }}
155
+ </button>
134
156
  </div>
135
157
  </div>
136
158
  </div>
159
+
137
160
  <div class="ml-modal-footer">
138
- <button class="ml-btn" @click="closeModal">取消</button>
139
- <button class="ml-btn primary" @click="saveAccount" :disabled="saving || !isFormValid">
161
+ <button class="ml-btn" @click="closeModal" :disabled="saving || testing">取消</button>
162
+ <button class="ml-btn primary" @click="saveAccount" :disabled="saving || testing || !isFormValid">
140
163
  {{ saving ? '保存中...' : '保存' }}
141
164
  </button>
142
165
  </div>
@@ -211,7 +234,7 @@ const emailProviders: Record<string, ProviderConfig> = {
211
234
  authGuide: '请先开启两步验证,然后创建应用专用密码作为授权码使用',
212
235
  helpUrl: 'https://support.google.com/accounts/answer/185833',
213
236
  needsProxy: true,
214
- proxyHint: 'Gmail 在中国大陆地区需要配置代理才能连接'
237
+ proxyHint: 'Gmail 可能需要配置代理'
215
238
  },
216
239
  outlook: {
217
240
  name: 'Outlook / Hotmail',
@@ -222,7 +245,7 @@ const emailProviders: Record<string, ProviderConfig> = {
222
245
  authGuide: '请在 Microsoft 账户「安全 > 应用密码」中创建应用密码',
223
246
  helpUrl: 'https://support.microsoft.com/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944',
224
247
  needsProxy: true,
225
- proxyHint: 'Outlook 在部分地区可能需要配置代理'
248
+ proxyHint: 'Outlook 可能需要配置代理'
226
249
  },
227
250
  yahoo: {
228
251
  name: 'Yahoo Mail',
@@ -318,8 +341,11 @@ const imapHostMap: Record<string, string> = {
318
341
  }
319
342
 
320
343
  const saving = ref(false)
344
+ const testing = ref(false)
321
345
  const showPassword = ref(false)
322
346
  const formError = ref('')
347
+ const testMessage = ref('')
348
+ const testSuccess = ref<boolean | null>(null)
323
349
  const selectedProvider = ref('')
324
350
  const quickServerSelect = ref('')
325
351
 
@@ -349,6 +375,8 @@ const formData = reactive({
349
375
  proxyUrl: '',
350
376
  })
351
377
 
378
+ const shouldForceTls = (port: number) => port === 993 || port === 465
379
+
352
380
  // 初始化表单
353
381
  watch(() => props.visible, (newVal) => {
354
382
  if (newVal) {
@@ -379,9 +407,24 @@ watch(() => props.visible, (newVal) => {
379
407
  })
380
408
  selectedProvider.value = ''
381
409
  }
410
+
411
+ testMessage.value = ''
412
+ testSuccess.value = null
382
413
  }
383
414
  })
384
415
 
416
+ watch(() => formData.imapPort, (port) => {
417
+ formData.imapTls = shouldForceTls(port)
418
+ })
419
+
420
+ watch(
421
+ () => [formData.email, formData.password, formData.imapHost, formData.imapPort, formData.imapTls, formData.proxyUrl],
422
+ () => {
423
+ testSuccess.value = null
424
+ testMessage.value = ''
425
+ }
426
+ )
427
+
385
428
  const resetForm = () => {
386
429
  formTouched.name = false
387
430
  formTouched.email = false
@@ -418,6 +461,42 @@ const onProviderChange = (value: string) => {
418
461
  }
419
462
  }
420
463
 
464
+ const canTestConfig = computed(() => {
465
+ return !!(formData.email && formData.imapHost && formData.password)
466
+ })
467
+
468
+ const testConnectionWithForm = async () => {
469
+ formError.value = ''
470
+ testMessage.value = ''
471
+
472
+ if (!canTestConfig.value) {
473
+ testSuccess.value = false
474
+ testMessage.value = '测试连接需要填写邮箱地址、IMAP 服务器和密码/授权码。'
475
+ return
476
+ }
477
+
478
+ testing.value = true
479
+ try {
480
+ const result = await accountApi.testTemp({
481
+ name: formData.name,
482
+ email: formData.email,
483
+ password: formData.password,
484
+ imapHost: formData.imapHost,
485
+ imapPort: formData.imapPort,
486
+ imapTls: formData.imapTls,
487
+ proxyUrl: formData.proxyUrl,
488
+ })
489
+
490
+ testSuccess.value = result.success
491
+ testMessage.value = result.success ? '连接测试成功,可以安全保存。' : `连接测试失败:${result.message}`
492
+ } catch (e) {
493
+ testSuccess.value = false
494
+ testMessage.value = `连接测试失败:${(e as Error).message}`
495
+ } finally {
496
+ testing.value = false
497
+ }
498
+ }
499
+
421
500
  // 快速服务器选择
422
501
  const onQuickServerSelect = (value: string) => {
423
502
  if (value) {
@@ -463,6 +542,11 @@ const saveAccount = async () => {
463
542
  return
464
543
  }
465
544
 
545
+ if (testSuccess.value !== true && canTestConfig.value) {
546
+ const confirmed = confirm('当前配置未通过连接测试,仍要保存吗?')
547
+ if (!confirmed) return
548
+ }
549
+
466
550
  formError.value = ''
467
551
  saving.value = true
468
552
  try {
@@ -489,7 +573,25 @@ const saveAccount = async () => {
489
573
  <style scoped>
490
574
  .account-modal {
491
575
  width: 100%;
492
- max-width: 520px;
576
+ max-width: 640px;
577
+ }
578
+
579
+ .form-section {
580
+ border: 1px solid var(--ml-border);
581
+ border-radius: 10px;
582
+ padding: 14px;
583
+ margin-bottom: 14px;
584
+ background: var(--ml-bg-base);
585
+ }
586
+
587
+ .section-title {
588
+ display: flex;
589
+ align-items: center;
590
+ gap: 6px;
591
+ font-size: 13px;
592
+ font-weight: 600;
593
+ color: var(--ml-text-secondary);
594
+ margin-bottom: 10px;
493
595
  }
494
596
 
495
597
  .form-error {
@@ -532,6 +634,37 @@ const saveAccount = async () => {
532
634
  color: var(--ml-warning);
533
635
  }
534
636
 
637
+ .test-box {
638
+ margin-top: 10px;
639
+ padding: 10px 12px;
640
+ border: 1px solid var(--ml-border);
641
+ border-radius: 8px;
642
+ display: flex;
643
+ align-items: center;
644
+ justify-content: space-between;
645
+ gap: 10px;
646
+ background: var(--ml-bg-container);
647
+ }
648
+
649
+ .test-box.success {
650
+ border-color: rgba(82, 196, 26, 0.45);
651
+ background: rgba(82, 196, 26, 0.08);
652
+ }
653
+
654
+ .test-box.failed {
655
+ border-color: rgba(255, 77, 79, 0.45);
656
+ background: rgba(255, 77, 79, 0.08);
657
+ }
658
+
659
+ .test-info {
660
+ display: flex;
661
+ align-items: center;
662
+ gap: 6px;
663
+ font-size: 13px;
664
+ color: var(--ml-text-secondary);
665
+ line-height: 1.5;
666
+ }
667
+
535
668
  .auth-guide {
536
669
  display: flex;
537
670
  align-items: flex-start;
@@ -588,6 +721,10 @@ const saveAccount = async () => {
588
721
  max-width: 200px;
589
722
  }
590
723
 
724
+ .switch-group {
725
+ min-width: 88px;
726
+ }
727
+
591
728
  .switch-wrapper {
592
729
  padding-top: 8px;
593
730
  }
@@ -641,4 +778,23 @@ const saveAccount = async () => {
641
778
  width: 1.2em;
642
779
  height: 1.2em;
643
780
  }
781
+
782
+ @media (max-width: 720px) {
783
+ .account-modal {
784
+ max-width: 96vw;
785
+ }
786
+
787
+ .test-box {
788
+ flex-direction: column;
789
+ align-items: stretch;
790
+ }
791
+
792
+ .server-input-row {
793
+ flex-direction: column;
794
+ }
795
+
796
+ .server-input-row .ml-select-wrapper {
797
+ width: 100%;
798
+ }
799
+ }
644
800
  </style>
@@ -122,7 +122,7 @@
122
122
  <div v-else>
123
123
  <p class="sync-tip">
124
124
  <Icon name="lightbulb" />
125
- 从邮箱服务器重新获取邮件。可用于恢复被删除的邮件。
125
+ 从邮箱服务器重新获取邮件。可用于恢复本地已删除、但服务器仍保留的邮件。
126
126
  </p>
127
127
  <div class="sync-warning">
128
128
  <Icon name="info" />
@@ -153,7 +153,7 @@
153
153
  </div>
154
154
  <div class="ml-help">
155
155
  <Icon name="info" />
156
- 系统会自动去重,只保存新邮件。同步期间请勿关闭页面。
156
+ 系统会按 Message-ID 自动去重并优先跳过已存在邮件。同步期间请勿关闭页面。
157
157
  </div>
158
158
  </div>
159
159
  </div>
@@ -174,7 +174,7 @@
174
174
  </template>
175
175
 
176
176
  <script setup lang="ts">
177
- import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
177
+ import { ref, reactive, computed, onMounted } from 'vue'
178
178
  import { send, receive } from '@koishijs/client'
179
179
  import { accountApi } from '../api'
180
180
  import type { MailAccount } from '../types'
@@ -209,20 +209,19 @@ const loadAccounts = async () => {
209
209
  }
210
210
  }
211
211
 
212
- // 处理账号状态变化事件
213
- const handleStatusChange = (data: { accountId: number; status: string; error?: string }) => {
212
+ // 在 setup 顶层注册监听,确保 Vue 正确绑定生命周期并及时接收事件
213
+ // 处理账号状态变化推送
214
+ receive('mail-manager/account-status-changed', (data: { accountId: number; status: string; error?: string | null }) => {
214
215
  const account = accounts.value.find(a => a.id === data.accountId)
215
216
  if (account) {
216
217
  account.status = data.status as MailAccount['status']
217
- if (data.error) {
218
- account.lastError = data.error
219
- } else {
220
- account.lastError = undefined
221
- }
222
- // 通知父组件更新统计(已连接数量可能变化)
218
+ account.lastError = data.error || undefined
223
219
  emit('refresh')
220
+ } else {
221
+ // 账号不在列表中(如新建账号时的竞态),重新加载以同步最新状态
222
+ loadAccounts().then(() => emit('refresh'))
224
223
  }
225
- }
224
+ })
226
225
 
227
226
  // 同步相关状态
228
227
  const showSyncModal = ref(false)
@@ -329,12 +328,6 @@ const deleteAccount = async (account: MailAccount) => {
329
328
 
330
329
  onMounted(() => {
331
330
  loadAccounts()
332
-
333
- // 监听状态变化事件
334
- // receive 函数会自动在组件卸载时清理监听器
335
- receive('mail-manager/account-status-changed', (data) => {
336
- handleStatusChange(data)
337
- })
338
331
  })
339
332
 
340
333
  </script>