Nuevas Épicas (MCH-029 a MCH-033): - Infraestructura SaaS multi-tenant - Auth Social (OAuth2) - Auditoría Empresarial - Feature Flags - Onboarding Wizard Nuevas Integraciones (INT-010 a INT-014): - Email Providers (SendGrid, Mailgun, SES) - Storage Cloud (S3, GCS, Azure) - OAuth Social - Redis Cache - Webhooks Outbound Nuevos ADRs (0004 a 0011): - Notifications Realtime - Feature Flags Strategy - Storage Abstraction - Webhook Retry Strategy - Audit Log Retention - Rate Limiting - OAuth Social Implementation - Email Multi-provider Actualizados: - MASTER_INVENTORY.yml - CONTEXT-MAP.yml - HERENCIA-SIMCO.md - Mapas de documentación Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
296 lines
5.8 KiB
Markdown
296 lines
5.8 KiB
Markdown
---
|
|
id: ADR-0011
|
|
type: ADR
|
|
title: "Email Multi-Provider"
|
|
status: Accepted
|
|
decision_date: 2026-01-10
|
|
updated_at: 2026-01-10
|
|
simco_version: "4.0.1"
|
|
stakeholders:
|
|
- "Equipo MiChangarrito"
|
|
tags:
|
|
- email
|
|
- notifications
|
|
- sendgrid
|
|
- ses
|
|
- multi-provider
|
|
---
|
|
|
|
# ADR-0011: Email Multi-Provider
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | ADR-0011 |
|
|
| **Estado** | Accepted |
|
|
| **Fecha** | 2026-01-10 |
|
|
| **Autor** | Architecture Team |
|
|
| **Supersede** | - |
|
|
|
|
---
|
|
|
|
## Contexto
|
|
|
|
MiChangarrito necesita enviar emails transaccionales (bienvenida, verificacion, notificaciones, facturas). Queremos:
|
|
|
|
1. Alta deliverability
|
|
2. Fallback si un proveedor falla
|
|
3. Flexibilidad para cambiar proveedor
|
|
4. Costos optimizados
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
**Implementamos Factory Pattern con soporte para SendGrid, AWS SES y SMTP generico, con fallback automatico.**
|
|
|
|
Orden de prioridad:
|
|
1. SendGrid (principal)
|
|
2. AWS SES (fallback)
|
|
3. SMTP (ultimo recurso)
|
|
|
|
---
|
|
|
|
## Alternativas Consideradas
|
|
|
|
### Opcion 1: Solo SendGrid
|
|
- **Pros:**
|
|
- Simple
|
|
- Buena API
|
|
- **Cons:**
|
|
- Single point of failure
|
|
- Vendor lock-in
|
|
|
|
### Opcion 2: Solo AWS SES
|
|
- **Pros:**
|
|
- Integrado con AWS
|
|
- Muy economico
|
|
- **Cons:**
|
|
- Setup complejo
|
|
- Sandbox restrictivo
|
|
|
|
### Opcion 3: Multi-Provider con fallback (Elegida)
|
|
- **Pros:**
|
|
- Alta disponibilidad
|
|
- Flexibilidad
|
|
- Optimizacion de costos
|
|
- **Cons:**
|
|
- Mas complejidad
|
|
- Mantener multiples integraciones
|
|
|
|
---
|
|
|
|
## Consecuencias
|
|
|
|
### Positivas
|
|
|
|
1. **Disponibilidad:** Fallback automatico
|
|
2. **Flexibilidad:** Cambiar provider por tenant
|
|
3. **Economia:** Usar el mas economico como default
|
|
4. **Deliverability:** SendGrid tiene buena reputacion
|
|
|
|
### Negativas
|
|
|
|
1. **Complejidad:** Tres implementaciones
|
|
2. **Configuracion:** Multiples sets de credenciales
|
|
|
|
---
|
|
|
|
## Implementacion
|
|
|
|
### Factory
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class EmailProviderFactory {
|
|
private providers: EmailProvider[];
|
|
|
|
constructor(
|
|
private sendgrid: SendGridProvider,
|
|
private ses: SESProvider,
|
|
private smtp: SMTPProvider,
|
|
) {
|
|
// Orden de prioridad
|
|
this.providers = [sendgrid, ses, smtp].filter(p => p.isConfigured());
|
|
}
|
|
|
|
async send(email: EmailDto): Promise<SendResult> {
|
|
for (const provider of this.providers) {
|
|
try {
|
|
return await provider.send(email);
|
|
} catch (error) {
|
|
this.logger.warn(`Provider ${provider.name} failed: ${error.message}`);
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error('All email providers failed');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Interface
|
|
|
|
```typescript
|
|
interface EmailProvider {
|
|
name: string;
|
|
isConfigured(): boolean;
|
|
send(email: EmailDto): Promise<SendResult>;
|
|
}
|
|
|
|
interface EmailDto {
|
|
to: string | string[];
|
|
subject: string;
|
|
html?: string;
|
|
text?: string;
|
|
template?: string;
|
|
variables?: Record<string, any>;
|
|
from?: string;
|
|
replyTo?: string;
|
|
attachments?: Attachment[];
|
|
}
|
|
```
|
|
|
|
### SendGrid Provider
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class SendGridProvider implements EmailProvider {
|
|
name = 'sendgrid';
|
|
|
|
private client: MailService;
|
|
|
|
constructor(config: ConfigService) {
|
|
if (config.get('SENDGRID_API_KEY')) {
|
|
this.client = new MailService();
|
|
this.client.setApiKey(config.get('SENDGRID_API_KEY'));
|
|
}
|
|
}
|
|
|
|
isConfigured(): boolean {
|
|
return !!this.client;
|
|
}
|
|
|
|
async send(email: EmailDto): Promise<SendResult> {
|
|
const msg = {
|
|
to: email.to,
|
|
from: email.from || process.env.EMAIL_FROM,
|
|
subject: email.subject,
|
|
html: email.html,
|
|
text: email.text,
|
|
};
|
|
|
|
const [response] = await this.client.send(msg);
|
|
|
|
return {
|
|
messageId: response.headers['x-message-id'],
|
|
provider: this.name,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Templates
|
|
|
|
### Almacenamiento
|
|
|
|
- Templates en base de datos (tenant-specific)
|
|
- Templates por defecto en codigo (fallback)
|
|
|
|
### Renderizado
|
|
|
|
```typescript
|
|
async renderTemplate(
|
|
key: string,
|
|
variables: Record<string, any>,
|
|
tenantId?: string,
|
|
): Promise<{ html: string; text: string }> {
|
|
// Buscar template custom del tenant
|
|
let template = await this.findTemplate(key, tenantId);
|
|
|
|
// Fallback a template por defecto
|
|
if (!template) {
|
|
template = this.getDefaultTemplate(key);
|
|
}
|
|
|
|
// Renderizar con Handlebars
|
|
const html = Handlebars.compile(template.html)(variables);
|
|
const text = Handlebars.compile(template.text)(variables);
|
|
|
|
return { html, text };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rate Limiting
|
|
|
|
### Por Tenant
|
|
|
|
```typescript
|
|
async checkTenantEmailLimit(tenantId: string): Promise<boolean> {
|
|
const plan = await this.getPlan(tenantId);
|
|
const key = `email:count:${tenantId}:${format(new Date(), 'yyyy-MM-dd-HH')}`;
|
|
|
|
const count = await this.redis.incr(key);
|
|
await this.redis.expire(key, 3600);
|
|
|
|
return count <= plan.emailsPerHour;
|
|
}
|
|
```
|
|
|
|
### Limites
|
|
|
|
| Plan | Por Hora | Por Dia |
|
|
|------|----------|---------|
|
|
| Basic | 100 | 500 |
|
|
| Pro | 1,000 | 10,000 |
|
|
| Enterprise | Ilimitado | Ilimitado |
|
|
|
|
---
|
|
|
|
## Tracking
|
|
|
|
### Webhooks de Estado
|
|
|
|
```typescript
|
|
// POST /webhooks/email/sendgrid
|
|
async handleSendGridWebhook(events: SendGridEvent[]) {
|
|
for (const event of events) {
|
|
await this.emailLogRepo.update(
|
|
{ providerMessageId: event.sg_message_id },
|
|
{
|
|
status: this.mapStatus(event.event),
|
|
[event.event + 'At']: new Date(event.timestamp * 1000),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Estados
|
|
|
|
- sent: Email enviado al proveedor
|
|
- delivered: Entregado al servidor destino
|
|
- opened: Abierto por destinatario (si tracking habilitado)
|
|
- clicked: Link clickeado
|
|
- bounced: Rebotado
|
|
- spam: Marcado como spam
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [SendGrid API](https://docs.sendgrid.com/api-reference)
|
|
- [AWS SES](https://docs.aws.amazon.com/ses/)
|
|
- [Nodemailer](https://nodemailer.com/)
|
|
- [INT-010: Email Providers](../02-integraciones/INT-010-email-providers.md)
|
|
- [MCH-029: Infraestructura SaaS](../01-epicas/MCH-029-infraestructura-saas.md)
|
|
|
|
---
|
|
|
|
**Fecha decision:** 2026-01-10
|
|
**Autores:** Architecture Team
|