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
+202
View File
@@ -0,0 +1,202 @@
// Package common defines shared value objects used across the Mail Service
// domain model.
package common
import (
"fmt"
"mime"
"net/mail"
"strings"
"time"
"golang.org/x/text/language"
)
// DeliveryID identifies one logical mail delivery accepted by Mail Service.
type DeliveryID string
// String returns DeliveryID as its stored identifier string.
func (id DeliveryID) String() string {
return string(id)
}
// IsZero reports whether DeliveryID does not contain a usable value.
func (id DeliveryID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether DeliveryID is non-empty and already normalized for
// domain use.
func (id DeliveryID) Validate() error {
return validateToken("delivery id", string(id))
}
// TemplateID identifies one template family owned by the filesystem-backed
// Mail Service template catalog.
type TemplateID string
// String returns TemplateID as its stored identifier string.
func (id TemplateID) String() string {
return string(id)
}
// IsZero reports whether TemplateID does not contain a usable value.
func (id TemplateID) IsZero() bool {
return strings.TrimSpace(string(id)) == ""
}
// Validate reports whether TemplateID is non-empty and already normalized for
// domain use.
func (id TemplateID) Validate() error {
return validateToken("template id", string(id))
}
// IdempotencyKey stores the caller-owned key used to deduplicate accepted
// delivery commands.
type IdempotencyKey string
// String returns IdempotencyKey as its stored string.
func (key IdempotencyKey) String() string {
return string(key)
}
// IsZero reports whether IdempotencyKey does not contain a usable value.
func (key IdempotencyKey) IsZero() bool {
return strings.TrimSpace(string(key)) == ""
}
// Validate reports whether IdempotencyKey is non-empty and already normalized
// for domain use.
func (key IdempotencyKey) Validate() error {
return validateToken("idempotency key", string(key))
}
// Email stores one normalized recipient or reply-to address.
type Email string
// String returns Email as its stored canonical string.
func (email Email) String() string {
return string(email)
}
// IsZero reports whether Email does not contain a usable address.
func (email Email) IsZero() bool {
return strings.TrimSpace(string(email)) == ""
}
// Validate reports whether Email is non-empty, trimmed, and matches the same
// single-address syntax expected by the trusted Mail Service contracts.
func (email Email) Validate() error {
raw := string(email)
if err := validateToken("email", raw); err != nil {
return err
}
parsedAddress, err := mail.ParseAddress(raw)
if err != nil || parsedAddress.Name != "" || parsedAddress.Address != raw {
return fmt.Errorf("email %q must be a single valid email address", raw)
}
return nil
}
// Locale stores one canonical BCP 47 language tag used by template selection
// and rendering.
type Locale string
// ParseLocale validates value as a BCP 47 language tag and returns the
// canonical stored representation used by the Mail Service domain model.
func ParseLocale(value string) (Locale, error) {
if err := validateToken("locale", value); err != nil {
return "", err
}
tag, err := language.Parse(value)
if err != nil {
return "", fmt.Errorf("locale %q must be a valid BCP 47 language tag: %w", value, err)
}
return Locale(tag.String()), nil
}
// String returns Locale as its stored canonical string.
func (locale Locale) String() string {
return string(locale)
}
// IsZero reports whether Locale does not contain a usable value.
func (locale Locale) IsZero() bool {
return strings.TrimSpace(string(locale)) == ""
}
// Validate reports whether Locale stores a canonical BCP 47 language tag.
func (locale Locale) Validate() error {
raw := string(locale)
if err := validateToken("locale", raw); err != nil {
return err
}
tag, err := language.Parse(raw)
if err != nil {
return fmt.Errorf("locale %q must be a valid BCP 47 language tag: %w", raw, err)
}
canonical := tag.String()
if raw != canonical {
return fmt.Errorf("locale %q must use canonical BCP 47 form %q", raw, canonical)
}
return nil
}
// AttachmentMetadata stores only the durable audit metadata kept for one
// accepted attachment. Raw bytes remain outside the long-lived domain model.
type AttachmentMetadata struct {
// Filename stores the user-facing attachment filename.
Filename string
// ContentType stores the MIME media type used for SMTP body construction.
ContentType string
// SizeBytes stores the decoded payload size in bytes.
SizeBytes int64
}
// Validate reports whether AttachmentMetadata contains a complete attachment
// audit entry.
func (metadata AttachmentMetadata) Validate() error {
if err := validateToken("attachment filename", metadata.Filename); err != nil {
return err
}
if err := validateToken("attachment content type", metadata.ContentType); err != nil {
return err
}
if _, _, err := mime.ParseMediaType(metadata.ContentType); err != nil {
return fmt.Errorf("attachment content type %q must be a valid MIME media type: %w", metadata.ContentType, err)
}
if metadata.SizeBytes < 0 {
return fmt.Errorf("attachment size bytes must not be negative")
}
return nil
}
// ValidateTimestamp reports whether value is present.
func ValidateTimestamp(name string, value time.Time) error {
if value.IsZero() {
return fmt.Errorf("%s must not be zero", name)
}
return nil
}
func validateToken(name string, value string) error {
switch {
case strings.TrimSpace(value) == "":
return fmt.Errorf("%s must not be empty", name)
case strings.TrimSpace(value) != value:
return fmt.Errorf("%s must not contain surrounding whitespace", name)
default:
return nil
}
}
+190
View File
@@ -0,0 +1,190 @@
package common
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIdentifierValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
run func() error
wantErr bool
}{
{
name: "valid delivery id",
run: func() error {
return DeliveryID("delivery-123").Validate()
},
},
{
name: "valid template id",
run: func() error {
return TemplateID("auth.login_code").Validate()
},
},
{
name: "valid idempotency key",
run: func() error {
return IdempotencyKey("notification:delivery-123").Validate()
},
},
{
name: "empty delivery id",
run: func() error {
return DeliveryID("").Validate()
},
wantErr: true,
},
{
name: "template id with whitespace",
run: func() error {
return TemplateID(" auth.login_code ").Validate()
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.run()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestEmailValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value Email
wantErr bool
}{
{name: "valid", value: Email("pilot@example.com")},
{name: "empty", value: Email(""), wantErr: true},
{name: "display name forbidden", value: Email("Pilot <pilot@example.com>"), wantErr: true},
{name: "whitespace forbidden", value: Email(" pilot@example.com "), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestParseLocale(t *testing.T) {
t.Parallel()
value, err := ParseLocale("fr-fr")
require.NoError(t, err)
require.Equal(t, Locale("fr-FR"), value)
require.NoError(t, value.Validate())
}
func TestLocaleValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value Locale
wantErr bool
}{
{name: "canonical language", value: Locale("en")},
{name: "canonical regional", value: Locale("fr-FR")},
{name: "non canonical", value: Locale("fr-fr"), wantErr: true},
{name: "invalid syntax", value: Locale("not a locale"), wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestAttachmentMetadataValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value AttachmentMetadata
wantErr bool
}{
{
name: "valid",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "text/plain; charset=utf-8",
SizeBytes: 512,
},
},
{
name: "invalid content type",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "plain text",
SizeBytes: 512,
},
wantErr: true,
},
{
name: "negative size",
value: AttachmentMetadata{
Filename: "report.txt",
ContentType: "text/plain",
SizeBytes: -1,
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.value.Validate()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}