smtp2shoutrrr/email.go
Felipe M. ca2f426cee
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
fix: handle all email content types and preserve query parameters
The Body() method now correctly handles:
- Plain text/plain and text/html emails (not just multipart)
- All multipart types (mixed, related, etc., not just alternative)
- Falls back to HTML if text/plain is not available
- Adds debug logging for body length and content type

Additionally, when sending to multiple targets, query parameters are now
merged instead of replaced, preserving service-specific parameters like
Mattermost's username, icon, and channel configuration.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 16:31:13 +01:00

92 lines
2.3 KiB
Go

package smtp2shoutrrr
import (
"bytes"
"fmt"
"io"
"log"
"log/slog"
"mime"
"mime/multipart"
"net/mail"
"strings"
)
type ReceivedEmail struct {
Recipients []string
Msg *mail.Message
body string
}
func (re *ReceivedEmail) Body() (string, error) {
if re.body == "" {
contentType := re.Msg.Header.Get("Content-Type")
if contentType == "" {
body, err := io.ReadAll(re.Msg.Body)
if err != nil {
return "", fmt.Errorf("failed to read email body: %w", err)
}
re.body = string(body)
} else {
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
log.Fatalf("Failed to parse Content-Type: %v", err)
}
if strings.HasPrefix(mediaType, "multipart/") {
// Handle any multipart type (alternative, mixed, related, etc.)
mr := multipart.NewReader(re.Msg.Body, params["boundary"])
var htmlBody string
for {
part, err := mr.NextPart()
if err != nil {
break
}
partContentType := part.Header.Get("Content-Type")
slog.Debug("Processing email part", slog.String("content_type", partContentType))
// Prefer text/plain, but keep HTML as fallback
if strings.HasPrefix(partContentType, "text/plain") {
body := new(bytes.Buffer)
_, err = body.ReadFrom(part)
_ = part.Close()
if err != nil {
slog.Error("Failed to read part body", slog.String("err", err.Error()))
return "", fmt.Errorf("failed to read part body: %w", err)
}
re.body = body.String()
break // Found text/plain, use it
} else if strings.HasPrefix(partContentType, "text/html") && htmlBody == "" {
// Store HTML as fallback if no text/plain found
body := new(bytes.Buffer)
_, err = body.ReadFrom(part)
_ = part.Close()
if err == nil {
htmlBody = body.String()
}
} else {
_ = part.Close()
}
}
// If no text/plain was found, use HTML
if re.body == "" && htmlBody != "" {
re.body = htmlBody
}
} else if strings.HasPrefix(mediaType, "text/") {
// Handle non-multipart text content (text/plain, text/html, etc.)
body, err := io.ReadAll(re.Msg.Body)
if err != nil {
return "", fmt.Errorf("failed to read email body: %w", err)
}
re.body = string(body)
}
}
}
return re.body, nil
}