{"id":50697269,"url":"https://github.com/linuxfoundation/lfx-v2-email-service","last_synced_at":"2026-06-09T07:30:43.356Z","repository":{"id":357453661,"uuid":"1236955056","full_name":"linuxfoundation/lfx-v2-email-service","owner":"linuxfoundation","description":"LFX v2 Platform Email Service","archived":false,"fork":false,"pushed_at":"2026-06-01T10:08:50.000Z","size":221,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T11:19:49.922Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/linuxfoundation.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-12T18:27:59.000Z","updated_at":"2026-05-27T23:15:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/linuxfoundation/lfx-v2-email-service","commit_stats":null,"previous_names":["linuxfoundation/lfx-v2-email-service"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/linuxfoundation/lfx-v2-email-service","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Flfx-v2-email-service","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Flfx-v2-email-service/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Flfx-v2-email-service/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Flfx-v2-email-service/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/linuxfoundation","download_url":"https://codeload.github.com/linuxfoundation/lfx-v2-email-service/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Flfx-v2-email-service/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34096950,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T07:30:38.134Z","updated_at":"2026-06-09T07:30:43.348Z","avatar_url":"https://github.com/linuxfoundation.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LFX V2 Email Service\n\nThin transactional email relay for the LFX Self-Service platform. Receives\npre-rendered email payloads over NATS request/reply, delivers them via\nAmazon SES SMTP, and tracks engagement events (opens, deliveries, bounces,\ncomplaints) in NATS KV.\n\n## Usage\n\n### Send an email\n\n**Subject:** `lfx.email-service.send_email`  \n**Queue group:** `lfx.email-service.queue`\n\n**Request payload fields:**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `to` | string | yes | Recipient email address |\n| `subject` | string | yes | Email subject line |\n| `html` | string | yes | HTML body — callers render this before publishing |\n| `text` | string | yes | Plain-text body — shown by clients that don't render HTML |\n| `from` | string | no | Sender address (e.g. `newsletter@lfx.linuxfoundation.org`). When omitted the service default (`DEFAULT_SMTP_FROM`) is used. The domain must be in the service's allowed list — see [Configuring the sender address](#configuring-the-sender-address). |\n| `from_display_name` | string | no | Display name shown in the From header (e.g. `LFX Newsletter`). When omitted the service default (`DEFAULT_SMTP_FROM_DISPLAY_NAME`, default: `\"LFX Self Serve\"`) is used. |\n| `reply_to` | string | no | Email address set on the SMTP `Reply-To` header. When set, mail client replies go to this address instead of the `From` address. The domain must be in the service's reply-to allowlist (`SMTP_ALLOWED_REPLY_TO_DOMAINS`, default: `linuxfoundation.org`). Subdomain suffix matching applies — the default permits `@linuxfoundation.org` and `@*.linuxfoundation.org`. Omitted from the message when not provided. |\n| `group_id` | string | no | Caller-supplied ID grouping related emails (e.g. an invite batch). Use it to query aggregate engagement counts via [`lfx.email-service.get_email_engagement_analytics`](#query-group-engagement-analytics). If omitted, a UUID is generated and returned but is not meaningful for analytics. |\n\n```json\n{\n  \"to\": \"user@example.com\",\n  \"subject\": \"You've been added as a Writer on Demo Project\",\n  \"html\": \"\u003chtml\u003e...\u003c/html\u003e\",\n  \"text\": \"You've been added as a Writer on Demo Project.\",\n  \"from\": \"newsletter@lfx.linuxfoundation.org\",\n  \"from_display_name\": \"LFX Newsletter\",\n  \"reply_to\": \"support@lfx.linuxfoundation.org\",\n  \"group_id\": \"invite-batch-abc123\"\n}\n```\n\n**Success response:**\n```json\n{ \"email_id\": \"\u003cuuid\u003e\", \"group_id\": \"\u003cgroup_id\u003e\" }\n```\n\n`email_id` is a UUID generated by the service and injected as the `X-LFX-TRACKING-ID`\nMIME header. Store it if you want to query delivery/open status later.\n\n**Error response:**\n```json\n{ \"error\": \"\u003creason\u003e\" }\n```\n\n| `error` value | Cause |\n|---|---|\n| `invalid request payload` | Request body is not valid JSON |\n| `to, subject, html, and text are required` | One or more required fields are missing |\n| `invalid from address` | `from` field is not a valid email address |\n| `from address domain not allowed` | `from` domain is not in the service's allowed list |\n| `invalid reply_to address` | `reply_to` field is not a valid email address |\n| `reply_to address domain not allowed` | `reply_to` domain is not in the service's allowed list |\n| `email delivery failed` | Service accepted the request but SMTP delivery failed |\n\n**Examples (NATS CLI):**\n```bash\n# Default sender (\"LFX Self Serve \u003cnoreply@lfx.linuxfoundation.org\u003e\")\nnats req lfx.email-service.send_email \\\n  '{\"to\":\"alice@example.com\",\"subject\":\"Test\",\"html\":\"\u003cp\u003eHi\u003c/p\u003e\",\"text\":\"Hi\"}'\n\n# Custom sender address and display name\nnats req lfx.email-service.send_email \\\n  '{\"to\":\"alice@example.com\",\"subject\":\"Test\",\"html\":\"\u003cp\u003eHi\u003c/p\u003e\",\"text\":\"Hi\",\"from\":\"newsletter@lfx.linuxfoundation.org\",\"from_display_name\":\"LFX Newsletter\"}'\n```\n\n#### Configuring the sender address\n\nThe `from` field lets callers send from any address whose domain is in the service's\nallowlist. The allowlist is configured via the `SMTP_ALLOWED_FROM_DOMAINS` env var\n(comma-separated, default: `lfx.linuxfoundation.org`).\n\n| Scenario | Behaviour |\n|---|---|\n| `from` omitted | Service default (`DEFAULT_SMTP_FROM`) is used |\n| `from` domain is in the allowlist | Email is sent from the specified address |\n| `from` domain is **not** in the allowlist | Request is rejected — `{\"error\":\"from address domain not allowed\"}` |\n| `from_display_name` omitted | Service default (`DEFAULT_SMTP_FROM_DISPLAY_NAME`, default: `\"LFX Self Serve\"`) is used |\n\n\u003e **Note:** Because delivery goes through Amazon SES, the `from` domain must also be\n\u003e a verified SES sending identity. Contact the platform team to add a new domain to\n\u003e both the SES configuration and `SMTP_ALLOWED_FROM_DOMAINS`.\n\n### Query email status\n\n**Subject:** `lfx.email-service.get_email_status`\n\nReturns the tracking record(s) for one or more emails. Only available when NATS KV\nis configured (JetStream enabled and both KV buckets exist).\n\nExactly one of `email_id` or `group_id` must be provided.\n\n**Request:**\n```json\n{ \"email_id\": \"\u003cuuid returned by send\u003e\" }\n```\n```json\n{ \"group_id\": \"\u003cgroup id\u003e\" }\n```\n\n**Success response — by `email_id`** — an `EmailRecipientRecord`:\n```json\n{\n  \"email_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"group_id\": \"invite-batch-abc123\",\n  \"to\": \"user@example.com\",\n  \"subject\": \"You've been added as a Writer on Demo Project\",\n  \"sent_at\": \"2025-01-15T10:30:00Z\",\n  \"delivered\": true,\n  \"delivered_at\": \"2025-01-15T10:30:02Z\",\n  \"opened\": true,\n  \"opened_at_list\": [\n    { \"event_id\": \"abc-sns-message-id-1\", \"opened_at\": \"2025-01-15T11:05:33Z\" },\n    { \"event_id\": \"abc-sns-message-id-2\", \"opened_at\": \"2025-01-15T14:22:10Z\" }\n  ],\n  \"last_opened_at\": \"2025-01-15T14:22:10Z\",\n  \"failed\": false\n}\n```\n\n`opened_at_list` contains one entry per unique open event (keyed by SNS `MessageId` to survive replays). Use `len(opened_at_list)` for the open count.\n\n**Success response — by `group_id`** — an array of `EmailRecipientRecord`:\n```json\n[\n  {\n    \"email_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"group_id\": \"invite-batch-abc123\",\n    \"to\": \"user@example.com\",\n    \"subject\": \"You've been added as a Writer on Demo Project\",\n    \"sent_at\": \"2025-01-15T10:30:00Z\",\n    \"delivered\": true,\n    \"delivered_at\": \"2025-01-15T10:30:02Z\",\n    \"opened\": false,\n    \"failed\": false\n  }\n]\n```\n\n`delivered`, `opened`, and `failed` are set by the SES engagement event poller\n(see [SES Engagement Event Tracking](#ses-engagement-event-tracking) below).\nThey remain `false` until the poller is enabled and the corresponding SES event\narrives.\n\n**Error response:**\n```json\n{ \"error\": \"\u003creason\u003e\" }\n```\n\n| `error` value | Cause |\n|---|---|\n| `invalid request payload` | Request body is not valid JSON |\n| `email_id or group_id is required` | Neither field was set |\n| `only one of email_id or group_id may be set` | Both fields were set |\n| `not found` | No record exists for the given `email_id` or `group_id` |\n\n**Examples (NATS CLI):**\n```bash\nnats req lfx.email-service.get_email_status \\\n  '{\"email_id\":\"550e8400-e29b-41d4-a716-446655440000\"}'\n\nnats req lfx.email-service.get_email_status \\\n  '{\"group_id\":\"invite-batch-abc123\"}'\n```\n\n### Query group engagement analytics\n\n**Subject:** `lfx.email-service.get_email_engagement_analytics`\n\nReturns aggregate counts across all emails in a group. Only available when\nNATS KV is configured.\n\n**Request:**\n```json\n{ \"group_id\": \"invite-batch-abc123\" }\n```\n\n**Success response:**\n```json\n{\n  \"group_id\": \"invite-batch-abc123\",\n  \"total_sent\": 42,\n  \"delivered\": 40,\n  \"opened\": 31,\n  \"unique_opened\": 18,\n  \"failed\": 2\n}\n```\n\n**Error response:**\n```json\n{ \"error\": \"\u003creason\u003e\" }\n```\n\n| `error` value | Cause |\n|---|---|\n| `invalid request payload` | Request body is not valid JSON or `group_id` is missing |\n| `not found` | No emails have been sent under the given `group_id` |\n\n**Example (NATS CLI):**\n```bash\nnats req lfx.email-service.get_email_engagement_analytics \\\n  '{\"group_id\":\"invite-batch-abc123\"}'\n```\n\n### Use with Go\n\nThe `pkg/api` package exports subject constants and request/response types.\n\n```bash\ngo get github.com/linuxfoundation/lfx-v2-email-service/pkg/api\n```\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/nats-io/nats.go\"\n\temailapi \"github.com/linuxfoundation/lfx-v2-email-service/pkg/api\"\n)\n\nfunc main() {\n\tnc, err := nats.Connect(nats.DefaultURL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer nc.Close()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\t// Send an email with a custom sender address and display name.\n\t// From and FromDisplayName are optional — omit them to use the service defaults.\n\treq := emailapi.SendEmailRequest{\n\t\tTo:              \"user@example.com\",\n\t\tSubject:         \"You've been added\",\n\t\tHTML:            \"\u003cp\u003eHello\u003c/p\u003e\",\n\t\tText:            \"Hello\",\n\t\tFrom:            \"newsletter@lfx.linuxfoundation.org\", // optional\n\t\tFromDisplayName: \"LFX Newsletter\",                     // optional\n\t\tGroupID:         \"my-batch-id\",\n\t}\n\tdata, _ := json.Marshal(req)\n\n\treply, err := nc.RequestWithContext(ctx, emailapi.SendEmailSubject, data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Check for an error response first — SendEmailErrorResponse and\n\t// SendEmailResponse are distinguished by the presence of the \"error\" field.\n\tvar errResp emailapi.SendEmailErrorResponse\n\tif err := json.Unmarshal(reply.Data, \u0026errResp); err == nil \u0026\u0026 errResp.Error != \"\" {\n\t\tfmt.Println(\"send failed:\", errResp.Error)\n\t\treturn\n\t}\n\n\tvar sendResp emailapi.SendEmailResponse\n\tif err := json.Unmarshal(reply.Data, \u0026sendResp); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"sent, email_id:\", sendResp.EmailID)\n\n\t// Query the delivery/open status of the email we just sent.\n\tstatusReq, _ := json.Marshal(emailapi.GetEmailStatusRequest{EmailID: sendResp.EmailID})\n\tstatusReply, err := nc.RequestWithContext(ctx, emailapi.GetEmailStatusSubject, statusReq)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar record emailapi.EmailRecipientRecord\n\tif err := json.Unmarshal(statusReply.Data, \u0026record); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"status: delivered=%v opened=%v failed=%v\\n\", record.Delivered, record.Opened, record.Failed)\n\n\t// Query status for all emails in the group.\n\tgroupStatusReq, _ := json.Marshal(emailapi.GetEmailStatusRequest{GroupID: sendResp.GroupID})\n\tgroupStatusReply, err := nc.RequestWithContext(ctx, emailapi.GetEmailStatusSubject, groupStatusReq)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar groupRecords []emailapi.EmailRecipientRecord\n\tif err := json.Unmarshal(groupStatusReply.Data, \u0026groupRecords); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"group status: %d emails\\n\", len(groupRecords))\n\n\t// Query aggregate engagement counts for the whole group.\n\tanalyticsReq, _ := json.Marshal(emailapi.GetEmailEngagementAnalyticsRequest{GroupID: sendResp.GroupID})\n\tanalyticsReply, err := nc.RequestWithContext(ctx, emailapi.GetEmailEngagementAnalyticsSubject, analyticsReq)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar analytics emailapi.GetEmailEngagementAnalyticsResponse\n\tif err := json.Unmarshal(analyticsReply.Data, \u0026analytics); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"group analytics: sent=%d delivered=%d opened=%d failed=%d\\n\",\n\t\tanalytics.TotalSent, analytics.Delivered, analytics.Opened, analytics.Failed)\n}\n```\n\n## Quick Start\n\n### Prerequisites\n\n- Go 1.24+\n- [NATS Server](https://docs.nats.io/running-a-nats-service/introduction/installation) or Docker\n- Local Kubernetes cluster with [OrbStack](https://orbstack.dev/) or similar\n- Mailpit running in the cluster for local SMTP capture (UI at `http://localhost:8025`)\n\n### Option 1 — Run directly with `make run`\n\n```bash\ncp .env.example .env\nsource .env \u0026\u0026 make run\n```\n\n`.env` is gitignored. `SMTP_USERNAME` and `SMTP_PASSWORD` can be left empty when\npointing at Mailpit (no auth required).\n\n### Option 2 — Build and deploy to local cluster with Helm\n\n```bash\ncp charts/lfx-v2-email-service/values.local.example.yaml \\\n   charts/lfx-v2-email-service/values.local.yaml\n\nmake docker-build\nmake helm-install-local\n```\n\n`values.local.yaml` is gitignored.\n\n## Environment Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `NATS_URL` | `nats://localhost:4222` | NATS server URL |\n| `PORT` | `8080` | HTTP health probe port |\n| `EMAIL_ENABLED` | `false` | Set `true` to enable SMTP delivery; when `false` requests succeed but delivery is skipped via `NoOpSender` |\n| `SMTP_HOST` | `localhost` | SMTP server hostname |\n| `SMTP_PORT` | `587` | SMTP server port (STARTTLS) |\n| `DEFAULT_SMTP_FROM` | `noreply@lfx.linuxfoundation.org` | Default envelope From address (falls back to legacy `SMTP_FROM` if unset) |\n| `DEFAULT_SMTP_FROM_DISPLAY_NAME` | `LFX Self Serve` | Default display name in the From header; overridable per message via `from_display_name` |\n| `SMTP_ALLOWED_FROM_DOMAINS` | `lfx.linuxfoundation.org` | Comma-separated domains permitted for per-message `from` overrides; set to `\"\"` to disable overrides entirely |\n| `SMTP_ALLOWED_REPLY_TO_DOMAINS` | `linuxfoundation.org` | Comma-separated base domains permitted for `reply_to`; subdomains are also permitted (e.g. `linuxfoundation.org` allows `lfx.linuxfoundation.org`); set to `\"\"` to disable |\n| `SMTP_ALLOWED_RECIPIENT_DOMAINS` | _(empty — permit all)_ | Comma-separated base domains permitted as recipients (subdomain suffix matching applies). When empty all recipient domains are permitted (production default). Set in non-prod (e.g. `linuxfoundation.org`) to prevent test mail from reaching real users' personal addresses. |\n| `SMTP_USERNAME` | _(empty)_ | SMTP credential (from Kubernetes Secret in production) |\n| `SMTP_PASSWORD` | _(empty)_ | SMTP credential (from Kubernetes Secret in production) |\n| `SES_CONFIGURATION_SET` | _(empty)_ | SES configuration set name. When set, `X-SES-CONFIGURATION-SET` is added to every outbound email to route engagement events. Omitted when empty. |\n| `SES_EVENTING_ENABLED` | `false` | Set `true` to start the SQS engagement event poller. Requires `SES_ENGAGEMENT_SQS_QUEUE_URL` and NATS KV — missing either is a fatal startup error. |\n| `SES_ENGAGEMENT_SQS_QUEUE_URL` | _(empty)_ | SQS queue URL that receives SNS-wrapped SES engagement events. Required when `SES_EVENTING_ENABLED=true`. |\n| `LOG_LEVEL` | `info` | Log level (`debug`, `info`, `warn`, `error`) |\n| `LOG_ADD_SOURCE` | `false` | Set `true` to include source file/line in log entries |\n\nIn production, `SES_CONFIGURATION_SET` and `SES_ENGAGEMENT_SQS_QUEUE_URL` are\ninjected from a Kubernetes Secret managed by External Secrets Operator\n(see `app.ses.engagementSecretName` in `charts/lfx-v2-email-service/values.yaml`).\n\n## File Structure\n\n```\nlfx-v2-email-service/\n├── cmd/email-service/\n│   ├── main.go          # Entry point: NATS subscriptions, SQS poller, HTTP health, graceful shutdown\n│   └── config.go        # Environment variable parsing\n├── internal/\n│   ├── domain/\n│   │   └── email.go     # Sender interface\n│   ├── infrastructure/\n│   │   ├── smtp/\n│   │   │   ├── sender.go    # SMTPSender — delivers via net/smtp, injects tracking headers\n│   │   │   ├── noop.go      # NoOpSender — logs only (EMAIL_ENABLED=false)\n│   │   │   └── message.go   # MIME message builder\n│   │   └── sqs/\n│   │       └── poller.go    # Long-polling SQS consumer (AWS SDK v2, IRSA credentials)\n│   ├── logging/\n│   │   └── logging.go   # Structured log helpers\n│   └── service/\n│       ├── send_email_handler.go                  # Handles send_email — sends and writes KV records\n│       ├── engagement_event_handler.go            # Handles SES engagement events from SQS\n│       ├── get_email_status_handler.go            # Handles get_email_status\n│       ├── get_email_engagement_analytics_handler.go  # Handles get_email_engagement_analytics\n│       └── mocks/\n│           └── kv.go    # Thread-safe in-memory KeyValue mock for tests\n├── pkg/\n│   ├── api/\n│   │   └── nats.go      # Public NATS subjects, request/response types, KV bucket constants\n│   └── redaction/\n│       └── redaction.go # Email address redaction for logs\n└── charts/lfx-v2-email-service/\n    ├── Chart.yaml\n    ├── values.yaml\n    └── templates/\n        ├── deployment.yaml\n        ├── externalsecret.yaml   # ESO secret for SES config set + SQS queue URL\n        ├── nats-kv-buckets.yaml  # Declares email-recipients and email-group-index KV buckets\n        └── service.yaml\n```\n\n## Development\n\nRun the test suite:\n\n```bash\nmake test\n```\n\nRun `make check` before committing — it verifies formatting, runs the linter,\nand checks license headers:\n\n```bash\nmake check\n```\n\nAll commits must be signed off per the [DCO](https://developercertificate.org/):\n\n```bash\ngit commit -s -m \"feat: ...\"\n```\n\n## SES Engagement Event Tracking\n\nThe service optionally captures SES engagement events (open, delivery, bounce,\ncomplaint) and stores them in NATS KV so callers can query whether their emails\nwere opened or delivered.\n\n### How it works\n\n1. **Send time**: `SendEmailHandler` injects two MIME headers into every outbound email:\n   - `X-SES-CONFIGURATION-SET: \u003cname\u003e` — routes SES events to the configured event destination\n   - `X-LFX-TRACKING-ID: \u003cgroup_id\u003e/\u003cemail_id\u003e` — a stable key SES echoes back in every engagement event\n\n2. **KV write on send**: After each successful SMTP delivery the handler writes an\n   `EmailRecipientRecord` to the `email-recipients` NATS KV bucket (key: `email_id`)\n   and appends the `email_id` to the caller's group in the `email-group-index` bucket\n   (key: `group_id`). Both writes use optimistic locking with a single retry on conflict.\n\n3. **SES event pipeline**: SES → SNS topic → SQS queue. The email service polls\n   the SQS queue in a background goroutine.\n\n4. **Poller**: `internal/infrastructure/sqs.Poller` long-polls the queue (20-second\n   wait, up to 10 messages per call) using AWS SDK v2 with IRSA credentials.\n   On consecutive `ReceiveMessage` failures it applies exponential backoff (capped\n   at 30 s) and aborts after 3 consecutive errors, triggering graceful shutdown.\n\n5. **Event handler**: `EngagementEventHandler` parses each SNS-wrapped SES event,\n   extracts the `email_id` from `X-LFX-TRACKING-ID`, looks up the KV record, updates\n   the relevant fields using SES-provided RFC3339 timestamps, and writes back with\n   optimistic locking. Unrecognised event types and missing records are silently skipped.\n\n### Enabling the poller\n\nSet `SES_EVENTING_ENABLED=true`. The service then requires both\n`SES_ENGAGEMENT_SQS_QUEUE_URL` and a reachable NATS KV — missing either is a **fatal\nstartup error** (the pod exits without restarting rather than looping).\n\nThe AWS-side infrastructure (SES configuration set, SNS topic, SQS queue) is\nprovisioned separately in `lfx-v2-opentofu`. The Helm chart reads the configuration\nset name and queue URL from a Kubernetes Secret created by External Secrets Operator\n(secret name configured via `app.ses.engagementSecretName` in `values.yaml`).\n\n## License\n\nCopyright The Linux Foundation and each contributor to LFX.\nSPDX-License-Identifier: MIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxfoundation%2Flfx-v2-email-service","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinuxfoundation%2Flfx-v2-email-service","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxfoundation%2Flfx-v2-email-service/lists"}