feat(email): integrate Mailjet for transactional email - #30
Conversation
Adds EmailSender interface in usecase/, MailjetSender implementation in internal/infrastructure/email/ with embedded HTML templates, and wires the client into App and Handler via MAILJET_API_KEY/SECRET_KEY env vars. Includes unit tests with mock and httptest-based HTTP tests; sandbox integration test skips when credentials are absent. Also brings stale bootstrap.md and routing.md docs up to date with FCM/queue fields added in prior features. Closes #20
|
Warning Review limit reached
More reviews will be available in 51 minutes and 52 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds Mailjet-based transactional email to the Go backend. Introduces a ChangesMailjet transactional email integration
Sequence Diagram(s)sequenceDiagram
actor Caller
participant bootstrap.Run
participant email.NewMailjetSender
participant MailjetSender
participant MailjetAPI
bootstrap.Run->>bootstrap.Run: loadConfig (MAILJET_API_KEY, MAILJET_SECRET_KEY, FROM_EMAIL, FROM_NAME)
alt credentials present
bootstrap.Run->>email.NewMailjetSender: apiKey, secretKey, fromEmail, fromName
email.NewMailjetSender-->>bootstrap.Run: *MailjetSender
bootstrap.Run->>bootstrap.Run: App.EmailSender = *MailjetSender
else credentials absent
bootstrap.Run->>bootstrap.Run: App.EmailSender = nil
end
Caller->>MailjetSender: SendWelcomeEmail(ctx, toEmail, toName)
MailjetSender->>MailjetSender: renderWelcomeTemplate(toName)
MailjetSender->>MailjetAPI: SendMailV31(messages)
MailjetAPI-->>MailjetSender: response
MailjetSender-->>Caller: nil or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/docs/bootstrap.md (1)
45-55: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd the Mailjet sender to the run sequence.
The docs jump from Firebase init straight to returning
App, but the runtime also conditionally buildsemailSenderbefore that return. Readers will miss the Mailjet wiring step.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/docs/bootstrap.md` around lines 45 - 55, The bootstrap run sequence documentation is missing the Mailjet email sender initialization step that occurs in the actual implementation. Add a new numbered step (between the current step 7 about Firebase initialization and step 8 about returning the App) that documents the conditional initialization of the emailSender via the Mailjet client, similar to how other optional components like Redis and Firebase are documented. This step should indicate it is skipped when the appropriate configuration is not provided, maintaining consistency with the documentation pattern for other conditional initializations.
🧹 Nitpick comments (2)
backend/internal/infrastructure/email/mailjet_test.go (1)
41-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClarify or remove the outdated comment.
The comment references "withSandbox is unexported" and discusses implementation details that don't match the actual code. The test correctly uses the exported
NewSandboxSenderconstructor. Consider simplifying this comment to just explain what the test does.📝 Suggested clarification
- // withSandbox is unexported but we can exercise it by keeping the sender - // internal to this package test; instead we call the exported constructor - // and rely on a helper that forces sandbox mode (see newSandboxSender). + // NewSandboxSender enables sandbox mode, which validates requests without delivery. sender := email.NewSandboxSender(apiKey, secretKey, fromEmail, fromName)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/internal/infrastructure/email/mailjet_test.go` around lines 41 - 43, The comment block above the test that uses NewSandboxSender contains outdated references to implementation details about withSandbox being unexported that no longer match the actual test implementation. Replace this comment with a simpler, clearer explanation that accurately describes what the test does, specifically that it uses the exported NewSandboxSender constructor to test the sandbox mode functionality, removing the confusing references to unexported functions and internal implementation details.backend/internal/infrastructure/email/mailjet.go (1)
84-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider caching the parsed template to improve performance.
The template is read and parsed on every
SendWelcomeEmailcall. For better performance, parse the template once duringNewMailjetSenderconstruction and store it as a*template.Templatefield in the struct.♻️ Proposed refactor to cache the template
Add a template field to the struct:
type MailjetSender struct { client *mailjet.Client fromEmail string fromName string sandboxMode bool + welcomeTmpl *template.Template }Parse the template once in the constructor:
func NewMailjetSender(apiKey, secretKey, fromEmail, fromName string, baseURL ...string) *MailjetSender { client := mailjet.NewMailjetClient(apiKey, secretKey, baseURL...) + tmpl, err := parseWelcomeTemplate() + if err != nil { + // Handle initialization error - could panic or return error from constructor + panic(fmt.Sprintf("email: failed to parse welcome template: %v", err)) + } return &MailjetSender{ client: client, fromEmail: fromEmail, fromName: fromName, + welcomeTmpl: tmpl, } }Update
SendWelcomeEmailto use the cached template:func (s *MailjetSender) SendWelcomeEmail(_ context.Context, toEmail, toName string) error { - html, err := renderWelcomeTemplate(toName) + html, err := s.renderWelcomeTemplate(toName) if err != nil { return fmt.Errorf("email: render welcome template: %w", err) }Simplify the render function:
-func renderWelcomeTemplate(name string) (string, error) { - raw, err := templateFS.ReadFile("templates/welcome.html") - if err != nil { - return "", fmt.Errorf("read welcome template: %w", err) - } - - tmpl, err := template.New("welcome").Parse(string(raw)) - if err != nil { - return "", fmt.Errorf("parse welcome template: %w", err) - } - +func parseWelcomeTemplate() (*template.Template, error) { + raw, err := templateFS.ReadFile("templates/welcome.html") + if err != nil { + return nil, fmt.Errorf("read welcome template: %w", err) + } + return template.New("welcome").Parse(string(raw)) +} + +func (s *MailjetSender) renderWelcomeTemplate(name string) (string, error) { var buf bytes.Buffer - if err := tmpl.Execute(&buf, map[string]string{"Name": name}); err != nil { + if err := s.welcomeTmpl.Execute(&buf, map[string]string{"Name": name}); err != nil { return "", fmt.Errorf("execute welcome template: %w", err) } return buf.String(), nil }Apply the same pattern to
NewSandboxSender.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/internal/infrastructure/email/mailjet.go` around lines 84 - 102, The renderWelcomeTemplate function reads and parses the welcome template file on every call, which is inefficient. Add a welcomeTemplate field (of type *template.Template) to the Mailjet sender struct and initialize it once in the NewMailjetSender constructor by reading and parsing the template. Then update renderWelcomeTemplate to accept the cached template as a parameter and only execute it without re-reading or re-parsing. Apply the same pattern to NewSandboxSender to ensure the template is cached there as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/docs/_index.md`:
- Line 22: The Transactional email (Mailjet) row in the documentation index
table is missing the wiring file references that are documented in email.md. Add
the server wiring file and the handler wiring file to the pipe-separated list of
files in the Mailjet index row, ensuring all documented files referenced in
email.md are included alongside the existing use case, sender, template, and
bootstrap file entries.
In `@backend/docs/bootstrap.md`:
- Around line 17-27: The App struct documentation in bootstrap.md is incomplete
and missing the EmailSender field that has been added to the actual App struct.
Add the EmailSender field to the type App struct definition in bootstrap.md
between FCMSender and Config, with appropriate documentation noting whether it
is optional and any corresponding environment variable conditions, following the
same comment pattern used for other optional fields like Firebase and FCMSender.
In `@backend/docs/environment.md`:
- Around line 39-43: The documentation for FROM_EMAIL and FROM_NAME currently
states they are only read when MAILJET_API_KEY is set, but the sender
initialization actually requires both MAILJET_API_KEY and MAILJET_SECRET_KEY to
be present. Update the descriptions for both FROM_EMAIL and FROM_NAME
environment variables to clarify that they are only read when both
MAILJET_API_KEY and MAILJET_SECRET_KEY are provided, rather than just the API
key alone.
In `@backend/docs/routing.md`:
- Around line 20-38: The documentation in routing.md is missing the emailSender
field from the Handler struct and the emailSender parameter from the NewHandler
function signature, even though these have been added to the actual
implementation in handler.go. Add emailSender as a field to the Handler struct
with an appropriate usecase type (following the pattern of other fields like
fcmSender) and add it as the last parameter to the NewHandler function signature
to keep the documentation in sync with the implementation.
In `@backend/internal/bootstrap/bootstrap.go`:
- Around line 147-156: The Mailjet sender initialization in the bootstrap.go
file only validates the presence of MailjetAPIKey and MailjetSecretKey before
creating the emailSender, but does not validate that FromEmail is also
configured. This allows the application to start with a misconfigured sender
that will fail later during email operations. Modify the condition that checks
whether to initialize the Mailjet sender to also verify that cfg.FromEmail is
not empty, ensuring all required configuration fields (MailjetAPIKey,
MailjetSecretKey, and FromEmail) are present. If the configuration is incomplete
or partially configured, return a startup configuration error instead of
silently proceeding with a broken sender.
---
Outside diff comments:
In `@backend/docs/bootstrap.md`:
- Around line 45-55: The bootstrap run sequence documentation is missing the
Mailjet email sender initialization step that occurs in the actual
implementation. Add a new numbered step (between the current step 7 about
Firebase initialization and step 8 about returning the App) that documents the
conditional initialization of the emailSender via the Mailjet client, similar to
how other optional components like Redis and Firebase are documented. This step
should indicate it is skipped when the appropriate configuration is not
provided, maintaining consistency with the documentation pattern for other
conditional initializations.
---
Nitpick comments:
In `@backend/internal/infrastructure/email/mailjet_test.go`:
- Around line 41-43: The comment block above the test that uses NewSandboxSender
contains outdated references to implementation details about withSandbox being
unexported that no longer match the actual test implementation. Replace this
comment with a simpler, clearer explanation that accurately describes what the
test does, specifically that it uses the exported NewSandboxSender constructor
to test the sandbox mode functionality, removing the confusing references to
unexported functions and internal implementation details.
In `@backend/internal/infrastructure/email/mailjet.go`:
- Around line 84-102: The renderWelcomeTemplate function reads and parses the
welcome template file on every call, which is inefficient. Add a welcomeTemplate
field (of type *template.Template) to the Mailjet sender struct and initialize
it once in the NewMailjetSender constructor by reading and parsing the template.
Then update renderWelcomeTemplate to accept the cached template as a parameter
and only execute it without re-reading or re-parsing. Apply the same pattern to
NewSandboxSender to ensure the template is cached there as well.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7058bcd5-22bd-41de-bb76-72ec8c0606fc
⛔ Files ignored due to path filters (1)
backend/go.sumis excluded by!**/*.sum
📒 Files selected for processing (17)
README.mdbackend/.env.examplebackend/docs/_index.mdbackend/docs/bootstrap.mdbackend/docs/email.mdbackend/docs/environment.mdbackend/docs/routing.mdbackend/go.modbackend/internal/bootstrap/bootstrap.gobackend/internal/infrastructure/email/mailjet.gobackend/internal/infrastructure/email/mailjet_test.gobackend/internal/infrastructure/email/templates/welcome.htmlbackend/internal/server/server.gobackend/internal/transport/handlers/handler.gobackend/internal/transport/handlers/health_handler_test.gobackend/internal/usecase/email.gobackend/internal/usecase/email_usecase_test.go
- Validate full Mailjet config at startup: if any of API key, secret key, or FROM_EMAIL is partially set, validateConfig now returns an error rather than starting with a misconfigured sender - Add EmailSender to App struct and Run sequence in bootstrap.md - Add emailSender field and NewHandler param to routing.md - Clarify FROM_EMAIL/FROM_NAME gate in environment.md (both credentials required, not just API key) - Add server.go and handler.go to _index.md Mailjet sources list
Summary
EmailSenderinterface inusecase/andMailjetSenderimplementation ininternal/infrastructure/email/using the Mailjet Go SDK v4welcome.htmltemplate rendered server-side viahtml/template;NewSandboxSenderconstructor for integration testing without real deliveryAppstruct andNewHandlerviaMAILJET_API_KEY,MAILJET_SECRET_KEY,FROM_EMAIL,FROM_NAME— sender isnilwhen keys are absentTest plan
EmailSenderinterface verifies correct args and error propagation (internal/usecase/email_usecase_test.go)httptest.Servercovers 401 error path and success response parsing (internal/infrastructure/email/mailjet_test.go)MAILJET_API_KEY/MAILJET_SECRET_KEYnot set; run with real credentials to hit live Mailjet sandboxgo vet ./...— cleanmake test— all non-Docker tests passpnpm lint && pnpm build— clean./gradlew lint && ./gradlew test— cleanDocs
backend/docs/email.mdcovering interface, template pattern, env setup, and testing approachbackend/docs/environment.mdwith Mailjet + Sentry varsbackend/docs/bootstrap.mdandbackend/docs/routing.md(FCM/queue fields from prior features)Closes #20
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests
Chores