mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-10-25 05:27:35 +08:00
use targets
This commit is contained in:
parent
198f487169
commit
127536799f
15 changed files with 739 additions and 523 deletions
10
docs/api.md
10
docs/api.md
|
|
@ -1,5 +1,7 @@
|
|||
# HTTP API
|
||||
|
||||
**DEPRECATED DOCS**, see https://api.wildduck.email
|
||||
|
||||
WildDuck Mail Server is a scalable IMAP / POP3 server that natively exposes internal data through an HTTP API.
|
||||
|
||||
This API is not meant to be used by end users but your application.
|
||||
|
|
@ -223,8 +225,6 @@ Response for a successful operation:
|
|||
"name": null,
|
||||
"address": "testuser01@example.com",
|
||||
"tags": ["green", "blue"],
|
||||
"forward": [],
|
||||
"targetUrl": "",
|
||||
"encryptMessages": false,
|
||||
"encryptForwarded": false,
|
||||
"quota": {
|
||||
|
|
@ -270,8 +270,6 @@ Response for a successful operation:
|
|||
"encryptForwarded": false,
|
||||
"pubKey": "",
|
||||
"keyInfo": false,
|
||||
"forward": [],
|
||||
"targetUrl": "",
|
||||
"limits": {
|
||||
"quota": {
|
||||
"allowed": 107374182400,
|
||||
|
|
@ -311,8 +309,6 @@ Creates a new user, returns the ID upon success.
|
|||
* **address** is the main email address for the user. If address is not set then a new one is generated based on the username and current domain name
|
||||
* **emptyAddress** if true, then do not set up an address for the user
|
||||
* **name** is the name for the user
|
||||
* **forward** is an array of email addresses to where all messages are forwarded to
|
||||
* **targetUrl** is an URL to where all messages are uploaded to
|
||||
* **quota** is the maximum storage in bytes allowed for this user. If not set then the default value is used
|
||||
* **retention** is the default retention time in ms for mailboxes. Messages in Trash and Junk folders have a capped retention time of 30 days.
|
||||
* **language** is the language code for the user, eg. "en" or "et". Mailbox names for the default mailboxes (eg. "Trash") depend on the language
|
||||
|
|
@ -358,8 +354,6 @@ Updates the properties of an user. Only specify these fields that you want to be
|
|||
* **user** (required) is the ID of the user
|
||||
* **name** is the updated name for the user
|
||||
* **password** is the updated password for the user (do not set if you do not want to change user password)
|
||||
* **forward** is an array of email addresses to where all messages are forwarded to
|
||||
* **targetUrl** is an URL to where all messages are uploaded to
|
||||
* **quota** is the maximum storage in bytes allowed for this user
|
||||
* **retention** is the default retention time in ms for mailboxes. Messages in Trash and Junk folders have a capped retention time of 30 days.
|
||||
* **language** is the language code for the user, eg. "en" or "et"
|
||||
|
|
|
|||
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": "2018-01-16T11:35:19.349Z",
"url": "http://apidocjs.com",
"version": "0.17.6"
}
});
|
||||
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": "2018-01-20T19:38:49.486Z",
"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": "2018-01-16T11:35:19.349Z",
"url": "http://apidocjs.com",
"version": "0.17.6"
}
}
|
||||
{
"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": "2018-01-20T19:38:49.486Z",
"url": "http://apidocjs.com",
"version": "0.17.6"
}
}
|
||||
|
|
|
|||
|
|
@ -1226,7 +1226,9 @@ module.exports = (db, server) => {
|
|||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/]
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
),
|
||||
forwards: Joi.number()
|
||||
|
|
@ -1547,7 +1549,9 @@ module.exports = (db, server) => {
|
|||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/]
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
),
|
||||
forwards: Joi.number().min(0),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"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",
|
||||
"description": "WildDuck API docs",
|
||||
"title": "WildDuck API",
|
||||
"url": "http://localhost:8080"
|
||||
"url": "https://api.wildduck.email"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,19 +189,20 @@ module.exports = (db, server) => {
|
|||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID for the Filter
|
||||
* @apiSuccess {String} name Name of the Filter
|
||||
* @apiSuccess {String} query_from Partial match for the From: header (case insensitive)
|
||||
* @apiSuccess {String} query_to Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiSuccess {String} query_subject Partial match for the Subject: header (case insensitive)
|
||||
* @apiSuccess {String} query_text Fulltext search against message text
|
||||
* @apiSuccess {Bolean} query_ha Does a message have to have an attachment or not
|
||||
* @apiSuccess {Number} query_size Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiSuccess {Bolean} action_seen If true then mark matching messages as Seen
|
||||
* @apiSuccess {Bolean} action_flag If true then mark matching messages as Flagged
|
||||
* @apiSuccess {Bolean} action_delete If true then do not store matching messages
|
||||
* @apiSuccess {Bolean} action_spam If true then store matching messags to Junk Mail folder
|
||||
* @apiSuccess {String} action_mailbox Mailbox ID to store matching messages to
|
||||
* @apiSuccess {String} action_forward An email address where matching messages should be forwarded to
|
||||
* @apiSuccess {String} action_targetUrl An URL where matching messages should be POSTed to
|
||||
* @apiSuccess {Object} query Rules that a message must match
|
||||
* @apiSuccess {String} query.from Partial match for the From: header (case insensitive)
|
||||
* @apiSuccess {String} query.to Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiSuccess {String} query.subject Partial match for the Subject: header (case insensitive)
|
||||
* @apiSuccess {String} query.text Fulltext search against message text
|
||||
* @apiSuccess {Bolean} query.ha Does a message have to have an attachment or not
|
||||
* @apiSuccess {Number} query.size Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiSuccess {Object} action Action to take with a matching message
|
||||
* @apiSuccess {Bolean} action.seen If true then mark matching messages as Seen
|
||||
* @apiSuccess {Bolean} action.flag If true then mark matching messages as Flagged
|
||||
* @apiSuccess {Bolean} action.delete If true then do not store matching messages
|
||||
* @apiSuccess {Bolean} action.spam If true then store matching messags to Junk Mail folder
|
||||
* @apiSuccess {String} action.mailbox Mailbox ID to store matching messages to
|
||||
* @apiSuccess {String[]} action.targets A list of email addresses / HTTP URLs to forward the message to
|
||||
*
|
||||
* @apiError error Description of the error
|
||||
*
|
||||
|
|
@ -214,8 +215,12 @@ module.exports = (db, server) => {
|
|||
* "success": true,
|
||||
* "id": "5a1c0ee490a34c67e266931c",
|
||||
* "created": "2017-11-27T13:11:00.835Z",
|
||||
* "query_from": "Mäger",
|
||||
* "action_seen": true
|
||||
* "query": {
|
||||
* "from": "Mäger"
|
||||
* },
|
||||
* "action": {
|
||||
* "seen": true
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
|
|
@ -300,21 +305,26 @@ module.exports = (db, server) => {
|
|||
success: true,
|
||||
id: filterData._id,
|
||||
name: filterData.name,
|
||||
query: {},
|
||||
action: {},
|
||||
created: filterData.created
|
||||
};
|
||||
|
||||
Object.keys((filterData.query && filterData.query.headers) || {}).forEach(key => {
|
||||
result['query_' + key] = filterData.query.headers[key];
|
||||
result.query[key] = filterData.query.headers[key];
|
||||
});
|
||||
|
||||
Object.keys(filterData.query || {}).forEach(key => {
|
||||
if (key !== 'headers') {
|
||||
result['query_' + key] = filterData.query[key];
|
||||
result.query[key] = filterData.query[key];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(filterData.action || {}).forEach(key => {
|
||||
result['action_' + key] = filterData.action[key];
|
||||
if (key === 'targets') {
|
||||
result.action.targets = filterData.action.targets.map(target => target.value);
|
||||
}
|
||||
result.action[key] = filterData.action[key];
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
|
|
@ -431,19 +441,20 @@ module.exports = (db, server) => {
|
|||
*
|
||||
* @apiParam {String} user Users unique ID.
|
||||
* @apiParam {String} [name] Name of the Filter
|
||||
* @apiParam {String} [query_from] Partial match for the From: header (case insensitive)
|
||||
* @apiParam {String} [query_to] Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiParam {String} [query_subject] Partial match for the Subject: header (case insensitive)
|
||||
* @apiParam {String} [query_text] Fulltext search against message text
|
||||
* @apiParam {Bolean} [query_ha] Does a message have to have an attachment or not
|
||||
* @apiParam {Number} [query_size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiParam {Bolean} [action_seen] If true then mark matching messages as Seen
|
||||
* @apiParam {Bolean} [action_flag] If true then mark matching messages as Flagged
|
||||
* @apiParam {Bolean} [action_delete] If true then do not store matching messages
|
||||
* @apiParam {Bolean} [action_spam] If true then store matching messags to Junk Mail folder
|
||||
* @apiParam {String} [action_mailbox] Mailbox ID to store matching messages to
|
||||
* @apiParam {String} [action_forward] An email address where matching messages should be forwarded to
|
||||
* @apiParam {String} [action_targetUrl] An URL where matching messages should be POSTed to
|
||||
* @apiParam {Object} query Rules that a message must match
|
||||
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
|
||||
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
|
||||
* @apiParam {String} [query.text] Fulltext search against message text
|
||||
* @apiParam {Bolean} [query.ha] Does a message have to have an attachment or not
|
||||
* @apiParam {Number} [query.size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiParam {Object} action Action to take with a matching message
|
||||
* @apiParam {Bolean} [action.seen] If true then mark matching messages as Seen
|
||||
* @apiParam {Bolean} [action.flag] If true then mark matching messages as Flagged
|
||||
* @apiParam {Bolean} [action.delete] If true then do not store matching messages
|
||||
* @apiParam {Bolean} [action.spam] If true then store matching messags to Junk Mail folder
|
||||
* @apiParam {String} [action.mailbox] Mailbox ID to store matching messages to
|
||||
* @apiParam {String[]} [action.targets] A list of email addresses / HTTP URLs to forward the message to
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID for the created Filter
|
||||
|
|
@ -454,8 +465,12 @@ module.exports = (db, server) => {
|
|||
* curl -i -XPOST http://localhost:8080/users/5a1bda70bfbd1442cd96c6f0/filters \
|
||||
* -H 'Content-type: application/json' \
|
||||
* -d '{
|
||||
* "query_from": "Mäger",
|
||||
* "action_seen": true
|
||||
* "query": {
|
||||
* "from": "Mäger"
|
||||
* },
|
||||
* "action": {
|
||||
* "seen": true
|
||||
* }
|
||||
* }'
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
|
|
@ -486,60 +501,64 @@ module.exports = (db, server) => {
|
|||
.max(255)
|
||||
.empty(''),
|
||||
|
||||
query_from: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_to: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_subject: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_text: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_ha: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
query_size: Joi.number().empty(''),
|
||||
|
||||
action_seen: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_flag: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_delete: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_spam: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
|
||||
action_mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.empty(''),
|
||||
action_forward: Joi.string()
|
||||
.email()
|
||||
.empty(''),
|
||||
action_targetUrl: Joi.string()
|
||||
.uri({
|
||||
scheme: ['http', 'https'],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
from: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
to: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
subject: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
text: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
ha: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
size: Joi.number().empty('')
|
||||
})
|
||||
.empty('')
|
||||
.required(),
|
||||
action: Joi.object()
|
||||
.keys({
|
||||
seen: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
flag: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
delete: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
spam: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.empty(''),
|
||||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
)
|
||||
})
|
||||
.required()
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
|
@ -574,48 +593,78 @@ module.exports = (db, server) => {
|
|||
let hasAction = false;
|
||||
|
||||
['from', 'to', 'subject'].forEach(key => {
|
||||
if (result.value['query_' + key]) {
|
||||
filterData.query.headers[key] = result.value['query_' + key].replace(/\s+/g, ' ');
|
||||
if (result.value.query[key]) {
|
||||
filterData.query.headers[key] = result.value.query[key].replace(/\s+/g, ' ');
|
||||
hasQuery = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.value.query_text) {
|
||||
filterData.query.text = result.value.query_text.replace(/\s+/g, ' ');
|
||||
if (result.value.query.text) {
|
||||
filterData.query.text = result.value.query.text.replace(/\s+/g, ' ');
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
if (typeof result.value.query_ha === 'boolean') {
|
||||
filterData.query.ha = result.value.query_ha;
|
||||
if (typeof result.value.query.ha === 'boolean') {
|
||||
filterData.query.ha = result.value.query.ha;
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
if (result.value.query_size) {
|
||||
filterData.query.size = result.value.query_size;
|
||||
if (result.value.query.size) {
|
||||
filterData.query.size = result.value.query.size;
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
['seen', 'flag', 'delete', 'spam'].forEach(key => {
|
||||
if (typeof result.value['action_' + key] === 'boolean') {
|
||||
filterData.action[key] = result.value['action_' + key];
|
||||
if (typeof result.value.action[key] === 'boolean') {
|
||||
filterData.action[key] = result.value.action[key];
|
||||
hasAction = true;
|
||||
}
|
||||
});
|
||||
|
||||
['forward', 'targetUrl'].forEach(key => {
|
||||
if (result.value['action_' + key]) {
|
||||
filterData.action[key] = result.value['action_' + key];
|
||||
hasAction = true;
|
||||
let targets = result.value.action.targets;
|
||||
|
||||
if (targets) {
|
||||
for (let i = 0, len = targets.length; i < len; i++) {
|
||||
let target = targets[i];
|
||||
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
|
||||
// email
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'mail',
|
||||
value: target
|
||||
};
|
||||
} else if (/^smtps?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'relay',
|
||||
value: target
|
||||
};
|
||||
} else if (/^https?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'http',
|
||||
value: target
|
||||
};
|
||||
} else {
|
||||
res.json({
|
||||
error: 'Unknown target type "' + target + '"',
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
filterData.action.targets = targets;
|
||||
hasAction = true;
|
||||
}
|
||||
|
||||
let checkFilterMailbox = done => {
|
||||
if (!result.value.action_mailbox) {
|
||||
if (!result.value.action.mailbox) {
|
||||
return done();
|
||||
}
|
||||
db.database.collection('mailboxes').findOne(
|
||||
{
|
||||
_id: new ObjectID(result.value.action_mailbox),
|
||||
_id: new ObjectID(result.value.action.mailbox),
|
||||
user
|
||||
},
|
||||
(err, mailboxData) => {
|
||||
|
|
@ -713,19 +762,20 @@ module.exports = (db, server) => {
|
|||
* @apiParam {String} user Users unique ID.
|
||||
* @apiParam {String} filter Filters unique ID.
|
||||
* @apiParam {String} [name] Name of the Filter
|
||||
* @apiParam {String} [query_from] Partial match for the From: header (case insensitive)
|
||||
* @apiParam {String} [query_to] Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiParam {String} [query_subject] Partial match for the Subject: header (case insensitive)
|
||||
* @apiParam {String} [query_text] Fulltext search against message text
|
||||
* @apiParam {Bolean} [query_ha] Does a message have to have an attachment or not
|
||||
* @apiParam {Number} [query_size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiParam {Bolean} [action_seen] If true then mark matching messages as Seen
|
||||
* @apiParam {Bolean} [action_flag] If true then mark matching messages as Flagged
|
||||
* @apiParam {Bolean} [action_delete] If true then do not store matching messages
|
||||
* @apiParam {Bolean} [action_spam] If true then store matching messags to Junk Mail folder
|
||||
* @apiParam {String} [action_mailbox] Mailbox ID to store matching messages to
|
||||
* @apiParam {String} [action_forward] An email address where matching messages should be forwarded to
|
||||
* @apiParam {String} [action_targetUrl] An URL where matching messages should be POSTed to
|
||||
* @apiParam {Object} query Rules that a message must match
|
||||
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
|
||||
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
|
||||
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
|
||||
* @apiParam {String} [query.text] Fulltext search against message text
|
||||
* @apiParam {Bolean} [query.ha] Does a message have to have an attachment or not
|
||||
* @apiParam {Number} [query.size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
|
||||
* @apiParam {Object} action Action to take with a matching message
|
||||
* @apiParam {Bolean} [action.seen] If true then mark matching messages as Seen
|
||||
* @apiParam {Bolean} [action.flag] If true then mark matching messages as Flagged
|
||||
* @apiParam {Bolean} [action.delete] If true then do not store matching messages
|
||||
* @apiParam {Bolean} [action.spam] If true then store matching messags to Junk Mail folder
|
||||
* @apiParam {String} [action.mailbox] Mailbox ID to store matching messages to
|
||||
* @apiParam {String[]} [action.targets] A list of email addresses / HTTP URLs to forward the message to
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID for the created Filter
|
||||
|
|
@ -736,8 +786,10 @@ module.exports = (db, server) => {
|
|||
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45/filters/5a1c0ee490a34c67e266931c \
|
||||
* -H 'Content-type: application/json' \
|
||||
* -d '{
|
||||
* "action_seen": "",
|
||||
* "action_flag": true
|
||||
* "action": {
|
||||
* "seen": "",
|
||||
* "flag": true
|
||||
* }
|
||||
* }'
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
|
|
@ -772,60 +824,60 @@ module.exports = (db, server) => {
|
|||
.max(255)
|
||||
.empty(''),
|
||||
|
||||
query_from: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_to: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_subject: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_text: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
query_ha: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
query_size: Joi.number().empty(''),
|
||||
|
||||
action_seen: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_flag: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_delete: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
action_spam: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
|
||||
action_mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.empty(''),
|
||||
action_forward: Joi.string()
|
||||
.email()
|
||||
.empty(''),
|
||||
action_targetUrl: Joi.string()
|
||||
.uri({
|
||||
scheme: ['http', 'https'],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
.empty('')
|
||||
query: Joi.object().keys({
|
||||
from: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
to: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
subject: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
text: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.empty(''),
|
||||
ha: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
size: Joi.number().empty('')
|
||||
}),
|
||||
action: Joi.object().keys({
|
||||
seen: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
flag: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
delete: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
spam: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.empty(''),
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.empty(''),
|
||||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
|
@ -847,78 +899,117 @@ module.exports = (db, server) => {
|
|||
let $set = {};
|
||||
let $unset = {};
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
if (result.value.name) {
|
||||
$set.name = result.value.name;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
let hasQuery = false;
|
||||
let hasAction = false;
|
||||
if (result.value.query) {
|
||||
['from', 'to', 'subject'].forEach(key => {
|
||||
if (result.value.query[key]) {
|
||||
$set['query.headers.' + key] = result.value.query[key].replace(/\s+/g, ' ');
|
||||
hasChanges = true;
|
||||
} else if (key in req.params.query) {
|
||||
// delete empty values
|
||||
$unset['query.headers.' + key] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
['from', 'to', 'subject'].forEach(key => {
|
||||
if (result.value['query_' + key]) {
|
||||
$set['query.headers.' + key] = result.value['query_' + key].replace(/\s+/g, ' ');
|
||||
hasQuery = true;
|
||||
} else if ('query_' + key in req.params) {
|
||||
$unset['query.headers.' + key] = true;
|
||||
hasQuery = true;
|
||||
if (result.value.query.text) {
|
||||
$set['query.text'] = result.value.query.text.replace(/\s+/g, ' ');
|
||||
hasChanges = true;
|
||||
} else if ('text' in req.params.query) {
|
||||
$unset['query.text'] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.value.query_text) {
|
||||
$set['query.text'] = result.value.query_text.replace(/\s+/g, ' ');
|
||||
hasQuery = true;
|
||||
} else if ('query_text' in req.params) {
|
||||
$unset['query.text'] = true;
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
if (typeof result.value.query_ha === 'boolean') {
|
||||
$set['query.ha'] = result.value.query_ha;
|
||||
hasQuery = true;
|
||||
} else if ('query_ha' in req.params) {
|
||||
$unset['query.ha'] = true;
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
if (result.value.query_size) {
|
||||
$set['query.size'] = result.value.query_size;
|
||||
hasQuery = true;
|
||||
} else if ('query_size' in req.params) {
|
||||
$unset['query.size'] = true;
|
||||
hasQuery = true;
|
||||
}
|
||||
|
||||
['seen', 'flag', 'delete', 'spam'].forEach(key => {
|
||||
if (typeof result.value['action_' + key] === 'boolean') {
|
||||
$set['action.' + key] = result.value['action_' + key];
|
||||
hasAction = true;
|
||||
} else if ('action_' + key in req.params) {
|
||||
$unset['action.' + key] = true;
|
||||
hasAction = true;
|
||||
if (typeof result.value.query.ha === 'boolean') {
|
||||
$set['query.ha'] = result.value.query.ha;
|
||||
hasChanges = true;
|
||||
} else if ('ha' in req.params.query) {
|
||||
$unset['query.ha'] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
['forward', 'targetUrl'].forEach(key => {
|
||||
if (result.value['action_' + key]) {
|
||||
$set['action.' + key] = result.value['action_' + key];
|
||||
hasAction = true;
|
||||
} else if ('action_' + key in req.params) {
|
||||
$unset['action.' + key] = true;
|
||||
hasAction = true;
|
||||
if (result.value.query.size) {
|
||||
$set['query.size'] = result.value.query.size;
|
||||
hasChanges = true;
|
||||
} else if ('size' in req.params.query) {
|
||||
$unset['query.size'] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (result.value.action) {
|
||||
['seen', 'flag', 'delete', 'spam'].forEach(key => {
|
||||
if (typeof result.value.action[key] === 'boolean') {
|
||||
$set['action.' + key] = result.value.action[key];
|
||||
hasChanges = true;
|
||||
} else if (key in req.params.action) {
|
||||
$unset['action.' + key] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
let targets = result.value.action.targets;
|
||||
|
||||
if (targets) {
|
||||
for (let i = 0, len = targets.length; i < len; i++) {
|
||||
let target = targets[i];
|
||||
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
|
||||
// email
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'mail',
|
||||
value: target
|
||||
};
|
||||
} else if (/^smtps?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'relay',
|
||||
value: target
|
||||
};
|
||||
} else if (/^https?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'http',
|
||||
value: target
|
||||
};
|
||||
} else {
|
||||
res.json({
|
||||
error: 'Unknown target type "' + target + '"',
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
$set['action.targets'] = targets;
|
||||
hasChanges = true;
|
||||
} else if ('targets' in req.params.action) {
|
||||
$unset['action.targets'] = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
let checkFilterMailbox = done => {
|
||||
if (!result.value.action_mailbox) {
|
||||
if ('action_mailbox' in req.params) {
|
||||
if (!result.value.action) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (!result.value.action.mailbox) {
|
||||
if ('mailbox' in req.params.action) {
|
||||
$unset['action.mailbox'] = true;
|
||||
hasAction = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
return done();
|
||||
}
|
||||
db.database.collection('mailboxes').findOne(
|
||||
{
|
||||
_id: new ObjectID(result.value.action_mailbox),
|
||||
_id: new ObjectID(result.value.action.mailbox),
|
||||
user
|
||||
},
|
||||
(err, mailboxData) => {
|
||||
|
|
@ -936,14 +1027,14 @@ module.exports = (db, server) => {
|
|||
return next();
|
||||
}
|
||||
$set['action.mailbox'] = mailboxData._id;
|
||||
hasAction = true;
|
||||
hasChanges = true;
|
||||
done();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
checkFilterMailbox(() => {
|
||||
if (!hasQuery && !hasAction) {
|
||||
if (!hasChanges) {
|
||||
res.json({
|
||||
error: 'No changes'
|
||||
});
|
||||
|
|
@ -1041,16 +1132,24 @@ function getFilterStrings(filter, mailboxes) {
|
|||
} else {
|
||||
return ['keep in INBOX'];
|
||||
}
|
||||
case 'forward':
|
||||
case 'targets':
|
||||
if (filter.action[key]) {
|
||||
return ['forward to', filter.action[key]];
|
||||
}
|
||||
break;
|
||||
case 'targetUrl':
|
||||
if (filter.action[key]) {
|
||||
let url = filter.action[key];
|
||||
let parsed = urllib.parse(url);
|
||||
return ['upload to', parsed.hostname || parsed.host];
|
||||
return [
|
||||
'forward to',
|
||||
filter.action[key]
|
||||
.map(target => {
|
||||
switch (target.type) {
|
||||
case 'http': {
|
||||
let parsed = urllib.parse(target.value);
|
||||
return parsed.hostname || parsed.host;
|
||||
}
|
||||
|
||||
default:
|
||||
return target.value;
|
||||
}
|
||||
})
|
||||
.join(', ')
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'spam':
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ module.exports = (db, server, messageHandler) => {
|
|||
* @apiSuccess {String} results.intro First 128 bytes of the message
|
||||
* @apiSuccess {Boolean} results.attachments Does the message have attachments
|
||||
* @apiSuccess {Boolean} results.seen Is this message alread seen or not
|
||||
* @apiSuccess {Boolean} results.deleted Does this message have a \Deleted flag (should not have as messages are automatically deleted once this flag is set)
|
||||
* @apiSuccess {Boolean} results.flagged Does this message have a \Flagged flag
|
||||
* @apiSuccess {Boolean} results.deleted Does this message have a \\Deleted flag (should not have as messages are automatically deleted once this flag is set)
|
||||
* @apiSuccess {Boolean} results.flagged Does this message have a \\Flagged flag
|
||||
* @apiSuccess {Boolean} results.answered Does this message have a \\Answered flag
|
||||
* @apiSuccess {Boolean} results.forwarded Does this message have a \$Forwarded flag
|
||||
* @apiSuccess {Object} results.contentType Parsed Content-Type header. Usually needed to identify encrypted messages and such
|
||||
* @apiSuccess {String} results.contentType.value MIME type of the message, eg. "multipart/mixed"
|
||||
* @apiSuccess {Object} results.contentType.params An object with Content-Type params as key-value pairs
|
||||
|
|
@ -93,6 +95,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
* "deleted": false,
|
||||
* "flagged": true,
|
||||
* "draft": false,
|
||||
* "answered": false,
|
||||
* "forwarded": false,
|
||||
* "url": "/users/59fc66a03e54454869460e45/mailboxes/59fc66a03e54454869460e46/messages/1",
|
||||
* "contentType": {
|
||||
* "value": "multipart/mixed",
|
||||
|
|
@ -233,7 +237,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
undeleted: true,
|
||||
flagged: true,
|
||||
draft: true,
|
||||
thread: true
|
||||
thread: true,
|
||||
flags: true
|
||||
},
|
||||
paginatedField: 'idate',
|
||||
sortAscending
|
||||
|
|
@ -663,7 +668,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
undeleted: true,
|
||||
flagged: true,
|
||||
draft: true,
|
||||
thread: true
|
||||
thread: true,
|
||||
flags: true
|
||||
},
|
||||
paginatedField: '_id',
|
||||
sortAscending: false
|
||||
|
|
@ -904,6 +910,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
undeleted: true,
|
||||
flagged: true,
|
||||
draft: true,
|
||||
flags: true,
|
||||
attachments: true,
|
||||
html: true,
|
||||
text: true,
|
||||
|
|
@ -1028,6 +1035,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
deleted: !messageData.undeleted,
|
||||
flagged: messageData.flagged,
|
||||
draft: messageData.draft,
|
||||
answered: messageData.flags.includes('\\Answered'),
|
||||
forwarded: messageData.flags.includes('$Forwarded'),
|
||||
html: messageData.html,
|
||||
text: messageData.text,
|
||||
forwardTargets: messageData.forwardTargets,
|
||||
|
|
@ -2439,7 +2448,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
undeleted: true,
|
||||
flagged: true,
|
||||
draft: true,
|
||||
thread: true
|
||||
thread: true,
|
||||
flags: true
|
||||
},
|
||||
paginatedField: '_id',
|
||||
sortAscending
|
||||
|
|
@ -2669,6 +2679,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
undeleted: true,
|
||||
flagged: true,
|
||||
draft: true,
|
||||
flags: true,
|
||||
attachments: true,
|
||||
html: true,
|
||||
text: true,
|
||||
|
|
@ -2776,6 +2787,8 @@ module.exports = (db, server, messageHandler) => {
|
|||
deleted: !messageData.undeleted,
|
||||
flagged: messageData.flagged,
|
||||
draft: messageData.draft,
|
||||
answered: messageData.flags.includes('\\Answered'),
|
||||
forwarded: messageData.flags.includes('$Forwarded'),
|
||||
html: messageData.html,
|
||||
text: messageData.text,
|
||||
forwardTargets: messageData.forwardTargets,
|
||||
|
|
@ -3124,7 +3137,9 @@ function formatMessageListing(messageData) {
|
|||
seen: !messageData.unseen,
|
||||
deleted: !messageData.undeleted,
|
||||
flagged: messageData.flagged,
|
||||
draft: messageData.draft
|
||||
draft: messageData.draft,
|
||||
answered: messageData.flags.includes('\\Answered'),
|
||||
forwarded: messageData.flags.includes('$Forwarded')
|
||||
};
|
||||
|
||||
let parsedContentType = parsedHeader['content-type'];
|
||||
|
|
|
|||
|
|
@ -76,106 +76,144 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
return done(null, false);
|
||||
}
|
||||
query.user = user;
|
||||
db.users.collection('messages').findOne(
|
||||
query,
|
||||
{
|
||||
fields: {
|
||||
'mimeTree.parsedHeader': true,
|
||||
thread: true
|
||||
}
|
||||
},
|
||||
(err, messageData) => {
|
||||
if (err) {
|
||||
err.code = 'InternalDatabaseError';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let headers = (messageData && messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
||||
let subject = headers.subject || '';
|
||||
try {
|
||||
subject = libmime.decodeWords(subject).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
|
||||
if (!/^\w+: /.test(subject)) {
|
||||
subject = ((options.reference.action === 'forward' ? 'Fwd' : 'Re') + ': ' + subject).trim();
|
||||
}
|
||||
|
||||
let sender = headers['reply-to'] || headers.from || headers.sender;
|
||||
let replyTo = [];
|
||||
let replyCc = [];
|
||||
let uniqueRecipients = new Set();
|
||||
|
||||
let checkAddress = (target, addr) => {
|
||||
let address = tools.normalizeAddress(addr.address);
|
||||
|
||||
if (address !== userData.address && !uniqueRecipients.has(address)) {
|
||||
uniqueRecipients.add(address);
|
||||
if (addr.name) {
|
||||
try {
|
||||
addr.name = libmime.decodeWords(addr.name).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
let getMessage = next => {
|
||||
let updateable = ['reply', 'replyAll', 'forward'];
|
||||
if (!options.reference || !updateable.includes(options.reference.action)) {
|
||||
return db.database.collection('messages').findOne(
|
||||
query,
|
||||
{
|
||||
fields: {
|
||||
'mimeTree.parsedHeader': true,
|
||||
thread: true
|
||||
}
|
||||
target.push(addr);
|
||||
}
|
||||
};
|
||||
|
||||
if (sender && sender.address) {
|
||||
checkAddress(replyTo, sender);
|
||||
}
|
||||
|
||||
if (options.reference.action === 'replyAll') {
|
||||
[].concat(headers.to || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyTo, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
[].concat(headers.cc || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyCc, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
}
|
||||
|
||||
let messageId = (headers['message-id'] || '').trim();
|
||||
let references = (headers.references || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(' ')
|
||||
.filter(mid => mid);
|
||||
|
||||
if (messageId && !references.includes(messageId)) {
|
||||
references.unshift(messageId);
|
||||
}
|
||||
if (references.length > 50) {
|
||||
references = references.slice(0, 50);
|
||||
}
|
||||
|
||||
let referenceData = {
|
||||
replyTo,
|
||||
replyCc,
|
||||
subject,
|
||||
thread: messageData.thread,
|
||||
inReplyTo: messageId,
|
||||
references: references.join(' ')
|
||||
};
|
||||
|
||||
return done(null, referenceData);
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
);
|
||||
let $addToSet = {};
|
||||
switch (options.reference.action) {
|
||||
case 'reply':
|
||||
case 'replyAll':
|
||||
$addToSet.flags = '\\Answered';
|
||||
break;
|
||||
case 'forward':
|
||||
$addToSet.flags = '$Forwarded';
|
||||
break;
|
||||
}
|
||||
|
||||
db.database.collection('messages').findOneAndUpdate(
|
||||
query,
|
||||
{
|
||||
$addToSet
|
||||
},
|
||||
{
|
||||
returnOriginal: false,
|
||||
projection: {
|
||||
'mimeTree.parsedHeader': true,
|
||||
thread: true
|
||||
}
|
||||
},
|
||||
(err, r) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, r && r.value);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
getMessage((err, messageData) => {
|
||||
if (err) {
|
||||
err.code = 'InternalDatabaseError';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let headers = (messageData && messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
||||
let subject = headers.subject || '';
|
||||
try {
|
||||
subject = libmime.decodeWords(subject).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
|
||||
if (!/^\w+: /.test(subject)) {
|
||||
subject = ((options.reference.action === 'forward' ? 'Fwd' : 'Re') + ': ' + subject).trim();
|
||||
}
|
||||
|
||||
let sender = headers['reply-to'] || headers.from || headers.sender;
|
||||
let replyTo = [];
|
||||
let replyCc = [];
|
||||
let uniqueRecipients = new Set();
|
||||
|
||||
let checkAddress = (target, addr) => {
|
||||
let address = tools.normalizeAddress(addr.address);
|
||||
|
||||
if (address !== userData.address && !uniqueRecipients.has(address)) {
|
||||
uniqueRecipients.add(address);
|
||||
if (addr.name) {
|
||||
try {
|
||||
addr.name = libmime.decodeWords(addr.name).trim();
|
||||
} catch (E) {
|
||||
// failed to parse value
|
||||
}
|
||||
}
|
||||
target.push(addr);
|
||||
}
|
||||
};
|
||||
|
||||
if (sender && sender.address) {
|
||||
checkAddress(replyTo, sender);
|
||||
}
|
||||
|
||||
if (options.reference.action === 'replyAll') {
|
||||
[].concat(headers.to || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyTo, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
[].concat(headers.cc || []).forEach(addr => {
|
||||
let walk = addr => {
|
||||
if (addr.address) {
|
||||
checkAddress(replyCc, addr);
|
||||
} else if (addr.group) {
|
||||
addr.group.forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(addr);
|
||||
});
|
||||
}
|
||||
|
||||
let messageId = (headers['message-id'] || '').trim();
|
||||
let references = (headers.references || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(' ')
|
||||
.filter(mid => mid);
|
||||
|
||||
if (messageId && !references.includes(messageId)) {
|
||||
references.unshift(messageId);
|
||||
}
|
||||
if (references.length > 50) {
|
||||
references = references.slice(0, 50);
|
||||
}
|
||||
|
||||
let referenceData = {
|
||||
replyTo,
|
||||
replyCc,
|
||||
subject,
|
||||
thread: messageData.thread,
|
||||
inReplyTo: messageId,
|
||||
references: references.join(' ')
|
||||
};
|
||||
|
||||
return done(null, referenceData);
|
||||
});
|
||||
};
|
||||
|
||||
getReferencedMessage((err, referenceData) => {
|
||||
|
|
@ -288,57 +326,56 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
let collector = new StreamCollect();
|
||||
let compiledEnvelope = compiled.getEnvelope();
|
||||
|
||||
messageHandler.counters.ttlcounter(
|
||||
'wdr:' + userData._id.toString(),
|
||||
compiledEnvelope.to.length,
|
||||
userData.recipients,
|
||||
false,
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
err.code = 'ERRREDIS';
|
||||
return callback(err);
|
||||
}
|
||||
let messageId = new ObjectID();
|
||||
let addToDeliveryQueue = next => {
|
||||
if (!compiledEnvelope.to || !compiledEnvelope.to.length || options.uploadOnly) {
|
||||
// no delivery, just build the message
|
||||
collector.on('data', () => false); //drain
|
||||
collector.on('end', () => {
|
||||
next(null, false);
|
||||
});
|
||||
collector.once('error', err => {
|
||||
next(err);
|
||||
});
|
||||
let stream = compiled.createReadStream();
|
||||
stream.once('error', err => collector.emit('error', err));
|
||||
stream.pipe(collector);
|
||||
return;
|
||||
}
|
||||
|
||||
let success = result.success;
|
||||
let sent = result.value;
|
||||
let ttl = result.ttl;
|
||||
|
||||
let ttlHuman = false;
|
||||
if (ttl) {
|
||||
if (ttl < 60) {
|
||||
ttlHuman = ttl + ' seconds';
|
||||
} else if (ttl < 3600) {
|
||||
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
||||
} else {
|
||||
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
||||
messageHandler.counters.ttlcounter(
|
||||
'wdr:' + userData._id.toString(),
|
||||
compiledEnvelope.to.length,
|
||||
userData.recipients,
|
||||
false,
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
err.code = 'ERRREDIS';
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, userData.recipients, ttl);
|
||||
let err = new Error(
|
||||
'You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : '')
|
||||
);
|
||||
err.code = 'ERRSENDINGLIMIT';
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
let success = result.success;
|
||||
let sent = result.value;
|
||||
let ttl = result.ttl;
|
||||
|
||||
let messageId = new ObjectID();
|
||||
let addToDeliveryQueue = next => {
|
||||
if (!compiledEnvelope.to || !compiledEnvelope.to.length || options.uploadOnly) {
|
||||
// no delivery, just build the message
|
||||
collector.on('data', () => false); //drain
|
||||
collector.on('end', () => {
|
||||
next(null, false);
|
||||
});
|
||||
collector.once('error', err => {
|
||||
next(err);
|
||||
});
|
||||
let stream = compiled.createReadStream();
|
||||
stream.once('error', err => collector.emit('error', err));
|
||||
stream.pipe(collector);
|
||||
let ttlHuman = false;
|
||||
if (ttl) {
|
||||
if (ttl < 60) {
|
||||
ttlHuman = ttl + ' seconds';
|
||||
} else if (ttl < 3600) {
|
||||
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
||||
} else {
|
||||
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
if (!success) {
|
||||
log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, userData.recipients, ttl);
|
||||
let err = new Error(
|
||||
'You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : '')
|
||||
);
|
||||
err.code = 'ERRSENDINGLIMIT';
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
|
||||
// push message to outbound queue
|
||||
|
|
@ -368,82 +405,94 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
stream.once('error', err => message.emit('error', err));
|
||||
stream.pipe(collector).pipe(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
addToDeliveryQueue((err, outbound) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
addToDeliveryQueue((err, outbound) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
if (overQuota) {
|
||||
log.info('API', 'STOREFAIL user=%s error=%s', user, 'Over quota');
|
||||
return callback(null, {
|
||||
id: false,
|
||||
mailbox: false,
|
||||
queueId: outbound,
|
||||
overQuota: true
|
||||
});
|
||||
}
|
||||
|
||||
// Checks if the message needs to be encrypted before storing it
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages ? userData.pubKey : false,
|
||||
{ chunks: collector.chunks, chunklen: collector.chunklen },
|
||||
(err, encrypted) => {
|
||||
let raw = false;
|
||||
if (!err && encrypted) {
|
||||
// message was encrypted, so use the result instead of raw
|
||||
raw = encrypted;
|
||||
}
|
||||
if (overQuota) {
|
||||
log.info('API', 'STOREFAIL user=%s error=%s', user, 'Over quota');
|
||||
return callback(null, {
|
||||
id: false,
|
||||
mailbox: false,
|
||||
queueId: outbound,
|
||||
overQuota: true
|
||||
|
||||
let meta = {
|
||||
source: 'API',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
origin: options.ip,
|
||||
sess: options.sess,
|
||||
time: new Date()
|
||||
};
|
||||
|
||||
if (options.meta) {
|
||||
Object.keys(options.meta || {}).forEach(key => {
|
||||
if (!(key in meta)) {
|
||||
meta[key] = options.meta[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Checks if the message needs to be encrypted before storing it
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages ? userData.pubKey : false,
|
||||
{ chunks: collector.chunks, chunklen: collector.chunklen },
|
||||
(err, encrypted) => {
|
||||
let raw = false;
|
||||
if (!err && encrypted) {
|
||||
// message was encrypted, so use the result instead of raw
|
||||
raw = encrypted;
|
||||
}
|
||||
let messageOptions = {
|
||||
user: userData._id,
|
||||
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox
|
||||
? new ObjectID(options.mailbox)
|
||||
: options.isDraft ? '\\Drafts' : '\\Sent',
|
||||
|
||||
let messageOptions = {
|
||||
user: userData._id,
|
||||
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox ? new ObjectID(options.mailbox) : '\\Sent',
|
||||
outbound,
|
||||
|
||||
outbound,
|
||||
meta,
|
||||
|
||||
meta: {
|
||||
source: 'API',
|
||||
from: compiledEnvelope.from,
|
||||
to: compiledEnvelope.to,
|
||||
origin: options.ip,
|
||||
sess: options.sess,
|
||||
time: new Date()
|
||||
},
|
||||
date: false,
|
||||
flags: ['\\Seen'].concat(options.isDraft ? '\\Draft' : []),
|
||||
|
||||
date: false,
|
||||
flags: ['\\Seen'],
|
||||
// always insert drafts, otherwise skip
|
||||
skipExisting: !options.isDraft
|
||||
};
|
||||
|
||||
// if similar message exists, then skip
|
||||
skipExisting: true
|
||||
};
|
||||
if (raw) {
|
||||
messageOptions.raw = raw;
|
||||
} else {
|
||||
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
messageOptions.raw = raw;
|
||||
} else {
|
||||
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
||||
}
|
||||
|
||||
messageHandler.add(messageOptions, (err, success, info) => {
|
||||
if (err) {
|
||||
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
|
||||
err.code = 'SUBMITFAIL';
|
||||
return callback(err);
|
||||
} else if (!info) {
|
||||
log.info('API', 'SUBMITSKIP user=%s message=already exists', user);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
});
|
||||
messageHandler.add(messageOptions, (err, success, info) => {
|
||||
if (err) {
|
||||
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
|
||||
err.code = 'SUBMITFAIL';
|
||||
return callback(err);
|
||||
} else if (!info) {
|
||||
log.info('API', 'SUBMITSKIP user=%s message=already exists', user);
|
||||
return callback(null, false);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -469,6 +518,7 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
* @apiParam {String} reference.action Either <code>reply</code>, <code>replyAll</code> or <code>forward</code>
|
||||
* @apiParam {String} [mailbox] Mailbox ID where to upload the message. If not set then message is uploaded to Sent Mail folder.
|
||||
* @apiParam {Boolean} [uploadOnly=false] If <code>true</code> then generated message is not added to the sending queue
|
||||
* @apiParam {Boolean} [isDraft=false] If <code>true</code> then treats this message as draft (should be used with uploadOnly=true)
|
||||
* @apiParam {String} [sendTime] Datestring for delivery if message should be sent some later time
|
||||
* @apiParam {Object} [envelope] SMTP envelope. If not provided then resolved either from message headers or from referenced message
|
||||
* @apiParam {Object} [envelope.from] Sender information. If not set then it is resolved to User's default address
|
||||
|
|
@ -498,6 +548,7 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
* @apiParam {String} [attachments.filename] Attachment filename
|
||||
* @apiParam {String} [attachments.contentType] MIME type for the attachment file
|
||||
* @apiParam {String} [attachments.cid] Content-ID value if you want to reference to this attachment from HTML formatted message
|
||||
* @apiParam {Object} [meta] Custom metainfo for the message
|
||||
* @apiParam {String} [sess] Session identifier for the logs
|
||||
* @apiParam {String} [ip] IP address for the logs
|
||||
*
|
||||
|
|
@ -713,6 +764,7 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
.max(255)
|
||||
})
|
||||
),
|
||||
meta: Joi.object().unknown(true),
|
||||
sess: Joi.string().max(255),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
|
|
|
|||
122
lib/api/users.js
122
lib/api/users.js
|
|
@ -41,7 +41,7 @@ module.exports = (db, server, userHandler) => {
|
|||
* @apiSuccess {String} results.name Name of the User
|
||||
* @apiSuccess {String} results.address Main email address of the User
|
||||
* @apiSuccess {String[]} results.tags List of tags associated with the User'
|
||||
* @apiSuccess {String[]} results.forward A list of email addresses to forward all incoming emails
|
||||
* @apiSuccess {String[]} targets List of forwarding targets
|
||||
* @apiSuccess {Boolean} results.encryptMessages If <code>true</code> then received messages are encrypted
|
||||
* @apiSuccess {Boolean} results.encryptForwarded If <code>true</code> then forwarded messages are encrypted
|
||||
* @apiSuccess {Object} results.quota Quota usage limits
|
||||
|
|
@ -216,8 +216,7 @@ module.exports = (db, server, userHandler) => {
|
|||
address: true,
|
||||
tags: true,
|
||||
storageUsed: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
targets: true,
|
||||
quota: true,
|
||||
activated: true,
|
||||
disabled: true,
|
||||
|
|
@ -260,8 +259,7 @@ module.exports = (db, server, userHandler) => {
|
|||
name: userData.name,
|
||||
address: userData.address,
|
||||
tags: userData.tags || [],
|
||||
forward: [].concat(userData.forward || []),
|
||||
targetUrl: userData.targetUrl,
|
||||
targets: userData.targets && userData.targets.map(t => t.value),
|
||||
encryptMessages: !!userData.encryptMessages,
|
||||
encryptForwarded: !!userData.encryptForwarded,
|
||||
quota: {
|
||||
|
|
@ -302,8 +300,7 @@ module.exports = (db, server, userHandler) => {
|
|||
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
||||
* @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key
|
||||
* @apiParam {String} [language] Language code for the User
|
||||
* @apiParam {String[]} [forward] A list of email addresses to forward all incoming emails
|
||||
* @apiParam {String} [targetUrl] An URL to post all incoming emails
|
||||
* @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} [quota] Allowed quota of the user in bytes
|
||||
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
||||
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
||||
|
|
@ -375,8 +372,14 @@ module.exports = (db, server, userHandler) => {
|
|||
.default(0),
|
||||
|
||||
name: Joi.string().max(256),
|
||||
forward: Joi.array().items(Joi.string().email()),
|
||||
targetUrl: Joi.string().max(256),
|
||||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
),
|
||||
|
||||
quota: Joi.number()
|
||||
.min(0)
|
||||
|
|
@ -434,8 +437,40 @@ module.exports = (db, server, userHandler) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
if (result.value.forward) {
|
||||
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
||||
let targets = result.value.targets;
|
||||
|
||||
if (targets) {
|
||||
for (let i = 0, len = targets.length; i < len; i++) {
|
||||
let target = targets[i];
|
||||
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
|
||||
// email
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'mail',
|
||||
value: target
|
||||
};
|
||||
} else if (/^smtps?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'relay',
|
||||
value: target
|
||||
};
|
||||
} else if (/^https?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'http',
|
||||
value: target
|
||||
};
|
||||
} else {
|
||||
res.json({
|
||||
error: 'Unknown target type "' + target + '"',
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
result.value.targets = targets;
|
||||
}
|
||||
|
||||
if ('pubKey' in req.params && !result.value.pubKey) {
|
||||
|
|
@ -616,8 +651,7 @@ module.exports = (db, server, userHandler) => {
|
|||
* @apiSuccess {String} keyInfo.name Name listed in public key
|
||||
* @apiSuccess {String} keyInfo.address E-mail address listed in public key
|
||||
* @apiSuccess {String} keyInfo.fingerprint Fingerprint of the public key
|
||||
* @apiSuccess {String[]} forward A list of email addresses to forward all incoming emails
|
||||
* @apiSuccess {String} targetUrl An URL to post all incoming emails
|
||||
* @apiSuccess {String[]} targets List of forwarding targets
|
||||
* @apiSuccess {Object} limits Account limits and usage
|
||||
* @apiSuccess {Object} limits.quota Quota usage limits
|
||||
* @apiSuccess {Number} limits.quota.allowed Allowed quota of the user in bytes
|
||||
|
|
@ -654,8 +688,10 @@ module.exports = (db, server, userHandler) => {
|
|||
* "encryptForwarded": false,
|
||||
* "pubKey": "",
|
||||
* "keyInfo": false,
|
||||
* "forward": [],
|
||||
* "targetUrl": "",
|
||||
* "targets": [
|
||||
* "my.old.address@example.com",
|
||||
* "smtp://mx2.zone.eu:25"
|
||||
* ],
|
||||
* "limits": {
|
||||
* "quota": {
|
||||
* "allowed": 107374182400,
|
||||
|
|
@ -771,8 +807,7 @@ module.exports = (db, server, userHandler) => {
|
|||
pubKey: userData.pubKey,
|
||||
keyInfo: getKeyInfo(userData.pubKey),
|
||||
|
||||
forward: [].concat(userData.forward || []),
|
||||
targetUrl: userData.targetUrl,
|
||||
targets: [].concat(userData.targets || []),
|
||||
|
||||
limits: {
|
||||
quota: {
|
||||
|
|
@ -823,8 +858,7 @@ module.exports = (db, server, userHandler) => {
|
|||
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
||||
* @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key
|
||||
* @apiParam {String} [language] Language code for the User
|
||||
* @apiParam {String[]} [forward] A list of email addresses to forward all incoming emails
|
||||
* @apiParam {String} [targetUrl] An URL to post all incoming emails
|
||||
* @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} [quota] Allowed quota of the user in bytes
|
||||
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
||||
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
||||
|
|
@ -884,10 +918,14 @@ module.exports = (db, server, userHandler) => {
|
|||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(256),
|
||||
forward: Joi.array().items(Joi.string().email()),
|
||||
targetUrl: Joi.string()
|
||||
.empty('')
|
||||
.max(256),
|
||||
targets: Joi.array().items(
|
||||
Joi.string().email(),
|
||||
Joi.string().uri({
|
||||
scheme: [/smtps?/, /https?/],
|
||||
allowRelative: false,
|
||||
relativeOnly: false
|
||||
})
|
||||
),
|
||||
|
||||
pubKey: Joi.string()
|
||||
.empty('')
|
||||
|
|
@ -942,12 +980,40 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
let user = new ObjectID(result.value.user);
|
||||
|
||||
if (result.value.forward) {
|
||||
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
||||
}
|
||||
let targets = result.value.targets;
|
||||
|
||||
if (!result.value.targetUrl && 'targetUrl' in req.params) {
|
||||
result.value.targetUrl = '';
|
||||
if (targets) {
|
||||
for (let i = 0, len = targets.length; i < len; i++) {
|
||||
let target = targets[i];
|
||||
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
|
||||
// email
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'mail',
|
||||
value: target
|
||||
};
|
||||
} else if (/^smtps?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'relay',
|
||||
value: target
|
||||
};
|
||||
} else if (/^https?:/i.test(target)) {
|
||||
targets[i] = {
|
||||
id: new ObjectID(),
|
||||
type: 'http',
|
||||
value: target
|
||||
};
|
||||
} else {
|
||||
res.json({
|
||||
error: 'Unknown target type "' + target + '"',
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
result.value.targets = targets;
|
||||
}
|
||||
|
||||
if (!result.value.name && 'name' in req.params) {
|
||||
|
|
|
|||
|
|
@ -80,8 +80,7 @@ class FilterHandler {
|
|||
let fields = {
|
||||
name: true,
|
||||
forwards: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
targets: true,
|
||||
autoreply: true,
|
||||
encryptMessages: true,
|
||||
encryptForwarded: true,
|
||||
|
|
@ -266,16 +265,9 @@ class FilterHandler {
|
|||
|
||||
// apply matching filter
|
||||
Object.keys(filter.action).forEach(key => {
|
||||
if (key === 'forward') {
|
||||
[].concat(filter.action[key] || []).forEach(address => {
|
||||
forwardTargets.set(address, { type: 'mail', value: address });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'targetUrl') {
|
||||
[].concat(filter.action[key] || []).forEach(address => {
|
||||
forwardTargets.set(address, { type: 'http', value: address });
|
||||
if (key === 'targets') {
|
||||
[].concat(filter.action[key] || []).forEach(target => {
|
||||
forwardTargets.set(target.value, target);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -328,20 +320,13 @@ class FilterHandler {
|
|||
};
|
||||
|
||||
let forwardMessage = done => {
|
||||
if (userData.forward && !filterActions.get('delete')) {
|
||||
if (userData.targets && !filterActions.get('delete')) {
|
||||
// forward to default recipient only if the message is not deleted
|
||||
(Array.isArray(userData.forward) ? userData.forward : [].concat(userData.forward || [])).forEach(forward => {
|
||||
if (forward) {
|
||||
forwardTargets.set(forward, { type: 'mail', value: forward });
|
||||
}
|
||||
userData.targets.forEach(target => {
|
||||
forwardTargets.set(target.value, target);
|
||||
});
|
||||
}
|
||||
|
||||
if (userData.targetUrl && !filterActions.get('delete')) {
|
||||
// forward to default URL only if the message is not deleted
|
||||
forwardTargets.set(userData.targetUrl, { type: 'http', value: userData.targetUrl });
|
||||
}
|
||||
|
||||
// never forward messages marked as spam
|
||||
if (!forwardTargets.size || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
|
|
|
|||
|
|
@ -748,8 +748,7 @@ class UserHandler {
|
|||
recipients: data.recipients || 0,
|
||||
forwards: data.forwards || 0,
|
||||
|
||||
forward: [].concat(data.forward || []),
|
||||
targetUrl: data.targetUrl || '',
|
||||
targets: [].concat(data.targets || []),
|
||||
|
||||
// autoreply status
|
||||
// off by default, can be changed later by user through the API
|
||||
|
|
|
|||
3
lmtp.js
3
lmtp.js
|
|
@ -76,8 +76,7 @@ const serverOptions = {
|
|||
{
|
||||
name: true,
|
||||
forwards: true,
|
||||
forward: true,
|
||||
targetUrl: true,
|
||||
targets: true,
|
||||
autoreply: true,
|
||||
encryptMessages: true,
|
||||
encryptForwarded: true,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@
|
|||
"test": "mongo --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && NODE_ENV=test grunt",
|
||||
"apidoc": "apidoc -i lib/api/ -o docs/"
|
||||
},
|
||||
"keywords": ["imap", "mail server"],
|
||||
"keywords": [
|
||||
"imap",
|
||||
"mail server"
|
||||
],
|
||||
"author": "Andris Reinman",
|
||||
"license": "EUPL-1.1+",
|
||||
"devDependencies": {
|
||||
|
|
@ -40,7 +43,7 @@
|
|||
"iconv-lite": "0.4.19",
|
||||
"ioredfour": "1.0.2-ioredis",
|
||||
"ioredis": "3.2.2",
|
||||
"joi": "13.1.0",
|
||||
"joi": "13.1.1",
|
||||
"js-yaml": "3.10.0",
|
||||
"key-fingerprint": "1.1.0",
|
||||
"libbase64": "1.0.2",
|
||||
|
|
@ -53,7 +56,7 @@
|
|||
"mongodb": "3.0.1",
|
||||
"mongodb-extended-json": "1.10.0",
|
||||
"node-forge": "^0.7.1",
|
||||
"nodemailer": "4.4.1",
|
||||
"nodemailer": "4.4.2",
|
||||
"npmlog": "4.1.2",
|
||||
"openpgp": "2.6.1",
|
||||
"qrcode": "1.2.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue