Skip to content

Commit

Permalink
feat(path): Account path argument can take either a path string, or a…
Browse files Browse the repository at this point in the history
…n array of strings to monitor multiple folders instead of just one
  • Loading branch information
andris9 committed Feb 21, 2024
1 parent 1fc1298 commit a7c6abc
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 29 deletions.
40 changes: 39 additions & 1 deletion lib/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,27 @@ class Account {
}
break;

case 'path':
if (!accountData[key] || !accountData[key].length) {
break;
}
if (accountData[key].length > 1 && (/^"|"$/.test(accountData[key]) || /^\[|\]$/.test(accountData[key]))) {
// seems like JSON array or string
try {
let value = JSON.parse(accountData[key]);
if (value === null) {
break;
}
result[key] = value;
} catch (err) {
this.logger.error({ msg: 'Failed to parse input from Redis', key, value: accountData[key], err });
}
} else {
// regular string
result[key] = accountData[key];
}
break;

case 'subconnections':
try {
let value = JSON.parse(accountData[key]);
Expand Down Expand Up @@ -301,12 +322,29 @@ class Account {
}
break;

case 'path':
if (
accountData[key] === null ||
accountData[key] === '*' ||
(Array.isArray(accountData[key]) && (accountData[key].length === 0 || (accountData[key].length === 1 && accountData[key][0] === '*')))
) {
result[key] = '';
} else {
try {
result[key] = JSON.stringify(accountData[key]);
} catch (err) {
this.logger.error({ msg: 'Failed to stringify input for Redis', key, err });
}
}
break;

case 'subconnections':
try {
result[key] = JSON.stringify(accountData[key]);
} catch (err) {
this.logger.error({ msg: 'Failed to stringify input for Redis', key, err });
}

break;

case 'imap':
Expand Down Expand Up @@ -520,7 +558,7 @@ class Account {

if ('path' in accountData && !reconnectRequested) {
try {
strictEqual(oldAccountData.path || '*', accountData.path || '*');
strictEqual(JSON.stringify(oldAccountData.path || '*'), JSON.stringify(accountData.path || '*'));
} catch (err) {
// changes detected!
reconnectRequested = true;
Expand Down
63 changes: 48 additions & 15 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,11 @@ class Connection {
mailbox = false;
}

async getCurrentListing(options) {
async getCurrentListing(options, allowSecondary) {
this.checkIMAPConnection();

const connectionClient = await this.getImapConnection(allowSecondary);

let accountData = await this.accountObject.loadAccountData();
let specialUseHints = {};

Expand All @@ -406,7 +408,7 @@ class Connection {
specialUseHints
});

let listing = await this.imapClient.list(options);
let listing = await connectionClient.list(options);

let inboxData = (listing || []).find(entry => /^INBOX$/i.test(entry.path));
if (inboxData && inboxData.delimiter) {
Expand Down Expand Up @@ -489,6 +491,12 @@ class Connection {

try {
let accountData = await this.accountObject.loadAccountData();

const accountPaths = [].concat(accountData.path || '*');
if (!accountPaths.length) {
accountPaths.push('*');
}

let listing = await this.getCurrentListing();

let syncNeeded = new Set();
Expand All @@ -497,8 +505,8 @@ class Connection {
// previously unseen
!this.mailboxes.has(normalizePath(entry.path))
) {
if (accountData.path && accountData.path !== '*') {
if (accountData.path !== entry.path && accountData.path !== entry.specialUse) {
if (!accountPaths.includes('*')) {
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
// ignore changes
entry.syncDisabled = true;
}
Expand Down Expand Up @@ -546,12 +554,22 @@ class Connection {
this.isGmail = imapClient.capabilities.has('X-GM-EXT-1') && listing.some(entry => entry.specialUse === '\\All');
this.isOutlook = /\boffice365\.com$/i.test(imapClient.host); // || /The Microsoft Exchange IMAP4 service is ready/.test(imapClient.greeting);

const accountPaths = [].concat(accountData.path || '*');
if (!accountPaths.length) {
accountPaths.push('*');
}

// store synced folder entries
const mainList = [];

for (let entry of listing) {
if (accountData.path && accountData.path !== '*') {
if (accountData.path !== entry.path && accountData.path !== entry.specialUse) {
if (!accountPaths.includes('*')) {
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
entry.syncDisabled = true;
} else {
this.main = entry;
// insert to stored list with the sorting index
let index = accountPaths.indexOf(entry.path) >= 0 ? accountPaths.indexOf(entry.path) : accountPaths.indexOf(entry.specialUse);
mainList.push({ index, entry });
}
} else {
if ((this.isGmail && entry.specialUse === '\\All') || (!this.isGmail && entry.specialUse === '\\Inbox')) {
Expand All @@ -570,6 +588,11 @@ class Connection {
this.mailboxes.set(normalizePath(entry.path), mailbox);
}

if (mainList.length) {
// set the highest synced entry as the main folder
this.main = mainList.sort((a, b) => a.index - b.index)[0].entry;
}

// Process untagged EXISTS responses
imapClient.on('exists', async event => {
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
Expand Down Expand Up @@ -1503,7 +1526,7 @@ class Connection {
async listMailboxes(options) {
this.checkIMAPConnection();

return await this.getCurrentListing(options);
return await this.getCurrentListing(options, true);
}

async moveMessage(id, target) {
Expand Down Expand Up @@ -1735,6 +1758,11 @@ class Connection {
}

runPostListing(accountData) {
const accountPaths = [].concat(accountData.path || '*');
if (!accountPaths.length) {
accountPaths.push('*');
}

this.getCurrentListing()
.then(listing => {
let syncNeeded = new Set();
Expand All @@ -1743,8 +1771,8 @@ class Connection {
// previously unseen
!this.mailboxes.has(normalizePath(entry.path))
) {
if (accountData.path && accountData.path !== '*') {
if (accountData.path !== entry.path && accountData.path !== entry.specialUse) {
if (!accountPaths.includes('*')) {
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
// ignore changes
entry.syncDisabled = true;
}
Expand Down Expand Up @@ -3365,7 +3393,7 @@ class Connection {
if (err.mailboxMissing) {
// this mailbox is missing, refresh listing
try {
await this.connection.getCurrentListing();
await this.getCurrentListing(false, true);
} catch (E) {
this.logger.error({ msg: 'Missing mailbox', err, E });
}
Expand Down Expand Up @@ -3396,7 +3424,7 @@ class Connection {

let mailboxes = [];

let listing = await this.getCurrentListing();
let listing = await this.getCurrentListing(false, true);

for (let path of accountData.subconnections || []) {
let entry = listing.find(entry => path === entry.path || path === entry.specialUse);
Expand All @@ -3405,16 +3433,21 @@ class Connection {
continue;
}

if (accountData.path && entry.path === accountData.path) {
const accountPaths = [].concat(accountData.path || '*');
if (!accountPaths.length) {
accountPaths.push('*');
}

if (accountPaths.includes(entry.path)) {
continue;
}

if (this.isGmail && (!accountData.path || accountData.path === '*') && !['\\Trash', '\\Junk'].includes(entry.specialUse)) {
if (this.isGmail && accountPaths.includes('*') && !['\\Trash', '\\Junk'].includes(entry.specialUse)) {
// no need to check this folder, as \All already covers it
continue;
}

if (!this.isGmail && (!accountData.path || accountData.path === '*') && entry.specialUse === '\\Inbox') {
if (!this.isGmail && accountPaths.includes('*') && entry.specialUse === '\\Inbox') {
// already the default
continue;
}
Expand Down
9 changes: 1 addition & 8 deletions lib/routes-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -6847,10 +6847,6 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}

accountData = formatAccountData(accountData);

if (accountData.path === '*') {
accountData.path = '';
}

accountData.imap = accountData.imap || {
disabled: !accountData.oauth2
};
Expand Down Expand Up @@ -6944,7 +6940,7 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
redirectUrl: `/admin/accounts/${request.params.account}`
}),

showAdvanced: accountData.path || accountData.proxy || accountData.webhooks,
showAdvanced: accountData.proxy || accountData.webhooks,

subConnectionInfo,

Expand Down Expand Up @@ -7283,7 +7279,6 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
email: request.payload.email,
proxy: request.payload.proxy,
smtpEhloName: request.payload.smtpEhloName,
path: request.payload.path || '*',
webhooks: request.payload.webhooks
};

Expand Down Expand Up @@ -7457,8 +7452,6 @@ ${Buffer.from(data.content, 'base64url').toString('base64')}
.example('https://myservice.com/imap/webhooks')
.description('Account-specific webhook URL'),

path: Joi.string().empty('').max(1024).default('').example('INBOX').description('Check changes only on selected path'),

smtp: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false),

smtp_auth_user: Joi.string().empty('').trim().max(1024),
Expand Down
11 changes: 10 additions & 1 deletion lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,14 @@ const accountCountersSchema = Joi.object({
events: Joi.object().unknown().description('Lifetime event counters').label('AcountCountersEvents').example({ messageNew: 30, messageDeleted: 5 })
}).label('AccountCounters');

const pathSchema = Joi.string().empty('').max(1024).example('INBOX');
const accountPathSchema = Joi.alternatives()
.try(pathSchema, Joi.array().items(pathSchema).min(1))
.allow(null)
.description(
'Check changes only on selected paths. Either a single string path or an array of paths. Can use references like `"\\Sent"` or `"\\Inbox"`. Set to `null` to unset.'
);

module.exports = {
ADDRESS_STRATEGIES,

Expand Down Expand Up @@ -1318,7 +1326,8 @@ module.exports = {
tokenRestrictionsSchema,
accountIdSchema,
ipSchema,
accountCountersSchema
accountCountersSchema,
accountPathSchema
};

/*
Expand Down
9 changes: 5 additions & 4 deletions workers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ const {
tokenRestrictionsSchema,
accountIdSchema,
ipSchema,
accountCountersSchema
accountCountersSchema,
accountPathSchema
} = require('../lib/schemas');

const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
Expand Down Expand Up @@ -2118,7 +2119,7 @@ When making API calls remember that requests against the same account are queued
name: Joi.string().max(256).required().example('My Email Account').description('Display name for the account'),
email: Joi.string().empty('').email().example('[email protected]').description('Default email address of the account'),

path: Joi.string().empty('').max(1024).default('*').example('INBOX').description('Check changes only on selected path'),
path: accountPathSchema,

subconnections: accountSchemas.subconnections,

Expand Down Expand Up @@ -2371,7 +2372,7 @@ When making API calls remember that requests against the same account are queued
name: Joi.string().max(256).example('My Email Account').description('Display name for the account'),
email: Joi.string().empty('').email().example('[email protected]').description('Default email address of the account'),

path: Joi.string().empty('').max(1024).default('*').example('INBOX').description('Check changes only on selected path'),
path: accountPathSchema,

subconnections: accountSchemas.subconnections,

Expand Down Expand Up @@ -2958,7 +2959,7 @@ When making API calls remember that requests against the same account are queued
notifyFrom: accountSchemas.notifyFrom,
syncFrom: accountSchemas.syncFrom,

path: Joi.string().empty('').max(1024).default('*').example('INBOX').description('Check changes only on selected path'),
path: accountPathSchema,

subconnections: accountSchemas.subconnections,

Expand Down

0 comments on commit a7c6abc

Please sign in to comment.