Skip to content

Commit

Permalink
fix(flag-colors): added a method to set Flag Color. The color is also…
Browse files Browse the repository at this point in the history
… included in the Fetch response structure
  • Loading branch information
andris9 committed Jan 31, 2024
1 parent d73a56a commit d840951
Show file tree
Hide file tree
Showing 4 changed files with 684 additions and 553 deletions.
81 changes: 79 additions & 2 deletions lib/imap-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,17 @@ const { PassThrough } = require('stream');

const { proxyConnection } = require('./proxy-connection');

const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath, expandRange } = require('./tools');
const {
comparePaths,
updateCapabilities,
getFolderTree,
formatMessageResponse,
getDecoder,
packMessageRange,
normalizePath,
expandRange,
getColorFlags
} = require('./tools');

const imapCommands = require('./imap-commands.js');

Expand Down Expand Up @@ -1177,6 +1187,10 @@ class ImapFlow extends EventEmitter {

updateEvent.flags = message.flags;

if (message.flagColor) {
updateEvent.flagColor = message.flagColor;
}

this.emit('flags', updateEvent);
}
}
Expand Down Expand Up @@ -1926,6 +1940,68 @@ class ImapFlow extends EventEmitter {
return await this.run('STORE', range, flags, queryOpts);
}

/**
* Sets a colored flag for an email. Only supported by mail clients like Apple Mail
*
* @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
* @param {string} The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
* @param {Object} [options]
* @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
* @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
* @returns {Promise<Boolean>} Did the operation succeed or not
*
* @example
* let mailbox = await client.mailboxOpen('INBOX');
* // add a purple flag for all emails
* await client.setFlagColor('1:*', 'Purple');
*/
async setFlagColor(range, color, options) {
options = options || {};

range = await this.resolveRange(range, options);
if (!range) {
return false;
}

let flagChanges = getColorFlags(color);
if (!flagChanges) {
return false;
}

let addResults;
let removeResults;

if (flagChanges.add && flagChanges.add.length) {
let queryOpts = Object.assign(
{
operation: 'add'
},
options,
{
useLabels: false, // override if set
// prevent triggering a premature Flags change notification
silent: flagChanges.remove && flagChanges.remove.length
}
);

addResults = await this.run('STORE', range, flagChanges.add, queryOpts);
}

if (flagChanges.remove && flagChanges.remove.length) {
let queryOpts = Object.assign(
{
operation: 'remove'
},
options,
{ useLabels: false } // override if set
);

removeResults = await this.run('STORE', range, flagChanges.remove, queryOpts);
}

return addResults || removeResults || false;
}

/**
* Delete messages from currently opened mailbox. Method does not indicate info about deleted messages,
* instead you should be using {@link ImapFlow#expunge} event for this
Expand Down Expand Up @@ -2077,7 +2153,7 @@ class ImapFlow extends EventEmitter {
* @typedef {Object} FetchQueryObject
* @global
* @property {Boolean} [uid] if `true` then include UID in the response
* @property {Boolean} [flags] if `true` then include flags Set in the response
* @property {Boolean} [flags] if `true` then include flags Set in the response. Also adds `flagColor` to the response if the message is flagged.
* @property {Boolean} [bodyStructure] if `true` then include parsed BODYSTRUCTURE object in the response
* @property {Boolean} [envelope] if `true` then include parsed ENVELOPE object in the response
* @property {Boolean} [internalDate] if `true` then include internal date value in the response
Expand Down Expand Up @@ -2148,6 +2224,7 @@ class ImapFlow extends EventEmitter {
* @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
* @property {Number} [size] message size
* @property {Set<string>} [flags] a set of message flags
* @property {String} [flagColor] flag color like "red", or "yellow". This value is derived from the `flags` Set and it uses the same color rules as Apple Mail
* @property {MessageEnvelopeObject} [envelope] message envelope
* @property {MessageStructureObject} [bodyStructure] message body structure
* @property {Date} [internalDate] message internal date
Expand Down
46 changes: 46 additions & 0 deletions lib/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const { createHash } = require('crypto');
const { JPDecoder } = require('./jp-decoder');
const iconv = require('iconv-lite');

const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];

module.exports = {
encodePath(connection, path) {
path = (path || '').toString();
Expand Down Expand Up @@ -191,6 +193,43 @@ module.exports = {
return tree;
},

getFlagColor(flags) {
if (!flags.has('\\Flagged')) {
return null;
}

const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;

const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise

return FLAG_COLORS[color] || 'red'; // default to red for the unused \b111
},

getColorFlags(color) {
const colorCode = color ? FLAG_COLORS.indexOf((color || '').toString().toLowerCase().trim()) : null;
if (colorCode < 0 && colorCode !== null) {
return null;
}

const bits = [];
bits[0] = colorCode & 1; // eslint-disable-line no-bitwise
bits[1] = colorCode & 2; // eslint-disable-line no-bitwise
bits[2] = colorCode & 4; // eslint-disable-line no-bitwise

let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };

for (let i = 0; i < bits.length; i++) {
if (bits[i]) {
result.add.push(`$MailFlagBit${i}`);
} else {
result.remove.push(`$MailFlagBit${i}`);
}
}
return result;
},

async formatMessageResponse(untagged, mailbox) {
let map = {};

Expand Down Expand Up @@ -351,6 +390,13 @@ module.exports = {
map.id = map.emailId || createHash('md5').update([path, mailbox.uidValidity.toString(), map.uid.toString()].join(':')).digest('hex');
}

if (map.flags) {
let flagColor = module.exports.getFlagColor(map.flags);
if (flagColor) {
map.flagColor = flagColor;
}
}

return map;
},

Expand Down
Loading

0 comments on commit d840951

Please sign in to comment.