Skip to content

Commit

Permalink
fix(api): Added support for paging cursors
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Jul 4, 2024
1 parent 91b3cad commit d3f7685
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 15 deletions.
19 changes: 16 additions & 3 deletions lib/api-client/gmail-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class PageCursor {
if (cursorType && this.type !== cursorType) {
let error = new Error('Invalid cursor');
error.code = 'InvalidCursorType';
error.statusCode = 400;
throw error;
}
}
Expand Down Expand Up @@ -139,7 +140,7 @@ class PageCursor {
}

prevPageCursor() {
if (this.cursorList.length <= 1) {
if (this.cursorList.length < 1) {
return null;
}

Expand Down Expand Up @@ -359,6 +360,14 @@ class GmailClient extends BaseClient {

await this.prepare();

let page = Number(query.page) || 0;
if (page > 0) {
let error = new Error('Invalid page number. Only paging cursors are allowed for Gmail accounts.');
error.code = 'InvalidInput';
error.statusCode = 400;
throw error;
}

let pageSize = Math.abs(Number(query.pageSize) || 20);
let requestQuery = {
maxResults: pageSize
Expand Down Expand Up @@ -1274,11 +1283,13 @@ class GmailClient extends BaseClient {
if (this.cachedLabels && !force && now <= this.cachedLabelsTime + 3600 * 1000) {
return this.cachedLabels;
}
let cachedLabels;

try {
cachedLabels = await this.request(`${GMAIL_API_BASE}/gmail/v1/users/me/labels`);
let cachedLabels = await this.request(`${GMAIL_API_BASE}/gmail/v1/users/me/labels`);
this.cachedLabels = cachedLabels?.labels;
this.cachedLabelsTime = now;

return this.cachedLabels;
} catch (err) {
if (this.cachedLabels) {
return this.cachedLabels;
Expand Down Expand Up @@ -1613,6 +1624,7 @@ class GmailClient extends BaseClient {
if (disabledKey in search) {
let error = new Error(`Unsupported search term "${disabledKey}"`);
error.code = 'UnsupportedSearchTerm';
error.statusCode = 400;
throw error;
}
}
Expand Down Expand Up @@ -1646,6 +1658,7 @@ class GmailClient extends BaseClient {
default: {
let error = new Error(`Unsupported search header "${headerKey}"`);
error.code = 'UnsupportedSearchTerm';
error.statusCode = 400;
throw error;
}
}
Expand Down
60 changes: 59 additions & 1 deletion lib/mailbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -2669,7 +2669,16 @@ class Mailbox {

async listMessages(options, allowSecondary) {
options = options || {};

let page = Number(options.page) || 0;

if (options.cursor) {
let cursorPage = this.decodeCursorStr(options.cursor);
if (typeof cursorPage === 'number' && cursorPage >= 0) {
page = cursorPage;
}
}

let pageSize = Math.abs(Number(options.pageSize) || 20);

const connectionClient = await this.connection.getImapConnection(allowSecondary);
Expand Down Expand Up @@ -2698,10 +2707,15 @@ class Mailbox {
let messages = [];
let seqMax, seqMin, range;

let nextPageCursor = page < pages - 1 ? this.encodeCursorString(page + 1) : null;
let prevPageCursor = page > 0 ? this.encodeCursorString(Math.min(page - 1, pages - 1)) : null;

if (!messageCount || page >= pages) {
return {
page,
pages,
nextPageCursor,
prevPageCursor,
messages
};
}
Expand Down Expand Up @@ -2762,7 +2776,9 @@ class Mailbox {
total: messageCount,
page,
pages,
// list newer first. servers like yahoo do not return ordered list, so we need to order manually
nextPageCursor,
prevPageCursor,
// List newer entries first. Servers like yahoo do not return ordered list, so we need to order manually
messages: messages.sort((a, b) => b.uid - a.uid)
};
} finally {
Expand Down Expand Up @@ -2884,6 +2900,48 @@ class Mailbox {

return false;
}

decodeCursorStr(cursorStr) {
let type = 'imap';

if (cursorStr) {
let splitPos = cursorStr.indexOf('_');
if (splitPos >= 0) {
let cursorType = cursorStr.substring(0, splitPos);
cursorStr = cursorStr.substring(splitPos + 1);
if (cursorType && type !== cursorType) {
let error = new Error('Invalid cursor');
error.code = 'InvalidCursorType';
throw error;
}
}

try {
let { page: cursorPage } = JSON.parse(Buffer.from(cursorStr, 'base64url'));
if (typeof cursorPage === 'number' && cursorPage >= 0) {
return cursorPage;
}
} catch (err) {
this.logger.error({ msg: 'Cursor parsing error', cursorStr, err });

let error = new Error('Invalid paging cursor');
error.code = 'InvalidCursorValue';
error.statusCode = 400;
throw error;
}
}

return null;
}

encodeCursorString(cursorPage) {
if (typeof cursorPage !== 'number' || cursorPage < 0) {
return null;
}
cursorPage = cursorPage || 0;
let type = 'imap';
return `${type}_${Buffer.from(JSON.stringify({ page: cursorPage })).toString('base64url')}`;
}
}

module.exports.Mailbox = Mailbox;
8 changes: 2 additions & 6 deletions lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ const documentStoreSchema = Joi.boolean()
const searchSchema = Joi.object({
seq: Joi.string()
.max(8 * 1024)
.description('Sequence number range. Not allowed with `documentStore`.'),
.description('Sequence number range. Only for IMAP accounts.'),

answered: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).description('Check if message is answered or not').label('AnsweredFlag'),
deleted: Joi.boolean()
Expand Down Expand Up @@ -1039,11 +1039,7 @@ const searchSchema = Joi.object({
.description('UID range')
.label('UIDRange'),

modseq: Joi.number()
.integer()
.min(0)
.description('Matches messages with modseq higher than value. Not allowed with `documentStore`.')
.label('ModseqLarger'),
modseq: Joi.number().integer().min(0).description('Matches messages with modseq higher than value. Only for IMAP accounts.').label('ModseqLarger'),

before: Joi.date().description('Matches messages received before date').label('EnvelopeBefore'),
since: Joi.date().description('Matches messages received after date').label('EnvelopeSince'),
Expand Down
33 changes: 28 additions & 5 deletions workers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3543,7 +3543,7 @@ When making API calls remember that requests against the same account are queued
.min(0)
.max(1024 * 1024 * 1024)
.example(5 * 1025 * 1024)
.description('Max length of text content. This setting is ignored if `documentStore` is `true`.'),
.description('Max length of text content'),
textType: Joi.string()
.lowercase()
.valid('html', 'plain', '*')
Expand Down Expand Up @@ -4308,7 +4308,7 @@ When making API calls remember that requests against the same account are queued
.min(0)
.max(1024 * 1024 * 1024)
.example(MAX_ATTACHMENT_SIZE)
.description('Max length of text content. This setting is ignored if `documentStore` is `true`.'),
.description('Max length of text content'),
textType: Joi.string()
.lowercase()
.valid('html', 'plain', '*')
Expand Down Expand Up @@ -4393,14 +4393,25 @@ When making API calls remember that requests against the same account are queued

query: Joi.object({
path: Joi.string().required().example('INBOX').description('Mailbox folder path').label('Path'),

cursor: Joi.string()
.trim()
.empty('')
.max(1024 * 1024)
.example('imap_kcQIji3UobDDTxc')
.description('Paging cursor from `nextPageCursor` or `prevPageCursor` value')
.label('PageCursor'),
page: Joi.number()
.integer()
.min(0)
.max(1024 * 1024)
.default(0)
.example(0)
.description('Page number (zero indexed, so use 0 for first page)')
.description(
'Page number (zero indexed, so use 0 for first page). Only supported for IMAP accounts. Deprecated, use paging cursor instead.'
)
.label('PageNumber'),

pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize'),
documentStore: documentStoreSchema.default(false)
}).label('MessageQuery')
Expand Down Expand Up @@ -4496,14 +4507,26 @@ When making API calls remember that requests against the same account are queued
otherwise: Joi.required()
})
.example('INBOX')
.description('Mailbox folder path. Not required if `documentStore` is `true`'),
.description('Mailbox folder path'),

cursor: Joi.string()
.trim()
.empty('')
.max(1024 * 1024)
.example('imap_kcQIji3UobDDTxc')
.description('Paging cursor from `nextPageCursor` or `prevPageCursor` value')
.label('PageCursor'),
page: Joi.number()
.integer()
.min(0)
.max(1024 * 1024)
.default(0)
.example(0)
.description('Page number (zero indexed, so use 0 for first page)'),
.description(
'Page number (zero indexed, so use 0 for first page). Only supported for IMAP accounts. Deprecated, use paging cursor instead.'
)
.label('PageNumber'),

pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page'),
documentStore: documentStoreSchema.default(false),
exposeQuery: Joi.boolean()
Expand Down

0 comments on commit d3f7685

Please sign in to comment.