Skip to content

Commit 7e2394b

Browse files
committed
fix logic for imap appended drafts too
1 parent 0f56a8d commit 7e2394b

2 files changed

Lines changed: 103 additions & 9 deletions

File tree

lib/api/messages.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
134134
}
135135

136136
if (includeHasDrafts && matchDraftReferences) {
137-
// MIME References contains the entire thread ancestry. meta.reference is the exact mailbox/UID pair the draft was created for.
137+
// References contains the entire thread ancestry. In-Reply-To identifies the direct parent for both API and IMAP drafts.
138138
group.draftReferences = {
139139
$addToSet: {
140-
$cond: ['$draft', '$meta.reference', false]
140+
$cond: ['$draft', '$mimeTree.parsedHeader.in-reply-to', false]
141141
}
142142
};
143143
}
@@ -171,13 +171,14 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
171171
}
172172

173173
if (matchDraftReferences) {
174-
message.hasDrafts = ((matchingThreadCount && matchingThreadCount.draftReferences) || []).some(
175-
reference =>
176-
reference &&
177-
reference.mailbox &&
178-
reference.mailbox.toString() === message.mailbox.toString() &&
179-
reference.id === message.uid
174+
const draftReferences = new Set(
175+
((matchingThreadCount && matchingThreadCount.draftReferences) || [])
176+
.flatMap(reference => [].concat(reference || []))
177+
.flatMap(reference => reference.toString().split(/\s+/))
178+
.filter(reference => reference)
180179
);
180+
181+
message.hasDrafts = draftReferences.has(message.msgid);
181182
} else {
182183
message.hasDrafts = !!(matchingThreadCount && matchingThreadCount.hasDrafts);
183184
}

test/api/messages-test.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const expect = chai.expect;
1010
chai.config.includeStack = true;
1111
const config = require('@zone-eu/wild-config');
1212
const { ObjectId } = require('mongodb');
13+
const { ImapFlow } = require('imapflow');
1314
const { parseSearchQuery, getMongoDBQuery } = require('../../lib/search-query');
1415

1516
const server = supertest.agent(`http://127.0.0.1:${config.api.port}`);
@@ -259,6 +260,7 @@ describe('Messages tests', function () {
259260
let queryAttachmentMessageId;
260261
let queryFlaggedSeenAttachmentMessageId;
261262
let queryAltMailboxMessageId;
263+
let testUsername;
262264
let testAddress;
263265

264266
const queryFixture = {
@@ -335,7 +337,7 @@ describe('Messages tests', function () {
335337

336338
before(async () => {
337339
const testUserTag = Date.now().toString(36);
338-
const testUsername = `messagestestsuser-${testUserTag}`;
340+
testUsername = `messagestestsuser-${testUserTag}`;
339341
testAddress = `${testUsername}@web.zone.test`;
340342
queryFixture.fromAddress = testAddress;
341343

@@ -1065,6 +1067,97 @@ describe('Messages tests', function () {
10651067
expect(archivedRoot).to.not.have.property('hasDrafts');
10661068
});
10671069

1070+
it('should GET /users/:user/search expect success / IMAP APPEND draft hasDrafts matches only the direct parent', async () => {
1071+
const mailboxPath = `imap-draft-reference-${Date.now().toString(36)}`;
1072+
const mailboxResponse = await server
1073+
.post(`/users/${user}/mailboxes`)
1074+
.send({ path: `/${mailboxPath}`, hidden: false, retention: 10000 })
1075+
.expect(200);
1076+
const mailbox = mailboxResponse.body.id;
1077+
1078+
const root = await server
1079+
.post(`/users/${user}/mailboxes/${mailbox}/messages`)
1080+
.send({
1081+
to: [{ address: 'imap-draft@example.com' }],
1082+
subject: 'IMAP Draft Thread',
1083+
text: 'Root message'
1084+
})
1085+
.expect(200);
1086+
1087+
const reply = await server
1088+
.post(`/users/${user}/mailboxes/${mailbox}/messages`)
1089+
.send({
1090+
to: [{ address: 'imap-draft@example.com' }],
1091+
text: 'Intermediate reply',
1092+
reference: {
1093+
mailbox,
1094+
id: root.body.message.id,
1095+
action: 'reply'
1096+
}
1097+
})
1098+
.expect(200);
1099+
1100+
await server.put(`/users/${user}/mailboxes/${mailbox}/messages/${reply.body.message.id}`).send({ draft: false }).expect(200);
1101+
1102+
const rootData = await server.get(`/users/${user}/mailboxes/${mailbox}/messages/${root.body.message.id}`).send({}).expect(200);
1103+
const replyData = await server.get(`/users/${user}/mailboxes/${mailbox}/messages/${reply.body.message.id}`).send({}).expect(200);
1104+
const rawDraft = [
1105+
`From: ${testAddress}`,
1106+
'To: imap-draft@example.com',
1107+
'Subject: Re: IMAP Draft Thread',
1108+
`Message-ID: <imap-draft-${Date.now().toString(36)}@web.zone.test>`,
1109+
`In-Reply-To: ${replyData.body.messageId}`,
1110+
`References: ${rootData.body.messageId} ${replyData.body.messageId}`,
1111+
'MIME-Version: 1.0',
1112+
'Content-Type: text/plain; charset=utf-8',
1113+
'',
1114+
'Draft uploaded with IMAP APPEND'
1115+
].join('\r\n');
1116+
1117+
const client = new ImapFlow({
1118+
host: '127.0.0.1',
1119+
port: config.imap.port,
1120+
secure: true,
1121+
auth: {
1122+
user: testUsername,
1123+
pass: 'secretpassword'
1124+
},
1125+
tls: {
1126+
rejectUnauthorized: false
1127+
},
1128+
logger: false
1129+
});
1130+
1131+
let appendResult;
1132+
try {
1133+
await client.connect();
1134+
appendResult = await client.append(mailboxPath, rawDraft, ['\\Draft']);
1135+
} finally {
1136+
if (client.usable) {
1137+
await client.logout();
1138+
} else {
1139+
client.close();
1140+
}
1141+
}
1142+
1143+
expect(appendResult.uid).to.be.a('number');
1144+
1145+
const appendedDraft = await server.get(`/users/${user}/mailboxes/${mailbox}/messages/${appendResult.uid}`).send({}).expect(200);
1146+
expect(appendedDraft.body).to.not.have.property('reference');
1147+
1148+
const expandedThread = await server
1149+
.get(`/users/${user}/search?thread=${rootData.body.thread}&includeHasDrafts=true&limit=3`)
1150+
.send({})
1151+
.expect(200);
1152+
1153+
expect(expandedThread.body.results.map(entry => entry.id)).to.deep.equal([
1154+
appendResult.uid,
1155+
reply.body.message.id,
1156+
root.body.message.id
1157+
]);
1158+
expect(expandedThread.body.results.map(entry => entry.hasDrafts)).to.deep.equal([false, true, false]);
1159+
});
1160+
10681161
it('should GET /users/:user/search expect success / q supports subject and in keywords', async () => {
10691162
const q = `subject:"${queryFixture.subjectKeyword}" in:${queryMailbox}`;
10701163
const search = await searchQ(q);

0 commit comments

Comments
 (0)