ultisuite-backend/internal/api/mail/validate_drafts.go
R3D347HR4Y 4eadb91a64 Enhance mail API with rate limiting, idempotency, and attachment management
- Added rate limiting for outbound email sends to prevent abuse, implemented in `internal/api/mail/sendguard`.
- Introduced idempotency key support for email sending to avoid duplicate submissions.
- Enhanced attachment handling with new limits and validation in `internal/api/mail/limits`.
- Updated outbox processing to include retry logic and circuit breaker for SMTP failures.
- Improved HTML sanitization for email content to enhance security.
- Added unit tests for new features, ensuring robust functionality and error handling.
- Updated configuration options in `.env.example` for new mail settings.
2026-05-22 17:19:16 +02:00

94 lines
3.0 KiB
Go

package mail
import (
"encoding/json"
"strconv"
"strings"
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
"github.com/ultisuite/ulti-backend/internal/mail/limits"
)
type draftRequest struct {
AccountID string `json:"account_id"`
IdentityID string `json:"identity_id"`
To []string `json:"to"`
Cc []string `json:"cc"`
Bcc []string `json:"bcc"`
Subject string `json:"subject"`
BodyText string `json:"body_text"`
BodyHTML string `json:"body_html"`
InReplyTo string `json:"in_reply_to"`
Attachments any `json:"attachments"`
}
func validateDraftRecipients(req *draftRequest) []apivalidate.FieldDetail {
var details []apivalidate.FieldDetail
for i, addr := range req.To {
if d := validateRecipient(addr); d != nil {
d.Field = "to[" + strconv.Itoa(i) + "]"
details = append(details, *d)
}
}
for i, addr := range req.Cc {
if d := validateRecipient(addr); d != nil {
d.Field = "cc[" + strconv.Itoa(i) + "]"
details = append(details, *d)
}
}
for i, addr := range req.Bcc {
if d := validateRecipient(addr); d != nil {
d.Field = "bcc[" + strconv.Itoa(i) + "]"
details = append(details, *d)
}
}
return details
}
func validateDraftContent(req *draftRequest) []apivalidate.FieldDetail {
var details []apivalidate.FieldDetail
if len(req.Subject) > maxSubjectLen {
details = append(details, apivalidate.FieldDetail{Field: "subject", Message: "too long"})
}
if len(req.BodyText) > limits.MaxBodyFieldBytes {
details = append(details, apivalidate.FieldDetail{Field: "body_text", Message: "too long"})
}
if len(req.BodyHTML) > limits.MaxBodyFieldBytes {
details = append(details, apivalidate.FieldDetail{Field: "body_html", Message: "too long"})
}
if req.InReplyTo != "" && len(req.InReplyTo) > 998 {
details = append(details, apivalidate.FieldDetail{Field: "in_reply_to", Message: "too long"})
}
if req.Attachments != nil {
if b, err := json.Marshal(req.Attachments); err != nil {
details = append(details, apivalidate.FieldDetail{Field: "attachments", Message: "invalid"})
} else if len(b) > limits.MaxSendRequestBodyBytes {
details = append(details, apivalidate.FieldDetail{Field: "attachments", Message: "too large"})
}
}
return details
}
func validateCreateDraft(req *draftRequest) *apivalidate.ValidationError {
var details []apivalidate.FieldDetail
if strings.TrimSpace(req.AccountID) == "" {
details = append(details, apivalidate.FieldDetail{Field: "account_id", Message: "required"})
}
details = append(details, validateDraftRecipients(req)...)
details = append(details, validateDraftContent(req)...)
if len(details) == 0 {
return nil
}
return apivalidate.NewValidationError(details...)
}
func validateUpdateDraft(req *draftRequest) *apivalidate.ValidationError {
var details []apivalidate.FieldDetail
details = append(details, validateDraftRecipients(req)...)
details = append(details, validateDraftContent(req)...)
if len(details) == 0 {
return nil
}
return apivalidate.NewValidationError(details...)
}