index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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: 'LOGIN',
  100. user: authData.user,
  101. credentials: {
  102. user: authData.user || '',
  103. pass: authData.pass
  104. },
  105. method: (authData.method || '').trim().toUpperCase() || false
  106. };
  107. }
  108. }
  109. /**
  110. * Sends an e-mail using the selected settings
  111. *
  112. * @param {Object} mail Mail object
  113. * @param {Function} callback Callback function
  114. */
  115. send(mail, callback) {
  116. this.getSocket(this.options, (err, socketOptions) => {
  117. if (err) {
  118. return callback(err);
  119. }
  120. let returned = false;
  121. let options = this.options;
  122. if (socketOptions && socketOptions.connection) {
  123. this.logger.info(
  124. {
  125. tnx: 'proxy',
  126. remoteAddress: socketOptions.connection.remoteAddress,
  127. remotePort: socketOptions.connection.remotePort,
  128. destHost: options.host || '',
  129. destPort: options.port || '',
  130. action: 'connected'
  131. },
  132. 'Using proxied socket from %s:%s to %s:%s',
  133. socketOptions.connection.remoteAddress,
  134. socketOptions.connection.remotePort,
  135. options.host || '',
  136. options.port || ''
  137. );
  138. // only copy options if we need to modify it
  139. options = shared.assign(false, options);
  140. Object.keys(socketOptions).forEach(key => {
  141. options[key] = socketOptions[key];
  142. });
  143. }
  144. let connection = new SMTPConnection(options);
  145. connection.once('error', err => {
  146. if (returned) {
  147. return;
  148. }
  149. returned = true;
  150. connection.close();
  151. return callback(err);
  152. });
  153. connection.once('end', () => {
  154. if (returned) {
  155. return;
  156. }
  157. let timer = setTimeout(() => {
  158. if (returned) {
  159. return;
  160. }
  161. returned = true;
  162. // still have not returned, this means we have an unexpected connection close
  163. let err = new Error('Unexpected socket close');
  164. if (connection && connection._socket && connection._socket.upgrading) {
  165. // starttls connection errors
  166. err.code = 'ETLS';
  167. }
  168. callback(err);
  169. }, 1000);
  170. try {
  171. timer.unref();
  172. } catch (E) {
  173. // Ignore. Happens on envs with non-node timer implementation
  174. }
  175. });
  176. let sendMessage = () => {
  177. let envelope = mail.message.getEnvelope();
  178. let messageId = mail.message.messageId();
  179. let recipients = [].concat(envelope.to || []);
  180. if (recipients.length > 3) {
  181. recipients.push('...and ' + recipients.splice(2).length + ' more');
  182. }
  183. if (mail.data.dsn) {
  184. envelope.dsn = mail.data.dsn;
  185. }
  186. this.logger.info(
  187. {
  188. tnx: 'send',
  189. messageId
  190. },
  191. 'Sending message %s to <%s>',
  192. messageId,
  193. recipients.join(', ')
  194. );
  195. connection.send(envelope, mail.message.createReadStream(), (err, info) => {
  196. returned = true;
  197. connection.close();
  198. if (err) {
  199. this.logger.error(
  200. {
  201. err,
  202. tnx: 'send'
  203. },
  204. 'Send error for %s: %s',
  205. messageId,
  206. err.message
  207. );
  208. return callback(err);
  209. }
  210. info.envelope = {
  211. from: envelope.from,
  212. to: envelope.to
  213. };
  214. info.messageId = messageId;
  215. try {
  216. return callback(null, info);
  217. } catch (E) {
  218. this.logger.error(
  219. {
  220. err: E,
  221. tnx: 'callback'
  222. },
  223. 'Callback error for %s: %s',
  224. messageId,
  225. E.message
  226. );
  227. }
  228. });
  229. };
  230. connection.connect(() => {
  231. if (returned) {
  232. return;
  233. }
  234. let auth = this.getAuth(mail.data.auth);
  235. if (auth) {
  236. connection.login(auth, err => {
  237. if (auth && auth !== this.auth && auth.oauth2) {
  238. auth.oauth2.removeAllListeners();
  239. }
  240. if (returned) {
  241. return;
  242. }
  243. if (err) {
  244. returned = true;
  245. connection.close();
  246. return callback(err);
  247. }
  248. sendMessage();
  249. });
  250. } else {
  251. sendMessage();
  252. }
  253. });
  254. });
  255. }
  256. /**
  257. * Verifies SMTP configuration
  258. *
  259. * @param {Function} callback Callback function
  260. */
  261. verify(callback) {
  262. let promise;
  263. if (!callback && typeof Promise === 'function') {
  264. promise = new Promise((resolve, reject) => {
  265. callback = shared.callbackPromise(resolve, reject);
  266. });
  267. }
  268. this.getSocket(this.options, (err, socketOptions) => {
  269. if (err) {
  270. return callback(err);
  271. }
  272. let options = this.options;
  273. if (socketOptions && socketOptions.connection) {
  274. this.logger.info(
  275. {
  276. tnx: 'proxy',
  277. remoteAddress: socketOptions.connection.remoteAddress,
  278. remotePort: socketOptions.connection.remotePort,
  279. destHost: options.host || '',
  280. destPort: options.port || '',
  281. action: 'connected'
  282. },
  283. 'Using proxied socket from %s:%s to %s:%s',
  284. socketOptions.connection.remoteAddress,
  285. socketOptions.connection.remotePort,
  286. options.host || '',
  287. options.port || ''
  288. );
  289. options = shared.assign(false, options);
  290. Object.keys(socketOptions).forEach(key => {
  291. options[key] = socketOptions[key];
  292. });
  293. }
  294. let connection = new SMTPConnection(options);
  295. let returned = false;
  296. connection.once('error', err => {
  297. if (returned) {
  298. return;
  299. }
  300. returned = true;
  301. connection.close();
  302. return callback(err);
  303. });
  304. connection.once('end', () => {
  305. if (returned) {
  306. return;
  307. }
  308. returned = true;
  309. return callback(new Error('Connection closed'));
  310. });
  311. let finalize = () => {
  312. if (returned) {
  313. return;
  314. }
  315. returned = true;
  316. connection.quit();
  317. return callback(null, true);
  318. };
  319. connection.connect(() => {
  320. if (returned) {
  321. return;
  322. }
  323. let authData = this.getAuth({});
  324. if (authData) {
  325. connection.login(authData, err => {
  326. if (returned) {
  327. return;
  328. }
  329. if (err) {
  330. returned = true;
  331. connection.close();
  332. return callback(err);
  333. }
  334. finalize();
  335. });
  336. } else {
  337. finalize();
  338. }
  339. });
  340. });
  341. return promise;
  342. }
  343. /**
  344. * Releases resources
  345. */
  346. close() {
  347. if (this.auth && this.auth.oauth2) {
  348. this.auth.oauth2.removeAllListeners();
  349. }
  350. this.emit('close');
  351. }
  352. }
  353. // expose to the world
  354. module.exports = SMTPTransport;