Skip to content

Commit

Permalink
Merge pull request #30 from NangoHQ/fix/gmail-attachment
Browse files Browse the repository at this point in the history
fix(gmail): add types, support text/plain attachment
  • Loading branch information
bodinsamuel authored Oct 2, 2024
2 parents 1f9ac28 + 9ab9fa1 commit 4806269
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 279 deletions.
2 changes: 1 addition & 1 deletion flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1933,7 +1933,7 @@ integrations:
but metadata can be set using the `backfillPeriodMs` property
to change the lookback. The property should be set in milliseconds.
input: OptionalBackfillSetting
version: 1.0.0
version: 1.0.1
output: GmailEmail
sync_type: incremental
endpoint: GET /google-mail/emails
Expand Down
2 changes: 1 addition & 1 deletion integrations/google-mail/nango.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ integrations:
but metadata can be set using the `backfillPeriodMs` property
to change the lookback. The property should be set in milliseconds.
input: OptionalBackfillSetting
version: 1.0.0
version: 1.0.1
output: GmailEmail
sync_type: incremental
endpoint: GET /google-mail/emails
Expand Down
27 changes: 15 additions & 12 deletions integrations/google-mail/syncs/emails.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NangoSync, GmailEmail, OptionalBackfillSetting } from '../../models';
import type { Schema$Message } from '../types';

// 1 year ago
const DEFAULT_BACKFILL_MS = 365 * 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -29,20 +30,20 @@ export default async function fetchData(nango: NangoSync) {
const emails: GmailEmail[] = [];

for (const message of messageList) {
const messageDetail = await nango.proxy({
const messageDetail = await nango.proxy<Schema$Message>({
method: 'GET',
endpoint: `/gmail/v1/users/me/messages/${message.id}`,
retries: 10
});

const headers = messageDetail.data.payload.headers.reduce((acc: any, current: any) => {
const headers: Record<string, any> = messageDetail.data.payload?.headers?.reduce((acc: any, current: any) => {
return {
...acc,
[current.name]: current.value
};
}, {});

emails.push(mapEmail(messageDetail, headers));
emails.push(mapEmail(messageDetail.data, headers));
}

await nango.batchSave(emails, 'GmailEmail');
Expand All @@ -51,22 +52,24 @@ export default async function fetchData(nango: NangoSync) {
} while (nextPageToken);
}

function mapEmail(messageDetail: any, headers: any): GmailEmail {
const parts = messageDetail.data.payload.parts || [];
function mapEmail(messageDetail: Schema$Message, headers: Record<string, any>): GmailEmail {
const parts = messageDetail.payload?.parts || [];
let body = '';
for (const part of parts) {
if (part.mimeType === 'text/plain') {
if (part.mimeType === 'text/plain' && part.body?.data) {
// Body can be empty if the part is an attachment
// https://developers.google.com/gmail/api/reference/rest/v1/users.messages.attachments#MessagePartBody
body = Buffer.from(part.body.data, 'base64').toString('utf8');
break;
}
}
return {
id: messageDetail.data.id,
sender: headers.From,
recipients: headers.To,
date: new Date(parseInt(messageDetail.data.internalDate)),
subject: headers.Subject,
id: messageDetail.id,
sender: headers['From'],
recipients: headers['To'],
date: new Date(parseInt(messageDetail.internalDate)),
subject: headers['Subject'],
body: body,
threadId: messageDetail.data.threadId
threadId: messageDetail.threadId
};
}
104 changes: 104 additions & 0 deletions integrations/google-mail/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// All types copied from
// https://github.com/googleapis/google-api-nodejs-client/blob/main/src/apis/gmail/v1.ts
// NB: they set `? | null` for all fields which is not true

/**
* An email message.
*/
export interface Schema$Message {
/**
* The ID of the last history record that modified this message.
*/
historyId?: string | null;
/**
* The immutable ID of the message.
*/
id: string;
/**
* The internal message creation timestamp (epoch ms), which determines ordering in the inbox. For normal SMTP-received email, this represents the time the message was originally accepted by Google, which is more reliable than the `Date` header. However, for API-migrated mail, it can be configured by client to be based on the `Date` header.
*/
internalDate: string;
/**
* List of IDs of labels applied to this message.
*/
labelIds?: string[] | null;
/**
* The parsed email structure in the message parts.
*/
payload?: Schema$MessagePart;
/**
* The entire email message in an RFC 2822 formatted and base64url encoded string. Returned in `messages.get` and `drafts.get` responses when the `format=RAW` parameter is supplied.
*/
raw?: string | null;
/**
* Estimated size in bytes of the message.
*/
sizeEstimate?: number | null;
/**
* A short part of the message text.
*/
snippet?: string | null;
/**
* The ID of the thread the message belongs to. To add a message or draft to a thread, the following criteria must be met: 1. The requested `threadId` must be specified on the `Message` or `Draft.Message` you supply with your request. 2. The `References` and `In-Reply-To` headers must be set in compliance with the [RFC 2822](https://tools.ietf.org/html/rfc2822) standard. 3. The `Subject` headers must match.
*/
threadId: string;
}

/**
* A single MIME message part.
*/
export interface Schema$MessagePart {
/**
* The message part body for this part, which may be empty for container MIME message parts.
*/
body?: Schema$MessagePartBody;
/**
* The filename of the attachment. Only present if this message part represents an attachment.
*/
filename?: string | null;
/**
* List of headers on this message part. For the top-level message part, representing the entire message payload, it will contain the standard RFC 2822 email headers such as `To`, `From`, and `Subject`.
*/
headers?: Schema$MessagePartHeader[];
/**
* The MIME type of the message part.
*/
mimeType?: string | null;
/**
* The immutable ID of the message part.
*/
partId?: string | null;
/**
* The child MIME message parts of this part. This only applies to container MIME message parts, for example `multipart/x`. For non- container MIME message part types, such as `text/plain`, this field is empty. For more information, see RFC 1521.
*/
parts?: Schema$MessagePart[];
}

/**
* The body of a single MIME message part.
*/
export interface Schema$MessagePartBody {
/**
* When present, contains the ID of an external attachment that can be retrieved in a separate `messages.attachments.get` request. When not present, the entire content of the message part body is contained in the data field.
*/
attachmentId?: string | null;
/**
* The body data of a MIME message part as a base64url encoded string. May be empty for MIME container types that have no message body or when the body data is sent as a separate attachment. An attachment ID is present if the body data is contained in a separate attachment.
*/
data?: string | null;
/**
* Number of bytes for the message part data (encoding notwithstanding).
*/
size?: number | null;
}

export interface Schema$MessagePartHeader {
/**
* The name of the header before the `:` separator. For example, `To`.
*/
name?: string | null;
/**
* The value of the header after the `:` separator. For example, `someuser@example.com`.
*/
value?: string | null;
}
Loading

0 comments on commit 4806269

Please sign in to comment.