feat: mail service

This commit is contained in:
Ilia Denisov
2026-04-17 18:39:16 +02:00
committed by GitHub
parent 23ffcb7535
commit 5b7593e6f6
183 changed files with 31215 additions and 248 deletions
@@ -24,6 +24,8 @@ import (
const expirationGracePeriod = 5 * time.Minute
const defaultPreferredLanguage = "en"
// Config configures one Redis-backed challenge store instance.
type Config struct {
// Addr is the Redis network address in host:port form.
@@ -59,6 +61,7 @@ type redisRecord struct {
ChallengeID string `json:"challenge_id"`
Email string `json:"email"`
CodeHashBase64 string `json:"code_hash_base64"`
PreferredLanguage string `json:"preferred_language,omitempty"`
Status challenge.Status `json:"status"`
DeliveryState challenge.DeliveryState `json:"delivery_state"`
CreatedAt string `json:"created_at"`
@@ -291,6 +294,7 @@ func redisRecordFromChallenge(record challenge.Challenge) (redisRecord, error) {
ChallengeID: record.ID.String(),
Email: record.Email.String(),
CodeHashBase64: base64.StdEncoding.EncodeToString(record.CodeHash),
PreferredLanguage: record.PreferredLanguage,
Status: record.Status,
DeliveryState: record.DeliveryState,
CreatedAt: formatTimestamp(record.CreatedAt),
@@ -354,13 +358,14 @@ func challengeFromRedisRecord(stored redisRecord) (challenge.Challenge, error) {
}
record := challenge.Challenge{
ID: common.ChallengeID(stored.ChallengeID),
Email: common.Email(stored.Email),
CodeHash: codeHash,
Status: stored.Status,
DeliveryState: stored.DeliveryState,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
ID: common.ChallengeID(stored.ChallengeID),
Email: common.Email(stored.Email),
CodeHash: codeHash,
PreferredLanguage: normalizeStoredPreferredLanguage(stored.PreferredLanguage),
Status: stored.Status,
DeliveryState: stored.DeliveryState,
CreatedAt: createdAt,
ExpiresAt: expiresAt,
Attempts: challenge.AttemptCounters{
Send: stored.SendAttemptCount,
Confirm: stored.ConfirmAttemptCount,
@@ -459,6 +464,15 @@ func formatOptionalTimestamp(value *time.Time) *string {
return &formatted
}
func normalizeStoredPreferredLanguage(value string) string {
preferredLanguage := strings.TrimSpace(value)
if preferredLanguage == "" {
return defaultPreferredLanguage
}
return preferredLanguage
}
func redisTTL(expiresAt time.Time) time.Duration {
ttl := time.Until(expiresAt.UTC())
if ttl < 0 {
@@ -451,13 +451,14 @@ func newTestStore(t *testing.T, server *miniredis.Miniredis, cfg Config) *Store
func testPendingChallenge(now time.Time) challenge.Challenge {
return challenge.Challenge{
ID: common.ChallengeID("challenge-pending"),
Email: common.Email("pilot@example.com"),
CodeHash: []byte("hashed-pending-code"),
Status: challenge.StatusPendingSend,
DeliveryState: challenge.DeliveryPending,
CreatedAt: now,
ExpiresAt: now.Add(challenge.InitialTTL),
ID: common.ChallengeID("challenge-pending"),
Email: common.Email("pilot@example.com"),
CodeHash: []byte("hashed-pending-code"),
PreferredLanguage: "en",
Status: challenge.StatusPendingSend,
DeliveryState: challenge.DeliveryPending,
CreatedAt: now,
ExpiresAt: now.Add(challenge.InitialTTL),
}
}
@@ -473,13 +474,14 @@ func testChallenge(now time.Time) challenge.Challenge {
}
return challenge.Challenge{
ID: common.ChallengeID("challenge-confirmed"),
Email: common.Email("pilot@example.com"),
CodeHash: []byte("hashed-code"),
Status: challenge.StatusConfirmedPendingExpire,
DeliveryState: challenge.DeliverySent,
CreatedAt: now,
ExpiresAt: now.Add(challenge.ConfirmedRetention),
ID: common.ChallengeID("challenge-confirmed"),
Email: common.Email("pilot@example.com"),
CodeHash: []byte("hashed-code"),
PreferredLanguage: "en",
Status: challenge.StatusConfirmedPendingExpire,
DeliveryState: challenge.DeliverySent,
CreatedAt: now,
ExpiresAt: now.Add(challenge.ConfirmedRetention),
Attempts: challenge.AttemptCounters{
Send: 1,
Confirm: 2,
@@ -495,6 +497,36 @@ func testChallenge(now time.Time) challenge.Challenge {
}
}
func TestStoreGetDefaultsMissingPreferredLanguageToEnglish(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
store := newTestStore(t, server, Config{})
now := time.Unix(1_775_130_250, 0).UTC()
record := testPendingChallenge(now)
stored, err := redisRecordFromChallenge(record)
require.NoError(t, err)
stored.PreferredLanguage = ""
payload := mustMarshalJSON(t, map[string]any{
"challenge_id": stored.ChallengeID,
"email": stored.Email,
"code_hash_base64": stored.CodeHashBase64,
"status": stored.Status,
"delivery_state": stored.DeliveryState,
"created_at": stored.CreatedAt,
"expires_at": stored.ExpiresAt,
"send_attempt_count": stored.SendAttemptCount,
"confirm_attempt_count": stored.ConfirmAttemptCount,
})
server.Set(store.lookupKey(record.ID), payload)
got, err := store.Get(context.Background(), record.ID)
require.NoError(t, err)
assert.Equal(t, "en", got.PreferredLanguage)
}
func timePointer(value time.Time) *time.Time {
return &value
}