@@ -88,7 +88,7 @@ jest.mock('@urbackend/common', () => {
8888 } ;
8989} ) ;
9090
91- const { Project, decrypt, redis, publicEmailQueue, MailTemplate, MailLog, AppError } = require ( '@urbackend/common' ) ;
91+ const { Project, decrypt, redis, publicEmailQueue, MailTemplate, MailLog, AppError, sendMailSchema } = require ( '@urbackend/common' ) ;
9292const mailController = require ( '../controllers/mail.controller' ) ;
9393const originalResendApiKey2 = process . env . RESEND_API_KEY_2 ;
9494
@@ -192,6 +192,23 @@ describe('mail.controller', () => {
192192 } ) ) ;
193193 } ) ;
194194
195+ test ( 'queues replyTo using the Resend payload field' , async ( ) => {
196+ const req = makeReq ( ) ;
197+ req . body . replyTo = 'replies@example.com' ;
198+ const res = makeRes ( ) ;
199+
200+ mockProjectConfig ( { _id : 'proj_1' , resendApiKey : null } ) ;
201+ decrypt . mockReturnValue ( null ) ;
202+ redis . eval . mockResolvedValue ( 1 ) ;
203+
204+ await mailController . sendMail ( req , res , next ) ;
205+
206+ expect ( publicEmailQueue . add ) . toHaveBeenCalledWith ( 'send-public-email' , expect . objectContaining ( {
207+ payload : expect . objectContaining ( { replyTo : 'replies@example.com' } )
208+ } ) , expect . any ( Object ) ) ;
209+ expect ( publicEmailQueue . add . mock . calls [ 0 ] [ 1 ] . payload . reply_to ) . toBeUndefined ( ) ;
210+ } ) ;
211+
195212 test ( 'enforces monthly limit' , async ( ) => {
196213 const req = makeReq ( ) ;
197214 const res = makeRes ( ) ;
@@ -502,6 +519,52 @@ describe('mail.controller', () => {
502519 expect ( next . mock . calls [ 0 ] [ 0 ] . statusCode ) . toBe ( 503 ) ;
503520 } ) ;
504521
522+ test ( 'uses Resend replyTo field for batch messages' , async ( ) => {
523+ const req = makeReq ( ) ;
524+ req . body = [ {
525+ to : 'u@example.com' ,
526+ replyTo : 'replies@example.com' ,
527+ subject : 'Batch' ,
528+ text : 'Hello'
529+ } ] ;
530+ const res = makeRes ( ) ;
531+
532+ mockProjectConfig ( { _id : 'proj_1' , resendApiKey : null } ) ;
533+ decrypt . mockReturnValue ( null ) ;
534+ redis . eval . mockResolvedValue ( 1 ) ;
535+ mockResendClient . batch . send . mockResolvedValue ( { data : [ { id : 're_123' } ] , error : null } ) ;
536+
537+ await mailController . sendBatchMail ( req , res , next ) ;
538+
539+ expect ( mockResendClient . batch . send ) . toHaveBeenCalledWith ( [
540+ expect . objectContaining ( { replyTo : [ 'replies@example.com' ] } )
541+ ] ) ;
542+ expect ( mockResendClient . batch . send . mock . calls [ 0 ] [ 0 ] [ 0 ] . reply_to ) . toBeUndefined ( ) ;
543+ } ) ;
544+
545+ test . each ( [
546+ [ { to : 'u@example.com' , subject : ' ' , text : 'Hello' } ] ,
547+ [ { to : 'u@example.com' , subject : 'Batch' , html : ' ' , text : '\n\t' } ] ,
548+ [ { to : 'u@example.com' , replyTo : [ ] , subject : 'Batch' , text : 'Hello' } ] ,
549+ ] ) ( 'rejects invalid batch mail payload %o' , async ( body ) => {
550+ const req = makeReq ( ) ;
551+ req . body = body ;
552+
553+ await mailController . sendBatchMail ( req , makeRes ( ) , next ) ;
554+
555+ expect ( next ) . toHaveBeenCalledWith ( expect . any ( AppError ) ) ;
556+ expect ( next . mock . calls [ 0 ] [ 0 ] . statusCode ) . toBe ( 400 ) ;
557+ } ) ;
558+
559+ test ( 'rejects an empty replyTo list for single-message mail' , ( ) => {
560+ expect ( ( ) => sendMailSchema . parse ( {
561+ to : 'u@example.com' ,
562+ replyTo : [ ] ,
563+ subject : 'Hello' ,
564+ text : 'Message'
565+ } ) ) . toThrow ( 'Reply-to list cannot be empty' ) ;
566+ } ) ;
567+
505568 test ( 'enforces BYOK gate for audience creation' , async ( ) => {
506569 const req = makeReq ( ) ;
507570 req . body = { name : 'Audience A' } ;
0 commit comments