@@ -40,6 +40,7 @@ const createSmtpTransport = async (): Promise<MailTransporter> => {
4040 }
4141 : { } ) ,
4242 } ;
43+ // Dynamic import keeps Node-only nodemailer out of Cloudflare email transport bundles.
4344 const { createTransport } = await import ( 'nodemailer' ) ;
4445
4546 return createTransport ( smtpConfig as TransportOptions ) ;
@@ -51,6 +52,7 @@ const getTransporter = async () => {
5152 if ( cachedTransporter ) return cachedTransporter ;
5253
5354 if ( env . NODE_ENV === 'test' ) {
55+ // Dynamic import keeps Node-only nodemailer out of Cloudflare email transport bundles.
5456 const { createTransport } = await import ( 'nodemailer' ) ;
5557 cachedTransporter = createTransport ( {
5658 name : 'noop' ,
@@ -78,59 +80,67 @@ const getTransporter = async () => {
7880
7981 if ( ! hasSmtpConfig ) {
8082 throw new Error (
81- 'Email transport not configured. Set SMTP_TRANSPORT to resend or smtp.' ,
83+ 'Email transport not configured. Set SMTP_TRANSPORT to cloudflare or smtp.' ,
8284 ) ;
8385 }
8486
8587 cachedTransporter = await createSmtpTransport ( ) ;
8688 return cachedTransporter ;
8789} ;
8890
89- const sendResendMail = async ( {
91+ const sendCloudflareMail = async ( {
9092 to,
9193 subject,
9294 body,
9395} : MessageInfo ) : Promise < unknown > => {
94- if ( ! env . RESEND_API_KEY ) {
95- throw new Error ( 'Missing RESEND_API_KEY' ) ;
96+ const apiToken = env . CLOUDFLARE_API_TOKEN ;
97+ const accountId = env . CLOUDFLARE_ACCOUNT_ID ;
98+
99+ if ( ! apiToken || ! accountId ) {
100+ throw new Error ( 'Missing CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID' ) ;
96101 }
97102
98- const response = await fetch ( 'https://api.resend.com/emails' , {
99- method : 'POST' ,
100- headers : {
101- Authorization : `Bearer ${ env . RESEND_API_KEY } ` ,
102- 'Content-Type' : 'application/json' ,
103+ const response = await fetch (
104+ `https://api.cloudflare.com/client/v4/accounts/${ accountId } /email/sending/send` ,
105+ {
106+ method : 'POST' ,
107+ headers : {
108+ Authorization : `Bearer ${ apiToken } ` ,
109+ 'Content-Type' : 'application/json' ,
110+ } ,
111+ body : JSON . stringify ( {
112+ from : env . EMAIL_FROM ?? from ,
113+ to,
114+ subject,
115+ html : body ,
116+ } ) ,
103117 } ,
104- body : JSON . stringify ( {
105- from,
106- to,
107- subject,
108- html : body ,
109- } ) ,
110- } ) ;
111-
112- if ( ! response . ok ) {
113- const errorBody = ( await response . json ( ) . catch ( ( ) => null ) ) as {
114- message ?: unknown ;
115- } | null ;
118+ ) ;
119+ const result = ( await response . json ( ) . catch ( ( ) => null ) ) as {
120+ success ?: boolean ;
121+ errors ?: { message ?: unknown } [ ] ;
122+ } | null ;
123+
124+ if ( ! response . ok || result ?. success !== true ) {
125+ const errorMessage = result ?. errors ?. [ 0 ] ?. message ;
116126 const message =
117- typeof errorBody ?. message === 'string'
118- ? errorBody . message
119- : `Resend email request failed with status ${ response . status } ` ;
127+ typeof errorMessage === 'string'
128+ ? errorMessage
129+ : `Cloudflare email request failed with status ${ response . status } ` ;
120130
121131 throw new Error ( message ) ;
122132 }
123133
124- return response . json ( ) ;
134+ return result ;
125135} ;
126136
127137export const sendMail = async ( {
128138 to,
129139 subject,
130140 body,
131141} : MessageInfo ) : Promise < unknown > => {
132- if ( env . SMTP_TRANSPORT === 'resend ' ) {
133- return sendResendMail ( { to, subject, body } ) ;
142+ if ( env . SMTP_TRANSPORT === 'cloudflare ' ) {
143+ return sendCloudflareMail ( { to, subject, body } ) ;
134144 }
135145
136146 const transporter = await getTransporter ( ) ;
0 commit comments