Implementing the Send Email Feature with Attachments feature using NestJS and TypeORM

Abdullah Irfan
6 min readNov 26, 2023

--

Image created by Bing

This is eleventh 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, with most of the features completed related to Gmail services, today we will wind up by implementing send email feature.

For sending the email with attachment support, we will be using Multer, further about can be read here. We will define DTO to accept emails of to (required), cc, bcc, subject, body and attachment as files variable (these five are optional). Since Multer is directly incorporated at controller level, we will set our DTO as:

import { IsEmail, IsOptional, IsString } from 'class-validator';

export class SendEmailDto {
@IsEmail()
to: string;

@IsOptional()
@IsEmail()
cc: string;

@IsOptional()
@IsEmail()
bcc: string;

@IsString()
@IsOptional()
subject: string;

@IsString()
@IsOptional()
body: string;
}

For the controller, a POST request will be used for the base route, where the account ID is set as a parameter, and Multer will be used to accept incoming objects. In attachments, there is a limit of 10 files with a maximum cumulative size of 25 MB. When these conditions are met, the controller will call the service function. The code for the controller method is as follows:

  @Post(':account_id')
@UseInterceptors(FileFieldsInterceptor([{ name: 'files', maxCount: 10 }]))
async sendMail(
@Param('account_id') account_id: string,
@Body() sendEmailDto: SendEmailDto,
@UploadedFiles() files: { files?: Express.Multer.File[] },
@Res() response: Response,
) {
// Check the accumulative size of the files
const totalSize = (files.files || []).reduce(
(sum, file) => sum + file.size,
0,
);
if (totalSize > 25 * 1024 * 1024) {
// 25 MB in bytes
throw new BadRequestException(
customMessage(
HttpStatus.BAD_REQUEST,
MESSAGE.FILE_SIZE_EXCEPTION_MESSAGE,
),
);
}

// If the size is within limits, proceed with the service call
const emailAttachments: Express.Multer.File[] = files?.files || [];
// Manuallay handle success response
response
.status(HttpStatus.OK)
.send(
await this.gmailAccountService.sendEmail(
account_id,
sendEmailDto,
emailAttachments,
),
);
}

In services we will utilize multiple functions, explanation of each is given with respective code below:

The sendEmail function will be utilized to send an email with specific parameters and attachments. When this function is called, it first retrieves a token using the given ID. If the token is valid, it proceeds to get the user's details and formats the from address. Using the OAuth2 client, it sets up the Gmail API client and creates a raw email with the provided parameters and attachments. The email is then sent, and the function returns the response from the API. This process ensures that the email is sent only if the user is authenticated, and the required parameters are met.

  async sendEmail(
id: string,
reqBody: SendEmailDto,
attachments: Express.Multer.File[],
): Promise<ResponseMessageInterface> {
const token = await this.getToken(id);
if (!token) {
return customMessage(HttpStatus.UNAUTHORIZED, MESSAGE.UNAUTHORIZED);
}
const user = await this.getUser(id);
const from = this.formatSender(user);

const oAuth2Client = getOAuthClient(token);
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });

const rawEmail = this.createRawEmail(
from,
reqBody.to,
attachments,
reqBody.subject,
reqBody.body,
reqBody.cc,
reqBody.bcc,
);

return customMessage(
HttpStatus.OK,
MESSAGE.SUCCESS,
await this.sendRawEmail(gmail, rawEmail),
);
}

The getUser function's purpose is to fetch user details from the repository using the provided ID.

  private async getUser(id: string): Promise<GmailAccounts> {
const user = await this.gmailAccountRepository.findOneBy({ id });
if (!user) throw new Error(MESSAGE.USER_NOT_FOUND);
return user;
}

The formatSender function is designed to format the from field of an email. It takes a Gmail user entity as input and returns a string that combines the user's full name and email address in a standard email format.

  private formatSender(user: GmailAccounts): string {
return `${user.full_name} <${user.email}>`;
}

The sendRawEmail function is responsible for sending the actual email using the Gmail API. It takes the Gmail API client and the raw email data in base64 format as inputs. The function attempts to send the email through the Gmail API and returns the response data.

  private async sendRawEmail(
gmail: gmail_v1.Gmail,
rawEmail: string,
): Promise<gmail_v1.Schema$Message> {
try {
const response = await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: rawEmail,
},
});
return response.data;
} catch (error) {
console.error('Error sending email: ' + error.message);
throw new Error(MESSAGE.BAD_REQUEST);
}
}

The createRawEmail function prepares a raw email message for sending via the Gmail API. It constructs an email by combining various components like the sender's address, recipient(s), subject, body, attachments, and optional CC/BCC recipients. The function then encodes the subject line in UTF-8 and formats the entire email into a raw string in base64 format. This function is essential for assembling all parts of an email into a format that is suitable for sending through the Gmail API.

  private createRawEmail(
from: string,
to: string | string[],
attachments: Express.Multer.File[],
subject?: string,
body?: string,
cc?: string | string[],
bcc?: string | string[],
): string {
const utf8Subject = this.encodeSubject(subject);
const messageParts = [
this.formatHeader(from, to, utf8Subject),
...this.addCcBcc(cc, bcc),
'--foo_bar_baz',
'Content-Type: text/html; charset=utf-8',
'MIME-Version: 1.0',
'',
body,
...this.addAttachments(attachments),
'--foo_bar_baz--',
];

return this.encodeEmail(messageParts.join('\n'));
}

The encodeSubject function encodes the subject line of an email into UTF-8 format. This encoding is necessary to ensure that the subject line is correctly displayed across different email clients and platforms, especially when dealing with special characters or non-English text.

  private encodeSubject(subject: string): string {
return `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=`;
}

The formatHeader function is used to format the header section of the email. It takes the sender's email address, recipient(s), and the UTF-8 encoded subject line as inputs and returns a string representing the header part of the email. This function ensures that the email's header is correctly structured, including essential details like the sender, recipient, and subject.

  private formatHeader(
from: string,
to: string | string[],
utf8Subject: string,
): string {
return [
`From: ${from}`,
`To: ${Array.isArray(to) ? to.join(', ') : to}`,
'Content-Type: multipart/mixed; boundary="foo_bar_baz"',
'MIME-Version: 1.0',
`Subject: ${utf8Subject}`,
'',
].join('\n');
}

The addCcBcc function adds CC (carbon copy) and BCC (blind carbon copy) recipients to the email, if provided. It formats the CC and BCC fields appropriately and returns them as part of the email header.

  private addCcBcc(cc?: string | string[], bcc?: string | string[]): string[] {
const headers = [];
if (cc) {
headers.push(`Cc: ${Array.isArray(cc) ? cc.join(', ') : cc}`);
}
if (bcc) {
headers.push(`Bcc: ${Array.isArray(bcc) ? bcc.join(', ') : bcc}`);
}
return headers;
}

he addAttachments function handles the inclusion of file attachments in the email. It takes an array of file attachments and formats each one, adding the necessary headers and encoding the file content in base64. This function is key to attaching files to the email in a format that is compatible with email standards.

  private addAttachments(attachments: Express.Multer.File[]): string[] {
return attachments.map((attachment) => {
const encodedContent = attachment.buffer.toString('base64');
return [
'--foo_bar_baz',
`Content-Type: ${attachment.mimetype}; name="${attachment.originalname}"`,
'Content-Transfer-Encoding: base64',
`Content-Disposition: attachment; filename="${attachment.originalname}"`,
'',
encodedContent,
].join('\n');
});
}

The encodeEmail function converts the entire email body, including headers, body, and attachments, into base64 format. This conversion is necessary for the email content to be sent via the Gmail API. The function also ensures that the base64 string is URL-safe by replacing certain characters and removing padding characters at the end. This final encoding step is crucial for the successful transmission of the email through the API.

  private encodeEmail(emailBody: string): string {
return Buffer.from(emailBody)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

We now have a fully operational Gmail sync system with email synchronization and sending email feature. From next stories, we will be focusing on adding/improving Nest related features and next story will be on building authentication system. As usual, the code for this story is available on GitHub in the feature/send-mail branch. If you appreciate this work, please show your support by clapping for the story and starring the repository.

--

--