Skip to content

Commit

Permalink
NewAttachment, NewEmail: improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidDurman committed Jan 8, 2025
1 parent 6f5c3b3 commit e52caea
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 260 deletions.
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
136 changes: 39 additions & 97 deletions src/appmixer/google/gmail/NewAttachment/NewAttachment.js
Original file line number Diff line number Diff line change
@@ -1,116 +1,58 @@
'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) => {
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;

Check failure on line 25 in src/appmixer/google/gmail/NewAttachment/NewAttachment.js

View workflow job for this annotation

GitHub Actions / build

Trailing spaces not allowed
}
return [];
});

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

// Save attachments and send them to the output port
let attachmentsOutput;

if (context.properties.download) {
attachmentsOutput = await Promise.map(attachments, attachment => {
const buffer = Buffer.from(attachment.data, 'base64');
return context.saveFileStream(
attachment.filename,
buffer
).then(res => {
return Object.assign(res, {
email: attachment.email,
attachment
});
});
});
} else {
attachmentsOutput = attachments.map(attachment => {
return Object.assign(attachment, {
email: attachment.email,
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(attachmentsOutput, out => {
return context.sendJson(out, '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
*/
const 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 || [], async (attachment) => {
const out = {
filename: attachment.filename,
mimetype: attachment.mimeType || 'application/octet-stream', // Ensure mimetype is set
size: attachment.size,
email: parsedEmail,
attachment: attachment
};
if (context.properties.download) {
const response = await emailCommons.callEndpoint(context, `/users/me/messages/${email.id}/attachments/${attachment.id}`, {
method: 'GET'
});
out.data = response.data.data;
}
return out;
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;
};
66 changes: 25 additions & 41 deletions src/appmixer/google/gmail/NewAttachment/component.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,17 @@
"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",
Expand All @@ -67,36 +47,38 @@
{
"name": "attachment",
"options": [
{ "label": "File ID", "value": "fileId" },
{ "label": "File Name", "value": "filename" },
{ "label": "Content Type", "value": "contentType" },
{ "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" },
"filename": { "type": "string" },
"size": { "type": "integer" },
"mimeType": { "type": "string" }
"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" },
"threadId": { "type": "string" },
"labelIds": { "type": "array", "items": { "type": "string" } },
"snippet": { "type": "string" },
"sizeEstimate": { "type": "integer" },
"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" },
"subject": { "type": "string" },
"text": { "type": "string" },
"html": { "type": "string" },
"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": {
Expand All @@ -107,6 +89,7 @@
},
"to": {
"type": "array",
"title": "Email Recipients",
"items": {
"type": "object",
"properties": {
Expand All @@ -119,6 +102,7 @@
},
"attachments": {
"type": "array",
"title": "Email Attachments",
"items": {
"type": "object",
"properties": {
Expand Down
44 changes: 8 additions & 36 deletions src/appmixer/google/gmail/NewEmail/NewEmail.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,16 @@ const emailCommons = require('../gmail-commons');
const Promise = require('bluebird');

Check failure on line 3 in src/appmixer/google/gmail/NewEmail/NewEmail.js

View workflow job for this annotation

GitHub Actions / build

'Promise' is assigned a value but never used

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

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

// Fetch all messages without sending labelIds
const data = await emailCommons.listNewMessages(
{ context, userId: 'me' },
context.state.id || null
);

newState.id = data.lastMessageId;

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 });
// Filter the emails based on selected labels, if any
await Promise.each(emails || [], async email => {
if (!email || !email.labelIds) {
throw new context.CancelError('Invalid email or email label');
}
async tick(context) {

if (isLabelsEmpty || labels.some(label => email.labelIds.includes(label.name))) {
await context.sendJson(emailCommons.normalizeEmail(email), 'out');
}
});
const { query } = context.properties;
const state = context.state;
const { emails, state: newState } = await emailCommons.listNewMessages(context, query, state);

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

0 comments on commit e52caea

Please sign in to comment.