| 'use strict'; |
| |
| const http = require('http'); |
| const https = require('https'); |
| const urllib = require('url'); |
| const zlib = require('zlib'); |
| const PassThrough = require('stream').PassThrough; |
| const Cookies = require('./cookies'); |
| const packageData = require('../../package.json'); |
| |
| const MAX_REDIRECTS = 5; |
| |
| module.exports = function (url, options) { |
| return fetch(url, options); |
| }; |
| |
| module.exports.Cookies = Cookies; |
| |
| function fetch(url, options) { |
| options = options || {}; |
| |
| options.fetchRes = options.fetchRes || new PassThrough(); |
| options.cookies = options.cookies || new Cookies(); |
| options.redirects = options.redirects || 0; |
| options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects; |
| |
| if (options.cookie) { |
| [].concat(options.cookie || []).forEach(cookie => { |
| options.cookies.set(cookie, url); |
| }); |
| options.cookie = false; |
| } |
| |
| let fetchRes = options.fetchRes; |
| let parsed = urllib.parse(url); |
| let method = (options.method || '').toString().trim().toUpperCase() || 'GET'; |
| let finished = false; |
| let cookies; |
| let body; |
| |
| let handler = parsed.protocol === 'https:' ? https : http; |
| |
| let headers = { |
| 'accept-encoding': 'gzip,deflate', |
| 'user-agent': 'nodemailer/' + packageData.version |
| }; |
| |
| Object.keys(options.headers || {}).forEach(key => { |
| headers[key.toLowerCase().trim()] = options.headers[key]; |
| }); |
| |
| if (options.userAgent) { |
| headers['user-agent'] = options.userAgent; |
| } |
| |
| if (parsed.auth) { |
| headers.Authorization = 'Basic ' + new Buffer(parsed.auth).toString('base64'); |
| } |
| |
| if ((cookies = options.cookies.get(url))) { |
| headers.cookie = cookies; |
| } |
| |
| if (options.body) { |
| if (options.contentType !== false) { |
| headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded'; |
| } |
| |
| if (typeof options.body.pipe === 'function') { |
| // it's a stream |
| headers['Transfer-Encoding'] = 'chunked'; |
| body = options.body; |
| body.on('error', err => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| }); |
| } else { |
| if (options.body instanceof Buffer) { |
| body = options.body; |
| } else if (typeof options.body === 'object') { |
| body = new Buffer(Object.keys(options.body).map(key => { |
| let value = options.body[key].toString().trim(); |
| return encodeURIComponent(key) + '=' + encodeURIComponent(value); |
| }).join('&')); |
| } else { |
| body = new Buffer(options.body.toString().trim()); |
| } |
| |
| headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded'; |
| headers['Content-Length'] = body.length; |
| } |
| // if method is not provided, use POST instead of GET |
| method = (options.method || '').toString().trim().toUpperCase() || 'POST'; |
| } |
| |
| let req; |
| let reqOptions = { |
| method, |
| host: parsed.hostname, |
| path: parsed.path, |
| port: parsed.port ? parsed.port : (parsed.protocol === 'https:' ? 443 : 80), |
| headers, |
| rejectUnauthorized: false, |
| agent: false |
| }; |
| |
| if (options.tls) { |
| Object.keys(options.tls).forEach(key => { |
| reqOptions[key] = options.tls[key]; |
| }); |
| } |
| |
| try { |
| req = handler.request(reqOptions); |
| } catch (E) { |
| finished = true; |
| setImmediate(() => { |
| E.type = 'FETCH'; |
| E.sourceUrl = url; |
| fetchRes.emit('error', E); |
| }); |
| return fetchRes; |
| } |
| |
| if (options.timeout) { |
| req.setTimeout(options.timeout, () => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| req.abort(); |
| let err = new Error('Request Timeout'); |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| }); |
| } |
| |
| req.on('error', err => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| }); |
| |
| req.on('response', res => { |
| let inflate; |
| |
| if (finished) { |
| return; |
| } |
| |
| switch (res.headers['content-encoding']) { |
| case 'gzip': |
| case 'deflate': |
| inflate = zlib.createUnzip(); |
| break; |
| } |
| |
| if (res.headers['set-cookie']) { |
| [].concat(res.headers['set-cookie'] || []).forEach(cookie => { |
| options.cookies.set(cookie, url); |
| }); |
| } |
| |
| if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { |
| // redirect |
| options.redirects++; |
| if (options.redirects > options.maxRedirects) { |
| finished = true; |
| let err = new Error('Maximum redirect count exceeded'); |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| req.abort(); |
| return; |
| } |
| return fetch(urllib.resolve(url, res.headers.location), options); |
| } |
| |
| fetchRes.statusCode = res.statusCode; |
| |
| if (res.statusCode >= 300 && !options.allowErrorResponse) { |
| finished = true; |
| let err = new Error('Invalid status code ' + res.statusCode); |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| req.abort(); |
| return; |
| } |
| |
| res.on('error', err => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| req.abort(); |
| }); |
| |
| if (inflate) { |
| res.pipe(inflate).pipe(fetchRes); |
| inflate.on('error', err => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| req.abort(); |
| }); |
| } else { |
| res.pipe(fetchRes); |
| } |
| }); |
| |
| setImmediate(() => { |
| if (body) { |
| try { |
| if (typeof body.pipe === 'function') { |
| return body.pipe(req); |
| } else { |
| req.write(body); |
| } |
| } catch (err) { |
| finished = true; |
| err.type = 'FETCH'; |
| err.sourceUrl = url; |
| fetchRes.emit('error', err); |
| return; |
| } |
| } |
| req.end(); |
| }); |
| |
| return fetchRes; |
| } |