index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. 'use strict';
  2. const EventEmitter = require('events');
  3. const SMTPConnection = require('../smtp-connection');
  4. const wellKnown = require('../well-known');
  5. const shared = require('../shared');
  6. const XOAuth2 = require('../xoauth2');
  7. const packageData = require('../../package.json');
  8. /**
  9. * Creates a SMTP transport object for Nodemailer
  10. *
  11. * @constructor
  12. * @param {Object} options Connection options
  13. */
  14. class SMTPTransport extends EventEmitter {
  15. constructor(options) {
  16. super();
  17. options = options || {};
  18. if (typeof options === 'string') {
  19. options = {
  20. url: options
  21. };
  22. }
  23. let urlData;
  24. let service = options.service;
  25. if (typeof options.getSocket === 'function') {
  26. this.getSocket = options.getSocket;
  27. }
  28. if (options.url) {
  29. urlData = shared.parseConnectionUrl(options.url);
  30. service = service || urlData.service;
  31. }
  32. this.options = shared.assign(
  33. false, // create new object
  34. options, // regular options
  35. urlData, // url options
  36. service && wellKnown(service) // wellknown options
  37. );
  38. this.logger = shared.getLogger(this.options, {
  39. component: this.options.component || 'smtp-transport'
  40. });
  41. // temporary object
  42. let connection = new SMTPConnection(this.options);
  43. this.name = 'SMTP';
  44. this.version = packageData.version + '[client:' + connection.version + ']';
  45. if (this.options.auth) {
  46. this.auth = this.getAuth({});
  47. }
  48. }
  49. /**
  50. * Placeholder function for creating proxy sockets. This method immediatelly returns
  51. * without a socket
  52. *
  53. * @param {Object} options Connection options
  54. * @param {Function} callback Callback function to run with the socket keys
  55. */
  56. getSocket(options, callback) {
  57. // return immediatelly
  58. return setImmediate(() => callback(null, false));
  59. }
  60. getAuth(authOpts) {
  61. if (!authOpts) {
  62. return this.auth;
  63. }
  64. let hasAuth = false;
  65. let authData = {};
  66. if (this.options.auth && typeof this.options.auth === 'object') {
  67. Object.keys(this.options.auth).forEach(key => {
  68. hasAuth = true;
  69. authData[key] = this.options.auth[key];
  70. });
  71. }
  72. if (authOpts && typeof authOpts === 'object') {
  73. Object.keys(authOpts).forEach(key => {
  74. hasAuth = true;
  75. authData[key] = authOpts[key];
  76. });
  77. }
  78. if (!hasAuth) {
  79. return false;
  80. }
  81. switch ((authData.type || '').toString().toUpperCase()) {
  82. case 'OAUTH2': {
  83. if (!authData.service && !authData.user) {
  84. return false;
  85. }
  86. let oauth2 = new XOAuth2(authData, this.logger);
  87. oauth2.provisionCallback = (this.mailer && this.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
  88. oauth2.on('token', token => this.mailer.emit('token', token));
  89. oauth2.on('error', err => this.emit('error', err));
  90. return {
  91. type: 'OAUTH2',
  92. user: authData.user,
  93. oauth2,
  94. method: 'XOAUTH2'
  95. };
  96. }
  97. default:
  98. return {
  99. type: (authData.type || '').toString().toUpperCase() || 'LOGIN',
  100. user: authData.user,
  101. credentials: {
  102. user: authData.user || '',
  103. pass: authData.pass,
  104. options: authData.options
  105. },
  106. method: (authData.method || '').trim().toUpperCase() || this.options.authMethod || false
  107. };
  108. }
  109. }
  110. /**
  111. * Sends an e-mail using the selected settings
  112. *
  113. * @param {Object} mail Mail object
  114. * @param {Function} callback Callback function
  115. */
  116. send(mail, callback) {
  117. this.getSocket(this.options, (err, socketOptions) => {
  118. if (err) {
  119. return callback(err);
  120. }
  121. let returned = false;
  122. let options = this.options;
  123. if (socketOptions && socketOptions.connection) {
  124. this.logger.info(
  125. {
  126. tnx: 'proxy',
  127. remoteAddress: socketOptions.connection.remoteAddress,
  128. remotePort: socketOptions.connection.remotePort,
  129. destHost: options.host || '',
  130. destPort: options.port || '',
  131. action: 'connected'
  132. },
  133. 'Using proxied socket from %s:%s to %s:%s',
  134. socketOptions.connection.remoteAddress,
  135. socketOptions.connection.remotePort,
  136. options.host || '',
  137. options.port || ''
  138. );
  139. // only copy options if we need to modify it
  140. options = shared.assign(false, options);
  141. Object.keys(socketOptions).forEach(key => {
  142. options[key] = socketOptions[key];
  143. });
  144. }
  145. let connection = new SMTPConnection(options);
  146. connection.once('error', err => {
  147. if (returned) {
  148. return;
  149. }
  150. returned = true;
  151. connection.close();
  152. return callback(err);
  153. });
  154. connection.once('end', () => {
  155. if (returned) {
  156. return;
  157. }
  158. let timer = setTimeout(() => {
  159. if (returned) {
  160. return;
  161. }
  162. returned = true;
  163. // still have not returned, this means we have an unexpected connection close
  164. let err = new Error('Unexpected socket close');
  165. if (connection && connection._socket && connection._socket.upgrading) {
  166. // starttls connection errors
  167. err.code = 'ETLS';
  168. }
  169. callback(err);
  170. }, 1000);
  171. try {
  172. timer.unref();
  173. } catch (E) {
  174. // Ignore. Happens on envs with non-node timer implementation
  175. }
  176. });
  177. let sendMessage = () => {
  178. let envelope = mail.message.getEnvelope();
  179. let messageId = mail.message.messageId();
  180. let recipients = [].concat(envelope.to || []);
  181. if (recipients.length > 3) {
  182. recipients.push('...and ' + recipients.splice(2).length + ' more');
  183. }
  184. if (mail.data.dsn) {
  185. envelope.dsn = mail.data.dsn;
  186. }
  187. this.logger.info(
  188. {
  189. tnx: 'send',
  190. messageId
  191. },
  192. 'Sending message %s to <%s>',
  193. messageId,
  194. recipients.join(', ')
  195. );
  196. connection.send(envelope, mail.message.createReadStream(), (err, info) => {
  197. returned = true;
  198. connection.close();
  199. if (err) {
  200. this.logger.error(
  201. {
  202. err,
  203. tnx: 'send'
  204. },
  205. 'Send error for %s: %s',
  206. messageId,
  207. err.message
  208. );
  209. return callback(err);
  210. }
  211. info.envelope = {
  212. from: envelope.from,
  213. to: envelope.to
  214. };
  215. info.messageId = messageId;
  216. try {
  217. return callback(null, info);
  218. } catch (E) {
  219. this.logger.error(
  220. {
  221. err: E,
  222. tnx: 'callback'
  223. },
  224. 'Callback error for %s: %s',
  225. messageId,
  226. E.message
  227. );
  228. }
  229. });
  230. };
  231. connection.connect(() => {
  232. if (returned) {
  233. return;
  234. }
  235. let auth = this.getAuth(mail.data.auth);
  236. if (auth) {
  237. connection.login(auth, err => {
  238. if (auth && auth !== this.auth && auth.oauth2) {
  239. auth.oauth2.removeAllListeners();
  240. }
  241. if (returned) {
  242. return;
  243. }
  244. if (err) {
  245. returned = true;
  246. connection.close();
  247. return callback(err);
  248. }
  249. sendMessage();
  250. });
  251. } else {
  252. sendMessage();
  253. }
  254. });
  255. });
  256. }
  257. /**
  258. * Verifies SMTP configuration
  259. *
  260. * @param {Function} callback Callback function
  261. */
  262. verify(callback) {
  263. let promise;
  264. if (!callback) {
  265. promise = new Promise((resolve, reject) => {
  266. callback = shared.callbackPromise(resolve, reject);
  267. });
  268. }
  269. this.getSocket(this.options, (err, socketOptions) => {
  270. if (err) {
  271. return callback(err);
  272. }
  273. let options = this.options;
  274. if (socketOptions && socketOptions.connection) {
  275. this.logger.info(
  276. {
  277. tnx: 'proxy',
  278. remoteAddress: socketOptions.connection.remoteAddress,
  279. remotePort: socketOptions.connection.remotePort,
  280. destHost: options.host || '',
  281. destPort: options.port || '',
  282. action: 'connected'
  283. },
  284. 'Using proxied socket from %s:%s to %s:%s',
  285. socketOptions.connection.remoteAddress,
  286. socketOptions.connection.remotePort,
  287. options.host || '',
  288. options.port || ''
  289. );
  290. options = shared.assign(false, options);
  291. Object.keys(socketOptions).forEach(key => {
  292. options[key] = socketOptions[key];
  293. });
  294. }
  295. let connection = new SMTPConnection(options);
  296. let returned = false;
  297. connection.once('error', err => {
  298. if (returned) {
  299. return;
  300. }
  301. returned = true;
  302. connection.close();
  303. return callback(err);
  304. });
  305. connection.once('end', () => {
  306. if (returned) {
  307. return;
  308. }
  309. returned = true;
  310. return callback(new Error('Connection closed'));
  311. });
  312. let finalize = () => {
  313. if (returned) {
  314. return;
  315. }
  316. returned = true;
  317. connection.quit();
  318. return callback(null, true);
  319. };
  320. connection.connect(() => {
  321. if (returned) {
  322. return;
  323. }
  324. let authData = this.getAuth({});
  325. if (authData) {
  326. connection.login(authData, err => {
  327. if (returned) {
  328. return;
  329. }
  330. if (err) {
  331. returned = true;
  332. connection.close();
  333. return callback(err);
  334. }
  335. finalize();
  336. });
  337. } else {
  338. finalize();
  339. }
  340. });
  341. });
  342. return promise;
  343. }
  344. /**
  345. * Releases resources
  346. */
  347. close() {
  348. if (this.auth && this.auth.oauth2) {
  349. this.auth.oauth2.removeAllListeners();
  350. }
  351. this.emit('close');
  352. }
  353. }
  354. // expose to the world
  355. module.exports = SMTPTransport;