Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gmail (major) Additional fixes and improvements #302

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"name": "appmixer.google.gmail.GetAttachment",
"name": "appmixer.google.gmail.DownloadAttachment",
"author": "Appmixer <[email protected]>",
"description": "Download message attachment content.",
"description": "Download an email attachment.",
"private": false,
"version": "1.0.0",
"auth": {
"service": "appmixer:google:gmail",
"scope": ["https://www.googleapis.com/auth/gmail.readonly"]
Expand Down Expand Up @@ -32,40 +31,27 @@
"type": "string"
}
},
"required": ["messageId", "attachmentId", "fileName"]
"required": ["messageId", "attachmentId"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileName is not reuquired, however flow throws the missing file name param if it's not provided
image

},
"inspector": {
"inputs": {
"messageId": {
"type": "text",
"index": 1,
"label": "Email Message",
"label": "Email Message ID",
"tooltip": "Enter the email message ID to retrieve attachments from."
},
"attachmentId": {
"when": { "regex": { "messageId": ".+" }},
"type": "text",
"index": 2,
"label": "Attachment",
"tooltip": "The attachment you want to retrieve",
"source": {
"url": "/component/appmixer/google/gmail/ListAttachments?outPort=out",
"data": {
"messages": {
"in/messageId": "inputs/in/messageId"
},
"properties": {
"sendWholeArray": true
},
"transform": "./ListAttachments#attachmentsToSelectArray"
}
}
"label": "Attachment ID",
"tooltip": "The ID of the attachment you want to retrieve."
},
"fileName": {
"type": "text",
"index": 3,
"label": "File Name",
"tooltip": "Name of the attachment with extension. Example: `image.png`"
"label": "Custom File Name",
"tooltip": "Optionally specify the file name, including the extension, for downloading the attachment. Example: image.png"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/appmixer/google/gmail/FindEmails/component.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"type": "text",
"label": "Search Query",
"index": 1,
"tooltip": "The search query to find emails."
"tooltip": "The search query to find emails. See <a target=_blank href=\"https://support.google.com/mail/answer/7190\">Google Documentation</a> for more info. Example: <code>from:[email protected] AND subject:dinner</code>."
},
"outputType": {
"type": "select",
Expand Down
2 changes: 1 addition & 1 deletion src/appmixer/google/gmail/ListAttachments/component.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"messageId": {
"type": "text",
"index": 1,
"label": "Email",
"label": "Email Message ID",
"tooltip": "The email message ID to retrieve attachments from."
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/appmixer/google/gmail/MoveEmail/component.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "appmixer.google.gmail.MoveEmail",
"author": "Appmixer <[email protected]>",
"description": "Moves a email to another folder.",
"description": "Moves an email to another folder.",
"private": false,
"version": "1.0.1",
"auth": {
Expand Down Expand Up @@ -43,9 +43,9 @@
"tooltip": "Insert an email message ID to move."
},
"folder": {
"type": "text",
"type": "select",
"label": "Folder",
"tooltip": "Select a name of folder where you want to move the email from.",
"tooltip": "Select a name of the folder where you want to move the email from.",
"index": 2,
"source": {
"url": "/component/appmixer/google/gmail/ListLabels?outPort=out",
Expand Down
122 changes: 40 additions & 82 deletions src/appmixer/google/gmail/NewAttachment/NewAttachment.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,57 @@
'use strict';

const emailCommons = require('../gmail-commons');
const Promise = require('bluebird');

module.exports = {
async tick(context) {
let newState = {};

const { labels: { AND: labels } = { AND: [] } } = context.properties;
const isLabelsEmpty = !labels.some(label => label.name);

// Fetch new messages from the inbox
const data = await emailCommons.listNewMessages(
{ context, userId: 'me' },
context.state.id || null
);

// Update the state with the latest message ID
newState.id = data.lastMessageId;

// Fetch the full email data for new messages
const emails = await Promise.map(data.newMessages, async message => {
return emailCommons.callEndpoint(context, `/users/me/messages/${message.id}`, {
method: 'GET',
params: { format: 'full' }
}).then(response => response.data).catch(err => {
// email can be deleted (permanently) in gmail between listNewMessages call and
// this getMessage call, in such case - ignore it and return null
if (err && err.response && err.response.status === 404) {
return null;
}
throw err;
});
}, { concurrency: 10 });
async tick(context) {

// Extract attachments from emails
let attachments = await Promise.map(emails, email => {
if (!email || !email.labelIds) {
// Skip if the email was deleted or labelIds is missing
return [];
const { download } = context.properties;
const state = context.state;
let query = context.properties.query;
query = (query ? query + ' AND ' : '') + 'has:attachment';
const { emails, state: newState } = await emailCommons.listNewMessages(context, query, state);

// Fetch attachments from emails.
const output = [];
await Promise.map(emails, async (email) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"message": "Promise.map is not a function"

image

if (!email) {
// Skip if the email was deleted.
return;
}

// Filter emails based on selected labels
if (isLabelsEmpty || labels.some(label => email.labelIds.includes(label.name))) {
return downloadAttachments(context, email);
if (!emailCommons.isNewInboxEmail(email.labelIds || [])) {
// Skip SENT and DRAFT emails.
return;
}
return [];
});

// Flatten the array of attachments
attachments = attachments.reduce((a, b) => a.concat(b), []);

// Save attachments and send them to the output port
let saved = await Promise.map(attachments, attachment => {
const buffer = Buffer.from(attachment.data, 'base64');
return context.saveFileStream(
attachment.filename,
buffer
).then(res => {
return Object.assign(res, { subject: attachment.subject });
return Promise.map(email.attachments || [], async (attachment) => {
const out = {
email,
attachment
};
if (download) {
const savedFile = await downloadAttachment(context, email.id, attachment.id, attachment.filename);
out.fileId = savedFile.fileId;
out.filename = savedFile.filename;
out.contentType = savedFile.contentType;
}
output.push(out);
});
});

await Promise.map(saved, savedFile => {
return context.sendJson(savedFile, 'attachment');
});

await context.saveState(newState);
await context.sendArray(output, 'attachment');
if (JSON.stringify(state != JSON.stringify(newState))) {
return context.saveState(newState);
}
}
};

/**
* Download attachments from an email.
* @param {Context} context
* @param {Object} email
* @return {Array<Object>} returns array with attachments
*/
let downloadAttachments = async (context, email) => {
if (!emailCommons.isNewInboxEmail(email.labelIds)) {
return []; // skip SENT and DRAFT emails
}

// Parse the email content to extract attachments
const parsedEmail = emailCommons.normalizeEmail(email);

return Promise.map(parsedEmail.attachments || [], attachment => {
return emailCommons.callEndpoint(context, `/users/me/messages/${email.id}/attachments/${attachment.id}`, {
method: 'GET'
}).then(response => {
return {
filename: attachment.filename,
mimetype: attachment.mimeType || 'application/octet-stream', // Ensure mimetype is set
size: attachment.size,
data: response.data.data,
subject: parsedEmail.payload.subject
};
});
const downloadAttachment = async (context, emailId, attachmentId, filename) => {
const response = await emailCommons.callEndpoint(context, `/users/me/messages/${emailId}/attachments/${attachmentId}`, {
method: 'GET'
});
const base64 = response.data.data;
const buffer = Buffer.from(base64, 'base64');
const savedFile = await context.saveFileStream(filename, buffer);
return savedFile;
};
124 changes: 93 additions & 31 deletions src/appmixer/google/gmail/NewAttachment/component.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,24 @@
"properties": {
"schema": {
"properties": {
"labels": {
"type": "object"
}
"query": { "type": "string" },
"download": { "type": "boolean" }
}
},
"inspector": {
"inputs": {
"labels": {
"type": "expression",
"label": "Labels",
"levels": [
"AND"
],
"query": {
"type": "text",
"label": "Email Query",
"index": 1,
"tooltip": "Select one or more labels to filter the emails that contain attachments. The trigger will activate if an attachment is received in an email that matches any of the selected labels. If no labels are selected, the trigger will activate for all attachments in all received emails.",
"fields": {
"name": {
"type": "select",
"label": "Name",
"index": 1,
"tooltip": "Select a name of the existing label.",
"source": {
"url": "/component/appmixer/google/gmail/ListLabels?outPort=out",
"data": {
"properties": {
"sendWholeArray": true
},
"transform": "./ListLabels#labelsToSelectArrayFiltered"
}
}
}
}
"tooltip": "The search query to find new emails. This allows you to only consider email messages that can be found using the query. See <a target=_blank href=\"https://support.google.com/mail/answer/7190\">Google Documentation</a> for more info. Example: <code>from:[email protected] AND subject:dinner</code>."
},
"download": {
"type": "toggle",
"label": "Download Attachment",
"defaultValue": true,
"index": 2,
"tooltip": "Set if you want to download the attachment content. Otherwise, set to false and use the Download Attachment component to get the attachment content. This can be useful if you want to filter attachments using the Condition utility based on custom criteria (e.g. the 'From' address of the sender) and only download attachments passing the filter."
}
}
}
Expand All @@ -61,10 +47,86 @@
{
"name": "attachment",
"options": [
{ "label": "File ID", "value": "fileId" },
{ "label": "File Name", "value": "filename" },
{ "label": "Content Type", "value": "contentType" },
{ "label": "Subject", "value": "subject" }
{ "label": "File ID", "value": "fileId", "schema": { "type": "string", "format": "appmixer-file-id" } },
{ "label": "File Name", "value": "filename", "schema": { "type": "string" } },
{ "label": "Content Type", "value": "contentType", "schema": { "type": "string" } },
{ "label": "Attachment", "value": "attachment", "schema": {
"type": "object",
"properties": {
"id": { "type": "string", "title": "Attachment ID" },
"filename": { "type": "string", "title": "Attachment File Name" },
"size": { "type": "integer", "title":"Attachment File Size" },
"mimeType": { "type": "string", "title": "Attachment MIME Type" }
}
} },
{ "label": "Email", "value": "email",
"schema": {
"type": "object",
"properties": {
"id": { "type": "string", "title": "Email ID" },
"threadId": { "type": "string", "title": "Email Thread ID" },
"labelIds": { "type": "array", "items": { "type": "string" }, "title": "Email Label IDs" },
"snippet": { "type": "string", "title": "Email Snippet" },
"sizeEstimate": { "type": "integer", "title": "Email Size Estimate" },
"payload": {
"type": "object",
"title": "Email Payload",
"properties": {
"date": { "type": "string", "format": "date-time", "title": "Email Date" },
"subject": { "type": "string", "title": "Email Subject" },
"text": { "type": "string", "title": "Email Text" },
"html": { "type": "string", "title": "Email HTML" },
"from": {
"type": "array",
"title": "Email Senders",
"items": {
"type": "object",
"properties": {
"address": { "type": "string" },
"name": { "type": "string" }
}
}
},
"to": {
"type": "array",
"title": "Email Recipients",
"items": {
"type": "object",
"properties": {
"address": { "type": "string" },
"name": { "type": "string" }
}
}
}
}
},
"attachments": {
"type": "array",
"title": "Email Attachments",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"title": "Attachment ID"
},
"filename": {
"type": "string",
"title": "Attachment Name"
},
"size": {
"type": "string",
"title": "Attachment Size"
},
"mimeType": {
"type": "string",
"title": "Attachment MIME Type"
}
}
}
}
}
} }
]
}
],
Expand Down
Loading
Loading