package smtp2shoutrrr import ( "bytes" "errors" "fmt" "io" "log/slog" "mime" "mime/multipart" "net/mail" "strings" ) // errMalformedMessage marks a message this server can never turn into a // notification. Redelivering the same bytes produces the same failure, so the // sender is told to give up rather than retry forever. var errMalformedMessage = errors.New("malformed message") 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 { return "", fmt.Errorf("%w: parsing Content-Type %q: %w", errMalformedMessage, contentType, 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 }