From cc75382869fb6fc3a135f1f9c9214b076f9b7cce Mon Sep 17 00:00:00 2001 From: LanQin Date: Thu, 25 Jun 2026 22:25:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(external=5Fimap):=20=E4=BF=AE=E5=A4=8D=20IM?= =?UTF-8?q?AP=20=E6=96=87=E4=BB=B6=E5=A4=B9=E5=88=97=E8=A1=A8=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在测试外部 IMAP 账号时,将文件夹列表失败提示改为更准确的错误信息。 - 为 OAuth XOAUTH2 认证和文件夹列表补充更明确的错误包装,便于定位问题。 - 对不支持 `LIST RETURN (STATUS ...)` 的 Outlook/Exchange 服务降级为普通 `LIST`,再单独查询文件夹状态。 --- apps/api/internal/app/external_imap.go | 40 +++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/api/internal/app/external_imap.go b/apps/api/internal/app/external_imap.go index 10abc39..112505d 100644 --- a/apps/api/internal/app/external_imap.go +++ b/apps/api/internal/app/external_imap.go @@ -317,7 +317,7 @@ func (a *App) handleTestExternalIMAPAccount(w http.ResponseWriter, r *http.Reque folders, err := client.ListFolders(r.Context()) if err != nil { a.updateExternalIMAPStatus(r.Context(), account.ID, "error", err.Error()) - respondError(w, http.StatusBadRequest, "connection failed: "+err.Error()) + respondError(w, http.StatusBadRequest, "list folders failed: "+err.Error()) return } a.updateExternalIMAPStatus(r.Context(), account.ID, "ok", "") @@ -1499,7 +1499,7 @@ func (a *App) openExternalIMAPClient(ctx context.Context, account externalIMAPAc } if err := c.Authenticate(newExternalIMAPXOAUTH2Client(account.Username, token)); err != nil { c.Close() - return nil, err + return nil, fmt.Errorf("oauth xoauth2 authenticate failed: %w", err) } } else { password, err := a.decryptExternalIMAPPassword(account.PasswordCiphertext) @@ -1525,13 +1525,17 @@ func newExternalIMAPXOAUTH2Client(username, token string) externalIMAPXOAUTH2Cli } func (c externalIMAPXOAUTH2Client) Start() (string, []byte, error) { - return "XOAUTH2", []byte("user=" + c.username + "\x01auth=Bearer " + c.token + "\x01\x01"), nil + return "XOAUTH2", externalIMAPXOAUTH2Response(c.username, c.token), nil } func (c externalIMAPXOAUTH2Client) Next(challenge []byte) ([]byte, error) { return []byte{}, nil } +func externalIMAPXOAUTH2Response(username, token string) []byte { + return []byte("user=" + username + "\x01auth=Bearer " + token + "\x01\x01") +} + func (a *App) externalIMAPOAuthAccessToken(ctx context.Context, account externalIMAPAccountRecord) (string, error) { access, err := a.decryptExternalIMAPPassword(account.OAuthAccessTokenCiphertext) if err != nil { @@ -1590,7 +1594,20 @@ func (c *goExternalIMAPClient) Close() error { } func (c *goExternalIMAPClient) ListFolders(ctx context.Context) ([]externalIMAPRemoteFolder, error) { - list, err := c.client.List("", "*", &imap.ListOptions{ReturnStatus: &imap.StatusOptions{NumMessages: true, NumUnseen: true}}).Collect() + caps := c.client.Caps() + var options *imap.ListOptions + if caps.Has(imap.CapIMAP4rev2) || caps.Has(imap.CapListStatus) { + options = &imap.ListOptions{ReturnStatus: &imap.StatusOptions{NumMessages: true, NumUnseen: true}} + } + list, err := c.client.List("", "*", options).Collect() + if err != nil && options != nil { + // Some Outlook/Exchange IMAP deployments advertise extended LIST + // capabilities but reject LIST RETURN (STATUS ...) with + // "BAD Command Argument Error. 12". Fall back to a plain LIST and + // fetch counts with STATUS separately. + options = nil + list, err = c.client.List("", "*", nil).Collect() + } if err != nil { return nil, err } @@ -1613,6 +1630,21 @@ func (c *goExternalIMAPClient) ListFolders(ctx context.Context) ([]externalIMAPR } folders = append(folders, f) } + if options == nil { + statusOptions := &imap.StatusOptions{NumMessages: true, NumUnseen: true} + for i := range folders { + status, err := c.client.Status(folders[i].Name, statusOptions).Wait() + if err != nil { + continue + } + if status.NumMessages != nil { + folders[i].TotalCount = int(*status.NumMessages) + } + if status.NumUnseen != nil { + folders[i].UnreadCount = int(*status.NumUnseen) + } + } + } if len(folders) == 0 { folders = append(folders, externalIMAPRemoteFolder{Name: "INBOX", Role: "Inbox"}) }