- Added a new endpoint for simulating rules based on sample messages, allowing users to test rule conditions and actions. - Enhanced webhook management with versioning, preview capabilities, and improved validation for webhook requests. - Updated service interfaces to support new functionalities, including max retries for webhooks and signing secrets. - Implemented observability metrics for webhook retries and dead-letter tracking, improving error handling and monitoring. - Enhanced unit tests to cover new simulation and webhook features, ensuring robust functionality and validation.
646 lines
20 KiB
Go
646 lines
20 KiB
Go
package mail
|
|
|
|
import (
|
|
"encoding/json"
|
|
"mime"
|
|
"net"
|
|
"net/mail"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/limits"
|
|
)
|
|
|
|
const (
|
|
maxAccountRequestBody = 32 << 10 // 32 KiB
|
|
maxWebhookRequestBody = 128 << 10 // 128 KiB
|
|
maxRulesRequestBody = 256 << 10 // 256 KiB
|
|
maxFlagsLabelsBody = 32 << 10 // 32 KiB
|
|
maxOutboxScheduleBody = 4 << 10 // 4 KiB
|
|
|
|
maxWebhookBodyTemplate = 64 << 10 // 64 KiB
|
|
maxWebhookHeaders = 20
|
|
maxHeaderNameLen = 256
|
|
maxHeaderValueLen = 8192
|
|
maxWebhookSecretLen = 512
|
|
defaultWebhookRetries = 3
|
|
maxWebhookRetries = 10
|
|
|
|
maxSubjectLen = 998
|
|
maxEmailLen = 320
|
|
maxHostLen = 253
|
|
maxAccountName = 128
|
|
maxUsernameLen = 256
|
|
maxPasswordLen = 256
|
|
maxWebhookName = 128
|
|
maxRuleName = 128
|
|
)
|
|
|
|
var allowedWebhookMethods = map[string]struct{}{
|
|
"POST": {},
|
|
"PUT": {},
|
|
"PATCH": {},
|
|
}
|
|
|
|
func containsNewline(s string) bool {
|
|
return strings.ContainsAny(s, "\r\n")
|
|
}
|
|
|
|
func validateEmailField(field, addr string) *apivalidate.FieldDetail {
|
|
addr = strings.TrimSpace(addr)
|
|
if addr == "" {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "required"}
|
|
}
|
|
if len(addr) > maxEmailLen {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "too long"}
|
|
}
|
|
if containsNewline(addr) {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "invalid"}
|
|
}
|
|
parsed, err := mail.ParseAddress(addr)
|
|
if err != nil || parsed.Address == "" {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "invalid"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateHostField(field, host string) *apivalidate.FieldDetail {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "required"}
|
|
}
|
|
if len(host) > maxHostLen {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "too long"}
|
|
}
|
|
if containsNewline(host) || strings.Contains(host, " ") {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "invalid"}
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
|
inner := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
|
|
if ip := net.ParseIP(inner); ip != nil {
|
|
return nil
|
|
}
|
|
}
|
|
if !isDNSHostname(host) {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "invalid"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isDNSHostname(host string) bool {
|
|
if strings.HasSuffix(host, ".") {
|
|
host = strings.TrimSuffix(host, ".")
|
|
}
|
|
if host == "" {
|
|
return false
|
|
}
|
|
labels := strings.Split(host, ".")
|
|
for _, label := range labels {
|
|
if len(label) == 0 || len(label) > 63 {
|
|
return false
|
|
}
|
|
if label[0] == '-' || label[len(label)-1] == '-' {
|
|
return false
|
|
}
|
|
for _, r := range label {
|
|
if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-') {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validatePortField(field string, port int) *apivalidate.FieldDetail {
|
|
if port < 1 || port > 65535 {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "must be between 1 and 65535"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCredentialField(field, value string, maxLen int) *apivalidate.FieldDetail {
|
|
if strings.TrimSpace(value) == "" {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "required"}
|
|
}
|
|
if len(value) > maxLen {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "too long"}
|
|
}
|
|
if containsNewline(value) {
|
|
return &apivalidate.FieldDetail{Field: field, Message: "invalid"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type createAccountRequest struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Provider string `json:"provider"`
|
|
IMAPHost string `json:"imap_host"`
|
|
IMAPPort int `json:"imap_port"`
|
|
IMAPTLS bool `json:"imap_tls"`
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPTLS bool `json:"smtp_tls"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func validateCreateAccount(req *createAccountRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if req.Name != "" && len(req.Name) > maxAccountName {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "too long"})
|
|
}
|
|
if d := validateEmailField("email", req.Email); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validateHostField("imap_host", req.IMAPHost); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validateHostField("smtp_host", req.SMTPHost); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validatePortField("imap_port", req.IMAPPort); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validatePortField("smtp_port", req.SMTPPort); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validateCredentialField("username", req.Username, maxUsernameLen); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validateCredentialField("password", req.Password, maxPasswordLen); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|
|
|
|
type sendMessageRequest struct {
|
|
AccountID string `json:"account_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"`
|
|
ReplyToMessageID string `json:"reply_to_message_id"`
|
|
ScheduleAt *string `json:"schedule_at"`
|
|
IdempotencyKey string `json:"-"`
|
|
}
|
|
|
|
type rescheduleOutboxRequest struct {
|
|
ScheduleAt string `json:"schedule_at"`
|
|
}
|
|
|
|
func validateRescheduleOutbox(req *rescheduleOutboxRequest) (*time.Time, *apivalidate.ValidationError) {
|
|
scheduleAt := strings.TrimSpace(req.ScheduleAt)
|
|
if scheduleAt == "" {
|
|
return nil, apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "schedule_at", Message: "required"})
|
|
}
|
|
parsed, err := time.Parse(time.RFC3339, scheduleAt)
|
|
if err != nil {
|
|
return nil, apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "schedule_at", Message: "invalid"})
|
|
}
|
|
if !parsed.After(time.Now()) {
|
|
return nil, apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "schedule_at", Message: "must be in the future"})
|
|
}
|
|
return &parsed, nil
|
|
}
|
|
|
|
func validateSendMessage(req *sendMessageRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if strings.TrimSpace(req.AccountID) == "" {
|
|
details = append(details, apivalidate.FieldDetail{Field: "account_id", Message: "required"})
|
|
}
|
|
recipients := append(append([]string{}, req.To...), append(req.Cc, req.Bcc...)...)
|
|
if len(recipients) == 0 {
|
|
details = append(details, apivalidate.FieldDetail{Field: "to", Message: "at least one recipient required"})
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
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 len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|
|
|
|
func validateRecipient(addr string) *apivalidate.FieldDetail {
|
|
addr = strings.TrimSpace(addr)
|
|
if addr == "" {
|
|
return &apivalidate.FieldDetail{Field: "email", Message: "required"}
|
|
}
|
|
if len(addr) > maxEmailLen || containsNewline(addr) {
|
|
return &apivalidate.FieldDetail{Field: "email", Message: "invalid"}
|
|
}
|
|
parsed, err := mail.ParseAddress(addr)
|
|
if err != nil || parsed.Address == "" {
|
|
return &apivalidate.FieldDetail{Field: "email", Message: "invalid"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type updateLabelsRequest struct {
|
|
Labels []string `json:"labels"`
|
|
}
|
|
|
|
func validateUpdateLabels(req *updateLabelsRequest) *apivalidate.ValidationError {
|
|
if req.Labels == nil {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "labels", Message: "required",
|
|
})
|
|
}
|
|
for i, label := range req.Labels {
|
|
if containsNewline(label) {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "labels[" + strconv.Itoa(i) + "]", Message: "invalid",
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type updateFlagsRequest struct {
|
|
Flags []string `json:"flags"`
|
|
}
|
|
|
|
func validateUpdateFlags(req *updateFlagsRequest) *apivalidate.ValidationError {
|
|
if req.Flags == nil {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "flags", Message: "required",
|
|
})
|
|
}
|
|
for i, flag := range req.Flags {
|
|
if strings.TrimSpace(flag) == "" || containsNewline(flag) {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "flags[" + strconv.Itoa(i) + "]", Message: "invalid",
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type createRuleRequest struct {
|
|
Name string `json:"name"`
|
|
AccountID string `json:"account_id"`
|
|
Priority int `json:"priority"`
|
|
Conditions any `json:"conditions"`
|
|
Actions any `json:"actions"`
|
|
}
|
|
|
|
func validateCreateRule(req *createRuleRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "required"})
|
|
} else if len(req.Name) > maxRuleName {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "too long"})
|
|
}
|
|
if req.Conditions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "conditions", Message: "required"})
|
|
}
|
|
if req.Actions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "actions", Message: "required"})
|
|
}
|
|
if len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|
|
|
|
type updateRuleRequest struct {
|
|
Name string `json:"name"`
|
|
Priority int `json:"priority"`
|
|
IsActive bool `json:"is_active"`
|
|
Conditions any `json:"conditions"`
|
|
Actions any `json:"actions"`
|
|
}
|
|
|
|
type simulateRuleSampleMessage struct {
|
|
From string `json:"from"`
|
|
To []string `json:"to"`
|
|
Subject string `json:"subject"`
|
|
BodyText string `json:"body_text"`
|
|
HasAttachments bool `json:"has_attachments"`
|
|
}
|
|
|
|
type simulateRuleInlineRule struct {
|
|
Conditions any `json:"conditions"`
|
|
Actions any `json:"actions"`
|
|
}
|
|
|
|
type simulateRuleRequest struct {
|
|
Message *simulateRuleSampleMessage `json:"message"`
|
|
RuleID string `json:"rule_id"`
|
|
Rule *simulateRuleInlineRule `json:"rule"`
|
|
}
|
|
|
|
func validateSimulateRule(req *simulateRuleRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if req.Message == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "message", Message: "required"})
|
|
}
|
|
hasRuleID := strings.TrimSpace(req.RuleID) != ""
|
|
hasInlineRule := req.Rule != nil
|
|
if hasRuleID && hasInlineRule {
|
|
details = append(details, apivalidate.FieldDetail{Field: "rule_id", Message: "provide rule_id or rule, not both"})
|
|
}
|
|
if !hasRuleID && !hasInlineRule {
|
|
details = append(details, apivalidate.FieldDetail{Field: "rule_id", Message: "rule_id or rule required"})
|
|
}
|
|
if hasInlineRule {
|
|
if req.Rule.Conditions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "rule.conditions", Message: "required"})
|
|
}
|
|
if req.Rule.Actions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "rule.actions", Message: "required"})
|
|
}
|
|
}
|
|
if len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|
|
|
|
func validateUpdateRule(req *updateRuleRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "required"})
|
|
} else if len(req.Name) > maxRuleName {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "too long"})
|
|
}
|
|
if req.Conditions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "conditions", Message: "required"})
|
|
}
|
|
if req.Actions == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "actions", Message: "required"})
|
|
}
|
|
if len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|
|
|
|
type createWebhookRequest struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Method string `json:"method"`
|
|
Headers map[string]string `json:"headers"`
|
|
BodyTemplate string `json:"body_template"`
|
|
SigningSecret string `json:"signing_secret"`
|
|
MaxRetries *int `json:"max_retries"`
|
|
}
|
|
|
|
type updateWebhookRequest struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Method string `json:"method"`
|
|
Headers map[string]string `json:"headers"`
|
|
BodyTemplate string `json:"body_template"`
|
|
SigningSecret string `json:"signing_secret"`
|
|
MaxRetries *int `json:"max_retries"`
|
|
}
|
|
|
|
type previewWebhookMessageRequest struct {
|
|
SenderName string `json:"sender_name"`
|
|
SenderEmail string `json:"sender_email"`
|
|
Subject string `json:"subject"`
|
|
BodyText string `json:"body_text"`
|
|
BodyHTML string `json:"body_html"`
|
|
Date string `json:"date"`
|
|
Recipients string `json:"recipients"`
|
|
HasAttachment bool `json:"has_attachment"`
|
|
MessageID string `json:"message_id"`
|
|
}
|
|
|
|
type previewWebhookRequest struct {
|
|
BodyTemplate string `json:"body_template"`
|
|
Message *previewWebhookMessageRequest `json:"message"`
|
|
}
|
|
|
|
func validateWebhookURL(raw string) *apivalidate.FieldDetail {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "required"}
|
|
}
|
|
if len(raw) > 2048 {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "too long"}
|
|
}
|
|
if containsNewline(raw) {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "invalid"}
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "invalid"}
|
|
}
|
|
scheme := strings.ToLower(u.Scheme)
|
|
if scheme != "http" && scheme != "https" {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "invalid scheme"}
|
|
}
|
|
if u.Host == "" {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "invalid"}
|
|
}
|
|
if u.User != nil {
|
|
return &apivalidate.FieldDetail{Field: "url", Message: "must not contain credentials"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateWebhookMethod(method string) (string, *apivalidate.FieldDetail) {
|
|
method = strings.TrimSpace(strings.ToUpper(method))
|
|
if method == "" {
|
|
return "", &apivalidate.FieldDetail{Field: "method", Message: "required"}
|
|
}
|
|
if _, ok := allowedWebhookMethods[method]; !ok {
|
|
return "", &apivalidate.FieldDetail{Field: "method", Message: "invalid"}
|
|
}
|
|
return method, nil
|
|
}
|
|
|
|
func isValidHeaderToken(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
if c < 0x21 || c == 0x7f || c == '(' || c == ')' || c == '<' || c == '>' ||
|
|
c == '@' || c == ',' || c == ';' || c == ':' || c == '\\' || c == '"' ||
|
|
c == '/' || c == '[' || c == ']' || c == '?' || c == '=' || c == '{' ||
|
|
c == '}' || c == ' ' || c == '\t' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validateWebhookHeaders(headers map[string]string) *apivalidate.ValidationError {
|
|
if len(headers) > maxWebhookHeaders {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "too many entries",
|
|
})
|
|
}
|
|
for name, value := range headers {
|
|
if len(name) > maxHeaderNameLen {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "header name too long",
|
|
})
|
|
}
|
|
if len(value) > maxHeaderValueLen {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "header value too long",
|
|
})
|
|
}
|
|
if containsNewline(name) || containsNewline(value) {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "invalid header",
|
|
})
|
|
}
|
|
if !isValidHeaderToken(name) {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "invalid header name",
|
|
})
|
|
}
|
|
if strings.EqualFold(name, "Content-Type") {
|
|
if _, _, err := mime.ParseMediaType(value); err != nil {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "headers", Message: "invalid content-type",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateWebhookBodyTemplate(body string) *apivalidate.FieldDetail {
|
|
if body == "" {
|
|
return nil
|
|
}
|
|
if len(body) > maxWebhookBodyTemplate {
|
|
return &apivalidate.FieldDetail{Field: "body_template", Message: "too large"}
|
|
}
|
|
if !json.Valid([]byte(body)) {
|
|
return &apivalidate.FieldDetail{Field: "body_template", Message: "invalid json"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateWebhookSigningSecret(secret string) *apivalidate.FieldDetail {
|
|
if secret == "" {
|
|
return nil
|
|
}
|
|
if len(secret) > maxWebhookSecretLen {
|
|
return &apivalidate.FieldDetail{Field: "signing_secret", Message: "too long"}
|
|
}
|
|
if containsNewline(secret) {
|
|
return &apivalidate.FieldDetail{Field: "signing_secret", Message: "invalid"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeWebhookMaxRetries(v *int) (int, *apivalidate.FieldDetail) {
|
|
if v == nil {
|
|
return defaultWebhookRetries, nil
|
|
}
|
|
if *v < 0 || *v > maxWebhookRetries {
|
|
return 0, &apivalidate.FieldDetail{Field: "max_retries", Message: "must be between 0 and 10"}
|
|
}
|
|
return *v, nil
|
|
}
|
|
|
|
func validateCreateWebhook(req *createWebhookRequest) (string, int, *apivalidate.ValidationError) {
|
|
var details []apivalidate.FieldDetail
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "required"})
|
|
} else if len(req.Name) > maxWebhookName {
|
|
details = append(details, apivalidate.FieldDetail{Field: "name", Message: "too long"})
|
|
}
|
|
if d := validateWebhookURL(req.URL); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
method, d := validateWebhookMethod(req.Method)
|
|
if d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
maxRetries, d := normalizeWebhookMaxRetries(req.MaxRetries)
|
|
if d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if d := validateWebhookSigningSecret(req.SigningSecret); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if len(details) > 0 {
|
|
return "", 0, apivalidate.NewValidationError(details...)
|
|
}
|
|
if verr := validateWebhookHeaders(req.Headers); verr != nil {
|
|
return "", 0, verr
|
|
}
|
|
if d := validateWebhookBodyTemplate(req.BodyTemplate); d != nil {
|
|
return "", 0, apivalidate.NewValidationError(*d)
|
|
}
|
|
return method, maxRetries, nil
|
|
}
|
|
|
|
func validateUpdateWebhook(req *updateWebhookRequest) (string, int, *apivalidate.ValidationError) {
|
|
return validateCreateWebhook(&createWebhookRequest{
|
|
Name: req.Name,
|
|
URL: req.URL,
|
|
Method: req.Method,
|
|
Headers: req.Headers,
|
|
BodyTemplate: req.BodyTemplate,
|
|
SigningSecret: req.SigningSecret,
|
|
MaxRetries: req.MaxRetries,
|
|
})
|
|
}
|
|
|
|
func validatePreviewWebhook(req *previewWebhookRequest) *apivalidate.ValidationError {
|
|
var details []apivalidate.FieldDetail
|
|
if req.Message == nil {
|
|
details = append(details, apivalidate.FieldDetail{Field: "message", Message: "required"})
|
|
}
|
|
if strings.TrimSpace(req.BodyTemplate) == "" {
|
|
details = append(details, apivalidate.FieldDetail{Field: "body_template", Message: "required"})
|
|
} else if d := validateWebhookBodyTemplate(req.BodyTemplate); d != nil {
|
|
details = append(details, *d)
|
|
}
|
|
if len(details) == 0 {
|
|
return nil
|
|
}
|
|
return apivalidate.NewValidationError(details...)
|
|
}
|