mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-02-24 07:54:46 +08:00
Allow TLS to be handled upstream
This commit is contained in:
parent
2a4fa1b35f
commit
ebf05cda58
9 changed files with 235 additions and 177 deletions
15
README.md
15
README.md
|
@ -360,7 +360,20 @@ be noticeable.
|
|||
|
||||
### HAProxy
|
||||
|
||||
When using Haproxy you can enable PROXY protocol to get correct remote addresses in logs
|
||||
When using HAProxy you can enable PROXY protocol to get correct remote addresses in server logs. You can use the most basic round-robin based balancing as no
|
||||
persistent sessions against specific hosts are needed. Use TCP load balancing with no extra settings both for plaintext and TLS connections.
|
||||
|
||||
If TLS is handled by HAProxy then use the following server config to indicate that WildDuck assumes to be a TLS server but TLS is handled upstream
|
||||
|
||||
```toml
|
||||
[imap]
|
||||
secure=true # this is a TLS server
|
||||
secured=true # TLS is handled upstream
|
||||
|
||||
[pop3]
|
||||
secure=true # this is a TLS server
|
||||
secured=true # TLS is handled upstream
|
||||
```
|
||||
|
||||
### Certificates
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ disableSTARTTLS=false
|
|||
|
||||
# If true, then expect HAProxy PROXY header as the first line of data
|
||||
useProxy=false
|
||||
# useProxy=true # expect PROXY from all conections
|
||||
# useProxy=['*'] # expect PROXY from all conections
|
||||
# useProxy=['1.2.3.4', '1.2.3.5'] # expect PROXY only from connections from listed IP addresses
|
||||
|
||||
# an array of IP addresses to ignore (not logged)
|
||||
ignoredHosts=[]
|
||||
|
|
|
@ -67,7 +67,7 @@ class IMAPConnection extends EventEmitter {
|
|||
this.secure = !!this._server.options.secure;
|
||||
|
||||
// Store remote address for later usage
|
||||
this.remoteAddress = options.remoteAddress || this._socket.remoteAddress;
|
||||
this.remoteAddress = (options.remoteAddress || this._socket.remoteAddress || '').replace(/^::ffff:/, '');
|
||||
|
||||
// Server hostname for the greegins
|
||||
this.name = (this._server.options.name || os.hostname()).toLowerCase();
|
||||
|
|
|
@ -51,6 +51,9 @@ class IMAPServer extends EventEmitter {
|
|||
if (err) {
|
||||
// ignore, should not happen
|
||||
}
|
||||
if (this.options.secured) {
|
||||
return this.connect(socket, socketOptions);
|
||||
}
|
||||
this._upgrade(socket, (err, tlsSocket) => {
|
||||
if (err) {
|
||||
return this._onError(err);
|
||||
|
@ -199,7 +202,10 @@ class IMAPServer extends EventEmitter {
|
|||
id: base32.encode(crypto.randomBytes(10)).toLowerCase()
|
||||
};
|
||||
|
||||
if (!this.options.useProxy) {
|
||||
if (
|
||||
!this.options.useProxy ||
|
||||
(Array.isArray(this.options.useProxy) && !this.options.useProxy.includes(socket.remoteAddress) && !this.options.useProxy.includes('*'))
|
||||
) {
|
||||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socket.remoteAddress);
|
||||
return setImmediate(() => callback(null, socketOptions));
|
||||
}
|
||||
|
|
2
imap.js
2
imap.js
|
@ -355,6 +355,8 @@ let createInterface = (ifaceOptions, callback) => {
|
|||
// Setup server
|
||||
const serverOptions = {
|
||||
secure: ifaceOptions.secure,
|
||||
secured: ifaceOptions.secured,
|
||||
|
||||
disableSTARTTLS: ifaceOptions.disableSTARTTLS,
|
||||
ignoreSTARTTLS: ifaceOptions.ignoreSTARTTLS,
|
||||
|
||||
|
|
|
@ -48,6 +48,9 @@ class POP3Server extends EventEmitter {
|
|||
if (err) {
|
||||
// ignore, should not happen
|
||||
}
|
||||
if (this.options.secured) {
|
||||
return this.connect(socket, socketOptions);
|
||||
}
|
||||
this._upgrade(socket, (err, tlsSocket) => {
|
||||
if (err) {
|
||||
return this._onError(err);
|
||||
|
@ -252,7 +255,10 @@ class POP3Server extends EventEmitter {
|
|||
id: base32.encode(crypto.randomBytes(10)).toLowerCase()
|
||||
};
|
||||
|
||||
if (!this.options.useProxy) {
|
||||
if (
|
||||
!this.options.useProxy ||
|
||||
(Array.isArray(this.options.useProxy) && !this.options.useProxy.includes(socket.remoteAddress) && !this.options.useProxy.includes('*'))
|
||||
) {
|
||||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socket.remoteAddress);
|
||||
return setImmediate(() => callback(null, socketOptions));
|
||||
}
|
||||
|
|
3
lmtp.js
3
lmtp.js
|
@ -31,6 +31,9 @@ config.on('reload', () => {
|
|||
const serverOptions = {
|
||||
lmtp: true,
|
||||
|
||||
secure: config.lmtp.secure,
|
||||
secured: config.lmtp.secured,
|
||||
|
||||
// log to console
|
||||
logger: {
|
||||
info(...args) {
|
||||
|
|
11
package.json
11
package.json
|
@ -8,7 +8,10 @@
|
|||
"test": "mongo --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && NODE_ENV=test grunt",
|
||||
"apidoc": "apidoc -i lib/api/ -o docs/"
|
||||
},
|
||||
"keywords": ["imap", "mail server"],
|
||||
"keywords": [
|
||||
"imap",
|
||||
"mail server"
|
||||
],
|
||||
"author": "Andris Reinman",
|
||||
"license": "EUPL-1.1",
|
||||
"devDependencies": {
|
||||
|
@ -39,7 +42,7 @@
|
|||
"humanparser": "1.5.0",
|
||||
"iconv-lite": "0.4.19",
|
||||
"ioredfour": "1.0.2-ioredis",
|
||||
"ioredis": "3.2.1",
|
||||
"ioredis": "3.2.2",
|
||||
"joi": "13.0.2",
|
||||
"js-yaml": "3.10.0",
|
||||
"libbase64": "1.0.1",
|
||||
|
@ -53,11 +56,11 @@
|
|||
"mongodb-extended-json": "^1.10.0",
|
||||
"nodemailer": "4.4.0",
|
||||
"npmlog": "4.1.2",
|
||||
"openpgp": "2.5.13",
|
||||
"openpgp": "2.5.14",
|
||||
"qrcode": "1.0.0",
|
||||
"restify": "6.3.4",
|
||||
"seq-index": "1.1.0",
|
||||
"smtp-server": "3.3.1",
|
||||
"smtp-server": "3.4.0",
|
||||
"speakeasy": "2.0.0",
|
||||
"tlds": "1.199.0",
|
||||
"u2f": "0.1.3",
|
||||
|
|
360
pop3.js
360
pop3.js
|
@ -21,6 +21,8 @@ const serverOptions = {
|
|||
host: config.pop3.host,
|
||||
|
||||
secure: config.pop3.secure,
|
||||
secured: config.pop3.secured,
|
||||
|
||||
disableSTARTTLS: config.pop3.disableSTARTTLS,
|
||||
ignoreSTARTTLS: config.pop3.ignoreSTARTTLS,
|
||||
|
||||
|
@ -86,58 +88,61 @@ const serverOptions = {
|
|||
|
||||
onListMessages(session, callback) {
|
||||
// only list messages in INBOX
|
||||
db.database.collection('mailboxes').findOne({
|
||||
user: session.user.id,
|
||||
path: 'INBOX'
|
||||
}, (err, mailbox) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
db.database.collection('mailboxes').findOne(
|
||||
{
|
||||
user: session.user.id,
|
||||
path: 'INBOX'
|
||||
},
|
||||
(err, mailbox) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
return callback(new Error('Mailbox not found for user'));
|
||||
}
|
||||
if (!mailbox) {
|
||||
return callback(new Error('Mailbox not found for user'));
|
||||
}
|
||||
|
||||
session.user.mailbox = mailbox._id;
|
||||
session.user.mailbox = mailbox._id;
|
||||
|
||||
db.database
|
||||
.collection('messages')
|
||||
.find({
|
||||
mailbox: mailbox._id
|
||||
})
|
||||
.project({
|
||||
uid: true,
|
||||
size: true,
|
||||
mailbox: true,
|
||||
// required to decide if we need to update flags after RETR
|
||||
flags: true,
|
||||
unseen: true
|
||||
})
|
||||
.sort([['uid', -1]])
|
||||
.limit(config.pop3.maxMessages || MAX_MESSAGES)
|
||||
.toArray((err, messages) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
db.database
|
||||
.collection('messages')
|
||||
.find({
|
||||
mailbox: mailbox._id
|
||||
})
|
||||
.project({
|
||||
uid: true,
|
||||
size: true,
|
||||
mailbox: true,
|
||||
// required to decide if we need to update flags after RETR
|
||||
flags: true,
|
||||
unseen: true
|
||||
})
|
||||
.sort([['uid', -1]])
|
||||
.limit(config.pop3.maxMessages || MAX_MESSAGES)
|
||||
.toArray((err, messages) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
messages: messages
|
||||
// showolder first
|
||||
.reverse()
|
||||
// compose message objects
|
||||
.map(message => ({
|
||||
id: message._id.toString(),
|
||||
uid: message.uid,
|
||||
mailbox: message.mailbox,
|
||||
size: message.size,
|
||||
flags: message.flags,
|
||||
seen: !message.unseen
|
||||
})),
|
||||
count: messages.length,
|
||||
size: messages.reduce((acc, message) => acc + message.size, 0)
|
||||
return callback(null, {
|
||||
messages: messages
|
||||
// showolder first
|
||||
.reverse()
|
||||
// compose message objects
|
||||
.map(message => ({
|
||||
id: message._id.toString(),
|
||||
uid: message.uid,
|
||||
mailbox: message.mailbox,
|
||||
size: message.size,
|
||||
flags: message.flags,
|
||||
seen: !message.unseen
|
||||
})),
|
||||
count: messages.length,
|
||||
size: messages.reduce((acc, message) => acc + message.size, 0)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onFetchMessage(message, session, callback) {
|
||||
|
@ -149,38 +154,42 @@ const serverOptions = {
|
|||
let err = new Error('Download was rate limited. Check again in ' + res.ttl + ' seconds');
|
||||
return callback(err);
|
||||
}
|
||||
db.database.collection('messages').findOne({
|
||||
_id: new ObjectID(message.id),
|
||||
// shard key
|
||||
mailbox: message.mailbox,
|
||||
uid: message.uid
|
||||
}, {
|
||||
mimeTree: true,
|
||||
size: true
|
||||
}, (err, message) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
db.database.collection('messages').findOne(
|
||||
{
|
||||
_id: new ObjectID(message.id),
|
||||
// shard key
|
||||
mailbox: message.mailbox,
|
||||
uid: message.uid
|
||||
},
|
||||
{
|
||||
mimeTree: true,
|
||||
size: true
|
||||
},
|
||||
(err, message) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!message) {
|
||||
return callback(new Error('Message does not exist or is already deleted'));
|
||||
}
|
||||
|
||||
let response = messageHandler.indexer.rebuild(message.mimeTree);
|
||||
if (!response || response.type !== 'stream' || !response.value) {
|
||||
return callback(new Error('Can not fetch message'));
|
||||
}
|
||||
|
||||
let limiter = new LimitedFetch({
|
||||
key: 'pdw:' + session.user.id,
|
||||
ttlcounter: messageHandler.counters.ttlcounter,
|
||||
maxBytes: config.pop3.maxDownloadMB * 1024 * 1024
|
||||
});
|
||||
|
||||
response.value.pipe(limiter);
|
||||
response.value.once('error', err => limiter.emit('error', err));
|
||||
|
||||
callback(null, limiter);
|
||||
}
|
||||
if (!message) {
|
||||
return callback(new Error('Message does not exist or is already deleted'));
|
||||
}
|
||||
|
||||
let response = messageHandler.indexer.rebuild(message.mimeTree);
|
||||
if (!response || response.type !== 'stream' || !response.value) {
|
||||
return callback(new Error('Can not fetch message'));
|
||||
}
|
||||
|
||||
let limiter = new LimitedFetch({
|
||||
key: 'pdw:' + session.user.id,
|
||||
ttlcounter: messageHandler.counters.ttlcounter,
|
||||
maxBytes: config.pop3.maxDownloadMB * 1024 * 1024
|
||||
});
|
||||
|
||||
response.value.pipe(limiter);
|
||||
response.value.once('error', err => limiter.emit('error', err));
|
||||
|
||||
callback(null, limiter);
|
||||
});
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -225,110 +234,123 @@ certs.registerReload(server, 'pop3');
|
|||
// move messages to trash
|
||||
function trashMessages(session, messages, callback) {
|
||||
// find Trash folder
|
||||
db.database.collection('mailboxes').findOne({
|
||||
user: session.user.id,
|
||||
specialUse: '\\Trash'
|
||||
}, (err, trashMailbox) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!trashMailbox) {
|
||||
return callback(new Error('Trash mailbox not found for user'));
|
||||
}
|
||||
|
||||
messageHandler.move(
|
||||
{
|
||||
user: session.user.id,
|
||||
// folder to move messages from
|
||||
source: {
|
||||
mailbox: session.user.mailbox
|
||||
},
|
||||
// folder to move messages to
|
||||
destination: trashMailbox,
|
||||
// list of UIDs to move
|
||||
messages: messages.map(message => message.uid),
|
||||
|
||||
// add \Seen flags to deleted messages
|
||||
markAsSeen: true
|
||||
},
|
||||
(err, success, meta) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
|
||||
db.database.collection('mailboxes').findOne(
|
||||
{
|
||||
user: session.user.id,
|
||||
specialUse: '\\Trash'
|
||||
},
|
||||
(err, trashMailbox) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!trashMailbox) {
|
||||
return callback(new Error('Trash mailbox not found for user'));
|
||||
}
|
||||
|
||||
messageHandler.move(
|
||||
{
|
||||
user: session.user.id,
|
||||
// folder to move messages from
|
||||
source: {
|
||||
mailbox: session.user.mailbox
|
||||
},
|
||||
// folder to move messages to
|
||||
destination: trashMailbox,
|
||||
// list of UIDs to move
|
||||
messages: messages.map(message => message.uid),
|
||||
|
||||
// add \Seen flags to deleted messages
|
||||
markAsSeen: true
|
||||
},
|
||||
(err, success, meta) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function markAsSeen(session, messages, callback) {
|
||||
let ids = messages.map(message => new ObjectID(message.id));
|
||||
|
||||
return db.database.collection('mailboxes').findOneAndUpdate({
|
||||
_id: session.user.mailbox
|
||||
}, {
|
||||
$inc: {
|
||||
modifyIndex: 1
|
||||
}
|
||||
}, {
|
||||
returnOriginal: false
|
||||
}, (err, item) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let mailboxData = item && item.value;
|
||||
if (!item) {
|
||||
return callback(new Error('Mailbox does not exist'));
|
||||
}
|
||||
|
||||
db.database.collection('messages').updateMany({
|
||||
_id: {
|
||||
$in: ids
|
||||
},
|
||||
user: session.user.id,
|
||||
mailbox: mailboxData._id,
|
||||
modseq: {
|
||||
$lt: mailboxData.modifyIndex
|
||||
return db.database.collection('mailboxes').findOneAndUpdate(
|
||||
{
|
||||
_id: session.user.mailbox
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
modifyIndex: 1
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
modseq: mailboxData.modifyIndex,
|
||||
unseen: false
|
||||
},
|
||||
$addToSet: {
|
||||
flags: '\\Seen'
|
||||
}
|
||||
}, {
|
||||
multi: true,
|
||||
w: 1
|
||||
}, err => {
|
||||
},
|
||||
{
|
||||
returnOriginal: false
|
||||
},
|
||||
(err, item) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
messageHandler.notifier.addEntries(
|
||||
mailboxData,
|
||||
false,
|
||||
messages.map(message => {
|
||||
let result = {
|
||||
command: 'FETCH',
|
||||
uid: message.uid,
|
||||
flags: message.flags.concat('\\Seen'),
|
||||
message: new ObjectID(message.id),
|
||||
|
||||
let mailboxData = item && item.value;
|
||||
if (!item) {
|
||||
return callback(new Error('Mailbox does not exist'));
|
||||
}
|
||||
|
||||
db.database.collection('messages').updateMany(
|
||||
{
|
||||
_id: {
|
||||
$in: ids
|
||||
},
|
||||
user: session.user.id,
|
||||
mailbox: mailboxData._id,
|
||||
modseq: {
|
||||
$lt: mailboxData.modifyIndex
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
modseq: mailboxData.modifyIndex,
|
||||
// Indicate that unseen values are changed. Not sure how much though
|
||||
unseenChange: true
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
() => {
|
||||
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
|
||||
callback(null, messages.length);
|
||||
unseen: false
|
||||
},
|
||||
$addToSet: {
|
||||
flags: '\\Seen'
|
||||
}
|
||||
},
|
||||
{
|
||||
multi: true,
|
||||
w: 1
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
messageHandler.notifier.addEntries(
|
||||
mailboxData,
|
||||
false,
|
||||
messages.map(message => {
|
||||
let result = {
|
||||
command: 'FETCH',
|
||||
uid: message.uid,
|
||||
flags: message.flags.concat('\\Seen'),
|
||||
message: new ObjectID(message.id),
|
||||
modseq: mailboxData.modifyIndex,
|
||||
// Indicate that unseen values are changed. Not sure how much though
|
||||
unseenChange: true
|
||||
};
|
||||
return result;
|
||||
}),
|
||||
() => {
|
||||
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
|
||||
callback(null, messages.length);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = done => {
|
||||
|
|
Loading…
Reference in a new issue