Skip to content

Functions

Email triggers_

Execute an Appwrite Function when an email arrives at its address. Use the built-in address of the function or connect a custom domain.

10 min read

Raw

An email trigger executes your function when an email arrives at one of the function's addresses. Each function has a built-in address, and you can also connect a custom domain. Appwrite receives the email and executes your function with the email and its attachments in the request body. Your function can save the email to a table, reply to the sender, or send the content to another service.

Email triggers are available on Appwrite Cloud, and they only receive email. To send email from your app, such as a reply, use Messaging.

How email triggers work

  • The email trigger is off by default. A function receives email only after you turn on its email trigger.
  • An email execution is a POST request to the / path of your function, with the x-appwrite-trigger header set to email.
  • Appwrite executes your function while the sending mail server waits, and accepts the email after the function finishes.
  • Each delivery creates one execution. A delivery is one copy of an email that a mail server hands to Appwrite. If a delivery includes several addresses of the same function, the function executes one time. If it also includes addresses of other functions, each of those functions executes one time.
  • A sending mail server can split one email into several deliveries, for example for addresses at different domains or for more than 50 recipients. Each delivery creates its own execution.
  • When your function returns an error, Appwrite still accepts the email. The execution shows as failed, and the sender does not get a bounce message.
  • When your function cannot run, or does not finish within the time limit, Appwrite does not accept the email. The sending mail server keeps the email and sends it again later, so your function can receive the same email more than one time.
  • Appwrite rejects an email when the email trigger of the function is off, the function is disabled, the function has no active deployment, or the custom domain is not verified. Appwrite also rejects email to an address that does not belong to a function, and email that exceeds the limits. The sending mail server then returns the email to the sender.

Turn on the email trigger

Email card with Email trigger turned on and the built-in address under Default address
Email card with Email trigger turned on and the built-in address under Default address

Turn on the email trigger of a function before it can receive email at its built-in address or at a custom domain.

  1. In the Appwrite Console's sidebar, click Functions.
  2. Open your function.
  3. Under the Settings tab, click Executions.
  4. In the Email card, turn on Email trigger.
  5. Click Update.

Built-in address

Each function has a built-in address that does not need DNS records.

  • Domain: <FUNCTION_ID>-<PROJECT_ID>.appwrite.email, with the function ID in lowercase.
  • Default address: the Email card in the Console shows contact@<FUNCTION_ID>-<PROJECT_ID>.appwrite.email.
  • Other addresses: any name before the @ works. For example, support@<FUNCTION_ID>-<PROJECT_ID>.appwrite.email and billing@<FUNCTION_ID>-<PROJECT_ID>.appwrite.email both execute the same function.
  • Receiving address: the function reads which address received the email from envelope.to.

A function does not have a built-in address when:

  • The function ID contains a . or a _.
  • The project ID ends with a -.
  • The function ID and the project ID together are longer than 62 characters.

To receive email for such a function, connect a custom domain.

Custom domain

Verify email domain screen with the MX and TXT records for mail.example.com
Verify email domain screen with the MX and TXT records for mail.example.com

You can connect a domain that you own, such as mail.example.com, to your function. Every email sent to that domain goes to your function, so use a domain that you don't use for any other email.

A custom domain needs two DNS records. The MX record sends the domain's email to Appwrite, and the TXT record proves that you own the domain. For mail.example.com, the records are:

TypeNameValue
MXmailThe Appwrite mail server that the Console shows. Any priority works.
TXT_appwrite.mailYour project ID

Most DNS providers expect names relative to your domain's DNS zone, as in this table. For an apex domain such as example.com, which has no subdomain, use @ for the MX record and _appwrite for the TXT record.

Email to the domain executes the function only after Appwrite verifies the domain. DNS records can take up to 48 hours to propagate.

To connect a domain:

  1. Turn on the email trigger of your function.
  2. Under the Domains tab of your function, click Add domain.
  3. Under Type, select Email.
  4. In Domain name, enter your domain, such as mail.example.com.
  5. Click Add.
  6. At your DNS provider, add the MX and TXT records that the Console shows.
  7. Click Verify.

If the records have not propagated yet, the domain stays unverified. To verify it later, under the Domains tab, click Retry next to the domain. Then click Verify.

Handle an email

Appwrite sends these headers with each email execution:

  • x-appwrite-trigger: email
  • x-appwrite-email-id: the delivery ID. It is the same as the id field of the email.
  • x-appwrite-email-recipient: the first address of your function in the delivery.
  • x-appwrite-email-domain: the domain of the address in x-appwrite-email-recipient.

Email executions do not receive the x-appwrite-execution-id, x-appwrite-client-ip, x-appwrite-country-code, x-appwrite-continent-code, or x-appwrite-continent-eu headers.

The request body is multipart/form-data. It has an email part with the email as JSON, and one part for each attachment, as Request body describes. This example saves each email as a row in a table and logs the size of each attachment. The table has from and subject columns, and a mediumtext column named text that fits an email body of up to 8 MB. To let the ephemeral API key of the function create rows, give the function the rows.write scope.

The Node.js, Deno, and Go examples read the body with a built-in multipart parser. The other examples split the body at the boundary from the content-type header.

import { Client, ID, TablesDB } from 'node-appwrite';
export default async ({ req, res, log }) => {
if (req.headers['x-appwrite-trigger'] !== 'email') {
return res.text('Not an email', 400);
}
const form = await new Response(req.bodyBinary, {
headers: { 'content-type': req.headers['content-type'] }
}).formData();
const email = JSON.parse(form.get('email'));
const client = new Client()
.setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT)
.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
.setKey(req.headers['x-appwrite-key']);
const tablesDB = new TablesDB(client);
await tablesDB.createRow({
databaseId: '<DATABASE_ID>',
tableId: '<TABLE_ID>',
rowId: ID.unique(),
data: {
from: email.from,
subject: email.subject,
text: email.text
}
});
for (const attachment of email.attachments) {
const file = form.get(attachment.id);
log(`Attachment ${attachment.name}: ${file.size} bytes`);
}
return res.empty();
};

Request body

The request body of an email execution is multipart/form-data, with the boundary in the content-type header. The first part is named email and contains the email as JSON. Each attachment follows in its own part, which is named after the attachment's id and contains the file's original bytes:

Plain text
--appwrite-email-9b2f6c0d4e8a1f3b5c7d9e0a2b4c6d8e
Content-Disposition: form-data; name="email"
Content-Type: application/json
{"id":"5f0c2a9e3b7d4e1f8a6c0b2d4e6f8a1c","envelope":{...},...}
--appwrite-email-9b2f6c0d4e8a1f3b5c7d9e0a2b4c6d8e
Content-Disposition: form-data; name="attachment-0"; filename="receipt.pdf"
Content-Type: application/pdf
<file bytes>
--appwrite-email-9b2f6c0d4e8a1f3b5c7d9e0a2b4c6d8e--

The email part has this format:

JSON
{
"id": "5f0c2a9e3b7d4e1f8a6c0b2d4e6f8a1c",
"envelope": {
"from": "walter@example.net",
"to": ["support@6a51290fe3d0727e293a-6a5128c4001f2d3e8a7b.appwrite.email"]
},
"from": "Walter O'Brien <walter@example.net>",
"to": "support@6a51290fe3d0727e293a-6a5128c4001f2d3e8a7b.appwrite.email",
"cc": "",
"replyTo": "",
"subject": "Order 1042 did not arrive",
"messageId": "<CAF3x9k2@mail.example.net>",
"date": "Wed, 23 Sep 2026 09:14:02 +0000",
"text": "Hi, my order 1042 did not arrive yet.",
"html": "<p>Hi, my order 1042 did not arrive yet.</p>",
"headers": {
"message-id": ["<CAF3x9k2@mail.example.net>"],
"date": ["Wed, 23 Sep 2026 09:14:02 +0000"],
"from": ["Walter O'Brien <walter@example.net>"],
"to": ["support@6a51290fe3d0727e293a-6a5128c4001f2d3e8a7b.appwrite.email"],
"subject": ["Order 1042 did not arrive"]
},
"attachments": [
{
"id": "attachment-0",
"name": "receipt.pdf",
"contentType": "application/pdf",
"contentId": "",
"disposition": "attachment",
"size": 48213
}
],
"transport": {
"remoteIp": "203.0.113.10",
"helo": "mail.example.net",
"tls": false
},
"receivedAt": "2026-09-23T09:14:03.482913571Z",
"spam": {
"score": 0.0213,
"verdict": "ham",
"model": "1fb022917838"
}
}
  • id: the delivery ID. It is the same as the x-appwrite-email-id header.
  • envelope: the sender and recipient addresses that the sending mail server gave during the SMTP connection. They can differ from the from and to headers.
  • envelope.from: the sender address in the envelope. It is empty when the sending server uses an empty sender, as bounce messages and many automatic replies do.
  • envelope.to: the addresses of your function in the delivery. When you receive an email as a BCC recipient or as a forwarded email, only envelope.to shows the address of your function.
  • from, to, cc, replyTo, subject, messageId, and date: the values of these headers in the email.
  • text and html: the plain text and HTML versions of the email body. A value is empty when the email does not have that version.
  • headers: these email headers: message-id, date, from, to, cc, reply-to, subject, in-reply-to, and references. The keys are lowercase, and each value is a list of the raw header values. A header that the email does not have is not in the object, and when the email has none of these headers, the value is an empty array ([]).
  • attachments: the attached files and other non-text parts of the email, such as embedded images. Each has an id, which is also the name of the part that contains the file, a name, a contentType, a contentId, a disposition, and a size in bytes.
  • transport: details of the SMTP connection that delivered the email: remoteIp, the name that the sending server gave in its HELO or EHLO greeting (helo), and whether the connection used TLS (tls).
  • receivedAt: the time when Appwrite received the email, as an RFC 3339 timestamp in UTC that can include fractions of a second.
  • spam: the spam score of the email.

Attachments

Each attachment is a part of the request body, and the part's name is the attachment's id. The part contains the decoded file, so its size matches the size of the attachment. An image that the HTML body embeds has a contentId, which the HTML references as cid:<contentId>.

Appwrite does not keep a copy of the attachments. To keep a file, upload it to Storage from your function.

Spam score

Appwrite scores each email with a spam filter and adds the result to the spam field:

  • score: a number from 0 to 1. A score near 0 means that the email looks legitimate, and a score near 1 means that it looks like spam.
  • verdict: ham (legitimate) for a score below 0.2, spam for a score of 0.99 or more, and unsure for any score in between.
  • model: the version of the spam filter that scored the email.

The value of spam is null when Appwrite cannot score the email. Appwrite does not reject or delay an email because of its score, so your function decides what to do with spam.

Duplicate emails

Your function can receive the same email more than one time. When Appwrite does not accept an email, for example because a function did not finish in time, the sending mail server sends the email again later. Each function in the delivery then executes again, including the functions that already finished. The sending mail server can also send an email again when the connection closes before Appwrite accepts the email.

Each copy is a new delivery with a new id. To detect a duplicate, compare the messageId value, which stays the same when a mail server sends the email again. Some emails do not have a Message-ID header, so messageId can be empty.

Limits

LimitValue
Size of an email as sent, including encoded attachments25 MB
Size of the request body: the email JSON and the decoded attachments20 MB
Combined size of the plain text and HTML bodies after decoding8 MB
Size of the email headers256 KB
Recipients in one delivery50. The sending mail server delivers to more recipients later, in a separate delivery.
MIME parts in one email200, nested at most 5 levels deep
Addresses with non-ASCII charactersNot supported
Duration of an email executionPlan for 60 seconds. Appwrite can stop an email execution after 60 seconds, even when the timeout of your function is longer. The sending mail server then sends the email again later.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.