diff --git a/pkg/channels/email.go b/pkg/channels/email.go
index 4c9c916e7..a3a801dc0 100644
--- a/pkg/channels/email.go
+++ b/pkg/channels/email.go
@@ -38,6 +38,8 @@ const (
reconnectBackoffMax = 10 * time.Minute
// default attachment max bytes
defaultAttachmentMaxBytes = 25 * 1024 * 1024 // 25MB
+ // max bytes to read per body part (text/plain, text/html) to avoid unbounded io.ReadAll
+ defaultBodyPartMaxBytes = 1 * 1024 * 1024 // 1MB
)
type EmailChannel struct {
@@ -764,10 +766,19 @@ func (c *EmailChannel) extractEmailBodyAndAttachments(msg *imap.Message) (conten
continue
}
- body, err := io.ReadAll(p.Body)
+ limit := int64(c.config.BodyPartMaxBytes)
+ if limit <= 0 {
+ limit = int64(defaultBodyPartMaxBytes)
+ }
+ limitedBody := io.LimitReader(p.Body, limit+1)
+ body, err := io.ReadAll(limitedBody)
if err != nil || len(body) == 0 {
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))
+ continue
+ }
bodyStr := strings.TrimSpace(string(body))
if bodyStr == "" {
continue
diff --git a/pkg/channels/email_test.go b/pkg/channels/email_test.go
index b9dc6b469..3086da457 100644
--- a/pkg/channels/email_test.go
+++ b/pkg/channels/email_test.go
@@ -112,36 +112,37 @@ func TestEmailChannel_extractEmailBodyAndAttachments(t *testing.T) {
assert.Empty(t, paths)
})
+ attachmentwithText := []byte(
+ "From: a@b.com\r\n" +
+ "To: c@d.com\r\n" +
+ "Subject: test-file-and-body\r\n" +
+ "Content-Type: multipart/mixed; boundary=\"outer\"\r\n" +
+ "MIME-Version: 1.0\r\n" +
+ "\r\n" +
+ "--outer\r\n" +
+ "Content-Type: multipart/alternative; boundary=\"alt\"\r\n" +
+ "\r\n" +
+ "--alt\r\n" +
+ "Content-Type: text/plain; charset=GBK\r\n" +
+ "Content-Transfer-Encoding: 7bit\r\n" +
+ "\r\n" +
+ "this body\r\n" +
+ "--alt\r\n" +
+ "Content-Type: text/html; charset=GBK\r\n" +
+ "Content-Transfer-Encoding: 7bit\r\n" +
+ "\r\n" +
+ "
this body
\r\n" +
+ "--alt--\r\n" +
+ "\r\n" +
+ "--outer\r\n" +
+ "Content-Type: text/plain; name=test.txt\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "Content-Disposition: attachment; filename=\"test.txt\"\r\n" +
+ "\r\n" +
+ "VGVzdC0xMTEx\r\n" +
+ "--outer--\r\n")
t.Run("attachment and text body", func(t *testing.T) {
- mimeBytes := []byte(
- "From: a@b.com\r\n" +
- "To: c@d.com\r\n" +
- "Subject: test-file-and-body\r\n" +
- "Content-Type: multipart/mixed; boundary=\"outer\"\r\n" +
- "MIME-Version: 1.0\r\n" +
- "\r\n" +
- "--outer\r\n" +
- "Content-Type: multipart/alternative; boundary=\"alt\"\r\n" +
- "\r\n" +
- "--alt\r\n" +
- "Content-Type: text/plain; charset=GBK\r\n" +
- "Content-Transfer-Encoding: 7bit\r\n" +
- "\r\n" +
- "this body\r\n" +
- "--alt\r\n" +
- "Content-Type: text/html; charset=GBK\r\n" +
- "Content-Transfer-Encoding: 7bit\r\n" +
- "\r\n" +
- "this body
\r\n" +
- "--alt--\r\n" +
- "\r\n" +
- "--outer\r\n" +
- "Content-Type: text/plain; name=test.txt\r\n" +
- "Content-Transfer-Encoding: base64\r\n" +
- "Content-Disposition: attachment; filename=\"test.txt\"\r\n" +
- "\r\n" +
- "VGVzdC0xMTEx\r\n" +
- "--outer--\r\n")
+ mimeBytes := attachmentwithText
section := &imap.BodySectionName{}
msg := &imap.Message{
Uid: 1,
@@ -155,6 +156,28 @@ func TestEmailChannel_extractEmailBodyAndAttachments(t *testing.T) {
assert.Contains(t, paths[0], filepath.Base(paths[0]))
})
+ newLimitClient := &EmailChannel{
+ config: config.EmailConfig{
+ BodyPartMaxBytes: 1,
+ AttachmentDir: t.TempDir(),
+ AttachmentMaxBytes: 1024,
+ },
+ }
+ t.Run("body part max bytes", func(t *testing.T) {
+
+ mimeBytes := attachmentwithText
+ section := &imap.BodySectionName{}
+ msg := &imap.Message{
+ Uid: 1,
+ Envelope: &imap.Envelope{Subject: "Test"},
+ 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.NotEmpty(t, paths)
+ assert.Equal(t, 1, len(paths))
+ assert.Contains(t, paths[0], filepath.Base(paths[0]))
+ })
}
func TestEmailChannel_extractTextFromHTML(t *testing.T) {
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 3277d9d06..4b2eeafe7 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -163,6 +163,7 @@ type EmailConfig struct {
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
// 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