mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-30 08:54:34 +08:00
fixed broken imap
This commit is contained in:
parent
c2dc6b488d
commit
8f6a367e8e
7 changed files with 1563 additions and 1155 deletions
32
README.md
32
README.md
|
@ -19,6 +19,38 @@ Run the server
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
|
## Create user
|
||||||
|
|
||||||
|
Users can be created with HTTP requests
|
||||||
|
|
||||||
|
### POST /user/create
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
|
||||||
|
* **username** is an email address of the user
|
||||||
|
* **password** is the password for the user
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -XPOST "http://localhost:8080/user/create" -H 'content-type: application/json' -d '{
|
||||||
|
"username": "username@example.com",
|
||||||
|
"password": "secretpass"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The response for successful operation should look like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"id": "58bd6815dddb5ac5063d3590",
|
||||||
|
"username": "username@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After you have created an user you can use these credentials to log in to the IMAP server. Additionally the LMTP server starts accepting mail for this email address.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Wild Duck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html).
|
Wild Duck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html).
|
||||||
|
|
179
api.js
Normal file
179
api.js
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('config');
|
||||||
|
const restify = require('restify');
|
||||||
|
const log = require('npmlog');
|
||||||
|
const mongodb = require('mongodb');
|
||||||
|
const MongoClient = mongodb.MongoClient;
|
||||||
|
const Joi = require('joi');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const punycode = require('punycode');
|
||||||
|
|
||||||
|
let database;
|
||||||
|
|
||||||
|
const server = restify.createServer();
|
||||||
|
|
||||||
|
server.use(restify.bodyParser({
|
||||||
|
maxBodySize: 0,
|
||||||
|
mapParams: true,
|
||||||
|
mapFiles: false,
|
||||||
|
overrideParams: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.post('/user/create', (req, res, next) => {
|
||||||
|
const schema = Joi.object().keys({
|
||||||
|
username: Joi.string().email().required(),
|
||||||
|
password: Joi.string().min(3).max(100).required()
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Joi.validate(req.params, schema, {
|
||||||
|
abortEarly: false,
|
||||||
|
convert: true,
|
||||||
|
allowUnknown: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
res.json({
|
||||||
|
error: result.error.message
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = normalizeAddress(result.value.username);
|
||||||
|
let password = result.value.password;
|
||||||
|
|
||||||
|
database.collection('users').findOne({
|
||||||
|
username
|
||||||
|
}, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return res.json({
|
||||||
|
error: 'MongoDB Error: ' + err.message,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
return res.json({
|
||||||
|
error: 'This username already exists',
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = bcrypt.hashSync(password, 8);
|
||||||
|
database.collection('users').insertOne({
|
||||||
|
username,
|
||||||
|
password: hash
|
||||||
|
}, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return res.json({
|
||||||
|
error: 'MongoDB Error: ' + err.message,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let uidValidity = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
database.collection('mailboxes').insertMany([{
|
||||||
|
username,
|
||||||
|
path: 'INBOX',
|
||||||
|
uidValidity,
|
||||||
|
uidNext: 1,
|
||||||
|
modifyIndex: 0,
|
||||||
|
subscribed: true
|
||||||
|
}, {
|
||||||
|
username,
|
||||||
|
path: 'Sent Mail',
|
||||||
|
specialUse: '\\Sent',
|
||||||
|
uidValidity,
|
||||||
|
uidNext: 1,
|
||||||
|
modifyIndex: 0,
|
||||||
|
subscribed: true
|
||||||
|
}, {
|
||||||
|
username,
|
||||||
|
path: 'Trash',
|
||||||
|
specialUse: '\\Trash',
|
||||||
|
uidValidity,
|
||||||
|
uidNext: 1,
|
||||||
|
modifyIndex: 0,
|
||||||
|
subscribed: true
|
||||||
|
}, {
|
||||||
|
username,
|
||||||
|
path: 'Junk',
|
||||||
|
specialUse: '\\Junk',
|
||||||
|
uidValidity,
|
||||||
|
uidNext: 1,
|
||||||
|
modifyIndex: 0,
|
||||||
|
subscribed: true
|
||||||
|
}], {
|
||||||
|
w: 1,
|
||||||
|
ordered: false
|
||||||
|
}, err => {
|
||||||
|
if (err) {
|
||||||
|
return res.json({
|
||||||
|
error: 'MongoDB Error: ' + err.message,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
id: result.insertedId,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizeAddress(address, withNames) {
|
||||||
|
if (typeof address === 'string') {
|
||||||
|
address = {
|
||||||
|
address
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!address || !address.address) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let user = address.address.substr(0, address.address.lastIndexOf('@'));
|
||||||
|
let domain = address.address.substr(address.address.lastIndexOf('@') + 1);
|
||||||
|
let addr = user.trim() + '@' + punycode.toASCII(domain.toLowerCase().trim());
|
||||||
|
|
||||||
|
if (withNames) {
|
||||||
|
return {
|
||||||
|
name: address.name || '',
|
||||||
|
address: addr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (imap, done) => {
|
||||||
|
MongoClient.connect(config.mongo, (err, mongo) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('LMTP', 'Could not initialize MongoDB: %s', err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
database = mongo;
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
server.on('error', err => {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
log.error('API', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.api.port, config.api.host, () => {
|
||||||
|
if (started) {
|
||||||
|
return server.close();
|
||||||
|
}
|
||||||
|
started = true;
|
||||||
|
log.info('API', 'Server listening on %s:%s', config.api.host || '0.0.0.0', config.api.port);
|
||||||
|
done(null, server);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,17 +5,20 @@ module.exports = {
|
||||||
level: 'silly'
|
level: 'silly'
|
||||||
},
|
},
|
||||||
|
|
||||||
imap: {
|
|
||||||
port: 9993,
|
|
||||||
host: '127.0.0.1',
|
|
||||||
maxUnflaggedMessages: 10
|
|
||||||
},
|
|
||||||
|
|
||||||
mongo: 'mongodb://127.0.0.1:27017/wildduck',
|
mongo: 'mongodb://127.0.0.1:27017/wildduck',
|
||||||
|
|
||||||
mx: {
|
imap: {
|
||||||
port: 2525,
|
port: 9993,
|
||||||
|
host: '127.0.0.1'
|
||||||
|
},
|
||||||
|
|
||||||
|
lmtp: {
|
||||||
|
port: 2424,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
maxMB: 2
|
maxMB: 5
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
port: 8080
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
192
lmtp.js
Normal file
192
lmtp.js
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('config');
|
||||||
|
const log = require('npmlog');
|
||||||
|
const SMTPServer = require('smtp-server').SMTPServer;
|
||||||
|
const mongodb = require('mongodb');
|
||||||
|
const MongoClient = mongodb.MongoClient;
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const punycode = require('punycode');
|
||||||
|
|
||||||
|
let imapServer;
|
||||||
|
let database;
|
||||||
|
|
||||||
|
const server = new SMTPServer({
|
||||||
|
|
||||||
|
// log to console
|
||||||
|
logger: {
|
||||||
|
info(...args) {
|
||||||
|
args.shift();
|
||||||
|
log.info('LMTP', ...args);
|
||||||
|
},
|
||||||
|
debug(...args) {
|
||||||
|
args.shift();
|
||||||
|
log.silly('LMTP', ...args);
|
||||||
|
},
|
||||||
|
error(...args) {
|
||||||
|
args.shift();
|
||||||
|
log.error('LMTP', ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
name: false,
|
||||||
|
lmtp: true,
|
||||||
|
|
||||||
|
// not required but nice-to-have
|
||||||
|
banner: 'Welcome to Wild Duck Mail Agent',
|
||||||
|
|
||||||
|
// disable STARTTLS to allow authentication in clear text mode
|
||||||
|
disabledCommands: ['AUTH', 'STARTTLS'],
|
||||||
|
|
||||||
|
// Accept messages up to 10 MB
|
||||||
|
size: config.lmtp.maxMB * 1024 * 1024,
|
||||||
|
|
||||||
|
// Validate RCPT TO envelope address. Example allows all addresses that do not start with 'deny'
|
||||||
|
// If this method is not set, all addresses are allowed
|
||||||
|
onRcptTo(address, session, callback) {
|
||||||
|
let username = normalizeAddress(address.address);
|
||||||
|
|
||||||
|
database.collection('users').findOne({
|
||||||
|
username
|
||||||
|
}, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('LMTP', err);
|
||||||
|
return callback(new Error('Database error'));
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
return callback(new Error('Unknown recipient'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.users) {
|
||||||
|
session.users = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
session.users.add(username);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle message stream
|
||||||
|
onData(stream, session, callback) {
|
||||||
|
let chunks = [];
|
||||||
|
let chunklen = 0;
|
||||||
|
let hash = crypto.createHash('md5');
|
||||||
|
stream.on('readable', () => {
|
||||||
|
let chunk;
|
||||||
|
while ((chunk = stream.read()) !== null) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
chunklen += chunk.length;
|
||||||
|
hash.update(chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('error', err => {
|
||||||
|
log.error('LMTP', err);
|
||||||
|
callback(new Error('Error reading from stream'));
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('end', () => {
|
||||||
|
let err;
|
||||||
|
if (stream.sizeExceeded) {
|
||||||
|
err = new Error('Error: message exceeds fixed maximum message size ' + config.lmtp.maxMB + ' MB');
|
||||||
|
err.responseCode = 552;
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.users || !session.users.size) {
|
||||||
|
return callback(new Error('Nowhere to save the mail to'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = Array.from(session.users);
|
||||||
|
let stored = 0;
|
||||||
|
let storeNext = () => {
|
||||||
|
if (stored >= users.length) {
|
||||||
|
return callback(null, 'Message queued as ' + hash.digest('hex').toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = users[stored++];
|
||||||
|
|
||||||
|
// add Delivered-To
|
||||||
|
let header = Buffer.from('Delivered-To: ' + username + '\r\n');
|
||||||
|
chunks.unshift(header);
|
||||||
|
chunklen += header.length;
|
||||||
|
|
||||||
|
imapServer.addToMailbox(username, 'INBOX', {
|
||||||
|
source: 'LMTP',
|
||||||
|
from: normalizeAddress(session.envelope.mailFrom && session.envelope.mailFrom.address || ''),
|
||||||
|
to: session.envelope.rcptTo.map(item => normalizeAddress(item.address)),
|
||||||
|
origin: session.remoteAddress,
|
||||||
|
originhost: session.clientHostname,
|
||||||
|
transhost: session.hostNameAppearsAs,
|
||||||
|
transtype: session.transmissionType,
|
||||||
|
time: Date.now()
|
||||||
|
}, false, [], Buffer.concat(chunks, chunklen), err => {
|
||||||
|
// remove Delivered-To
|
||||||
|
chunks.shift();
|
||||||
|
chunklen -= header.length;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
log.error('LMTP', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
storeNext();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
storeNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizeAddress(address, withNames) {
|
||||||
|
if (typeof address === 'string') {
|
||||||
|
address = {
|
||||||
|
address
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!address || !address.address) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let user = address.address.substr(0, address.address.lastIndexOf('@'));
|
||||||
|
let domain = address.address.substr(address.address.lastIndexOf('@') + 1);
|
||||||
|
let addr = user.trim() + '@' + punycode.toASCII(domain.toLowerCase().trim());
|
||||||
|
|
||||||
|
if (withNames) {
|
||||||
|
return {
|
||||||
|
name: address.name || '',
|
||||||
|
address: addr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (imap, done) => {
|
||||||
|
MongoClient.connect(config.mongo, (err, mongo) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('LMTP', 'Could not initialize MongoDB: %s', err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
database = mongo;
|
||||||
|
imapServer = imap;
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
server.on('error', err => {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
log.error('LMTP', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.lmtp.port, config.lmtp.host, () => {
|
||||||
|
if (started) {
|
||||||
|
return server.close();
|
||||||
|
}
|
||||||
|
started = true;
|
||||||
|
done(null, server);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "wildduck",
|
"name": "wildduck",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "IMAP server built with Node.js and MongoDB",
|
"description": "IMAP server built with Node.js and MongoDB",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -20,19 +20,21 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"addressparser": "^1.0.1",
|
"addressparser": "^1.0.1",
|
||||||
"clone": "^1.0.2",
|
|
||||||
"libbase64": "^0.1.0",
|
|
||||||
"nodemailer-fetch": "^1.6.0",
|
|
||||||
"utf7": "^1.0.2",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"clone": "^1.0.2",
|
||||||
"config": "^1.25.1",
|
"config": "^1.25.1",
|
||||||
|
"joi": "^10.2.2",
|
||||||
|
"libbase64": "^0.1.0",
|
||||||
"mailparser": "^2.0.2",
|
"mailparser": "^2.0.2",
|
||||||
"mongodb": "^2.2.24",
|
"mongodb": "^2.2.24",
|
||||||
|
"nodemailer-fetch": "^1.6.0",
|
||||||
"npmlog": "^4.0.2",
|
"npmlog": "^4.0.2",
|
||||||
"redis": "^2.6.5",
|
"redis": "^2.6.5",
|
||||||
|
"restify": "^4.3.0",
|
||||||
"smtp-server": "^2.0.2",
|
"smtp-server": "^2.0.2",
|
||||||
"uuid": "^3.0.1",
|
"toml": "^2.3.2",
|
||||||
"toml": "^2.3.2"
|
"utf7": "^1.0.2",
|
||||||
|
"uuid": "^3.0.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
Loading…
Add table
Reference in a new issue