2026-09-15 13:00:32 +02:00
"use strict" ;
var _ _createBinding = ( this && this . _ _createBinding ) || ( Object . create ? ( function ( o , m , k , k2 ) {
if ( k2 === undefined ) k2 = k ;
var desc = Object . getOwnPropertyDescriptor ( m , k ) ;
if ( ! desc || ( "get" in desc ? ! m . _ _esModule : desc . writable || desc . configurable ) ) {
desc = { enumerable : true , get : function ( ) { return m [ k ] ; } } ;
}
Object . defineProperty ( o , k2 , desc ) ;
} ) : ( function ( o , m , k , k2 ) {
if ( k2 === undefined ) k2 = k ;
o [ k2 ] = m [ k ] ;
} ) ) ;
var _ _setModuleDefault = ( this && this . _ _setModuleDefault ) || ( Object . create ? ( function ( o , v ) {
Object . defineProperty ( o , "default" , { enumerable : true , value : v } ) ;
} ) : function ( o , v ) {
o [ "default" ] = v ;
} ) ;
var _ _importStar = ( this && this . _ _importStar ) || ( function ( ) {
var ownKeys = function ( o ) {
ownKeys = Object . getOwnPropertyNames || function ( o ) {
var ar = [ ] ;
for ( var k in o ) if ( Object . prototype . hasOwnProperty . call ( o , k ) ) ar [ ar . length ] = k ;
return ar ;
} ;
return ownKeys ( o ) ;
} ;
return function ( mod ) {
if ( mod && mod . _ _esModule ) return mod ;
var result = { } ;
if ( mod != null ) for ( var k = ownKeys ( mod ) , i = 0 ; i < k . length ; i ++ ) if ( k [ i ] !== "default" ) _ _createBinding ( result , mod , k [ i ] ) ;
_ _setModuleDefault ( result , mod ) ;
return result ;
} ;
} ) ( ) ;
var _ _importDefault = ( this && this . _ _importDefault ) || function ( mod ) {
return ( mod && mod . _ _esModule ) ? mod : { "default" : mod } ;
} ;
Object . defineProperty ( exports , "__esModule" , { value : true } ) ;
const node _events _1 = require ( "node:events" ) ;
const pool _resource _js _1 = _ _importDefault ( require ( "./pool-resource.js" ) ) ;
const index _js _1 = _ _importDefault ( require ( "../smtp-connection/index.js" ) ) ;
const index _js _2 = _ _importDefault ( require ( "../well-known/index.js" ) ) ;
const shared = _ _importStar ( require ( "../shared/index.js" ) ) ;
const errors = _ _importStar ( require ( "../errors.js" ) ) ;
const packageData = _ _importStar ( require ( "../package-info.js" ) ) ;
2020-02-29 23:01:03 +01:00
/**
* Creates a SMTP pool transport object for Nodemailer
*
* @constructor
2026-09-15 13:00:32 +02:00
* @param options SMTP Connection options
2020-02-29 23:01:03 +01:00
*/
2026-09-15 13:00:32 +02:00
class SMTPPool extends node _events _1 . EventEmitter {
2020-02-29 23:01:03 +01:00
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 && ( 0 , index _js _2 . default ) ( service ) ) // wellknown options
2020-02-29 23:01:03 +01:00
) ;
this . options . maxConnections = this . options . maxConnections || 5 ;
this . options . maxMessages = this . options . maxMessages || 100 ;
this . logger = shared . getLogger ( this . options , {
component : this . options . component || 'smtp-pool'
} ) ;
this . name = 'SMTP (pool)' ;
2026-04-28 12:50:45 +02:00
this . version = packageData . version + '[client:' + packageData . version + ']' ;
2020-02-29 23:01:03 +01:00
this . _rateLimit = {
counter : 0 ,
timeout : null ,
waiting : [ ] ,
checkpoint : false ,
delta : Number ( this . options . rateDelta ) || 1000 ,
limit : Number ( this . options . rateLimit ) || 0
} ;
this . _closed = false ;
this . _queue = [ ] ;
this . _connections = [ ] ;
this . _connectionCounter = 0 ;
this . idling = true ;
setImmediate ( ( ) => {
if ( this . idling ) {
this . emit ( 'idle' ) ;
}
} ) ;
}
/**
* 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
}
/**
* Queues an e-mail to be sent 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 ) {
if ( this . _closed ) {
return false ;
}
this . _queue . push ( {
mail ,
requeueAttempts : 0 ,
callback
} ) ;
if ( this . idling && this . _queue . length >= this . options . maxConnections ) {
this . idling = false ;
}
setImmediate ( ( ) => this . _processMessages ( ) ) ;
return true ;
}
/**
* Closes all connections in the pool. If there is a message being sent, the connection
* is closed later
*/
close ( ) {
let connection ;
2026-04-28 12:50:45 +02:00
const len = this . _connections . length ;
2020-02-29 23:01:03 +01:00
this . _closed = true ;
2026-09-15 13:00:32 +02:00
// release connections gated by the rate limiter so they become
// available and are torn down below instead of leaking with a
// cleared timer that never fires
this . _clearRateLimit ( ) ;
2020-02-29 23:01:03 +01:00
if ( ! len && ! this . _queue . length ) {
return ;
}
// remove all available connections
for ( let i = len - 1 ; i >= 0 ; i -- ) {
if ( this . _connections [ i ] && this . _connections [ i ] . available ) {
connection = this . _connections [ i ] ;
connection . close ( ) ;
2026-09-15 13:00:32 +02:00
this . logger . info ( {
tnx : 'connection' ,
cid : connection . id ,
action : 'removed'
} , 'Connection #%s removed' , connection . id ) ;
2020-02-29 23:01:03 +01:00
}
}
if ( len && ! this . _connections . length ) {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'connection'
} , 'All connections removed' ) ;
2020-02-29 23:01:03 +01:00
}
if ( ! this . _queue . length ) {
return ;
}
// make sure that entire queue would be cleaned
2026-04-28 12:50:45 +02:00
const invokeCallbacks = ( ) => {
2020-02-29 23:01:03 +01:00
if ( ! this . _queue . length ) {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'connection'
} , 'Pending queue entries cleared' ) ;
2020-02-29 23:01:03 +01:00
return ;
}
2026-04-28 12:50:45 +02:00
const entry = this . _queue . shift ( ) ;
2020-02-29 23:01:03 +01:00
if ( entry && typeof entry . callback === 'function' ) {
try {
entry . callback ( new Error ( 'Connection pool was closed' ) ) ;
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
// the queue is drained without a connection, so there is no cid to log
this . logger . error ( {
err : E ,
tnx : 'callback'
} , 'Callback error: %s' , E . message ) ;
2020-02-29 23:01:03 +01:00
}
}
setImmediate ( invokeCallbacks ) ;
} ;
setImmediate ( invokeCallbacks ) ;
}
/**
* Check the queue and available connections. If there is a message to be sent and there is
* an available connection, then use this connection to send the mail
2026-09-15 13:00:32 +02:00
* @internal
2020-02-29 23:01:03 +01:00
*/
_processMessages ( ) {
// do nothing if already closed
if ( this . _closed ) {
return ;
}
// do nothing if queue is empty
if ( ! this . _queue . length ) {
if ( ! this . idling ) {
// no pending jobs
this . idling = true ;
this . emit ( 'idle' ) ;
}
return ;
}
// find first available connection
2026-04-28 12:50:45 +02:00
let connection = this . _connections . find ( c => c . available ) ;
2020-02-29 23:01:03 +01:00
if ( ! connection && this . _connections . length < this . options . maxConnections ) {
connection = this . _createConnection ( ) ;
}
if ( ! connection ) {
// no more free connection slots available
this . idling = false ;
return ;
}
// check if there is free space in the processing queue
if ( ! this . idling && this . _queue . length < this . options . maxConnections ) {
this . idling = true ;
this . emit ( 'idle' ) ;
}
2026-04-28 12:50:45 +02:00
const entry = ( connection . queueEntry = this . _queue . shift ( ) ) ;
2020-02-29 23:01:03 +01:00
entry . messageId = ( connection . queueEntry . mail . message . getHeader ( 'message-id' ) || '' ) . replace ( /[<>\s]/g , '' ) ;
connection . available = false ;
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'pool' ,
cid : connection . id ,
messageId : entry . messageId ,
action : 'assign'
} , 'Assigned message <%s> to #%s (%s)' , entry . messageId , connection . id , connection . messages + 1 ) ;
2020-02-29 23:01:03 +01:00
if ( this . _rateLimit . limit ) {
this . _rateLimit . counter ++ ;
if ( ! this . _rateLimit . checkpoint ) {
this . _rateLimit . checkpoint = Date . now ( ) ;
}
}
connection . send ( entry . mail , ( err , info ) => {
// only process callback if current handler is not changed
if ( entry === connection . queueEntry ) {
try {
entry . callback ( err , info ) ;
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
this . logger . error ( {
err : E ,
tnx : 'callback' ,
cid : connection . id
} , 'Callback error for #%s: %s' , connection . id , E . message ) ;
2020-02-29 23:01:03 +01:00
}
connection . queueEntry = false ;
}
} ) ;
}
/**
* Creates a new pool resource
2026-09-15 13:00:32 +02:00
* @internal
2020-02-29 23:01:03 +01:00
*/
_createConnection ( ) {
2026-09-15 13:00:32 +02:00
const connection = new pool _resource _js _1 . default ( this ) ;
2020-02-29 23:01:03 +01:00
connection . id = ++ this . _connectionCounter ;
2026-09-15 13:00:32 +02:00
this . logger . info ( {
tnx : 'pool' ,
cid : connection . id ,
action : 'conection'
} , 'Created new pool resource #%s' , connection . id ) ;
2020-02-29 23:01:03 +01:00
// resource comes available
connection . on ( 'available' , ( ) => {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'connection' ,
cid : connection . id ,
action : 'available'
} , 'Connection #%s became available' , connection . id ) ;
2020-02-29 23:01:03 +01:00
if ( this . _closed ) {
// if already closed run close() that will remove this connections from connections list
this . close ( ) ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
// check if there's anything else to send
this . _processMessages ( ) ;
}
} ) ;
// resource is terminated with an error
2026-09-15 13:00:32 +02:00
connection . once ( 'error' , ( err ) => {
2026-04-28 12:50:45 +02:00
if ( err . code !== errors . EMAXLIMIT ) {
2026-09-15 13:00:32 +02:00
this . logger . warn ( {
err ,
tnx : 'pool' ,
cid : connection . id
} , 'Pool Error for #%s: %s' , connection . id , err . message ) ;
}
else {
this . logger . debug ( {
tnx : 'pool' ,
cid : connection . id ,
action : 'maxlimit'
} , 'Max messages limit exchausted for #%s' , connection . id ) ;
2020-02-29 23:01:03 +01:00
}
if ( connection . queueEntry ) {
try {
connection . queueEntry . callback ( err ) ;
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
this . logger . error ( {
err : E ,
tnx : 'callback' ,
cid : connection . id
} , 'Callback error for #%s: %s' , connection . id , E . message ) ;
2020-02-29 23:01:03 +01:00
}
connection . queueEntry = false ;
}
// remove the erroneus connection from connections list
this . _removeConnection ( connection ) ;
this . _continueProcessing ( ) ;
} ) ;
connection . once ( 'close' , ( ) => {
2026-09-15 13:00:32 +02:00
this . logger . info ( {
tnx : 'connection' ,
cid : connection . id ,
action : 'closed'
} , 'Connection #%s was closed' , connection . id ) ;
2020-02-29 23:01:03 +01:00
this . _removeConnection ( connection ) ;
if ( connection . queueEntry ) {
// If the connection closed when sending, add the message to the queue again
// if max number of requeues is not reached yet
// Note that we must wait a bit.. because the callback of the 'error' handler might be called
// in the next event loop
setTimeout ( ( ) => {
if ( connection . queueEntry ) {
if ( this . _shouldRequeuOnConnectionClose ( connection . queueEntry ) ) {
this . _requeueEntryOnConnectionClose ( connection ) ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
this . _failDeliveryOnConnectionClose ( connection ) ;
}
}
this . _continueProcessing ( ) ;
} , 50 ) ;
2026-09-15 13:00:32 +02:00
}
else {
2025-12-25 10:58:28 +01:00
if ( ! this . _closed && this . idling && ! this . _connections . length ) {
this . emit ( 'clear' ) ;
}
2020-02-29 23:01:03 +01:00
this . _continueProcessing ( ) ;
}
} ) ;
this . _connections . push ( connection ) ;
return connection ;
}
2026-09-15 13:00:32 +02:00
/** @internal */
2020-02-29 23:01:03 +01:00
_shouldRequeuOnConnectionClose ( queueEntry ) {
if ( this . options . maxRequeues === undefined || this . options . maxRequeues < 0 ) {
return true ;
}
2020-03-24 13:34:13 +01:00
return queueEntry . requeueAttempts < this . options . maxRequeues ;
2020-02-29 23:01:03 +01:00
}
2026-09-15 13:00:32 +02:00
/** @internal */
2020-02-29 23:01:03 +01:00
_failDeliveryOnConnectionClose ( connection ) {
if ( connection . queueEntry && connection . queueEntry . callback ) {
try {
connection . queueEntry . callback ( new Error ( 'Reached maximum number of retries after connection was closed' ) ) ;
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
this . logger . error ( {
err : E ,
tnx : 'callback' ,
messageId : connection . queueEntry . messageId ,
cid : connection . id
} , 'Callback error for #%s: %s' , connection . id , E . message ) ;
2020-02-29 23:01:03 +01:00
}
connection . queueEntry = false ;
}
}
2026-09-15 13:00:32 +02:00
/** @internal */
2020-02-29 23:01:03 +01:00
_requeueEntryOnConnectionClose ( connection ) {
2026-04-28 12:50:45 +02:00
connection . queueEntry . requeueAttempts += 1 ;
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'pool' ,
cid : connection . id ,
messageId : connection . queueEntry . messageId ,
action : 'requeue'
} , 'Re-queued message <%s> for #%s. Attempt: #%s' , connection . queueEntry . messageId , connection . id , connection . queueEntry . requeueAttempts ) ;
2020-02-29 23:01:03 +01:00
this . _queue . unshift ( connection . queueEntry ) ;
connection . queueEntry = false ;
}
/**
* Continue to process message if the pool hasn't closed
2026-09-15 13:00:32 +02:00
* @internal
2020-02-29 23:01:03 +01:00
*/
_continueProcessing ( ) {
if ( this . _closed ) {
this . close ( ) ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
setTimeout ( ( ) => this . _processMessages ( ) , 100 ) ;
}
}
/**
* Remove resource from pool
*
2026-09-15 13:00:32 +02:00
* @param connection The PoolResource to remove
* @internal
2020-02-29 23:01:03 +01:00
*/
_removeConnection ( connection ) {
2026-04-28 12:50:45 +02:00
const index = this . _connections . indexOf ( connection ) ;
2020-02-29 23:01:03 +01:00
if ( index !== - 1 ) {
this . _connections . splice ( index , 1 ) ;
}
}
/**
* Checks if connections have hit current rate limit and if so, queues the availability callback
*
2026-09-15 13:00:32 +02:00
* @param callback Callback function to run once rate limiter has been cleared
* @internal
2020-02-29 23:01:03 +01:00
*/
_checkRateLimit ( callback ) {
if ( ! this . _rateLimit . limit ) {
return callback ( ) ;
}
2026-04-28 12:50:45 +02:00
const now = Date . now ( ) ;
2020-02-29 23:01:03 +01:00
if ( this . _rateLimit . counter < this . _rateLimit . limit ) {
return callback ( ) ;
}
this . _rateLimit . waiting . push ( callback ) ;
if ( this . _rateLimit . checkpoint <= now - this . _rateLimit . delta ) {
return this . _clearRateLimit ( ) ;
2026-04-28 12:50:45 +02:00
}
if ( ! this . _rateLimit . timeout ) {
2020-02-29 23:01:03 +01:00
this . _rateLimit . timeout = setTimeout ( ( ) => this . _clearRateLimit ( ) , this . _rateLimit . delta - ( now - this . _rateLimit . checkpoint ) ) ;
this . _rateLimit . checkpoint = now ;
}
}
/**
* Clears current rate limit limitation and runs paused callback
2026-09-15 13:00:32 +02:00
* @internal
2020-02-29 23:01:03 +01:00
*/
_clearRateLimit ( ) {
clearTimeout ( this . _rateLimit . timeout ) ;
this . _rateLimit . timeout = null ;
this . _rateLimit . counter = 0 ;
this . _rateLimit . checkpoint = false ;
// resume all paused connections
while ( this . _rateLimit . waiting . length ) {
2026-04-28 12:50:45 +02:00
const cb = this . _rateLimit . waiting . shift ( ) ;
2020-02-29 23:01:03 +01:00
setImmediate ( cb ) ;
}
}
/**
* Returns true if there are free slots in the queue
*/
isIdle ( ) {
return this . idling ;
}
verify ( callback ) {
let promise ;
if ( ! callback ) {
promise = new Promise ( ( resolve , reject ) => {
callback = shared . callbackPromise ( resolve , reject ) ;
} ) ;
}
2026-09-15 13:00:32 +02:00
const auth = new pool _resource _js _1 . default ( this ) . auth ;
2020-02-29 23:01:03 +01:00
this . getSocket ( this . options , ( err , socketOptions ) => {
if ( err ) {
return callback ( err ) ;
}
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-09-15 13:00:32 +02:00
const connection = new index _js _1 . default ( options ) ;
2020-02-29 23:01:03 +01:00
let returned = false ;
connection . once ( 'error' , err => {
if ( returned ) {
return ;
}
returned = true ;
connection . close ( ) ;
return callback ( err ) ;
} ) ;
connection . once ( 'end' , ( ) => {
if ( returned ) {
return ;
}
returned = true ;
return callback ( new Error ( 'Connection closed' ) ) ;
} ) ;
2026-04-28 12:50:45 +02:00
const finalize = ( ) => {
2020-02-29 23:01:03 +01:00
if ( returned ) {
return ;
}
returned = true ;
connection . quit ( ) ;
return callback ( null , true ) ;
} ;
connection . connect ( ( ) => {
if ( returned ) {
return ;
}
2020-03-24 13:34:13 +01:00
if ( auth && ( connection . allowsAuth || options . forceAuth ) ) {
2020-02-29 23:01:03 +01:00
connection . login ( auth , err => {
if ( returned ) {
return ;
}
if ( err ) {
returned = true ;
connection . close ( ) ;
return callback ( err ) ;
}
finalize ( ) ;
} ) ;
2026-09-15 13:00:32 +02:00
}
else if ( ! auth && 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 ;
connection . close ( ) ;
return callback ( err ) ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
finalize ( ) ;
}
} ) ;
} ) ;
return promise ;
}
}
2026-09-15 13:00:32 +02:00
exports . default = SMTPPool ;
module . exports = exports . default ;
Object . defineProperty ( module . exports , 'default' , { value : exports . default , enumerable : false , writable : true , configurable : true } ) ;