2017-03-21 06:07:23 +08:00
'use strict' ;
const config = require ( 'config' ) ;
const uuidV1 = require ( 'uuid/v1' ) ;
const ObjectID = require ( 'mongodb' ) . ObjectID ;
const RedFour = require ( 'redfour' ) ;
const Indexer = require ( '../imap-core/lib/indexer/indexer' ) ;
const ImapNotifier = require ( './imap-notifier' ) ;
2017-03-30 01:06:09 +08:00
const tools = require ( './tools' ) ;
2017-03-21 06:07:23 +08:00
class MessageHandler {
constructor ( database ) {
this . database = database ;
this . indexer = new Indexer ( {
database
} ) ;
this . notifier = new ImapNotifier ( {
database ,
pushOnly : true
} ) ;
this . redlock = new RedFour ( {
2017-03-30 01:06:09 +08:00
redis : tools . redisConfig ( config . redis ) ,
2017-03-21 06:07:23 +08:00
namespace : 'wildduck'
} ) ;
}
getMailbox ( options , callback ) {
let query = { } ;
if ( options . mailbox ) {
if ( typeof options . mailbox === 'object' && options . mailbox . _id ) {
return setImmediate ( null , options . mailbox ) ;
}
query . _id = options . mailbox ;
} else {
2017-03-22 16:30:10 +08:00
query . user = options . user ;
2017-03-21 06:07:23 +08:00
query . path = options . path ;
}
this . database . collection ( 'mailboxes' ) . findOne ( query , ( err , mailbox ) => {
if ( err ) {
return callback ( err ) ;
}
if ( ! mailbox ) {
let err = new Error ( 'Mailbox is missing' ) ;
err . imapResponse = 'TRYCREATE' ;
return callback ( err ) ;
}
callback ( null , mailbox ) ;
} ) ;
}
add ( options , callback ) {
let id = new ObjectID ( ) ;
let mimeTree = this . indexer . parseMimeTree ( options . raw ) ;
let size = this . indexer . getSize ( mimeTree ) ;
let bodystructure = this . indexer . getBodyStructure ( mimeTree ) ;
let envelope = this . indexer . getEnvelope ( mimeTree ) ;
let messageId = envelope [ 9 ] || ( '<' + uuidV1 ( ) + '@wildduck.email>' ) ;
this . getMailbox ( options , ( err , mailbox ) => {
if ( err ) {
return callback ( err ) ;
}
this . indexer . storeAttachments ( id , mimeTree , 50 * 1024 , err => {
if ( err ) {
return callback ( err ) ;
}
// Another server might be waiting for the lock like this.
this . redlock . waitAcquireLock ( mailbox . _id . toString ( ) , 30 * 1000 , 10 * 1000 , ( err , lock ) => {
if ( err ) {
return callback ( err ) ;
}
if ( ! lock || ! lock . success ) {
// did not get a insert lock in 10 seconds
return callback ( new Error ( 'The user you are trying to contact is receiving mail at a rate that prevents additional messages from being delivered. Please resend your message at a later time' ) ) ;
}
2017-03-27 04:58:05 +08:00
this . database . collection ( 'users' ) . findOneAndUpdate ( {
_id : mailbox . user
2017-03-21 06:07:23 +08:00
} , {
$inc : {
2017-03-27 04:58:05 +08:00
storageUsed : size ,
messages : 1
2017-03-21 06:07:23 +08:00
}
2017-03-27 04:58:05 +08:00
} , err => {
2017-03-21 06:07:23 +08:00
if ( err ) {
this . redlock . releaseLock ( lock , ( ) => false ) ;
return callback ( err ) ;
}
2017-03-27 04:58:05 +08:00
let rollback = err => {
this . database . collection ( 'users' ) . findOneAndUpdate ( {
_id : mailbox . user
} , {
$inc : {
storageUsed : - size ,
messages : - 1
}
} , ( ) => {
this . redlock . releaseLock ( lock , ( ) => callback ( err ) ) ;
} ) ;
2017-03-21 06:07:23 +08:00
} ;
2017-03-27 04:58:05 +08:00
// acquire new UID+MODSEQ
this . database . collection ( 'mailboxes' ) . findOneAndUpdate ( {
_id : mailbox . _id
} , {
$inc : {
// allocate bot UID and MODSEQ values so when journal is later sorted by
// modseq then UIDs are always in ascending order
uidNext : 1 ,
2017-03-30 01:06:09 +08:00
modifyIndex : 1
2017-03-27 04:58:05 +08:00
}
} , ( err , item ) => {
2017-03-21 06:07:23 +08:00
if ( err ) {
2017-03-27 04:58:05 +08:00
return rollback ( err ) ;
}
if ( ! item || ! item . value ) {
// was not able to acquire a lock
let err = new Error ( 'Mailbox is missing' ) ;
err . imapResponse = 'TRYCREATE' ;
return rollback ( err ) ;
2017-03-21 06:07:23 +08:00
}
2017-03-27 04:58:05 +08:00
let mailbox = item . value ;
2017-03-21 06:07:23 +08:00
2017-03-27 04:58:05 +08:00
let internaldate = options . date && new Date ( options . date ) || new Date ( ) ;
let headerdate = mimeTree . parsedHeader . date && new Date ( mimeTree . parsedHeader . date ) || false ;
if ( ! headerdate || headerdate . toString ( ) === 'Invalid Date' ) {
headerdate = internaldate ;
}
2017-03-21 06:07:23 +08:00
2017-03-27 04:58:05 +08:00
let message = {
_id : id ,
2017-03-30 01:06:09 +08:00
2017-03-27 04:58:05 +08:00
mailbox : mailbox . _id ,
2017-03-30 01:06:09 +08:00
user : mailbox . user ,
2017-03-27 04:58:05 +08:00
uid : mailbox . uidNext ,
2017-03-30 01:06:09 +08:00
modseq : mailbox . modifyIndex + 1 ,
2017-03-27 04:58:05 +08:00
internaldate ,
headerdate ,
flags : [ ] . concat ( options . flags || [ ] ) ,
size ,
2017-03-30 01:06:09 +08:00
2017-03-27 04:58:05 +08:00
meta : options . meta || { } ,
2017-03-30 01:06:09 +08:00
2017-03-27 04:58:05 +08:00
mimeTree ,
envelope ,
bodystructure ,
messageId
} ;
this . database . collection ( 'messages' ) . insertOne ( message , err => {
if ( err ) {
2017-03-30 01:06:09 +08:00
return rollback ( err ) ;
2017-03-27 04:58:05 +08:00
}
let uidValidity = mailbox . uidValidity ;
let uid = message . uid ;
this . notifier . addEntries ( mailbox , false , {
command : 'EXISTS' ,
uid : message . uid ,
message : message . _id ,
modseq : message . modseq
} , ( ) => {
this . redlock . releaseLock ( lock , ( ) => {
this . notifier . fire ( mailbox . user , mailbox . path ) ;
return callback ( null , true , {
uidValidity ,
uid
} ) ;
2017-03-21 06:07:23 +08:00
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
}
2017-03-27 15:36:45 +08:00
updateQuota ( mailbox , inc , callback ) {
inc = inc || { } ;
if ( ! inc . messages ) {
return callback ( ) ;
}
this . database . collection ( 'mailboxes' ) . findOneAndUpdate ( {
_id : mailbox . _id
} , {
$inc : {
storageUsed : Number ( inc . storageUsed ) || 0 ,
messages : Number ( inc . messages ) || 0
}
} , ( ) => {
this . database . collection ( 'users' ) . findOneAndUpdate ( {
_id : mailbox . user
} , {
$inc : {
storageUsed : Number ( inc . storageUsed ) || 0 ,
messages : Number ( inc . messages ) || 0
}
} , callback ) ;
} ) ;
}
del ( messageId , callback ) {
this . database . collection ( 'messages' ) . findOne ( {
_id : typeof messageId === 'string' ? new ObjectID ( messageId ) : messageId
} , ( err , message ) => {
if ( err ) {
return callback ( err ) ;
}
if ( ! message ) {
return callback ( new Error ( 'Message does not exist' ) ) ;
}
this . database . collection ( 'mailboxes' ) . findOne ( {
_id : message . mailbox
} , ( err , mailbox ) => {
if ( err ) {
return callback ( err ) ;
}
if ( ! mailbox ) {
return callback ( new Error ( 'Mailbox does not exist' ) ) ;
}
this . database . collection ( 'messages' ) . deleteOne ( {
_id : message . _id
} , err => {
if ( err ) {
return callback ( err ) ;
}
this . updateQuota ( mailbox , {
storageUsed : - message . size ,
messages : - 1
} , ( ) => {
// remove link to message from attachments (if any exist)
this . database . collection ( 'attachments.files' ) . updateMany ( {
'metadata.messages' : message . _id
} , {
$pull : {
'metadata.messages' : message . _id
}
} , {
multi : true ,
w : 1
} , err => {
if ( err ) {
// ignore as we don't really care if we have orphans or not
}
this . notifier . addEntries ( mailbox , false , {
command : 'EXPUNGE' ,
uid : message . uid ,
message : message . _id
} , ( ) => {
this . notifier . fire ( mailbox . user , mailbox . path ) ;
// delete all attachments that do not have any active links to message objects
this . database . collection ( 'attachments.files' ) . deleteMany ( {
'metadata.messages' : {
$size : 0
}
} , err => {
if ( err ) {
// ignore as we don't really care if we have orphans or not
}
return callback ( null , true ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
}
2017-03-21 06:07:23 +08:00
}
module . exports = MessageHandler ;