From 4fa489cb0286377f0b20dfee852b417200eb77ce Mon Sep 17 00:00:00 2001 From: zhouliang Date: Tue, 24 Feb 2026 15:12:04 +0800 Subject: [PATCH] style: ggolangci-lin and go vet fix --- pkg/channels/email.go | 85 +++++++++++++++++++++++++------------- pkg/channels/email_test.go | 76 +++++++++++++++++++++++----------- pkg/channels/manager.go | 2 +- pkg/config/config.go | 32 +++++++------- 4 files changed, 128 insertions(+), 67 deletions(-) diff --git a/pkg/channels/email.go b/pkg/channels/email.go index f79efc518..f305b6920 100644 --- a/pkg/channels/email.go +++ b/pkg/channels/email.go @@ -11,6 +11,7 @@ import ( "net/smtp" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -175,7 +176,7 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error if port <= 0 { port = 465 } - addr := fmt.Sprintf("%s:%d", c.config.SMTPServer, port) //nolint:govet // format string is safe, this is domain:port + addr := net.JoinHostPort(c.config.SMTPServer, strconv.Itoa(port)) host := c.config.SMTPServer if c.config.SMTPUseTLS { @@ -230,9 +231,7 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error // Some servers on 587 do not require STARTTLS; continue anyway logger.WarnCF("email", "STARTTLS failed, connection may be unencrypted; credentials could be sent in plaintext", - map[string]interface{}{ - "error": err.Error(), - }) + map[string]any{"error": err.Error()}) _ = err } auth := smtp.PlainAuth("", c.config.Username, c.config.Password, host) @@ -317,7 +316,7 @@ func (c *EmailChannel) connect() error { } } - logger.InfoCF("email", "Connected to IMAP server", map[string]interface{}{ + logger.InfoCF("email", "Connected to IMAP server", map[string]any{ "server": c.config.IMAPServer, "mailbox": mailbox, "last_uid": c.lastUID, @@ -403,7 +402,7 @@ func (c *EmailChannel) reconnectWithBackoff(ctx context.Context) error { if err == nil { return nil } - logger.ErrorCF("email", "IMAP reconnect failed, retrying with backoff", map[string]interface{}{ + logger.ErrorCF("email", "IMAP reconnect failed, retrying with backoff", map[string]any{ "error": err.Error(), "backoff": backoff.String(), }) timer := time.NewTimer(backoff) @@ -480,7 +479,7 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati } if cl.State() != imap.SelectedState { if err := c.reconnectWithBackoff(ctx); err != nil { - logger.ErrorCF("email", "Failed to reconnect after IDLE error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF("email", "Failed to reconnect after IDLE error", map[string]any{"error": err.Error()}) return } continue @@ -510,10 +509,14 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati c.imapClient.Updates = nil } c.mu.Unlock() - logger.ErrorCF("email", "IDLE ended with error after update", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF("email", "IDLE ended with error after update", map[string]any{"error": err.Error()}) if err := c.reconnectWithBackoff(ctx); err != nil { // reconnect failed, exit IDLE loop - logger.ErrorCF("email", "Failed to reconnect after IDLE error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF( + "email", + "Failed to reconnect after IDLE error", + map[string]any{"error": err.Error()}, + ) return } } @@ -526,10 +529,14 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati c.imapClient.Updates = nil } c.mu.Unlock() - logger.ErrorCF("email", "IDLE ended with error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF("email", "IDLE ended with error", map[string]any{"error": err.Error()}) if err := c.reconnectWithBackoff(ctx); err != nil { // reconnect failed , exit IDLE loop - logger.ErrorCF("email", "Failed to reconnect after IDLE error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF( + "email", + "Failed to reconnect after IDLE error", + map[string]any{"error": err.Error()}, + ) return } } @@ -572,13 +579,13 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) { uids, err := cl.UidSearch(criteria) if err != nil { - logger.ErrorCF("email", "Failed to search emails", map[string]interface{}{ + logger.ErrorCF("email", "Failed to search emails", map[string]any{ "error": err.Error(), }) c.closeIMAPClient() if err := c.reconnectWithBackoff(ctx); err != nil { logger.ErrorCF("email", "Failed to reconnect after search emails error", - map[string]interface{}{"error": err.Error()}) + map[string]any{"error": err.Error()}) return } continue @@ -615,20 +622,27 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) { // Mark as seen after fully read seenSet := new(imap.SeqSet) seenSet.AddNum(msg.Uid) - if err := cl.UidStore(seenSet, imap.FormatFlagsOp(imap.AddFlags, true), []interface{}{imap.SeenFlag}, nil); err != nil { - logger.DebugCF("email", "Failed to mark email as seen", map[string]interface{}{ + if err := cl.UidStore( + seenSet, + imap.FormatFlagsOp(imap.AddFlags, true), + []any{imap.SeenFlag}, + nil, + ); err != nil { + logger.DebugCF("email", "Failed to mark email as seen", map[string]any{ "uid": msg.Uid, "error": err.Error(), }) } } if err := <-done; err != nil { - logger.ErrorCF("email", "Failed to fetch emails", map[string]interface{}{ + logger.ErrorCF("email", "Failed to fetch emails", map[string]any{ "error": err.Error(), }) c.closeIMAPClient() if err := c.reconnectWithBackoff(ctx); err != nil { - logger.ErrorCF("email", "Failed to reconnect after fetch emails error", map[string]interface{}{"error": err.Error()}) + logger.ErrorCF("email", "Failed to reconnect after fetch emails error", map[string]any{ + "error": err.Error(), + }) return } continue @@ -671,7 +685,7 @@ func (c *EmailChannel) processEmail(msg *imap.Message) { // Check allowlist if !c.IsAllowed(senderID) { - logger.DebugCF("email", "Email from unauthorized sender", map[string]interface{}{ + logger.DebugCF("email", "Email from unauthorized sender", map[string]any{ "sender": senderID, }) return @@ -698,7 +712,7 @@ func (c *EmailChannel) processEmail(msg *imap.Message) { metadata["to"] = fmt.Sprintf("%s@%s", to.MailboxName, to.HostName) } - logger.InfoCF("email", "Email received", map[string]interface{}{ + logger.InfoCF("email", "Email received", map[string]any{ "sender_id": senderID, "subject": envelope.Subject, "preview": utils.Truncate(content, 80), @@ -722,7 +736,7 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten bodySection := &imap.BodySectionName{} bodyReader := msg.GetBody(bodySection) if bodyReader == nil { - logger.DebugCF("email", "No body in FETCH response", map[string]interface{}{"uid": msg.Uid}) + logger.DebugCF("email", "No body in FETCH response", map[string]any{"uid": msg.Uid}) if subject != "" { return fmt.Sprintf("Subject: %s\n\n[No body content]", subject), nil } @@ -731,7 +745,7 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten mr, err := mail.CreateReader(bodyReader) if err != nil { - logger.DebugCF("email", "Failed to create mail reader", map[string]interface{}{"error": err.Error()}) + logger.DebugCF("email", "Failed to create mail reader", map[string]any{"error": err.Error()}) if subject != "" { return fmt.Sprintf("Subject: %s\n\n[Failed to parse email body]", subject), nil } @@ -750,7 +764,7 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten break } if err != nil { - logger.DebugCF("email", "Failed to read email part", map[string]interface{}{"error": err.Error()}) + logger.DebugCF("email", "Failed to read email part", map[string]any{"error": err.Error()}) continue } @@ -771,7 +785,8 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten mediaPaths = append(mediaPaths, localPath) attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filepath.Base(localPath))) } else { - attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s (save failed, you can check the attachment size limit in the config(attachment_max_bytes))]", filename)) + attachmentRefs = append(attachmentRefs, + fmt.Sprintf("[attachment: %s (save failed, check attachment_max_bytes in config)]", filename)) } } else { attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filename)) @@ -789,7 +804,13 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten continue } if len(body) > int(limit) { - textParts = append(textParts, fmt.Sprintf("[body part exceeds size limit (max %d bytes), you can check body_part_max_bytes in config]", limit)) + textParts = append( + textParts, + fmt.Sprintf( + "[body part exceeds size limit (max %d bytes), you can check body_part_max_bytes in config]", + limit, + ), + ) continue } bodyStr := strings.TrimSpace(string(body)) @@ -839,7 +860,7 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str return "" } if err := os.MkdirAll(dir, 0o700); err != nil { - logger.DebugCF("email", "Failed to create attachment dir", map[string]interface{}{"error": err.Error(), "dir": dir}) + logger.DebugCF("email", "Failed to create attachment dir", map[string]any{"error": err.Error(), "dir": dir}) return "" } limit := int64(c.config.AttachmentMaxBytes) @@ -855,7 +876,11 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str localPath := filepath.Join(dir, localName) f, err := os.Create(localPath) if err != nil { - logger.DebugCF("email", "Failed to create attachment file", map[string]interface{}{"error": err.Error(), "path": localPath}) + logger.DebugCF( + "email", + "Failed to create attachment file", + map[string]any{"error": err.Error(), "path": localPath}, + ) return "" } defer f.Close() @@ -864,12 +889,16 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str n, err := io.Copy(f, limited) if err != nil { _ = os.Remove(localPath) - logger.DebugCF("email", "Failed to write attachment", map[string]interface{}{"error": err.Error(), "path": localPath}) + logger.DebugCF("email", "Failed to write attachment", map[string]any{"error": err.Error(), "path": localPath}) return "" } if n > limit { _ = os.Remove(localPath) - logger.DebugCF("email", "Attachment exceeds size limit, skipped", map[string]interface{}{"path": localPath, "limit": limit}) + logger.DebugCF( + "email", + "Attachment exceeds size limit, skipped", + map[string]any{"path": localPath, "limit": limit}, + ) return "" } return localPath diff --git a/pkg/channels/email_test.go b/pkg/channels/email_test.go index 3b1647511..5637fa1b1 100644 --- a/pkg/channels/email_test.go +++ b/pkg/channels/email_test.go @@ -68,9 +68,21 @@ func TestEmailChannel_decodeRFC2047Filename(t *testing.T) { s string want string }{ - {name: "normal", s: "正常.png", want: "正常.png"}, - {name: "GB2312-Quoted-Printable", s: "=?GB2312?Q?gb2312=B2=E2=CA=D4=B2=E2=CA=D4.png?=", want: "gb2312测试测试.png"}, - {name: "GBK-Base64", s: "=?GBK?B?yfqzybLiytTNvMasLnBuZw==?=", want: "生成测试图片.png"}, + { + name: "normal", + s: "正常.png", //nolint:gosmopolitan + want: "正常.png", //nolint:gosmopolitan + }, + { + name: "GB2312-Quoted-Printable", + s: "=?GB2312?Q?gb2312=B2=E2=CA=D4=B2=E2=CA=D4.png?=", + want: "gb2312测试测试.png", //nolint:gosmopolitan + }, + { + name: "GBK-Base64", + s: "=?GBK?B?yfqzybLiytTNvMasLnBuZw==?=", + want: "生成测试图片.png", //nolint:gosmopolitan + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -109,7 +121,9 @@ func TestEmailChannel_extractEmailBodyAndAttachments(t *testing.T) { }) t.Run("html body", func(t *testing.T) { - mimeBytes := []byte("From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Hello world

") + mimeBytes := []byte( + "From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/html; charset=utf-8\r\n\r\n

Hello world

", + ) section := &imap.BodySectionName{} msg := &imap.Message{ Uid: 1, @@ -182,7 +196,11 @@ func TestEmailChannel_extractEmailBodyAndAttachments(t *testing.T) { Body: map[*imap.BodySectionName]imap.Literal{section: bytes.NewReader(mimeBytes)}, } content, paths := newLimitClient.extractEmailBodyAndAttachments(msg) - assert.Contains(t, content, "[body part exceeds size limit (max 1 bytes), you can check body_part_max_bytes in config]") + assert.Contains( + t, + content, + "[body part exceeds size limit (max 1 bytes), you can check body_part_max_bytes in config]", + ) assert.NotEmpty(t, paths) assert.Equal(t, 1, len(paths)) assert.Contains(t, paths[0], filepath.Base(paths[0])) @@ -271,21 +289,28 @@ func TestEmailChannel_checkNewEmails(t *testing.T) { mockClient := &client.Client{} c.imapClient = mockClient // mock login and select to return mockClient - mockey.Mock(mockey.GetMethod(mockClient, "Login")).To(func(imapClient *client.Client, username, password string) error { - return nil - }).Build() - mockey.Mock(mockey.GetMethod(mockClient, "Select")).To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) { - return &imap.MailboxStatus{ - UidNext: 20, - }, nil - }).Build() - mockey.Mock(mockey.GetMethod(mockClient, "UidSearch")).To(func(imapClient *client.Client, criteria *imap.SearchCriteria) ([]uint32, error) { - return []uint32{21}, nil - }).Build() + mockey.Mock(mockey.GetMethod(mockClient, "Login")). + To(func(imapClient *client.Client, username, password string) error { + return nil + }). + Build() + mockey.Mock(mockey.GetMethod(mockClient, "Select")). + To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) { + return &imap.MailboxStatus{ + UidNext: 20, + }, nil + }). + Build() + mockey.Mock(mockey.GetMethod(mockClient, "UidSearch")). + To(func(imapClient *client.Client, criteria *imap.SearchCriteria) ([]uint32, error) { + return []uint32{21}, nil + }). + Build() mockey.Mock(mockey.GetMethod(mockClient, "UidFetch")).To( func(imapClient *client.Client, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { mimeBytes := []byte( - "From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello world") + "From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello world", + ) section := &imap.BodySectionName{} msg := &imap.Message{ Uid: 1, @@ -303,7 +328,7 @@ func TestEmailChannel_checkNewEmails(t *testing.T) { return imap.SelectedState }).Build() mockey.Mock(mockey.GetMethod(mockClient, "UidStore")).To( - func(imapClient *client.Client, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, + func(imapClient *client.Client, seqset *imap.SeqSet, item imap.StoreItem, value any, ch chan *imap.Message, ) error { return nil @@ -311,7 +336,8 @@ func TestEmailChannel_checkNewEmails(t *testing.T) { // --------------- mock end --------------- ctx := context.Background() c.CheckNewEmails(context.Background()) - timeoutCtx, _ := context.WithTimeout(ctx, time.Second) // nolint:govet // context.WithTimeout is safe + timeoutCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() messge, ok := c.bus.ConsumeInbound(timeoutCtx) assert.True(t, ok) assert.True(t, strings.Contains(messge.Content, "Hello world")) @@ -351,11 +377,13 @@ func TestEmailChannel_runIdleLoop(t *testing.T) { return imap.SelectedState }).Build() triggerChannel := make(chan struct{}, 1) - mockey.Mock(mockey.GetMethod(mockClient, "Idle")).To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error { - <-triggerChannel - self.Updates <- &client.StatusUpdate{} - return nil - }).Build() + mockey.Mock(mockey.GetMethod(mockClient, "Idle")). + To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error { + <-triggerChannel + self.Updates <- &client.StatusUpdate{} + return nil + }). + Build() // --------------- mock end --------------- ctx := context.Background() go c.runIdleLoop(ctx, 2*time.Second) diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 004042bf6..6be337dec 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -179,7 +179,7 @@ func (m *Manager) initChannels() error { logger.DebugC("channels", "Attempting to initialize Email channel") email, err := NewEmailChannel(m.config.Channels.Email, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Email channel", map[string]interface{}{ + logger.ErrorCF("channels", "Failed to initialize Email channel", map[string]any{ "error": err.Error(), }) } else { diff --git a/pkg/config/config.go b/pkg/config/config.go index 9a4c4fc61..b9f4db65a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -264,25 +264,29 @@ type LINEConfig struct { } type EmailConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"` - IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"` - IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"` - Username string `json:"username" env:"PICOCLAW_CHANNELS_EMAIL_USERNAME"` - Password string `json:"password" env:"PICOCLAW_CHANNELS_EMAIL_PASSWORD"` - Mailbox string `json:"mailbox" env:"PICOCLAW_CHANNELS_EMAIL_MAILBOX"` // 默认 "INBOX" - CheckInterval int `json:"check_interval" env:"PICOCLAW_CHANNELS_EMAIL_CHECK_INTERVAL"` // seconds, default 30; polling when IDLE disabled - UseTLS bool `json:"use_tls" env:"PICOCLAW_CHANNELS_EMAIL_USE_TLS"` - // ForcedPolling: when the mail server does not implement IDLE/NOOP per spec, set true to use app-level polling at CheckInterval. + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"` + IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"` + IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"` + Username string `json:"username" env:"PICOCLAW_CHANNELS_EMAIL_USERNAME"` + Password string `json:"password" env:"PICOCLAW_CHANNELS_EMAIL_PASSWORD"` + Mailbox string `json:"mailbox" env:"PICOCLAW_CHANNELS_EMAIL_MAILBOX"` // 默认 "INBOX" + // seconds, default 30; polling when IDLE disabled + CheckInterval int `json:"check_interval" env:"PICOCLAW_CHANNELS_EMAIL_CHECK_INTERVAL"` + UseTLS bool `json:"use_tls" env:"PICOCLAW_CHANNELS_EMAIL_USE_TLS"` + // ForcedPolling: when the mail server does not implement IDLE/NOOP per spec, + // set true to use app-level polling at CheckInterval. ForcedPolling bool `json:"forced_polling" env:"PICOCLAW_CHANNELS_EMAIL_FORCED_POLLING"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_EMAIL_ALLOW_FROM"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_EMAIL_ALLOW_FROM"` AttachmentDir string `json:"attachment_dir" env:"PICOCLAW_CHANNELS_EMAIL_ATTACHMENT_DIR"` // max size per attachment (default 25*1024*1024(25MB)), 0 = use default - AttachmentMaxBytes int `json:"attachment_max_bytes" env:"PICOCLAW_CHANNELS_EMAIL_ATTACHMENT_MAX_BYTES"` // max size per attachment (default 25MB), 0 = use default - BodyPartMaxBytes int `json:"body_part_max_bytes" env:"PICOCLAW_CHANNELS_EMAIL_BODY_PART_MAX_BYTES"` // max size per body part (text/plain, text/html) to avoid unbounded io.ReadAll (default 1MB), 0 = use default + AttachmentMaxBytes int `json:"attachment_max_bytes" env:"PICOCLAW_CHANNELS_EMAIL_ATTACHMENT_MAX_BYTES"` + // max size per body part (text/plain, text/html) to avoid unbounded io.ReadAll (default 1MB), 0 = use default + BodyPartMaxBytes int `json:"body_part_max_bytes" env:"PICOCLAW_CHANNELS_EMAIL_BODY_PART_MAX_BYTES"` // SMTP send (optional, if not configured, Send is not available) SMTPServer string `json:"smtp_server" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_SERVER"` - SMTPPort int `json:"smtp_port" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"` // 465 或 587 - SMTPUseTLS bool `json:"smtp_use_tls" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USE_TLS"` // 465 用 true,587 可用 false+STARTTLS + SMTPPort int `json:"smtp_port" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"` // 465 或 587 + // 465 用 true,587 可用 false+STARTTLS + SMTPUseTLS bool `json:"smtp_use_tls" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USE_TLS"` } type OneBotConfig struct {