Implementing Gmail Email Attachment Download feature with NestJS and TypeORM

Abdullah Irfan
4 min readNov 25, 2023

--

Image created by Bing

This is tenth story of series Building a Robust Backend: A Comprehensive Guide Using NestJS, TypeORM, and Microservices. Our purpose is to build an email sync system for Gmail, oAuth2 for email accounts set, we can proceed with introducing features in our application. Today we are going to write API endpoint to get/download email attachment using URL we received in emails body.

A typical email object in an array includes an attachments key. This key may contain an empty array or an array of objects with attachment-related information in key-value pairs. The keys within each attachment object are url, filename, and mimeType. An example of such an email object is as follows:

{
"id": "a6745278-d9be-4d53-afbc-d4c14a1c4dc1",
"account_id": "84cee8cc-366a-44fc-aca3-b3b4275d6f12",
"subject": "Sample Mail",
"from": "Abdullah Irfan <abdullahirfan99@gmail.com>",
"to": "abdullahirfandev@gmail.com",
"cc": null,
"bcc": null,
"date": "2023-11-25T13:02:16.000Z",
"body": "",
"attachments": [
{
"url": "https://www.googleapis.com/gmail/v1/users/me/messages/18c069308cf2352e/attachments/ANGjdJ8ft0wM6Hhj7IMXI_KP43OaAf39ghardLhJnR5aY7VMWUgfY2e0oDahssXZTdy3ZZyWyJQ6I_u_sNR4x4_YUXNIT7p_5XrHGk0mxg4SXnMtdk6LZm5o1WcF1aOGMFNBmmx3I-oBe0pbyAJiw4c5spo_pHCITkFJc3pqG4K9KXjmgVnCfENMLnRhox3g5nZf-CgIVEvheIXZsRmNTuRB6KAe0nRYqFN_j5OrCeH-wCxXzEZd8l4o6huEYufDnE40NZqppS5QXXpkLmd4-PJu_UgEFrTnI7zyOiBPO8dDKqLDbR5Lk9D_IyWS6Sc3RvfCfH4bFxNg5604MW_Og05xzebtiVp14k2j6YsmQyKNTuJOHpHGViNzO5onIwd4bQV1an0fKgmhbF0T0Jhg",
"filename": "final_ce0b6e97-7aad-4c67-9250-f9c430ab4af8.jpeg",
"mimeType": "image/jpeg"
}
],
"label_ids": [
"UNREAD",
"IMPORTANT",
"CATEGORY_PERSONAL",
"INBOX"
],
"thread_id": "18c069308cf2352e"
}

Unlike in last few stories where GET requests were being used to get the data, here we will need to use POST request with body containing url, filename and mimeType data. To handle this, we will define new DTO, download-attachment.dto.ts with following code:

export class DownloadAttachmentDto {
url: string;
filename: string;
mimeType: string;
}

To download the attachment, we need these three pieces of information: the original source name, type, and an access token. Let’s begin by writing a method for this. To download the file, we need an access token, which we can obtain through the getToken method. We will then set the headers with authorization and user agent and make the GET request.

In the response from the API, we will receive base64 encoded data containing URL-safe characters. To retrieve the original response, we will replace - with + and _ with /. We will then set the response headers to Content-Disposition with the original file name to enable downloading. Finally, we will buffer the data from base64 and send it in the body. The code for this will be as follows:

  public async downloadAttachment(
accountId: string,
downloadAttachmentDto: DownloadAttachmentDto,
response: Response,
): Promise<ResponseMessageInterface | void> {
try {
const token = await this.validToken(accountId);
if (!token) {
return customMessage(HttpStatus.UNAUTHORIZED, MESSAGE.UNAUTHORIZED);
}
const headers = {
Authorization: `Bearer ${token.access_token}`,
'User-Agent': 'medium-nestjs',
};

const fetchResponse = await fetch(downloadAttachmentDto.url, {
method: 'GET',
headers: headers,
});

if (!fetchResponse.ok) {
return customMessage(HttpStatus.BAD_REQUEST, MESSAGE.BAD_REQUEST);
}

response.setHeader(
'Content-Disposition',
`attachment; filename="${downloadAttachmentDto.filename}"`,
);
response.setHeader(
'Content-Type',
downloadAttachmentDto.mimeType || 'application/octet-stream',
);

let responseObj = '';
const transformStream = new Transform({
transform(chunk, encoding, callback) {
const data = chunk.toString().replace(/-/g, '+').replace(/_/g, '/');
responseObj += data;
callback();
},
});

fetchResponse.body.pipe(transformStream).on('finish', () => {
response.setHeader(
'Content-Disposition',
`attachment; filename="${downloadAttachmentDto.filename}"`,
);
response.setHeader(
'Content-Type',
downloadAttachmentDto.mimeType || 'application/octet-stream',
);
const base64Data = JSON.parse(responseObj).data;
response.send(Buffer.from(base64Data, 'base64'));
});
} catch (error) {
console.error('Error downloading the attachment:', error);
return customMessage(HttpStatus.BAD_REQUEST, MESSAGE.BAD_REQUEST);
}
}

Finally, to call this service, we will define an API endpoint to retrieve the email object. In the controller, we will define a route named download-attachment which will include account_id as a request parameter and response object since in case of success we will be directly sending attachment data. The code for this controller method is as follows:

@Post('download-attachment/:account_id')
async downloadAttachment(
@Param('account_id') accountId: string,
@Body() downloadAttachmentDto: DownloadAttachmentDto,
@Res() response: Response,
) {
return this.gmailAccountService.downloadAttachment(
accountId,
downloadAttachmentDto,
response,
);
}

When we will make an API call from Postman, the attachment will be received in the body. We can also verify if it’s downloadable by clicking the drop-down next to the Send button and selecting Send and Download, as shown in the image.

API returning image in Body as response.

We now have a fully operational system for downloading attachments. In the next story, we will conclude our series on Gmail service-related methods by defining the final method for sending an email with an attachment. As usual, the code for this story is available on GitHub in the feature/attachment-download branch. If you appreciate this work, please show your support by clapping for the story and starring the repository.

--

--