// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package courier import ( "context" "encoding/json" "fmt" "io" "time" "github.com/pkg/errors" "github.com/ory/x/httpx" "github.com/ory/x/logrusx" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/request" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" ) type ( httpChannel struct { id string requestConfig *request.Config d channelDependencies } channelDependencies interface { otelx.Provider logrusx.Provider httpx.ClientProvider jsonnetsecure.VMProvider ConfigProvider } ) var _ Channel = new(httpChannel) func newHttpChannel(id string, requestConfig *request.Config, d channelDependencies) *httpChannel { return &httpChannel{ id: id, requestConfig: requestConfig, d: d, } } func (c *httpChannel) ID() string { return c.id } type httpDataModel struct { Recipient string `json:"recipient"` Subject string `json:"subject"` Body string `json:"body"` // HTMLBody optionally contains the HTML version of an email template when available. HTMLBody string `json:"html_body,omitempty"` TemplateType template.TemplateType `json:"template_type"` TemplateData Template `json:"template_data"` MessageType string `json:"message_type"` RequestHeaders json.RawMessage `json:"request_headers"` } func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { ctx, span := c.d.Tracer(ctx).Tracer().Start(ctx, "courier.httpChannel.Dispatch") defer otelx.End(span, &err) builder, err := request.NewBuilder(c.requestConfig, c.d) if err != nil { return errors.WithStack(err) } tmpl, err := newTemplate(c.d, msg) if err != nil { return errors.WithStack(err) } td := httpDataModel{ Recipient: msg.Recipient, Subject: msg.Subject, Body: msg.Body, TemplateType: msg.TemplateType, TemplateData: tmpl, RequestHeaders: msg.RequestHeaders, MessageType: msg.Type.String(), } c.tryPopulateHTMLBody(ctx, tmpl, &td) req, err := builder.BuildRequest(ctx, td) if err != nil { return errors.WithStack(err) } res, err := c.d.HTTPClient(ctx, // fail fast and let the courier retry if needed instead of blocking the queue httpx.ResilientClientWithMaxRetry(0), httpx.ResilientClientWithConnectionTimeout(10*time.Second), ).Do(req) if err != nil { return errors.WithStack(err) } // Close the original body, not the NopCloser below: closing it returns // the connection to the pool and ends the otelhttp client span. defer func(body io.ReadCloser) { _ = body.Close() }(res.Body) res.Body = io.NopCloser(io.LimitReader(res.Body, 1024)) logger := c.d.Logger(). WithField("http_server", c.requestConfig.URL). WithField("message_id", msg.ID). WithField("message_nid", msg.NID). WithField("message_type", msg.Type). WithField("message_template_type", msg.TemplateType). WithField("message_subject", msg.Subject) if res.StatusCode >= 200 && res.StatusCode < 300 { logger.Debug("Courier sent out mailer.") return nil } err = errors.Errorf( "unable to dispatch mail delivery because upstream server replied with status code %d", res.StatusCode, ) body, _ := io.ReadAll(res.Body) logger. WithError(err). WithField("http_response_body", string(body)). Error("sending mail via HTTP failed.") return errors.WithStack(err) } func (c *httpChannel) tryPopulateHTMLBody(ctx context.Context, tmpl Template, td *httpDataModel) { if emailTmpl, ok := tmpl.(EmailTemplate); ok { // Only get the HTML body from the template; plaintext body comes from msg.Body // to maintain backward compatibility with existing behavior if htmlBody, err := emailTmpl.EmailBody(ctx); err != nil { c.d.Logger().WithError(err).Error("Unable to get email HTML body from template.") } else { td.HTMLBody = htmlBody } } } func newTemplate(d template.Dependencies, msg Message) (Template, error) { switch msg.Type { case MessageTypeEmail: return NewEmailTemplateFromMessage(d, msg) case MessageTypeSMS: return NewSMSTemplateFromMessage(d, msg) default: return nil, fmt.Errorf("received unexpected message type: %s", msg.Type) } }