- 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.
94 lines
3.0 KiB
Go
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...)
|
|
}
|