TStack: Enhanced Email Service Integration
Hey guys! Ever wish you could just effortlessly send emails from your Deno applications? Well, buckle up, because we're diving deep into integrating email sending capabilities into your TStack starter template. We're talking support for multiple providers, from lightweight SMTP (the default) to slick external services like Resend, SendGrid, and AWS SES. Let's make your app a communication powerhouse!
The Lowdown on Email in Deno: Research Findings
Navigating the Deno Email Landscape
So, first things first: let's get real about sending emails in Deno. It's not quite as straightforward as you might think.
- The Reality Check: Deno doesn't have built-in email support. Bummer, right?
- The Good News: You can totally use SMTP libraries, like the excellent
deno.land/x/smtp. - The Even Better News: We can integrate with external APIs. Think Resend, SendGrid, and AWS SES – all ready to go!
Email Options: A Head-to-Head Comparison
Okay, let's break down the different email options out there, so you can pick the best fit for your project. I've broken down the best approaches in a table so that you can view it.
| Method | Free Tier | Pros | Cons | Best For |
|---|---|---|---|---|
| SMTP | Unlimited (own server) | Free, no external dependencies | Complex setup, deliverability issues | Development, small projects |
| Gmail SMTP | 500/day | Free, easy setup | Low limits, app passwords | Testing, personal projects |
| Resend | 3,000/month | Modern API, great DX | Requires API key | Startups, modern apps |
| SendGrid | 100/day | Industry standard | Complex API | Enterprise |
| AWS SES | 62,000/month* | Cheapest at scale | Complex setup | High-volume apps |
| Postmark | 100/month | Best deliverability | Expensive | Transactional emails |
*Free tier only if hosted on AWS
The Deliverability Dilemma: Why External Services Shine
Let's be real, getting your emails delivered is crucial. Here’s why using external services is a smart move:
- SMTP Issues: Setting up SMTP can be a pain. Emails often end up in spam folders without proper configuration. You've gotta set up SPF/DKIM/DMARC records. Residential IPs can get blocked. There's no built-in retry logic or analytics. Plus, it's synchronous, meaning it can block your request thread.
- External Services to the Rescue: These services handle the technical stuff. They ensure proper email authentication (SPF/DKIM/DMARC), offering high deliverability rates (95% or higher!). Plus, you get analytics, webhooks, email templates, async/queue support, and bounce/complaint handling. That's a win-win!
The Recommended Approach: A Hybrid Solution
So, what's the best way forward? I suggest a hybrid approach.
Default: SMTP (Lightweight)
- We'll include basic SMTP support right out of the box.
- It'll work with any SMTP server, like Gmail or Mailgun.
- It's free for development and easy to configure via your
.envfile. - The package size? Super lightweight (~5KB).
Optional: External Providers
- Easy to switch to external providers using environment variables.
- Same API, different provider, so switching is seamless.
- Ready for production when you need it.
Architecture Design: Building the Email Service
Email Provider Abstraction: The Foundation
We're gonna create a clean, flexible foundation. Here's how our EmailProvider interface will look:
// src/shared/services/email/types.ts
export interface EmailOptions {
from?: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
replyTo?: string;
attachments?: Array<{
filename: string;
content: string | Uint8Array;
}>;
}
export interface EmailResponse {
success: boolean;
messageId?: string;
error?: string;
}
export abstract class EmailProvider {
abstract send(options: EmailOptions): Promise<EmailResponse>;
abstract isConfigured(): boolean;
}
Provider Implementations: Making it Real
Now, let's build the concrete implementations for each provider. I'll provide you with each implementation of the code.
1. SMTP Provider (Default) – The Simple Solution
Here's the code for our SMTP provider. It's the default and will get you up and running quickly.
// src/shared/services/email/providers/smtp.provider.ts
import { SmtpClient } from 'https://deno.land/x/smtp@v0.7.0/mod.ts';
import type { EmailProvider, EmailOptions, EmailResponse } from '../types.ts';
export class SMTPProvider implements EmailProvider {
private client: SmtpClient;
constructor() {
this.client = new SmtpClient();
}
isConfigured(): boolean {
return !!(
Deno.env.get('SMTP_HOST') &&
Deno.env.get('SMTP_USER') &&
Deno.env.get('SMTP_PASSWORD')
);
}
async send(options: EmailOptions): Promise<EmailResponse> {
try {
const host = Deno.env.get('SMTP_HOST')!;
const port = parseInt(Deno.env.get('SMTP_PORT') || '587');
const secure = Deno.env.get('SMTP_SECURE') === 'true';
if (secure) {
await this.client.connectTLS({ hostname: host, port });
} else {
await this.client.connect({ hostname: host, port });
}
await this.client.send({
to: Array.isArray(options.to) ? options.to.join(',') : options.to,
subject: options.subject,
content: options.html || options.text || '',
html: options.html,
});
await this.client.close();
return { success: true };
} catch (error) {
console.error('SMTP send failed:', error);
return { success: false, error: error.message };
}
}
}
2. Resend Provider (Optional) – For Modern Apps
Resend is a great choice. Here's how to integrate it:
// src/shared/services/email/providers/resend.provider.ts
import { Resend } from 'https://esm.sh/resend@3.0.0';
import type { EmailProvider, EmailOptions, EmailResponse } from '../types.ts';
export class ResendProvider implements EmailProvider {
private client: Resend;
constructor() {
this.client = new Resend(Deno.env.get('RESEND_API_KEY'));
}
isConfigured(): boolean {
return !!Deno.env.get('RESEND_API_KEY');
}
async send(options: EmailOptions): Promise<EmailResponse> {
try {
const result = await this.client.emails.send({
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
reply_to: options.replyTo,
});
return {
success: true,
messageId: result.data?.id
};
} catch (error) {
console.error('Resend send failed:', error);
return { success: false, error: error.message };
}
}
}
3. SendGrid Provider (Optional) – Industry Standard
Here’s how to use SendGrid:
// src/shared/services/email/providers/sendgrid.provider.ts
import type { EmailProvider, EmailOptions, EmailResponse } from '../types.ts';
export class SendGridProvider implements EmailProvider {
private apiKey: string;
constructor() {
this.apiKey = Deno.env.get('SENDGRID_API_KEY') || ''; // Load API key from environment variable
}
isConfigured(): boolean {
return !!Deno.env.get('SENDGRID_API_KEY');
}
async send(options: EmailOptions): Promise<EmailResponse> {
try {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: Array.isArray(options.to)
? options.to.map(email => ({ email }))
: [{ email: options.to }],
}],
from: { email: options.from || Deno.env.get('EMAIL_FROM')! },
subject: options.subject,
content: [
options.html && { type: 'text/html', value: options.html },
options.text && { type: 'text/plain', value: options.text },
].filter(Boolean),
}),
});
if (response.ok) {
return { success: true, messageId: response.headers.get('x-message-id') || undefined };
}
const error = await response.text();
return { success: false, error };
} catch (error) {
console.error('SendGrid send failed:', error);
return { success: false, error: error.message };
}
}
}
4. AWS SES Provider (Optional) – For High Volume
And finally, AWS SES:
// src/shared/services/email/providers/ses.provider.ts
import type { EmailProvider, EmailOptions, EmailResponse } from '../types.ts';
export class SESProvider implements EmailProvider {
// AWS SES implementation using AWS SDK or direct API calls
// Simplified for now - full implementation during development
isConfigured(): boolean {
return !!(
Deno.env.get('AWS_ACCESS_KEY_ID') &&
Deno.env.get('AWS_SECRET_ACCESS_KEY') &&
Deno.env.get('AWS_REGION')
);
}
async send(options: EmailOptions): Promise<EmailResponse> {
// TODO: Implement AWS SES v4 signing and API call
throw new Error('AWS SES provider not yet implemented');
}
}
Email Service (Main Interface) – The Orchestrator
This is where it all comes together. Here's the EmailService code:
// src/shared/services/email/email.service.ts
import { SMTPProvider } from './providers/smtp.provider.ts';
import { ResendProvider } from './providers/resend.provider.ts';
import { SendGridProvider } from './providers/sendgrid.provider.ts';
import { SESProvider } from './providers/ses.provider.ts';
import type { EmailProvider, EmailOptions, EmailResponse } from './types.ts';
class EmailService {
private provider: EmailProvider;
constructor() {
const providerType = Deno.env.get('EMAIL_PROVIDER') || 'smtp';
switch (providerType.toLowerCase()) {
case 'resend':
this.provider = new ResendProvider();
break;
case 'sendgrid':
this.provider = new SendGridProvider();
break;
case 'ses':
this.provider = new SESProvider();
break;
case 'smtp':
default:
this.provider = new SMTPProvider();
}
if (!this.provider.isConfigured()) {
console.warn(`Email provider '${providerType}' is not properly configured`);
}
}
async send(options: EmailOptions): Promise<EmailResponse> {
return await this.provider.send(options);
}
// Helper methods
async sendText(to: string, subject: string, text: string): Promise<EmailResponse> {
return this.send({ to, subject, text });
}
async sendHTML(to: string, subject: string, html: string): Promise<EmailResponse> {
return this.send({ to, subject, html });
}
}
export const emailService = new EmailService();
Environment Variables: Your Configuration Guide
Now, let’s configure the environment variables.
SMTP Configuration (Default)
# Email Provider Selection
EMAIL_PROVIDER=smtp
# SMTP Settings
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_FROM=noreply@yourdomain.com
Resend Configuration
EMAIL_PROVIDER=resend
RESEND_API_KEY=re_xxxxxxxxxx
EMAIL_FROM=noreply@yourdomain.com
SendGrid Configuration
EMAIL_PROVIDER=sendgrid
SENDGRID_API_KEY=SG.xxxxxxxxxx
EMAIL_FROM=noreply@yourdomain.com
AWS SES Configuration
EMAIL_PROVIDER=ses
AWS_ACCESS_KEY_ID=AKIAxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxx
AWS_REGION=us-east-1
EMAIL_FROM=noreply@yourdomain.com
Usage Examples: Putting it into Action
Let's see how you'll actually use this email service. Easy peasy!
Basic Email Sending
import { emailService } from '@/shared/services/email/email.service';
// Simple text email
await emailService.sendText(
'user@example.com',
'Welcome to TStack!',
'Thanks for signing up.'
);
// HTML email
await emailService.sendHTML(
'user@example.com',
'Welcome to TStack!',
'<h1>Welcome!</h1><p>Thanks for signing up.</p>'
);
// Full options
const result = await emailService.send({
to: 'user@example.com',
subject: 'Password Reset',
html: '<p>Click here to reset: <a href="...">Reset</a></p>',
text: 'Click here to reset: https://...',
replyTo: 'support@example.com',
});
if (result.success) {
console.log('Email sent!', result.messageId);
} else {
console.error('Email failed:', result.error);
}
Integration with Auth System
Here’s a real-world example: integrating with your auth system.
// src/entities/users/user.service.ts
import { emailService } from '@/shared/services/email/email.service';
export class UserService {
async register(data: RegisterDTO) {
const user = await db.insert(users).values(data);
// Send welcome email
await emailService.sendHTML(
user.email,
'Welcome to Our Platform',
'<h1>Welcome to our platform!</h1><p>Thanks for joining us!</p>'
);
return user;
}
async requestPasswordReset(email: string) {
const token = generateResetToken();
await emailService.send({
to: email,
subject: 'Password Reset Request',
html: `<p>Click to reset: <a href="https://app.com/reset?token=${token}">Reset Password</a></p>`,
text: `Reset your password: https://app.com/reset?token=${token}`,
});
}
}
Email Templates: Level Up Your Emails
Here's how you can make your emails look amazing with templates. This is an advanced technique.
// src/shared/services/email/templates/welcome.template.ts
export const welcomeTemplate = (name: string) => ({
subject: 'Welcome to TStack!',
html: `
<html>
<body>
<p>Thanks for joining us.</p>
</body>
</html>
`,
text: `Welcome ${name}! Thanks for joining us.`,
});
// Usage
const email = welcomeTemplate(user.name);
await emailService.send({
to: user.email,
...email,
});
Implementation Phases: Breaking it Down
Let’s outline the phases of bringing this email service to life.
Phase 1: Core Email Service (v1.1.1) - 4 hours
- Create the email service architecture.
- Implement the SMTP provider (the default).
- Add environment configuration.
- Include basic error handling.
- Implement simple text/HTML helper methods.
Phase 2: External Providers (v1.1.1) - 3 hours
- Implement the Resend provider.
- Implement the SendGrid provider.
- Implement provider auto-selection via environment variables.
- Implement configuration validation.
Phase 3: Advanced Features (v1.2.0) - 4 hours
- Implement an email templates system.
- Add async queue support (with Issue #9 Redis).
- Implement retry logic.
- Implement bounce/complaint handling.
Phase 4: AWS SES (v1.2.0) - 3 hours
- Implement the AWS SES provider.
- Implement AWS v4 signature signing.
- Implement region support.
Phase 5: Testing & Documentation (v1.1.1) - 2 hours
- Create unit tests for each provider.
- Create integration tests.
- Write comprehensive documentation.
- Add example usage in the README.
Dependencies: What You’ll Need
Here are the dependencies we're working with:
{
"imports": {
"@std/smtp": "https://deno.land/x/smtp@v0.7.0/mod.ts",
"resend": "https://esm.sh/resend@3.0.0"
}
}
CLI Integration (Optional): Streamlining Setup
Let's add a neat little feature: Using a flag in the CLI to generate with a specific provider.
tstack create my-app --with-email=smtp # Default
tstack create my-app --with-email=resend # Resend setup
tstack create my-app --with-email=sendgrid # SendGrid setup
tstack create my-app --with-email=ses # AWS SES setup
This will:
- Add the necessary dependencies.
- Generate provider-specific config files.
- Set up environment variables.
- Include usage examples.
Security Considerations: Keeping it Safe
Let's keep things secure. Here are the things we must consider:
- Never commit API keys: Use
.envfiles, always. - Validate email addresses: Prevent header injection attacks.
- Implement rate limiting: Prevent abuse (use Issue #9 Redis).
- SPF/DKIM/DMARC: External providers handle this, but make sure to set them up properly.
- Use TLS/SSL: Always use encrypted connections.
Production Recommendations: Choose Wisely
Here’s a quick guide to help you choose the right email service for your project:
Small Projects (less than 500 emails/day)
- Use Gmail SMTP (it's free!).
- Or Resend's free tier (3,000 emails/month).
Medium Projects (500-10K emails/day)
- Use Resend (0/month for 50K emails).
- Or SendGrid (5/month for 40K emails).
Large Scale (10K+ emails/day)
- Use AWS SES (costs about $0.10 per 1,000 emails).
- Or consider a dedicated SMTP server.
Testing Strategy: Making Sure It Works
Here’s how we'll test our email service:
// tests/email.service.test.ts
Deno.test('Email service - SMTP provider', async () => {
const result = await emailService.send({
to: 'test@example.com',
subject: 'Test',
text: 'Testing',
});
assertEquals(result.success, true);
});
// Mock external providers in tests
Deno.test('Email service - Resend provider (mock)', async () => {
// Mock Resend API
// Test provider switching
// Test error handling
});
Related Issues: What’s Next?
Here are some related issues:
- #9 - Redis integration (for an async email queue, which will boost performance).
- #11 - Contact form module (this email service will be essential for that).
- Future: Email templates, a notifications system.
Target Release: When Will This Be Ready?
- v1.1.1: Core SMTP, Resend, and SendGrid providers (the essentials).
- v1.2.0: AWS SES, email templates, and an async queue (more advanced features).
Success Criteria: What We Aim For
Here's what we want to achieve with this integration:
✅ Sending emails via SMTP without relying on external dependencies. ✅ Easy switching to Resend or SendGrid using an environment variable. ✅ A clean abstraction (a provider-agnostic API, so you can swap providers easily). ✅ Robust production-ready error handling. ✅ Thorough documentation and testing to make sure everything works perfectly!
That's it, guys! We hope you have the best experience. Let's make your app a smash hit!"