mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-11-01 17:16:58 +08:00
better drafts handling
This commit is contained in:
parent
c8426f11ca
commit
b4023c53ea
4 changed files with 262 additions and 174 deletions
|
|
@ -231,6 +231,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
fields: {
|
||||
_id: true,
|
||||
uid: true,
|
||||
msgid: true,
|
||||
'meta.from': true,
|
||||
hdate: true,
|
||||
subject: true,
|
||||
|
|
@ -239,6 +240,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
'mimeTree.parsedHeader.cc': true,
|
||||
'mimeTree.parsedHeader.sender': true,
|
||||
'mimeTree.parsedHeader.content-type': true,
|
||||
'mimeTree.parsedHeader.references': true,
|
||||
ha: true,
|
||||
intro: true,
|
||||
unseen: true,
|
||||
|
|
@ -669,6 +671,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
fields: {
|
||||
_id: true,
|
||||
uid: true,
|
||||
msgid: true,
|
||||
mailbox: true,
|
||||
'meta.from': true,
|
||||
hdate: true,
|
||||
|
|
@ -678,6 +681,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
'mimeTree.parsedHeader.to': true,
|
||||
'mimeTree.parsedHeader.cc': true,
|
||||
'mimeTree.parsedHeader.content-type': true,
|
||||
'mimeTree.parsedHeader.references': true,
|
||||
ha: true,
|
||||
intro: true,
|
||||
unseen: true,
|
||||
|
|
@ -1056,7 +1060,11 @@ module.exports = (db, server, messageHandler) => {
|
|||
html: messageData.html,
|
||||
text: messageData.text,
|
||||
forwardTargets: messageData.forwardTargets,
|
||||
attachments: messageData.attachments || []
|
||||
attachments: messageData.attachments || [],
|
||||
references: (parsedHeader.references || '')
|
||||
.toString()
|
||||
.split(/\s+/)
|
||||
.filter(ref => ref)
|
||||
};
|
||||
|
||||
let parsedContentType = parsedHeader['content-type'];
|
||||
|
|
@ -2458,6 +2466,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
_id: true,
|
||||
mailbox: true,
|
||||
uid: true,
|
||||
msgid: true,
|
||||
'meta.from': true,
|
||||
hdate: true,
|
||||
subject: true,
|
||||
|
|
@ -2466,6 +2475,7 @@ module.exports = (db, server, messageHandler) => {
|
|||
'mimeTree.parsedHeader.to': true,
|
||||
'mimeTree.parsedHeader.cc': true,
|
||||
'mimeTree.parsedHeader.content-type': true,
|
||||
'mimeTree.parsedHeader.references': true,
|
||||
ha: true,
|
||||
intro: true,
|
||||
unseen: true,
|
||||
|
|
@ -3163,6 +3173,7 @@ function formatMessageListing(messageData) {
|
|||
from: from && from[0],
|
||||
to,
|
||||
cc,
|
||||
messageId: messageData.msgid,
|
||||
subject: messageData.subject,
|
||||
date: messageData.hdate.toISOString(),
|
||||
intro: messageData.intro,
|
||||
|
|
@ -3172,7 +3183,11 @@ function formatMessageListing(messageData) {
|
|||
flagged: messageData.flagged,
|
||||
draft: messageData.draft,
|
||||
answered: messageData.flags.includes('\\Answered'),
|
||||
forwarded: messageData.flags.includes('$Forwarded')
|
||||
forwarded: messageData.flags.includes('$Forwarded'),
|
||||
references: (parsedHeader.references || '')
|
||||
.toString()
|
||||
.split(/\s+/)
|
||||
.filter(ref => ref)
|
||||
};
|
||||
|
||||
let parsedContentType = parsedHeader['content-type'];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const config = require('wild-config');
|
|||
const log = require('npmlog');
|
||||
const libmime = require('libmime');
|
||||
const MailComposer = require('nodemailer/lib/mail-composer');
|
||||
const htmlToText = require('html-to-text');
|
||||
const Joi = require('../joi');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const tools = require('../tools');
|
||||
|
|
@ -321,6 +322,16 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
attachments: options.attachments || []
|
||||
};
|
||||
|
||||
// ensure plaintext content if html is provided
|
||||
if (data.html && !data.text) {
|
||||
try {
|
||||
// might explode on long or strange strings
|
||||
data.text = htmlToText.fromString(data.html);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let compiler = new MailComposer(data);
|
||||
let compiled = compiler.compile();
|
||||
let collector = new StreamCollect();
|
||||
|
|
@ -464,8 +475,8 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
date: false,
|
||||
flags: ['\\Seen'].concat(options.isDraft ? '\\Draft' : []),
|
||||
|
||||
// always insert drafts, otherwise skip
|
||||
skipExisting: !options.isDraft
|
||||
// always insert messages
|
||||
skipExisting: false
|
||||
};
|
||||
|
||||
if (raw) {
|
||||
|
|
@ -484,11 +495,37 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
return callback(null, false);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
let done = () =>
|
||||
callback(null, {
|
||||
id: info.uid,
|
||||
mailbox: info.mailbox,
|
||||
queueId: outbound
|
||||
});
|
||||
|
||||
if (options.draft) {
|
||||
return db.database.collection('messages').findOne(
|
||||
{
|
||||
mailbox: new ObjectID(options.draft.mailbox),
|
||||
uid: options.draft.id
|
||||
},
|
||||
(err, messageData) => {
|
||||
if (err || !messageData || messageData.user.toString() !== user.toString()) {
|
||||
return done();
|
||||
}
|
||||
|
||||
messageHandler.del(
|
||||
{
|
||||
user,
|
||||
mailbox: new ObjectID(options.draft.mailbox),
|
||||
messageData,
|
||||
archive: !messageData.flags.includes('\\Draft')
|
||||
},
|
||||
done
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -646,6 +683,23 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
.required()
|
||||
}),
|
||||
|
||||
// if true then treat this message as a draft
|
||||
isDraft: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false),
|
||||
|
||||
// if set then this message is based on a draft that should be deleted after processing
|
||||
draft: Joi.object().keys({
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
id: Joi.number().required()
|
||||
}),
|
||||
|
||||
uploadOnly: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
|
|
|
|||
|
|
@ -430,183 +430,189 @@ class MessageHandler {
|
|||
};
|
||||
}
|
||||
|
||||
this.database.collection('messages').findOne(
|
||||
{
|
||||
mailbox: mailboxData._id,
|
||||
hdate: messageOpts.hdate,
|
||||
msgid: messageOpts.msgid,
|
||||
uid: {
|
||||
$gt: 0,
|
||||
$lt: mailboxData.uidNext
|
||||
}
|
||||
},
|
||||
queryOpts,
|
||||
(err, messageData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
let query = {
|
||||
mailbox: mailboxData._id,
|
||||
hdate: messageOpts.hdate,
|
||||
msgid: messageOpts.msgid,
|
||||
uid: {
|
||||
$gt: 0,
|
||||
$lt: mailboxData.uidNext
|
||||
}
|
||||
};
|
||||
|
||||
this.database.collection('messages').findOne(query, queryOpts, (err, messageData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!messageData) {
|
||||
// nothing to do here, continue adding message
|
||||
return callback();
|
||||
}
|
||||
|
||||
let existingId = messageData._id;
|
||||
let existingUid = messageData.uid;
|
||||
let existingMailbox = messageData.mailbox;
|
||||
let outbound = [].concat(messageData.outbound || []).concat(options.outbound || []);
|
||||
if (outbound) {
|
||||
messageData.outbound = outbound;
|
||||
}
|
||||
|
||||
if (options.skipExisting) {
|
||||
// message already exists, just skip it
|
||||
if (options.outbound) {
|
||||
// new outbound ID's. update
|
||||
return this.database.collection('messages').findOneAndUpdate(
|
||||
{
|
||||
_id: messageData._id,
|
||||
mailbox: messageData.mailbox,
|
||||
uid: messageData.uid
|
||||
},
|
||||
{
|
||||
$addToSet: {
|
||||
outbound: { $each: [].concat(options.outbound || []) }
|
||||
}
|
||||
},
|
||||
{
|
||||
returnOriginal: true,
|
||||
projection: {
|
||||
_id: true,
|
||||
outbound: true
|
||||
}
|
||||
},
|
||||
() =>
|
||||
callback(null, true, {
|
||||
uid: existingUid,
|
||||
id: existingId,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'skip'
|
||||
})
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
if (!messageData) {
|
||||
// nothing to do here, continue adding message
|
||||
return callback();
|
||||
}
|
||||
return callback(null, true, {
|
||||
uid: existingUid,
|
||||
id: existingId,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'skip'
|
||||
});
|
||||
}
|
||||
|
||||
let existingId = messageData._id;
|
||||
let existingUid = messageData.uid;
|
||||
let outbound = [].concat(messageData.outbound || []).concat(options.outbound || []);
|
||||
if (outbound) {
|
||||
messageData.outbound = outbound;
|
||||
}
|
||||
// As duplicate message was found, update UID, MODSEQ and FLAGS
|
||||
|
||||
if (options.skipExisting) {
|
||||
// message already exists, just skip it
|
||||
if (options.outbound) {
|
||||
// new outbound ID's. update
|
||||
return this.database.collection('messages').findOneAndUpdate(
|
||||
{
|
||||
_id: messageData._id,
|
||||
mailbox: messageData.mailbox,
|
||||
uid: messageData.uid
|
||||
},
|
||||
{
|
||||
$addToSet: {
|
||||
outbound: { $each: [].concat(options.outbound || []) }
|
||||
}
|
||||
},
|
||||
{
|
||||
returnOriginal: true,
|
||||
projection: {
|
||||
_id: true,
|
||||
outbound: true
|
||||
}
|
||||
},
|
||||
() =>
|
||||
callback(null, true, {
|
||||
uid: existingUid,
|
||||
id: existingId,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'skip'
|
||||
})
|
||||
|
||||
);
|
||||
// acquire new UID+MODSEQ
|
||||
this.database.collection('mailboxes').findOneAndUpdate(
|
||||
{
|
||||
_id: mailboxData._id
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
// allocate both UID and MODSEQ values so when journal is later sorted by
|
||||
// modseq then UIDs are always in ascending order
|
||||
uidNext: 1,
|
||||
modifyIndex: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
returnOriginal: true
|
||||
},
|
||||
(err, item) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, true, {
|
||||
uid: existingUid,
|
||||
id: existingId,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'skip'
|
||||
});
|
||||
}
|
||||
if (!item || !item.value) {
|
||||
// was not able to acquire a lock
|
||||
let err = new Error('Mailbox is missing');
|
||||
err.imapResponse = 'TRYCREATE';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// As duplicate message was found, update UID, MODSEQ and FLAGS
|
||||
let mailboxData = item.value;
|
||||
let newUid = mailboxData.uidNext;
|
||||
let newModseq = mailboxData.modifyIndex + 1;
|
||||
|
||||
// acquire new UID+MODSEQ
|
||||
this.database.collection('mailboxes').findOneAndUpdate(
|
||||
{
|
||||
_id: mailboxData._id
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
// allocate both UID and MODSEQ values so when journal is later sorted by
|
||||
// modseq then UIDs are always in ascending order
|
||||
uidNext: 1,
|
||||
modifyIndex: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
returnOriginal: true
|
||||
},
|
||||
(err, item) => {
|
||||
// UID is immutable, so if we want to change it, we need to copy the message
|
||||
|
||||
messageData._id = messageOpts.id;
|
||||
// inserted message might not be in the same mailbox as the deleted one
|
||||
messageData.mailbox = mailboxData._id;
|
||||
messageData.uid = newUid;
|
||||
messageData.modseq = newModseq;
|
||||
messageData.flags = messageOpts.flags;
|
||||
|
||||
messageData.unseen = !messageOpts.flags.includes('\\Seen');
|
||||
messageData.flagged = messageOpts.flags.includes('\\Flagged');
|
||||
messageData.undeleted = !messageOpts.flags.includes('\\Deleted');
|
||||
messageData.draft = messageOpts.flags.includes('\\Draft');
|
||||
|
||||
this.database.collection('messages').insertOne(messageData, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!item || !item.value) {
|
||||
// was not able to acquire a lock
|
||||
let err = new Error('Mailbox is missing');
|
||||
err.imapResponse = 'TRYCREATE';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let mailboxData = item.value;
|
||||
let newUid = mailboxData.uidNext;
|
||||
let newModseq = mailboxData.modifyIndex + 1;
|
||||
|
||||
// UID is immutable, so if we want to change it, we need to copy the message
|
||||
|
||||
messageData._id = messageOpts.id;
|
||||
messageData.uid = newUid;
|
||||
messageData.modseq = newModseq;
|
||||
messageData.flags = messageOpts.flags;
|
||||
|
||||
this.database.collection('messages').insertOne(messageData, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
this.database.collection('messages').deleteOne(
|
||||
{
|
||||
_id: existingId,
|
||||
// hash key
|
||||
mailbox: mailboxData._id,
|
||||
uid: existingUid
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
// TODO: how to resolve this? we might end up with two copies of the same message :S
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) {
|
||||
options.session.writeStream.write(options.session.formatResponse('EXPUNGE', existingUid));
|
||||
}
|
||||
|
||||
if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) {
|
||||
options.session.writeStream.write(options.session.formatResponse('EXISTS', messageData.uid));
|
||||
}
|
||||
|
||||
this.notifier.addEntries(
|
||||
mailboxData,
|
||||
{
|
||||
command: 'EXPUNGE',
|
||||
ignore: options.session && options.session.id,
|
||||
uid: existingUid,
|
||||
message: existingId,
|
||||
unseen: messageData.unseen,
|
||||
// modseq is needed to avoid updating mailbox entry
|
||||
modseq: newModseq
|
||||
},
|
||||
() => {
|
||||
this.notifier.addEntries(
|
||||
mailboxData,
|
||||
{
|
||||
command: 'EXISTS',
|
||||
uid: messageData.uid,
|
||||
ignore: options.session && options.session.id,
|
||||
message: messageData._id,
|
||||
modseq: messageData.modseq,
|
||||
unseen: messageData.unseen
|
||||
},
|
||||
() => {
|
||||
this.notifier.fire(mailboxData.user);
|
||||
return callback(null, true, {
|
||||
uidValidity: mailboxData.uidValidity,
|
||||
uid: newUid,
|
||||
id: messageData._id,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'update'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
this.database.collection('messages').deleteOne(
|
||||
{
|
||||
_id: existingId,
|
||||
// hash key
|
||||
mailbox: existingMailbox,
|
||||
uid: existingUid
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
// TODO: how to resolve this? we might end up with two copies of the same message :S
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (options.session && options.session.selected && options.session.selected.mailbox.toString() === existingMailbox.toString()) {
|
||||
options.session.writeStream.write(options.session.formatResponse('EXPUNGE', existingUid));
|
||||
}
|
||||
|
||||
if (options.session && options.session.selected && options.session.selected.mailbox.toString() === mailboxData._id.toString()) {
|
||||
options.session.writeStream.write(options.session.formatResponse('EXISTS', messageData.uid));
|
||||
}
|
||||
|
||||
this.notifier.addEntries(
|
||||
existingMailbox.toString() === mailboxData._id.toString() ? mailboxData : existingMailbox,
|
||||
{
|
||||
command: 'EXPUNGE',
|
||||
ignore: options.session && options.session.id,
|
||||
uid: existingUid,
|
||||
message: existingId,
|
||||
unseen: messageData.unseen,
|
||||
// modseq is needed to avoid updating mailbox entry
|
||||
modseq: newModseq
|
||||
},
|
||||
() => {
|
||||
this.notifier.addEntries(
|
||||
mailboxData,
|
||||
{
|
||||
command: 'EXISTS',
|
||||
uid: messageData.uid,
|
||||
ignore: options.session && options.session.id,
|
||||
message: messageData._id,
|
||||
modseq: messageData.modseq,
|
||||
unseen: messageData.unseen
|
||||
},
|
||||
() => {
|
||||
this.notifier.fire(mailboxData.user);
|
||||
return callback(null, true, {
|
||||
uidValidity: mailboxData.uidValidity,
|
||||
uid: newUid,
|
||||
id: messageData._id,
|
||||
mailbox: mailboxData._id,
|
||||
status: 'update'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
updateQuota(mailboxData, inc, callback) {
|
||||
|
|
|
|||
|
|
@ -496,7 +496,20 @@ echo "server {
|
|||
ssl_certificate /etc/wildduck/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/wildduck/certs/privkey.pem;
|
||||
|
||||
# special config for EventSource to disable gzip
|
||||
location /api/events {
|
||||
proxy_http_version 1.1;
|
||||
gzip off;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header HOST \$http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header HOST \$http_host;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue