The problem:

At my company Conio, we have a custom job application page in our site. This page allow users to submit a regular application. In the form we ask also CV and Cover letter. Since the site is a Next JS project, the form will call a POST api which is handled by Next Routing. If you want to send a simple html/text email, code is so simple, but if you want to add files, you need to build a raw template and it is very complicated when you need to mix --NextPart with Content-Transfer-Encoding and Content-Disposition.


The solution (my solution):

Since I have multiple place in my company site where I need to build emails (not only job application page), I decided to develop an utility class which give me appendFile() method to call sequentially so I can add more than 1 file.

export class RawMessageDataBuilder {
    private msg = "";

    constructor({
        to,
        fullname,
    }: {
        fullname: string;
        to: string[];
    }) {
        this.msg = `From: <noreply@yourdomain.com>
To: ${to.join(",")}
Subject: Job Application - ${fullname}
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="NextPart"

--NextPart
Content-Type: text/html; charset=us-ascii
<body>
<p>Thank for application ${fullname}</p>
</body>
`;
    }

    appendFile(base64String: string, fileName: string, mimeType: string) {
        this.msg += `--NextPart
Content-Type: ${mimeType}; 
Content-Disposition: attachment; filename="${fileName}"
Content-Transfer-Encoding: base64
${base64String}
`;
    }

    build() {
        if (!this.msg.endsWith("--NextPart--")) {
            this.msg += `--NextPart--`;
        }
        return this.msg;
    }
}

Before I described scenario where we have a page with form and user can add files. How to handle files in form with Next.JS api is out of scope, because it add complexity. The following code show a more simple use case where maybe the company send a job_details.pdf file to user.

import { SendRawEmailCommand, SendRawEmailCommandInput, SESClient } from "@aws-sdk/client-ses";
import { RawMessageDataBuilder } from "components/jobs/utils";
import { readFileSync } from "fs";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
    const jobDetailPdf = readFileSync("job_details.pdf"); // return a buffer
    const fileBase64Cv = jobDetailPdf.toString("base64");
    const builder = new RawMessageDataBuilder({
        fullname: "Mario Rossi",
        to: ["mario.rossi@gmail.com"],
    });
    builder.appendFile(fileBase64Cv, "job_details.pdf", "application/pdf");
    const params: SendRawEmailCommandInput = {
        RawMessage: {
            Data: Buffer.from(builder.build(), "utf8"),
        },
        Source: "noreply@yourdomain.com" /* required */,
    };

    const command = new SendRawEmailCommand(params);
    const sesClient = new SESClient({
        region: "eu-west-1",
        credentials: {
            accessKeyId: "xxx",
            secretAccessKey: "yyy",
        },
    });

    try {
        await sesClient.send(command);
        return new Response("Success", {
            status: 200,
        });
    } catch {
        return NextResponse.json({ error: "error sending email" }, { status: 400 });
    }
}

Line 17:18 show how to use SESClient to send a raw email.