emailjs-smtp-client
Version:
SMTP Client allows you to connect to an SMTP server in JS.
889 lines (774 loc) • 30.6 kB
JavaScript
/* eslint-disable camelcase */
import { encode } from 'emailjs-base64'
import TCPSocket from 'emailjs-tcp-socket'
import { TextDecoder, TextEncoder } from 'text-encoding'
import SmtpClientResponseParser from './parser'
import createDefaultLogger from './logger'
import {
LOG_LEVEL_ERROR,
LOG_LEVEL_WARN,
LOG_LEVEL_INFO,
LOG_LEVEL_DEBUG
} from './common'
var DEBUG_TAG = 'SMTP Client'
/**
* Lower Bound for socket timeout to wait since the last data was written to a socket
*/
const TIMEOUT_SOCKET_LOWER_BOUND = 10000
/**
* Multiplier for socket timeout:
*
* We assume at least a GPRS connection with 115 kb/s = 14,375 kB/s tops, so 10 KB/s to be on
* the safe side. We can timeout after a lower bound of 10s + (n KB / 10 KB/s). A 1 MB message
* upload would be 110 seconds to wait for the timeout. 10 KB/s === 0.1 s/B
*/
const TIMEOUT_SOCKET_MULTIPLIER = 0.1
class SmtpClient {
/**
* Creates a connection object to a SMTP server and allows to send mail through it.
* Call `connect` method to inititate the actual connection, the constructor only
* defines the properties but does not actually connect.
*
* NB! The parameter order (host, port) differs from node.js "way" (port, host)
*
* @constructor
*
* @param {String} [host="localhost"] Hostname to conenct to
* @param {Number} [port=25] Port number to connect to
* @param {Object} [options] Optional options object
* @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection
* @param {String} [options.name] Client hostname for introducing itself to the server
* @param {Object} [options.auth] Authentication options. Depends on the preferred authentication method. Usually {user, pass}
* @param {String} [options.authMethod] Force specific authentication method
* @param {Boolean} [options.disableEscaping] If set to true, do not escape dots on the beginning of the lines
*/
constructor (host, port, options = {}) {
this.options = options
this.timeoutSocketLowerBound = TIMEOUT_SOCKET_LOWER_BOUND
this.timeoutSocketMultiplier = TIMEOUT_SOCKET_MULTIPLIER
this.port = port || (this.options.useSecureTransport ? 465 : 25)
this.host = host || 'localhost'
/**
* If set to true, start an encrypted connection instead of the plaintext one
* (recommended if applicable). If useSecureTransport is not set but the port used is 465,
* then ecryption is used by default.
*/
this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 465
this.options.auth = this.options.auth || false // Authentication object. If not set, authentication step will be skipped.
this.options.name = this.options.name || 'localhost' // Hostname of the client, this will be used for introducing to the server
this.socket = false // Downstream TCP socket to the SMTP server, created with mozTCPSocket
this.destroyed = false // Indicates if the connection has been closed and can't be used anymore
this.waitDrain = false // Keeps track if the downstream socket is currently full and a drain event should be waited for or not
// Private properties
this._parser = new SmtpClientResponseParser() // SMTP response parser object. All data coming from the downstream server is feeded to this parser
this._authenticatedAs = null // If authenticated successfully, stores the username
this._supportedAuth = [] // A list of authentication mechanisms detected from the EHLO response and which are compatible with this library
this._dataMode = false // If true, accepts data from the upstream to be passed directly to the downstream socket. Used after the DATA command
this._lastDataBytes = '' // Keep track of the last bytes to see how the terminating dot should be placed
this._envelope = null // Envelope object for tracking who is sending mail to whom
this._currentAction = null // Stores the function that should be run after a response has been received from the server
this._secureMode = !!this.options.useSecureTransport // Indicates if the connection is secured or plaintext
this._socketTimeoutTimer = false // Timer waiting to declare the socket dead starting from the last write
this._socketTimeoutStart = false // Start time of sending the first packet in data mode
this._socketTimeoutPeriod = false // Timeout for sending in data mode, gets extended with every send()
// Activate logging
this.createLogger()
// Event placeholders
this.onerror = (e) => { } // Will be run when an error occurs. The `onclose` event will fire subsequently.
this.ondrain = () => { } // More data can be buffered in the socket.
this.onclose = () => { } // The connection to the server has been closed
this.onidle = () => { } // The connection is established and idle, you can send mail now
this.onready = (failedRecipients) => { } // Waiting for mail body, lists addresses that were not accepted as recipients
this.ondone = (success) => { } // The mail has been sent. Wait for `onidle` next. Indicates if the message was queued by the server.
}
/**
* Initiate a connection to the server
*/
connect (SocketContructor = TCPSocket) {
this.socket = SocketContructor.open(this.host, this.port, {
binaryType: 'arraybuffer',
useSecureTransport: this._secureMode,
ca: this.options.ca,
tlsWorkerPath: this.options.tlsWorkerPath,
ws: this.options.ws
})
// allows certificate handling for platform w/o native tls support
// oncert is non standard so setting it might throw if the socket object is immutable
try {
this.socket.oncert = this.oncert
} catch (E) { }
this.socket.onerror = this._onError.bind(this)
this.socket.onopen = this._onOpen.bind(this)
}
/**
* Pauses `data` events from the downstream SMTP server
*/
suspend () {
if (this.socket && this.socket.readyState === 'open') {
this.socket.suspend()
}
}
/**
* Resumes `data` events from the downstream SMTP server. Be careful of not
* resuming something that is not suspended - an error is thrown in this case
*/
resume () {
if (this.socket && this.socket.readyState === 'open') {
this.socket.resume()
}
}
/**
* Sends QUIT
*/
quit () {
this.logger.debug(DEBUG_TAG, 'Sending QUIT...')
this._sendCommand('QUIT')
this._currentAction = this.close
}
/**
* Reset authentication
*
* @param {Object} [auth] Use this if you want to authenticate as another user
*/
reset (auth) {
this.options.auth = auth || this.options.auth
this.logger.debug(DEBUG_TAG, 'Sending RSET...')
this._sendCommand('RSET')
this._currentAction = this._actionRSET
}
/**
* Closes the connection to the server
*/
close () {
this.logger.debug(DEBUG_TAG, 'Closing connection...')
if (this.socket && this.socket.readyState === 'open') {
this.socket.close()
} else {
this._destroy()
}
}
// Mail related methods
/**
* Initiates a new message by submitting envelope data, starting with
* `MAIL FROM:` command. Use after `onidle` event
*
* @param {Object} envelope Envelope object in the form of {from:"...", to:["..."]}
*/
useEnvelope (envelope) {
this._envelope = envelope || {}
this._envelope.from = [].concat(this._envelope.from || ('anonymous@' + this.options.name))[0]
this._envelope.to = [].concat(this._envelope.to || [])
// clone the recipients array for latter manipulation
this._envelope.rcptQueue = [].concat(this._envelope.to)
this._envelope.rcptFailed = []
this._envelope.responseQueue = []
this._currentAction = this._actionMAIL
this.logger.debug(DEBUG_TAG, 'Sending MAIL FROM...')
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>')
}
/**
* Send ASCII data to the server. Works only in data mode (after `onready` event), ignored
* otherwise
*
* @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
* @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
*/
send (chunk) {
// works only in data mode
if (!this._dataMode) {
// this line should never be reached but if it does,
// act like everything's normal.
return true
}
// TODO: if the chunk is an arraybuffer, use a separate function to send the data
return this._sendString(chunk)
}
/**
* Indicates that a data stream for the socket is ended. Works only in data
* mode (after `onready` event), ignored otherwise. Use it when you are done
* with sending the mail. This method does not close the socket. Once the mail
* has been queued by the server, `ondone` and `onidle` are emitted.
*
* @param {Buffer} [chunk] Chunk of data to be sent to the server
*/
end (chunk) {
// works only in data mode
if (!this._dataMode) {
// this line should never be reached but if it does,
// act like everything's normal.
return true
}
if (chunk && chunk.length) {
this.send(chunk)
}
// redirect output from the server to _actionStream
this._currentAction = this._actionStream
// indicate that the stream has ended by sending a single dot on its own line
// if the client already closed the data with \r\n no need to do it again
if (this._lastDataBytes === '\r\n') {
this.waitDrain = this._send(new Uint8Array([0x2E, 0x0D, 0x0A]).buffer) // .\r\n
} else if (this._lastDataBytes.substr(-1) === '\r') {
this.waitDrain = this._send(new Uint8Array([0x0A, 0x2E, 0x0D, 0x0A]).buffer) // \n.\r\n
} else {
this.waitDrain = this._send(new Uint8Array([0x0D, 0x0A, 0x2E, 0x0D, 0x0A]).buffer) // \r\n.\r\n
}
// end data mode, reset the variables for extending the timeout in data mode
this._dataMode = false
this._socketTimeoutStart = false
this._socketTimeoutPeriod = false
return this.waitDrain
}
// PRIVATE METHODS
// EVENT HANDLERS FOR THE SOCKET
/**
* Connection listener that is run when the connection to the server is opened.
* Sets up different event handlers for the opened socket
*
* @event
* @param {Event} evt Event object. Not used
*/
_onOpen (event) {
if (event && event.data && event.data.proxyHostname) {
this.options.name = event.data.proxyHostname
}
this.socket.ondata = this._onData.bind(this)
this.socket.onclose = this._onClose.bind(this)
this.socket.ondrain = this._onDrain.bind(this)
this._parser.ondata = this._onCommand.bind(this)
this._currentAction = this._actionGreeting
}
/**
* Data listener for chunks of data emitted by the server
*
* @event
* @param {Event} evt Event object. See `evt.data` for the chunk received
*/
_onData (evt) {
clearTimeout(this._socketTimeoutTimer)
var stringPayload = new TextDecoder('UTF-8').decode(new Uint8Array(evt.data))
this.logger.debug(DEBUG_TAG, 'SERVER: ' + stringPayload)
this._parser.send(stringPayload)
}
/**
* More data can be buffered in the socket, `waitDrain` is reset to false
*
* @event
* @param {Event} evt Event object. Not used
*/
_onDrain () {
this.waitDrain = false
this.ondrain()
}
/**
* Error handler for the socket
*
* @event
* @param {Event} evt Event object. See evt.data for the error
*/
_onError (evt) {
if (evt instanceof Error && evt.message) {
this.logger.error(DEBUG_TAG, evt)
this.onerror(evt)
} else if (evt && evt.data instanceof Error) {
this.logger.error(DEBUG_TAG, evt.data)
this.onerror(evt.data)
} else {
this.logger.error(DEBUG_TAG, new Error((evt && evt.data && evt.data.message) || evt.data || evt || 'Error'))
this.onerror(new Error((evt && evt.data && evt.data.message) || evt.data || evt || 'Error'))
}
this.close()
}
/**
* Indicates that the socket has been closed
*
* @event
* @param {Event} evt Event object. Not used
*/
_onClose () {
this.logger.debug(DEBUG_TAG, 'Socket closed.')
this._destroy()
}
/**
* This is not a socket data handler but the handler for data emitted by the parser,
* so this data is safe to use as it is always complete (server might send partial chunks)
*
* @event
* @param {Object} command Parsed data
*/
_onCommand (command) {
if (typeof this._currentAction === 'function') {
this._currentAction(command)
}
}
_onTimeout () {
// inform about the timeout and shut down
var error = new Error('Socket timed out!')
this._onError(error)
}
/**
* Ensures that the connection is closed and such
*/
_destroy () {
clearTimeout(this._socketTimeoutTimer)
if (!this.destroyed) {
this.destroyed = true
this.onclose()
}
}
/**
* Sends a string to the socket.
*
* @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
* @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
*/
_sendString (chunk) {
// escape dots
if (!this.options.disableEscaping) {
chunk = chunk.replace(/\n\./g, '\n..')
if ((this._lastDataBytes.substr(-1) === '\n' || !this._lastDataBytes) && chunk.charAt(0) === '.') {
chunk = '.' + chunk
}
}
// Keeping eye on the last bytes sent, to see if there is a <CR><LF> sequence
// at the end which is needed to end the data stream
if (chunk.length > 2) {
this._lastDataBytes = chunk.substr(-2)
} else if (chunk.length === 1) {
this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk
}
this.logger.debug(DEBUG_TAG, 'Sending ' + chunk.length + ' bytes of payload')
// pass the chunk to the socket
this.waitDrain = this._send(new TextEncoder('UTF-8').encode(chunk).buffer)
return this.waitDrain
}
/**
* Send a string command to the server, also append \r\n if needed
*
* @param {String} str String to be sent to the server
*/
_sendCommand (str) {
this.waitDrain = this._send(new TextEncoder('UTF-8').encode(str + (str.substr(-2) !== '\r\n' ? '\r\n' : '')).buffer)
}
_send (buffer) {
this._setTimeout(buffer.byteLength)
return this.socket.send(buffer)
}
_setTimeout (byteLength) {
var prolongPeriod = Math.floor(byteLength * this.timeoutSocketMultiplier)
var timeout
if (this._dataMode) {
// we're in data mode, so we count only one timeout that get extended for every send().
var now = Date.now()
// the old timeout start time
this._socketTimeoutStart = this._socketTimeoutStart || now
// the old timeout period, normalized to a minimum of TIMEOUT_SOCKET_LOWER_BOUND
this._socketTimeoutPeriod = (this._socketTimeoutPeriod || this.timeoutSocketLowerBound) + prolongPeriod
// the new timeout is the delta between the new firing time (= timeout period + timeout start time) and now
timeout = this._socketTimeoutStart + this._socketTimeoutPeriod - now
} else {
// set new timout
timeout = this.timeoutSocketLowerBound + prolongPeriod
}
clearTimeout(this._socketTimeoutTimer) // clear pending timeouts
this._socketTimeoutTimer = setTimeout(this._onTimeout.bind(this), timeout) // arm the next timeout
}
/**
* Intitiate authentication sequence if needed
*/
_authenticateUser () {
if (!this.options.auth) {
// no need to authenticate, at least no data given
this._currentAction = this._actionIdle
this.onidle() // ready to take orders
return
}
var auth
if (!this.options.authMethod && this.options.auth.xoauth2) {
this.options.authMethod = 'XOAUTH2'
}
if (this.options.authMethod) {
auth = this.options.authMethod.toUpperCase().trim()
} else {
// use first supported
auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim()
}
switch (auth) {
case 'LOGIN':
// LOGIN is a 3 step authentication process
// C: AUTH LOGIN
// C: BASE64(USER)
// C: BASE64(PASS)
this.logger.debug(DEBUG_TAG, 'Authentication via AUTH LOGIN')
this._currentAction = this._actionAUTH_LOGIN_USER
this._sendCommand('AUTH LOGIN')
return
case 'PLAIN':
// AUTH PLAIN is a 1 step authentication process
// C: AUTH PLAIN BASE64(\0 USER \0 PASS)
this.logger.debug(DEBUG_TAG, 'Authentication via AUTH PLAIN')
this._currentAction = this._actionAUTHComplete
this._sendCommand(
// convert to BASE64
'AUTH PLAIN ' +
encode(
// this.options.auth.user+'\u0000'+
'\u0000' + // skip authorization identity as it causes problems with some servers
this.options.auth.user + '\u0000' +
this.options.auth.pass)
)
return
case 'XOAUTH2':
// See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange
this.logger.debug(DEBUG_TAG, 'Authentication via AUTH XOAUTH2')
this._currentAction = this._actionAUTH_XOAUTH2
this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this.options.auth.user, this.options.auth.xoauth2))
return
}
this._onError(new Error('Unknown authentication method ' + auth))
}
// ACTIONS FOR RESPONSES FROM THE SMTP SERVER
/**
* Initial response from the server, must have a status 220
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionGreeting (command) {
if (command.statusCode !== 220) {
this._onError(new Error('Invalid greeting: ' + command.data))
return
}
if (this.options.lmtp) {
this.logger.debug(DEBUG_TAG, 'Sending LHLO ' + this.options.name)
this._currentAction = this._actionLHLO
this._sendCommand('LHLO ' + this.options.name)
} else {
this.logger.debug(DEBUG_TAG, 'Sending EHLO ' + this.options.name)
this._currentAction = this._actionEHLO
this._sendCommand('EHLO ' + this.options.name)
}
}
/**
* Response to LHLO
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionLHLO (command) {
if (!command.success) {
this.logger.error(DEBUG_TAG, 'LHLO not successful')
this._onError(new Error(command.data))
return
}
// Process as EHLO response
this._actionEHLO(command)
}
/**
* Response to EHLO. If the response is an error, try HELO instead
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionEHLO (command) {
var match
if (!command.success) {
if (!this._secureMode && this.options.requireTLS) {
var errMsg = 'STARTTLS not supported without EHLO'
this.logger.error(DEBUG_TAG, errMsg)
this._onError(new Error(errMsg))
return
}
// Try HELO instead
this.logger.warn(DEBUG_TAG, 'EHLO not successful, trying HELO ' + this.options.name)
this._currentAction = this._actionHELO
this._sendCommand('HELO ' + this.options.name)
return
}
// Detect if the server supports PLAIN auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)PLAIN/i)) {
this.logger.debug(DEBUG_TAG, 'Server supports AUTH PLAIN')
this._supportedAuth.push('PLAIN')
}
// Detect if the server supports LOGIN auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)LOGIN/i)) {
this.logger.debug(DEBUG_TAG, 'Server supports AUTH LOGIN')
this._supportedAuth.push('LOGIN')
}
// Detect if the server supports XOAUTH2 auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)XOAUTH2/i)) {
this.logger.debug(DEBUG_TAG, 'Server supports AUTH XOAUTH2')
this._supportedAuth.push('XOAUTH2')
}
// Detect maximum allowed message size
if ((match = command.line.match(/SIZE (\d+)/i)) && Number(match[1])) {
const maxAllowedSize = Number(match[1])
this.logger.debug(DEBUG_TAG, 'Maximum allowd message size: ' + maxAllowedSize)
}
// Detect if the server supports STARTTLS
if (!this._secureMode) {
if ((command.line.match(/[ -]STARTTLS\s?$/mi) && !this.options.ignoreTLS) || !!this.options.requireTLS) {
this._currentAction = this._actionSTARTTLS
this.logger.debug(DEBUG_TAG, 'Sending STARTTLS')
this._sendCommand('STARTTLS')
return
}
}
this._authenticateUser()
}
/**
* Handles server response for STARTTLS command. If there's an error
* try HELO instead, otherwise initiate TLS upgrade. If the upgrade
* succeedes restart the EHLO
*
* @param {String} str Message from the server
*/
_actionSTARTTLS (command) {
if (!command.success) {
this.logger.error(DEBUG_TAG, 'STARTTLS not successful')
this._onError(new Error(command.data))
return
}
this._secureMode = true
this.socket.upgradeToSecure()
// restart protocol flow
this._currentAction = this._actionEHLO
this._sendCommand('EHLO ' + this.options.name)
}
/**
* Response to HELO
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionHELO (command) {
if (!command.success) {
this.logger.error(DEBUG_TAG, 'HELO not successful')
this._onError(new Error(command.data))
return
}
this._authenticateUser()
}
/**
* Response to AUTH LOGIN, if successful expects base64 encoded username
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionAUTH_LOGIN_USER (command) {
if (command.statusCode !== 334 || command.data !== 'VXNlcm5hbWU6') {
this.logger.error(DEBUG_TAG, 'AUTH LOGIN USER not successful: ' + command.data)
this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6 ": ' + command.data))
return
}
this.logger.debug(DEBUG_TAG, 'AUTH LOGIN USER successful')
this._currentAction = this._actionAUTH_LOGIN_PASS
this._sendCommand(encode(this.options.auth.user))
}
/**
* Response to AUTH LOGIN username, if successful expects base64 encoded password
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionAUTH_LOGIN_PASS (command) {
if (command.statusCode !== 334 || command.data !== 'UGFzc3dvcmQ6') {
this.logger.error(DEBUG_TAG, 'AUTH LOGIN PASS not successful: ' + command.data)
this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6 ": ' + command.data))
return
}
this.logger.debug(DEBUG_TAG, 'AUTH LOGIN PASS successful')
this._currentAction = this._actionAUTHComplete
this._sendCommand(encode(this.options.auth.pass))
}
/**
* Response to AUTH XOAUTH2 token, if error occurs send empty response
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionAUTH_XOAUTH2 (command) {
if (!command.success) {
this.logger.warn(DEBUG_TAG, 'Error during AUTH XOAUTH2, sending empty response')
this._sendCommand('')
this._currentAction = this._actionAUTHComplete
} else {
this._actionAUTHComplete(command)
}
}
/**
* Checks if authentication succeeded or not. If successfully authenticated
* emit `idle` to indicate that an e-mail can be sent using this connection
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionAUTHComplete (command) {
if (!command.success) {
this.logger.debug(DEBUG_TAG, 'Authentication failed: ' + command.data)
this._onError(new Error(command.data))
return
}
this.logger.debug(DEBUG_TAG, 'Authentication successful.')
this._authenticatedAs = this.options.auth.user
this._currentAction = this._actionIdle
this.onidle() // ready to take orders
}
/**
* Used when the connection is idle and the server emits timeout
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionIdle (command) {
if (command.statusCode > 300) {
this._onError(new Error(command.line))
return
}
this._onError(new Error(command.data))
}
/**
* Response to MAIL FROM command. Proceed to defining RCPT TO list if successful
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionMAIL (command) {
if (!command.success) {
this.logger.debug(DEBUG_TAG, 'MAIL FROM unsuccessful: ' + command.data)
this._onError(new Error(command.data))
return
}
if (!this._envelope.rcptQueue.length) {
this._onError(new Error('Can\'t send mail - no recipients defined'))
} else {
this.logger.debug(DEBUG_TAG, 'MAIL FROM successful, proceeding with ' + this._envelope.rcptQueue.length + ' recipients')
this.logger.debug(DEBUG_TAG, 'Adding recipient...')
this._envelope.curRecipient = this._envelope.rcptQueue.shift()
this._currentAction = this._actionRCPT
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>')
}
}
/**
* Response to a RCPT TO command. If the command is unsuccessful, try the next one,
* as this might be related only to the current recipient, not a global error, so
* the following recipients might still be valid
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionRCPT (command) {
if (!command.success) {
this.logger.warn(DEBUG_TAG, 'RCPT TO failed for: ' + this._envelope.curRecipient)
// this is a soft error
this._envelope.rcptFailed.push(this._envelope.curRecipient)
} else {
this._envelope.responseQueue.push(this._envelope.curRecipient)
}
if (!this._envelope.rcptQueue.length) {
if (this._envelope.rcptFailed.length < this._envelope.to.length) {
this._currentAction = this._actionDATA
this.logger.debug(DEBUG_TAG, 'RCPT TO done, proceeding with payload')
this._sendCommand('DATA')
} else {
this._onError(new Error('Can\'t send mail - all recipients were rejected'))
this._currentAction = this._actionIdle
}
} else {
this.logger.debug(DEBUG_TAG, 'Adding recipient...')
this._envelope.curRecipient = this._envelope.rcptQueue.shift()
this._currentAction = this._actionRCPT
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>')
}
}
/**
* Response to the RSET command. If successful, clear the current authentication
* information and reauthenticate.
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionRSET (command) {
if (!command.success) {
this.logger.error(DEBUG_TAG, 'RSET unsuccessful ' + command.data)
this._onError(new Error(command.data))
return
}
this._authenticatedAs = null
this._authenticateUser()
}
/**
* Response to the DATA command. Server is now waiting for a message, so emit `onready`
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionDATA (command) {
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
// some servers might use 250 instead
if ([250, 354].indexOf(command.statusCode) < 0) {
this.logger.error(DEBUG_TAG, 'DATA unsuccessful ' + command.data)
this._onError(new Error(command.data))
return
}
this._dataMode = true
this._currentAction = this._actionIdle
this.onready(this._envelope.rcptFailed)
}
/**
* Response from the server, once the message stream has ended with <CR><LF>.<CR><LF>
* Emits `ondone`.
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
_actionStream (command) {
var rcpt
if (this.options.lmtp) {
// LMTP returns a response code for *every* successfully set recipient
// For every recipient the message might succeed or fail individually
rcpt = this._envelope.responseQueue.shift()
if (!command.success) {
this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' failed.')
this._envelope.rcptFailed.push(rcpt)
} else {
this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' succeeded.')
}
if (this._envelope.responseQueue.length) {
this._currentAction = this._actionStream
return
}
this._currentAction = this._actionIdle
this.ondone(true)
} else {
// For SMTP the message either fails or succeeds, there is no information
// about individual recipients
if (!command.success) {
this.logger.error(DEBUG_TAG, 'Message sending failed.')
} else {
this.logger.debug(DEBUG_TAG, 'Message sent successfully.')
}
this._currentAction = this._actionIdle
this.ondone(!!command.success)
}
// If the client wanted to do something else (eg. to quit), do not force idle
if (this._currentAction === this._actionIdle) {
// Waiting for new connections
this.logger.debug(DEBUG_TAG, 'Idling while waiting for new connections...')
this.onidle()
}
}
/**
* Builds a login token for XOAUTH2 authentication command
*
* @param {String} user E-mail address of the user
* @param {String} token Valid access token for the user
* @return {String} Base64 formatted login token
*/
_buildXOAuth2Token (user, token) {
var authData = [
'user=' + (user || ''),
'auth=Bearer ' + token,
'',
''
]
// base64("user={User}\x00auth=Bearer {Token}\x00\x00")
return encode(authData.join('\x01'))
}
createLogger (creator = createDefaultLogger) {
const logger = creator((this.options.auth || {}).user || '', this.host)
this.logLevel = this.LOG_LEVEL_ALL
this.logger = {
debug: (...msgs) => { if (LOG_LEVEL_DEBUG >= this.logLevel) { logger.debug(msgs) } },
info: (...msgs) => { if (LOG_LEVEL_INFO >= this.logLevel) { logger.info(msgs) } },
warn: (...msgs) => { if (LOG_LEVEL_WARN >= this.logLevel) { logger.warn(msgs) } },
error: (...msgs) => { if (LOG_LEVEL_ERROR >= this.logLevel) { logger.error(msgs) } }
}
}
}
export default SmtpClient