--- 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 { 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; } interface EmailDto { to: string | string[]; subject: string; html?: string; text?: string; template?: string; variables?: Record; 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 { 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, 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 { 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