TStack: Enhanced Email Service Integration

by SLV Team 43 views
TStack: Supercharge Your App with 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 .env file.
  • 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:

  1. Never commit API keys: Use .env files, always.
  2. Validate email addresses: Prevent header injection attacks.
  3. Implement rate limiting: Prevent abuse (use Issue #9 Redis).
  4. SPF/DKIM/DMARC: External providers handle this, but make sure to set them up properly.
  5. 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!"