Add Cloudflare email and collaboration workflows
This commit is contained in:
@@ -36,16 +36,21 @@ func NewPostmarkSender(serverToken, from, apiURL string, logger *slog.Logger) *P
|
||||
serverToken: serverToken,
|
||||
apiURL: strings.TrimRight(apiURL, "/"),
|
||||
from: from,
|
||||
client: &http.Client{Timeout: postmarkTimeout},
|
||||
logger: logger,
|
||||
client: &http.Client{
|
||||
Timeout: postmarkTimeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type postmarkRequest struct {
|
||||
From string `json:"From"`
|
||||
To string `json:"To"`
|
||||
Subject string `json:"Subject"`
|
||||
TextBody string `json:"TextBody"`
|
||||
From string `json:"From"`
|
||||
To string `json:"To"`
|
||||
Subject string `json:"Subject"`
|
||||
TextBody string `json:"TextBody"`
|
||||
Headers []postmarkHeader `json:"Headers,omitempty"`
|
||||
}
|
||||
|
||||
@@ -66,13 +71,13 @@ type postmarkResponse struct {
|
||||
// send if they need different reply-to addresses.
|
||||
func (p *PostmarkSender) Send(ctx context.Context, to []string, subject, body string) error {
|
||||
if p.serverToken == "" {
|
||||
return fmt.Errorf("postmark server token not configured")
|
||||
return &PermanentError{Cause: fmt.Errorf("postmark server token not configured")}
|
||||
}
|
||||
if p.from == "" {
|
||||
return fmt.Errorf("postmark from address not configured")
|
||||
return &PermanentError{Cause: fmt.Errorf("postmark from address not configured")}
|
||||
}
|
||||
if len(to) == 0 {
|
||||
return fmt.Errorf("postmark send requires at least one recipient")
|
||||
return &PermanentError{Cause: fmt.Errorf("postmark send requires at least one recipient")}
|
||||
}
|
||||
|
||||
payload := postmarkRequest{
|
||||
@@ -96,7 +101,7 @@ func (p *PostmarkSender) Send(ctx context.Context, to []string, subject, body st
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("postmark request: %w", err)
|
||||
return &TemporaryError{Cause: fmt.Errorf("postmark request: %w", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -107,7 +112,7 @@ func (p *PostmarkSender) Send(ctx context.Context, to []string, subject, body st
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("postmark rejected: status=%d body=%s", resp.StatusCode, truncate(string(respBytes)))
|
||||
return &PermanentError{Cause: fmt.Errorf("postmark rejected: status=%d body=%s", resp.StatusCode, truncate(string(respBytes)))}
|
||||
}
|
||||
|
||||
var parsed postmarkResponse
|
||||
@@ -117,7 +122,7 @@ func (p *PostmarkSender) Send(ctx context.Context, to []string, subject, body st
|
||||
if parsed.ErrorCode != 0 {
|
||||
// Postmark distinguishes "inactive recipient" (406) and other errors. Treat
|
||||
// 406 as non-retryable; others bubble up as generic failures.
|
||||
return fmt.Errorf("postmark error code=%d: %s", parsed.ErrorCode, parsed.Message)
|
||||
return &PermanentError{Cause: fmt.Errorf("postmark error code=%d: %s", parsed.ErrorCode, parsed.Message)}
|
||||
}
|
||||
|
||||
p.logger.Info("postmark email sent", "to", parsed.To, "subject", subject)
|
||||
@@ -139,3 +144,11 @@ type TemporaryError struct {
|
||||
|
||||
func (t *TemporaryError) Error() string { return t.Cause.Error() }
|
||||
func (t *TemporaryError) Unwrap() error { return t.Cause }
|
||||
|
||||
// PermanentError marks a provider rejection that cannot succeed on retry.
|
||||
type PermanentError struct {
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (p *PermanentError) Error() string { return p.Cause.Error() }
|
||||
func (p *PermanentError) Unwrap() error { return p.Cause }
|
||||
|
||||
Reference in New Issue
Block a user