style: ggolangci-lin and go vet fix
This commit is contained in:
parent
7f80199342
commit
4fa489cb02
4 changed files with 128 additions and 67 deletions
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -175,7 +176,7 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
if port <= 0 {
|
if port <= 0 {
|
||||||
port = 465
|
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
|
host := c.config.SMTPServer
|
||||||
|
|
||||||
if c.config.SMTPUseTLS {
|
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
|
// Some servers on 587 do not require STARTTLS; continue anyway
|
||||||
logger.WarnCF("email",
|
logger.WarnCF("email",
|
||||||
"STARTTLS failed, connection may be unencrypted; credentials could be sent in plaintext",
|
"STARTTLS failed, connection may be unencrypted; credentials could be sent in plaintext",
|
||||||
map[string]interface{}{
|
map[string]any{"error": err.Error()})
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
_ = err
|
_ = err
|
||||||
}
|
}
|
||||||
auth := smtp.PlainAuth("", c.config.Username, c.config.Password, host)
|
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,
|
"server": c.config.IMAPServer,
|
||||||
"mailbox": mailbox,
|
"mailbox": mailbox,
|
||||||
"last_uid": c.lastUID,
|
"last_uid": c.lastUID,
|
||||||
|
|
@ -403,7 +402,7 @@ func (c *EmailChannel) reconnectWithBackoff(ctx context.Context) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 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(),
|
"error": err.Error(), "backoff": backoff.String(),
|
||||||
})
|
})
|
||||||
timer := time.NewTimer(backoff)
|
timer := time.NewTimer(backoff)
|
||||||
|
|
@ -480,7 +479,7 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati
|
||||||
}
|
}
|
||||||
if cl.State() != imap.SelectedState {
|
if cl.State() != imap.SelectedState {
|
||||||
if err := c.reconnectWithBackoff(ctx); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -510,10 +509,14 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati
|
||||||
c.imapClient.Updates = nil
|
c.imapClient.Updates = nil
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
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 {
|
if err := c.reconnectWithBackoff(ctx); err != nil {
|
||||||
// reconnect failed, exit IDLE loop
|
// 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -526,10 +529,14 @@ func (c *EmailChannel) runIdleLoop(ctx context.Context, pollInterval time.Durati
|
||||||
c.imapClient.Updates = nil
|
c.imapClient.Updates = nil
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
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 {
|
if err := c.reconnectWithBackoff(ctx); err != nil {
|
||||||
// reconnect failed , exit IDLE loop
|
// 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -572,13 +579,13 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) {
|
||||||
|
|
||||||
uids, err := cl.UidSearch(criteria)
|
uids, err := cl.UidSearch(criteria)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
c.closeIMAPClient()
|
c.closeIMAPClient()
|
||||||
if err := c.reconnectWithBackoff(ctx); err != nil {
|
if err := c.reconnectWithBackoff(ctx); err != nil {
|
||||||
logger.ErrorCF("email", "Failed to reconnect after search emails error",
|
logger.ErrorCF("email", "Failed to reconnect after search emails error",
|
||||||
map[string]interface{}{"error": err.Error()})
|
map[string]any{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -615,20 +622,27 @@ func (c *EmailChannel) CheckNewEmails(ctx context.Context) {
|
||||||
// Mark as seen after fully read
|
// Mark as seen after fully read
|
||||||
seenSet := new(imap.SeqSet)
|
seenSet := new(imap.SeqSet)
|
||||||
seenSet.AddNum(msg.Uid)
|
seenSet.AddNum(msg.Uid)
|
||||||
if err := cl.UidStore(seenSet, imap.FormatFlagsOp(imap.AddFlags, true), []interface{}{imap.SeenFlag}, nil); err != nil {
|
if err := cl.UidStore(
|
||||||
logger.DebugCF("email", "Failed to mark email as seen", map[string]interface{}{
|
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(),
|
"uid": msg.Uid, "error": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := <-done; err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
c.closeIMAPClient()
|
c.closeIMAPClient()
|
||||||
if err := c.reconnectWithBackoff(ctx); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -671,7 +685,7 @@ func (c *EmailChannel) processEmail(msg *imap.Message) {
|
||||||
|
|
||||||
// Check allowlist
|
// Check allowlist
|
||||||
if !c.IsAllowed(senderID) {
|
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,
|
"sender": senderID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -698,7 +712,7 @@ func (c *EmailChannel) processEmail(msg *imap.Message) {
|
||||||
metadata["to"] = fmt.Sprintf("%s@%s", to.MailboxName, to.HostName)
|
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,
|
"sender_id": senderID,
|
||||||
"subject": envelope.Subject,
|
"subject": envelope.Subject,
|
||||||
"preview": utils.Truncate(content, 80),
|
"preview": utils.Truncate(content, 80),
|
||||||
|
|
@ -722,7 +736,7 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten
|
||||||
bodySection := &imap.BodySectionName{}
|
bodySection := &imap.BodySectionName{}
|
||||||
bodyReader := msg.GetBody(bodySection)
|
bodyReader := msg.GetBody(bodySection)
|
||||||
if bodyReader == nil {
|
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 != "" {
|
if subject != "" {
|
||||||
return fmt.Sprintf("Subject: %s\n\n[No body content]", subject), nil
|
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)
|
mr, err := mail.CreateReader(bodyReader)
|
||||||
if err != nil {
|
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 != "" {
|
if subject != "" {
|
||||||
return fmt.Sprintf("Subject: %s\n\n[Failed to parse email body]", subject), nil
|
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
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -771,7 +785,8 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten
|
||||||
mediaPaths = append(mediaPaths, localPath)
|
mediaPaths = append(mediaPaths, localPath)
|
||||||
attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filepath.Base(localPath)))
|
attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filepath.Base(localPath)))
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filename))
|
attachmentRefs = append(attachmentRefs, fmt.Sprintf("[attachment: %s]", filename))
|
||||||
|
|
@ -789,7 +804,13 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(body) > int(limit) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
bodyStr := strings.TrimSpace(string(body))
|
bodyStr := strings.TrimSpace(string(body))
|
||||||
|
|
@ -839,7 +860,7 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
limit := int64(c.config.AttachmentMaxBytes)
|
limit := int64(c.config.AttachmentMaxBytes)
|
||||||
|
|
@ -855,7 +876,11 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str
|
||||||
localPath := filepath.Join(dir, localName)
|
localPath := filepath.Join(dir, localName)
|
||||||
f, err := os.Create(localPath)
|
f, err := os.Create(localPath)
|
||||||
if err != nil {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
@ -864,12 +889,16 @@ func (c *EmailChannel) saveAttachmentToLocal(uid uint32, index int, filename str
|
||||||
n, err := io.Copy(f, limited)
|
n, err := io.Copy(f, limited)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(localPath)
|
_ = 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 ""
|
return ""
|
||||||
}
|
}
|
||||||
if n > limit {
|
if n > limit {
|
||||||
_ = os.Remove(localPath)
|
_ = 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 ""
|
||||||
}
|
}
|
||||||
return localPath
|
return localPath
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,21 @@ func TestEmailChannel_decodeRFC2047Filename(t *testing.T) {
|
||||||
s string
|
s string
|
||||||
want 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: "normal",
|
||||||
{name: "GBK-Base64", s: "=?GBK?B?yfqzybLiytTNvMasLnBuZw==?=", want: "生成测试图片.png"},
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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) {
|
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<html><body><h1>Hello world</h1></body></html>")
|
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<html><body><h1>Hello world</h1></body></html>",
|
||||||
|
)
|
||||||
section := &imap.BodySectionName{}
|
section := &imap.BodySectionName{}
|
||||||
msg := &imap.Message{
|
msg := &imap.Message{
|
||||||
Uid: 1,
|
Uid: 1,
|
||||||
|
|
@ -182,7 +196,11 @@ func TestEmailChannel_extractEmailBodyAndAttachments(t *testing.T) {
|
||||||
Body: map[*imap.BodySectionName]imap.Literal{section: bytes.NewReader(mimeBytes)},
|
Body: map[*imap.BodySectionName]imap.Literal{section: bytes.NewReader(mimeBytes)},
|
||||||
}
|
}
|
||||||
content, paths := newLimitClient.extractEmailBodyAndAttachments(msg)
|
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.NotEmpty(t, paths)
|
||||||
assert.Equal(t, 1, len(paths))
|
assert.Equal(t, 1, len(paths))
|
||||||
assert.Contains(t, paths[0], filepath.Base(paths[0]))
|
assert.Contains(t, paths[0], filepath.Base(paths[0]))
|
||||||
|
|
@ -271,21 +289,28 @@ func TestEmailChannel_checkNewEmails(t *testing.T) {
|
||||||
mockClient := &client.Client{}
|
mockClient := &client.Client{}
|
||||||
c.imapClient = mockClient
|
c.imapClient = mockClient
|
||||||
// mock login and select to return mockClient
|
// mock login and select to return mockClient
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Login")).To(func(imapClient *client.Client, username, password string) error {
|
mockey.Mock(mockey.GetMethod(mockClient, "Login")).
|
||||||
return nil
|
To(func(imapClient *client.Client, username, password string) error {
|
||||||
}).Build()
|
return nil
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Select")).To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) {
|
}).
|
||||||
return &imap.MailboxStatus{
|
Build()
|
||||||
UidNext: 20,
|
mockey.Mock(mockey.GetMethod(mockClient, "Select")).
|
||||||
}, nil
|
To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) {
|
||||||
}).Build()
|
return &imap.MailboxStatus{
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "UidSearch")).To(func(imapClient *client.Client, criteria *imap.SearchCriteria) ([]uint32, error) {
|
UidNext: 20,
|
||||||
return []uint32{21}, nil
|
}, nil
|
||||||
}).Build()
|
}).
|
||||||
|
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(
|
mockey.Mock(mockey.GetMethod(mockClient, "UidFetch")).To(
|
||||||
func(imapClient *client.Client, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
func(imapClient *client.Client, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
||||||
mimeBytes := []byte(
|
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{}
|
section := &imap.BodySectionName{}
|
||||||
msg := &imap.Message{
|
msg := &imap.Message{
|
||||||
Uid: 1,
|
Uid: 1,
|
||||||
|
|
@ -303,7 +328,7 @@ func TestEmailChannel_checkNewEmails(t *testing.T) {
|
||||||
return imap.SelectedState
|
return imap.SelectedState
|
||||||
}).Build()
|
}).Build()
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "UidStore")).To(
|
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,
|
ch chan *imap.Message,
|
||||||
) error {
|
) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -311,7 +336,8 @@ func TestEmailChannel_checkNewEmails(t *testing.T) {
|
||||||
// --------------- mock end ---------------
|
// --------------- mock end ---------------
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
c.CheckNewEmails(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)
|
messge, ok := c.bus.ConsumeInbound(timeoutCtx)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.True(t, strings.Contains(messge.Content, "Hello world"))
|
assert.True(t, strings.Contains(messge.Content, "Hello world"))
|
||||||
|
|
@ -351,11 +377,13 @@ func TestEmailChannel_runIdleLoop(t *testing.T) {
|
||||||
return imap.SelectedState
|
return imap.SelectedState
|
||||||
}).Build()
|
}).Build()
|
||||||
triggerChannel := make(chan struct{}, 1)
|
triggerChannel := make(chan struct{}, 1)
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Idle")).To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error {
|
mockey.Mock(mockey.GetMethod(mockClient, "Idle")).
|
||||||
<-triggerChannel
|
To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error {
|
||||||
self.Updates <- &client.StatusUpdate{}
|
<-triggerChannel
|
||||||
return nil
|
self.Updates <- &client.StatusUpdate{}
|
||||||
}).Build()
|
return nil
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
// --------------- mock end ---------------
|
// --------------- mock end ---------------
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
go c.runIdleLoop(ctx, 2*time.Second)
|
go c.runIdleLoop(ctx, 2*time.Second)
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ func (m *Manager) initChannels() error {
|
||||||
logger.DebugC("channels", "Attempting to initialize Email channel")
|
logger.DebugC("channels", "Attempting to initialize Email channel")
|
||||||
email, err := NewEmailChannel(m.config.Channels.Email, m.bus)
|
email, err := NewEmailChannel(m.config.Channels.Email, m.bus)
|
||||||
if err != nil {
|
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(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -264,25 +264,29 @@ type LINEConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"`
|
||||||
IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"`
|
IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"`
|
||||||
IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"`
|
IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"`
|
||||||
Username string `json:"username" env:"PICOCLAW_CHANNELS_EMAIL_USERNAME"`
|
Username string `json:"username" env:"PICOCLAW_CHANNELS_EMAIL_USERNAME"`
|
||||||
Password string `json:"password" env:"PICOCLAW_CHANNELS_EMAIL_PASSWORD"`
|
Password string `json:"password" env:"PICOCLAW_CHANNELS_EMAIL_PASSWORD"`
|
||||||
Mailbox string `json:"mailbox" env:"PICOCLAW_CHANNELS_EMAIL_MAILBOX"` // 默认 "INBOX"
|
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
|
// seconds, default 30; polling when IDLE disabled
|
||||||
UseTLS bool `json:"use_tls" env:"PICOCLAW_CHANNELS_EMAIL_USE_TLS"`
|
CheckInterval int `json:"check_interval" env:"PICOCLAW_CHANNELS_EMAIL_CHECK_INTERVAL"`
|
||||||
// ForcedPolling: when the mail server does not implement IDLE/NOOP per spec, set true to use app-level polling at CheckInterval.
|
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"`
|
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"`
|
AttachmentDir string `json:"attachment_dir" env:"PICOCLAW_CHANNELS_EMAIL_ATTACHMENT_DIR"`
|
||||||
// max size per attachment (default 25*1024*1024(25MB)), 0 = use default
|
// 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
|
AttachmentMaxBytes int `json:"attachment_max_bytes" env:"PICOCLAW_CHANNELS_EMAIL_ATTACHMENT_MAX_BYTES"`
|
||||||
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
|
// 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)
|
// SMTP send (optional, if not configured, Send is not available)
|
||||||
SMTPServer string `json:"smtp_server" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_SERVER"`
|
SMTPServer string `json:"smtp_server" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_SERVER"`
|
||||||
SMTPPort int `json:"smtp_port" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PORT"` // 465 或 587
|
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
|
// 465 用 true,587 可用 false+STARTTLS
|
||||||
|
SMTPUseTLS bool `json:"smtp_use_tls" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USE_TLS"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneBotConfig struct {
|
type OneBotConfig struct {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue