diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3ba6b..85851db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 (August 04, 2023) +* Added checkbox `Get Attachment` to `Poll for New Mail` trigger +* Added metadata field `Attachments` to `Send Mail` action + ## 2.0.0 (July 19, 2023) * Breaking change! Reworked authentication mechanism - implemented Secrets feature * Add new action - Send Mail diff --git a/README.md b/README.md index 78a7683..f15bd95 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Per one execution it is possible to poll 1000 mails by defaults, this can be cha * **Mail Folder** - Dropdown list with available Outlook mail folders * **Start Time** - Start datetime of polling. Defaults: `1970-01-01T00:00:00.000Z` * **Poll Only Unread Mail** - CheckBox, if set, only unread mails will be poll +* **Get Attachment** - CheckBox, if checked, email attachments will be downloaded to the platform and the link will be provided as a part of the output metadata with the key `attachments` * **Emit Behavior** - Options are: default is `Emit Individually` emits each mail in separate message, `Emit All` emits all found mails in one message #### Expected output metadata @@ -182,3 +183,4 @@ In case of a success, output metadata simply repeats the incoming message. I.e. ``` ## Known limitations + * When utilizing the `Send Mail` action with attachments, the component operates smoothly with files up to 20MB by default. However, if you intend to work with larger files, it is advisable to create or increase the environment variable `EIO_REQUIRED_RAM_MB`, which serves as the memory usage limit for the component, initially set at 256MB \ No newline at end of file diff --git a/component.json b/component.json index e78b41b..67ce6f9 100644 --- a/component.json +++ b/component.json @@ -1,7 +1,7 @@ { "title": "Outlook", "description": "elastic.io integration component for Office 365 Outlook REST API", - "version": "2.0.0", + "version": "2.1.0", "authClientTypes": [ "oauth2" ], @@ -55,6 +55,10 @@ "label": "Poll Only Unread Mail", "viewClass": "CheckBoxView" }, + "getAttachment": { + "label": "Get Attachment", + "viewClass": "CheckBoxView" + }, "emitBehavior": { "viewClass": "SelectView", "prompt": "Select Emit Behavior, defaults to Emit Individually", diff --git a/lib/OutlookClient.js b/lib/OutlookClient.js index 8f15701..07a0d25 100644 --- a/lib/OutlookClient.js +++ b/lib/OutlookClient.js @@ -13,11 +13,13 @@ class OutlookClient extends Client { } async sendMail(msg) { - const message = utils.buildMessage(msg); + const message = await utils.buildMessage(msg); await this.apiRequest({ url: '/me/sendMail', method: 'POST', data: message, + maxContentLength: Infinity, + maxBodyLength: Infinity, }); } @@ -47,6 +49,24 @@ class OutlookClient extends Client { return response.data.value; } + async getAttachments(folderId, mailId) { + const url = `/me/mailFolders/${folderId}/messages/${mailId}/attachments`; + const response = await this.apiRequest({ + url, + method: 'GET', + }); + return response.data.value; + } + + async downloadAttachment(folderId, mailId, attachmentId) { + const url = `/me/mailFolders/${folderId}/messages/${mailId}/attachments/${attachmentId}/$value`; + return this.apiRequest({ + url, + method: 'GET', + responseType: 'stream', + }); + } + async getMyLatestContacts(lastModifiedDateTime) { const response = await this.apiRequest({ url: `/me/contacts?$orderby=lastModifiedDateTime asc&$top=900&$filter=lastModifiedDateTime gt ${lastModifiedDateTime}`, diff --git a/lib/schemas/readMail.out.json b/lib/schemas/readMail.out.json index 16e9ad3..06b9017 100644 --- a/lib/schemas/readMail.out.json +++ b/lib/schemas/readMail.out.json @@ -170,6 +170,26 @@ "required": true } } + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "size": { + "type": "number" + } + } + } } } -} +} \ No newline at end of file diff --git a/lib/schemas/sendMail.in.json b/lib/schemas/sendMail.in.json index 8f7c42e..bbc2c6e 100644 --- a/lib/schemas/sendMail.in.json +++ b/lib/schemas/sendMail.in.json @@ -71,7 +71,10 @@ "type": "string", "required": false, "name": "The type of the content. Possible values are text and html", - "enum": ["text", "html"] + "enum": [ + "text", + "html" + ] } } }, @@ -79,6 +82,23 @@ "type": "boolean", "required": false, "name": "Save to Sent items" + }, + "attachments": { + "type": "array", + "title": "Attachments", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "File name" + }, + "url": { + "type": "string", + "title": "URL to file" + } + } + } } } -} +} \ No newline at end of file diff --git a/lib/triggers/readMail.js b/lib/triggers/readMail.js index 84bd430..01401dd 100644 --- a/lib/triggers/readMail.js +++ b/lib/triggers/readMail.js @@ -1,14 +1,21 @@ -/* eslint-disable no-param-reassign */ +/* eslint-disable no-param-reassign, no-restricted-syntax, no-loop-func */ const { messages } = require('elasticio-node'); +const commons = require('@elastic.io/component-commons-library'); const { OutlookClient } = require('../OutlookClient'); const { getFolders } = require('../utils/selectViewModels'); +const { + getUserAgent, +} = require('../utils/utils'); +/** + * @type {OutlookClient} + */ let client; exports.process = async function process(msg, cfg, snapshot) { this.logger.info('Poll for New Mail trigger starting...'); const { - folderId, startTime, pollOnlyUnreadMail, emitBehavior = 'emitIndividually', + folderId, startTime, pollOnlyUnreadMail, emitBehavior = 'emitIndividually', getAttachment, } = cfg; snapshot.lastModifiedDateTime = snapshot.lastModifiedDateTime || startTime || new Date(0).toISOString(); this.logger.info(`Getting message from specified folder, starting from: ${snapshot.lastModifiedDateTime}, pollOnlyUnreadMail ${pollOnlyUnreadMail || false}`); @@ -18,6 +25,35 @@ exports.process = async function process(msg, cfg, snapshot) { this.logger.debug('Results are received'); if (results.length > 0) { this.logger.info(`New Mails found, going to emit, Emit Behavior: ${emitBehavior}...`); + if (getAttachment) { + const mailsWithAttachments = results.filter((mail) => mail.hasAttachments); + if (mailsWithAttachments.length > 0) { + this.logger.info(`${mailsWithAttachments.length} mails with attachments found`); + const attachmentsProcessor = new commons.AttachmentProcessor(getUserAgent(), msg.id); + for (const mail of mailsWithAttachments) { + const mailAttachments = await client.getAttachments(folderId, mail.id); + this.logger.info(`${mailAttachments.length} attachments in mail found, going to download`); + for (const mailAttachment of mailAttachments) { + const getAttachmentFunction = async () => (await client.downloadAttachment(folderId, mail.id, mailAttachment.id)).data; + const attachmentId = await attachmentsProcessor.uploadAttachment(getAttachmentFunction); + const attachmentUrl = attachmentsProcessor.getMaesterAttachmentUrlById(attachmentId); + const attachment = { + name: mailAttachment.name, + contentType: mailAttachment.contentType, + size: mailAttachment.size, + url: attachmentUrl, + }; + if (mail.attachments) { + mail.attachments.push(attachment); + } else { + mail.attachments = [attachment]; + } + } + } + } else { + this.logger.info('Mails with attachments not found'); + } + } switch (emitBehavior) { case 'emitIndividually': this.logger.info('Starting to emit individually found mails...'); diff --git a/lib/utils/utils.js b/lib/utils/utils.js index aada6ab..6c6f54f 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -1,6 +1,9 @@ +/* eslint-disable no-restricted-syntax */ const { axiosReqWithRetryOnServerError } = require('@elastic.io/component-commons-library/dist/src/externalApi'); const { version, dependencies } = require('@elastic.io/component-commons-library/package.json'); +const commons = require('@elastic.io/component-commons-library'); const compJson = require('../../component.json'); +const packageJson = require('../../package.json'); const auth = { username: process.env.ELASTICIO_API_USERNAME, @@ -33,14 +36,36 @@ module.exports.refreshSecret = async (credentialId) => { module.exports.isNumberNaN = (num) => Number(num).toString() === 'NaN'; -module.exports.buildMessage = (msg) => { +const getUserAgent = () => { + const { version: compVersion } = compJson; + const libVersion = packageJson.dependencies['@elastic.io/component-commons-library']; + return `${compJson.title}/${compVersion} component-commons-library/${libVersion}`; +}; + +module.exports.buildMessage = async (msg) => { const { ccRecipients, saveToSentItems, subject, toRecipients, + attachments: attachmentsIn, } = msg.body; - return { + const attachmentsOut = []; + if (attachmentsIn && attachmentsIn.length > 0) { + const attachmentsProcessor = new commons.AttachmentProcessor(getUserAgent(), msg.id); + for (const attachment of attachmentsIn) { + const { name, url } = attachment; + if (!name || !url) throw new Error('Both "File name" and "URL to file" fields must be provided'); + const { data } = await attachmentsProcessor.getAttachment(url, 'arraybuffer'); + const attachmentBody = { + '@odata.type': '#microsoft.graph.fileAttachment', + name, + contentBytes: Buffer.from(data, 'binary').toString('base64'), + }; + attachmentsOut.push(attachmentBody); + } + } + const result = { message: { ccRecipients, body: { @@ -52,4 +77,8 @@ module.exports.buildMessage = (msg) => { }, saveToSentItems: saveToSentItems || true, }; + if (attachmentsOut.length > 0) result.message.attachments = attachmentsOut; + return result; }; + +module.exports.getUserAgent = getUserAgent;