addeed /addresses/resolve

This commit is contained in:
Andris Reinman 2018-01-04 12:03:25 +02:00
parent 7e18ad65a3
commit bfdd0f03a6
7 changed files with 450 additions and 173 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-03T11:22:08.500Z", "url": "http://apidocjs.com", "version": "0.17.6" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-04T10:03:09.783Z", "url": "http://apidocjs.com", "version": "0.17.6" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-03T11:22:08.500Z", "url": "http://apidocjs.com", "version": "0.17.6" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-04T10:03:09.783Z", "url": "http://apidocjs.com", "version": "0.17.6" } }

View file

@ -113,12 +113,12 @@ module.exports = (db, server) => {
let filter = query
? {
address: {
// cannot use dotless version as this would break domain search
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
$options: ''
}
}
address: {
// cannot use dotless version as this would break domain search
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
$options: ''
}
}
: {};
db.users.collection('addresses').count(filter, (err, total) => {
@ -1001,6 +1001,11 @@ module.exports = (db, server) => {
* @apiParam {String[]} targets An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25")
* @apiParam {Number} [forwards] Daily allowed forwarding count for this address
* @apiParam {Boolean} [allowWildcard=false] If <code>true</code> then address value can be in the form of <code>*@example.com</code>, otherwise using * is not allowed
* @apiParam {Object} [autoreply] Autoreply information
* @apiParam {Boolean} [autoreply.enabled] If true, then autoreply is enabled for this address
* @apiParam {String} [autoreply.subject] Autoreply subject line
* @apiParam {String} [autoreply.text] Autoreply plaintext content
* @apiParam {String} [autoreply.html] Autoreply HTML content
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
@ -1053,7 +1058,24 @@ module.exports = (db, server) => {
forwards: Joi.number()
.min(0)
.default(0),
allowWildcard: Joi.boolean().truthy(['Y', 'true', 'yes', 1])
allowWildcard: Joi.boolean().truthy(['Y', 'true', 'yes', 1]),
autoreply: Joi.object().keys({
enabled: Joi.boolean()
.truthy(['Y', 'true', 'yes', 1])
.default(true),
subject: Joi.string()
.empty('')
.trim()
.max(128),
text: Joi.string()
.empty('')
.trim()
.max(128 * 1024),
html: Joi.string()
.empty('')
.trim()
.max(128 * 1024)
})
});
const result = Joi.validate(req.params, schema, {
@ -1075,6 +1097,32 @@ module.exports = (db, server) => {
let targets = result.value.targets;
let forwards = result.value.forwards;
if (result.value.autoreply) {
if (!result.value.autoreply.subject && 'subject' in req.params.autoreply) {
result.value.autoreply.subject = '';
}
if (!result.value.autoreply.text && 'text' in req.params.autoreply) {
result.value.autoreply.text = '';
if (!result.value.autoreply.html) {
// make sure we also update html part
result.value.autoreply.html = '';
}
}
if (!result.value.autoreply.html && 'html' in req.autoreply.params) {
result.value.autoreply.html = '';
if (!result.value.autoreply.text) {
// make sure we also update plaintext part
result.value.autoreply.text = '';
}
}
} else {
result.value.autoreply = {
enabled: false
};
}
// needed to resolve users for addresses
let addrlist = [];
let cachedAddrviews = new WeakMap();
@ -1202,6 +1250,7 @@ module.exports = (db, server) => {
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
targets,
forwards,
autoreply: result.value.autoreply,
created: new Date()
},
(err, r) => {
@ -1240,6 +1289,11 @@ module.exports = (db, server) => {
* @apiParam {String} address ID of the Address
* @apiParam {String[]} [targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25"). If set then overwrites previous targets array
* @apiParam {Number} [forwards] Daily allowed forwarding count for this address
* @apiParam {Object} [autoreply] Autoreply information
* @apiParam {Boolean} [autoreply.enabled] If true, then autoreply is enabled for this address
* @apiParam {String} [autoreply.subject] Autoreply subject line
* @apiParam {String} [autoreply.text] Autoreply plaintext content
* @apiParam {String} [autoreply.html] Autoreply HTML content
*
* @apiSuccess {Boolean} success Indicates successful response
*
@ -1283,7 +1337,22 @@ module.exports = (db, server) => {
})
)
.min(1),
forwards: Joi.number().min(0)
forwards: Joi.number().min(0),
autoreply: Joi.object().keys({
enabled: Joi.boolean().truthy(['Y', 'true', 'yes', 1]),
subject: Joi.string()
.empty('')
.trim()
.max(128),
text: Joi.string()
.empty('')
.trim()
.max(128 * 1024),
html: Joi.string()
.empty('')
.trim()
.max(128 * 1024)
})
});
const result = Joi.validate(req.params, schema, {
@ -1306,6 +1375,32 @@ module.exports = (db, server) => {
updates.forwards = result.value.forwards;
}
if (result.value.autoreply) {
if (!result.value.autoreply.subject && 'subject' in req.params.autoreply) {
result.value.autoreply.subject = '';
}
if (!result.value.autoreply.text && 'text' in req.params.autoreply) {
result.value.autoreply.text = '';
if (!result.value.autoreply.html) {
// make sure we also update html part
result.value.autoreply.html = '';
}
}
if (!result.value.autoreply.html && 'html' in req.autoreply.params) {
result.value.autoreply.html = '';
if (!result.value.autoreply.text) {
// make sure we also update plaintext part
result.value.autoreply.text = '';
}
}
Object.keys(result.value.autoreply).forEach(key => {
updates['autoreply.' + key] = result.value.autoreply[key];
});
}
db.users.collection('addresses').findOne(
{
_id: address
@ -1562,6 +1657,11 @@ module.exports = (db, server) => {
* @apiSuccess {Number} limits.forwards.allowed How many messages per 24 hour can be forwarded
* @apiSuccess {Number} limits.forwards.used How many messages are forwarded during current 24 hour period
* @apiSuccess {Number} limits.forwards.ttl Time until the end of current 24 hour period
* @apiSuccess {Object} autoreply Autoreply information
* @apiSuccess {Boolean} autoreply.enabled If true, then autoreply is enabled for this address
* @apiSuccess {String} autoreply.subject Autoreply subject line
* @apiSuccess {String} autoreply.text Autoreply plaintext content
* @apiSuccess {String} autoreply.html Autoreply HTML content
* @apiSuccess {String} created Datestring of the time the address was created
*
* @apiError error Description of the error
@ -1668,6 +1768,7 @@ module.exports = (db, server) => {
ttl: forwardsTtl >= 0 ? forwardsTtl : false
}
},
autoreply: addressData.autoreply || { enabled: false },
created: addressData.created
});
@ -1676,4 +1777,181 @@ module.exports = (db, server) => {
}
);
});
/**
* @api {get} /addresses/resolve/:address Get Address info
* @apiName GetAddressInfo
* @apiGroup Addresses
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} address ID of the Address
* @apiParam {String} address ID of the Address or e-mail address string
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
* @apiSuccess {String} address E-mail address string
* @apiSuccess {String} user ID of the user if the address belongs to an User
* @apiSuccess {String[]} targets List of forwarding targets if this is a Forwarded address
* @apiSuccess {Object} limits Account limits and usage for Forwarded address
* @apiSuccess {Object} limits.forwards Forwarding quota
* @apiSuccess {Number} limits.forwards.allowed How many messages per 24 hour can be forwarded
* @apiSuccess {Number} limits.forwards.used How many messages are forwarded during current 24 hour period
* @apiSuccess {Number} limits.forwards.ttl Time until the end of current 24 hour period
* @apiSuccess {Object} autoreply Autoreply information
* @apiSuccess {Boolean} autoreply.enabled If true, then autoreply is enabled for this address
* @apiSuccess {String} autoreply.subject Autoreply subject line
* @apiSuccess {String} autoreply.text Autoreply plaintext content
* @apiSuccess {String} autoreply.html Autoreply HTML content
* @apiSuccess {String} created Datestring of the time the address was created
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i http://localhost:8080/addresses/resolve/k%C3%A4ru%40j%C3%B5geva.ee
*
* @apiSuccessExample {json} User-Address:
* HTTP/1.1 200 OK
* {
* "success": true,
* "id": "59ef21aef255ed1d9d790e81",
* "address": "user@example.com",
* "user": "59ef21aef255ed1d9d771bb"
* "created": "2017-10-24T11:19:10.911Z"
* }
*
* @apiSuccessExample {json} Forwarded-Address:
* HTTP/1.1 200 OK
* {
* "success": true,
* "id": "59ef21aef255ed1d9d790e81",
* "address": "user@example.com",
* "targets": [
* "my.other.address@example.com"
* ],
* "limits": {
* "forwards": {
* "allowed": 2000,
* "used": 0,
* "ttl": false
* }
* },
* "autoreply": {
* "enabled": false
* },
* "created": "2017-10-24T11:19:10.911Z"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This address does not exist"
* }
*/
server.get('/addresses/resolve/:address', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
address: [
Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
Joi.string().email()
]
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
let query = {};
if (result.value.address.indexOf('@') >= 0) {
let address = tools.normalizeAddress(result.value.address);
query = {
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'))
};
} else {
let address = new ObjectID(result.value.address);
query = {
_id: address
};
}
db.users.collection('addresses').findOne(query, (err, addressData) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!addressData) {
res.status(404);
res.json({
error: 'Invalid or unknown address',
code: 'AddressNotFound'
});
return next();
}
if (addressData.user) {
res.json({
success: true,
id: addressData._id,
address: addressData.address,
user: addressData.user,
created: addressData.created
});
return next();
}
db.redis
.multi()
// sending counters are stored in Redis
.get('wdf:' + addressData._id.toString())
.ttl('wdf:' + addressData._id.toString())
.exec((err, result) => {
if (err) {
// ignore
}
let forwards = Number(addressData.forwards) || config.maxForwards || consts.MAX_FORWARDS;
let forwardsSent = Number(result && result[0] && result[0][1]) || 0;
let forwardsTtl = Number(result && result[1] && result[1][1]) || 0;
res.json({
success: true,
id: addressData._id,
address: addressData.address,
targets: addressData.targets && addressData.targets.map(t => t.value),
limits: {
forwards: {
allowed: forwards,
used: forwardsSent,
ttl: forwardsTtl >= 0 ? forwardsTtl : false
}
},
autoreply: addressData.autoreply || { enabled: false },
created: addressData.created
});
return next();
});
});
});
};

View file

@ -5,171 +5,147 @@ const MessageSplitter = require('./message-splitter');
const consts = require('./consts');
const errors = require('./errors');
module.exports = (options, callback) => {
module.exports = (options, autoreplyData, callback) => {
if (!options.sender || /mailer-daemon|no-?reply/gi.test(options.sender)) {
return callback(null, false);
}
let curtime = new Date();
options.db.database.collection('autoreplies').findOne(
{
user: options.userData._id,
start: {
$lte: curtime
},
end: {
$gte: curtime
}
},
(err, autoreply) => {
if (err) {
return callback(err);
}
// step 1. check if recipient is valid (non special address)
// step 2. check if recipient not in cache list
// step 3. parse headers, check if not automatic message
// step 4. prepare message with special headers (in-reply-to, references, Auto-Submitted)
if (!autoreply || !autoreply.status) {
return callback(null, false);
}
let messageHeaders = false;
let messageSplitter = new MessageSplitter();
// step 1. check if recipient is valid (non special address)
// step 2. check if recipient not in cache list
// step 3. parse headers, check if not automatic message
// step 4. prepare message with special headers (in-reply-to, references, Auto-Submitted)
messageSplitter.once('headers', headers => {
messageHeaders = headers;
let messageHeaders = false;
let messageSplitter = new MessageSplitter();
let autoSubmitted = headers.getFirst('Auto-Submitted');
if (autoSubmitted && autoSubmitted.toLowerCase() !== 'no') {
// skip automatic messages
return callback(null, false);
}
let precedence = headers.getFirst('Precedence');
if (precedence && ['list', 'junk', 'bulk'].includes(precedence.toLowerCase())) {
return callback(null, false);
}
let listUnsubscribe = headers.getFirst('List-Unsubscribe');
if (listUnsubscribe) {
return callback(null, false);
}
let suppressAutoresponse = headers.getFirst('X-Auto-Response-Suppress');
if (suppressAutoresponse && /OOF|AutoReply/i.test(suppressAutoresponse)) {
return callback(null, false);
}
messageSplitter.once('headers', headers => {
messageHeaders = headers;
let autoSubmitted = headers.getFirst('Auto-Submitted');
if (autoSubmitted && autoSubmitted.toLowerCase() !== 'no') {
// skip automatic messages
return callback(null, false);
}
let precedence = headers.getFirst('Precedence');
if (precedence && ['list', 'junk', 'bulk'].includes(precedence.toLowerCase())) {
return callback(null, false);
}
let listUnsubscribe = headers.getFirst('List-Unsubscribe');
if (listUnsubscribe) {
return callback(null, false);
}
let suppressAutoresponse = headers.getFirst('X-Auto-Response-Suppress');
if (suppressAutoresponse && /OOF|AutoReply/i.test(suppressAutoresponse)) {
options.db.redis
.multi()
// delete all old entries
.zremrangebyscore('war:' + autoreplyData._id, '-inf', Date.now() - consts.MAX_AUTOREPLY_INTERVAL)
// add new entry if not present
.zadd('war:' + autoreplyData._id, 'NX', Date.now(), options.sender)
// if no-one touches this key from now, then delete after max interval has passed
.expire('war:' + autoreplyData._id, consts.MAX_AUTOREPLY_INTERVAL)
.exec((err, result) => {
if (err) {
errors.notify(err, { userId: autoreplyData._id });
return callback(null, false);
}
options.db.redis
.multi()
// delete all old entries
.zremrangebyscore('war:' + autoreply._id, '-inf', Date.now() - consts.MAX_AUTOREPLY_INTERVAL)
// add new entry if not present
.zadd('war:' + autoreply._id, 'NX', Date.now(), options.sender)
// if no-one touches this key from now, then delete after max interval has passed
.expire('war:' + autoreply._id, consts.MAX_AUTOREPLY_INTERVAL)
.exec((err, result) => {
if (err) {
errors.notify(err, { userId: options.userData._id });
return callback(null, false);
}
if (!result || !result[1] || !result[1][1]) {
// already responded
return callback(null, false);
}
if (!result || !result[1] || !result[1][1]) {
// already responded
return callback(null, false);
}
// check limiting counters
options.messageHandler.counters.ttlcounter('wda:' + autoreplyData._id, 1, consts.MAX_AUTOREPLIES, false, (err, result) => {
if (err || !result.success) {
return callback(null, false);
}
// check limiting counters
options.messageHandler.counters.ttlcounter('wda:' + options.userData._id, 1, consts.MAX_AUTOREPLIES, false, (err, result) => {
if (err || !result.success) {
return callback(null, false);
let data = {
envelope: {
from: '',
to: options.sender
},
from: {
name: options.userData && options.userData.name,
address: options.recipient
},
to: options.sender,
subject: (autoreplyData.subject && 'Auto: ' + autoreplyData.subject) || {
prepared: true,
value: 'Auto: Re: ' + headers.getFirst('Subject')
},
headers: {
'Auto-Submitted': 'auto-replied',
'X-WD-Autoreply-For': (options.parentId || '').toString()
},
inReplyTo: headers.getFirst('Message-ID'),
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
text: autoreplyData.text,
html: autoreplyData.html
};
let compiler = new MailComposer(data);
let message = options.maildrop.push(
{
parentId: options.parentId,
reason: 'autoreply',
from: '',
to: options.sender,
interface: 'autoreplies'
},
(err, ...args) => {
if (err || !args[0]) {
if (err) {
err.code = err.code || 'ERRCOMPOSE';
}
return callback(err, ...args);
}
let data = {
envelope: {
from: '',
to: options.sender
},
from: {
name: options.userData.name,
address: options.recipient
},
to: options.sender,
subject: autoreply.subject
? 'Auto: ' + autoreply.subject
: {
prepared: true,
value: 'Auto: Re: ' + headers.getFirst('Subject')
},
headers: {
'Auto-Submitted': 'auto-replied',
'X-WD-Autoreply-For': (options.parentId || '').toString()
},
inReplyTo: headers.getFirst('Message-ID'),
references: (headers.getFirst('Message-ID') + ' ' + headers.getFirst('References')).trim(),
text: autoreply.text,
html: autoreply.html
};
let compiler = new MailComposer(data);
let message = options.maildrop.push(
options.db.database.collection('messagelog').insertOne(
{
id: args[0].id,
messageId: args[0].messageId,
parentId: options.parentId,
reason: 'autoreply',
action: 'AUTOREPLY',
from: '',
to: options.sender,
interface: 'autoreplies'
created: new Date()
},
(err, ...args) => {
if (err || !args[0]) {
if (err) {
err.code = err.code || 'ERRCOMPOSE';
}
return callback(err, ...args);
}
options.db.database.collection('messagelog').insertOne(
{
id: args[0].id,
messageId: args[0].messageId,
parentId: options.parentId,
action: 'AUTOREPLY',
from: '',
to: options.sender,
created: new Date()
},
() => callback(err, args && args[0].id)
);
}
() => callback(err, args && args[0].id)
);
}
);
if (message) {
compiler
.compile()
.createReadStream()
.pipe(message);
}
});
});
if (message) {
compiler
.compile()
.createReadStream()
.pipe(message);
}
});
});
});
messageSplitter.on('error', () => false);
messageSplitter.on('data', () => false);
messageSplitter.on('end', () => false);
messageSplitter.on('error', () => false);
messageSplitter.on('data', () => false);
messageSplitter.on('end', () => false);
setImmediate(() => {
let pos = 0;
let writeNextChunk = () => {
if (messageHeaders || pos >= options.chunks.length) {
return messageSplitter.end();
}
let chunk = options.chunks[pos++];
if (!messageSplitter.write(chunk)) {
return messageSplitter.once('drain', writeNextChunk);
} else {
setImmediate(writeNextChunk);
}
};
setImmediate(() => {
let pos = 0;
let writeNextChunk = () => {
if (messageHeaders || pos >= options.chunks.length) {
return messageSplitter.end();
}
let chunk = options.chunks[pos++];
if (!messageSplitter.write(chunk)) {
return messageSplitter.once('drain', writeNextChunk);
} else {
setImmediate(writeNextChunk);
});
}
);
}
};
setImmediate(writeNextChunk);
});
};

View file

@ -380,9 +380,9 @@ class FilterHandler {
targets: forwardTargets.size
? Array.from(forwardTargets).map(row => ({
type: row[1].type,
value: row[1].value
}))
type: row[1].type,
value: row[1].value
}))
: false,
chunks,
@ -405,20 +405,43 @@ class FilterHandler {
return setImmediate(done);
}
autoreply(
let curtime = new Date();
options.db.database.collection('autoreplies').findOne(
{
db: this.db,
maildrop: this.maildrop,
parentId: prepared.id,
userData,
sender,
recipient,
chunks,
chunklen,
messageHandler: this.messageHandler
user: userData._id,
start: {
$lte: curtime
},
end: {
$gte: curtime
}
},
done
(err, autoreplyData) => {
if (err) {
return callback(err);
}
if (!autoreplyData || !autoreplyData.status) {
return callback(null, false);
}
autoreply(
{
db: this.db,
maildrop: this.maildrop,
parentId: prepared.id,
userData,
sender,
recipient,
chunks,
chunklen,
messageHandler: this.messageHandler
},
autoreplyData,
done
);
}
);
};
@ -549,10 +572,10 @@ class FilterHandler {
},
!isEncrypted
? {
// reuse parsed values
mimeTree: messageOpts.prepared.mimeTree,
maildata: messageOpts.maildata
}
// reuse parsed values
mimeTree: messageOpts.prepared.mimeTree,
maildata: messageOpts.maildata
}
: false
);
});