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 _stream _1 = require ( "node:stream" ) ;
const index _js _1 = _ _importDefault ( require ( "../fetch/index.js" ) ) ;
const node _crypto _1 = _ _importDefault ( require ( "node:crypto" ) ) ;
const shared = _ _importStar ( require ( "../shared/index.js" ) ) ;
const errors = _ _importStar ( require ( "../errors.js" ) ) ;
2020-02-29 23:01:03 +01:00
/**
* XOAUTH2 access_token generator for Gmail.
* Create client ID for web applications in Google API console to use it.
* See Offline Access for receiving the needed refreshToken for an user
* https://developers.google.com/accounts/docs/OAuth2WebServer#offline
*
* Usage for generating access tokens with a custom method using provisionCallback:
* provisionCallback(user, renew, callback)
* * user is the username to get the token for
* * renew is a boolean that if true indicates that existing token failed and needs to be renewed
* * callback is the callback to run with (error, accessToken [, expires])
* * accessToken is a string
* * expires is an optional expire time in milliseconds
* If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself
*
* @constructor
2026-09-15 13:00:32 +02:00
* @param options Client information for token generation
* @param options.user User e-mail address
* @param options.clientId Client ID value
* @param options.clientSecret Client secret value
* @param options.refreshToken Refresh token for an user
* @param options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
* @param options.accessToken An existing valid accessToken
* @param options.privateKey Private key for JSW
* @param options.expires Optional Access Token expire time in ms
* @param options.timeout Optional TTL for Access Token in seconds
* @param options.provisionCallback Function to run when a new access token is required
* @param options.tls Optional TLS options forwarded to the HTTPS token request. Defaults to strict cert validation; supply { rejectUnauthorized: false } only for self-hosted OAuth providers on private CAs.
2020-02-29 23:01:03 +01:00
*/
2026-09-15 13:00:32 +02:00
class XOAuth2 extends node _stream _1 . Stream {
2020-02-29 23:01:03 +01:00
constructor ( options , logger ) {
super ( ) ;
this . options = options || { } ;
if ( options && options . serviceClient ) {
if ( ! options . privateKey || ! options . user ) {
2026-04-28 12:50:45 +02:00
const err = new Error ( 'Options "privateKey" and "user" are required for service account!' ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
setImmediate ( ( ) => this . emit ( 'error' , err ) ) ;
2020-02-29 23:01:03 +01:00
return ;
}
2026-04-28 12:50:45 +02:00
const serviceRequestTimeout = Math . min ( Math . max ( Number ( this . options . serviceRequestTimeout ) || 0 , 0 ) , 3600 ) ;
2020-02-29 23:01:03 +01:00
this . options . serviceRequestTimeout = serviceRequestTimeout || 5 * 60 ;
}
2026-09-15 13:00:32 +02:00
this . logger = shared . getLogger ( {
logger
} , {
component : this . options . component || 'OAuth2'
} ) ;
2020-02-29 23:01:03 +01:00
this . provisionCallback = typeof this . options . provisionCallback === 'function' ? this . options . provisionCallback : false ;
this . options . accessUrl = this . options . accessUrl || 'https://accounts.google.com/o/oauth2/token' ;
this . options . customHeaders = this . options . customHeaders || { } ;
this . options . customParams = this . options . customParams || { } ;
this . accessToken = this . options . accessToken || false ;
if ( this . options . expires && Number ( this . options . expires ) ) {
this . expires = this . options . expires ;
2026-09-15 13:00:32 +02:00
}
else {
2026-04-28 12:50:45 +02:00
const timeout = Math . max ( Number ( this . options . timeout ) || 0 , 0 ) ;
2020-02-29 23:01:03 +01:00
this . expires = ( timeout && Date . now ( ) + timeout * 1000 ) || 0 ;
}
2025-12-25 10:58:28 +01:00
this . renewing = false ; // Track if renewal is in progress
this . renewalQueue = [ ] ; // Queue for pending requests during renewal
2020-02-29 23:01:03 +01:00
}
/**
* Returns or generates (if previous has expired) a XOAuth2 token
*
2026-09-15 13:00:32 +02:00
* @param renew If false then use cached access token (if available)
* @param callback Callback function with error object and token string
2020-02-29 23:01:03 +01:00
*/
getToken ( renew , callback ) {
if ( ! renew && this . accessToken && ( ! this . expires || this . expires > Date . now ( ) ) ) {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'reuse'
} , 'Reusing existing access token for %s' , this . options . user ) ;
2020-02-29 23:01:03 +01:00
return callback ( null , this . accessToken ) ;
}
2025-12-25 10:58:28 +01:00
// check if it is possible to renew, if not, return the current token or error
if ( ! this . provisionCallback && ! this . options . refreshToken && ! this . options . serviceClient ) {
if ( this . accessToken ) {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
2025-12-25 10:58:28 +01:00
tnx : 'OAUTH2' ,
user : this . options . user ,
2026-09-15 13:00:32 +02:00
action : 'reuse'
} , 'Reusing existing access token (no refresh capability) for %s' , this . options . user ) ;
return callback ( null , this . accessToken ) ;
}
this . logger . error ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'renew'
} , 'Cannot renew access token for %s: No refresh mechanism available' , this . options . user ) ;
2026-04-28 12:50:45 +02:00
const err = new Error ( "Can't create new access token for user" ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2025-12-25 10:58:28 +01:00
}
// If renewal already in progress, queue this request instead of starting another
if ( this . renewing ) {
2026-09-15 13:00:32 +02:00
this . renewalQueue . push ( { renew , callback } ) ;
return ;
2025-12-25 10:58:28 +01:00
}
this . renewing = true ;
// Handles token renewal completion - processes queued requests and cleans up
const generateCallback = ( err , accessToken ) => {
this . renewalQueue . forEach ( item => item . callback ( err , accessToken ) ) ;
this . renewalQueue = [ ] ;
this . renewing = false ;
if ( err ) {
2026-09-15 13:00:32 +02:00
this . logger . error ( {
err ,
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'renew'
} , 'Failed generating new Access Token for %s' , this . options . user ) ;
}
else {
this . logger . info ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'renew'
} , 'Generated new Access Token for %s' , this . options . user ) ;
2020-02-29 23:01:03 +01:00
}
2025-12-25 10:58:28 +01:00
// Complete original request
callback ( err , accessToken ) ;
2020-02-29 23:01:03 +01:00
} ;
if ( this . provisionCallback ) {
this . provisionCallback ( this . options . user , ! ! renew , ( err , accessToken , expires ) => {
if ( ! err && accessToken ) {
this . accessToken = accessToken ;
this . expires = expires || 0 ;
}
generateCallback ( err , accessToken ) ;
} ) ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
this . generateToken ( generateCallback ) ;
}
}
/**
* Updates token values
*
2026-09-15 13:00:32 +02:00
* @param accessToken New access token
* @param timeout Access token lifetime in seconds
2020-02-29 23:01:03 +01:00
*
* Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
*/
updateToken ( accessToken , timeout ) {
this . accessToken = accessToken ;
timeout = Math . max ( Number ( timeout ) || 0 , 0 ) ;
this . expires = ( timeout && Date . now ( ) + timeout * 1000 ) || 0 ;
this . emit ( 'token' , {
user : this . options . user ,
accessToken : accessToken || '' ,
expires : this . expires
} ) ;
}
/**
* Generates a new XOAuth2 token with the credentials provided at initialization
*
2026-09-15 13:00:32 +02:00
* @param callback Callback function with error object and token string
2020-02-29 23:01:03 +01:00
*/
generateToken ( callback ) {
let urlOptions ;
let loggedUrlOptions ;
if ( this . options . serviceClient ) {
// service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
2026-04-28 12:50:45 +02:00
const iat = Math . floor ( Date . now ( ) / 1000 ) ; // unix time
const tokenData = {
2020-02-29 23:01:03 +01:00
iss : this . options . serviceClient ,
scope : this . options . scope || 'https://mail.google.com/' ,
sub : this . options . user ,
aud : this . options . accessUrl ,
iat ,
exp : iat + this . options . serviceRequestTimeout
} ;
2020-07-09 15:48:05 +02:00
let token ;
try {
token = this . jwtSignRS256 ( tokenData ) ;
2026-09-15 13:00:32 +02:00
}
catch ( _err ) {
2026-04-28 12:50:45 +02:00
const err = new Error ( "Can't generate token. Check your auth options" ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2020-07-09 15:48:05 +02:00
}
2020-02-29 23:01:03 +01:00
urlOptions = {
grant _type : 'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
assertion : token
} ;
loggedUrlOptions = {
grant _type : 'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
assertion : tokenData
} ;
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
if ( ! this . options . refreshToken ) {
2026-04-28 12:50:45 +02:00
const err = new Error ( "Can't create new access token for user" ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2020-02-29 23:01:03 +01:00
}
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
urlOptions = {
client _id : this . options . clientId || '' ,
client _secret : this . options . clientSecret || '' ,
refresh _token : this . options . refreshToken ,
grant _type : 'refresh_token'
} ;
loggedUrlOptions = {
client _id : this . options . clientId || '' ,
client _secret : ( this . options . clientSecret || '' ) . substr ( 0 , 6 ) + '...' ,
refresh _token : ( this . options . refreshToken || '' ) . substr ( 0 , 6 ) + '...' ,
grant _type : 'refresh_token'
} ;
}
2026-04-28 12:50:45 +02:00
Object . assign ( urlOptions , this . options . customParams ) ;
Object . assign ( loggedUrlOptions , this . options . customParams ) ;
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'generate'
} , 'Requesting token using: %s' , JSON . stringify ( loggedUrlOptions ) ) ;
2020-02-29 23:01:03 +01:00
this . postRequest ( this . options . accessUrl , urlOptions , this . options , ( error , body ) => {
let data ;
if ( error ) {
return callback ( error ) ;
}
try {
data = JSON . parse ( body . toString ( ) ) ;
2026-09-15 13:00:32 +02:00
}
catch ( E ) {
2020-02-29 23:01:03 +01:00
return callback ( E ) ;
}
if ( ! data || typeof data !== 'object' ) {
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'post'
} , 'Response: %s' , ( body || '' ) . toString ( ) ) ;
2026-04-28 12:50:45 +02:00
const err = new Error ( 'Invalid authentication response' ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
const logData = Object . assign ( { } , data ) ;
if ( logData . access _token ) {
logData . access _token = ( logData . access _token || '' ) . toString ( ) . substr ( 0 , 6 ) + '...' ;
}
2026-09-15 13:00:32 +02:00
this . logger . debug ( {
tnx : 'OAUTH2' ,
user : this . options . user ,
action : 'post'
} , 'Response: %s' , JSON . stringify ( logData ) ) ;
2020-02-29 23:01:03 +01:00
if ( data . error ) {
2020-11-12 16:30:46 +01:00
// Error Response : https://tools.ietf.org/html/rfc6749#section-5.2
let errorMessage = data . error ;
2020-11-12 16:31:20 +01:00
if ( data . error _description ) {
errorMessage += ': ' + data . error _description ;
2020-11-12 16:30:46 +01:00
}
2020-11-12 16:31:20 +01:00
if ( data . error _uri ) {
errorMessage += ' (' + data . error _uri + ')' ;
2020-11-12 16:30:46 +01:00
}
2026-04-28 12:50:45 +02:00
const err = new Error ( errorMessage ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2020-02-29 23:01:03 +01:00
}
if ( data . access _token ) {
this . updateToken ( data . access _token , data . expires _in ) ;
return callback ( null , this . accessToken ) ;
}
2026-04-28 12:50:45 +02:00
const err = new Error ( 'No access token' ) ;
2026-02-05 08:49:11 +01:00
err . code = errors . EOAUTH2 ;
return callback ( err ) ;
2020-02-29 23:01:03 +01:00
} ) ;
}
/**
* Converts an access_token and user id into a base64 encoded XOAuth2 token
*
2026-09-15 13:00:32 +02:00
* @param [accessToken] Access token string
* @return Base64 encoded token for IMAP or SMTP login
2020-02-29 23:01:03 +01:00
*/
buildXOAuth2Token ( accessToken ) {
2026-04-28 12:50:45 +02:00
const authData = [ 'user=' + ( this . options . user || '' ) , 'auth=Bearer ' + ( accessToken || this . accessToken ) , '' , '' ] ;
2020-02-29 23:01:03 +01:00
return Buffer . from ( authData . join ( '\x01' ) , 'utf-8' ) . toString ( 'base64' ) ;
}
/**
* Custom POST request handler.
2026-09-15 13:00:32 +02:00
* This is only needed to keep paths short in Windows, usually this module
2020-02-29 23:01:03 +01:00
* is a dependency of a dependency and if it tries to require something
* like the request module the paths get way too long to handle for Windows.
* As we do only a simple POST request we do not actually require complicated
* logic support (no redirects, no nothing) anyway.
*
2026-09-15 13:00:32 +02:00
* @param url Url to POST to
* @param payload Payload to POST
* @param params Client options, the customHeaders and tls values are used for the request
* @param callback Callback function with (err, buff)
2020-02-29 23:01:03 +01:00
*/
postRequest ( url , payload , params , callback ) {
let returned = false ;
2026-04-28 12:50:45 +02:00
const chunks = [ ] ;
2020-02-29 23:01:03 +01:00
let chunklen = 0 ;
2026-06-15 07:32:52 +02:00
const fetchOptions = {
2020-02-29 23:01:03 +01:00
method : 'post' ,
headers : params . customHeaders ,
body : payload ,
allowErrorResponse : true
2026-06-15 07:32:52 +02:00
} ;
2026-09-15 13:00:32 +02:00
// OAuth2 token endpoints are credential-bearing. src/fetch already
2026-06-15 07:32:52 +02:00
// validates certs by default; pin rejectUnauthorized:true here so the
// token fetch stays strict, while still layering params.tls (the
// user's options.tls) on top so callers with a self-hosted provider on
// a private CA can override.
if ( /^https:/i . test ( url ) ) {
fetchOptions . tls = Object . assign ( { rejectUnauthorized : true } , params . tls || { } ) ;
}
2026-09-15 13:00:32 +02:00
const req = ( 0 , index _js _1 . default ) ( url , fetchOptions ) ;
2020-02-29 23:01:03 +01:00
req . on ( 'readable' , ( ) => {
let chunk ;
while ( ( chunk = req . read ( ) ) !== null ) {
chunks . push ( chunk ) ;
chunklen += chunk . length ;
}
} ) ;
req . once ( 'error' , err => {
if ( returned ) {
return ;
}
returned = true ;
return callback ( err ) ;
} ) ;
req . once ( 'end' , ( ) => {
if ( returned ) {
return ;
}
returned = true ;
return callback ( null , Buffer . concat ( chunks , chunklen ) ) ;
} ) ;
}
/**
* Encodes a buffer or a string into Base64url format
*
2026-09-15 13:00:32 +02:00
* @param data The data to convert
* @return The encoded string
2020-02-29 23:01:03 +01:00
*/
toBase64URL ( data ) {
if ( typeof data === 'string' ) {
data = Buffer . from ( data ) ;
}
return data
. toString ( 'base64' )
. replace ( /[=]+/g , '' ) // remove '='s
. replace ( /\+/g , '-' ) // '+' → '-'
. replace ( /\//g , '_' ) ; // '/' → '_'
}
/**
* Creates a JSON Web Token signed with RS256 (SHA256 + RSA)
*
2026-09-15 13:00:32 +02:00
* @param payload The payload to include in the generated token
* @return The generated and signed token
2020-02-29 23:01:03 +01:00
*/
jwtSignRS256 ( payload ) {
2026-09-15 13:00:32 +02:00
const signedPayload = [ '{"alg":"RS256","typ":"JWT"}' , JSON . stringify ( payload ) ] . map ( val => this . toBase64URL ( val ) ) . join ( '.' ) ;
const signature = node _crypto _1 . default
. createSign ( 'RSA-SHA256' )
. update ( signedPayload )
. sign ( this . options . privateKey ) ;
return signedPayload + '.' + this . toBase64URL ( signature ) ;
2020-02-29 23:01:03 +01:00
}
}
2026-09-15 13:00:32 +02:00
exports . default = XOAuth2 ;
module . exports = exports . default ;
Object . defineProperty ( module . exports , 'default' , { value : exports . default , enumerable : false , writable : true , configurable : true } ) ;