diff --git a/lib/outbox.js b/lib/outbox.js index d656de44..475d4cbe 100644 --- a/lib/outbox.js +++ b/lib/outbox.js @@ -91,4 +91,50 @@ async function del(options) { return false; } -module.exports = { list, del }; +async function get(options) { + options = options || {}; + + let logger = options.logger; + + if (!options.queueId) { + return false; + } + + let job = await submitQueue.getJob(options.queueId); + if (!job) { + return false; + } + + let scheduled = job.timestamp + (Number(job.opts.delay) || 0); + + let backoffDelay = Number(job.opts.backoff && job.opts.backoff.delay) || 0; + let nextAttempt = job.attemptsMade ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade) * backoffDelay) : scheduled; + + if (job.opts.attempts <= job.attemptsMade) { + nextAttempt = false; + } + + try { + let queueEntryBuf = await redis.hgetBuffer(`${REDIS_PREFIX}iaq:${job.data.account}`, job.data.queueId); + if (!queueEntryBuf) { + return false; + } + } catch (err) { + logger.error({ msg: 'Failed to retrieve queued message', account: job.data.account, queueId: job.data.queueId, messageId: job.data.messageId, err }); + throw err; + } + + let response = Object.assign(job.data, { + created: new Date(Number(job.created || job.timestamp)).toISOString(), + //status: job.name, + progress: job.progress, + attemptsMade: job.attemptsMade, + attempts: job.opts.attempts, + scheduled: new Date(scheduled).toISOString(), + nextAttempt: nextAttempt ? new Date(nextAttempt).toISOString() : false + }); + + return response; +} + +module.exports = { list, del, get }; diff --git a/lib/schemas.js b/lib/schemas.js index 8c9aac17..e834d95b 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -1328,6 +1328,43 @@ const defaultAccountTypeSchema = Joi.string() .description('Display the form for the specified account type (either "imap" or an OAuth2 app ID) instead of allowing the user to choose') .label('DefaultAccountType'); +const outboxEntrySchema = Joi.object({ + queueId: Joi.string().example('1869c5692565f756b33').description('Outbox queue ID'), + account: accountIdSchema.required(), + source: Joi.string().example('smtp').valid('smtp', 'api').description('How this message was added to the queue'), + + messageId: Joi.string().max(996).example('').description('Message ID'), + envelope: Joi.object({ + from: Joi.string().email().allow('').example('sender@example.com'), + to: Joi.array().items(Joi.string().email().required().example('recipient@example.com')) + }).description('SMTP envelope'), + + subject: Joi.string() + .allow('') + .max(10 * 1024) + .example('What a wonderful message') + .description('Message subject'), + + created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this message was queued'), + scheduled: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('When this message is supposed to be delivered'), + nextAttempt: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Next delivery attempt'), + + attemptsMade: Joi.number().integer().example(3).description('How many times EmailEngine has tried to deliver this email'), + attempts: Joi.number().integer().example(3).description('How many delivery attempts to make until message is considered as failed'), + + progress: Joi.object({ + status: Joi.string().valid('queued', 'processing', 'submitted', 'error').example('queued').description('Current state of the sending'), + response: Joi.string().example('250 Message Accepted').description('Response from the SMTP server. Only if state=processing'), + error: Joi.object({ + message: Joi.string().example('Authentication failed').description('Error message'), + code: Joi.string().example('EAUTH').description('Error code'), + statusCode: Joi.string().example(502).description('SMTP response code') + }) + .label('OutboxListProgressError') + .description('Error information if state=error') + }).label('OutboxEntryProgress') +}).label('OutboxEntry'); + module.exports = { ADDRESS_STRATEGIES, @@ -1361,7 +1398,8 @@ module.exports = { accountPathSchema, messageSpecialUseSchema, defaultAccountTypeSchema, - fromAddressSchema + fromAddressSchema, + outboxEntrySchema }; /* diff --git a/workers/api.js b/workers/api.js index f9d01e52..83b28f4d 100644 --- a/workers/api.js +++ b/workers/api.js @@ -160,7 +160,8 @@ const { accountCountersSchema, accountPathSchema, defaultAccountTypeSchema, - fromAddressSchema + fromAddressSchema, + outboxEntrySchema } = require('../lib/schemas'); const OAuth2ProviderSchema = Joi.string() @@ -5837,55 +5838,67 @@ When making API calls remember that requests against the same account are queued page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'), pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'), - messages: Joi.array() - .items( - Joi.object({ - queueId: Joi.string().example('1869c5692565f756b33').description('Outbox queue ID'), - account: accountIdSchema.required(), - source: Joi.string().example('smtp').valid('smtp', 'api').description('How this message was added to the queue'), + messages: Joi.array().items(outboxEntrySchema).label('OutboxListEntries') + }).label('OutboxListResponse'), + failAction: 'log' + } + } + }); - messageId: Joi.string().max(996).example('').description('Message ID'), - envelope: Joi.object({ - from: Joi.string().email().allow('').example('sender@example.com'), - to: Joi.array().items(Joi.string().email().required().example('recipient@example.com')) - }).description('SMTP envelope'), + server.route({ + method: 'GET', + path: '/v1/outbox/{queueId}', - subject: Joi.string() - .allow('') - .max(10 * 1024) - .example('What a wonderful message') - .description('Message subject'), - - created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this message was queued'), - scheduled: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('When this message is supposed to be delivered'), - nextAttempt: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('Next delivery attempt'), - - attemptsMade: Joi.number().integer().example(3).description('How many times EmailEngine has tried to deliver this email'), - attempts: Joi.number() - .integer() - .example(3) - .description('How many delivery attempts to make until message is considered as failed'), - - progress: Joi.object({ - status: Joi.string() - .valid('queued', 'processing', 'submitted', 'error') - .example('queued') - .description('Current state of the sending'), - response: Joi.string() - .example('250 Message Accepted') - .description('Response from the SMTP server. Only if state=processing'), - error: Joi.object({ - message: Joi.string().example('Authentication failed').description('Error message'), - code: Joi.string().example('EAUTH').description('Error code'), - statusCode: Joi.string().example(502).description('SMTP response code') - }) - .label('OutboxListProgressError') - .description('Error information if state=error') - }).label('OutboxListProgress') - }).label('OutboxListItem') - ) - .label('OutboxListEntries') - }).label('OutboxListResponse'), + async handler(request) { + try { + let outboxEntry = await outbox.get({ queueId: request.params.queueId, logger }); + if (!outboxEntry) { + let message = 'Requested queue entry was not found'; + let error = Boom.boomify(new Error(message), { statusCode: 404 }); + throw error; + } + return outboxEntry; + } catch (err) { + request.logger.error({ msg: 'API request failed', err }); + if (Boom.isBoom(err)) { + throw err; + } + let error = Boom.boomify(err, { statusCode: err.statusCode || 500 }); + if (err.code) { + error.output.payload.code = err.code; + } + throw error; + } + }, + + options: { + description: 'Get queued message', + notes: 'Gets a queued message in the Outbox', + tags: ['api', 'Outbox'], + + plugins: {}, + + auth: { + strategy: 'api-token', + mode: 'required' + }, + cors: CORS_CONFIG, + + validate: { + options: { + stripUnknown: false, + abortEarly: false, + convert: true + }, + failAction, + + params: Joi.object({ + queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required() + }).label('OutboxEntryParams') + }, + + response: { + schema: outboxEntrySchema, failAction: 'log' } } @@ -5935,7 +5948,7 @@ When making API calls remember that requests against the same account are queued params: Joi.object({ queueId: Joi.string().max(100).example('d41f0423195f271f').description('Queue identifier for scheduled email').required() - }).label('DeleteOutboxEntry') + }).label('OutboxEntryParams') }, response: {