Table of Contents
- Introduction
- Identifying Requirements
- Preparing the Environment
- 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
- Conclusion
- Considerations
- 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
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
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 {
}
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
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);
}
}
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);
}
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,
);
}
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);
}
}
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);
}
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 ../..
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:
{
"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:
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.
Top comments (1)
Amazing!