Skip to content

Commit

Permalink
fix(encrypted-mailboxes): Add functionality of singular encrypted mai…
Browse files Browse the repository at this point in the history
…lboxes 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
  • Loading branch information
NickOvt authored Jan 13, 2025
1 parent 8feae38 commit 17bca3e
Show file tree
Hide file tree
Showing 6 changed files with 733 additions and 397 deletions.
10 changes: 8 additions & 2 deletions lib/api/mailboxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ module.exports = (db, server, mailboxHandler) => {
specialUse: mailboxData.specialUse,
modifyIndex: mailboxData.modifyIndex,
subscribed: mailboxData.subscribed,
hidden: !!mailboxData.hidden
hidden: !!mailboxData.hidden,
encryptMessages: !!mailboxData.encryptMessages
};

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

let opts = {
subscribed: true,
hidden: !!result.value.hidden
hidden: !!result.value.hidden,
encryptMessages: !!result.value.encryptMessages
};

if (retention) {
Expand Down Expand Up @@ -402,6 +405,7 @@ module.exports = (db, server, mailboxHandler) => {
modifyIndex: Joi.number().required().description('Modification sequence number. Incremented on every change in the mailbox.'),
subscribed: booleanSchema.required().description('Mailbox subscription status. IMAP clients may unsubscribe from a folder.'),
hidden: booleanSchema.required().description('Is the folder hidden or not'),
encryptMessages: booleanSchema.required().description('If true then messages in this mailbox are encrypted'),
total: Joi.number().required().description('How many messages are stored in this mailbox'),
unseen: Joi.number().required().description('How many unseen messages are stored in this mailbox')
}).$_setFlag('objectName', 'GetMailboxResponse')
Expand Down Expand Up @@ -530,6 +534,7 @@ module.exports = (db, server, mailboxHandler) => {
modifyIndex: mailboxData.modifyIndex,
subscribed: mailboxData.subscribed,
hidden: !!mailboxData.hidden,
encryptMessages: !!mailboxData.encryptMessages,
total,
unseen
});
Expand All @@ -556,6 +561,7 @@ module.exports = (db, server, mailboxHandler) => {
'Retention policy for the Mailbox (in ms). Changing retention value only affects messages added to this folder after the change'
),
subscribed: booleanSchema.description('Change Mailbox subscription state'),
encryptMessages: booleanSchema.description('If true then messages in this mailbox are encrypted'),
hidden: booleanSchema.description('Is the folder hidden or not. Hidden folders can not be opened in IMAP.'),
sess: sessSchema,
ip: sessIPSchema
Expand Down
3 changes: 2 additions & 1 deletion lib/api/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2410,7 +2410,8 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
});
}

if (userData.encryptMessages && !result.value.draft) {
if ((userData.encryptMessages || mailboxData.encryptMessages) && !result.value.draft) {
// encrypt message if global encryption ON or encrypted target mailbox
try {
let encrypted = await encryptMessage(userData.pubKey, raw);
if (encrypted) {
Expand Down
13 changes: 9 additions & 4 deletions lib/filter-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ class FilterHandler {

let rawchunks = chunks;

let prepared;
let raw;

let prepared;
if (options.mimeTree) {
if (options.mimeTree && options.mimeTree.header) {
// remove old headers
Expand All @@ -157,7 +158,7 @@ class FilterHandler {
mimeTree: options.mimeTree
});
} else {
let raw = Buffer.concat(chunks, chunklen);
raw = Buffer.concat(chunks, chunklen);
prepared = await this.prepareMessage({
raw
});
Expand Down Expand Up @@ -660,10 +661,14 @@ class FilterHandler {

date: false,
flags,

rawchunks
rawchunks,
chunklen
};

if (raw) {
messageOpts.raw = raw;
}

if (options.verificationResults) {
messageOpts.verificationResults = options.verificationResults;
}
Expand Down
102 changes: 102 additions & 0 deletions lib/handlers/on-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,

notifyLongRunning();

let targetMailboxEncrypted = false;

if (targetData.encryptMessages) {
targetMailboxEncrypted = true;
}

try {
while ((messageData = await cursor.next())) {
tools.checkSocket(socket); // do we even have to copy anything?
Expand All @@ -141,6 +147,12 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,
uid: messageData.uid,
_id: messageData._id
};

const parsedHeader = (messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
const parsedContentType = parsedHeader['content-type'];

const isMessageEncrypted = parsedContentType ? parsedContentType.subtype === 'encrypted' : false;

// Copying is not done in bulk to minimize risk of going out of sync with incremental UIDs
sourceUid.unshift(messageData.uid);
let item = await db.database.collection('mailboxes').findOneAndUpdate(
Expand Down Expand Up @@ -218,6 +230,96 @@ async function copyHandler(server, messageHandler, connection, mailbox, update,
{ writeConcern: 'majority' }
);

const newPrepared = await new Promise((resolve, reject) => {
if (targetMailboxEncrypted && !isMessageEncrypted && userData.pubKey) {
// encrypt message
// get raw from existing mimetree
let outputStream = messageHandler.indexer.rebuild(messageData.mimeTree); // get raw rebuilder response obj (.value is the stream)

if (!outputStream || outputStream.type !== 'stream' || !outputStream.value) {
return reject(new Error('Cannot fetch message'));
}
outputStream = outputStream.value; // set stream to actual stream object (.value)

let chunks = [];
let chunklen = 0;
outputStream
.on('readable', () => {
let chunk;
while ((chunk = outputStream.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
})
.on('end', () => {
const raw = Buffer.concat(chunks, chunklen);
messageHandler.encryptMessages(userData.pubKey, raw, (err, res) => {
if (err) {
return reject(err);
}

// encrypted rebuilt raw

if (res) {
messageHandler.prepareMessage({ raw: res }, (err, prepared) => {
if (err) {
return reject(err);
}
// prepared new message structure from encrypted raw

const maildata = messageHandler.indexer.getMaildata(prepared.mimeTree);

// add attachments of encrypted messages
if (maildata.attachments && maildata.attachments.length) {
messageData.attachments = maildata.attachments;
messageData.ha = maildata.attachments.some(a => !a.related);
} else {
messageData.ha = false;
}

// remove fields that may leak data in FE or DB
delete messageData.text;
delete messageData.html;
messageData.intro = '';

messageHandler.indexer.storeNodeBodies(maildata, prepared.mimeTree, err => {
// store new attachments
let cleanup = () => {
let attachmentIds = Object.keys(prepared.mimeTree.attachmentMap || {}).map(
key => prepared.mimeTree.attachmentMap[key]
);

messageHandler.attachmentStorage.deleteMany(attachmentIds, maildata.magic);

if (err) {
return reject(err);
}
};

if (err) {
return cleanup(err);
}

return resolve(prepared);
});
});
}
});
});
} else {
resolve(false);
}
});

// replace fields
if (newPrepared) {
messageData.mimeTree = newPrepared.mimeTree;
messageData.size = newPrepared.size;
messageData.bodystructure = newPrepared.bodystructure;
messageData.envelope = newPrepared.envelope;
messageData.headers = newPrepared.headers;
}

let r = await db.database.collection('messages').insertOne(messageData, { writeConcern: 'majority' });

if (!r || !r.acknowledged) {
Expand Down
Loading

0 comments on commit 17bca3e

Please sign in to comment.