From d19e9b062e2f493cff03c4534197830e778b2ac5 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Fri, 3 Nov 2017 14:11:59 +0200 Subject: [PATCH] support tags --- imap.js | 2 +- indexes.yaml | 8 ++- lib/api/users.js | 145 +++++++++++++++++++++++++++++++++++++------- lib/user-handler.js | 23 +++++-- package.json | 6 +- 5 files changed, 154 insertions(+), 30 deletions(-) diff --git a/imap.js b/imap.js index b9ffd123..f7785660 100644 --- a/imap.js +++ b/imap.js @@ -125,7 +125,7 @@ function clearExpiredMessages() { }); }; - if (config.imap.disableRetention) { + if (!config.imap.disableRetention) { // delete all attachments that do not have any active links to message objects return messageHandler.attachmentStorage.deleteOrphaned(() => done(null, true)); } diff --git a/indexes.yaml b/indexes.yaml index 0ca3fc37..8a1f712a 100644 --- a/indexes.yaml +++ b/indexes.yaml @@ -53,9 +53,15 @@ indexes: key: ns: 1 unameview: 1 +- collection: users + type: users # index applies to users database + index: + name: users + key: + tagsview: 1 + sparse: true # Indexes for the addresses collection - - collection: addresses type: users # index applies to users database index: diff --git a/lib/api/users.js b/lib/api/users.js index 6311c28e..a00a561f 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -17,12 +17,16 @@ module.exports = (db, server, userHandler) => { const schema = Joi.object().keys({ query: Joi.string() .empty('') - .alphanum() .lowercase() .max(128), - onlyAddresses: Joi.boolean() - .truthy(['Y', 'true', 'yes', 1]) - .default(false), + tags: Joi.string() + .trim() + .empty('') + .max(1024), + requiredTags: Joi.string() + .trim() + .empty('') + .max(1024), limit: Joi.number() .default(20) .min(1) @@ -59,24 +63,60 @@ module.exports = (db, server, userHandler) => { let filter = query ? { - address: { - $regex: query.replace(/\./g, ''), - $options: '' - } + $or: [ + { + address: { + $regex: query.replace(/\./g, ''), + $options: '' + } + }, + { + unameview: { + $regex: query.replace(/\./g, ''), + $options: '' + } + } + ] } : {}; - if (result.value.onlyAddresses) { - if (filter.address) { - filter.$and = [].concat(filter.$and || []).concat({ address: filter.address }); - delete filter.address; - filter.$and.push({ - address: { $ne: '' } - }); - } else { - filter.address = { $ne: '' }; - } + let tagSeen = new Set(); + + let requiredTags = (result.value.requiredTags || '') + .split(',') + .map(tag => tag.toLowerCase().trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag)) { + tagSeen.add(tag); + return true; + } + return false; + }); + + let tags = (result.value.tags || '') + .split(',') + .map(tag => tag.toLowerCase().trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag)) { + tagSeen.add(tag); + return true; + } + return false; + }); + + let tagsview = {}; + if (requiredTags.length) { + tagsview.$all = requiredTags; } + if (tags.length) { + tagsview.$in = tags; + } + + if (requiredTags.length || tags.length) { + filter.tagsview = tagsview; + } + + console.log(require('util').inspect(filter, false, 22)); db.users.collection('users').count(filter, (err, total) => { if (err) { @@ -94,11 +134,14 @@ module.exports = (db, server, userHandler) => { username: true, name: true, address: true, + tags: true, storageUsed: true, forward: true, targetUrl: true, quota: true, + activated: true, disabled: true, + password: true, encryptMessages: true, encryptForwarded: true }, @@ -135,6 +178,7 @@ module.exports = (db, server, userHandler) => { username: userData.username, name: userData.name, address: userData.address, + tags: userData.tags || [], forward: userData.forward, targetUrl: userData.targetUrl, encryptMessages: !!userData.encryptMessages, @@ -143,6 +187,8 @@ module.exports = (db, server, userHandler) => { allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024, used: Math.max(Number(userData.storageUsed) || 0, 0) }, + hasPasswordSet: !!userData.password, + activated: userData.activated, disabled: userData.disabled })) }; @@ -195,6 +241,12 @@ module.exports = (db, server, userHandler) => { .min(0) .default(0), + tags: Joi.array().items( + Joi.string() + .trim() + .max(128) + ), + pubKey: Joi.string() .empty('') .trim() @@ -239,6 +291,23 @@ module.exports = (db, server, userHandler) => { result.value.pubKey = ''; } + if (result.value.tags) { + let tagSeen = new Set(); + let tags = result.value.tags + .map(tag => tag.trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag.toLowerCase())) { + tagSeen.add(tag.toLowerCase()); + return true; + } + return false; + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + + result.value.tags = tags; + result.value.tagsview = tags.map(tag => tag.toLowerCase()); + } + checkPubKey(result.value.pubKey, err => { if (err) { res.json({ @@ -368,6 +437,8 @@ module.exports = (db, server, userHandler) => { } }, + tags: userData.tags || [], + hasPasswordSet: !!userData.password, activated: userData.activated, disabled: userData.disabled }); @@ -425,6 +496,12 @@ module.exports = (db, server, userHandler) => { recipients: Joi.number().min(0), forwards: Joi.number().min(0), + tags: Joi.array().items( + Joi.string() + .trim() + .max(128) + ), + disabled: Joi.boolean() .empty('') .truthy(['Y', 'true', 'yes', 1]), @@ -473,6 +550,22 @@ module.exports = (db, server, userHandler) => { result.value.pubKey = ''; } + if (result.value.tags) { + let tagSeen = new Set(); + let tags = result.value.tags + .map(tag => tag.trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag.toLowerCase())) { + tagSeen.add(tag.toLowerCase()); + return true; + } + return false; + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + result.value.tags = tags; + result.value.tagsview = tags.map(tag => tag.toLowerCase()); + } + checkPubKey(result.value.pubKey, err => { if (err) { res.json({ @@ -550,7 +643,12 @@ module.exports = (db, server, userHandler) => { .hex() .lowercase() .length(24) - .required() + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); const result = Joi.validate(req.params, schema, { @@ -668,7 +766,12 @@ module.exports = (db, server, userHandler) => { .hex() .lowercase() .length(24) - .required() + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); const result = Joi.validate(req.params, schema, { @@ -685,7 +788,7 @@ module.exports = (db, server, userHandler) => { let user = new ObjectID(result.value.user); - userHandler.reset(user, (err, password) => { + userHandler.reset(user, result.value, (err, password) => { if (err) { res.json({ error: err.message diff --git a/lib/user-handler.js b/lib/user-handler.js index 755e2c54..480051cb 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -519,7 +519,8 @@ class UserHandler { // Users with an empty password can not log in let hash = data.password ? bcrypt.hashSync(data.password, consts.BCRYPT_ROUNDS) : ''; let id = new ObjectID(); - this.users.collection('users').insertOne({ + + userData = { _id: id, username: data.username, @@ -564,7 +565,13 @@ class UserHandler { // until setup value is not true, this account is not usable activated: false, disabled: true - }, err => { + }; + + if (data.tags && data.tags.length) { + userData.tags = data.tags; + } + + this.users.collection('users').insertOne(userData, err => { if (err) { log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message); @@ -773,7 +780,7 @@ class UserHandler { }); } - reset(user, callback) { + reset(user, data, callback) { let password = generatePassword.generate({ length: 12, uppercase: true, @@ -804,7 +811,15 @@ class UserHandler { return callback(new Error('Could not update user ' + user)); } - return callback(null, password); + return this.logAuthEvent( + user, + { + action: 'reset', + sess: data.session, + ip: data.ip + }, + () => callback(null, password) + ); }); } diff --git a/package.json b/package.json index b6f842a0..cf9ebe7d 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,13 @@ "linkify-it": "2.0.3", "mailsplit": "4.0.2", "mobileconfig": "2.1.0", - "mongo-cursor-pagination": "5.0.0", + "mongo-cursor-pagination": "6.0.0", "mongodb": "2.2.33", "nodemailer": "4.3.1", "npmlog": "4.1.2", "openpgp": "2.5.12", "qrcode": "0.9.0", - "restify": "6.2.3", + "restify": "6.3.0", "seq-index": "1.1.0", "smtp-server": "3.3.0", "speakeasy": "2.0.0", @@ -65,7 +65,7 @@ "url": "git://github.com/wildduck-email/wildduck.git" }, "optionalDependencies": { - "@ronomon/crypto-async": "2.2.0", + "@ronomon/crypto-async": "2.2.1", "modern-syslog": "1.1.4" } }