Skip to content

Announcing email triggers for Appwrite Functions_

Appwrite Functions can now execute when an email arrives. Each function has a built-in email address, and you can connect a custom domain with MX and TXT records.

6 min read

Many apps receive work by email. Customers send support requests, vendors send invoices, and monitoring services send alerts. To process that email in code, you usually need an inbound email provider, a webhook endpoint, and a MIME parser.

Appwrite Functions on Appwrite Cloud now have email triggers. A function can execute when an email arrives at its address. Appwrite receives the email, parses it, and executes your function with the email as JSON and each attachment as a file.

How email triggers work

The email trigger is off by default, so you turn it on for each function that should receive email. After that, each email that arrives at an address of the function creates one execution. The execution is a POST request, and the x-appwrite-trigger header is email.

Appwrite handles each email in this order:

  1. Checks that each recipient address belongs to an enabled function that has the email trigger on and an active deployment. An address on a custom domain also needs a verified domain.
  2. Receives the email over SMTP and scores it with a spam filter.
  3. Parses the headers, the plain text body, the HTML body, and the attachments.
  4. Executes each function with the email in the request body, while the sending mail server waits.
  5. Confirms receipt to the sending mail server after the functions finish.

Appwrite does not store the email or its attachments. Your function gets them in one request, so it needs no MIME parser and no mail server of its own.

When your function returns an error, Appwrite still confirms the email, and the sender does not get a bounce message, so check the execution logs of the function for errors. When the function cannot run or does not finish in time, Appwrite does not confirm the email, and the sending mail server sends it again later.

Receive email at the built-in address

Each function has a built-in email domain, <FUNCTION_ID>-<PROJECT_ID>.appwrite.email, and it does not need DNS records.

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. The function reads the address that received the email from envelope.to.

To turn on the trigger, open your function in the Appwrite Console, go to Settings > Executions, turn on Email trigger in the Email card, and click Update. The card shows an address of the function under Default address.

Connect a custom domain

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

Turn on the email trigger first. Then open the Domains tab of your function, click Add domain, select the Email type, enter the domain, and click Add. The Console shows two DNS records to add at your DNS provider:

  • An MX record that sends the email of the domain to the Appwrite mail server.
  • A TXT record named _appwrite.mail with your project ID as the value. This record proves that you own the domain.

After you add the records, click Verify.

Handle an email in your function

The request body is multipart/form-data. Its first part is named email and contains the email as JSON: the sender, the recipients, the subject, the plain text and HTML bodies, a selection of headers, a list of attachments, and the spam score. This shortened example shows the main fields. The documentation has the full format.

JSON
{
  "id": "5f0c2a9e3b7d4e1f8a6c0b2d4e6f8a1c",
  "envelope": {
    "from": "walter@example.net",
    "to": ["support@6a51290fe3d0727e293a-6a5128c4001f2d3e8a7b.appwrite.email"]
  },
  "from": "Walter O'Brien <walter@example.net>",
  "subject": "Order 1042 did not arrive",
  "text": "Hi, my order 1042 did not arrive yet.",
  "html": "<p>Hi, my order 1042 did not arrive yet.</p>",
  "attachments": [
    {
      "id": "attachment-0",
      "name": "receipt.pdf",
      "contentType": "application/pdf",
      "size": 48213
    }
  ],
  "spam": {
    "score": 0.0213,
    "verdict": "ham",
    "model": "1fb022917838"
  }
}

Each attachment follows the email part as its own part. The part is named after the attachment's id and contains the original file bytes.

This example saves each email to a table and logs the size of each attachment. Give the function the rows.write scope so that it can create rows. The Node.js, Deno, and Go examples read the body with a built-in multipart parser, and the other examples split the body at the boundary from the content-type header.

Node.js
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();
};

Duplicates, spam, and attachments

Plan for three behaviors when you write a function with an email trigger:

  • Duplicates are possible. When Appwrite does not confirm an email, for example because a function did not finish in time, the sending mail server sends the email again, and every function in the delivery executes again. Each copy gets a new id, so compare messageId to skip duplicates.
  • The spam score is a hint. The spam field has a score from 0 to 1 and a verdict of ham, unsure, or spam. Appwrite does not reject email because of it. Automated email such as order confirmations and sign-in codes can score as spam, so test the scores with the email you expect before your function drops anything.
  • Appwrite keeps no copy. The attachments exist only in the request body. Upload them to Storage if you need them later.

Use cases

  • Support inbox. Save each email to a table, and reply to the customer with Messaging.
  • Invoice processing. Read PDF attachments from vendors and store the extracted totals.
  • Alerts from other services. Parse the alerts that monitoring services send by email and open an incident.
  • Conversations by email. Give each conversation its own address, such as ticket-1042@<FUNCTION_ID>-<PROJECT_ID>.appwrite.email or ticket-1042@mail.example.com on a custom domain, and read the address from envelope.to to find the conversation.

Get started

Email triggers are available for Appwrite Functions on Appwrite Cloud. The email triggers documentation has the full request format, the DNS setup for custom domains, and the limits.

Resources

An email trigger executes an Appwrite Function when an email arrives at an address that is connected to the function. Appwrite receives the email, parses it, and executes the function with the email as JSON and each attachment as a file in the request body.

No. Each function has a built-in email domain, <FUNCTION_ID>-<PROJECT_ID>.appwrite.email, that receives email without DNS records after you turn on the email trigger. You need DNS records only to connect a custom domain: an MX record and a TXT record.

Email triggers only receive email. A function can send email with Appwrite Messaging, for example to reply to the sender.

A function can receive the same email more than one time, for example when the sending mail server retries after a function did not finish in time. Each copy gets a new id, so compare the messageId of the email to find duplicates.

The request body is multipart/form-data. The email part lists the attachments, and each attachment follows as its own part, named after the attachment's id, with the original file bytes. Appwrite does not store attachments, so upload them to Storage if you need them later.

Appwrite scores each email with a spam filter and adds the score and a verdict to the email. It does not reject email because of the score, so your function decides what to do with spam.

Read next

Ready to build?_