Sending Emails with Dart and Appwrite Serverless Functions
Tags: appwrite, dart, serverless, programming
Published: 2023-09-09 15:35:26

## Table of Contents

1. Introduction
2. Identifying Requirements
3. Preparing the Environment
4. Hands-On
   - Create or Link to a Project
   - Create a Function
   - Code the Function
     - Using the http Package
     - Wrapping with Try/Catch
     - Response Class
     - Email Dispatch Method
     - Handling Requests
     - Code mounted
     - Set Up Variables in Functions
     - Deployment
   - Testing
     - Testing via Appwrite Console
     - Testing via CURL Request
5. Conclusion
6. Considerations
7. Additional Resources

## Introduction
Sending emails is a common requirement for various business and project purposes. Whether it's for user authentication, sending invoices, or notifying customers, having a flexible and modular email dispatch solution is essential. In this tutorial, we'll explore how to create a serverless email dispatch function using Dart and Appwrite. This solution not only sends emails but also adheres to principles like modularization and the Open/Closed Solid Principle.

## Identifying Requirements

Before we start building our email dispatch service, let's identify the key requirements:

- Sender: The email address from which the email will be sent.
- Recipients: The list of email addresses and names of the recipients.
- Body: The content of the email.
- Subject: The subject line of the email.

These requirements will help us define the contract for our service.
## Preparing the Environment

For this tutorial, we'll need the following tools:

- Appwrite CLI
- Brevo Account
- Dart
- Visual Studio Code

Feel free to use alternative tools, but ensure they are properly configured in your environment. In this tutorial, I'll be using Appwrite Console version 1.1.2.

## Hands-On
### Create or Link to a Project

To get started, we need a project to host our functions. You can create a new project or link to an existing one using the Appwrite CLI:

```bash
appwrite init project
```
Follow the CLI prompts to either link to an existing project or create a new one.
### Create a Function

Now, let's create a Dart runtime function using the following command:

```bash
appwrite init function
```
Answer the questions prompted by the CLI to configure your function. Remove all the comment and initial code. At the end, your function might look like this:

```dart
Future<void> start(final req, final res) async {

}
```

### Code the Function
#### Using the http Package

We'll use the http package to perform HTTP requests for sending emails. To add it to your project, navigate to your functions directory and run:

```bash
dart pub add http
```
#### Wrapping with Try/Catch

To handle errors gracefully, wrap your code with a try/catch block like this:

```dart
Future<void> start(final req, final res) async {
  try {
    // Your code goes here
  } catch (error) {
    return res.send('Error: ${error.toString()}', status: 400);
  }
}
```
#### Response Class

Create a response class to define the structure of the response:

```dart
class ResponseMap {
  final String message;
  final int statusCode;

  ResponseMap(this.message, this.statusCode);
}
```
#### Email Dispatch Method

Now, create a method that sends the email:

```dart
Future<ResponseMap> sendTransactionalEmail({
  required List<Map<String, dynamic>> recipients,
  required String apiKey,
  required String body,
  required String host,
  required String senderEmail,
  required String subject,
}) async {
  if (recipients.isEmpty) {
    throw ArgumentError('Recipients list cannot be empty');
  }
  if (apiKey.isEmpty) {
    throw ArgumentError('API Key cannot be empty');
  }
  if (body.isEmpty) {
    throw ArgumentError('Email body cannot be empty');
  }
  if (host.isEmpty) {
    throw ArgumentError('Email host cannot be empty');
  }
  if (senderMail.isEmpty) {
    throw ArgumentError('Sender email cannot be empty');
  }
  if (subject.isEmpty) {
    throw ArgumentError('Email subject cannot be empty');
  }

  final url = Uri.parse(host);
  final headers = {
    'Content-Type': 'application/json',
    'api-key': apiKey,
  };

  final sender = {'email': senderMail};

  final to = recipients;

  final data = {
    'sender': sender,
    'to': to,
    'htmlContent': body,
    'subject': subject
  };

  final response = await http.post(
    url,
    headers: headers,
    body: jsonEncode(data),
  );

  return ResponseMap(
    'Email sent successfully! ${response.reasonPhrase}',
    response.statusCode,
  );
}
```
#### Handling Requests

Handle incoming requests in your function:

```dart
Future<void> start(final req, final res) async {
  try {
    final apiKey = req.variables['BREVO_API_KEY'];
    if (apiKey == null) {
      throw ArgumentError('Missing API Key');
    }

    final emailHost = req.variables['EMAIL_HOST'];
    if (emailHost == null) {
      throw ArgumentError('Missing Email Host');
    }

    if (req.payload == null) {
      throw ArgumentError('Missing payload');
    }

    final payload = jsonDecode(req.payload);
    if (payload == null) {
      throw ArgumentError('Missing payload');
    }

    final senderMail = payload['sender'];
    if (senderMail == null) {
      throw ArgumentError('Missing sender email');
    }

    final subject = payload['subject'];
    if (subject == null) {
      throw ArgumentError('Missing email subject');
    }

    final body = payload['body'];
    if (body == null) {
      throw ArgumentError('Missing email body');
    }

    final recipients = <Map<String, dynamic>>[];

    for (var recipient in payload['recipients']) {
      recipients.add({"email": recipient['email'], "name": recipient['name']});
    }

    final result = await sendTransactionalEmail(
      apiKey: apiKey,
      body: body,
      host: emailHost,
      recipients: recipients,
      senderMail: senderMail,
      subject: subject,
    );

    res.send(result.message, status: result.statsCode);
  } catch (error) {
    res.send('Error: ${error.toString()}', status: 400);
  }
}
```
#### Code mounted
In the end, your code should look like this

```dart
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> start(final req, final res) async {
  try {
    final apiKey = req.variables['BREVO_API_KEY'];
    if (apiKey == null) {
      res.send('Missing API Key', status: 400);
    }

    final emailHost = req.variables['EMAIL_HOST'];
    if (emailHost == null) {
      res.send('Missing Email Host', status: 400);
    }

    final payload = jsonDecode(req.payload);
    if (payload == null) {
      res.send('Missing payload', status: 400);
    }

    final senderMail = payload['sender'];
    if (senderMail == null) {
      res.send('Missing sender email', status: 400);
    }

    final subject = payload['subject'];
    if (subject == null) {
      res.send('Missing email subject', status: 400);
    }

    final body = payload['body'];
    if (body == null) {
      res.send('Missing email body', status: 400);
    }

    final recipients = <Map<String, dynamic>>[];

    for (var recipient in payload['recipients']) {
      recipients.add({"email": recipient['email'], "name": recipient['name']});
    }

    final result = await sendTransactionalEmail(
      apiKey: apiKey,
      body: body,
      host: emailHost,
      recipients: recipients,
      senderMail: senderMail,
      subject: subject,
    );

    res.send(result.message, status: result.statsCode);
  } catch (error) {
    res.send('Error: ${error.toString()}', status: 400);
  }
}

Future<ResponseMap> sendTransactionalEmail({
  required List<Map<String, dynamic>> recipients,
  required String apiKey,
  required String body,
  required String host,
  required String senderMail,
  required String subject,
}) async {
  if (recipients.isEmpty) {
    throw ArgumentError('Recipients list cannot be empty');
  }
  if (apiKey.isEmpty) {
    throw ArgumentError('API Key cannot be empty');
  }
  if (body.isEmpty) {
    throw ArgumentError('Email body cannot be empty');
  }
  if (host.isEmpty) {
    throw ArgumentError('Email host cannot be empty');
  }
  if (senderMail.isEmpty) {
    throw ArgumentError('Sender email cannot be empty');
  }
  if (subject.isEmpty) {
    throw ArgumentError('Email subject cannot be empty');
  }

  final url = Uri.parse(host);
  final headers = {
    'Content-Type': 'application/json',
    'api-key': apiKey,
  };

  final sender = {'email': senderMail};

  final to = recipients;

  final data = {
    'sender': sender,
    'to': to,
    'htmlContent': body,
    'subject': subject
  };

  final response = await http.post(
    url,
    headers: headers,
    body: jsonEncode(data),
  );

  return ResponseMap(
    'Email sent successfully! ${response.reasonPhrase}',
    response.statusCode,
  );
}

class ResponseMap {
  final String message;
  final int statsCode;

  ResponseMap(this.message, this.statsCode);
}
```
#### Set Up Variables in Functions
In the Functions Variables settings, configure the following variables:

- BREVO_API_KEY
- EMAIL_HOST

You'll obtain these values from your Brevo Account panel. These should remain secret and not be included in your request payloads.
#### Deployment

Deploy your Appwrite function. Ensure you are in the same directory as your functions:

```bash
cd ../..
```
```bash
appwrite deploy function
```
Select your function and deploy it.
### Testing

You can test your function via the Appwrite Console or by making an HTTP request. Here's an example JSON payload for testing:

```json
{
  "sender": "your-brevo-senders-option@mail.com",
  "body": "<!DOCTYPE html><html><head><title>Simple HTML Message</title></head><body><h1 style=\"color: blue;\">Simple HTML Message</h1><p>This is a sample HTML email.</p><p>You can customize it as needed.</p></body></html>",
  "subject": "Testing Email Sending with Functions",
  "recipients": [
    {
      "email": "recipient@mail.com",
      "name": "Recipient Name"
    }
  ]
}
```
Alternatively, you can use CURL to make an HTTP request:

```bash

curl --request POST \
  --url https://cloud.appwrite.io/v1/functions/YOUR-FUNCTION-ID/executions \
  --header 'Content-Type: application/json' \
  --header 'X-Appwrite-Key: YOUR-API-KEY' \
  --header 'X-Appwrite-Project: YOUR-PROJECT-ID' \
  --data '{
  "data": "{\"sender\":\"sender@mail.com\",\"body\":\"<!DOCTYPE html><html><head><title>Simple HTML Message</title></head><body><h1 style=\\\"color: blue;\\\">Simple HTML Message</h1><p>This is a sample HTML email.</p><p>You can customize it as needed.</p></body></html>\",\"subject\":\"Testing Email Sending with Functions\",\"recipients\":[{\"email\":\"recipient@mail.com\",\"name\":\"Recipient Name\"}]}"
}
'
```
## Conclusion

In this tutorial, we've learned how to create a serverless email dispatch function using Dart and Appwrite. By following modularization principles and adhering to the Open/Closed Solid Principle, you can build a flexible and reliable email service for your applications.
## Considerations

When creating serverless functions, consider the pros and cons. These functions provide a RESTful interface to specific services, making it language-agnostic and location-independent. However, ensure proper error handling, security, and scalability for production use.
In addition, by reading some posts on internet I tried to identify how properly return errors in the request but I could not handle it. Appwrite docs only teachs to current version. If you test it, you'll see my request, on error, it will return the proper message, but still gives a 200 status code. It can be properly resolved on latest appwrite version with proper documentation.

## Additional Resources
- [Appwrite Documentation](https://appwrite.io/docs)
- [Dart Documentation](https://dart.dev/guides)
- [HTTP Package Documentation](https://pub.dev/packages/http)