DEV Community

Cover image for Creating a Weekly Mailer with Rio
Denizhan Toprak
Denizhan Toprak

Posted on • Updated on

Creating a Weekly Mailer with Rio

Project git: https://github.com/rettersoft/rio-samples
Rio Docs: https://docs.retter.io

What to Learn With This Project?

In this Rio project, our main goal is to creating a backend for an application that mails to its subscribers weekly.
In that road we are going to look at Rio's:

  • Scheduling.
  • Using other SDK's (Postmark).
  • Communication between classes.
  • Singleton architecture
  • Model Usage

For following along you may want to use Postmark also. To getting your POSTMARK_API_TOKEN visit Postmark.

Our Project's End Result

First we need to have a logic for our subscribers. For example they need to be able to subscribe :). Like This:

New Subscriber Image

After we run this method we need to validate the mail. So we e-mail the subscriber. Only then we will be able to send weekly mails to them. Like This:

Validation Email Image

After that we can send our weekly mails to validated subs!

Weekly Email Image

Don't worry your mail doesn't need to be so simple. You can use the HTML as you please in your mails.

Let's get to it!

Creating Models

In Rio Models are used to validate method's input, make them more readable and easier to Debug. For More: Rio Models.

We need only one model, which is Subscriber. And only the email will be mandatory. Rest will not be the input validation but the variables we need for further use.

Subscriber

{
    "type": "object",
    "properties": {
        "email": {
            "type": "string",
            "format": "email"
        },
        "isApproved": {
            "type": "boolean"
        },
        "createdAt": {
            "type": "number"
        }
    },
    "required": ["email"],
    "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

Using Other SDK's

Using other SDK's are extremely easy in Rio. Just go to your class package.json and add your desired SDK and it's version. Mine is looking like this:

Image description

Than deploy your project. That' s it you can use your SDK in your class.

Note that we are going to use POSTMARK both Subscriber and Mailer class. So add postmark to both of them.

In postmark and many other SDK's we need a token to use the accual SDK functions. And adding the tokens manually in the files can be a little bit cumbersome
and unsafe (tokens may be precious). So adding to them to the Enviroments is accually a good idea. So lets do that.

Environments Image

In our files we can use this token like this:

const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN);

Subscriber Class

This class will be responsible of creating subscribers, e-mailing them for validation, unsubscribe them if wanted, and validate them. But first we need to kinda "initialize" the class in index.ts. Let's go step by step.

template.yml

This file determines which methods are going to be used in this class and how to use them. For example we just created our model. In template.yml we decide which of the methods are going to use this model.

Our's looks like this for the Subscriber class:

init: index.init
getState: index.getState
getInstanceId: index.getInstanceId
methods:
  - method: getSubscribers
    handler: subscribe.getSubscribers
  - method: subscribe
    inputModel: Subscriber
    handler: subscribe.subscribe
  - method: validate
    inputModel: Subscriber
    handler: subscribe.validate
  - method: unSubscribe
    inputModel: Subscriber
    handler: subscribe.unSubscribe
  - method: sendMailToSubscribers
    handler: subscribe.sendMailToSubscribers
Enter fullscreen mode Exit fullscreen mode

As you see we give each method a name in method, where to run them in handler and if necessary an inputModel for the models it is going to use.
There are a few more options if you want to check out in template docs.

index.ts

I am just going to drop the whole index.ts then break it down.

import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";

const rdk = new RDK();

export async function authorizer(data: Data): Promise<Response> {
    return { statusCode: 401 };
}

export async function init(data: Data): Promise<InitResponse> {
    return {
        state: {
            private: {
                subscribers: [],
                preSubscribers: [],
            },
        },
    };
}

export async function getInstanceId(data: Data): Promise<string> {
    return "defaultInstance";
}

export async function getState(data: Data): Promise<Response> {
    return { statusCode: 200, body: data.state };
}
Enter fullscreen mode Exit fullscreen mode

In Init we determine the variables to be created when the class gets created. So we create a subscribers array which will hold the validated subscribers and the preSubscribers which are not validated.
We create them here so we don' t need to worry about "If they exist" later.

getInstanceId is a makes this whole project singleton. We return the instanceId as "defaultInstance" so there will be only one instance in this project which is the "defaultInstance". You can change it's return value to your needs of course.

Rest of the code is default code actually. So if you get curious about rest check the docs!

Subscribe.ts

This file going to hold the main methods we are going to use.
The imports and const values for this class looks likes this:

import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";
import { Subscriber } from "./rio";
const postmark = require("postmark");
const rdk = new RDK();
const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN); // you put your postmarkToken in Settings -> Enviroment (c.retter.io)
Enter fullscreen mode Exit fullscreen mode

Let's keep going!

subscribe method

This method's input section needs to be looked at. If we look at this sample everything going to make sense:

export interface Data<I = any, O = any, PUB = KeyValue, PRIV = KeyValue, USER = UserState, ROLE = RoleState>

So for I (input) we typed Subscriber. So our Input value should be in the format of Subscriber (our model). And for PRIV (private state) we typed { preSubscribers: Subscriber[]; subscribers: Subscriber[]. So in private state there should be preSubscribers and subscribers array. And their members should be in the format of Subscriber. That's it!

/**
 * @description creates a preSubscriber and sends a confirmation email
 */
export async function subscribe(
    data: Data<
        Subscriber,
        any,
        any,
        { preSubscribers: Subscriber[]; subscribers: Subscriber[] }
    >
): Promise<Data> {
    // check if there is already a subscriber or presubscriber with the same email
    let subscriber = data.state.private.preSubscribers.find(
        (s) => s.email === data.request.body.email
    );
    if (!subscriber) {
        subscriber = data.state.private.subscribers.find(
            (s) => s.email === data.request.body.email
        );
    }

    // if there is a subscriber with the same email
    if (subscriber) {
        data.response = {
            statusCode: 404,
            body: {
                status: "This member is already registered to preSubscribers",
            },
        };
    }

    // if there is no subscriber with the same email
    else {
        const preSubscriber: Subscriber = {
            email: data.request.body.email,
            createdAt: Date.now(),
            isApproved: false,
        };
        data.state.private.preSubscribers.push(preSubscriber);
        // send mail to preSubscriber's mail to confirm subscription
        await client.sendEmail({
            From: "denizhan@rettermobile.com",
            To: preSubscriber.email,
            Subject: "Confirm your subscription",
            HtmlBody: `
            <h1>Welcome</h1>
            <p>Thanks for trying Rio News. We’re thrilled to have you on board. To finish your register process, validate your account by clicking the link below:</p>
            <!-- Action -->
            <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
              <tr>
                <td align="center">
                  <!-- Border based button https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
                  <table width="100%" border="0" cellspacing="0" cellpadding="0">
                    <tr>
                      <td align="center">
                        <table border="0" cellspacing="0" cellpadding="0">
                          <tr>
                            <td>
                                <a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate</a>
                            </td>
                          </tr>
                        </table>
                      </td>
                    </tr>
                  </table>
                </td>
              </tr>
            </table>
            </table>
            `,
            //HtmlBody: `<html> <body> <h1>Confirm your subscription</h1> <p>Click <a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate</a> to confirm your subscription</p> </body> </html>`,
            TextBody: `Confirm your subscription by clicking the link below \n https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}`,
        });

        data.response = {
            statusCode: 200,
            body: {
                status: "New preSubscriber added to preSubscribers",
            },
        };
    }
    // send mail to new preSubscriber

    return data;
}
Enter fullscreen mode Exit fullscreen mode

This method basically gets the input e-mail from the request and check if there are any users which has the same e-mail. If not, it sends a mail to input e-mail address using postmark. And in validation link we run the validate method.

How Did We Run a Method via Link?
If we take a closer look to this link:

<a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate </a>

We should give the projectId via ${data.context.projectId}. This finds our project. Then we need to Call this method. After that we navigate to the method we want to run (in this case validate). Finally the instance value (in this case defaultInstance.
Giving parameters when running methods without tokens can be achieved by using querystringparams. In this example after we gave our instance we determine the variable email and value will be what' s inside the brackets ${ }.

Validate Method

As you can see we get the input using data.requst.queryStringParams here. After that the variable we want (in this case email).

/**
 * @description validates a preSubscriber and adds it to subscribers and removes from preSubscribers
 */
export async function validate(
    data: Data<
        Subscriber,
        any,
        any,
        { preSubscribers: Subscriber[]; subscribers: Subscriber[] }
    >
): Promise<Data> {
    // check if user already registered to subscribers
    let subscriber = data.state.private.preSubscribers.find(
        (s) => s.email === data.request.queryStringParams.email
    );

    if (!subscriber) {
        data.response = {
            statusCode: 404,
            body: {
                status: "This member is not in the preSubscribers",
            },
        };
    } else {
        data.state.private.preSubscribers =
            data.state.private.preSubscribers.filter(
                (s) => s.email !== data.request.queryStringParams.email
            );
        subscriber.isApproved = true;
        data.state.private.subscribers.push(subscriber);
        data.response = {
            statusCode: 200,
            body: {
                status: `New subscriber ${subscriber.email} added to subscribers`,
            },
        };
    }

    return data;
}
Enter fullscreen mode Exit fullscreen mode

In this method we get the input email and check this email if it is in the preSubscribers array which we hold in the state.private. If nothing goes wrong. We create a subscriber from that email and add that subscriber to subscribers array.

unSubscribe Method

Nothing different here. This method is for the subsribers wants to leave our application.

/**
 * @description Removes the subscriber to its email, from the preSubscribers and subscribers
 */
export async function unSubscribe(
    data: Data<
        Subscriber,
        any,
        any,
        { subscribers: Subscriber[]; preSubscribers: Subscriber[] }
    >
): Promise<Data> {
    // check if user already registered to subscribers
    let subscriber = data.state.private.subscribers.find(
        (s) => s.email === data.request.body.email
    );
    if (!subscriber) {
        subscriber = data.state.private.preSubscribers.find(
            (s) => s.email === data.request.body.email
        );
    }

    if (!subscriber) {
        data.response = {
            statusCode: 404,
            body: {
                status: "This member is not in the subscribers or preSubscribers",
            },
        };
    } else {
        data.state.private.subscribers = data.state.private.subscribers.filter(
            (s) => s.email !== data.request.body.email
        );
        data.state.private.preSubscribers =
            data.state.private.preSubscribers.filter(
                (s) => s.email !== data.request.body.email
            );
        data.response = {
            statusCode: 200,
            body: {
                status: "Subscriber has removed from subscribers and presSubscribers",
            },
        };
    }

    return data;
}

Enter fullscreen mode Exit fullscreen mode

We remove the all subscribers or preSubscribers which has the email given.

getSubscribers

Communcation Between Classes is a another aspect of this sample. This method provides that functionallity. We simply return the subscribers in this method to call this method from another class later.

/**
 * @description returns the subscribers list for other classes to use
 */
export async function getSubscribers(
    data: Data<any, any, any, { subscribers: Subscriber[] }>
): Promise<Data> {
    data.response = {
        statusCode: 200,
        body: {
            subscribers: data.state.private.subscribers,
        },
    };
    return data;
}
Enter fullscreen mode Exit fullscreen mode

Mailer Class

This class has only one purpouse: send weekly mail to the subcribers using scheduling.

template.yml

We determine when and how often the method is going to run in here.

Our template for this class looks like this:

init: index.init
getState: index.getState
getInstanceId: index.getInstanceId
methods:
  - method: sendMailToSubscribers
    type: STATIC
    handler: sendMail.sendMailToSubscribers
    schedule: cron(0 18 ? * MON-FRI *)
Enter fullscreen mode Exit fullscreen mode

For scheduled methods, type must be Static. And we can determine the schedule ourselves. Rio uses cron or we can use rate like this: rate(30 minutes). For this example this sendMailToSubscribers method is going to be run at 18.00 every weekday. You can customise that whatever you like. For further information about this check scheduling.

sendMailToSubscribers method and Whole File

Because this file only includes this method I tought it is best to share the whole picture.

import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";
import { Subscriber, Classes } from "./rio"; // get the classes for calling the "getSubscribers" method we saw earlier
const postmark = require("postmark");
const rdk = new RDK();
const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN); // you put yours in Settings -> Enviroment (c.retter.io)

/**
 * @description sends email to all subscribers
 */
export async function sendMailToSubscribers(
    data: Data<any, any, any, { subscribers: Subscriber[] }>
): Promise<Data> {
    const subscribeInstance = await Classes.Subscribe.getInstance();
    const subscribers = await subscribeInstance.getSubscribers();
    const subscribersArray = subscribers.body.subscribers;

    //create mail template
    const mailTemplate = {
        From: "denizhan@rettermobile.com",
        To: "",
        Subject: "Hi From Retter!",
        TextBody: "Hi there, here is your news this week: \n Blah Blah Blah",
    };

    // use await because mailing to all subscribers will not be happen instantly.
    // otherwise errors may occure
    await Promise.all(
        subscribersArray.map(async (subscriber) => {
            mailTemplate.To = subscriber.email;
            await client.sendEmail(mailTemplate);
        })
    );

    data.response = {
        statusCode: 200,
        body: {
            status: "Mail sent to all subscribers",
            subscribersArray,
        },
    };

    return data;
}

Enter fullscreen mode Exit fullscreen mode

As you see in the code, our first goal is to get subscribersArray. In order to do that we get the instance of Subscribe class we imported. Than we can call the other classes method from now on, such as getSubscribers method from Subscribe class.

After getting the subscribers, we create our mail template for Postmark. We change the destination for these mails in a loop and send their recievers.

Note that we used await Promise.All here. This is to making sure that every subscribers will get their mail.

All Done!!!

That' it. Now you can use this to mail your friends weekly? Or in a big project that has 100.000 subs? Who knows? Maybe completely different project is in your mind. Whatever it is, hope this text helped you!

If you have any suggestions for this article please share.

Thanks!

Top comments (0)