UNPKG

@pipedream/microsoft_outlook

Version:

Pipedream Microsoft Outlook Components

720 lines (703 loc) 21.4 kB
import { getFileStreamAndMetadata } from "@pipedream/platform"; import { Client } from "@microsoft/microsoft-graph-client"; import "isomorphic-fetch"; import pickBy from "lodash.pickby"; const DEFAULT_LIMIT = 50; export default { type: "app", app: "microsoft_outlook", propDefinitions: { recipients: { label: "Recipients", description: "Array of email addresses", type: "string[]", optional: true, default: [], }, ccRecipients: { label: "CC Recipients", description: "Array of email addresses", type: "string[]", optional: true, default: [], }, bccRecipients: { label: "BCC Recipients", description: "Array of email addresses", type: "string[]", optional: true, default: [], }, subject: { label: "Subject", description: "Subject of the email", type: "string", optional: true, }, contentType: { label: "Content Type", description: "Content type (default `text`)", type: "string", optional: true, options: [ "text", "html", ], default: "text", }, content: { label: "Content", description: "Content of the email in text or html format", type: "string", optional: true, }, files: { type: "string[]", label: "File Paths or URLs", description: "Provide either an array of file URLs or an array of paths to a files in the /tmp directory (for example, /tmp/myFile.pdf).", format: "file-ref", optional: true, }, contact: { label: "Contact", description: "The contact to be updated", type: "string", async options({ page }) { const limit = DEFAULT_LIMIT; const contactResponse = await this.listContacts({ params: { $top: limit, $skip: limit * page, }, }); return contactResponse.value.map((co) => ({ label: co.displayName, value: co.id, })); }, }, givenName: { label: "Given name", description: "Given name of the contact", type: "string", optional: true, }, surname: { label: "Surname", description: "Surname of the contact", type: "string", optional: true, }, emailAddresses: { label: "Email addresses", description: "Email addresses", type: "string[]", optional: true, }, businessPhones: { label: "Recipients", description: "Array of phone numbers", type: "string[]", optional: true, }, expand: { label: "Expand", description: "Additional properties", type: "object", optional: true, }, label: { type: "string", label: "Label", description: "The name of the label/category to add", async options({ userId, messageId, excludeMessageLabels, onlyMessageLabels, }) { const { value } = await this.listLabels({ userId, }); let labels = value; if (messageId) { const { categories } = await this.getMessage({ userId, messageId, }); labels = excludeMessageLabels ? labels.filter(({ displayName }) => !categories.includes(displayName)) : onlyMessageLabels ? labels.filter(({ displayName }) => categories.includes(displayName)) : labels; } return labels?.map(({ displayName }) => displayName) || []; }, }, messageId: { type: "string", label: "Message ID", description: "The identifier of the message to update", async options({ userId, page, }) { const limit = DEFAULT_LIMIT; const { value } = await this.listMessages({ userId, params: { $top: limit, $skip: limit * page, $orderby: "createdDateTime desc", }, }); return value?.map(({ id: value, subject: label, }) => ({ value, label, })) || []; }, }, folderIds: { type: "string[]", label: "Folder IDs to Monitor", description: "Specify the folder IDs or names in Outlook that you want to monitor for new emails. Leave empty to monitor all folders (excluding \"Sent Items\" and \"Drafts\").", async options({ page }) { const limit = DEFAULT_LIMIT; const folders = await this.listAllFolders({ params: { $top: limit, $skip: limit * page, }, }); return folders?.map(({ id: value, displayName: label, }) => ({ value, label, })) || []; }, }, attachmentId: { type: "string", label: "Attachment ID", description: "The identifier of the attachment to download", async options({ userId, messageId, page, }) { const limit = DEFAULT_LIMIT; const { value: attachments } = await this.listAttachments({ userId, messageId, params: { $top: limit, $skip: limit * page, }, }); return attachments?.map(({ id: value, name: label, }) => ({ value, label, })) || []; }, }, userId: { type: "string", label: "User ID", description: "The ID of the user to get messages for", useQuery: true, async options({ query }) { const args = query ? { params: { $search: `"${encodeURIComponent("displayName:" + query)}" OR "${encodeURIComponent("mail:" + query)}" OR "${encodeURIComponent("userPrincipalName:" + query)}"`, }, headers: { "ConsistencyLevel": "eventual", }, } : {}; const { value: users } = await this.listUsers(args); return users?.map(({ id: value, displayName, mail, }) => ({ value, label: `${displayName} (${mail})`, })) || []; }, }, sharedFolderId: { type: "string", label: "Shared Folder ID", description: "The ID of the shared folder to get messages for", async options({ userId, page, }) { const sharedFolders = await this.listSharedFolders({ userId, params: { $top: DEFAULT_LIMIT, $skip: DEFAULT_LIMIT * page, }, }); return sharedFolders?.map(({ id: value, displayName, }) => ({ value, label: displayName, })) || []; }, }, folderId: { type: "string", label: "Folder ID", description: "The ID of the mail folder to retrieve. Use the **List Folders** action to get the list of folders.", }, maxResults: { type: "integer", label: "Max Results", description: "The maximum number of results to return", default: 100, optional: true, }, search: { type: "string", label: "Search", description: "Search for an email in Microsoft Outlook. Can search for specific message properties such as `\"to:example@example.com\"` or `\"subject:example\"`. If the property is excluded, the search targets the default propertes `from`, `subject`, and `body`. For example, `\"pizza\"` will search for messages with the word `pizza` in the subject, body, or from address, but `\"to:example@example.com\"` will only search for messages to `example@example.com`. Not for use with `$filter` or `$orderby`.", optional: true, }, filter: { type: "string", label: "Filter", description: "Filters results. For example, `contains(subject, 'meet for lunch?')` will include messages whose subject contains ‘meet for lunch?’. [See documentation](https://learn.microsoft.com/en-us/graph/filter-query-parameter) for the full list of operations. Not for use with `$search`.", optional: true, }, orderBy: { type: "string", label: "Order By", description: "Order results by a property. For example, `receivedDateTime desc` will order messages by the received date in descending order. Not for use with `$search`.", optional: true, }, }, methods: { _userPath(userId) { return userId ? `/users/${userId}` : "/me"; }, client() { return Client.init({ authProvider: (done) => { done(null, this.$auth.oauth_access_token); }, }); }, async createHook({ data = {} } = {}) { return await this.client().api("/subscriptions") .post(data); }, async renewHook({ hookId, data = {}, } = {}) { return await this.client().api(`/subscriptions/${hookId}`) .patch(data); }, async deleteHook({ hookId } = {}) { return await this.client().api(`/subscriptions/${hookId}`) .delete(); }, async streamToBase64(stream) { return new Promise((resolve, reject) => { const chunks = []; stream.on("data", (chunk) => chunks.push(chunk)); stream.on("end", () => { const buffer = Buffer.concat(chunks); resolve(buffer.toString("base64")); }); stream.on("error", reject); }); }, async streamToBuffer(stream) { // Node.js Readable stream if (typeof stream.on === "function") { return new Promise((resolve, reject) => { const chunks = []; stream.on("data", (chunk) => chunks.push(chunk)); stream.on("end", () => resolve(Buffer.concat(chunks))); stream.on("error", reject); }); } // Web ReadableStream (WHATWG) if (typeof stream.getReader === "function") { const reader = stream.getReader(); const chunks = []; while (true) { const { done, value, } = await reader.read(); if (done) break; chunks.push(Buffer.from(value)); } return Buffer.concat(chunks); } throw new Error("Unknown stream type returned by Microsoft Graph client"); }, async prepareMessageBody(self) { const toRecipients = []; const ccRecipients = []; const bccRecipients = []; for (const address of self.recipients) { toRecipients.push({ emailAddress: { address, }, }); } for (const address of self.ccRecipients) { if (address.trim() !== "") { ccRecipients.push({ emailAddress: { address, }, }); } } for (const address of self.bccRecipients) { if (address.trim() !== "") { bccRecipients.push({ emailAddress: { address, }, }); } } const attachments = []; for (let i = 0; self.files && i < self.files.length; i++) { const { stream, metadata, } = await getFileStreamAndMetadata(self.files[i]); const base64 = await this.streamToBase64(stream); attachments.push({ "@odata.type": "#microsoft.graph.fileAttachment", "name": metadata.name, "contentType": metadata.contentType, "contentBytes": base64, }); } const message = { subject: self.subject, attachments, }; if (self.content) { message.body = { content: self.content, contentType: self.contentType, }; } if (toRecipients.length > 0) message.toRecipients = toRecipients; if (ccRecipients.length > 0) message.ccRecipients = ccRecipients; if (bccRecipients.length > 0) message.bccRecipients = bccRecipients; return message; }, async sendEmail({ userId, data = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/sendMail`) .post(data); }, async replyToEmail({ userId, messageId, data = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}/reply`) .post(data); }, async createDraft({ userId, data = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages`) .post(data); }, async createContact({ data = {} } = {}) { return await this.client().api("/me/contacts") .post(data); }, async listContacts({ filterAddress, params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .get(); } if (filterAddress) { params["$filter"] = `emailAddresses/any(a:a/address eq '${filterAddress}')`; } return await this.client().api("/me/contacts") .query(pickBy(params)) .get(); }, async updateContact({ contactId, data = {}, } = {}) { return await this.client().api(`/me/contacts/${contactId}`) .patch(data); }, async getMessage({ userId, messageId, params = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}`) .query(pickBy(params)) .get(); }, async listMessages({ userId, folderScope, params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .header("ConsistencyLevel", "eventual") .get(); } const path = folderScope ? `${this._userPath(userId)}/mailFolders/${folderScope}/messages` : `${this._userPath(userId)}/messages`; return await this.client().api(path) .header("ConsistencyLevel", "eventual") .query(pickBy(params)) .get(); }, /** * List messages from a user's inbox. Uses this.client(), this._userPath(userId), * and pickBy(params) for the initial request; when nextLink is provided, fetches * the next page from that URL so callers can paginate using Graph's @odata.nextLink. * * @param {Object} opts - Options for the request * @param {string} [opts.userId] - User ID; omitted for the signed-in user * @param {Object} [opts.params={}] - Query params (e.g. $filter, $select, $orderby) * @param {string} [opts.nextLink] - Full URL for the next page (from @odata.nextLink) * @returns {Promise<Object>} Graph API response with value array and optional @odata.nextLink */ async listInboxMessages({ userId, params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .get(); } return await this.client().api(`${this._userPath(userId)}/mailFolders/inbox/messages`) .query(pickBy(params)) .get(); }, async countMessages({ userId, folderScope, sharedFolderId, filter, } = {}) { let path; if (sharedFolderId && userId) { path = `/users/${userId}/mailFolders/${sharedFolderId}/messages`; } else if (folderScope) { path = `${this._userPath(userId)}/mailFolders/${folderScope}/messages`; } else { path = `${this._userPath(userId)}/messages`; } return await this.client().api(path) .header("ConsistencyLevel", "eventual") .query(pickBy({ $count: "true", $top: 1, $filter: filter || undefined, })) .get(); }, async listSharedFolderMessages({ userId, sharedFolderId, params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .header("ConsistencyLevel", "eventual") .get(); } return await this.client().api(`/users/${userId}/mailFolders/${sharedFolderId}/messages`) .header("ConsistencyLevel", "eventual") .query(pickBy(params)) .get(); }, async getContact({ contactId, params = {}, } = {}) { return await this.client().api(`/me/contacts/${contactId}`) .query(pickBy(params)) .get(); }, async listLabels({ userId, params = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/outlook/masterCategories`) .query(pickBy(params)) .get(); }, async listFolders({ params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .get(); } return await this.client().api("/me/mailFolders") .query(pickBy(params)) .get(); }, async listAllFolders({ parentFolderId, params = {}, } = {}) { const { value } = await this.client().api(`/me/mailFolders${parentFolderId ? `/${parentFolderId}/childFolders` : ""}`) .query(pickBy(params)) .get(); const foldersArray = []; for (const folder of value) { foldersArray.push(folder); foldersArray.push(...await this.listAllFolders({ parentFolderId: folder.id, params, })); } return foldersArray; }, async moveMessage({ userId, messageId, data = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}/move`) .post(data); }, async updateMessage({ userId, messageId, data = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}`) .patch(data); }, async getAttachment({ userId, messageId, attachmentId, params = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}/attachments/${attachmentId}/$value`) .responseType("stream") .query(pickBy(params)) .get(); }, async getAttachmentInfo({ userId, messageId, attachmentId, params = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}/attachments/${attachmentId}`) .query(pickBy(params)) .get(); }, async listAttachments({ userId, messageId, params = {}, } = {}) { return await this.client().api(`${this._userPath(userId)}/messages/${messageId}/attachments`) .query(pickBy(params)) .get(); }, async listUsers({ params = {} } = {}) { return await this.client().api("/users") .query(pickBy(params)) .get(); }, async listSharedFolders({ userId, parentFolderId, params = {}, } = {}) { const { value } = await this.client().api(`/users/${userId}/mailFolders${parentFolderId ? `/${parentFolderId}/childFolders` : ""}`) .query(pickBy(params)) .get(); const foldersArray = []; for (const folder of value) { foldersArray.push(folder); const { // eslint-disable-next-line no-unused-vars $skip, $top, ...childParams } = params; foldersArray.push(...await this.listSharedFolders({ userId, parentFolderId: folder.id, params: childParams, })); } return foldersArray; }, async listSharedFoldersPaged({ userId, params = {}, nextLink, } = {}) { if (nextLink) { return await this.client().api(nextLink) .get(); } return await this.client().api(`/users/${userId}/mailFolders`) .query(pickBy(params)) .get(); }, async getFolderById({ folderId, params = {}, } = {}) { return this.client().api(`/me/mailFolders/${folderId}`) .query(pickBy(params)) .get(); }, async getSharedFolderById({ userId, folderId, params = {}, } = {}) { return this.client().api(`/users/${userId}/mailFolders/${folderId}`) .query(pickBy(params)) .get(); }, async *paginate({ fn, args = {}, max, meta, }) { const limit = DEFAULT_LIMIT; args = { ...args, params: { ...args?.params, $top: limit, $skip: 0, }, }; let hasMore = true; let count = 0; let nextLink; let usedNextLink = false; do { const response = await fn({ ...args, ...(nextLink ? { nextLink, params: undefined, } : {}), }); if (meta && response?.["@odata.count"] !== undefined) { meta["@odata.count"] = response["@odata.count"]; } const { value } = response; for (const item of value) { yield item; if (max && ++count >= max) { return; } } nextLink = response?.["@odata.nextLink"]; if (nextLink) { hasMore = true; usedNextLink = true; continue; } if (!usedNextLink && args?.params && typeof args.params.$skip === "number") { hasMore = value?.length === limit; if (hasMore) { args.params.$skip += limit; } } else { hasMore = false; } } while (hasMore); }, }, };