DEV Community

William Cunha Cardoso
William Cunha Cardoso

Posted on

Sending Emails with Dart and Appwrite Serverless Functions

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:

appwrite init project
Enter fullscreen mode Exit fullscreen mode

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:

appwrite init function
Enter fullscreen mode Exit fullscreen mode

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:

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

}
Enter fullscreen mode Exit fullscreen mode

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:

dart pub add http
Enter fullscreen mode Exit fullscreen mode

Wrapping with Try/Catch

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

Future<void> start(final req, final res) async {
  try {
    // Your code goes here
  } catch (error) {
    return res.send('Error: ${error.toString()}', status: 400);
  }
}
Enter fullscreen mode Exit fullscreen mode

Response Class

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

class ResponseMap {
  final String message;
  final int statusCode;

  ResponseMap(this.message, this.statusCode);
}
Enter fullscreen mode Exit fullscreen mode

Email Dispatch Method

Now, create a method that sends the email:

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,
  );
}
Enter fullscreen mode Exit fullscreen mode

Handling Requests

Handle incoming requests in your function:

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Code mounted

In the end, your code should look like this

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

cd ../..
Enter fullscreen mode Exit fullscreen mode
appwrite deploy function
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use CURL to make an HTTP request:


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\"}]}"
}
'
Enter fullscreen mode Exit fullscreen mode

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

Top comments (1)

Collapse
 
joaopereirajp profile image
João Carlos Pereira

Amazing!