mirror of
				https://github.com/nodemailer/wildduck.git
				synced 2025-11-01 00:56:02 +08:00 
			
		
		
		
	v1.17.0
This commit is contained in:
		
							parent
							
								
									e5919187a4
								
							
						
					
					
						commit
						feda41d58b
					
				
					 11 changed files with 281 additions and 133 deletions
				
			
		
							
								
								
									
										23
									
								
								api.js
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								api.js
									
										
									
									
									
								
							|  | @ -12,6 +12,7 @@ const ImapNotifier = require('./lib/imap-notifier'); | |||
| const db = require('./lib/db'); | ||||
| const certs = require('./lib/certs'); | ||||
| const tools = require('./lib/tools'); | ||||
| const consts = require('./lib/consts'); | ||||
| const crypto = require('crypto'); | ||||
| const Gelf = require('gelf'); | ||||
| const os = require('os'); | ||||
|  | @ -156,7 +157,7 @@ server.use((req, res, next) => { | |||
| 
 | ||||
| server.use(restify.plugins.gzipResponse()); | ||||
| 
 | ||||
| server.use(restify.plugins.queryParser()); | ||||
| server.use(restify.plugins.queryParser({ allowDots: true })); | ||||
| server.use( | ||||
|     restify.plugins.bodyParser({ | ||||
|         maxBodySize: 0, | ||||
|  | @ -240,19 +241,18 @@ server.use( | |||
|                         .digest('hex'); | ||||
| 
 | ||||
|                     if (signature !== tokenData.s) { | ||||
|                         // rogue token
 | ||||
|                         // rogue token or invalidated secret
 | ||||
|                         try { | ||||
|                             await db.redis | ||||
|                                 .multi() | ||||
|                                 .del('tn:token:' + tokenHash) | ||||
|                                 .srem('tn:user:' + tokenData.user, tokenHash) | ||||
|                                 .exec(); | ||||
|                         } catch (err) { | ||||
|                             // ignore
 | ||||
|                         } | ||||
|                     } else if (tokenData.ttl && isNaN(tokenData.ttl) && Number(tokenData.ttl) > 0) { | ||||
|                         let tokenTTL = Number(tokenData.ttl); | ||||
|                         let tokenLifetime = config.api.accessControl.tokenLifetime || 30 * 24 * 3600; | ||||
|                         let tokenLifetime = config.api.accessControl.tokenLifetime || consts.ACCESS_TOKEN_MAX_LIFETIME; | ||||
| 
 | ||||
|                         // check if token is not too old
 | ||||
|                         if (tokenLifetime < (Date.now() - Number(tokenData.created)) / 1000) { | ||||
|  | @ -261,13 +261,26 @@ server.use( | |||
|                                 await db.redis | ||||
|                                     .multi() | ||||
|                                     .expire('tn:token:' + tokenHash, tokenTTL) | ||||
|                                     .expire('tn:user:' + tokenData.user, tokenTTL) | ||||
|                                     .exec(); | ||||
|                             } catch (err) { | ||||
|                                 // ignore
 | ||||
|                             } | ||||
|                             req.role = tokenData.role; | ||||
|                             req.user = tokenData.user; | ||||
|                             req.accessToken = { | ||||
|                                 hash: tokenHash, | ||||
|                                 user: tokenData.user | ||||
|                             }; | ||||
|                         } else { | ||||
|                             // expired token, clear it
 | ||||
|                             try { | ||||
|                                 await db.redis | ||||
|                                     .multi() | ||||
|                                     .del('tn:token:' + tokenHash) | ||||
|                                     .exec(); | ||||
|                             } catch (err) { | ||||
|                                 // ignore
 | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         req.role = tokenData.role; | ||||
|  |  | |||
|  | @ -12,11 +12,19 @@ secure=false | |||
| 
 | ||||
| [accessControl] | ||||
| # If true then require a valid access token to perform API calls | ||||
| # If a client provides a token then it is validated even if using a token is not required | ||||
| enabled=false | ||||
| 
 | ||||
| # Secret for HMAC | ||||
| # Changing this value invalidates all tokens | ||||
| secret="a secret cat" | ||||
| 
 | ||||
| # Generated access token TTL in seconds. Token TTL gets extended by this value every time the token is used. Defaults to 14 days | ||||
| #tokenTTL=1209600 | ||||
| 
 | ||||
| # Generated access token max lifetime in seconds. Defaults to 180 days | ||||
| #tokenLifetime=15552000 | ||||
| 
 | ||||
| [roles] | ||||
| # @include "roles.json" | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1 +1 @@ | |||
| define({
  "name": "wildduck",
  "version": "1.0.0",
  "description": "WildDuck API docs",
  "title": "WildDuck API",
  "url": "https://api.wildduck.email",
  "sampleUrl": false,
  "defaultVersion": "0.0.0",
  "apidoc": "0.3.0",
  "generator": {
    "name": "apidoc",
    "time": "2019-03-26T14:41:36.494Z",
    "url": "http://apidocjs.com",
    "version": "0.17.7"
  }
}); | ||||
| define({
  "name": "wildduck",
  "version": "1.0.0",
  "description": "WildDuck API docs",
  "title": "WildDuck API",
  "url": "https://api.wildduck.email",
  "sampleUrl": false,
  "defaultVersion": "0.0.0",
  "apidoc": "0.3.0",
  "generator": {
    "name": "apidoc",
    "time": "2019-04-05T12:08:29.034Z",
    "url": "http://apidocjs.com",
    "version": "0.17.7"
  }
}); | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| {
  "name": "wildduck",
  "version": "1.0.0",
  "description": "WildDuck API docs",
  "title": "WildDuck API",
  "url": "https://api.wildduck.email",
  "sampleUrl": false,
  "defaultVersion": "0.0.0",
  "apidoc": "0.3.0",
  "generator": {
    "name": "apidoc",
    "time": "2019-03-26T14:41:36.494Z",
    "url": "http://apidocjs.com",
    "version": "0.17.7"
  }
} | ||||
| {
  "name": "wildduck",
  "version": "1.0.0",
  "description": "WildDuck API docs",
  "title": "WildDuck API",
  "url": "https://api.wildduck.email",
  "sampleUrl": false,
  "defaultVersion": "0.0.0",
  "apidoc": "0.3.0",
  "generator": {
    "name": "apidoc",
    "time": "2019-04-05T12:08:29.034Z",
    "url": "http://apidocjs.com",
    "version": "0.17.7"
  }
} | ||||
|  |  | |||
|  | @ -196,7 +196,7 @@ module.exports = (db, server, userHandler) => { | |||
|                 } catch (err) { | ||||
|                     let response = { | ||||
|                         error: err.message, | ||||
|                         code: 'AuthFailed' || err.code, | ||||
|                         code: err.code || 'AuthFailed', | ||||
|                         id: user.toString() | ||||
|                     }; | ||||
|                     res.json(response); | ||||
|  | @ -213,6 +213,54 @@ module.exports = (db, server, userHandler) => { | |||
|         }) | ||||
|     ); | ||||
| 
 | ||||
|     /** | ||||
|      * @api {delete} /authenticate Invalidate authentication token | ||||
|      * @apiName DeleteAuth | ||||
|      * @apiGroup Authentication | ||||
|      * @apiDescription This method invalidates currently used authentication token. If token is not provided then nothing happens | ||||
|      * @apiHeader {String} X-Access-Token Optional access token if authentication is enabled | ||||
|      * @apiHeaderExample {json} Header-Example: | ||||
|      * { | ||||
|      *   "X-Access-Token": "59fc66a03e54454869460e45" | ||||
|      * } | ||||
|      * | ||||
|      * @apiSuccess {Boolean} success Indicates successful response | ||||
|      * | ||||
|      * @apiError error Description of the error | ||||
|      * @apiError [code] Error code | ||||
|      * | ||||
|      * @apiExample {curl} Example usage: | ||||
|      *     curl -i -XDELETE "http://localhost:8080/authenticate" \ | ||||
|      *     -H 'X-Access-Token: 59fc66a03e54454869460e45' | ||||
|      * | ||||
|      * @apiSuccessExample {json} Success-Response: | ||||
|      *     HTTP/1.1 200 OK | ||||
|      *     { | ||||
|      *       "success": true | ||||
|      *     } | ||||
|      * | ||||
|      */ | ||||
|     server.del( | ||||
|         '/authenticate', | ||||
|         tools.asyncifyJson(async (req, res, next) => { | ||||
|             res.charSet('utf-8'); | ||||
| 
 | ||||
|             if (req.accessToken) { | ||||
|                 try { | ||||
|                     await db.redis | ||||
|                         .multi() | ||||
|                         .del('tn:token:' + req.accessToken.hash) | ||||
|                         .exec(); | ||||
|                 } catch (err) { | ||||
|                     // ignore
 | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             res.json({ success: true }); | ||||
|             return next(); | ||||
|         }) | ||||
|     ); | ||||
| 
 | ||||
|     /** | ||||
|      * @api {get} /users/:user/authlog List authentication Events | ||||
|      * @apiName GetAuthlog | ||||
|  |  | |||
|  | @ -553,6 +553,11 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|      * @apiParam {Boolean} [attachments] If true, then matches only messages with attachments | ||||
|      * @apiParam {Boolean} [flagged] If true, then matches only messages with \Flagged flags | ||||
|      * @apiParam {Boolean} [searchable] If true, then matches messages not in Junk or Trash | ||||
|      * @apiParam {Object} [or] Allows to specify some requests as OR (default is AND). At least one of the values in or block must match | ||||
|      * @apiParam {String} [or.query] Search string, uses MongoDB fulltext index. Covers data from mesage body and also common headers like from, to, subject etc. | ||||
|      * @apiParam {String} [or.from] Partial match for the From: header line | ||||
|      * @apiParam {String} [or.to] Partial match for the To: and Cc: header lines | ||||
|      * @apiParam {String} [or.subject] Partial match for the Subject: header line | ||||
|      * @apiParam {Number} [limit=20] How many records to return | ||||
|      * @apiParam {Number} [page=1] Current page number. Informational only, page numbers start from 1 | ||||
|      * @apiParam {Number} [next] Cursor value for next page, retrieved from <code>nextCursor</code> response value | ||||
|  | @ -596,6 +601,9 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|      * @apiExample {curl} Example usage: | ||||
|      *     curl -i "http://localhost:8080/users/59fc66a03e54454869460e45/search?query=Ryan" | ||||
|      * | ||||
|      * @apiExample {curl} Using OR: | ||||
|      *     curl -i "http://localhost:8080/users/59fc66a03e54454869460e45/search?or.from=Ryan&or.to=Ryan" | ||||
|      * | ||||
|      * @apiSuccessExample {json} Success-Response: | ||||
|      *     HTTP/1.1 200 OK | ||||
|      *     { | ||||
|  | @ -659,6 +667,23 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|                     .hex() | ||||
|                     .length(24) | ||||
|                     .empty(''), | ||||
| 
 | ||||
|                 or: Joi.object().keys({ | ||||
|                     query: Joi.string() | ||||
|                         .trim() | ||||
|                         .max(255) | ||||
|                         .empty(''), | ||||
|                     from: Joi.string() | ||||
|                         .trim() | ||||
|                         .empty(''), | ||||
|                     to: Joi.string() | ||||
|                         .trim() | ||||
|                         .empty(''), | ||||
|                     subject: Joi.string() | ||||
|                         .trim() | ||||
|                         .empty('') | ||||
|                 }), | ||||
| 
 | ||||
|                 query: Joi.string() | ||||
|                     .trim() | ||||
|                     .max(255) | ||||
|  | @ -736,6 +761,10 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|             let user = new ObjectID(result.value.user); | ||||
|             let mailbox = result.value.mailbox ? new ObjectID(result.value.mailbox) : false; | ||||
|             let thread = result.value.thread ? new ObjectID(result.value.thread) : false; | ||||
| 
 | ||||
|             let orTerms = result.value.or || {}; | ||||
|             let orQuery = []; | ||||
| 
 | ||||
|             let query = result.value.query; | ||||
|             let datestart = result.value.datestart || false; | ||||
|             let dateend = result.value.dateend || false; | ||||
|  | @ -791,6 +820,9 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|             if (query) { | ||||
|                 filter.searchable = true; | ||||
|                 filter.$text = { $search: query, $language: 'none' }; | ||||
|             } else if (orTerms.query) { | ||||
|                 filter.searchable = true; | ||||
|                 orQuery.push({ $text: { $search: query, $language: 'none' } }); | ||||
|             } | ||||
| 
 | ||||
|             if (mailbox) { | ||||
|  | @ -845,6 +877,22 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (orTerms.from) { | ||||
|                 let regex = orTerms.from.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||||
|                 orQuery.push({ | ||||
|                     headers: { | ||||
|                         $elemMatch: { | ||||
|                             key: 'from', | ||||
|                             value: { | ||||
|                                 $regex: regex, | ||||
|                                 $options: 'i' | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (filterTo) { | ||||
|                 let regex = filterTo.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||||
|                 if (!filter.$and) { | ||||
|  | @ -879,6 +927,36 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (orTerms.to) { | ||||
|                 let regex = orTerms.to.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||||
| 
 | ||||
|                 orQuery.push({ | ||||
|                     headers: { | ||||
|                         $elemMatch: { | ||||
|                             key: 'to', | ||||
|                             value: { | ||||
|                                 $regex: regex, | ||||
|                                 $options: 'i' | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 orQuery.push({ | ||||
|                     headers: { | ||||
|                         $elemMatch: { | ||||
|                             key: 'cc', | ||||
|                             value: { | ||||
|                                 $regex: regex, | ||||
|                                 $options: 'i' | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (filterSubject) { | ||||
|                 let regex = filterSubject.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||||
|                 if (!filter.$and) { | ||||
|  | @ -898,11 +976,31 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler) => { | |||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (orTerms.subject) { | ||||
|                 let regex = filterSubject.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||||
|                 orQuery.push({ | ||||
|                     headers: { | ||||
|                         $elemMatch: { | ||||
|                             key: 'subject', | ||||
|                             value: { | ||||
|                                 $regex: regex, | ||||
|                                 $options: 'i' | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (filterAttachments) { | ||||
|                 filter.ha = true; | ||||
|                 mailboxNeeded = true; | ||||
|             } | ||||
| 
 | ||||
|             if (orQuery.length) { | ||||
|                 filter.$or = orQuery; | ||||
|             } | ||||
| 
 | ||||
|             if (!mailbox && mailboxNeeded) { | ||||
|                 // generate a list of mailbox ID values
 | ||||
|                 let mailboxes; | ||||
|  |  | |||
|  | @ -90,5 +90,10 @@ module.exports = { | |||
|     // mongdb query TTL limits
 | ||||
|     DB_MAX_TIME_USERS: 1 * 1000, | ||||
|     DB_MAX_TIME_MAILBOXES: 800, | ||||
|     DB_MAX_TIME_MESSAGES: 10 * 1000 | ||||
|     DB_MAX_TIME_MESSAGES: 10 * 1000, | ||||
| 
 | ||||
|     // access token default ttl in seconds (token ttl time is extended every time token is used by this value)
 | ||||
|     ACCESS_TOKEN_DEFAULT_TTL: 14 * 24 * 3600, | ||||
|     // access token can be extended until max lifetime value is reached in seconds
 | ||||
|     ACCESS_TOKEN_MAX_LIFETIME: 180 * 24 * 3600 | ||||
| }; | ||||
|  |  | |||
|  | @ -45,6 +45,12 @@ class UserHandler { | |||
|     } | ||||
| 
 | ||||
|     resolveAddress(address, options, callback) { | ||||
|         this.asyncResolveAddress(address, options) | ||||
|             .catch(err => callback(err)) | ||||
|             .then(result => callback(null, result)); | ||||
|     } | ||||
| 
 | ||||
|     async asyncResolveAddress(address, options) { | ||||
|         options = options || {}; | ||||
|         let wildcard = !!options.wildcard; | ||||
| 
 | ||||
|  | @ -53,8 +59,9 @@ class UserHandler { | |||
|             removeDots: true | ||||
|         }); | ||||
| 
 | ||||
|         let username = address.substr(0, address.indexOf('@')); | ||||
|         let domain = address.substr(address.indexOf('@') + 1); | ||||
|         let atPos = address.indexOf('@'); | ||||
|         let username = address.substr(0, atPos); | ||||
|         let domain = address.substr(atPos + 1); | ||||
| 
 | ||||
|         let projection = { | ||||
|             user: true, | ||||
|  | @ -65,69 +72,53 @@ class UserHandler { | |||
|             projection[key] = true; | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             let addressData; | ||||
|             // try exact match
 | ||||
|         this.users.collection('addresses').findOne( | ||||
|             addressData = await this.users.collection('addresses').findOne( | ||||
|                 { | ||||
|                     addrview: username + '@' + domain | ||||
|                 }, | ||||
|                 { | ||||
|                     projection, | ||||
|                     maxTimeMS: consts.DB_MAX_TIME_USERS | ||||
|             }, | ||||
|             (err, addressData) => { | ||||
|                 if (err) { | ||||
|                     err.code = 'InternalDatabaseError'; | ||||
|                     return callback(err); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|             if (addressData) { | ||||
|                     return callback(null, addressData); | ||||
|                 return addressData; | ||||
|             } | ||||
| 
 | ||||
|                 let aliasDomain; | ||||
|             // try an alias
 | ||||
|                 let checkAliases = done => { | ||||
|                     this.users.collection('domainaliases').findOne( | ||||
|             let aliasDomain; | ||||
|             let aliasData = await this.users.collection('domainaliases').findOne( | ||||
|                 { alias: domain }, | ||||
|                 { | ||||
|                     maxTimeMS: consts.DB_MAX_TIME_USERS | ||||
|                         }, | ||||
|                         (err, aliasData) => { | ||||
|                             if (err) { | ||||
|                                 return done(err); | ||||
|                             } | ||||
|                             if (!aliasData) { | ||||
|                                 return done(); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|             if (aliasData) { | ||||
|                 aliasDomain = aliasData.domain; | ||||
| 
 | ||||
|                             this.users.collection('addresses').findOne( | ||||
|                 addressData = await this.users.collection('addresses').findOne( | ||||
|                     { | ||||
|                         addrview: username + '@' + aliasDomain | ||||
|                     }, | ||||
|                     { | ||||
|                         projection, | ||||
|                         maxTimeMS: consts.DB_MAX_TIME_USERS | ||||
|                                 }, | ||||
|                                 done | ||||
|                             ); | ||||
|                     } | ||||
|                 ); | ||||
|                 }; | ||||
| 
 | ||||
|                 checkAliases((err, addressData) => { | ||||
|                     if (err) { | ||||
|                         err.code = 'InternalDatabaseError'; | ||||
|                         return callback(err); | ||||
|                     } | ||||
| 
 | ||||
|                 if (addressData) { | ||||
|                         return callback(null, addressData); | ||||
|                     return addressData; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!wildcard) { | ||||
|                         return callback(null, false); | ||||
|                 // wildcard not allowed, so there is nothing else to check for
 | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             let query = { | ||||
|  | @ -140,49 +131,36 @@ class UserHandler { | |||
|             } | ||||
| 
 | ||||
|             // try to find a catch-all address
 | ||||
|                     this.users.collection('addresses').findOne( | ||||
|                         query, | ||||
|                         { | ||||
|             addressData = await this.users.collection('addresses').findOne(query, { | ||||
|                 projection, | ||||
|                 maxTimeMS: consts.DB_MAX_TIME_USERS | ||||
|                         }, | ||||
|                         (err, addressData) => { | ||||
|                             if (err) { | ||||
|                                 err.code = 'InternalDatabaseError'; | ||||
|                                 return callback(err); | ||||
|                             } | ||||
|             }); | ||||
| 
 | ||||
|             if (addressData) { | ||||
|                                 return callback(null, addressData); | ||||
|                 return addressData; | ||||
|             } | ||||
| 
 | ||||
|             // try to find a catch-all user (eg. "postmaster@*")
 | ||||
|                             this.users.collection('addresses').findOne( | ||||
|             addressData = await this.users.collection('addresses').findOne( | ||||
|                 { | ||||
|                     addrview: username + '@*' | ||||
|                 }, | ||||
|                 { | ||||
|                     projection, | ||||
|                     maxTimeMS: consts.DB_MAX_TIME_USERS | ||||
|                                 }, | ||||
|                                 (err, addressData) => { | ||||
|                                     if (err) { | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|             if (addressData) { | ||||
|                 return addressData; | ||||
|             } | ||||
|         } catch (err) { | ||||
|             err.code = 'InternalDatabaseError'; | ||||
|                                         return callback(err); | ||||
|             throw err; | ||||
|         } | ||||
| 
 | ||||
|                                     if (!addressData) { | ||||
|                                         return callback(null, false); | ||||
|                                     } | ||||
| 
 | ||||
|                                     return callback(null, addressData); | ||||
|                                 } | ||||
|                             ); | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
|         ); | ||||
|         // no match was found
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -3331,7 +3309,7 @@ class UserHandler { | |||
|             .update(accessToken) | ||||
|             .digest('hex'); | ||||
|         let key = 'tn:token:' + tokenHash; | ||||
|         let ttl = config.api.accessControl.tokenTTL || 3600; | ||||
|         let ttl = config.api.accessControl.tokenTTL || consts.ACCESS_TOKEN_DEFAULT_TTL; | ||||
| 
 | ||||
|         let tokenData = { | ||||
|             user: user.toString(), | ||||
|  | @ -3354,9 +3332,7 @@ class UserHandler { | |||
|         await this.redis | ||||
|             .multi() | ||||
|             .hmset(key, tokenData) | ||||
|             .sadd('tn:user:' + user, tokenHash) | ||||
|             .expire(key, ttl) | ||||
|             .expire('tn:user:' + user, ttl) | ||||
|             .exec(); | ||||
| 
 | ||||
|         return accessToken; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|     "name": "wildduck", | ||||
|     "version": "1.16.2", | ||||
|     "version": "1.17.0", | ||||
|     "description": "IMAP/POP3 server built with Node.js and MongoDB", | ||||
|     "main": "server.js", | ||||
|     "scripts": { | ||||
|  | @ -19,7 +19,7 @@ | |||
|         "apidoc": "0.17.7", | ||||
|         "browserbox": "0.9.1", | ||||
|         "chai": "4.2.0", | ||||
|         "eslint": "5.15.3", | ||||
|         "eslint": "5.16.0", | ||||
|         "eslint-config-nodemailer": "1.2.0", | ||||
|         "eslint-config-prettier": "4.1.0", | ||||
|         "grunt": "1.0.4", | ||||
|  | @ -29,7 +29,7 @@ | |||
|         "grunt-shell-spawn": "0.4.0", | ||||
|         "grunt-wait": "0.3.0", | ||||
|         "icedfrisby": "1.5.0", | ||||
|         "mailparser": "2.5.0", | ||||
|         "mailparser": "2.6.0", | ||||
|         "mocha": "5.2.0", | ||||
|         "request": "2.88.0" | ||||
|     }, | ||||
|  | @ -53,7 +53,7 @@ | |||
|         "libbase64": "1.0.3", | ||||
|         "libmime": "4.0.1", | ||||
|         "libqp": "1.1.0", | ||||
|         "mailsplit": "4.2.4", | ||||
|         "mailsplit": "4.3.1", | ||||
|         "mobileconfig": "2.2.0", | ||||
|         "mongo-cursor-pagination": "7.1.0", | ||||
|         "mongodb": "3.2.2", | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue