11import nodemailer from 'nodemailer' ;
22import type { TransportOptions as NodemailerTransportOptions } from 'nodemailer' ;
3+ import { setTimeout as sleep } from 'timers/promises' ;
34
45interface SmtpConfig {
56 host : string ;
@@ -18,6 +19,13 @@ interface SmtpConfig {
1819 rejectUnauthorized : boolean ;
1920}
2021
22+ interface HttpMailConfig {
23+ apiKey : string ;
24+ apiUrl : string ;
25+ from : string ;
26+ timeoutMs : number ;
27+ }
28+
2129function getConfig ( ) : SmtpConfig {
2230 const host = process . env . SMTP_HOST ;
2331 const port = Number ( process . env . SMTP_PORT || 587 ) ;
@@ -55,6 +63,15 @@ function getConfig(): SmtpConfig {
5563 } ;
5664}
5765
66+ function getHttpConfig ( ) : HttpMailConfig | null {
67+ const apiKey = process . env . SMTP_API_KEY ;
68+ const from = process . env . SMTP_FROM ;
69+ if ( ! apiKey || ! from ) return null ;
70+ const apiUrl = process . env . SMTP_API_URL || 'https://api.smtp2go.com/v3/email/send' ;
71+ const timeoutMs = Number ( process . env . SMTP_API_TIMEOUT_MS || 10000 ) ;
72+ return { apiKey, apiUrl, from, timeoutMs } ;
73+ }
74+
5875function maskEmail ( email : string ) {
5976 const [ local , domain ] = email . split ( '@' ) ;
6077 if ( ! domain ) return email ;
@@ -116,6 +133,11 @@ type MailTransportOptions = NodemailerTransportOptions & {
116133} ;
117134
118135export async function sendOtpEmail ( to : string , code : string ) {
136+ const httpCfg = getHttpConfig ( ) ;
137+ if ( httpCfg ) {
138+ return sendViaHttpApi ( httpCfg , to , code ) ;
139+ }
140+
119141 const cfg = getConfig ( ) ;
120142 const transportOptions : MailTransportOptions = {
121143 host : cfg . host ,
@@ -164,3 +186,45 @@ export async function sendOtpEmail(to: string, code: string) {
164186 throw wrapped ;
165187 }
166188}
189+
190+ async function sendViaHttpApi ( cfg : HttpMailConfig , to : string , code : string ) {
191+ const startedAt = Date . now ( ) ;
192+ const controller = new AbortController ( ) ;
193+ const timer = setTimeout ( ( ) => controller . abort ( ) , cfg . timeoutMs ) ;
194+ const payload = {
195+ api_key : cfg . apiKey ,
196+ to : [ to ] ,
197+ sender : cfg . from ,
198+ subject : 'Código de verificación' ,
199+ text_body : `Tu código de verificación es: ${ code } ` ,
200+ } ;
201+ try {
202+ const res = await fetch ( cfg . apiUrl , {
203+ method : 'POST' ,
204+ headers : { 'Content-Type' : 'application/json' } ,
205+ body : JSON . stringify ( payload ) ,
206+ signal : controller . signal ,
207+ } ) ;
208+ if ( ! res . ok ) {
209+ const body = await res . text ( ) ;
210+ throw new Error ( `API email error ${ res . status } : ${ body ?. slice ( 0 , 200 ) } ` ) ;
211+ }
212+ const data = ( await res . json ( ) ) as { data ?: { message_id ?: string ; succeeded ?: number } } ;
213+ console . info (
214+ `[mailer] OTP sent via HTTPS API to ${ maskEmail ( to ) } in ${ Date . now ( ) - startedAt } ms (id=${ data ?. data ?. message_id ?? 'n/a' } )` ,
215+ ) ;
216+ return data ;
217+ } catch ( error ) {
218+ if ( error instanceof DOMException && error . name === 'AbortError' ) {
219+ throw new Error ( 'No se pudo enviar el correo (timeout API).' ) ;
220+ }
221+ console . error (
222+ `[mailer] HTTPS email send failed to ${ maskEmail ( to ) } after ${ Date . now ( ) - startedAt } ms` ,
223+ error ,
224+ ) ;
225+ throw error ;
226+ } finally {
227+ clearTimeout ( timer as any ) ;
228+ await sleep ( 0 ) ; // yield event loop
229+ }
230+ }
0 commit comments