Files
upstream-ory-kratos/courier/smtp_test.go
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

250 lines
9.1 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package courier_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"io"
"math/big"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/courier"
templates "github.com/ory/kratos/courier/template/email"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
gomail "github.com/ory/mail/v3"
"github.com/ory/x/configx"
)
func TestNewSMTPClientPreventLeak(t *testing.T) {
// Test for https://hackerone.com/reports/2384028
invalidURL := "sm<>t>p://f%oo::bar:baz@my-server:1234:122/"
conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeyCourierSMTPURL, invalidURL))
channels, err := conf.CourierChannels(t.Context())
require.NoError(t, err)
require.Len(t, channels, 1)
_, err = courier.NewSMTPClient(reg, channels[0].SMTPConfig)
require.Error(t, err)
assert.NotContains(t, err.Error(), invalidURL)
}
func TestNewSMTP(t *testing.T) {
conf, reg := pkg.NewFastRegistryWithMocks(t)
setupSMTPClient := func(stringURL string) *courier.SMTPClient {
conf.MustSet(t.Context(), config.ViperKeyCourierSMTPURL, stringURL)
channels, err := conf.CourierChannels(t.Context())
require.NoError(t, err)
require.Len(t, channels, 1)
c, err := courier.NewSMTPClient(reg, channels[0].SMTPConfig)
require.NoError(t, err)
return c
}
// Should enforce StartTLS => dialer.StartTLSPolicy = gomail.MandatoryStartTLS and dialer.SSL = false
smtp := setupSMTPClient("smtp://foo:bar@my-server:1234/")
assert.Equal(t, smtp.StartTLSPolicy, gomail.MandatoryStartTLS, "StartTLS not enforced")
assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled")
// Should enforce TLS => dialer.SSL = true
smtp = setupSMTPClient("smtps://foo:bar@my-server:1234/")
assert.Equal(t, smtp.SSL, true, "Implicit TLS should be enabled")
// Should allow cleartext => dialer.StartTLSPolicy = gomail.OpportunisticStartTLS and dialer.SSL = false
smtp = setupSMTPClient("smtp://foo:bar@my-server:1234/?disable_starttls=true")
assert.Equal(t, smtp.StartTLSPolicy, gomail.OpportunisticStartTLS, "StartTLS is enforced")
assert.Equal(t, smtp.SSL, false, "Implicit TLS should not be enabled")
// Test cert based SMTP client auth
clientCert, clientKey, err := generateTestClientCert(t)
require.NoError(t, err)
t.Cleanup(func() { _ = os.Remove(clientCert.Name()) }) //nolint:gosec
t.Cleanup(func() { _ = os.Remove(clientKey.Name()) }) //nolint:gosec
conf.MustSet(t.Context(), config.ViperKeyCourierSMTPClientCertPath, clientCert.Name())
conf.MustSet(t.Context(), config.ViperKeyCourierSMTPClientKeyPath, clientKey.Name())
clientPEM, err := tls.LoadX509KeyPair(clientCert.Name(), clientKey.Name())
require.NoError(t, err)
smtpWithCert := setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server")
assert.Equal(t, smtpWithCert.SSL, true, "Implicit TLS should be enabled")
assert.Equal(t, smtpWithCert.Host, "subdomain.my-server", "SMTP Dialer host should match")
assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match")
assert.Equal(t, smtpWithCert.TLSConfig.ServerName, "my-server", "TLS config server name should match")
assert.Contains(t, smtpWithCert.TLSConfig.Certificates, clientPEM, "TLS config should contain client pem")
// error case: invalid client key
require.NoError(t, conf.Set(t.Context(), config.ViperKeyCourierSMTPClientKeyPath, clientCert.Name())) // mixup client key and client cert
smtpWithCert = setupSMTPClient("smtps://subdomain.my-server:1234/?server_name=my-server")
assert.Equal(t, len(smtpWithCert.TLSConfig.Certificates), 0, "TLS config certificates should be empty")
}
func TestNotAllowedPrivateIPs(t *testing.T) {
smtp, _ := testhelpers.StartMailhog(t, false)
_, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{
config.ViperKeyCourierSMTPURL: smtp,
config.ViperKeyCourierSMTPFrom: "test-stub@ory.sh",
config.ViperKeyCourierSMTPFromName: "Bob",
config.ViperKeyCourierSMTPHeaders + ".test-stub-header1": "foo",
config.ViperKeyCourierSMTPHeaders + ".test-stub-header2": "bar",
config.ViperKeyCourierMessageRetries: 50,
config.ViperKeyClientSMTPNoPrivateIPRanges: true,
}))
c, err := reg.Courier(t.Context())
require.NoError(t, err)
c.FailOnDispatchError()
id, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "test-recipient-1@example.org",
Subject: "test-subject-1",
Body: "test-body-1",
}))
require.NoError(t, err)
require.NotZero(t, id)
err = c.DispatchQueue(t.Context())
require.ErrorContains(t, err, "no route to host")
}
func TestQueueEmail(t *testing.T) {
smtp, api := testhelpers.StartMailhog(t, true)
_, reg := pkg.NewRegistryDefaultWithDSN(t, "", configx.WithValues(map[string]any{
config.ViperKeyCourierSMTPURL: smtp,
config.ViperKeyCourierSMTPFrom: "test-stub@ory.sh",
config.ViperKeyCourierSMTPFromName: "Bob",
config.ViperKeyCourierSMTPHeaders + ".test-stub-header1": "foo",
config.ViperKeyCourierSMTPHeaders + ".test-stub-header2": "bar",
config.ViperKeyCourierMessageRetries: 50,
config.ViperKeyClientSMTPNoPrivateIPRanges: false,
}))
c, err := reg.Courier(t.Context())
require.NoError(t, err)
c.FailOnDispatchError()
_, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "invalid-email",
Subject: "test-subject-1",
Body: "test-body-1",
}))
require.Error(t, err)
id, err := c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "test-recipient-1@example.org",
Subject: "test-subject-1",
Body: "test-body-1",
}))
require.NoError(t, err)
require.NotZero(t, id)
id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "test-recipient-2@example.org",
Subject: "test-subject-2",
Body: "test-body-2",
}))
require.NoError(t, err)
require.NotZero(t, id)
id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "test-recipient-3@example.org",
Subject: "test-subject-3",
Body: "test-body-3",
}))
require.NoError(t, err)
require.NotZero(t, id)
// A schema-approved non-RFC carrier address must queue and be delivered.
id, err = c.QueueEmail(t.Context(), templates.NewTestStub(&templates.TestStubModel{
To: "foo.@docomo.ne.jp",
Subject: "test-subject-docomo",
Body: "test-body-docomo",
}))
require.NoError(t, err)
require.NotZero(t, id)
require.EventuallyWithT(t, func(t *assert.CollectT) {
require.NoError(t, c.DispatchQueue(context.Background()))
}, time.Second, 10*time.Millisecond)
res, err := testhelpers.NewTestClient(t).Get(api + "/api/v2/messages")
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body)
require.EqualValues(t, 4, gjson.GetBytes(body, "total").Int())
for k := 1; k <= 3; k++ {
assert.Contains(t, string(body), fmt.Sprintf("test-subject-%d", k))
assert.Contains(t, string(body), fmt.Sprintf("test-body-%d", k))
assert.Contains(t, string(body), fmt.Sprintf("test-recipient-%d@example.org", k))
assert.Contains(t, string(body), "test-stub@ory.sh")
}
assert.Contains(t, string(body), "test-subject-docomo")
assert.Contains(t, string(body), "test-body-docomo")
assert.Contains(t, string(body), "foo.@docomo.ne.jp")
assert.Contains(t, string(body), "Bob")
assert.Contains(t, string(body), `"test-stub-header1":["foo"]`)
assert.Contains(t, string(body), `"test-stub-header2":["bar"]`)
}
func generateTestClientCert(t *testing.T) (clientCert *os.File, clientKey *os.File, err error) {
hostName := flag.String("host", "127.0.0.1", "Hostname to certify")
priv, err := rsa.GenerateKey(rand.Reader, 1024) // #nosec G403 -- test code
require.NoError(t, err)
now := time.Now()
certTemplate := x509.Certificate{
SerialNumber: big.NewInt(1234),
Subject: pkix.Name{
CommonName: *hostName,
Organization: []string{"myorg"},
},
NotBefore: now.Add(-300 * time.Second),
NotAfter: now.Add(24 * time.Hour),
SubjectKeyId: []byte{1, 2, 3, 4},
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
}
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &priv.PublicKey, priv)
require.NoError(t, err)
clientCert, err = os.CreateTemp("./test", "testCert")
require.NoError(t, err)
defer func() { _ = clientCert.Close() }()
require.NoError(t, pem.Encode(clientCert, &pem.Block{Type: "CERTIFICATE", Bytes: cert}))
clientKey, err = os.CreateTemp("./test", "testKey")
require.NoError(t, err)
defer func() { _ = clientKey.Close() }()
require.NoError(t, pem.Encode(clientKey, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}))
return clientCert, clientKey, nil
}