mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-27 10:18:25 +08:00
Added PROXY support for IMAP, POP3
This commit is contained in:
parent
2d2e6abaa0
commit
6e838f4ac7
7 changed files with 199 additions and 21 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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('*'),
|
||||
|
|
2
imap.js
2
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,
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue