diff --git a/config/imap.toml b/config/imap.toml index d0bab010..50e1510d 100644 --- a/config/imap.toml +++ b/config/imap.toml @@ -1,8 +1,7 @@ # If enabled then Wild Duck exposes an IMAP interface for listing and fetching emails enabled=true port=9993 -# by default bind to localhost only -host="127.0.0.1" +host="0.0.0.0" # Use `true` for port 993 and `false` for 143. If connection is not secured # on connection then Wild Duck enables STARTTLS extension @@ -20,6 +19,9 @@ disableRetention=false # If true, then disables STARTTLS support disableSTARTTLS=false +# If true, then expect HAProxy PROXY header as the first line of data +useProxy=false + [id] #name="Wild Duck IMAP" #version="1.0.0" diff --git a/config/pop3.toml b/config/pop3.toml index 1d2e8ea7..cc8ef80c 100644 --- a/config/pop3.toml +++ b/config/pop3.toml @@ -2,7 +2,7 @@ enabled=true port=9995 # by default bind to localhost only -host="127.0.0.1" +host="0.0.0.0" # Use `true` for port 995 and `false` for 110. Try to always use `true` as the included # POP3 server is limited and does not support the STLS command @@ -15,6 +15,9 @@ disableVersionString=false # POP3 server never lists all messages but only a limited length list maxMessages=250 +# If true, then expect HAProxy PROXY header as the first line of data +useProxy=false + [tls] # If certificate path is not defined, use global or built-in self-signed certs #key="/path/to/server/key.pem" diff --git a/imap-core/lib/imap-connection.js b/imap-core/lib/imap-connection.js index e088686a..ee6658ba 100644 --- a/imap-core/lib/imap-connection.js +++ b/imap-core/lib/imap-connection.js @@ -22,11 +22,13 @@ const SOCKET_TIMEOUT = 10 * 60 * 1000; * @param {Object} socket Socket instance */ class IMAPConnection extends EventEmitter { - constructor(server, socket) { + constructor(server, socket, options) { super(); + options = options || {}; + // Random session ID, used for logging - this.id = crypto.randomBytes(9).toString('base64'); + this.id = options.id || crypto.randomBytes(9).toString('base64'); this.compression = false; this._deflate = false; @@ -63,7 +65,7 @@ class IMAPConnection extends EventEmitter { this.secure = !!this._server.options.secure; // Store remote address for later usage - this.remoteAddress = this._socket.remoteAddress; + this.remoteAddress = options.remoteAddress || this._socket.remoteAddress; // Server hostname for the greegins this.name = (this._server.options.name || os.hostname()).toLowerCase(); @@ -129,6 +131,7 @@ class IMAPConnection extends EventEmitter { this.id, this.clientHostname ); + this.send('* OK ' + ((this._server.options.id && this._server.options.id.name) || packageInfo.name) + ' ready'); }); } diff --git a/imap-core/lib/imap-server.js b/imap-core/lib/imap-server.js index 10ced25f..6464abec 100644 --- a/imap-core/lib/imap-server.js +++ b/imap-core/lib/imap-server.js @@ -2,6 +2,7 @@ const net = require('net'); const tls = require('tls'); +const crypto = require('crypto'); const IMAPConnection = require('./imap-connection').IMAPConnection; const tlsOptions = require('./tls-options'); const EventEmitter = require('events').EventEmitter; @@ -45,22 +46,34 @@ class IMAPServer extends EventEmitter { // setup server listener and connection handler if (this.options.secure && !this.options.needsUpgrade) { this.server = net.createServer(this.options, socket => { - this._upgrade(socket, (err, tlsSocket) => { + this._handleProxy(socket, (err, socketOptions) => { if (err) { - return this._onError(err); + // ignore, should not happen } - this.connect(tlsSocket); + this._upgrade(socket, (err, tlsSocket) => { + if (err) { + return this._onError(err); + } + this.connect(tlsSocket, socketOptions); + }); }); }); } else { - this.server = net.createServer(this.options, socket => this.connect(socket)); + this.server = net.createServer(this.options, socket => + this._handleProxy(socket, (err, socketOptions) => { + if (err) { + // ignore, should not happen + } + this.connect(socket, socketOptions); + }) + ); } this._setListeners(); } - connect(socket) { - let connection = new IMAPConnection(this, socket); + connect(socket, socketOptions) { + let connection = new IMAPConnection(this, socket, socketOptions); this.connections.add(connection); connection.on('error', this._onError.bind(this)); connection.init(); @@ -180,6 +193,75 @@ class IMAPServer extends EventEmitter { this.emit('error', err); } + _handleProxy(socket, callback) { + if (!this.options.useProxy) { + return setImmediate(callback); + } + + let chunks = []; + let chunklen = 0; + let socketReader = () => { + let chunk; + while ((chunk = socket.read()) !== null) { + for (let i = 0, len = chunk.length; i < len; i++) { + let chr = chunk[i]; + if (chr === 0x0a) { + socket.removeListener('readable', socketReader); + chunks.push(chunk.slice(0, i + 1)); + chunklen += i + 1; + let remainder = chunk.slice(i + 1); + if (remainder.length) { + socket.unshift(remainder); + } + + let socketOptions = { + id: crypto.randomBytes(9).toString('base64') + }; + + let header = Buffer.concat(chunks, chunklen) + .toString() + .trim(); + + let params = (header || '').toString().split(' '); + let commandName = params.shift().toUpperCase(); + if (commandName !== 'PROXY') { + try { + socket.end('* BAD Invalid PROXY header\r\n'); + } catch (E) { + // ignore + } + return; + } + + if (params[1]) { + this.logger.info( + { + tnx: 'proxy', + cid: socketOptions.id, + proxy: params[1].trim().toLowerCase(), + destination: socket.remoteAddress + }, + '[%s] PROXY from %s through %s', + socketOptions.id, + params[1].trim().toLowerCase(), + socket.remoteAddress + ); + socketOptions.remoteAddress = params[1].trim().toLowerCase(); + if (params[3]) { + socketOptions.remotePort = Number(params[3].trim()) || socketOptions.remotePort; + } + } + + return callback(null, socketOptions); + } + } + chunks.push(chunk); + chunklen += chunk.length; + } + }; + socket.on('readable', socketReader); + } + _upgrade(socket, callback) { let socketOptions = { secureContext: this.secureContext.get('*'), diff --git a/imap.js b/imap.js index 8d9225ac..02d7bfcb 100644 --- a/imap.js +++ b/imap.js @@ -44,6 +44,8 @@ const serverOptions = { disableSTARTTLS: config.imap.disableSTARTTLS, ignoreSTARTTLS: config.imap.ignoreSTARTTLS, + useProxy: !!config.imap.useProxy, + id: { name: config.imap.name || 'Wild Duck IMAP Server', version: config.imap.version || packageData.version, diff --git a/lib/pop3/connection.js b/lib/pop3/connection.js index 727156eb..15006f25 100644 --- a/lib/pop3/connection.js +++ b/lib/pop3/connection.js @@ -8,16 +8,20 @@ const DataStream = require('nodemailer/lib/smtp-connection/data-stream'); const SOCKET_TIMEOUT = 60 * 1000; class POP3Connection extends EventEmitter { - constructor(server, socket) { + constructor(server, socket, options) { super(); + + options = options || {}; + this._server = server; this._socket = socket; this._closed = false; this._closing = false; - this.remoteAddress = this._socket.remoteAddress; - this._id = crypto.randomBytes(9).toString('base64'); + // Store remote address for later usage + this.remoteAddress = options.remoteAddress || this._socket.remoteAddress; + this._id = options.id || crypto.randomBytes(9).toString('base64'); this.processing = false; this.queue = []; diff --git a/lib/pop3/server.js b/lib/pop3/server.js index 030521aa..258b0819 100644 --- a/lib/pop3/server.js +++ b/lib/pop3/server.js @@ -3,6 +3,7 @@ const EventEmitter = require('events'); const net = require('net'); const tls = require('tls'); +const crypto = require('crypto'); const tlsOptions = require('../../imap-core/lib/tls-options'); const shared = require('nodemailer/lib/shared'); const POP3Connection = require('./connection'); @@ -42,15 +43,27 @@ class POP3Server extends EventEmitter { if (this.options.secure && !this.options.needsUpgrade) { this.server = net.createServer(this.options, socket => { - this._upgrade(socket, (err, tlsSocket) => { + this._handleProxy(socket, (err, socketOptions) => { if (err) { - return this._onError(err); + // ignore, should not happen } - this.connect(tlsSocket); + this._upgrade(socket, (err, tlsSocket) => { + if (err) { + return this._onError(err); + } + this.connect(tlsSocket, socketOptions); + }); }); }); } else { - this.server = net.createServer(this.options, socket => this.connect(socket)); + this.server = net.createServer(this.options, socket => { + this._handleProxy(socket, (err, socketOptions) => { + if (err) { + // ignore, should not happen + } + this.connect(socket, socketOptions); + }); + }); } this._setListeners(); @@ -233,8 +246,77 @@ class POP3Server extends EventEmitter { this.emit('error', err); } - connect(socket) { - let connection = new POP3Connection(this, socket); + _handleProxy(socket, callback) { + if (!this.options.useProxy) { + return setImmediate(callback); + } + + let chunks = []; + let chunklen = 0; + let socketReader = () => { + let chunk; + while ((chunk = socket.read()) !== null) { + for (let i = 0, len = chunk.length; i < len; i++) { + let chr = chunk[i]; + if (chr === 0x0a) { + socket.removeListener('readable', socketReader); + chunks.push(chunk.slice(0, i + 1)); + chunklen += i + 1; + let remainder = chunk.slice(i + 1); + if (remainder.length) { + socket.unshift(remainder); + } + + let socketOptions = { + id: crypto.randomBytes(9).toString('base64') + }; + + let header = Buffer.concat(chunks, chunklen) + .toString() + .trim(); + + let params = (header || '').toString().split(' '); + let commandName = params.shift().toUpperCase(); + if (commandName !== 'PROXY') { + try { + socket.end('-ERR Invalid PROXY header\r\n'); + } catch (E) { + // ignore + } + return; + } + + if (params[1]) { + this.logger.info( + { + tnx: 'proxy', + cid: socketOptions.id, + proxy: params[1].trim().toLowerCase(), + destination: socket.remoteAddress + }, + '[%s] PROXY from %s through %s', + socketOptions.id, + params[1].trim().toLowerCase(), + socket.remoteAddress + ); + socketOptions.remoteAddress = params[1].trim().toLowerCase(); + if (params[3]) { + socketOptions.remotePort = Number(params[3].trim()) || socketOptions.remotePort; + } + } + + return callback(null, socketOptions); + } + } + chunks.push(chunk); + chunklen += chunk.length; + } + }; + socket.on('readable', socketReader); + } + + connect(socket, socketOptions) { + let connection = new POP3Connection(this, socket, socketOptions); this.connections.add(connection); connection.once('error', err => { this.connections.delete(connection);