[cloud-api] Verify SMTP credentials in /auth endpoint

Summary:
This patch will prevent users from being able to connect accounts which sync
mail but fail to send.

This commit includes a couple pieces:
* Adds a call to nodemailer's `verify()` function in the /auth endpoint
* Adds Error object conversion for SMTP errors. Since we don't implement our
  own connection object or connection pool for SMTP, we simply wrap the couple
  places we call functions from nodemailer that connect to SMTP, namely
  SendmailClient's _send() and the new verify() call in /auth.
* Moves RetryableError to the 'errors' module since it's now a base class for
  retryable IMAP //and// SMTP errors.
* Moves the main `smtpConfig()` logic which used to live on the Account model
  into AuthHelpers so it can be shared between the Account model and the verify
  code.
* Converts a few different places to use `import` syntax instead of
  `require` syntax for module imports. Apologies for not splitting this out
  into a separate diff—would have been a fair amount of work and looks not too
  difficult to skim over in the context of the rest of the patch.
* Fixing a bug in a previous commit where erroring sends would crash because of
  using `this._transporter.options` instead of `this._transporter.transporter.options`

Test Plan: manual

Reviewers: evan, halla, juan

Reviewed By: halla, juan

Differential Revision: https://phab.nylas.com/D4200
This commit is contained in:
Christine Spang 2017-03-10 12:21:04 -08:00
parent 76234eaa9b
commit a57e4bdd20
13 changed files with 371 additions and 292 deletions

View file

@ -242,10 +242,12 @@ export default class SendDraftTask extends BaseDraftTask {
const errorMessage = (err.body && err.body.message) || ''
message = `Sorry, this message could not be sent, please try again.`;
message += `\n\nReason: ${err.message}`
if (errorMessage.includes('Network Error')) {
if (errorMessage.includes('unable to reach your SMTP server')) {
message = `Sorry, this message could not be sent. There was a network error, please make sure you are online.`
}
if (errorMessage.includes('Invalid login')) {
if (errorMessage.includes('Incorrect SMTP username or password') ||
errorMessage.includes('SMTP protocol error') ||
errorMessage.includes('unable to look up your SMTP host')) {
Actions.updateAccount(this.draft.accountId, {syncState: Account.SYNC_STATE_AUTH_FAILED})
message = `Sorry, this message could not be sent due to an authentication error. Please re-authenticate your account and try again.`
}

View file

@ -1,5 +1,6 @@
import _ from 'underscore'
import {
Errors,
IMAPErrors,
SendmailClient,
MetricsReporter,
@ -352,7 +353,7 @@ class SyncWorker {
// Check if we've encountered a retryable/network error.
// If so, we don't want to save the error to the account, which will cause
// a red box to show up.
if (error instanceof IMAPErrors.RetryableError) {
if (error instanceof Errors.RetryableError) {
this._retryScheduler.nextDelay()
return
}
@ -421,7 +422,7 @@ class SyncWorker {
const moreToSync = folders.some((f) => !f.isSyncComplete())
if (error != null) {
if (error instanceof IMAPErrors.RetryableError) {
if (error instanceof Errors.RetryableError) {
interval = this._retryScheduler.currentDelay();
} else {
interval = AC_SYNC_LOOP_INTERVAL_MS;

View file

@ -1,5 +1,5 @@
import {Actions} from 'nylas-exports'
import {IMAPErrors} from 'isomorphic-core'
import {Errors} from 'isomorphic-core'
import SyncbackTask from './syncback-tasks/syncback-task'
import SyncbackTaskFactory from './syncback-task-factory';
@ -184,7 +184,7 @@ class SyncbackTaskRunner {
} catch (error) {
const after = new Date();
if (error instanceof IMAPErrors.RetryableError) {
if (error instanceof Errors.RetryableError) {
Actions.recordUserEvent('Retrying syncback task', {
accountId: this._account.id,
provider: this._account.provider,

View file

@ -7,11 +7,12 @@ module.exports = {
Imap: require('imap'),
Errors: require('./src/errors'),
IMAPErrors: require('./src/imap-errors'),
SMTPErrors: require('./src/smtp-errors'),
loadModels: require('./src/load-models'),
AuthHelpers: require('./src/auth-helpers'),
PromiseUtils: require('./src/promise-utils'),
DatabaseTypes: require('./src/database-types'),
IMAPConnection: require('./src/imap-connection'),
IMAPConnection: require('./src/imap-connection').default,
IMAPConnectionPool: require('./src/imap-connection-pool'),
SendmailClient: require('./src/sendmail-client'),
DeltaStreamBuilder: require('./src/delta-stream-builder'),

View file

@ -0,0 +1,234 @@
import _ from 'underscore'
import Joi from 'joi'
import atob from 'atob';
import nodemailer from 'nodemailer';
import IMAPConnection from './imap-connection'
import {NylasError, RetryableError} from './errors'
import {convertSmtpError} from './smtp-errors'
const imapSmtpSettings = Joi.object().keys({
imap_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
imap_port: Joi.number().integer().required(),
imap_username: Joi.string().required(),
imap_password: Joi.string().required(),
smtp_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
smtp_port: Joi.number().integer().required(),
smtp_username: Joi.string().required(),
smtp_password: Joi.string().required(),
smtp_custom_config: Joi.object(),
ssl_required: Joi.boolean().required(),
}).required();
const resolvedGmailSettings = Joi.object().keys({
xoauth2: Joi.string().required(),
expiry_date: Joi.number().integer().required(),
}).required();
const office365Settings = Joi.object().keys({
name: Joi.string().required(),
type: Joi.string().valid('office365').required(),
email: Joi.string().required(),
password: Joi.string().required(),
username: Joi.string().required(),
}).required();
export const SUPPORTED_PROVIDERS = new Set(
['gmail', 'office365', 'imap', 'icloud', 'yahoo', 'fastmail']
);
export function credentialsForProvider({provider, settings, email}) {
if (provider === "gmail") {
const connectionSettings = {
imap_username: email,
imap_host: 'imap.gmail.com',
imap_port: 993,
smtp_username: email,
smtp_host: 'smtp.gmail.com',
smtp_port: 465,
ssl_required: true,
}
const connectionCredentials = {
xoauth2: settings.xoauth2,
expiry_date: settings.expiry_date,
}
return {connectionSettings, connectionCredentials}
} else if (provider === "office365") {
const connectionSettings = {
imap_host: 'outlook.office365.com',
imap_port: 993,
ssl_required: true,
smtp_custom_config: {
host: 'smtp.office365.com',
port: 587,
secure: false,
tls: {ciphers: 'SSLv3'},
},
}
const connectionCredentials = {
imap_username: email,
imap_password: settings.password || settings.imap_password,
smtp_username: email,
smtp_password: settings.password || settings.smtp_password,
}
return {connectionSettings, connectionCredentials}
} else if (SUPPORTED_PROVIDERS.has(provider)) {
const connectionSettings = _.pick(settings, [
'imap_host', 'imap_port',
'smtp_host', 'smtp_port',
'ssl_required', 'smtp_custom_config',
]);
const connectionCredentials = _.pick(settings, [
'imap_username', 'imap_password',
'smtp_username', 'smtp_password',
]);
return {connectionSettings, connectionCredentials}
}
throw new Error(`Invalid provider: ${provider}`)
}
function bearerToken(xoauth2) {
// We have to unpack the access token from the entire XOAuth2
// token because it is re-packed during the SMTP connection login.
// https://github.com/nodemailer/smtp-connection/blob/master/lib/smtp-connection.js#L1418
const bearer = "Bearer ";
const decoded = atob(xoauth2);
const tokenIndex = decoded.indexOf(bearer) + bearer.length;
return decoded.substring(tokenIndex, decoded.length - 2);
}
export function smtpConfigFromSettings(provider, connectionSettings, connectionCredentials) {
let config;
const {smtp_host, smtp_port, ssl_required} = connectionSettings;
if (connectionSettings.smtp_custom_config) {
config = connectionSettings.smtp_custom_config;
} else {
config = {
host: smtp_host,
port: smtp_port,
secure: ssl_required,
};
}
if (provider === 'gmail') {
const {xoauth2} = connectionCredentials;
if (!xoauth2) {
throw new Error("Missing XOAuth2 Token")
}
const token = bearerToken(xoauth2);
config.auth = { user: connectionSettings.smtp_username, xoauth2: token }
} else if (SUPPORTED_PROVIDERS.has(provider)) {
const {smtp_username, smtp_password} = connectionCredentials
config.auth = { user: smtp_username, pass: smtp_password}
} else {
throw new Error(`${provider} not yet supported`)
}
return config;
}
export function imapAuthRouteConfig() {
return {
description: 'Authenticates a new account.',
tags: ['accounts'],
auth: false,
validate: {
payload: {
email: Joi.string().email().required(),
name: Joi.string().required(),
provider: Joi.string().valid(...SUPPORTED_PROVIDERS).required(),
settings: Joi.alternatives().try(imapSmtpSettings, office365Settings, resolvedGmailSettings),
},
},
}
}
export function imapAuthHandler(upsertAccount) {
const MAX_RETRIES = 2
const authHandler = (request, reply, retryNum = 0) => {
const dbStub = {};
const {email, provider, name} = request.payload;
const connectionChecks = [];
const {connectionSettings, connectionCredentials} = credentialsForProvider(request.payload)
// All IMAP accounts require a valid SMTP server for sending, and we never
// want to allow folks to connect accounts and find out later that they
// entered the wrong SMTP credentials. So verify here also!
const smtpConfig = smtpConfigFromSettings(provider, connectionSettings, connectionCredentials);
const smtpTransport = nodemailer.createTransport(Object.assign({
connectionTimeout: 30000,
}, smtpConfig));
const smtpVerifyPromise = smtpTransport.verify().catch((error) => {
throw convertSmtpError(error);
})
connectionChecks.push(smtpVerifyPromise);
connectionChecks.push(IMAPConnection.connect({
settings: Object.assign({}, connectionSettings, connectionCredentials),
logger: request.logger,
db: dbStub,
}));
Promise.all(connectionChecks).then((results) => {
for (const result of results) {
// close any IMAP connections we opened
if (result && result.end) { result.end(); }
}
const accountParams = {
name: name,
provider: provider,
emailAddress: email,
connectionSettings: connectionSettings,
}
return upsertAccount(accountParams, connectionCredentials)
})
.then(({account, token}) => {
const response = account.toJSON();
response.account_token = token.value;
reply(JSON.stringify(response));
return
})
.catch((err) => {
const logger = request.logger.child({
account_name: name,
account_provider: provider,
account_email: email,
connection_settings: connectionSettings,
error_name: err.name,
error_message: err.message,
error_tb: err.stack,
})
if (err instanceof RetryableError) {
if (retryNum < MAX_RETRIES) {
setTimeout(() => {
request.logger.info(`${err.name}. Retry #${retryNum + 1}`)
authHandler(request, reply, retryNum + 1)
}, 100)
return
}
logger.error('Encountered retryable error while attempting to authenticate')
reply({message: err.userMessage, type: "api_error"}).code(err.statusCode);
return
}
logger.error("Error trying to authenticate")
let userMessage = "Please contact support@nylas.com. An unforeseen error has occurred.";
let statusCode = 500;
if (err instanceof NylasError) {
if (err.userMessage) {
userMessage = err.userMessage;
}
if (err.statusCode) {
statusCode = err.statusCode;
}
}
reply({message: userMessage, type: "api_error"}).code(statusCode);
return;
})
}
return authHandler
}

View file

@ -1,184 +0,0 @@
const _ = require('underscore')
const Joi = require('joi');
const IMAPErrors = require('./imap-errors')
const IMAPConnection = require('./imap-connection')
const imapSmtpSettings = Joi.object().keys({
imap_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
imap_port: Joi.number().integer().required(),
imap_username: Joi.string().required(),
imap_password: Joi.string().required(),
smtp_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
smtp_port: Joi.number().integer().required(),
smtp_username: Joi.string().required(),
smtp_password: Joi.string().required(),
smtp_custom_config: Joi.object(),
ssl_required: Joi.boolean().required(),
}).required();
const resolvedGmailSettings = Joi.object().keys({
xoauth2: Joi.string().required(),
expiry_date: Joi.number().integer().required(),
}).required();
const office365Settings = Joi.object().keys({
name: Joi.string().required(),
type: Joi.string().valid('office365').required(),
email: Joi.string().required(),
password: Joi.string().required(),
username: Joi.string().required(),
}).required();
const USER_ERRORS = {
AUTH_500: "Please contact support@nylas.com. An unforeseen error has occurred.",
IMAP_AUTH: "Incorrect username or password",
IMAP_RETRY: "We were unable to reach your mail provider. Please try again.",
IMAP_CERT: "We couldn't make a secure connection to your mail provider. Please contact support@nylas.com.",
}
const SUPPORTED_PROVIDERS = new Set(
['gmail', 'office365', 'imap', 'icloud', 'yahoo', 'fastmail']
);
function credentialsForProvider({provider, settings, email}) {
if (provider === "gmail") {
const connectionSettings = {
imap_username: email,
imap_host: 'imap.gmail.com',
imap_port: 993,
smtp_username: email,
smtp_host: 'smtp.gmail.com',
smtp_port: 465,
ssl_required: true,
}
const connectionCredentials = {
xoauth2: settings.xoauth2,
expiry_date: settings.expiry_date,
}
return {connectionSettings, connectionCredentials}
} else if (provider === "office365") {
const connectionSettings = {
imap_host: 'outlook.office365.com',
imap_port: 993,
ssl_required: true,
smtp_custom_config: {
host: 'smtp.office365.com',
port: 587,
secure: false,
tls: {ciphers: 'SSLv3'},
},
}
const connectionCredentials = {
imap_username: email,
imap_password: settings.password || settings.imap_password,
smtp_username: email,
smtp_password: settings.password || settings.smtp_password,
}
return {connectionSettings, connectionCredentials}
} else if (SUPPORTED_PROVIDERS.has(provider)) {
const connectionSettings = _.pick(settings, [
'imap_host', 'imap_port',
'smtp_host', 'smtp_port',
'ssl_required', 'smtp_custom_config',
]);
const connectionCredentials = _.pick(settings, [
'imap_username', 'imap_password',
'smtp_username', 'smtp_password',
]);
return {connectionSettings, connectionCredentials}
}
throw new Error(`Invalid provider: ${provider}`)
}
module.exports = {
SUPPORTED_PROVIDERS,
credentialsForProvider,
imapAuthRouteConfig() {
return {
description: 'Authenticates a new account.',
tags: ['accounts'],
auth: false,
validate: {
payload: {
email: Joi.string().email().required(),
name: Joi.string().required(),
provider: Joi.string().valid(...SUPPORTED_PROVIDERS).required(),
settings: Joi.alternatives().try(imapSmtpSettings, office365Settings, resolvedGmailSettings),
},
},
}
},
imapAuthHandler(upsertAccount) {
const MAX_RETRIES = 2
const authHandler = (request, reply, retryNum = 0) => {
const dbStub = {};
const connectionChecks = [];
const {email, provider, name} = request.payload;
const {connectionSettings, connectionCredentials} = credentialsForProvider(request.payload)
connectionChecks.push(IMAPConnection.connect({
settings: Object.assign({}, connectionSettings, connectionCredentials),
logger: request.logger,
db: dbStub,
}));
Promise.all(connectionChecks).then((conns) => {
for (const conn of conns) {
if (conn) { conn.end(); }
}
const accountParams = {
name: name,
provider: provider,
emailAddress: email,
connectionSettings: connectionSettings,
}
return upsertAccount(accountParams, connectionCredentials)
})
.then(({account, token}) => {
const response = account.toJSON();
response.account_token = token.value;
reply(JSON.stringify(response));
return
})
.catch((err) => {
const logger = request.logger.child({
account_name: name,
account_provider: provider,
account_email: email,
connection_settings: connectionSettings,
error: err,
error_message: err.message,
})
if (err instanceof IMAPErrors.IMAPAuthenticationError) {
logger.error({err}, 'Encountered authentication error while attempting to authenticate')
reply({message: USER_ERRORS.IMAP_AUTH, type: "api_error"}).code(401);
return
}
if (err instanceof IMAPErrors.IMAPCertificateError) {
logger.error({err}, 'Encountered certificate error while attempting to authenticate')
reply({message: USER_ERRORS.IMAP_CERT, type: "api_error"}).code(401);
return
}
if (err instanceof IMAPErrors.RetryableError) {
if (retryNum < MAX_RETRIES) {
setTimeout(() => {
request.logger.info(`IMAP Timeout. Retry #${retryNum + 1}`)
authHandler(request, reply, retryNum + 1)
}, 100)
return
}
logger.error({err}, 'Encountered retryable error while attempting to authenticate')
reply({message: USER_ERRORS.IMAP_RETRY, type: "api_error"}).code(408);
return
}
logger.error({err}, 'Encountered unknown error while attempting to authenticate')
reply({message: USER_ERRORS.AUTH_500, type: "api_error"}).code(500);
return
})
}
return authHandler
},
}

View file

@ -1,5 +1,4 @@
class NylasError extends Error {
export class NylasError extends Error {
toJSON() {
let json = {}
if (super.toJSON) {
@ -13,7 +12,7 @@ class NylasError extends Error {
}
}
class APIError extends NylasError {
export class APIError extends NylasError {
constructor(message, statusCode, data) {
super(message);
this.statusCode = statusCode;
@ -21,7 +20,8 @@ class APIError extends NylasError {
}
}
module.exports = {
NylasError,
APIError,
}
/**
* An abstract base class that can be used to indicate Errors that may fix
* themselves when retried
*/
export class RetryableError extends NylasError { }

View file

@ -1,4 +1,4 @@
const IMAPConnection = require('./imap-connection');
const IMAPConnection = require('./imap-connection').default;
const IMAPErrors = require('./imap-errors');
const {inDevMode} = require('./env-helpers')

View file

@ -31,7 +31,7 @@ const ONE_HOUR_SECS = 60 * 60;
const AUTH_TIMEOUT_MS = 30 * 1000;
const DEFAULT_SOCKET_TIMEOUT_MS = 30 * 1000;
class IMAPConnection extends EventEmitter {
export default class IMAPConnection extends EventEmitter {
static DefaultSocketTimeout = DEFAULT_SOCKET_TIMEOUT_MS;
@ -393,4 +393,3 @@ class IMAPConnection extends EventEmitter {
}
IMAPConnection.Capabilities = Capabilities;
module.exports = IMAPConnection

View file

@ -1,28 +1,44 @@
const {NylasError} = require('./errors')
/**
* An abstract base class that can be used to indicate IMAPErrors that may
* fix themselves when retried
*/
class RetryableError extends NylasError { }
import {NylasError, RetryableError} from './errors'
export class IMAPRetryableError extends RetryableError {
constructor(msg) {
super(msg)
this.userMessage = "We were unable to reach your IMAP provider. Please try again.";
this.statusCode = 408;
}
}
/**
* IMAPErrors that originate from NodeIMAP. See `convertImapError` for
* documentation on underlying causes
*/
class IMAPSocketError extends RetryableError { }
class IMAPConnectionTimeoutError extends RetryableError { }
class IMAPAuthenticationTimeoutError extends RetryableError { }
class IMAPProtocolError extends NylasError { }
class IMAPAuthenticationError extends NylasError { }
class IMAPTransientAuthenticationError extends RetryableError { }
export class IMAPSocketError extends IMAPRetryableError { }
export class IMAPConnectionTimeoutError extends IMAPRetryableError { }
export class IMAPAuthenticationTimeoutError extends IMAPRetryableError { }
export class IMAPTransientAuthenticationError extends IMAPRetryableError { }
class IMAPConnectionNotReadyError extends RetryableError {
export class IMAPProtocolError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "IMAP protocol error. Please contact support@nylas.com."
this.statusCode = 401
}
}
export class IMAPAuthenticationError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "Incorrect IMAP username or password.";
this.statusCode = 401;
}
}
export class IMAPConnectionNotReadyError extends IMAPRetryableError {
constructor(funcName) {
super(`${funcName} - You must call connect() first.`);
}
}
class IMAPConnectionEndedError extends RetryableError {
export class IMAPConnectionEndedError extends IMAPRetryableError {
constructor(msg = "The IMAP Connection was ended.") {
super(msg);
}
@ -33,7 +49,13 @@ class IMAPConnectionEndedError extends RetryableError {
* of time, but not over the short spans of time in which it'd make sense
* for us to retry.
*/
class IMAPCertificateError extends NylasError { }
export class IMAPCertificateError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "We couldn't make a secure connection to your SMTP server. Please contact support@nylas.com."
this.statusCode = 401
}
}
/**
* IMAPErrors may come from:
@ -74,7 +96,7 @@ class IMAPCertificateError extends NylasError { }
* Message: 'Timed out while authenticating with server'
*
*/
function convertImapError(imapError) {
export function convertImapError(imapError) {
let error = imapError;
if (/try again/i.test(imapError.message)) {
@ -126,17 +148,3 @@ function convertImapError(imapError) {
error.source = imapError.source
return error
}
module.exports = {
convertImapError,
RetryableError,
IMAPSocketError,
IMAPConnectionTimeoutError,
IMAPAuthenticationTimeoutError,
IMAPProtocolError,
IMAPAuthenticationError,
IMAPTransientAuthenticationError,
IMAPConnectionNotReadyError,
IMAPConnectionEndedError,
IMAPCertificateError,
};

View file

@ -1,8 +1,7 @@
const atob = require('atob')
const crypto = require('crypto');
const {JSONColumn, JSONArrayColumn} = require('../database-types');
const {SUPPORTED_PROVIDERS, credentialsForProvider} = require('../auth-helpers');
const {credentialsForProvider, smtpConfigFromSettings} = require('../auth-helpers');
const {DB_ENCRYPTION_ALGORITHM, DB_ENCRYPTION_PASSWORD} = process.env;
@ -108,16 +107,6 @@ module.exports = (sequelize, Sequelize) => {
}
},
bearerToken(xoauth2) {
// We have to unpack the access token from the entire XOAuth2
// token because it is re-packed during the SMTP connection login.
// https://github.com/nodemailer/smtp-connection/blob/master/lib/smtp-connection.js#L1418
const bearer = "Bearer ";
const decoded = atob(xoauth2);
const tokenIndex = decoded.indexOf(bearer) + bearer.length;
return decoded.substring(tokenIndex, decoded.length - 2);
},
smtpConfig() {
// We always call credentialsForProvider() here because n1Cloud
// sometimes needs to send emails for accounts which did not have their
@ -127,34 +116,7 @@ module.exports = (sequelize, Sequelize) => {
settings: Object.assign({}, this.decryptedCredentials(), this.connectionSettings),
email: this.emailAddress,
});
let config;
const {smtp_host, smtp_port, ssl_required} = connectionSettings;
if (connectionSettings.smtp_custom_config) {
config = connectionSettings.smtp_custom_config;
} else {
config = {
host: smtp_host,
port: smtp_port,
secure: ssl_required,
};
}
if (this.provider === 'gmail') {
const {xoauth2} = connectionCredentials;
if (!xoauth2) {
throw new Error("Missing XOAuth2 Token")
}
const token = this.bearerToken(xoauth2);
config.auth = { user: connectionSettings.smtp_username, xoauth2: token }
} else if (SUPPORTED_PROVIDERS.has(this.provider)) {
const {smtp_username, smtp_password} = connectionCredentials
config.auth = { user: smtp_username, pass: smtp_password}
} else {
throw new Error(`${this.provider} not yet supported`)
}
return config;
return smtpConfigFromSettings(this.provider, connectionSettings, connectionCredentials);
},
},
});

View file

@ -1,8 +1,9 @@
/* eslint no-useless-escape: 0 */
const fs = require('fs');
const nodemailer = require('nodemailer');
const mailcomposer = require('mailcomposer');
const {APIError} = require('./errors')
import fs from 'fs'
import nodemailer from 'nodemailer'
import mailcomposer from 'mailcomposer'
import {APIError} from './errors'
import {convertSmtpError} from './smtp-errors'
const MAX_RETRIES = 1;
@ -27,10 +28,8 @@ class SendmailClient {
try {
results = await this._transporter.sendMail(msgData);
} catch (err) {
// TODO: shouldn't retry on permanent errors like Invalid login
// TODO: should also wait between retries :(
// Keep retrying for MAX_RETRIES
error = err;
error = convertSmtpError(err);
this._logger.error(err);
}
if (!results) {
@ -47,22 +46,15 @@ class SendmailClient {
}
this._logger.error('Max sending retries reached');
// TODO: figure out how to parse different errors, like in cloud-core
// https://github.com/nylas/cloud-core/blob/production/sync-engine/inbox/sendmail/smtp/postel.py#L354
if (/invalid login/i.test(error.message)) {
throw new APIError(`Sending failed - Invalid login`, 401, {originalError: error})
let userMessage = 'Sending failed';
let statusCode = 500;
if (error && error.userMessage && error.statusCode) {
userMessage = `Sending failed - ${error.userMessage}`;
statusCode = error.statusCode;
}
if (error.message.includes("getaddrinfo ENOTFOUND")) {
throw new APIError(`Sending failed - Network Error`, 401, {originalError: error})
}
if (error.message.includes("connect ETIMEDOUT")) {
throw new APIError('Sending failed - Network Error', 401, {originalError: error})
}
const {host, port, secure} = this._transporter.options;
throw new APIError('Sending failed', 500, {
const {host, port, secure} = this._transporter.transporter.options;
throw new APIError(userMessage, statusCode, {
originalError: error,
smtp_host: host,
smtp_port: port,

View file

@ -0,0 +1,64 @@
import {NylasError, RetryableError} from './errors'
export class SMTPRetryableError extends RetryableError {
constructor(msg) {
super(msg)
this.userMessage = "We were unable to reach your SMTP server. Please try again."
this.statusCode = 408
}
}
export class SMTPConnectionTimeoutError extends SMTPRetryableError { }
export class SMTPConnectionEndedError extends SMTPRetryableError { }
export class SMTPConnectionTLSError extends SMTPRetryableError { }
export class SMTPProtocolError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "SMTP protocol error. Please check your SMTP settings."
this.statusCode = 401
}
}
export class SMTPConnectionDNSError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "We were unable to look up your SMTP host. Please check the SMTP server name."
this.statusCode = 401
}
}
export class SMTPAuthenticationError extends NylasError {
constructor(msg) {
super(msg)
this.userMessage = "Incorrect SMTP username or password."
this.statusCode = 401
}
}
/* Nodemailer's errors are just regular old Error objects, so we have to
* test the error message to determine more about what they mean
*/
export function convertSmtpError(err) {
// TODO: what error is thrown if you're offline?
// TODO: what error is thrown if the message you're sending is too large?
if (/(?:connection timeout)|(?:connect etimedout)/i.test(err.message)) {
return new SMTPConnectionTimeoutError(err)
}
if (/connection closed/i.test(err.message)) {
return new SMTPConnectionEndedError(err)
}
if (/error initiating tls/i.test(err.message)) {
return new SMTPConnectionTLSError(err);
}
if (/getaddrinfo enotfound/i.test(err.message)) {
return new SMTPConnectionDNSError(err);
}
if (/unknown protocol/i.test(err.message)) {
return new SMTPProtocolError(err);
}
if (/(?:invalid login)|(?:username and password not accepted)|(?:incorrect username or password)/i.test(err.message)) {
return new SMTPAuthenticationError(err);
}
return err;
}