mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-10-26 05:56:59 +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
|
# 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.
|
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.
|
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,
|
"name": null,
|
||||||
"address": "testuser01@example.com",
|
"address": "testuser01@example.com",
|
||||||
"tags": ["green", "blue"],
|
"tags": ["green", "blue"],
|
||||||
"forward": [],
|
|
||||||
"targetUrl": "",
|
|
||||||
"encryptMessages": false,
|
"encryptMessages": false,
|
||||||
"encryptForwarded": false,
|
"encryptForwarded": false,
|
||||||
"quota": {
|
"quota": {
|
||||||
|
|
@ -270,8 +270,6 @@ Response for a successful operation:
|
||||||
"encryptForwarded": false,
|
"encryptForwarded": false,
|
||||||
"pubKey": "",
|
"pubKey": "",
|
||||||
"keyInfo": false,
|
"keyInfo": false,
|
||||||
"forward": [],
|
|
||||||
"targetUrl": "",
|
|
||||||
"limits": {
|
"limits": {
|
||||||
"quota": {
|
"quota": {
|
||||||
"allowed": 107374182400,
|
"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
|
* **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
|
* **emptyAddress** if true, then do not set up an address for the user
|
||||||
* **name** is the name 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
|
* **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.
|
* **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
|
* **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
|
* **user** (required) is the ID of the user
|
||||||
* **name** is the updated name for 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)
|
* **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
|
* **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.
|
* **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"
|
* **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(
|
targets: Joi.array().items(
|
||||||
Joi.string().email(),
|
Joi.string().email(),
|
||||||
Joi.string().uri({
|
Joi.string().uri({
|
||||||
scheme: [/smtps?/, /https?/]
|
scheme: [/smtps?/, /https?/],
|
||||||
|
allowRelative: false,
|
||||||
|
relativeOnly: false
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
forwards: Joi.number()
|
forwards: Joi.number()
|
||||||
|
|
@ -1547,7 +1549,9 @@ module.exports = (db, server) => {
|
||||||
targets: Joi.array().items(
|
targets: Joi.array().items(
|
||||||
Joi.string().email(),
|
Joi.string().email(),
|
||||||
Joi.string().uri({
|
Joi.string().uri({
|
||||||
scheme: [/smtps?/, /https?/]
|
scheme: [/smtps?/, /https?/],
|
||||||
|
allowRelative: false,
|
||||||
|
relativeOnly: false
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
forwards: Joi.number().min(0),
|
forwards: Joi.number().min(0),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "wildduck",
|
"name": "wildduck",
|
||||||
"version": "1.0.0",
|
"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",
|
"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 {Boolean} success Indicates successful response
|
||||||
* @apiSuccess {String} id ID for the Filter
|
* @apiSuccess {String} id ID for the Filter
|
||||||
* @apiSuccess {String} name Name of the Filter
|
* @apiSuccess {String} name Name of the Filter
|
||||||
* @apiSuccess {String} query_from Partial match for the From: header (case insensitive)
|
* @apiSuccess {Object} query Rules that a message must match
|
||||||
* @apiSuccess {String} query_to Partial match for the To:/Cc: headers (case insensitive)
|
* @apiSuccess {String} query.from Partial match for the From: header (case insensitive)
|
||||||
* @apiSuccess {String} query_subject Partial match for the Subject: header (case insensitive)
|
* @apiSuccess {String} query.to Partial match for the To:/Cc: headers (case insensitive)
|
||||||
* @apiSuccess {String} query_text Fulltext search against message text
|
* @apiSuccess {String} query.subject Partial match for the Subject: header (case insensitive)
|
||||||
* @apiSuccess {Bolean} query_ha Does a message have to have an attachment or not
|
* @apiSuccess {String} query.text Fulltext search against message text
|
||||||
* @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} query.ha Does a message have to have an attachment or not
|
||||||
* @apiSuccess {Bolean} action_seen If true then mark matching messages as Seen
|
* @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_flag If true then mark matching messages as Flagged
|
* @apiSuccess {Object} action Action to take with a matching message
|
||||||
* @apiSuccess {Bolean} action_delete If true then do not store matching messages
|
* @apiSuccess {Bolean} action.seen If true then mark matching messages as Seen
|
||||||
* @apiSuccess {Bolean} action_spam If true then store matching messags to Junk Mail folder
|
* @apiSuccess {Bolean} action.flag If true then mark matching messages as Flagged
|
||||||
* @apiSuccess {String} action_mailbox Mailbox ID to store matching messages to
|
* @apiSuccess {Bolean} action.delete If true then do not store matching messages
|
||||||
* @apiSuccess {String} action_forward An email address where matching messages should be forwarded to
|
* @apiSuccess {Bolean} action.spam If true then store matching messags to Junk Mail folder
|
||||||
* @apiSuccess {String} action_targetUrl An URL where matching messages should be POSTed to
|
* @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
|
* @apiError error Description of the error
|
||||||
*
|
*
|
||||||
|
|
@ -214,8 +215,12 @@ module.exports = (db, server) => {
|
||||||
* "success": true,
|
* "success": true,
|
||||||
* "id": "5a1c0ee490a34c67e266931c",
|
* "id": "5a1c0ee490a34c67e266931c",
|
||||||
* "created": "2017-11-27T13:11:00.835Z",
|
* "created": "2017-11-27T13:11:00.835Z",
|
||||||
* "query_from": "Mäger",
|
* "query": {
|
||||||
* "action_seen": true
|
* "from": "Mäger"
|
||||||
|
* },
|
||||||
|
* "action": {
|
||||||
|
* "seen": true
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @apiErrorExample {json} Error-Response:
|
* @apiErrorExample {json} Error-Response:
|
||||||
|
|
@ -300,21 +305,26 @@ module.exports = (db, server) => {
|
||||||
success: true,
|
success: true,
|
||||||
id: filterData._id,
|
id: filterData._id,
|
||||||
name: filterData.name,
|
name: filterData.name,
|
||||||
|
query: {},
|
||||||
|
action: {},
|
||||||
created: filterData.created
|
created: filterData.created
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys((filterData.query && filterData.query.headers) || {}).forEach(key => {
|
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 => {
|
Object.keys(filterData.query || {}).forEach(key => {
|
||||||
if (key !== 'headers') {
|
if (key !== 'headers') {
|
||||||
result['query_' + key] = filterData.query[key];
|
result.query[key] = filterData.query[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(filterData.action || {}).forEach(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);
|
res.json(result);
|
||||||
|
|
@ -431,19 +441,20 @@ module.exports = (db, server) => {
|
||||||
*
|
*
|
||||||
* @apiParam {String} user Users unique ID.
|
* @apiParam {String} user Users unique ID.
|
||||||
* @apiParam {String} [name] Name of the Filter
|
* @apiParam {String} [name] Name of the Filter
|
||||||
* @apiParam {String} [query_from] Partial match for the From: header (case insensitive)
|
* @apiParam {Object} query Rules that a message must match
|
||||||
* @apiParam {String} [query_to] Partial match for the To:/Cc: headers (case insensitive)
|
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
|
||||||
* @apiParam {String} [query_subject] Partial match for the Subject: header (case insensitive)
|
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
|
||||||
* @apiParam {String} [query_text] Fulltext search against message text
|
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
|
||||||
* @apiParam {Bolean} [query_ha] Does a message have to have an attachment or not
|
* @apiParam {String} [query.text] Fulltext search against message text
|
||||||
* @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} [query.ha] Does a message have to have an attachment or not
|
||||||
* @apiParam {Bolean} [action_seen] If true then mark matching messages as Seen
|
* @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_flag] If true then mark matching messages as Flagged
|
* @apiParam {Object} action Action to take with a matching message
|
||||||
* @apiParam {Bolean} [action_delete] If true then do not store matching messages
|
* @apiParam {Bolean} [action.seen] If true then mark matching messages as Seen
|
||||||
* @apiParam {Bolean} [action_spam] If true then store matching messags to Junk Mail folder
|
* @apiParam {Bolean} [action.flag] If true then mark matching messages as Flagged
|
||||||
* @apiParam {String} [action_mailbox] Mailbox ID to store matching messages to
|
* @apiParam {Bolean} [action.delete] If true then do not store matching messages
|
||||||
* @apiParam {String} [action_forward] An email address where matching messages should be forwarded to
|
* @apiParam {Bolean} [action.spam] If true then store matching messags to Junk Mail folder
|
||||||
* @apiParam {String} [action_targetUrl] An URL where matching messages should be POSTed to
|
* @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 {Boolean} success Indicates successful response
|
||||||
* @apiSuccess {String} id ID for the created Filter
|
* @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 \
|
* curl -i -XPOST http://localhost:8080/users/5a1bda70bfbd1442cd96c6f0/filters \
|
||||||
* -H 'Content-type: application/json' \
|
* -H 'Content-type: application/json' \
|
||||||
* -d '{
|
* -d '{
|
||||||
* "query_from": "Mäger",
|
* "query": {
|
||||||
* "action_seen": true
|
* "from": "Mäger"
|
||||||
|
* },
|
||||||
|
* "action": {
|
||||||
|
* "seen": true
|
||||||
|
* }
|
||||||
* }'
|
* }'
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Success-Response:
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
|
@ -486,60 +501,64 @@ module.exports = (db, server) => {
|
||||||
.max(255)
|
.max(255)
|
||||||
.empty(''),
|
.empty(''),
|
||||||
|
|
||||||
query_from: Joi.string()
|
query: Joi.object()
|
||||||
.trim()
|
.keys({
|
||||||
.max(255)
|
from: Joi.string()
|
||||||
.empty(''),
|
.trim()
|
||||||
query_to: Joi.string()
|
.max(255)
|
||||||
.trim()
|
.empty(''),
|
||||||
.max(255)
|
to: Joi.string()
|
||||||
.empty(''),
|
.trim()
|
||||||
query_subject: Joi.string()
|
.max(255)
|
||||||
.trim()
|
.empty(''),
|
||||||
.max(255)
|
subject: Joi.string()
|
||||||
.empty(''),
|
.trim()
|
||||||
query_text: Joi.string()
|
.max(255)
|
||||||
.trim()
|
.empty(''),
|
||||||
.max(255)
|
text: Joi.string()
|
||||||
.empty(''),
|
.trim()
|
||||||
query_ha: Joi.boolean()
|
.max(255)
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
.empty(''),
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
ha: Joi.boolean()
|
||||||
.empty(''),
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
query_size: Joi.number().empty(''),
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
|
.empty(''),
|
||||||
action_seen: Joi.boolean()
|
size: Joi.number().empty('')
|
||||||
.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('')
|
.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, {
|
const result = Joi.validate(req.params, schema, {
|
||||||
|
|
@ -574,48 +593,78 @@ module.exports = (db, server) => {
|
||||||
let hasAction = false;
|
let hasAction = false;
|
||||||
|
|
||||||
['from', 'to', 'subject'].forEach(key => {
|
['from', 'to', 'subject'].forEach(key => {
|
||||||
if (result.value['query_' + key]) {
|
if (result.value.query[key]) {
|
||||||
filterData.query.headers[key] = result.value['query_' + key].replace(/\s+/g, ' ');
|
filterData.query.headers[key] = result.value.query[key].replace(/\s+/g, ' ');
|
||||||
hasQuery = true;
|
hasQuery = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.value.query_text) {
|
if (result.value.query.text) {
|
||||||
filterData.query.text = result.value.query_text.replace(/\s+/g, ' ');
|
filterData.query.text = result.value.query.text.replace(/\s+/g, ' ');
|
||||||
hasQuery = true;
|
hasQuery = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof result.value.query_ha === 'boolean') {
|
if (typeof result.value.query.ha === 'boolean') {
|
||||||
filterData.query.ha = result.value.query_ha;
|
filterData.query.ha = result.value.query.ha;
|
||||||
hasQuery = true;
|
hasQuery = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.value.query_size) {
|
if (result.value.query.size) {
|
||||||
filterData.query.size = result.value.query_size;
|
filterData.query.size = result.value.query.size;
|
||||||
hasQuery = true;
|
hasQuery = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
['seen', 'flag', 'delete', 'spam'].forEach(key => {
|
['seen', 'flag', 'delete', 'spam'].forEach(key => {
|
||||||
if (typeof result.value['action_' + key] === 'boolean') {
|
if (typeof result.value.action[key] === 'boolean') {
|
||||||
filterData.action[key] = result.value['action_' + key];
|
filterData.action[key] = result.value.action[key];
|
||||||
hasAction = true;
|
hasAction = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
['forward', 'targetUrl'].forEach(key => {
|
let targets = result.value.action.targets;
|
||||||
if (result.value['action_' + key]) {
|
|
||||||
filterData.action[key] = result.value['action_' + key];
|
if (targets) {
|
||||||
hasAction = true;
|
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 => {
|
let checkFilterMailbox = done => {
|
||||||
if (!result.value.action_mailbox) {
|
if (!result.value.action.mailbox) {
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
db.database.collection('mailboxes').findOne(
|
db.database.collection('mailboxes').findOne(
|
||||||
{
|
{
|
||||||
_id: new ObjectID(result.value.action_mailbox),
|
_id: new ObjectID(result.value.action.mailbox),
|
||||||
user
|
user
|
||||||
},
|
},
|
||||||
(err, mailboxData) => {
|
(err, mailboxData) => {
|
||||||
|
|
@ -713,19 +762,20 @@ module.exports = (db, server) => {
|
||||||
* @apiParam {String} user Users unique ID.
|
* @apiParam {String} user Users unique ID.
|
||||||
* @apiParam {String} filter Filters unique ID.
|
* @apiParam {String} filter Filters unique ID.
|
||||||
* @apiParam {String} [name] Name of the Filter
|
* @apiParam {String} [name] Name of the Filter
|
||||||
* @apiParam {String} [query_from] Partial match for the From: header (case insensitive)
|
* @apiParam {Object} query Rules that a message must match
|
||||||
* @apiParam {String} [query_to] Partial match for the To:/Cc: headers (case insensitive)
|
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
|
||||||
* @apiParam {String} [query_subject] Partial match for the Subject: header (case insensitive)
|
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
|
||||||
* @apiParam {String} [query_text] Fulltext search against message text
|
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
|
||||||
* @apiParam {Bolean} [query_ha] Does a message have to have an attachment or not
|
* @apiParam {String} [query.text] Fulltext search against message text
|
||||||
* @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} [query.ha] Does a message have to have an attachment or not
|
||||||
* @apiParam {Bolean} [action_seen] If true then mark matching messages as Seen
|
* @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_flag] If true then mark matching messages as Flagged
|
* @apiParam {Object} action Action to take with a matching message
|
||||||
* @apiParam {Bolean} [action_delete] If true then do not store matching messages
|
* @apiParam {Bolean} [action.seen] If true then mark matching messages as Seen
|
||||||
* @apiParam {Bolean} [action_spam] If true then store matching messags to Junk Mail folder
|
* @apiParam {Bolean} [action.flag] If true then mark matching messages as Flagged
|
||||||
* @apiParam {String} [action_mailbox] Mailbox ID to store matching messages to
|
* @apiParam {Bolean} [action.delete] If true then do not store matching messages
|
||||||
* @apiParam {String} [action_forward] An email address where matching messages should be forwarded to
|
* @apiParam {Bolean} [action.spam] If true then store matching messags to Junk Mail folder
|
||||||
* @apiParam {String} [action_targetUrl] An URL where matching messages should be POSTed to
|
* @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 {Boolean} success Indicates successful response
|
||||||
* @apiSuccess {String} id ID for the created Filter
|
* @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 \
|
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45/filters/5a1c0ee490a34c67e266931c \
|
||||||
* -H 'Content-type: application/json' \
|
* -H 'Content-type: application/json' \
|
||||||
* -d '{
|
* -d '{
|
||||||
* "action_seen": "",
|
* "action": {
|
||||||
* "action_flag": true
|
* "seen": "",
|
||||||
|
* "flag": true
|
||||||
|
* }
|
||||||
* }'
|
* }'
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Success-Response:
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
|
@ -772,60 +824,60 @@ module.exports = (db, server) => {
|
||||||
.max(255)
|
.max(255)
|
||||||
.empty(''),
|
.empty(''),
|
||||||
|
|
||||||
query_from: Joi.string()
|
query: Joi.object().keys({
|
||||||
.trim()
|
from: Joi.string()
|
||||||
.max(255)
|
.trim()
|
||||||
.empty(''),
|
.max(255)
|
||||||
query_to: Joi.string()
|
.empty(''),
|
||||||
.trim()
|
to: Joi.string()
|
||||||
.max(255)
|
.trim()
|
||||||
.empty(''),
|
.max(255)
|
||||||
query_subject: Joi.string()
|
.empty(''),
|
||||||
.trim()
|
subject: Joi.string()
|
||||||
.max(255)
|
.trim()
|
||||||
.empty(''),
|
.max(255)
|
||||||
query_text: Joi.string()
|
.empty(''),
|
||||||
.trim()
|
text: Joi.string()
|
||||||
.max(255)
|
.trim()
|
||||||
.empty(''),
|
.max(255)
|
||||||
query_ha: Joi.boolean()
|
.empty(''),
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
ha: Joi.boolean()
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
.empty(''),
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
query_size: Joi.number().empty(''),
|
.empty(''),
|
||||||
|
size: Joi.number().empty('')
|
||||||
action_seen: Joi.boolean()
|
}),
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
action: Joi.object().keys({
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
seen: Joi.boolean()
|
||||||
.empty(''),
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
action_flag: Joi.boolean()
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
.empty(''),
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
flag: Joi.boolean()
|
||||||
.empty(''),
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
action_delete: Joi.boolean()
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
.empty(''),
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
delete: Joi.boolean()
|
||||||
.empty(''),
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
action_spam: Joi.boolean()
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
.empty(''),
|
||||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
spam: Joi.boolean()
|
||||||
.empty(''),
|
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||||
|
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||||
action_mailbox: Joi.string()
|
.empty(''),
|
||||||
.hex()
|
mailbox: Joi.string()
|
||||||
.lowercase()
|
.hex()
|
||||||
.length(24)
|
.lowercase()
|
||||||
.empty(''),
|
.length(24)
|
||||||
action_forward: Joi.string()
|
.empty(''),
|
||||||
.email()
|
targets: Joi.array().items(
|
||||||
.empty(''),
|
Joi.string().email(),
|
||||||
action_targetUrl: Joi.string()
|
Joi.string().uri({
|
||||||
.uri({
|
scheme: [/smtps?/, /https?/],
|
||||||
scheme: ['http', 'https'],
|
allowRelative: false,
|
||||||
allowRelative: false,
|
relativeOnly: false
|
||||||
relativeOnly: false
|
})
|
||||||
})
|
)
|
||||||
.empty('')
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = Joi.validate(req.params, schema, {
|
const result = Joi.validate(req.params, schema, {
|
||||||
|
|
@ -847,78 +899,117 @@ module.exports = (db, server) => {
|
||||||
let $set = {};
|
let $set = {};
|
||||||
let $unset = {};
|
let $unset = {};
|
||||||
|
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
if (result.value.name) {
|
if (result.value.name) {
|
||||||
$set.name = result.value.name;
|
$set.name = result.value.name;
|
||||||
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasQuery = false;
|
if (result.value.query) {
|
||||||
let hasAction = false;
|
['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.text) {
|
||||||
if (result.value['query_' + key]) {
|
$set['query.text'] = result.value.query.text.replace(/\s+/g, ' ');
|
||||||
$set['query.headers.' + key] = result.value['query_' + key].replace(/\s+/g, ' ');
|
hasChanges = true;
|
||||||
hasQuery = true;
|
} else if ('text' in req.params.query) {
|
||||||
} else if ('query_' + key in req.params) {
|
$unset['query.text'] = true;
|
||||||
$unset['query.headers.' + key] = true;
|
hasChanges = true;
|
||||||
hasQuery = true;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (result.value.query_text) {
|
if (typeof result.value.query.ha === 'boolean') {
|
||||||
$set['query.text'] = result.value.query_text.replace(/\s+/g, ' ');
|
$set['query.ha'] = result.value.query.ha;
|
||||||
hasQuery = true;
|
hasChanges = true;
|
||||||
} else if ('query_text' in req.params) {
|
} else if ('ha' in req.params.query) {
|
||||||
$unset['query.text'] = true;
|
$unset['query.ha'] = true;
|
||||||
hasQuery = true;
|
hasChanges = 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;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
['forward', 'targetUrl'].forEach(key => {
|
if (result.value.query.size) {
|
||||||
if (result.value['action_' + key]) {
|
$set['query.size'] = result.value.query.size;
|
||||||
$set['action.' + key] = result.value['action_' + key];
|
hasChanges = true;
|
||||||
hasAction = true;
|
} else if ('size' in req.params.query) {
|
||||||
} else if ('action_' + key in req.params) {
|
$unset['query.size'] = true;
|
||||||
$unset['action.' + key] = true;
|
hasChanges = true;
|
||||||
hasAction = 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 => {
|
let checkFilterMailbox = done => {
|
||||||
if (!result.value.action_mailbox) {
|
if (!result.value.action) {
|
||||||
if ('action_mailbox' in req.params) {
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.value.action.mailbox) {
|
||||||
|
if ('mailbox' in req.params.action) {
|
||||||
$unset['action.mailbox'] = true;
|
$unset['action.mailbox'] = true;
|
||||||
hasAction = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
db.database.collection('mailboxes').findOne(
|
db.database.collection('mailboxes').findOne(
|
||||||
{
|
{
|
||||||
_id: new ObjectID(result.value.action_mailbox),
|
_id: new ObjectID(result.value.action.mailbox),
|
||||||
user
|
user
|
||||||
},
|
},
|
||||||
(err, mailboxData) => {
|
(err, mailboxData) => {
|
||||||
|
|
@ -936,14 +1027,14 @@ module.exports = (db, server) => {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
$set['action.mailbox'] = mailboxData._id;
|
$set['action.mailbox'] = mailboxData._id;
|
||||||
hasAction = true;
|
hasChanges = true;
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkFilterMailbox(() => {
|
checkFilterMailbox(() => {
|
||||||
if (!hasQuery && !hasAction) {
|
if (!hasChanges) {
|
||||||
res.json({
|
res.json({
|
||||||
error: 'No changes'
|
error: 'No changes'
|
||||||
});
|
});
|
||||||
|
|
@ -1041,16 +1132,24 @@ function getFilterStrings(filter, mailboxes) {
|
||||||
} else {
|
} else {
|
||||||
return ['keep in INBOX'];
|
return ['keep in INBOX'];
|
||||||
}
|
}
|
||||||
case 'forward':
|
case 'targets':
|
||||||
if (filter.action[key]) {
|
if (filter.action[key]) {
|
||||||
return ['forward to', filter.action[key]];
|
return [
|
||||||
}
|
'forward to',
|
||||||
break;
|
filter.action[key]
|
||||||
case 'targetUrl':
|
.map(target => {
|
||||||
if (filter.action[key]) {
|
switch (target.type) {
|
||||||
let url = filter.action[key];
|
case 'http': {
|
||||||
let parsed = urllib.parse(url);
|
let parsed = urllib.parse(target.value);
|
||||||
return ['upload to', parsed.hostname || parsed.host];
|
return parsed.hostname || parsed.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return target.value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'spam':
|
case 'spam':
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@ module.exports = (db, server, messageHandler) => {
|
||||||
* @apiSuccess {String} results.intro First 128 bytes of the message
|
* @apiSuccess {String} results.intro First 128 bytes of the message
|
||||||
* @apiSuccess {Boolean} results.attachments Does the message have attachments
|
* @apiSuccess {Boolean} results.attachments Does the message have attachments
|
||||||
* @apiSuccess {Boolean} results.seen Is this message alread seen or not
|
* @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.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.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 {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 {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
|
* @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,
|
* "deleted": false,
|
||||||
* "flagged": true,
|
* "flagged": true,
|
||||||
* "draft": false,
|
* "draft": false,
|
||||||
|
* "answered": false,
|
||||||
|
* "forwarded": false,
|
||||||
* "url": "/users/59fc66a03e54454869460e45/mailboxes/59fc66a03e54454869460e46/messages/1",
|
* "url": "/users/59fc66a03e54454869460e45/mailboxes/59fc66a03e54454869460e46/messages/1",
|
||||||
* "contentType": {
|
* "contentType": {
|
||||||
* "value": "multipart/mixed",
|
* "value": "multipart/mixed",
|
||||||
|
|
@ -233,7 +237,8 @@ module.exports = (db, server, messageHandler) => {
|
||||||
undeleted: true,
|
undeleted: true,
|
||||||
flagged: true,
|
flagged: true,
|
||||||
draft: true,
|
draft: true,
|
||||||
thread: true
|
thread: true,
|
||||||
|
flags: true
|
||||||
},
|
},
|
||||||
paginatedField: 'idate',
|
paginatedField: 'idate',
|
||||||
sortAscending
|
sortAscending
|
||||||
|
|
@ -663,7 +668,8 @@ module.exports = (db, server, messageHandler) => {
|
||||||
undeleted: true,
|
undeleted: true,
|
||||||
flagged: true,
|
flagged: true,
|
||||||
draft: true,
|
draft: true,
|
||||||
thread: true
|
thread: true,
|
||||||
|
flags: true
|
||||||
},
|
},
|
||||||
paginatedField: '_id',
|
paginatedField: '_id',
|
||||||
sortAscending: false
|
sortAscending: false
|
||||||
|
|
@ -904,6 +910,7 @@ module.exports = (db, server, messageHandler) => {
|
||||||
undeleted: true,
|
undeleted: true,
|
||||||
flagged: true,
|
flagged: true,
|
||||||
draft: true,
|
draft: true,
|
||||||
|
flags: true,
|
||||||
attachments: true,
|
attachments: true,
|
||||||
html: true,
|
html: true,
|
||||||
text: true,
|
text: true,
|
||||||
|
|
@ -1028,6 +1035,8 @@ module.exports = (db, server, messageHandler) => {
|
||||||
deleted: !messageData.undeleted,
|
deleted: !messageData.undeleted,
|
||||||
flagged: messageData.flagged,
|
flagged: messageData.flagged,
|
||||||
draft: messageData.draft,
|
draft: messageData.draft,
|
||||||
|
answered: messageData.flags.includes('\\Answered'),
|
||||||
|
forwarded: messageData.flags.includes('$Forwarded'),
|
||||||
html: messageData.html,
|
html: messageData.html,
|
||||||
text: messageData.text,
|
text: messageData.text,
|
||||||
forwardTargets: messageData.forwardTargets,
|
forwardTargets: messageData.forwardTargets,
|
||||||
|
|
@ -2439,7 +2448,8 @@ module.exports = (db, server, messageHandler) => {
|
||||||
undeleted: true,
|
undeleted: true,
|
||||||
flagged: true,
|
flagged: true,
|
||||||
draft: true,
|
draft: true,
|
||||||
thread: true
|
thread: true,
|
||||||
|
flags: true
|
||||||
},
|
},
|
||||||
paginatedField: '_id',
|
paginatedField: '_id',
|
||||||
sortAscending
|
sortAscending
|
||||||
|
|
@ -2669,6 +2679,7 @@ module.exports = (db, server, messageHandler) => {
|
||||||
undeleted: true,
|
undeleted: true,
|
||||||
flagged: true,
|
flagged: true,
|
||||||
draft: true,
|
draft: true,
|
||||||
|
flags: true,
|
||||||
attachments: true,
|
attachments: true,
|
||||||
html: true,
|
html: true,
|
||||||
text: true,
|
text: true,
|
||||||
|
|
@ -2776,6 +2787,8 @@ module.exports = (db, server, messageHandler) => {
|
||||||
deleted: !messageData.undeleted,
|
deleted: !messageData.undeleted,
|
||||||
flagged: messageData.flagged,
|
flagged: messageData.flagged,
|
||||||
draft: messageData.draft,
|
draft: messageData.draft,
|
||||||
|
answered: messageData.flags.includes('\\Answered'),
|
||||||
|
forwarded: messageData.flags.includes('$Forwarded'),
|
||||||
html: messageData.html,
|
html: messageData.html,
|
||||||
text: messageData.text,
|
text: messageData.text,
|
||||||
forwardTargets: messageData.forwardTargets,
|
forwardTargets: messageData.forwardTargets,
|
||||||
|
|
@ -3124,7 +3137,9 @@ function formatMessageListing(messageData) {
|
||||||
seen: !messageData.unseen,
|
seen: !messageData.unseen,
|
||||||
deleted: !messageData.undeleted,
|
deleted: !messageData.undeleted,
|
||||||
flagged: messageData.flagged,
|
flagged: messageData.flagged,
|
||||||
draft: messageData.draft
|
draft: messageData.draft,
|
||||||
|
answered: messageData.flags.includes('\\Answered'),
|
||||||
|
forwarded: messageData.flags.includes('$Forwarded')
|
||||||
};
|
};
|
||||||
|
|
||||||
let parsedContentType = parsedHeader['content-type'];
|
let parsedContentType = parsedHeader['content-type'];
|
||||||
|
|
|
||||||
|
|
@ -76,106 +76,144 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
||||||
return done(null, false);
|
return done(null, false);
|
||||||
}
|
}
|
||||||
query.user = user;
|
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 getMessage = next => {
|
||||||
let subject = headers.subject || '';
|
let updateable = ['reply', 'replyAll', 'forward'];
|
||||||
try {
|
if (!options.reference || !updateable.includes(options.reference.action)) {
|
||||||
subject = libmime.decodeWords(subject).trim();
|
return db.database.collection('messages').findOne(
|
||||||
} catch (E) {
|
query,
|
||||||
// failed to parse value
|
{
|
||||||
}
|
fields: {
|
||||||
|
'mimeTree.parsedHeader': true,
|
||||||
if (!/^\w+: /.test(subject)) {
|
thread: true
|
||||||
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);
|
},
|
||||||
}
|
next
|
||||||
};
|
);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
);
|
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) => {
|
getReferencedMessage((err, referenceData) => {
|
||||||
|
|
@ -288,57 +326,56 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
||||||
let collector = new StreamCollect();
|
let collector = new StreamCollect();
|
||||||
let compiledEnvelope = compiled.getEnvelope();
|
let compiledEnvelope = compiled.getEnvelope();
|
||||||
|
|
||||||
messageHandler.counters.ttlcounter(
|
let messageId = new ObjectID();
|
||||||
'wdr:' + userData._id.toString(),
|
let addToDeliveryQueue = next => {
|
||||||
compiledEnvelope.to.length,
|
if (!compiledEnvelope.to || !compiledEnvelope.to.length || options.uploadOnly) {
|
||||||
userData.recipients,
|
// no delivery, just build the message
|
||||||
false,
|
collector.on('data', () => false); //drain
|
||||||
(err, result) => {
|
collector.on('end', () => {
|
||||||
if (err) {
|
next(null, false);
|
||||||
err.code = 'ERRREDIS';
|
});
|
||||||
return callback(err);
|
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;
|
messageHandler.counters.ttlcounter(
|
||||||
let sent = result.value;
|
'wdr:' + userData._id.toString(),
|
||||||
let ttl = result.ttl;
|
compiledEnvelope.to.length,
|
||||||
|
userData.recipients,
|
||||||
let ttlHuman = false;
|
false,
|
||||||
if (ttl) {
|
(err, result) => {
|
||||||
if (ttl < 60) {
|
if (err) {
|
||||||
ttlHuman = ttl + ' seconds';
|
err.code = 'ERRREDIS';
|
||||||
} else if (ttl < 3600) {
|
return callback(err);
|
||||||
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
|
||||||
} else {
|
|
||||||
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
let success = result.success;
|
||||||
log.info('API', 'RCPTDENY denied sent=%s allowed=%s expires=%ss.', sent, userData.recipients, ttl);
|
let sent = result.value;
|
||||||
let err = new Error(
|
let ttl = result.ttl;
|
||||||
'You reached a daily sending limit for your account' + (ttl ? '. Limit expires in ' + ttlHuman : '')
|
|
||||||
);
|
|
||||||
err.code = 'ERRSENDINGLIMIT';
|
|
||||||
return setImmediate(() => callback(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageId = new ObjectID();
|
let ttlHuman = false;
|
||||||
let addToDeliveryQueue = next => {
|
if (ttl) {
|
||||||
if (!compiledEnvelope.to || !compiledEnvelope.to.length || options.uploadOnly) {
|
if (ttl < 60) {
|
||||||
// no delivery, just build the message
|
ttlHuman = ttl + ' seconds';
|
||||||
collector.on('data', () => false); //drain
|
} else if (ttl < 3600) {
|
||||||
collector.on('end', () => {
|
ttlHuman = Math.round(ttl / 60) + ' minutes';
|
||||||
next(null, false);
|
} else {
|
||||||
});
|
ttlHuman = Math.round(ttl / 3600) + ' hours';
|
||||||
collector.once('error', err => {
|
}
|
||||||
next(err);
|
}
|
||||||
});
|
|
||||||
let stream = compiled.createReadStream();
|
|
||||||
stream.once('error', err => collector.emit('error', err));
|
|
||||||
stream.pipe(collector);
|
|
||||||
|
|
||||||
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
|
// push message to outbound queue
|
||||||
|
|
@ -368,82 +405,94 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
||||||
stream.once('error', err => message.emit('error', err));
|
stream.once('error', err => message.emit('error', err));
|
||||||
stream.pipe(collector).pipe(message);
|
stream.pipe(collector).pipe(message);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
addToDeliveryQueue((err, outbound) => {
|
addToDeliveryQueue((err, outbound) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
// ignore
|
// 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');
|
let meta = {
|
||||||
return callback(null, {
|
source: 'API',
|
||||||
id: false,
|
from: compiledEnvelope.from,
|
||||||
mailbox: false,
|
to: compiledEnvelope.to,
|
||||||
queueId: outbound,
|
origin: options.ip,
|
||||||
overQuota: true
|
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
|
let messageOptions = {
|
||||||
messageHandler.encryptMessage(
|
user: userData._id,
|
||||||
userData.encryptMessages ? userData.pubKey : false,
|
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox
|
||||||
{ chunks: collector.chunks, chunklen: collector.chunklen },
|
? new ObjectID(options.mailbox)
|
||||||
(err, encrypted) => {
|
: options.isDraft ? '\\Drafts' : '\\Sent',
|
||||||
let raw = false;
|
|
||||||
if (!err && encrypted) {
|
|
||||||
// message was encrypted, so use the result instead of raw
|
|
||||||
raw = encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageOptions = {
|
outbound,
|
||||||
user: userData._id,
|
|
||||||
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox ? new ObjectID(options.mailbox) : '\\Sent',
|
|
||||||
|
|
||||||
outbound,
|
meta,
|
||||||
|
|
||||||
meta: {
|
date: false,
|
||||||
source: 'API',
|
flags: ['\\Seen'].concat(options.isDraft ? '\\Draft' : []),
|
||||||
from: compiledEnvelope.from,
|
|
||||||
to: compiledEnvelope.to,
|
|
||||||
origin: options.ip,
|
|
||||||
sess: options.sess,
|
|
||||||
time: new Date()
|
|
||||||
},
|
|
||||||
|
|
||||||
date: false,
|
// always insert drafts, otherwise skip
|
||||||
flags: ['\\Seen'],
|
skipExisting: !options.isDraft
|
||||||
|
};
|
||||||
|
|
||||||
// if similar message exists, then skip
|
if (raw) {
|
||||||
skipExisting: true
|
messageOptions.raw = raw;
|
||||||
};
|
} else {
|
||||||
|
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
||||||
|
}
|
||||||
|
|
||||||
if (raw) {
|
messageHandler.add(messageOptions, (err, success, info) => {
|
||||||
messageOptions.raw = raw;
|
if (err) {
|
||||||
} else {
|
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
|
||||||
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
|
err.code = 'SUBMITFAIL';
|
||||||
}
|
return callback(err);
|
||||||
|
} else if (!info) {
|
||||||
messageHandler.add(messageOptions, (err, success, info) => {
|
log.info('API', 'SUBMITSKIP user=%s message=already exists', user);
|
||||||
if (err) {
|
return callback(null, false);
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
});
|
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} 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 {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} [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 {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] 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
|
* @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.filename] Attachment filename
|
||||||
* @apiParam {String} [attachments.contentType] MIME type for the attachment file
|
* @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 {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} [sess] Session identifier for the logs
|
||||||
* @apiParam {String} [ip] IP address for the logs
|
* @apiParam {String} [ip] IP address for the logs
|
||||||
*
|
*
|
||||||
|
|
@ -713,6 +764,7 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
||||||
.max(255)
|
.max(255)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
meta: Joi.object().unknown(true),
|
||||||
sess: Joi.string().max(255),
|
sess: Joi.string().max(255),
|
||||||
ip: Joi.string().ip({
|
ip: Joi.string().ip({
|
||||||
version: ['ipv4', 'ipv6'],
|
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.name Name of the User
|
||||||
* @apiSuccess {String} results.address Main email address 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.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.encryptMessages If <code>true</code> then received messages are encrypted
|
||||||
* @apiSuccess {Boolean} results.encryptForwarded If <code>true</code> then forwarded messages are encrypted
|
* @apiSuccess {Boolean} results.encryptForwarded If <code>true</code> then forwarded messages are encrypted
|
||||||
* @apiSuccess {Object} results.quota Quota usage limits
|
* @apiSuccess {Object} results.quota Quota usage limits
|
||||||
|
|
@ -216,8 +216,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
address: true,
|
address: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
storageUsed: true,
|
storageUsed: true,
|
||||||
forward: true,
|
targets: true,
|
||||||
targetUrl: true,
|
|
||||||
quota: true,
|
quota: true,
|
||||||
activated: true,
|
activated: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
|
@ -260,8 +259,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
name: userData.name,
|
name: userData.name,
|
||||||
address: userData.address,
|
address: userData.address,
|
||||||
tags: userData.tags || [],
|
tags: userData.tags || [],
|
||||||
forward: [].concat(userData.forward || []),
|
targets: userData.targets && userData.targets.map(t => t.value),
|
||||||
targetUrl: userData.targetUrl,
|
|
||||||
encryptMessages: !!userData.encryptMessages,
|
encryptMessages: !!userData.encryptMessages,
|
||||||
encryptForwarded: !!userData.encryptForwarded,
|
encryptForwarded: !!userData.encryptForwarded,
|
||||||
quota: {
|
quota: {
|
||||||
|
|
@ -302,8 +300,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
* @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} [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} [language] Language code for the User
|
||||||
* @apiParam {String[]} [forward] A list of email addresses to forward 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 {String} [targetUrl] An URL to post all incoming emails
|
|
||||||
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
||||||
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
||||||
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
||||||
|
|
@ -375,8 +372,14 @@ module.exports = (db, server, userHandler) => {
|
||||||
.default(0),
|
.default(0),
|
||||||
|
|
||||||
name: Joi.string().max(256),
|
name: Joi.string().max(256),
|
||||||
forward: Joi.array().items(Joi.string().email()),
|
targets: Joi.array().items(
|
||||||
targetUrl: Joi.string().max(256),
|
Joi.string().email(),
|
||||||
|
Joi.string().uri({
|
||||||
|
scheme: [/smtps?/, /https?/],
|
||||||
|
allowRelative: false,
|
||||||
|
relativeOnly: false
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
quota: Joi.number()
|
quota: Joi.number()
|
||||||
.min(0)
|
.min(0)
|
||||||
|
|
@ -434,8 +437,40 @@ module.exports = (db, server, userHandler) => {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.value.forward) {
|
let targets = result.value.targets;
|
||||||
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
|
||||||
|
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) {
|
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.name Name listed in public key
|
||||||
* @apiSuccess {String} keyInfo.address E-mail address 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} keyInfo.fingerprint Fingerprint of the public key
|
||||||
* @apiSuccess {String[]} forward A list of email addresses to forward all incoming emails
|
* @apiSuccess {String[]} targets List of forwarding targets
|
||||||
* @apiSuccess {String} targetUrl An URL to post all incoming emails
|
|
||||||
* @apiSuccess {Object} limits Account limits and usage
|
* @apiSuccess {Object} limits Account limits and usage
|
||||||
* @apiSuccess {Object} limits.quota Quota usage limits
|
* @apiSuccess {Object} limits.quota Quota usage limits
|
||||||
* @apiSuccess {Number} limits.quota.allowed Allowed quota of the user in bytes
|
* @apiSuccess {Number} limits.quota.allowed Allowed quota of the user in bytes
|
||||||
|
|
@ -654,8 +688,10 @@ module.exports = (db, server, userHandler) => {
|
||||||
* "encryptForwarded": false,
|
* "encryptForwarded": false,
|
||||||
* "pubKey": "",
|
* "pubKey": "",
|
||||||
* "keyInfo": false,
|
* "keyInfo": false,
|
||||||
* "forward": [],
|
* "targets": [
|
||||||
* "targetUrl": "",
|
* "my.old.address@example.com",
|
||||||
|
* "smtp://mx2.zone.eu:25"
|
||||||
|
* ],
|
||||||
* "limits": {
|
* "limits": {
|
||||||
* "quota": {
|
* "quota": {
|
||||||
* "allowed": 107374182400,
|
* "allowed": 107374182400,
|
||||||
|
|
@ -771,8 +807,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
pubKey: userData.pubKey,
|
pubKey: userData.pubKey,
|
||||||
keyInfo: getKeyInfo(userData.pubKey),
|
keyInfo: getKeyInfo(userData.pubKey),
|
||||||
|
|
||||||
forward: [].concat(userData.forward || []),
|
targets: [].concat(userData.targets || []),
|
||||||
targetUrl: userData.targetUrl,
|
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
quota: {
|
quota: {
|
||||||
|
|
@ -823,8 +858,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
* @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} [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} [language] Language code for the User
|
||||||
* @apiParam {String[]} [forward] A list of email addresses to forward 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 {String} [targetUrl] An URL to post all incoming emails
|
|
||||||
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
||||||
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
||||||
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
||||||
|
|
@ -884,10 +918,14 @@ module.exports = (db, server, userHandler) => {
|
||||||
name: Joi.string()
|
name: Joi.string()
|
||||||
.empty('')
|
.empty('')
|
||||||
.max(256),
|
.max(256),
|
||||||
forward: Joi.array().items(Joi.string().email()),
|
targets: Joi.array().items(
|
||||||
targetUrl: Joi.string()
|
Joi.string().email(),
|
||||||
.empty('')
|
Joi.string().uri({
|
||||||
.max(256),
|
scheme: [/smtps?/, /https?/],
|
||||||
|
allowRelative: false,
|
||||||
|
relativeOnly: false
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
pubKey: Joi.string()
|
pubKey: Joi.string()
|
||||||
.empty('')
|
.empty('')
|
||||||
|
|
@ -942,12 +980,40 @@ module.exports = (db, server, userHandler) => {
|
||||||
|
|
||||||
let user = new ObjectID(result.value.user);
|
let user = new ObjectID(result.value.user);
|
||||||
|
|
||||||
if (result.value.forward) {
|
let targets = result.value.targets;
|
||||||
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.value.targetUrl && 'targetUrl' in req.params) {
|
if (targets) {
|
||||||
result.value.targetUrl = '';
|
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) {
|
if (!result.value.name && 'name' in req.params) {
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,7 @@ class FilterHandler {
|
||||||
let fields = {
|
let fields = {
|
||||||
name: true,
|
name: true,
|
||||||
forwards: true,
|
forwards: true,
|
||||||
forward: true,
|
targets: true,
|
||||||
targetUrl: true,
|
|
||||||
autoreply: true,
|
autoreply: true,
|
||||||
encryptMessages: true,
|
encryptMessages: true,
|
||||||
encryptForwarded: true,
|
encryptForwarded: true,
|
||||||
|
|
@ -266,16 +265,9 @@ class FilterHandler {
|
||||||
|
|
||||||
// apply matching filter
|
// apply matching filter
|
||||||
Object.keys(filter.action).forEach(key => {
|
Object.keys(filter.action).forEach(key => {
|
||||||
if (key === 'forward') {
|
if (key === 'targets') {
|
||||||
[].concat(filter.action[key] || []).forEach(address => {
|
[].concat(filter.action[key] || []).forEach(target => {
|
||||||
forwardTargets.set(address, { type: 'mail', value: address });
|
forwardTargets.set(target.value, target);
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'targetUrl') {
|
|
||||||
[].concat(filter.action[key] || []).forEach(address => {
|
|
||||||
forwardTargets.set(address, { type: 'http', value: address });
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -328,20 +320,13 @@ class FilterHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
let forwardMessage = done => {
|
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
|
// forward to default recipient only if the message is not deleted
|
||||||
(Array.isArray(userData.forward) ? userData.forward : [].concat(userData.forward || [])).forEach(forward => {
|
userData.targets.forEach(target => {
|
||||||
if (forward) {
|
forwardTargets.set(target.value, target);
|
||||||
forwardTargets.set(forward, { type: 'mail', value: forward });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// never forward messages marked as spam
|
||||||
if (!forwardTargets.size || filterActions.get('spam')) {
|
if (!forwardTargets.size || filterActions.get('spam')) {
|
||||||
return setImmediate(done);
|
return setImmediate(done);
|
||||||
|
|
|
||||||
|
|
@ -748,8 +748,7 @@ class UserHandler {
|
||||||
recipients: data.recipients || 0,
|
recipients: data.recipients || 0,
|
||||||
forwards: data.forwards || 0,
|
forwards: data.forwards || 0,
|
||||||
|
|
||||||
forward: [].concat(data.forward || []),
|
targets: [].concat(data.targets || []),
|
||||||
targetUrl: data.targetUrl || '',
|
|
||||||
|
|
||||||
// autoreply status
|
// autoreply status
|
||||||
// off by default, can be changed later by user through the API
|
// 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,
|
name: true,
|
||||||
forwards: true,
|
forwards: true,
|
||||||
forward: true,
|
targets: true,
|
||||||
targetUrl: true,
|
|
||||||
autoreply: true,
|
autoreply: true,
|
||||||
encryptMessages: true,
|
encryptMessages: true,
|
||||||
encryptForwarded: true,
|
encryptForwarded: true,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
"test": "mongo --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && NODE_ENV=test grunt",
|
"test": "mongo --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && NODE_ENV=test grunt",
|
||||||
"apidoc": "apidoc -i lib/api/ -o docs/"
|
"apidoc": "apidoc -i lib/api/ -o docs/"
|
||||||
},
|
},
|
||||||
"keywords": ["imap", "mail server"],
|
"keywords": [
|
||||||
|
"imap",
|
||||||
|
"mail server"
|
||||||
|
],
|
||||||
"author": "Andris Reinman",
|
"author": "Andris Reinman",
|
||||||
"license": "EUPL-1.1+",
|
"license": "EUPL-1.1+",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -40,7 +43,7 @@
|
||||||
"iconv-lite": "0.4.19",
|
"iconv-lite": "0.4.19",
|
||||||
"ioredfour": "1.0.2-ioredis",
|
"ioredfour": "1.0.2-ioredis",
|
||||||
"ioredis": "3.2.2",
|
"ioredis": "3.2.2",
|
||||||
"joi": "13.1.0",
|
"joi": "13.1.1",
|
||||||
"js-yaml": "3.10.0",
|
"js-yaml": "3.10.0",
|
||||||
"key-fingerprint": "1.1.0",
|
"key-fingerprint": "1.1.0",
|
||||||
"libbase64": "1.0.2",
|
"libbase64": "1.0.2",
|
||||||
|
|
@ -53,7 +56,7 @@
|
||||||
"mongodb": "3.0.1",
|
"mongodb": "3.0.1",
|
||||||
"mongodb-extended-json": "1.10.0",
|
"mongodb-extended-json": "1.10.0",
|
||||||
"node-forge": "^0.7.1",
|
"node-forge": "^0.7.1",
|
||||||
"nodemailer": "4.4.1",
|
"nodemailer": "4.4.2",
|
||||||
"npmlog": "4.1.2",
|
"npmlog": "4.1.2",
|
||||||
"openpgp": "2.6.1",
|
"openpgp": "2.6.1",
|
||||||
"qrcode": "1.2.0",
|
"qrcode": "1.2.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue