feat: support WangYi email

1. Add support for WangYi email (e.g., 163.com, 126.com) IMAP connection

2. For WangYi email, the ID command must be executed before the SELECT/EXAMINE command to establish a secure connection; otherwise, the error "SELECT Unsafe Login. Please contact kefu@1888.com for help" will be thrown

3. When using the IDLE command for WangYi email, multiple messages will be returned by the server upon receiving a new email
This commit is contained in:
zhouliang 2026-02-26 02:26:22 +08:00
parent caf9d14adc
commit 611334b3e2

View file

@ -299,8 +299,18 @@ func (c *EmailChannel) connect() error {
if mailbox == "" { if mailbox == "" {
mailbox = "INBOX" mailbox = "INBOX"
} }
if c.checkIsWangYiEmail() {
status, err := cl.Select(mailbox, false) // wangyi is not follow the IMAPz specification, it requires to use ID COMMAND to select mailbox, you need to add ID COMMAND to the select command
// if not, it will return "SELECT Unsafe Login. Please contact kefu@1888.com for help" error
cmd := &imap.Command{Name: "ID", Arguments: []any{
[]any{"name", "picoclaw", "version", "1.0"},
}}
_, idErr := cl.Execute(cmd, nil)
if idErr != nil {
return fmt.Errorf("failed to execute ID COMMAND: %w", idErr)
}
}
status, err := cl.Select(mailbox, true)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "Unsafe Login") || strings.Contains(err.Error(), "不安全") { //nolint:gosmopolitan if strings.Contains(err.Error(), "Unsafe Login") || strings.Contains(err.Error(), "不安全") { //nolint:gosmopolitan
return fmt.Errorf( return fmt.Errorf(
@ -335,6 +345,13 @@ func (c *EmailChannel) connect() error {
return nil return nil
} }
func (c *EmailChannel) checkIsWangYiEmail() bool {
server := strings.ToLower(c.config.IMAPServer)
return strings.Contains(server, "163.com") ||
strings.Contains(server, "126.com") ||
strings.Contains(server, "yeah.net")
}
// syncLastUID fetches the mailbox max UID and sets lastUID so only mail after connect is processed. // syncLastUID fetches the mailbox max UID and sets lastUID so only mail after connect is processed.
func (c *EmailChannel) syncLastUID(cl *client.Client) error { func (c *EmailChannel) syncLastUID(cl *client.Client) error {
c.mu.Lock() c.mu.Lock()
@ -594,7 +611,7 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) {
criteria.Uid = seqset criteria.Uid = seqset
} }
uids, err := cl.UidSearch(criteria) searchUids, err := cl.UidSearch(criteria)
if err != nil { if err != nil {
logger.ErrorCF("email", "Failed to search emails", map[string]any{ logger.ErrorCF("email", "Failed to search emails", map[string]any{
"error": err.Error(), "error": err.Error(),
@ -608,6 +625,14 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) {
continue continue
} }
// a new email may correspond to multiple IMAP updates
// use max uid to avoid duplicate
uids := make([]uint32, 0, len(searchUids))
for _, uid := range searchUids {
if uid > lastUID {
uids = append(uids, uid)
}
}
if len(uids) == 0 { if len(uids) == 0 {
return return
} }