From a45d136546f683aa50830d58a8fe8877f605c998 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 20 Oct 2025 12:06:53 +0700 Subject: [PATCH] Use base32 encoded UUID to reduce address size --- README.md | 6 +++--- docker/postfix/transport_maps | 4 ++-- internal/api/handlers.go | 30 ++++++++++++++++++++++++++---- internal/receiver/receiver.go | 35 +++++++++++++++++++++++++++++++---- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b9db23c..a4ded59 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,9 @@ You'll obtain the best results with a custom [transport rule](https://www.postfi ``` # Transport map - route test emails to happyDeliver LMTP server - # Pattern: test-@yourdomain.com -> LMTP on localhost:2525 + # Pattern: test-@yourdomain.com -> LMTP on localhost:2525 - /^test-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}@yourdomain\.com$/ lmtp:inet:127.0.0.1:2525 + /^test-[a-zA-Z2-7-]{26,30}@yourdomain\.com$/ lmtp:inet:127.0.0.1:2525 ``` 3. Append the created file to `transport_maps` in your `main.cf`: @@ -144,7 +144,7 @@ Response: ```json { "id": "550e8400-e29b-41d4-a716-446655440000", - "email": "test-550e8400@localhost", + "email": "test-kfauqaao-ukj2if3n-fgrfkiafaa@localhost", "status": "pending", "message": "Send your test email to the address above" } diff --git a/docker/postfix/transport_maps b/docker/postfix/transport_maps index 49fdb98..cc1deed 100644 --- a/docker/postfix/transport_maps +++ b/docker/postfix/transport_maps @@ -1,4 +1,4 @@ # Transport map - route test emails to happyDeliver LMTP server -# Pattern: test-@domain.com -> LMTP on localhost:2525 +# Pattern: test-@domain.com -> LMTP on localhost:2525 -/^test-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}@.*$/ lmtp:inet:127.0.0.1:2525 +/^test-[a-zA-Z2-7-]{26,30}@.*$/ lmtp:inet:127.0.0.1:2525 diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 79d839e..896df32 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -22,8 +22,10 @@ package api import ( + "encoding/base32" "fmt" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -50,16 +52,36 @@ func NewAPIHandler(store storage.Storage, cfg *config.Config) *APIHandler { } } +// uuidToBase32 converts a UUID to a URL-safe Base32 string (without padding) +// with hyphens every 7 characters for better readability +func uuidToBase32(id uuid.UUID) string { + // Use RFC 4648 Base32 encoding (URL-safe) + encoded := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(id[:]) + // Convert to lowercase for better readability + encoded = strings.ToLower(encoded) + + // Insert hyphens every 7 characters + var result strings.Builder + for i, char := range encoded { + if i > 0 && i%7 == 0 { + result.WriteRune('-') + } + result.WriteRune(char) + } + + return result.String() +} + // CreateTest creates a new deliverability test // (POST /test) func (h *APIHandler) CreateTest(c *gin.Context) { // Generate a unique test ID (no database record created) testID := uuid.New() - // Generate test email address + // Generate test email address using Base32-encoded UUID email := fmt.Sprintf("%s%s@%s", h.config.Email.TestAddressPrefix, - testID.String(), + uuidToBase32(testID), h.config.Email.Domain, ) @@ -94,10 +116,10 @@ func (h *APIHandler) GetTest(c *gin.Context, id openapi_types.UUID) { apiStatus = TestStatusPending } - // Generate test email address + // Generate test email address using Base32-encoded UUID email := fmt.Sprintf("%s%s@%s", h.config.Email.TestAddressPrefix, - id.String(), + uuidToBase32(id), h.config.Email.Domain, ) diff --git a/internal/receiver/receiver.go b/internal/receiver/receiver.go index 1132b54..fb8d36e 100644 --- a/internal/receiver/receiver.go +++ b/internal/receiver/receiver.go @@ -22,6 +22,7 @@ package receiver import ( + "encoding/base32" "encoding/json" "fmt" "io" @@ -112,8 +113,34 @@ func (r *EmailReceiver) ProcessEmailBytes(rawEmail []byte, recipientEmail string return nil } +// base32ToUUID converts a URL-safe Base32 string (without padding) to a UUID +// Hyphens are ignored during decoding +func base32ToUUID(encoded string) (uuid.UUID, error) { + // Remove hyphens for decoding + encoded = strings.ReplaceAll(encoded, "-", "") + + // Convert to uppercase for Base32 decoding + encoded = strings.ToUpper(encoded) + + // Decode from Base32 + decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encoded) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to decode base32: %w", err) + } + + // Ensure we have exactly 16 bytes for UUID + if len(decoded) != 16 { + return uuid.Nil, fmt.Errorf("decoded bytes length is %d, expected 16", len(decoded)) + } + + // Convert bytes to UUID + var id uuid.UUID + copy(id[:], decoded) + return id, nil +} + // extractTestID extracts the UUID from the test email address -// Expected format: test-@domain.com +// Expected format: test-@domain.com func (r *EmailReceiver) extractTestID(email string) (uuid.UUID, error) { // Remove angle brackets if present (e.g., ) email = strings.Trim(email, "<>") @@ -133,10 +160,10 @@ func (r *EmailReceiver) extractTestID(email string) (uuid.UUID, error) { uuidStr := strings.TrimPrefix(localPart, r.config.Email.TestAddressPrefix) - // Parse UUID - testID, err := uuid.Parse(uuidStr) + // Decode Base32 to UUID + testID, err := base32ToUUID(uuidStr) if err != nil { - return uuid.Nil, fmt.Errorf("invalid UUID in email address: %s", uuidStr) + return uuid.Nil, fmt.Errorf("invalid Base32 encoding in email address: %s - %w", uuidStr, err) } return testID, nil