mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-03-01 10:24:40 +08:00
Updated address resolving
This commit is contained in:
parent
6cdbf24faf
commit
55d7b87915
11 changed files with 672 additions and 140 deletions
2
api.js
2
api.js
|
@ -22,6 +22,7 @@ const updatesRoutes = require('./lib/api/updates');
|
|||
const authRoutes = require('./lib/api/auth');
|
||||
const autoreplyRoutes = require('./lib/api/autoreply');
|
||||
const submitRoutes = require('./lib/api/submit');
|
||||
const domainaliasRoutes = require('./lib/api/domainaliases');
|
||||
|
||||
const serverOptions = {
|
||||
name: 'Wild Duck API',
|
||||
|
@ -141,6 +142,7 @@ module.exports = done => {
|
|||
authRoutes(db, server, userHandler);
|
||||
autoreplyRoutes(db, server);
|
||||
submitRoutes(db, server, messageHandler);
|
||||
domainaliasRoutes(db, server);
|
||||
|
||||
server.on('error', err => {
|
||||
if (!started) {
|
||||
|
|
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. 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": "2017-11-30T14:32:47.076Z",
"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": "2017-12-01T13:04:16.194Z",
"url": "http://apidocjs.com",
"version": "0.17.6"
}
});
|
||||
|
|
|
@ -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": "2017-11-30T14:32:47.076Z",
"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": "2017-12-01T13:04:16.194Z",
"url": "http://apidocjs.com",
"version": "0.17.6"
}
}
|
||||
|
|
16
indexes.yaml
16
indexes.yaml
|
@ -81,6 +81,22 @@ indexes:
|
|||
key:
|
||||
user: 1
|
||||
|
||||
# Indexes for the domainaliases collection
|
||||
- collection: domainaliases
|
||||
type: users # index applies to users database
|
||||
index:
|
||||
name: domainalias
|
||||
unique: true
|
||||
key:
|
||||
alias: 1
|
||||
|
||||
- collection: domainaliases
|
||||
type: users # index applies to users database
|
||||
index:
|
||||
name: domainlist
|
||||
key:
|
||||
domain: 1
|
||||
|
||||
# Indexes for the application specific passwords collection
|
||||
|
||||
- collection: asps
|
||||
|
|
|
@ -177,7 +177,7 @@ module.exports = (db, server) => {
|
|||
* @apiDescription Add a new email address for an User. Addresses can contain unicode characters.
|
||||
* Dots in usernames are normalized so no need to create both "firstlast@example.com" and "first.last@example.com"
|
||||
*
|
||||
* Special address <code>*@example.com</code> catches all emails to that domain without a registered destination (requires <code>allowWildcard</code> argument)
|
||||
* Special addresses <code>*@example.com</code> and <code>username@*</code> catches all emails to these domains or users without a registered destination (requires <code>allowWildcard</code> argument)
|
||||
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
||||
* @apiHeaderExample {json} Header-Example:
|
||||
* {
|
||||
|
@ -195,7 +195,7 @@ module.exports = (db, server) => {
|
|||
* @apiError error Description of the error
|
||||
*
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i -XPOST http://localhost:8080/users/59fc66a03e54454869460e45/addresses/5a1d4541153888cdcd62a71b \
|
||||
* curl -i -XPOST http://localhost:8080/users/59fc66a03e54454869460e45/addresses \
|
||||
* -H 'Content-type: application/json' \
|
||||
* -d '{
|
||||
* "address": "my.new.address@example.com"
|
||||
|
@ -204,7 +204,8 @@ module.exports = (db, server) => {
|
|||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true
|
||||
* "success": true,
|
||||
* "id": "59ef21aef255ed1d9d790e81"
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
|
@ -231,11 +232,6 @@ module.exports = (db, server) => {
|
|||
|
||||
let address = tools.normalizeAddress(req.params.address);
|
||||
|
||||
if (/[\u0080-\uFFFF]/.test(req.params.address)) {
|
||||
// replace unicode characters in email addresses before validation
|
||||
req.params.address = req.params.address.replace(/[\u0080-\uFFFF]/g, 'x');
|
||||
}
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
|
@ -258,18 +254,29 @@ module.exports = (db, server) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
if ((!result.value.allowWildcard && req.params.address.indexOf('*') === 0) || req.params.address.indexOf('*') > 0) {
|
||||
res.json({
|
||||
error: 'Address can not contain *'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
let wcpos = req.params.address.indexOf('*');
|
||||
|
||||
if (result.value.allowWildcard && req.params.address.indexOf('*') === 0 && main) {
|
||||
res.json({
|
||||
error: 'Main address can not contain *'
|
||||
});
|
||||
return next();
|
||||
if (wcpos >= 0) {
|
||||
if (!result.value.allowWildcard) {
|
||||
res.json({
|
||||
error: 'Address can not contain *'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (/[^@]\*|\*[^@]/.test(result.value) || wcpos !== req.params.address.lastIndexOf('*')) {
|
||||
res.json({
|
||||
error: 'Invalid wildcard address, use "*@domain" or "user@*"'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (main) {
|
||||
res.json({
|
||||
error: 'Main address can not contain *'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
db.users.collection('users').findOne(
|
||||
|
|
490
lib/api/domainaliases.js
Normal file
490
lib/api/domainaliases.js
Normal file
|
@ -0,0 +1,490 @@
|
|||
'use strict';
|
||||
|
||||
const Joi = require('../joi');
|
||||
const MongoPaging = require('mongo-cursor-pagination-node6');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const tools = require('../tools');
|
||||
|
||||
module.exports = (db, server) => {
|
||||
/**
|
||||
* @api {get} /addresses List registered Domain Aliases
|
||||
* @apiName GetAliases
|
||||
* @apiGroup Domain Aliases
|
||||
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
||||
* @apiHeaderExample {json} Header-Example:
|
||||
* {
|
||||
* "X-Access-Token": "59fc66a03e54454869460e45"
|
||||
* }
|
||||
*
|
||||
* @apiParam {String} [query] Partial match of a Domain Alias or Domain name
|
||||
* @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
|
||||
* @apiParam {Number} [previous] Cursor value for previous page, retrieved from <code>previousCursor</code> response value
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {Number} total How many results were found
|
||||
* @apiSuccess {Number} page Current page number. Derived from <code>page</code> query argument
|
||||
* @apiSuccess {String} previousCursor Either a cursor string or false if there are not any previous results
|
||||
* @apiSuccess {String} nextCursor Either a cursor string or false if there are not any next results
|
||||
* @apiSuccess {Object[]} results Aliases listing
|
||||
* @apiSuccess {String} results.id ID of the Domain Alias
|
||||
* @apiSuccess {String} results.alias Domain Alias
|
||||
* @apiSuccess {String} results.domain The domain this alias applies to
|
||||
*
|
||||
* @apiError error Description of the error
|
||||
*
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i http://localhost:8080/domainaliases
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true,
|
||||
* "total": 1,
|
||||
* "page": 1,
|
||||
* "previousCursor": false,
|
||||
* "nextCursor": false,
|
||||
* "results": [
|
||||
* {
|
||||
* "id": "59ef21aef255ed1d9d790e81",
|
||||
* "alias": "example.net",
|
||||
* "domain": "example.com"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "error": "Database error"
|
||||
* }
|
||||
*/
|
||||
server.get({ name: 'domainaliases', path: '/domainaliases' }, (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
query: Joi.string()
|
||||
.trim()
|
||||
.empty('')
|
||||
.max(255),
|
||||
limit: Joi.number()
|
||||
.default(20)
|
||||
.min(1)
|
||||
.max(250),
|
||||
next: Joi.string()
|
||||
.empty('')
|
||||
.mongoCursor()
|
||||
.max(1024),
|
||||
previous: Joi.string()
|
||||
.empty('')
|
||||
.mongoCursor()
|
||||
.max(1024),
|
||||
page: Joi.number().default(1)
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.query, schema, {
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
allowUnknown: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let query = result.value.query;
|
||||
let limit = result.value.limit;
|
||||
let page = result.value.page;
|
||||
let pageNext = result.value.next;
|
||||
let pagePrevious = result.value.previous;
|
||||
|
||||
let filter = query
|
||||
? {
|
||||
$or: [
|
||||
{
|
||||
alias: {
|
||||
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
$options: ''
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
domain: {
|
||||
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
$options: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
: {};
|
||||
|
||||
db.users.collection('domainaliases').count(filter, (err, total) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let opts = {
|
||||
limit,
|
||||
query: filter,
|
||||
fields: {
|
||||
_id: true,
|
||||
alias: true,
|
||||
domain: true
|
||||
},
|
||||
paginatedField: 'alias',
|
||||
sortAscending: true
|
||||
};
|
||||
|
||||
if (pageNext) {
|
||||
opts.next = pageNext;
|
||||
} else if (pagePrevious) {
|
||||
opts.previous = pagePrevious;
|
||||
}
|
||||
|
||||
MongoPaging.find(db.users.collection('domainaliases'), opts, (err, result) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!result.hasPrevious) {
|
||||
page = 1;
|
||||
}
|
||||
|
||||
let response = {
|
||||
success: true,
|
||||
query,
|
||||
total,
|
||||
page,
|
||||
previousCursor: result.hasPrevious ? result.previous : false,
|
||||
nextCursor: result.hasNext ? result.next : false,
|
||||
results: (result.results || []).map(domainData => ({
|
||||
id: domainData._id.toString(),
|
||||
alias: domainData.alias,
|
||||
domain: domainData.domain
|
||||
}))
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
return next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @api {post} /domainaliases/addresses Create new Domain Alias
|
||||
* @apiName PostDomainAlias
|
||||
* @apiGroup Domain Aliases
|
||||
* @apiDescription Add a new Alias for a Domain
|
||||
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
||||
* @apiHeaderExample {json} Header-Example:
|
||||
* {
|
||||
* "X-Access-Token": "59fc66a03e54454869460e45"
|
||||
* }
|
||||
*
|
||||
* @apiParam {String} alias Domain Alias
|
||||
* @apiParam {String} domain Domain name this Alias applies to
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID of the Domain Alias
|
||||
*
|
||||
* @apiError error Description of the error
|
||||
*
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i -XPOST http://localhost:8080/domainaliases \
|
||||
* -H 'Content-type: application/json' \
|
||||
* -d '{
|
||||
* "domain": "example.com",
|
||||
* "alias": "example.org"
|
||||
* }'
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true,
|
||||
* "id": "59ef21aef255ed1d9d790e81"
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "error": "This user does not exist"
|
||||
* }
|
||||
*/
|
||||
server.post('/domainaliases', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
alias: Joi.string()
|
||||
.hostname()
|
||||
.required(),
|
||||
domain: Joi.string()
|
||||
.hostname()
|
||||
.required()
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let alias = tools.normalizeDomain(req.params.alias);
|
||||
let domain = tools.normalizeDomain(req.params.domain);
|
||||
|
||||
db.users.collection('domainaliases').findOne(
|
||||
{
|
||||
alias
|
||||
},
|
||||
(err, aliasData) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (aliasData) {
|
||||
res.json({
|
||||
error: 'This domain alias already exists'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// insert alias address to email address registry
|
||||
db.users.collection('domainaliases').insertOne(
|
||||
{
|
||||
alias,
|
||||
domain,
|
||||
created: new Date()
|
||||
},
|
||||
(err, r) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let insertId = r.insertedId;
|
||||
|
||||
res.json({
|
||||
success: !!insertId,
|
||||
id: insertId
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @api {get} /domainaliases/:alias Request Alias information
|
||||
* @apiName GetDomainAlias
|
||||
* @apiGroup Domain Aliases
|
||||
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
||||
* @apiHeaderExample {json} Header-Example:
|
||||
* {
|
||||
* "X-Access-Token": "59fc66a03e54454869460e45"
|
||||
* }
|
||||
*
|
||||
* @apiParam {String} alias ID of the Alias
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID of the Alias
|
||||
* @apiSuccess {String} alias Alias domain
|
||||
* @apiSuccess {String} domain Alias target
|
||||
* @apiSuccess {String} created Datestring of the time the alias was created
|
||||
*
|
||||
* @apiError error Description of the error
|
||||
*
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i http://localhost:8080/domainaliases/59ef21aef255ed1d9d790e7a
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true,
|
||||
* "id": "59ef21aef255ed1d9d790e7a",
|
||||
* "alias": "example.net",
|
||||
* "domain": "example.com",
|
||||
* "created": "2017-10-24T11:19:10.911Z"
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "error": "This Alias does not exist"
|
||||
* }
|
||||
*/
|
||||
server.get('/domainaliases/:alias', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
alias: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required()
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let alias = new ObjectID(result.value.alias);
|
||||
|
||||
db.users.collection('domainaliases').findOne(
|
||||
{
|
||||
_id: alias
|
||||
},
|
||||
(err, aliasData) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (!aliasData) {
|
||||
res.status(404);
|
||||
res.json({
|
||||
error: 'Invalid or unknown alias'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
id: aliasData._id,
|
||||
alias: aliasData.alias,
|
||||
domain: aliasData.domain,
|
||||
created: aliasData.created
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @api {delete} /domainaliases/:alias Delete an Alias
|
||||
* @apiName DeleteDomainAlias
|
||||
* @apiGroup Domain Aliases
|
||||
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
||||
* @apiHeaderExample {json} Header-Example:
|
||||
* {
|
||||
* "X-Access-Token": "59fc66a03e54454869460e45"
|
||||
* }
|
||||
*
|
||||
* @apiParam {String} alias ID of the Alias
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
*
|
||||
* @apiError error Description of the error
|
||||
*
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i -XDELETE http://localhost:8080/domainaliases/59ef21aef255ed1d9d790e81
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "error": "Database error"
|
||||
* }
|
||||
*/
|
||||
server.del('/domainaliases/:alias', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
alias: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required()
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let alias = new ObjectID(result.value.alias);
|
||||
|
||||
db.users.collection('domainaliases').findOne(
|
||||
{
|
||||
_id: alias
|
||||
},
|
||||
(err, aliasData) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!aliasData) {
|
||||
res.status(404);
|
||||
res.json({
|
||||
error: 'Invalid or unknown email alias identifier'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// delete address from email address registry
|
||||
db.users.collection('domainaliases').deleteOne(
|
||||
{
|
||||
_id: alias
|
||||
},
|
||||
(err, r) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: !!r.deletedCount
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
38
lib/tools.js
38
lib/tools.js
|
@ -45,6 +45,23 @@ function checkRangeQuery(uids, ne) {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeDomain(domain) {
|
||||
domain = (domain || '').toLowerCase().trim();
|
||||
try {
|
||||
if (/^xn--/.test(domain)) {
|
||||
domain = punycode
|
||||
.toUnicode(domain)
|
||||
.normalize('NFC')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
function normalizeAddress(address, withNames) {
|
||||
if (typeof address === 'string') {
|
||||
address = {
|
||||
|
@ -59,24 +76,10 @@ function normalizeAddress(address, withNames) {
|
|||
.normalize('NFC')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
let domain = address.address
|
||||
.substr(address.address.lastIndexOf('@') + 1)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
let encodedDomain = domain;
|
||||
try {
|
||||
if (/^xn--/.test(domain)) {
|
||||
encodedDomain = punycode
|
||||
.toUnicode(domain)
|
||||
.normalize('NFC')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
let addr = user + '@' + encodedDomain;
|
||||
let domain = normalizeDomain(address.address.substr(address.address.lastIndexOf('@') + 1));
|
||||
|
||||
let addr = user + '@' + domain;
|
||||
|
||||
if (withNames) {
|
||||
return {
|
||||
|
@ -338,6 +341,7 @@ function escapeRegexStr(string) {
|
|||
|
||||
module.exports = {
|
||||
normalizeAddress,
|
||||
normalizeDomain,
|
||||
redisConfig,
|
||||
checkRangeQuery,
|
||||
decodeAddresses,
|
||||
|
|
|
@ -65,7 +65,8 @@ class UserHandler {
|
|||
}
|
||||
|
||||
// try to find existing email address
|
||||
let address = tools.normalizeAddress(username);
|
||||
let address = tools.normalizeAddress(username).replace(/\+[^@]*@/, '@');
|
||||
// try exact match
|
||||
this.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'))
|
||||
|
@ -84,28 +85,82 @@ class UserHandler {
|
|||
return next(null, { _id: addressData.user });
|
||||
}
|
||||
|
||||
// try to find a catch-all address
|
||||
this.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: '*' + address.substr(address.indexOf('@'))
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
(err, addressData) => {
|
||||
// try an alias
|
||||
let checkAliases = done => {
|
||||
this.users.collection('domainalias').findOne({ alias: address.substr(address.indexOf('@') + 1) }, (err, aliasData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return done(err);
|
||||
}
|
||||
if (!aliasData) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (!addressData) {
|
||||
return callback(null, false);
|
||||
}
|
||||
this.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + '@' + aliasData.domain
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
done
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
next(null, { _id: addressData.user });
|
||||
checkAliases((err, addressData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
if (addressData) {
|
||||
return next(null, { _id: addressData.user });
|
||||
}
|
||||
|
||||
// try to find a catch-all address
|
||||
this.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: '*' + address.substr(address.indexOf('@'))
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
(err, addressData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addressData) {
|
||||
next(null, { _id: addressData.user });
|
||||
}
|
||||
// try to find a catch-all user
|
||||
this.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + +'@*'
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
user: true
|
||||
}
|
||||
},
|
||||
(err, addressData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!addressData) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
next(null, { _id: addressData.user });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
122
lmtp.js
122
lmtp.js
|
@ -8,11 +8,13 @@ const ObjectID = require('mongodb').ObjectID;
|
|||
const SMTPServer = require('smtp-server').SMTPServer;
|
||||
const tools = require('./lib/tools');
|
||||
const MessageHandler = require('./lib/message-handler');
|
||||
const UserHandler = require('./lib/user-handler');
|
||||
const FilterHandler = require('./lib/filter-handler');
|
||||
const db = require('./lib/db');
|
||||
const certs = require('./lib/certs');
|
||||
|
||||
let messageHandler;
|
||||
let userHandler;
|
||||
let filterHandler;
|
||||
let spamChecks, spamHeaderKeys;
|
||||
|
||||
|
@ -69,90 +71,39 @@ const serverOptions = {
|
|||
// If this method is not set, all addresses are allowed
|
||||
onRcptTo(rcpt, session, callback) {
|
||||
let originalRecipient = tools.normalizeAddress(rcpt.address);
|
||||
let recipient = originalRecipient.replace(/\+[^@]*@/, '@');
|
||||
|
||||
let resolveAddress = next => {
|
||||
db.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: recipient.substr(0, recipient.indexOf('@')).replace(/\./g, '') + recipient.substr(recipient.indexOf('@'))
|
||||
},
|
||||
(err, address) => {
|
||||
if (err) {
|
||||
log.error('LMTP', err);
|
||||
return callback(new Error('Database error'));
|
||||
}
|
||||
if (address) {
|
||||
return next(null, address);
|
||||
}
|
||||
|
||||
db.users.collection('addresses').findOne(
|
||||
{
|
||||
addrview: '*' + recipient.substr(recipient.indexOf('@'))
|
||||
},
|
||||
(err, address) => {
|
||||
if (err) {
|
||||
log.error('LMTP', err);
|
||||
return callback(new Error('Database error'));
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return callback(new Error('Unknown recipient'));
|
||||
}
|
||||
|
||||
next(null, address);
|
||||
}
|
||||
);
|
||||
userHandler.get(
|
||||
originalRecipient,
|
||||
{
|
||||
name: true,
|
||||
forwards: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
autoreply: true,
|
||||
encryptMessages: true,
|
||||
encryptForwarded: true,
|
||||
pubKey: true
|
||||
},
|
||||
(err, userData) => {
|
||||
if (err) {
|
||||
log.error('LMTP', err);
|
||||
return callback(new Error('Database error'));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
resolveAddress((err, address) => {
|
||||
if (err) {
|
||||
log.error('LMTP', err);
|
||||
return callback(new Error('Database error'));
|
||||
}
|
||||
if (!address) {
|
||||
return callback(new Error('Unknown recipient'));
|
||||
}
|
||||
|
||||
db.users.collection('users').findOne(
|
||||
{
|
||||
_id: address.user
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
name: true,
|
||||
forwards: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
autoreply: true,
|
||||
encryptMessages: true,
|
||||
encryptForwarded: true,
|
||||
pubKey: true
|
||||
}
|
||||
},
|
||||
(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 = [];
|
||||
}
|
||||
|
||||
session.users.push({
|
||||
recipient: originalRecipient,
|
||||
user
|
||||
});
|
||||
|
||||
callback();
|
||||
if (!userData) {
|
||||
return callback(new Error('Unknown recipient'));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!session.users) {
|
||||
session.users = [];
|
||||
}
|
||||
|
||||
session.users.push({
|
||||
recipient: originalRecipient,
|
||||
user: userData
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Handle message stream
|
||||
|
@ -264,6 +215,13 @@ module.exports = done => {
|
|||
attachments: config.attachments
|
||||
});
|
||||
|
||||
userHandler = new UserHandler({
|
||||
database: db.database,
|
||||
users: db.users,
|
||||
redis: db.redis,
|
||||
authlogExpireDays: config.log.authlogExpireDays
|
||||
});
|
||||
|
||||
filterHandler = new FilterHandler({
|
||||
database: db.database,
|
||||
users: db.users,
|
||||
|
|
Loading…
Reference in a new issue