mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-10 15:14:40 +08:00
Added API endpoint to push changes in user account
This commit is contained in:
parent
ad1ac6d280
commit
ba3e111ed2
17 changed files with 388 additions and 140 deletions
82
README.md
82
README.md
|
@ -148,7 +148,7 @@ If a messages is downloaded by a client this message gets marked as _Seen_
|
||||||
|
|
||||||
If a messages is deleted by a client this message gets marked as Seen and moved to Trash folder
|
If a messages is deleted by a client this message gets marked as Seen and moved to Trash folder
|
||||||
|
|
||||||
## HTTP API
|
# HTTP API
|
||||||
|
|
||||||
> **NB!** The HTTP API is being re-designed
|
> **NB!** The HTTP API is being re-designed
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ TODO:
|
||||||
2. Search/list messages
|
2. Search/list messages
|
||||||
3. Expose journal updates through WebSocket or similar
|
3. Expose journal updates through WebSocket or similar
|
||||||
|
|
||||||
#### Responses
|
### Responses
|
||||||
|
|
||||||
All failed responses look like the following:
|
All failed responses look like the following:
|
||||||
|
|
||||||
|
@ -170,13 +170,13 @@ All failed responses look like the following:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Users
|
## Users
|
||||||
|
|
||||||
User accounts
|
User accounts
|
||||||
|
|
||||||
#### Get one user
|
### Get one user
|
||||||
|
|
||||||
##### GET /users/{user}
|
#### GET /users/{user}
|
||||||
|
|
||||||
Returns data about a specific user
|
Returns data about a specific user
|
||||||
|
|
||||||
|
@ -220,9 +220,9 @@ Response for a successful operation:
|
||||||
|
|
||||||
Recipient/forward limits assume that messages are sent using ZoneMTA with [zonemta-wildduck](https://github.com/wildduck-email/zonemta-wildduck) plugin, otherwise the counters are not updated.
|
Recipient/forward limits assume that messages are sent using ZoneMTA with [zonemta-wildduck](https://github.com/wildduck-email/zonemta-wildduck) plugin, otherwise the counters are not updated.
|
||||||
|
|
||||||
#### Add a new user
|
### Add a new user
|
||||||
|
|
||||||
##### POST /users
|
#### POST /users
|
||||||
|
|
||||||
Creates a new user, returns the ID upon success.
|
Creates a new user, returns the ID upon success.
|
||||||
|
|
||||||
|
@ -258,9 +258,9 @@ Response for a successful operation:
|
||||||
|
|
||||||
After you have created an user you can use these credentials to log in to the IMAP server.
|
After you have created an user you can use these credentials to log in to the IMAP server.
|
||||||
|
|
||||||
#### Update user details
|
### Update user details
|
||||||
|
|
||||||
##### PUT /users/{user}
|
#### PUT /users/{user}
|
||||||
|
|
||||||
Updates the properties of an user. Only specify these fields that you want to be updated.
|
Updates the properties of an user. Only specify these fields that you want to be updated.
|
||||||
|
|
||||||
|
@ -292,13 +292,13 @@ Response for a successful operation:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### UserAddresses
|
## UserAddresses
|
||||||
|
|
||||||
Manage email addresses and aliases for an user.
|
Manage email addresses and aliases for an user.
|
||||||
|
|
||||||
#### List addresses
|
### List addresses
|
||||||
|
|
||||||
##### GET /users/{user}/addresses
|
#### GET /users/{user}/addresses
|
||||||
|
|
||||||
Lists all registered email addresses for an user.
|
Lists all registered email addresses for an user.
|
||||||
|
|
||||||
|
@ -334,9 +334,9 @@ Response for a successful operation:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get one address
|
### Get one address
|
||||||
|
|
||||||
##### GET /users/{user}/addresses/{address}
|
#### GET /users/{user}/addresses/{address}
|
||||||
|
|
||||||
Returns data about a specific address.
|
Returns data about a specific address.
|
||||||
|
|
||||||
|
@ -363,9 +363,9 @@ Response for a successful operation:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add a new address
|
### Add a new address
|
||||||
|
|
||||||
##### POST /users/{user}/addresses
|
#### POST /users/{user}/addresses
|
||||||
|
|
||||||
Creates a new email address alias for an existing user, returns the ID upon success.
|
Creates a new email address alias for an existing user, returns the ID upon success.
|
||||||
|
|
||||||
|
@ -394,9 +394,9 @@ Response for a successful operation:
|
||||||
|
|
||||||
After you have registered a new address then LMTP maildrop server starts accepting mail for it and stores messages to the users mailbox.
|
After you have registered a new address then LMTP maildrop server starts accepting mail for it and stores messages to the users mailbox.
|
||||||
|
|
||||||
#### Update address details
|
### Update address details
|
||||||
|
|
||||||
##### PUT /users/{user}/addresses/{address}
|
#### PUT /users/{user}/addresses/{address}
|
||||||
|
|
||||||
Updates the properties of an address. Currently, only `main` can be updated.
|
Updates the properties of an address. Currently, only `main` can be updated.
|
||||||
|
|
||||||
|
@ -421,9 +421,9 @@ Response for a successful operation:
|
||||||
"success": true
|
"success": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
#### Delete an alias address
|
### Delete an alias address
|
||||||
|
|
||||||
##### DELETE /users/{user}/addresses/{address}
|
#### DELETE /users/{user}/addresses/{address}
|
||||||
|
|
||||||
Deletes an email address alias from an existing user.
|
Deletes an email address alias from an existing user.
|
||||||
|
|
||||||
|
@ -446,9 +446,11 @@ Response for a successful operation:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Recalculate user quota
|
## UserQuota
|
||||||
|
|
||||||
##### POST /users/{user}/quota/reset
|
### Recalculate user quota
|
||||||
|
|
||||||
|
#### POST /users/{user}/quota/reset
|
||||||
|
|
||||||
Recalculates used storage for an user. Use this when it seems that quota counters for an user do not match with reality.
|
Recalculates used storage for an user. Use this when it seems that quota counters for an user do not match with reality.
|
||||||
|
|
||||||
|
@ -473,6 +475,40 @@ Response for a successful operation:
|
||||||
|
|
||||||
Be aware though that this method is not atomic and should be done only if quota counters are way off.
|
Be aware though that this method is not atomic and should be done only if quota counters are way off.
|
||||||
|
|
||||||
|
## UserUpdates
|
||||||
|
|
||||||
|
Get user related events as an Event Source stream
|
||||||
|
|
||||||
|
### Stream update events
|
||||||
|
|
||||||
|
#### GET /users/{user}/updates
|
||||||
|
|
||||||
|
Streams changes in user account as EventSource stream
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
- **user** (required) is the ID of the user
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```
|
||||||
|
curl "http://localhost:8080/users/59467f27535f8f0f067ba8e6/updates"
|
||||||
|
```
|
||||||
|
|
||||||
|
Response stream:
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"command":"EXISTS", "message":"596e0703f0bdd512aeac3600", "mailbox":"596c9c37ef2213165daadc65",...}
|
||||||
|
id: 596e0703f0bdd512aeac3605
|
||||||
|
|
||||||
|
data: {"command":"CREATE","mailbox":"596e09853f845a14f3620b5c","name":"My Mail",...}
|
||||||
|
id: 596e09853f845a14f3620b5d
|
||||||
|
```
|
||||||
|
|
||||||
|
First entry in the event stream indicates that a message with id `596e0703f0bdd512aeac3600` was added to mailbox `596c9c37ef2213165daadc65`, second entry indicates that a new mailbox called *"My Mail"* with id `596e09853f845a14f3620b5c` was created.
|
||||||
|
|
||||||
|
Be aware though that this connection needs to be properly closed if you do not want to end up with memory leaks.
|
||||||
|
|
||||||
## Message filtering
|
## Message filtering
|
||||||
|
|
||||||
> The filtering system is subject to change with the API updates. Most probably the filters are going to reside in separate collection and not as part of the user object.
|
> The filtering system is subject to change with the API updates. Most probably the filters are going to reside in separate collection and not as part of the user object.
|
||||||
|
@ -514,7 +550,7 @@ Filters are configuration objects stored in the `filters` array of the users obj
|
||||||
action: {
|
action: {
|
||||||
|
|
||||||
// mark message as seen
|
// mark message as seen
|
||||||
seen: true,
|
unseen: false,
|
||||||
|
|
||||||
// mark message as flagged
|
// mark message as flagged
|
||||||
flag: true,
|
flag: true,
|
||||||
|
|
162
api.js
162
api.js
|
@ -5,9 +5,11 @@ const restify = require('restify');
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const crypto = require('crypto');
|
||||||
const tools = require('./lib/tools');
|
const tools = require('./lib/tools');
|
||||||
const consts = require('./lib/consts');
|
const consts = require('./lib/consts');
|
||||||
const UserHandler = require('./lib/user-handler');
|
const UserHandler = require('./lib/user-handler');
|
||||||
|
const ImapNotifier = require('./lib/imap-notifier');
|
||||||
const db = require('./lib/db');
|
const db = require('./lib/db');
|
||||||
const certs = require('./lib/certs').get('api');
|
const certs = require('./lib/certs').get('api');
|
||||||
const ObjectID = require('mongodb').ObjectID;
|
const ObjectID = require('mongodb').ObjectID;
|
||||||
|
@ -27,6 +29,7 @@ if (certs && config.api.secure) {
|
||||||
const server = restify.createServer(serverOptions);
|
const server = restify.createServer(serverOptions);
|
||||||
|
|
||||||
let userHandler;
|
let userHandler;
|
||||||
|
let notifier;
|
||||||
|
|
||||||
server.use(restify.plugins.queryParser());
|
server.use(restify.plugins.queryParser());
|
||||||
server.use(
|
server.use(
|
||||||
|
@ -1034,6 +1037,161 @@ server.get('/users/:user/mailboxes/:mailbox', (req, res, next) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get('/users/:user/updates', (req, res, next) => {
|
||||||
|
res.charSet('utf-8');
|
||||||
|
|
||||||
|
const schema = Joi.object().keys({
|
||||||
|
user: Joi.string().hex().lowercase().length(24).required(),
|
||||||
|
'Last-Event-ID': Joi.string().hex().lowercase().length(24)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.header('Last-Event-ID')) {
|
||||||
|
req.params['Last-Event-ID'] = req.header('Last-Event-ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Joi.validate(req.params, schema, {
|
||||||
|
abortEarly: false,
|
||||||
|
convert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
res.json({
|
||||||
|
error: result.error.message
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = new ObjectID(result.value.user);
|
||||||
|
let lastEventId = result.value['Last-Event-ID'] ? new ObjectID(result.value['Last-Event-ID']) : false;
|
||||||
|
|
||||||
|
db.users.collection('users').findOne({
|
||||||
|
_id: user
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
username: true,
|
||||||
|
address: true
|
||||||
|
}
|
||||||
|
}, (err, userData) => {
|
||||||
|
if (err) {
|
||||||
|
res.json({
|
||||||
|
error: 'MongoDB Error: ' + err.message
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (!userData) {
|
||||||
|
res.json({
|
||||||
|
error: 'This user does not exist'
|
||||||
|
});
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = { id: crypto.randomBytes(10).toString('base64'), user: { id: userData._id, username: userData.username } };
|
||||||
|
|
||||||
|
let journalReading = false;
|
||||||
|
let journalReader = () => {
|
||||||
|
if (journalReading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
journalReading = true;
|
||||||
|
loadJournalStream(req, res, user, lastEventId, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
// ignore?
|
||||||
|
}
|
||||||
|
lastEventId = info && info.lastEventId;
|
||||||
|
journalReading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let close = () => {
|
||||||
|
notifier.removeListener(session, '*', journalReader);
|
||||||
|
};
|
||||||
|
|
||||||
|
let setup = () => {
|
||||||
|
notifier.addListener(session, '*', journalReader);
|
||||||
|
|
||||||
|
let finished = false;
|
||||||
|
let done = () => {
|
||||||
|
if (finished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finished = true;
|
||||||
|
close();
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
req.connection.setTimeout(30 * 60 * 1000, done);
|
||||||
|
req.connection.on('end', done);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', Connection: 'close' });
|
||||||
|
|
||||||
|
if (lastEventId) {
|
||||||
|
loadJournalStream(req, res, user, lastEventId, setup);
|
||||||
|
} else {
|
||||||
|
db.database.collection('journal').findOne({ user }, { sort: { _id: -1 } }, (err, latest) => {
|
||||||
|
if (!err && latest) {
|
||||||
|
lastEventId = latest._id;
|
||||||
|
}
|
||||||
|
setup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatJournalData(e) {
|
||||||
|
let data = {};
|
||||||
|
Object.keys(e).forEach(key => {
|
||||||
|
if (!['_id', 'ignore', 'user'].includes(key)) {
|
||||||
|
data[key] = e[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = [];
|
||||||
|
response.push('data: ' + JSON.stringify(data));
|
||||||
|
response.push('id: ' + e._id.toString());
|
||||||
|
|
||||||
|
return response.join('\n') + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJournalStream(req, res, user, lastEventId, done) {
|
||||||
|
let query = { user };
|
||||||
|
if (lastEventId) {
|
||||||
|
query._id = { $gt: lastEventId };
|
||||||
|
}
|
||||||
|
let cursor = db.database.collection('journal').find(query).sort({ _id: 1 });
|
||||||
|
let processed = 0;
|
||||||
|
let processNext = () => {
|
||||||
|
cursor.next((err, e) => {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
if (!e) {
|
||||||
|
return cursor.close(() => {
|
||||||
|
// delete all attachments that do not have any active links to message objects
|
||||||
|
done(null, {
|
||||||
|
lastEventId,
|
||||||
|
processed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEventId = e._id;
|
||||||
|
|
||||||
|
if (!e || !e.command) {
|
||||||
|
// skip
|
||||||
|
return processNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.write(formatJournalData(e));
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
processNext();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
processNext();
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = done => {
|
module.exports = done => {
|
||||||
if (!config.imap.enabled) {
|
if (!config.imap.enabled) {
|
||||||
return setImmediate(() => done(null, false));
|
return setImmediate(() => done(null, false));
|
||||||
|
@ -1042,6 +1200,10 @@ module.exports = done => {
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis });
|
userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis });
|
||||||
|
notifier = new ImapNotifier({
|
||||||
|
database: db.database,
|
||||||
|
redis: db.redis
|
||||||
|
});
|
||||||
|
|
||||||
server.on('error', err => {
|
server.on('error', err => {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ module.exports = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._server.notifier.fire(this.session.user.username, mailbox, {
|
this._server.notifier.fire(this.session.user.id, mailbox, {
|
||||||
action: 'DELETE',
|
action: 'DELETE',
|
||||||
mailbox
|
mailbox
|
||||||
});
|
});
|
||||||
|
|
|
@ -279,7 +279,7 @@ module.exports = function(options) {
|
||||||
uidNext: folder.uidNext,
|
uidNext: folder.uidNext,
|
||||||
uidValidity: folder.uidValidity,
|
uidValidity: folder.uidValidity,
|
||||||
highestModseq: folder.modifyIndex,
|
highestModseq: folder.modifyIndex,
|
||||||
unseen: folder.messages.filter(message => message.flags.indexOf('\\Seen') < 0).length
|
unseen: folder.messages.filter(message => !message.flags.includes('\\Seen')).length
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -313,7 +313,7 @@ module.exports = function(options) {
|
||||||
uid: message.uid
|
uid: message.uid
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.notifier.fire(session.user.username, mailbox);
|
this.notifier.fire(session.user.id, mailbox);
|
||||||
|
|
||||||
return callback(null, true, {
|
return callback(null, true, {
|
||||||
uidValidity: folder.uidValidity,
|
uidValidity: folder.uidValidity,
|
||||||
|
@ -339,7 +339,7 @@ module.exports = function(options) {
|
||||||
|
|
||||||
let processMessages = () => {
|
let processMessages = () => {
|
||||||
if (i >= folder.messages.length) {
|
if (i >= folder.messages.length) {
|
||||||
this.notifier.fire(session.user.username, mailbox);
|
this.notifier.fire(session.user.id, mailbox);
|
||||||
return callback(null, true, modified);
|
return callback(null, true, modified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,7 +445,7 @@ module.exports = function(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = [];
|
let entries = [];
|
||||||
for ((i = 0), (len = deleted.length); i < len; i++) {
|
for (i = 0, len = deleted.length; i < len; i++) {
|
||||||
entries.push({
|
entries.push({
|
||||||
command: 'EXPUNGE',
|
command: 'EXPUNGE',
|
||||||
ignore: session.id,
|
ignore: session.id,
|
||||||
|
@ -457,7 +457,7 @@ module.exports = function(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
||||||
this.notifier.fire(session.user.username, mailbox);
|
this.notifier.fire(session.user.id, mailbox);
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -490,7 +490,7 @@ module.exports = function(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((i = 0), (len = messages.length); i < len; i++) {
|
for (i = 0, len = messages.length; i < len; i++) {
|
||||||
messages[i].uid = destinationFolder.uidNext++;
|
messages[i].uid = destinationFolder.uidNext++;
|
||||||
destinationUid.push(messages[i].uid);
|
destinationUid.push(messages[i].uid);
|
||||||
destinationFolder.messages.push(messages[i]);
|
destinationFolder.messages.push(messages[i]);
|
||||||
|
@ -532,7 +532,7 @@ module.exports = function(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if BODY[] is touched, then add \Seen flag and notify other clients
|
// if BODY[] is touched, then add \Seen flag and notify other clients
|
||||||
if (message.flags.indexOf('\\Seen') < 0) {
|
if (!message.flags.includes('\\Seen')) {
|
||||||
message.flags.unshift('\\Seen');
|
message.flags.unshift('\\Seen');
|
||||||
entries.push({
|
entries.push({
|
||||||
command: 'FETCH',
|
command: 'FETCH',
|
||||||
|
@ -549,7 +549,7 @@ module.exports = function(options) {
|
||||||
let processMessage = () => {
|
let processMessage = () => {
|
||||||
if (pos >= folder.messages.length) {
|
if (pos >= folder.messages.length) {
|
||||||
// once messages are processed show relevant updates
|
// once messages are processed show relevant updates
|
||||||
this.notifier.fire(session.user.username, mailbox);
|
this.notifier.fire(session.user.id, mailbox);
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
}
|
}
|
||||||
let message = folder.messages[pos++];
|
let message = folder.messages[pos++];
|
||||||
|
|
25
indexes.yaml
25
indexes.yaml
|
@ -151,16 +151,18 @@ indexes:
|
||||||
text: 5
|
text: 5
|
||||||
- collection: messages
|
- collection: messages
|
||||||
index:
|
index:
|
||||||
name: mailbox_seen_flag
|
# in most cases we only care about unseen, not seen messages
|
||||||
|
name: mailbox_unseen_flag
|
||||||
key:
|
key:
|
||||||
mailbox: 1
|
mailbox: 1
|
||||||
seen: 1
|
unseen: 1
|
||||||
- collection: messages
|
- collection: messages
|
||||||
index:
|
index:
|
||||||
name: mailbox_deleted_flag
|
# some mail agents list messages that do not have the \Deleted flag set
|
||||||
|
name: mailbox_undeleted_flag
|
||||||
key:
|
key:
|
||||||
mailbox: 1
|
mailbox: 1
|
||||||
deleted: 1
|
undeleted: 1
|
||||||
- collection: messages
|
- collection: messages
|
||||||
index:
|
index:
|
||||||
name: mailbox_flagged_flag
|
name: mailbox_flagged_flag
|
||||||
|
@ -219,10 +221,25 @@ indexes:
|
||||||
|
|
||||||
- collection: journal
|
- collection: journal
|
||||||
index:
|
index:
|
||||||
|
# this index is used to apply changes in a mailbox for IMAP session
|
||||||
name: mailbox_modseq
|
name: mailbox_modseq
|
||||||
key:
|
key:
|
||||||
mailbox: 1
|
mailbox: 1
|
||||||
modseq: 1
|
modseq: 1
|
||||||
|
- collection: journal
|
||||||
|
index:
|
||||||
|
# this index is used to send updates to a logged in webmail user
|
||||||
|
name: user_limit_id
|
||||||
|
key:
|
||||||
|
user: 1
|
||||||
|
_id: 1
|
||||||
|
- collection: journal
|
||||||
|
index:
|
||||||
|
# this index is used to find the latest journal entry
|
||||||
|
name: user_limit_id_reverse
|
||||||
|
key:
|
||||||
|
user: 1
|
||||||
|
_id: -1
|
||||||
- collection: journal
|
- collection: journal
|
||||||
index:
|
index:
|
||||||
name: autoexpire
|
name: autoexpire
|
||||||
|
|
|
@ -46,11 +46,23 @@ module.exports = server => (path, session, callback) => {
|
||||||
retention: user.retention
|
retention: user.retention
|
||||||
};
|
};
|
||||||
|
|
||||||
db.database.collection('mailboxes').insertOne(mailbox, err => {
|
db.database.collection('mailboxes').insertOne(mailbox, (err, r) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
return server.notifier.addEntries(
|
||||||
|
session.user.id,
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
command: 'CREATE',
|
||||||
|
mailbox: r.insertId,
|
||||||
|
name: path
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
server.notifier.fire(session.user.id, path);
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,6 +27,14 @@ module.exports = server => (path, session, callback) => {
|
||||||
return callback(null, 'CANNOT');
|
return callback(null, 'CANNOT');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.notifier.addEntries(
|
||||||
|
session.user.id,
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
command: 'DROP',
|
||||||
|
mailbox: mailbox._id
|
||||||
|
},
|
||||||
|
() => {
|
||||||
db.database.collection('mailboxes').deleteOne({
|
db.database.collection('mailboxes').deleteOne({
|
||||||
_id: mailbox._id
|
_id: mailbox._id
|
||||||
}, err => {
|
}, err => {
|
||||||
|
@ -76,14 +84,8 @@ module.exports = server => (path, session, callback) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let done = () => {
|
let done = () => {
|
||||||
db.database.collection('journal').deleteMany({
|
server.notifier.fire(session.user.id, path);
|
||||||
mailbox: mailbox._id
|
|
||||||
}, err => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!storageUsed) {
|
if (!storageUsed) {
|
||||||
|
@ -105,5 +107,7 @@ module.exports = server => (path, session, callback) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
.find({
|
.find({
|
||||||
user: session.user.id,
|
user: session.user.id,
|
||||||
mailbox: mailbox._id,
|
mailbox: mailbox._id,
|
||||||
deleted: true
|
undeleted: false
|
||||||
})
|
})
|
||||||
.project({
|
.project({
|
||||||
_id: true,
|
_id: true,
|
||||||
|
|
|
@ -164,7 +164,7 @@ module.exports = server => (path, options, session, callback) => {
|
||||||
flags: '\\Seen'
|
flags: '\\Seen'
|
||||||
},
|
},
|
||||||
$set: {
|
$set: {
|
||||||
seen: true
|
unseen: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,14 @@ module.exports = server => (path, newname, session, callback) => {
|
||||||
if (mailbox) {
|
if (mailbox) {
|
||||||
return callback(null, 'ALREADYEXISTS');
|
return callback(null, 'ALREADYEXISTS');
|
||||||
}
|
}
|
||||||
|
return server.notifier.addEntries(
|
||||||
|
session.user.id,
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
command: 'RENAME',
|
||||||
|
name: newname
|
||||||
|
},
|
||||||
|
() => {
|
||||||
db.database.collection('mailboxes').findOneAndUpdate({
|
db.database.collection('mailboxes').findOneAndUpdate({
|
||||||
user: session.user.id,
|
user: session.user.id,
|
||||||
path
|
path
|
||||||
|
@ -43,7 +50,10 @@ module.exports = server => (path, newname, session, callback) => {
|
||||||
return callback(null, 'NONEXISTENT');
|
return callback(null, 'NONEXISTENT');
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, true);
|
server.notifier.fire(session.user.id, path);
|
||||||
|
return callback(null, true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,7 @@ module.exports = server => (path, session, callback) => {
|
||||||
.collection('messages')
|
.collection('messages')
|
||||||
.find({
|
.find({
|
||||||
mailbox: mailbox._id,
|
mailbox: mailbox._id,
|
||||||
seen: false
|
unseen: true
|
||||||
})
|
})
|
||||||
.count((err, unseen) => {
|
.count((err, unseen) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -130,9 +130,9 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
flagsupdate = {
|
flagsupdate = {
|
||||||
$set: {
|
$set: {
|
||||||
flags: message.flags,
|
flags: message.flags,
|
||||||
seen: message.flags.includes('\\Seen'),
|
unseen: !message.flags.includes('\\Seen'),
|
||||||
flagged: message.flags.includes('\\Flagged'),
|
flagged: message.flags.includes('\\Flagged'),
|
||||||
deleted: message.flags.includes('\\Deleted'),
|
undeleted: !message.flags.includes('\\Deleted'),
|
||||||
draft: message.flags.includes('\\Draft')
|
draft: message.flags.includes('\\Draft')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -171,7 +171,7 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
flagsupdate.$set = {};
|
flagsupdate.$set = {};
|
||||||
if (newFlags.includes('\\Seen')) {
|
if (newFlags.includes('\\Seen')) {
|
||||||
flagsupdate.$set = {
|
flagsupdate.$set = {
|
||||||
seen: true
|
unseen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (newFlags.includes('\\Flagged')) {
|
if (newFlags.includes('\\Flagged')) {
|
||||||
|
@ -181,7 +181,7 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
}
|
}
|
||||||
if (newFlags.includes('\\Deleted')) {
|
if (newFlags.includes('\\Deleted')) {
|
||||||
flagsupdate.$set = {
|
flagsupdate.$set = {
|
||||||
deleted: true
|
undeleted: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (newFlags.includes('\\Draft')) {
|
if (newFlags.includes('\\Draft')) {
|
||||||
|
@ -225,7 +225,7 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
flagsupdate.$set = {};
|
flagsupdate.$set = {};
|
||||||
if (oldFlags.includes('\\Seen')) {
|
if (oldFlags.includes('\\Seen')) {
|
||||||
flagsupdate.$set = {
|
flagsupdate.$set = {
|
||||||
seen: false
|
unseen: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (oldFlags.includes('\\Flagged')) {
|
if (oldFlags.includes('\\Flagged')) {
|
||||||
|
@ -235,7 +235,7 @@ module.exports = server => (path, update, session, callback) => {
|
||||||
}
|
}
|
||||||
if (oldFlags.includes('\\Deleted')) {
|
if (oldFlags.includes('\\Deleted')) {
|
||||||
flagsupdate.$set = {
|
flagsupdate.$set = {
|
||||||
deleted: false
|
undeleted: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (oldFlags.includes('\\Draft')) {
|
if (oldFlags.includes('\\Draft')) {
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ImapNotifier extends EventEmitter {
|
||||||
clearTimeout(data.timeout);
|
clearTimeout(data.timeout);
|
||||||
publishTimers.delete(ev);
|
publishTimers.delete(ev);
|
||||||
this._listeners.emit(ev);
|
this._listeners.emit(ev);
|
||||||
|
this._listeners.emit(ev.split(':').shift() + ':*');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (publishTimers.has(ev)) {
|
if (publishTimers.has(ev)) {
|
||||||
|
@ -49,7 +50,7 @@ class ImapNotifier extends EventEmitter {
|
||||||
data.count++;
|
data.count++;
|
||||||
|
|
||||||
if (data.initial < Date.now() - 1000) {
|
if (data.initial < Date.now() - 1000) {
|
||||||
// if the event has been held back already for a second, the fire immediatelly
|
// if the event has been held back already for a second, then fire immediatelly
|
||||||
return fire();
|
return fire();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,8 +77,9 @@ class ImapNotifier extends EventEmitter {
|
||||||
let data = JSON.parse(message);
|
let data = JSON.parse(message);
|
||||||
if (data.e && !data.p) {
|
if (data.e && !data.p) {
|
||||||
scheduleDataEvent(data.e);
|
scheduleDataEvent(data.e);
|
||||||
} else {
|
} else if (data.e) {
|
||||||
this._listeners.emit(data.e, data.p);
|
this._listeners.emit(data.e, data.p);
|
||||||
|
this._listeners.emit(data.e.split(':').shift() + ':*', data.p);
|
||||||
}
|
}
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
//
|
//
|
||||||
|
@ -94,8 +96,11 @@ class ImapNotifier extends EventEmitter {
|
||||||
* @param {String} user
|
* @param {String} user
|
||||||
* @returns {String} md5 hex
|
* @returns {String} md5 hex
|
||||||
*/
|
*/
|
||||||
_eventName(path, user) {
|
_eventName(user, path) {
|
||||||
return crypto.createHash('md5').update(user.toString() + ':' + path).digest('hex');
|
if (path.length >= 32) {
|
||||||
|
path = crypto.createHash('md5').update(path).digest('hex');
|
||||||
|
}
|
||||||
|
return user + ':' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,6 +218,7 @@ class ImapNotifier extends EventEmitter {
|
||||||
entry.modseq = entry.modseq || modseq;
|
entry.modseq = entry.modseq || modseq;
|
||||||
entry.created = entry.created || created;
|
entry.created = entry.created || created;
|
||||||
entry.mailbox = entry.mailbox || mailbox._id;
|
entry.mailbox = entry.mailbox || mailbox._id;
|
||||||
|
entry.user = mailbox.user;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated.length) {
|
if (updated.length) {
|
||||||
|
|
|
@ -160,9 +160,9 @@ class MessageHandler {
|
||||||
msgid,
|
msgid,
|
||||||
|
|
||||||
// use boolean for more commonly used (and searched for) flags
|
// use boolean for more commonly used (and searched for) flags
|
||||||
seen: flags.includes('\\Seen'),
|
unseen: !flags.includes('\\Seen'),
|
||||||
flagged: flags.includes('\\Flagged'),
|
flagged: flags.includes('\\Flagged'),
|
||||||
deleted: flags.includes('\\Deleted'),
|
undeleted: !flags.includes('\\Deleted'),
|
||||||
draft: flags.includes('\\Draft'),
|
draft: flags.includes('\\Draft'),
|
||||||
|
|
||||||
magic: maildata.magic,
|
magic: maildata.magic,
|
||||||
|
@ -653,7 +653,7 @@ class MessageHandler {
|
||||||
message.rdate = Date.now() + (target.retention || 0);
|
message.rdate = Date.now() + (target.retention || 0);
|
||||||
|
|
||||||
if (options.markAsSeen) {
|
if (options.markAsSeen) {
|
||||||
message.seen = true;
|
message.unseen = false;
|
||||||
if (!message.flags.includes('\\Seen')) {
|
if (!message.flags.includes('\\Seen')) {
|
||||||
message.flags.push('\\Seen');
|
message.flags.push('\\Seen');
|
||||||
}
|
}
|
||||||
|
@ -683,7 +683,8 @@ class MessageHandler {
|
||||||
removeEntries.push({
|
removeEntries.push({
|
||||||
command: 'EXPUNGE',
|
command: 'EXPUNGE',
|
||||||
ignore: options.session && options.session.id,
|
ignore: options.session && options.session.id,
|
||||||
uid: sourceUid
|
uid: messageUid,
|
||||||
|
message: messageId
|
||||||
});
|
});
|
||||||
|
|
||||||
existsEntries.push({
|
existsEntries.push({
|
||||||
|
|
|
@ -438,7 +438,7 @@ class POP3Connection extends EventEmitter {
|
||||||
this.session.state = 'UPDATE';
|
this.session.state = 'UPDATE';
|
||||||
|
|
||||||
let deleted = this.session.listing.messages.filter(message => message.popped);
|
let deleted = this.session.listing.messages.filter(message => message.popped);
|
||||||
let seen = this.session.listing.messages.filter(message => !message.seen && message.fetched && !message.popped);
|
let seen = this.session.listing.messages.filter(message => message.seen && message.fetched && !message.popped);
|
||||||
|
|
||||||
if (!deleted.length && !seen.length) {
|
if (!deleted.length && !seen.length) {
|
||||||
return finish();
|
return finish();
|
||||||
|
|
|
@ -188,7 +188,7 @@ class POP3Server extends EventEmitter {
|
||||||
* @param {Function} callback Callback to run with message listing
|
* @param {Function} callback Callback to run with message listing
|
||||||
*/
|
*/
|
||||||
onListMessages(session, callback) {
|
onListMessages(session, callback) {
|
||||||
// messages are objects {id: 'abc', size: 123, seen: false}
|
// messages are objects {id: 'abc', size: 123, seen: true}
|
||||||
return callback(null, {
|
return callback(null, {
|
||||||
messages: [],
|
messages: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
|
|
6
pop3.js
6
pop3.js
|
@ -93,7 +93,7 @@ const serverOptions = {
|
||||||
mailbox: true,
|
mailbox: true,
|
||||||
// required to decide if we need to update flags after RETR
|
// required to decide if we need to update flags after RETR
|
||||||
flags: true,
|
flags: true,
|
||||||
seen: true
|
unseen: true
|
||||||
})
|
})
|
||||||
.sort([['uid', -1]])
|
.sort([['uid', -1]])
|
||||||
.limit(config.pop3.maxMessages || MAX_MESSAGES)
|
.limit(config.pop3.maxMessages || MAX_MESSAGES)
|
||||||
|
@ -113,7 +113,7 @@ const serverOptions = {
|
||||||
mailbox: message.mailbox,
|
mailbox: message.mailbox,
|
||||||
size: message.size,
|
size: message.size,
|
||||||
flags: message.flags,
|
flags: message.flags,
|
||||||
seen: message.seen
|
seen: !message.unseen
|
||||||
})),
|
})),
|
||||||
count: messages.length,
|
count: messages.length,
|
||||||
size: messages.reduce((acc, message) => acc + message.size, 0)
|
size: messages.reduce((acc, message) => acc + message.size, 0)
|
||||||
|
@ -263,7 +263,7 @@ function markAsSeen(session, messages, callback) {
|
||||||
}, {
|
}, {
|
||||||
$set: {
|
$set: {
|
||||||
modseq: mailboxData.modifyIndex,
|
modseq: mailboxData.modifyIndex,
|
||||||
seen: true
|
unseen: false
|
||||||
},
|
},
|
||||||
$addToSet: {
|
$addToSet: {
|
||||||
flags: '\\Seen'
|
flags: '\\Seen'
|
||||||
|
|
Loading…
Add table
Reference in a new issue