Skip to content

Commit 17bca3e

Browse files
authored
fix(encrypted-mailboxes): Add functionality of singular encrypted mailboxes ZMS-181 (#758)
* add support for encrypted mailboxes * mailboxes.js remove unnecessary default value * filtering-handler, add raw to call to addmessage. Feature: Encrypted mailboxes added * encrypt messages copied into encrypted mailbox * fix streams in on-copy and message-handler. message-handler optimizations, filter-handler optimizations
1 parent 8feae38 commit 17bca3e

File tree

6 files changed

+733
-397
lines changed

6 files changed

+733
-397
lines changed

lib/api/mailboxes.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ module.exports = (db, server, mailboxHandler) => {
215215
specialUse: mailboxData.specialUse,
216216
modifyIndex: mailboxData.modifyIndex,
217217
subscribed: mailboxData.subscribed,
218-
hidden: !!mailboxData.hidden
218+
hidden: !!mailboxData.hidden,
219+
encryptMessages: !!mailboxData.encryptMessages
219220
};
220221

221222
if (mailboxData.retention) {
@@ -293,6 +294,7 @@ module.exports = (db, server, mailboxHandler) => {
293294
.min(0)
294295
.description('Retention policy for the created Mailbox. Milliseconds after a message added to mailbox expires. Set to 0 to disable.'),
295296
sess: sessSchema,
297+
encryptMessages: booleanSchema.default(false).description('If true then messages in this mailbox are encrypted'),
296298
ip: sessIPSchema
297299
},
298300
queryParams: {},
@@ -346,7 +348,8 @@ module.exports = (db, server, mailboxHandler) => {
346348

347349
let opts = {
348350
subscribed: true,
349-
hidden: !!result.value.hidden
351+
hidden: !!result.value.hidden,
352+
encryptMessages: !!result.value.encryptMessages
350353
};
351354

352355
if (retention) {
@@ -402,6 +405,7 @@ module.exports = (db, server, mailboxHandler) => {
402405
modifyIndex: Joi.number().required().description('Modification sequence number. Incremented on every change in the mailbox.'),
403406
subscribed: booleanSchema.required().description('Mailbox subscription status. IMAP clients may unsubscribe from a folder.'),
404407
hidden: booleanSchema.required().description('Is the folder hidden or not'),
408+
encryptMessages: booleanSchema.required().description('If true then messages in this mailbox are encrypted'),
405409
total: Joi.number().required().description('How many messages are stored in this mailbox'),
406410
unseen: Joi.number().required().description('How many unseen messages are stored in this mailbox')
407411
}).$_setFlag('objectName', 'GetMailboxResponse')
@@ -530,6 +534,7 @@ module.exports = (db, server, mailboxHandler) => {
530534
modifyIndex: mailboxData.modifyIndex,
531535
subscribed: mailboxData.subscribed,
532536
hidden: !!mailboxData.hidden,
537+
encryptMessages: !!mailboxData.encryptMessages,
533538
total,
534539
unseen
535540
});
@@ -556,6 +561,7 @@ module.exports = (db, server, mailboxHandler) => {
556561
'Retention policy for the Mailbox (in ms). Changing retention value only affects messages added to this folder after the change'
557562
),
558563
subscribed: booleanSchema.description('Change Mailbox subscription state'),
564+
encryptMessages: booleanSchema.description('If true then messages in this mailbox are encrypted'),
559565
hidden: booleanSchema.description('Is the folder hidden or not. Hidden folders can not be opened in IMAP.'),
560566
sess: sessSchema,
561567
ip: sessIPSchema

lib/api/messages.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2410,7 +2410,8 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
24102410
});
24112411
}
24122412

2413-
if (userData.encryptMessages && !result.value.draft) {
2413+
if ((userData.encryptMessages || mailboxData.encryptMessages) && !result.value.draft) {
2414+
// encrypt message if global encryption ON or encrypted target mailbox
24142415
try {
24152416
let encrypted = await encryptMessage(userData.pubKey, raw);
24162417
if (encrypted) {

lib/filter-handler.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@ class FilterHandler {
141141

142142
let rawchunks = chunks;
143143

144-
let prepared;
144+
let raw;
145145

146+
let prepared;
146147
if (options.mimeTree) {
147148
if (options.mimeTree && options.mimeTree.header) {
148149
// remove old headers
@@ -157,7 +158,7 @@ class FilterHandler {
157158
mimeTree: options.mimeTree
158159
});
159160
} else {
160-
let raw = Buffer.concat(chunks, chunklen);
161+
raw = Buffer.concat(chunks, chunklen);
161162
prepared = await this.prepareMessage({
162163
raw
163164
});
@@ -660,10 +661,14 @@ class FilterHandler {
660661

661662
date: false,
662663
flags,
663-
664-
rawchunks
664+
rawchunks,
665+
chunklen
665666
};
666667

668+
if (raw) {
669+
messageOpts.raw = raw;
670+
}
671+
667672
if (options.verificationResults) {
668673
messageOpts.verificationResults = options.verificationResults;
669674
}

lib/handlers/on-copy.js

+102
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,
132132

133133
notifyLongRunning();
134134

135+
let targetMailboxEncrypted = false;
136+
137+
if (targetData.encryptMessages) {
138+
targetMailboxEncrypted = true;
139+
}
140+
135141
try {
136142
while ((messageData = await cursor.next())) {
137143
tools.checkSocket(socket); // do we even have to copy anything?
@@ -141,6 +147,12 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,
141147
uid: messageData.uid,
142148
_id: messageData._id
143149
};
150+
151+
const parsedHeader = (messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
152+
const parsedContentType = parsedHeader['content-type'];
153+
154+
const isMessageEncrypted = parsedContentType ? parsedContentType.subtype === 'encrypted' : false;
155+
144156
// Copying is not done in bulk to minimize risk of going out of sync with incremental UIDs
145157
sourceUid.unshift(messageData.uid);
146158
let item = await db.database.collection('mailboxes').findOneAndUpdate(
@@ -218,6 +230,96 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,
218230
{ writeConcern: 'majority' }
219231
);
220232

233+
const newPrepared = await new Promise((resolve, reject) => {
234+
if (targetMailboxEncrypted && !isMessageEncrypted && userData.pubKey) {
235+
// encrypt message
236+
// get raw from existing mimetree
237+
let outputStream = messageHandler.indexer.rebuild(messageData.mimeTree); // get raw rebuilder response obj (.value is the stream)
238+
239+
if (!outputStream || outputStream.type !== 'stream' || !outputStream.value) {
240+
return reject(new Error('Cannot fetch message'));
241+
}
242+
outputStream = outputStream.value; // set stream to actual stream object (.value)
243+
244+
let chunks = [];
245+
let chunklen = 0;
246+
outputStream
247+
.on('readable', () => {
248+
let chunk;
249+
while ((chunk = outputStream.read()) !== null) {
250+
chunks.push(chunk);
251+
chunklen += chunk.length;
252+
}
253+
})
254+
.on('end', () => {
255+
const raw = Buffer.concat(chunks, chunklen);
256+
messageHandler.encryptMessages(userData.pubKey, raw, (err, res) => {
257+
if (err) {
258+
return reject(err);
259+
}
260+
261+
// encrypted rebuilt raw
262+
263+
if (res) {
264+
messageHandler.prepareMessage({ raw: res }, (err, prepared) => {
265+
if (err) {
266+
return reject(err);
267+
}
268+
// prepared new message structure from encrypted raw
269+
270+
const maildata = messageHandler.indexer.getMaildata(prepared.mimeTree);
271+
272+
// add attachments of encrypted messages
273+
if (maildata.attachments && maildata.attachments.length) {
274+
messageData.attachments = maildata.attachments;
275+
messageData.ha = maildata.attachments.some(a => !a.related);
276+
} else {
277+
messageData.ha = false;
278+
}
279+
280+
// remove fields that may leak data in FE or DB
281+
delete messageData.text;
282+
delete messageData.html;
283+
messageData.intro = '';
284+
285+
messageHandler.indexer.storeNodeBodies(maildata, prepared.mimeTree, err => {
286+
// store new attachments
287+
let cleanup = () => {
288+
let attachmentIds = Object.keys(prepared.mimeTree.attachmentMap || {}).map(
289+
key => prepared.mimeTree.attachmentMap[key]
290+
);
291+
292+
messageHandler.attachmentStorage.deleteMany(attachmentIds, maildata.magic);
293+
294+
if (err) {
295+
return reject(err);
296+
}
297+
};
298+
299+
if (err) {
300+
return cleanup(err);
301+
}
302+
303+
return resolve(prepared);
304+
});
305+
});
306+
}
307+
});
308+
});
309+
} else {
310+
resolve(false);
311+
}
312+
});
313+
314+
// replace fields
315+
if (newPrepared) {
316+
messageData.mimeTree = newPrepared.mimeTree;
317+
messageData.size = newPrepared.size;
318+
messageData.bodystructure = newPrepared.bodystructure;
319+
messageData.envelope = newPrepared.envelope;
320+
messageData.headers = newPrepared.headers;
321+
}
322+
221323
let r = await db.database.collection('messages').insertOne(messageData, { writeConcern: 'majority' });
222324

223325
if (!r || !r.acknowledged) {

0 commit comments

Comments
 (0)