2026-09-15 13:00:32 +02:00
import { EventEmitter } from 'node:events' ;
import SMTPConnection from '../smtp-connection/index.js' ;
import wellKnown from '../well-known/index.js' ;
import * as shared from '../shared/index.js' ;
import XOAuth2 from '../xoauth2/index.js' ;
import * as errors from '../errors.js' ;
import * as packageData from '../package-info.js' ;
2020-02-29 23:01:03 +01:00
/**
* Creates a SMTP transport object for Nodemailer
*
* @constructor
2026-09-15 13:00:32 +02:00
* @param options Connection options
2020-02-29 23:01:03 +01:00
*/
class SMTPTransport extends EventEmitter {
constructor ( options ) {
super ();
options = options || {};
if ( typeof options === 'string' ) {
options = {
url : options
};
}
let urlData ;
let service = options . service ;
if ( typeof options . getSocket === 'function' ) {
this . getSocket = options . getSocket ;
}
if ( options . url ) {
urlData = shared . parseConnectionUrl ( options . url );
service = service || urlData . service ;
}
2026-09-15 13:00:32 +02:00
this . options = shared . assign ( false , // create new object
options , // regular options
urlData , // url options
( service && wellKnown ( service )) // wellknown options
2020-02-29 23:01:03 +01:00
);
this . logger = shared . getLogger ( this . options , {
component : this . options . component || 'smtp-transport'
});
this . name = 'SMTP' ;
2026-04-28 12:50:45 +02:00
this . version = packageData . version + '[client:' + packageData . version + ']' ;
2020-02-29 23:01:03 +01:00
if ( this . options . auth ) {
this . auth = this . getAuth ({});
}
}
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
2026-09-15 13:00:32 +02:00
* @param options Connection options
* @param callback Callback function to run with the socket keys
2020-02-29 23:01:03 +01:00
*/
getSocket ( options , callback ) {
// return immediatelly
2026-09-15 13:00:32 +02:00
setImmediate (() => callback ( null , false ));
2020-02-29 23:01:03 +01:00
}
getAuth ( authOpts ) {
if ( ! authOpts ) {
2026-06-15 07:32:52 +02:00
if ( this . auth && this . auth . oauth2 && this . mailer ) {
// Transport-level auth is resolved in the constructor, before the Mail wrapper
// assigns `this.mailer`, so a provision callback registered with
// `transporter.set('oauth2_provision_cb', ...)` has to be re-checked here
this . auth . oauth2 . provisionCallback = this . mailer . get ( 'oauth2_provision_cb' ) || this . auth . oauth2 . provisionCallback ;
}
2020-02-29 23:01:03 +01:00
return this . auth ;
}
2026-09-15 13:00:32 +02:00
const authData = Object . assign ({}, this . options . auth && typeof this . options . auth === 'object' ? this . options . auth : {}, typeof authOpts === 'object' ? authOpts : {});
2026-04-28 12:50:45 +02:00
if ( Object . keys ( authData ). length === 0 ) {
2020-02-29 23:01:03 +01:00
return false ;
}
switch (( authData . type || '' ). toString (). toUpperCase ()) {
case 'OAUTH2' : {
if ( ! authData . service && ! authData . user ) {
return false ;
}
2026-04-28 12:50:45 +02:00
const oauth2 = new XOAuth2 ( authData , this . logger );
2020-02-29 23:01:03 +01:00
oauth2 . provisionCallback = ( this . mailer && this . mailer . get ( 'oauth2_provision_cb' )) || oauth2 . provisionCallback ;
2026-09-15 13:00:32 +02:00
oauth2 . on ( 'token' , ( token ) => this . mailer . emit ( 'token' , token ));
2020-02-29 23:01:03 +01:00
oauth2 . on ( 'error' , err => this . emit ( 'error' , err ));
return {
type : 'OAUTH2' ,
user : authData . user ,
oauth2 ,
method : 'XOAUTH2'
};
}
default :
return {
type : ( authData . type || '' ). toString (). toUpperCase () || 'LOGIN' ,
user : authData . user ,
credentials : {
user : authData . user || '' ,
pass : authData . pass ,
options : authData . options
},
method : ( authData . method || '' ). trim (). toUpperCase () || this . options . authMethod || false
};
}
}
/**
* Sends an e-mail using the selected settings
*
2026-09-15 13:00:32 +02:00
* @param mail Mail object
* @param callback Callback function
2020-02-29 23:01:03 +01:00
*/
send ( mail , callback ) {
this . getSocket ( this . options , ( err , socketOptions ) => {
if ( err ) {
return callback ( err );
}
let returned = false ;
let options = this . options ;
if ( socketOptions && socketOptions . connection ) {
2026-09-15 13:00:32 +02:00
this . logger . info ({
tnx : 'proxy' ,
remoteAddress : socketOptions . connection . remoteAddress ,
remotePort : socketOptions . connection . remotePort ,
destHost : options . host || '' ,
destPort : options . port || '' ,
action : 'connected'
}, 'Using proxied socket from %s:%s to %s:%s' , socketOptions . connection . remoteAddress , socketOptions . connection . remotePort , options . host || '' , options . port || '' );
2020-02-29 23:01:03 +01:00
// only copy options if we need to modify it
2026-04-28 12:50:45 +02:00
options = Object . assign ( shared . assign ( false , options ), socketOptions );
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
const connection = new SMTPConnection ( options );
2026-06-15 07:32:52 +02:00
let perCallAuth ;
const cleanupPerCallAuth = () => {
if ( perCallAuth && perCallAuth !== this . auth && perCallAuth . oauth2 ) {
perCallAuth . oauth2 . removeAllListeners ();
}
perCallAuth = null ;
};
2020-02-29 23:01:03 +01:00
connection . once ( 'error' , err => {
if ( returned ) {
return ;
}
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
connection . close ();
return callback ( err );
});
2026-04-28 12:50:45 +02:00
const sendMessage = () => {
const envelope = mail . message . getEnvelope ();
const messageId = mail . message . messageId ();
const recipients = []. concat ( envelope . to || []);
2020-02-29 23:01:03 +01:00
if ( recipients . length > 3 ) {
recipients . push ( '...and ' + recipients . splice ( 2 ). length + ' more' );
}
if ( mail . data . dsn ) {
envelope . dsn = mail . data . dsn ;
}
2025-12-25 10:58:28 +01:00
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if ( mail . data . requireTLSExtensionEnabled ) {
envelope . requireTLSExtensionEnabled = mail . data . requireTLSExtensionEnabled ;
}
2026-09-15 13:00:32 +02:00
this . logger . info ({
tnx : 'send' ,
messageId
}, 'Sending message %s to <%s>' , messageId , recipients . join ( ', ' ));
2020-02-29 23:01:03 +01:00
connection . send ( envelope , mail . message . createReadStream (), ( err , info ) => {
2026-10-03 10:23:35 +02:00
if ( returned ) {
// the connection error handler has already reported this send
return ;
}
2020-02-29 23:01:03 +01:00
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
connection . close ();
if ( err ) {
2026-09-15 13:00:32 +02:00
this . logger . error ({
err ,
tnx : 'send'
}, 'Send error for %s: %s' , messageId , err . message );
2020-02-29 23:01:03 +01:00
return callback ( err );
}
info . envelope = {
from : envelope . from ,
to : envelope . to
};
info . messageId = messageId ;
try {
return callback ( null , info );
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
this . logger . error ({
err : E ,
tnx : 'callback'
}, 'Callback error for %s: %s' , messageId , E . message );
2020-02-29 23:01:03 +01:00
}
});
};
2026-10-03 10:23:35 +02:00
connection . connect ( err => {
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
2026-10-03 10:23:35 +02:00
if ( err ) {
// the server closed the connection before the greeting
returned = true ;
connection . close ();
return callback ( err );
}
2026-06-15 07:32:52 +02:00
perCallAuth = this . getAuth ( mail . data . auth );
if ( perCallAuth && ( connection . allowsAuth || options . forceAuth )) {
connection . login ( perCallAuth , err => {
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
if ( err ) {
returned = true ;
connection . close ();
return callback ( err );
}
sendMessage ();
});
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
sendMessage ();
}
});
});
}
verify ( callback ) {
let promise ;
if ( ! callback ) {
promise = new Promise (( resolve , reject ) => {
callback = shared . callbackPromise ( resolve , reject );
});
}
2026-10-01 09:48:38 +02:00
// the error paths hand over the error alone
const done = callback ;
2020-02-29 23:01:03 +01:00
this . getSocket ( this . options , ( err , socketOptions ) => {
if ( err ) {
2026-10-01 09:48:38 +02:00
return done ( err );
2020-02-29 23:01:03 +01:00
}
let options = this . options ;
if ( socketOptions && socketOptions . connection ) {
2026-09-15 13:00:32 +02:00
this . logger . info ({
tnx : 'proxy' ,
remoteAddress : socketOptions . connection . remoteAddress ,
remotePort : socketOptions . connection . remotePort ,
destHost : options . host || '' ,
destPort : options . port || '' ,
action : 'connected'
}, 'Using proxied socket from %s:%s to %s:%s' , socketOptions . connection . remoteAddress , socketOptions . connection . remotePort , options . host || '' , options . port || '' );
2026-04-28 12:50:45 +02:00
options = Object . assign ( shared . assign ( false , options ), socketOptions );
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
const connection = new SMTPConnection ( options );
2020-02-29 23:01:03 +01:00
let returned = false ;
2026-06-15 07:32:52 +02:00
let perCallAuth ;
const cleanupPerCallAuth = () => {
if ( perCallAuth && perCallAuth !== this . auth && perCallAuth . oauth2 ) {
perCallAuth . oauth2 . removeAllListeners ();
}
perCallAuth = null ;
};
2020-02-29 23:01:03 +01:00
connection . once ( 'error' , err => {
if ( returned ) {
return ;
}
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
connection . close ();
2026-10-01 09:48:38 +02:00
return done ( err );
2020-02-29 23:01:03 +01:00
});
connection . once ( 'end' , () => {
if ( returned ) {
return ;
}
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2026-10-01 09:48:38 +02:00
return done ( new Error ( 'Connection closed' ));
2020-02-29 23:01:03 +01:00
});
2026-04-28 12:50:45 +02:00
const finalize = () => {
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
connection . quit ();
2026-10-01 09:48:38 +02:00
return done ( null , true );
2020-02-29 23:01:03 +01:00
};
2026-10-03 10:23:35 +02:00
connection . connect ( err => {
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
2026-10-03 10:23:35 +02:00
if ( err ) {
returned = true ;
connection . close ();
return done ( err );
}
2026-06-15 07:32:52 +02:00
perCallAuth = this . getAuth ({});
if ( perCallAuth && ( connection . allowsAuth || options . forceAuth )) {
connection . login ( perCallAuth , err => {
cleanupPerCallAuth ();
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
if ( err ) {
returned = true ;
connection . close ();
2026-10-01 09:48:38 +02:00
return done ( err );
2020-02-29 23:01:03 +01:00
}
finalize ();
});
2026-09-15 13:00:32 +02:00
}
else if ( ! perCallAuth && connection . allowsAuth && options . forceAuth ) {
2026-04-28 12:50:45 +02:00
const err = new Error ( 'Authentication info was not provided' );
2026-02-05 08:49:11 +01:00
err . code = errors . ENOAUTH ;
2022-10-21 16:52:45 +02:00
returned = true ;
2026-06-15 07:32:52 +02:00
cleanupPerCallAuth ();
2022-10-21 16:52:45 +02:00
connection . close ();
2026-10-01 09:48:38 +02:00
return done ( err );
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
finalize ();
}
});
});
return promise ;
}
/**
* Releases resources
*/
close () {
if ( this . auth && this . auth . oauth2 ) {
this . auth . oauth2 . removeAllListeners ();
}
this . emit ( 'close' );
}
}
2026-09-15 13:00:32 +02:00
export default SMTPTransport ;