# Building with email/password authentication
Although Gadget provides a default email/password authentication method, there is a lot of flexibility to configure or build on top of the user emails sent through `emails` in the action context.
## Custom email templates
Whether you want to customize the copy and style of the emails you send on the `sendVerifyEmail`/`sendResetPassword` action or send any other transactional/marketing emails within your app, you can easily do so.
1. Create a constant value representing the desired custom email template (`CustomTemplate`) and declare an expression containing an HTML 5 document.
2. Structure this document however you want your email copy and style to be.
3. Now to render your custom template, when sending an email (via `emails.sendMail()`) within the `html` parameter you will pass the custom template like below:
Create a constant value representing the desired custom email template (`CustomTemplate`) and declare an expression containing an HTML 5 document.
Structure this document however you want your email copy and style to be.
Now to render your custom template, when sending an email (via `emails.sendMail()`) within the `html` parameter you will pass the custom template like below:
```typescript
export const onSuccess: ActionOnSuccess = async ({
params,
record,
logger,
api,
emails,
}) => {
if (
!record.emailVerified &&
record.emailVerificationToken &&
params.user?.emailVerificationCode
) {
const url = new URL("/verify-email", Config.appUrl);
url.searchParams.append("code", params.user?.emailVerificationCode);
// create your custom email template
const CustomTemplate = `
Email Verification
`;
await emails.sendMail({
to: record.email,
subject: `Verify your email with ${Config.appName}`,
// Pass your custom template
// The default template is an EJS string
html: DefaultEmailTemplates.renderEmailTemplate(CustomTemplate, {
url: url.toString(),
}),
});
}
};
```
When passing the `CustomTemplate` through `emails.sendMail()`, note that there is no `text` parameter to pass as that is replaced by the content within your `CustomTemplate`.
### Email parameters
The `sendMail` function takes in a `MailData` object. The `MailData` object has the following params:
| Name | Type | Description |
| --- | --- | --- |
| `to` | `string`, `Address`, `string[]`, `Address[]` | The address(es) that the email will be sent to. The `to` param can be an email address or `Address` object, or an array of either. For example: `john@smith.com` or `{ address: "john@smith.com", name: "John Smith" }` |
| `from` | `string`, `Address` | The `from` address displayed to users as to who the email came from. This param is optional. If not provided, the `from` address will be set to `noreply@{your-app-slug}.gadget.app`. **Note**: When using the Gadget built-in email transport, you can only send mail from your Gadget app's domain. If your app uses a custom domain, mail will be sent from your `.gadget.app` address and the display name will be set to `noreply@{your-custom-domain}` so it appears to come from your custom domain. |
| `cc` | `string`, `Address`,`string[]`, \`Address\[\]\`\` | The `cc` address(es) that the email will be sent to. The `cc` param can be an email address or `Address` object, or an array of either. For example: `john@smith.com` or `{ address: "john@smith.com", name: "John Smith" }` |
| `bcc` | `string`, `Address`, `string[]`, `Address[]` | The `bcc` address(es) that the email will be sent to. The `bcc` param can be an email address or `Address` object, or an array of either. For example: `john@smith.com` or `{ address: "john@smith.com", name: "John Smith" }` |
| `replyTo` | `string`, `Address`,`string[]`, `Address[]` | The `replyTo` address(es) to attach to the email. The `replyTo` param can be an email address or `Address` object. For example: `john@smith.com` or `{ address: "john@smith.com", name: "John Smith" }` |
| `sender` | `string`, `Address` | The `sender` address used to identify the agent responsible for the actual transmission of the email. Can only be set when using a custom transport. |
| `html` | `string` | The HTML body of the email. |
| `text` | `string` | The plain text body of the email. |
| `attachments` | `Attachment[]` | An array of files to be attached to the email. The `attachment` is an object with either a `path` param which represents the `string` path to the file, or a `content` param which represents the `Buffer` or `string` raw data of the file, and an optional `filename` param which represents the name of the file. If a filename is not provided, the original name of the file will be used. |
| `inReplyTo` | `string` | The `inReplyTo` message ID property to set on the email. |
| `references` | `string` | The `references` message ID property to set on the email. |
| `headers` | `Record` | A list of custom headers to set on the email. |
When using Gadget's built in email transport, you can only send emails from your app's domains, like `hello@your-app-slug.gadget.app`. If you need to send emails from different domains, you must set up your own email transport.
### Adding attachments to emails with GadgetMailer
Let's say we have a model called `csvFile` with a field `link` which is a `file` type and we want to dynamically create files to store in our model. To attach a generated csv file to an email we can do the following:
```typescript
import { Base64 } from "base64-string";
import { DefaultEmailTemplates } from "gadget-server";
export const onSuccess: ActionOnSuccess = async ({ api, emails }) => {
// 1. Create CSV content
const csvContent = "name,age\njohn,32";
// 2. Encode the CSV content
const enc = new Base64();
const b64 = enc.urlEncode(csvContent);
// 3. Set the file name
const csvFileName = `test.csv`;
// 4. Create the file
const file = await api.csvFile.create({
link: {
base64: b64,
fileName: csvFileName,
},
});
const CustomTemplate = `
Email with CSV
Sending a CSV
See attachment
`;
// 5. Send the email with the attachment
await emails.sendMail({
to: "john@smith.com",
subject: `Welcome`,
html: DefaultEmailTemplates.renderEmailTemplate(CustomTemplate, {}),
attachments: [
{
filename: csvFileName,
path: file.link?.url,
},
],
});
};
```
We can also fetch the file from a URL and pass it in as an attachment:
```typescript
import pdf from "pdf-parse";
import { DefaultEmailTemplates } from "gadget-server";
const getPdfBuffer = async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (error) {
console.error("Error fetching PDF:", error);
throw error;
}
};
export const onSuccess: ActionOnSuccess = async ({ emails }) => {
// 1. Fetch the PDF file from a URL
const url = "https://example.com/example.pdf";
const pdfBuffer = await getPdfBuffer(url);
const CustomTemplate = `
Email with PDF
Sending a PDF
See attachment
`;
// 2. Send the email with the attachment
await emails.sendMail({
to: "john@smith.com",
subject: `Welcome`,
html: DefaultEmailTemplates.renderEmailTemplate(CustomTemplate, {}),
attachments: [
{
filename: "myPdf.pdf",
content: pdfBuffer,
},
],
});
};
```
## Setting up an external transporter
If you decide to use an external email transport like Amazon SES, SendGrid, or Mailgun, you can configure that within Gadget.
If the same credentials apply to every email in your app, [create a boot file](https://docs.gadget.dev/guides/http-routes/route-structure#boot-plugins) and declare the transport there. Use [environment variables](https://docs.gadget.dev/guides/development-tools/environment-variables) to store the credentials. If each tenant has its own credentials, set the transport inside the action just before you send the email.
The parameters passed to `setTransport()` depend on the [type of transport](https://nodemailer.com/transports/) you want to use. Refer to the Nodemailer or external transport service documentation for the exact configuration fields.
For example, if each Shopify shop stores its own SMTP credentials in an `emailSettings` model linked to `shopifyShop`, you can look up the current shop with `connections.shopify.currentShopId`, load that shop's settings, and configure the transport for that request:
```typescript
export const run: ActionRun = async ({ api, connections, emails }) => {
const shopId = connections.shopify.currentShopId;
if (!shopId) {
throw new Error("This action must run in the context of a Shopify shop");
}
const emailSettings = await api.emailSettings.findFirst({
filter: {
shopId: { equals: shopId },
},
select: {
smtpHost: true,
smtpPort: true,
smtpUsername: true,
smtpPassword: true,
fromEmail: true,
},
});
if (!emailSettings) {
v;
throw new Error("No email settings found for this shop");
}
emails.setTransport({
host: emailSettings.smtpHost,
port: emailSettings.smtpPort,
auth: {
user: emailSettings.smtpUsername,
pass: emailSettings.smtpPassword,
},
});
await emails.sendMail({
to: "customer@example.com",
from: emailSettings.fromEmail,
subject: "Your order is confirmed",
html: "Thanks for your order.
",
});
};
```
In this example, the `shopId` comes from the current Shopify session, so Gadget automatically gives you the tenant identifier for the merchant making the request. If you are not using Shopify, use the tenant identifier available in your own session, route params, or action params and query your settings model the same way.
In summary to configure an external transporter:
* Store your transporter credentials in one place, either globally or in a tenant-specific model.
* Resolve the correct tenant identifier before sending the email.
* Load the transport configuration and pass it to `emails.setTransport()`.
* Call `emails.sendMail()` after the transport has been configured.