4.6.2-alpha
Note MailBots version 4+ is tightly coupled with FollowUpThen to reflect our (and our user's!) priorities.
FollowUpThen lifecycle hooks (ex: mailbot.onFutTriggerUser
) allow a developer to add value to FollowUpThen (the original, proto-MailBot) by modifying behavior or injecting UI elements at different points in the followup lifecycle.
MailBots still exists a platform to extend FollowUpThen. We may re-release it as an independent system in the future. If you'd like to see that happen, or have any feedback in general, feel free to email help@humans.fut.io.
MailBots is a platform for creating bots, AIs and assistants that get things done right from your inbox. Read more at mailbots.com.
Go to mailbots.com and create a MailBot. The bot creation process sets up a working instance of this framework.
Next, let's tell our MailBot what to do when it receives an email:
var MailBot = require("mailbots");
var mailbot = new MailBot(); // assuming .env is set
// When someone emails: say-hi@my-bot.eml.bot, respond "Hello Human!"
mailbot.onCommand("say-hi", function(bot) {
bot.webhook.quickReply("Hello Human!");
bot.webhook.respond();
});
mailbot.listen();
say-hi@my-bot.eml.bot
is an example of an "email command". Whatever is before the @ sign is a command to your bot to accomplish some task. Read more about email commands.
Tip: Use our reference guide to quickly look up helpers and method names.
A MailBot's purpose is to help someone get something done quickly, efficiently and without leaving their inbox.
A unit of work accomplished by a MailBot is called a "task". Tasks can scheduled, edited or "completed". Tasks are always associated with a command.
MailBots always work in the context of commands while accomplishing tasks. A command can be thought of as the instruction for how to complete a particular task, or the purpose of the task. Email Commands (shown above) are one way to issue commands. Creating a task with the API also requires a command.
A task (that carries out a command) can be created then wait for the perfect moment to notify a user. Timliness is a core feature of of a MailBot.
When events happen in the MailBots platform (ex: an email is received), your MailBot receives webhooks. Your MailBot then gets some useful bit of work done (ex: enters data into a CRM) and responds with JSON that tells the MailBots platform what to do next (ex: send an email, store data, set a reminder, etc). JSON in, JSON out.
This library simplifies much of the above architecture, allowing you to simply create handler functions for certain events.
The webhook request and response can be viewed via the MailBots Sandbox.
MailBots are composed of handlers – functions that run when certain events occur. For example: When the bot receives an email at this address, execute these actions
Handlers and other bot functionality can be packaged into "skills" – high-level abilities that can shared between bots. Here are some example skills:
You can publish, share and install new skills from npm.
Create an email command that says hi.
var MailBotsApp = require("mailbots");
var app = new MailBotsApp();
mailbot.onCommand("hi", function(bot) {
bot.webhook.quickReply("Hi back!");
bot.webhook.respond();
});
mailbot.listen();
The first handler creates the reminder. The second one handles the reminder when it becomes due.
mailbot.onCommand("hi", function(bot) {
// Schedule the task to trigger in 1 minute
bot.webhook.setTriggerTime("1min");
bot.webhook.respond();
});
// When a task with "hi" command triggers, run this
mailbot.onTrigger("hi", function(bot) {
bot.webhook.quickReply("Hi 1 minute later!");
bot.webhook.respond();
});
MailBots can render quick-action buttons (mailto links that map to executable code) that let users get things done without leaving their inbox. Read more about action emails.
// This first handler renders an email with an action-email button
mailbot.onCommand("send-buttons", function(bot) {
bot.webhook.sendEmail({
to: bot.get('source.from')
from: "MyBot",
subject: bot.get('source.subject'),
body: [
// 👇 An email-action
{
type: "button",
behavior: "action",
text: "Press Me",
action: 'say.hi',
subject: "Just hit 'send'",
}
]
});
bot.webhook.respond();
};
// This handler handles the email action
mailbot.onAction('say.hi', function(bot) {
bot.webhook.quickReply('hi');
// Lots of useful things can be done here. Completing a todo item, adding notes to a CRM, etc.
bot.webhook.respond();
});
mailbot.listen();
Let's install a skill to that extracts the email message from previously quoted emails and signatures.
var mailbotsTalon = require("mailbots-talon");
mailbot.onCommand("hi", function(bot) {
const emailWithoutSignature = mailbotsTalon.getEmail(bot);
bot.quickReply(
"Here is the email without the signature:" + emailWithoutSignature
);
bot.webhook.respond();
});
Note: The first matching event handler ends the request, even if subsequent handlers also match, except where otherwise noted.
mailbot.onCommand(command, handlerFn)
Handle when an email command is received. This also fires when a new task is created via the API (All tasks have a command to define their purpose or reason for existence.)
mailbot.onCommand("todo", function(bot) {
//handle when a task is created with this command string
});
Regular expressions work with all handlers.
mailbot.onCommand(/todo.*/, function(bot) {
//Handle todo.me@my-ext.eml.bot, todo.you@my-ext.eml.bot, todo.everyone@my-ext.eml.bot
});
mailbot.onTrigger(command, handlerFn)
Timeliness is a MailBot superpower. A user can schedule a task, then days, months or years later your bot can follow up at the exact right moment – scheduled by the user, or by another event. Read more about triggering.
Tasks with different commands may trigger differently. For example:
mailbot.onTrigger("todo.me", function(bot) {
// Assigned to myself, so only remind me
});
mailbot.onTrigger("todo.assign", function(bot) {
// Assigned to the person in the 'to' field, so remind them
});
mailbot.onTrigger("todo.crm", function(bot) {
// Query CRM API, populate email followup with contact data
});
mailbot.onAction(action, handlerFn)
Handle when a user performs an action that relates to a task.
For example a user can send an action-email to accomplish an action without leaving their inbox (postponing a reminder, completing a todo item, logging a call, etc).
mailbot.onAction("complete", function(bot) {
// Complete the related todo
});
MailBot Conversations
Set the reply-to address of a MailBot email to an action email to carry out a conversation with the user.
mailbot.onAction("assistant", function(bot) {
// Use luis-ai middlweare to dtermine intent
// Store conversation state
// send-email whose reply-to is also "assistant"
});
mailbot.onTaskViewed(command, handlerFn)
Handle when a task is viewed in the MailBots Web UI, allowing a user to view and interact with a future MailBots email.
mailbot.onTaskViewed("todo.me", function(bot) {
// Show a preview of the future email
});
Different task commands may render differently. For example a task with command todo.crm
may query and render current CRM data within the preview.
mailbot.onEvent(event, handlerFn)
Handle when the MailBot receives an inbound webhok from a 3rd party system about an external event. For example, a support ticket is created or a lead is added to a CRM.
Note: This action does not automatically create a MailBots Task. One can be created with mailbots-sdk
mailbot.onEvent("issue.created", async function(bot) {
// Handle event, for example, create a MailBots Task.
const mailBotsClient = MailBotsClient.fromBot(bot);
await mailBotsClient.createTask({
// Pre-authenticated API client
});
bot.webhook.respond({ webhook: { status: "success" } });
});
mailbot.onSettingsViewed(handlerFn)
Handle when a user views this MailBot's settings.
Bots can build custom settings pages. These are rendered when a user views bot settings on the MailBots.com admin UI.
See the settings page reference.
The handler's only parameter is a callback function that responds with JSON to to render a settings form. The callback function is passed the bot
object as usual.
Settings form JSON can be easily built using the settings helper functions.
Unlike the other handlers, every instance of this handler is called. (ie, all settings pages from all settings handlers are rendered).
NOTE: Do not call bot.webhook.respond()
at the end of this particular request. MailBots' internals take care of compiling the JSON and responding.
// Render a settings field for the user to enter their first name
mailbot.onSettingsViewed(async function(bot) {
const todoSettings = bot.webhook.settingsPage({
namespace: "todo",
title: "Todo Settings", // Page title
menuTitle: "Todo" // Name of menu item
});
todoSettings.setUrlParams({ foo: "bar" }); // send a URL param (useful for showing dialogs)
const urlParams = bot.get("url_params"); // retrieve URL params (see below)
todoSettings.input({ name: "first_name", title: "First name" });
todoSettings.buton({ type: "submit" });
// Populate form values
todoSettings.populate(bot.get("mailbot.stored_data.todo"));
// Note bot.webhook.respond() is NOT called
});
URL parameters are passed through to the settings webhook. Use this to pass data into your settings when linking to it or displaying dialogs to the users (see above handler)
mailbot.onSettingsViewed(function(bot) {
const settingsPage = bot.webhook.settingsPage({ namespace: "todo" });
const urlParams = bot.get("url_params", {}); //defualts to empty object
if (urlParams.linkInstructions)) {
settings.text(`# Instructions to link your account!`);
}
// Note that there is no submit button. It's just an informational page.
});
You can also pass URL params via the urlParams
key in the button
form element (it must be type:submit
);
// within a onSettingsViewed form as shown above
settings.button({
submitText: "Save Notification Settings",
type: "submit",
urlParams: { saveSettings: 1 }
// Tip: Pass through all URL current params, but use caution! (see note)
// urlParams: {saveSettings: 1, ...bot.get("url_params", {})}
});
NOTE: URL parameters are an easy way to pass data into your bot settings, but keep this in mind while using URL params: Anyone can link a user to their settings page with anything in URL. Do not, for example, create a url like: /settings?delete_everything=true
that deletes all their tasks. An unsuspecting user may arrive on their settings page from an external link, not see this in the URL and submit the form only find themselves without any data. Read more.
mailbot.onSettingsSubmit(handlerFn)
Handle when the user submits their settings.
To persist the newly saved data, it must be explicitly saved. Use bot.webhook.saveMailBotData()
or
bot.set("mailbot.stored_data")
or API calls to do this.
Newly submitted values are available under settings
.
Previously saved values are available under the mailbot.stored_data
.
Every instance of this handler is called when any settings form is saved (similar to the above onSettingsViewed
handler)
This handler is a good place begin an oauth handshake, set up data in other system or perform API calls to other systems.
mailbot.onSettingsSubmit(bot => {
// assuming the same "todo" namespace as shown in the above examples
const data = bot.get("settings.todo");
// handler is fired any times settings are saved, even if its not our form
if (!data) return;
// perform API calls, validate connections, update external systems here
// validate or modify data, then save it.
bot.set("mailbot.store_data.todo", data);
// This error would show to the user
if (error) {
bot.webhook.respond({
webhook: {
status: "error",
message: "This is a warning message"
}
});
}
return;
});
URL params are useful for passing data into settings handlers, showing dialogs and more. We tried to preserve the mental model of URL parameters while working with settings forms, but but it does not always apply exactly.
In the onSettingsSubmit handler, you need to pass url_params
parameters through the webhoook response:
// in onSettingsSubmit handler
bot.set("url_params", { show_success_dialog: "true" });
This is now accessible as a "url_param" in your onSettingsViewed
handler. You will also see it as a URL
param in the settings UI:
// in the onSettingsViewed handler, render a dialog with a button that dismisses the dialog
// const settingsForm set up earlier
const urlParams = bot.get("url_params", {});
if (urlParams.show_success_dialog) {
settingsForm.text("# Success \nYour todo list has been set up!");
settingsForm.button({ type: "submit", text: "dismiss", urlParams({ dismissSuccess: true }) });
};
You can also set URL parameters using the button
element which can trigger different actions based on the URL
(Note the CSR caution above).
// back ino onSettingsSubmit handler.
if (bot.get("url_params.dissmissSuccess")) {
bot.saveMailBotData("todo.show_success_dialog", null); // set to null to clear the value
}
Setting URL parameters in the onSettingsViewed
hook requires a different method:
// onSettingsViewed
// Force the page to have specific URL params
settingsPage.setUrlParams({ key: "value" });
// Force the settings page to have no URL params
settingsPage.setUrlParams({});
mailbot.on(webhookEvent, handlerFn)
This is a generic handler for any webhook. It can be used to handle any inbound webhook – mainly ones that are not covered by the handlers above. (Of note: The handlers above are simply wrappers for this lower-level handler).
Example:
mailbot.on("mailbot.installed", function(bot) {
// Handle when a MailBot is installed
// Create task with MailBots SDK
// bot.webhook.respond();
});
The first paramater can be:
type
. (ie. mailbot.installed
)type
The second parameter is the function handler that runs only if the matching condition (the first parameter) is met.
mailbot.on("mailbot.installed", function(bot) {
// Handle when a MailBot is installed
// Create a task from mailbots sdk
// bot.webhook.respond();
});
Ideally, error conditions are handled within the normal application flow. If something unexpected happens you can set up a custom error handler for adding logs, alerting or providing error messaging to users.
If you want to send the user your own error email, use the sendEmail method from the MailBots SDK. (Not all webhook responses send emails).
// define a custom error handler
app.setErrorHandler(function(error, bot) {
// myCustomLogger.log(error);
// send email with mailbots sdk
// send a custom error email to user
// Respond with 5xx status code to send the generic MailBots error email to the user
// bot.response.status(500);
bot.webhook.respond({
status: "error",
message: "A custom error message" // Shown to user if in the web UI.
}); // A webhook response must be sent
});
The below handlers are fired in response to FollowUpThen lifecycle events. Their response signature differs from the native MailBots handlers above since it is injecting UI elements and behavioral changes into the followup cycle.
The response style for all FollowUpThen Lifecycle Hook handlers is shown in onFutCreateUser below.
Available options for the response are described in the ISkillReturnValue
interface within types.ts
Modify the FollowUpThen user's confirmation email or modify the task object when a followup is created.
mailbot.onFutCreateUser(bot => {
bod.webhook.addFutUiBlocks([
{
type: "text",
text: "Text block"
}
]);
});
// using native JSON response with TypeScript
mailbot.onFutCreateUser(bot => {
const ISkillReturnValue: response = {
futUiAddition: [
{
type: "text",
text: "Text block"
}
]
};
bot.responseJson = response;
return;
});
In rare cases (currently only when a task (-t) type followup is being created, a non-FUT user receives an email which can be modified using the above hook.
Render of UI elements into the preview email. (Shown when when a FUT user clicks "preview"
to see what a reminder format will do). These elements are usually identical to the ones shown in the
onFutViewUser
and onFutTriggerUser
hooks.
If a preview will trigger the a followup to a non-FUT user (ie, when used in "cc"), this allows allows for a preview of what this will look like.
Render UI elements when a user is viewing a followup.
If a FUT has emails sent to non-users, use this to render UI elements for only the non-user email.
Render UI elements that are only visible to the FollowUpThen user when a followup becomes due.
Render UI elements that are only for the non-user when a followup becomes due (if the followup format has a non-user email component).
Take action when a task is edited. This may involve creating, removing or unlinking a linked resource.
The UI elements above may trigger email-based actions (fired from email, or from the FUT UI). This handler allows for the handling of these actions.
The bot
object passed to the handlers above contains useful helpers that make it easy to handle bot requests. See webhook helpers reference docs.
Setting Data Works By Shallow Merging Data is set by shallow merging. For example.
bot.webhook.setTaskData("my_namespace", { name: "Joe" });
bot.webhook.setTaskData("my_namespace", { key: "123" });
// task data is now
console.log(bot.webhook.responseJson);
// {my_namespace: { name: "Joe", key: "123" }}
bot.webhook.setTaskData("my_namespace", {
name: "Joe",
data: { value: "here" } // ⚠️ Overwritten (shallow merge)
});
bot.webhook.setTaskData("my_namespace", {
data: { value2: "there" }
});
// task data is now
console.log(bot.webhook.responseJson);
// {my_namespace: { data: { value2: "there" } }}
"Skills" are sharable pieces of bot functionality. Skills can encapsulate everything they need (handlers, settings panels helper funcitons and UI elements) so they are ready for use installed. They are great for keeping your code organized and for sharing functionality with others.
We could better organize our handlers in the above examples by grouping them into different files. For example:
// my-new-reminder-skill.js
module.exports.activate = function(mailbot) {
// Handlers can go here
// mailbot.onCommand...
};
Activate the skill's handlers like this (normally done in the top-level to ensure the skill is activated only once):
// In top-level app.js
const myNewReminderSkill = require("./my-new-skill")(mailbot);
myNewReminderSkill.activate(mailbot);
Isolating your handlers within skills is a great way to keep your project organized. The down-side is that the skill owns each request from beginning to end – not very flexible.
The next sections cover more granular and composable technique. for sharing functionality across multiple handlers or multiple MailBots.
A "one-bot function" is a function that takes a single bot
object (an instance of BotRequest) which, itself, contains the request / response state and utilities to alter the request. It is one instance of a bot request, hence, a one-bot function.
One-bot functions are called within handlers, allowing for a top-level handler to compose multiple one-bot functions to get something done.
// sayHi.js
// A simple "one-bot function"
function sayHi(bot) {
bot.webhook.quickReply("hi");
}
module.exports = sayHi;
// in main app.js
const sayHi = require("./sayHi.js");
//...
mailbots.onCommand("hi", function(bot) {
sayHi(bot);
bot.webhook.respond();
});
Skills can also share UI elements.
By convention, UI functions that output UI start with render
. For example, renderMemorizationControls
.
var memorizeSkill = require("mailbots-memorize");
memorizeSkill.activate(mailbot); // activate handlers
mailbot.onCommand("remember", function(bot) {
bot.webhook.sendEmail({
to: "you@email.com"
from: "MailBots",
subject: "Email Subject"
body: [
{
type: 'title',
text: 'A Title'
},
// Render JSON UI
memorizeSkill.renderMemorizationControls(bot)
]
})
bot.webhook.respond();
}
The mailbots
framework relies on Express.js for many of its internals. Your skill can access the internal Express app
object at mailbot.app
, allowing your skill to do anything that can be done with Express: authenticate other services, interact with APIs, respond to webhooks and render web pages.
Just like in handling routes in Express:
mailbot.app.get("/hi", function(req, res) {
res.send("<h1>Hi http request!</h1>");
});
Middlware can be exported and "used" by other skills. This is useful for implementing common functionality across multiple handlers.
// Export middleware to log everything
function logEverythingMiddleware(req, res, next) {
const bot = res.locals.bot; // bot lives here
const emailSubject = bot.get("source.subject");
require("my-great-logger").log(`Got an email about ${emailSubject}`);
next(); // <-- Don't forget this!
}
module.exports = logEverythingMiddleware;
};
// Using our middleware
const logEverythingMiddleware = require("./log-everything");
// Apply to all subsequent handlers
// Note: It does not apply to earlier handlers
mailbot.app.use(logEverythingMiddleware);
mailbot.onCommand("command-one", function(bot) {
// everything is logged
});
mailbot.onCommand("command-two", function(bot) {
// everything is logged
});
mailbot.onCommand("command-three", function(bot) {
// everything is logged
});
To prevent conflicts and facilitate debugging, it is helpful to follow these conventions. For examples below, our skill is skill-name
.
The module name
# Example module name
npm install mailbots-skill-name
Store data against the task or bot in a subject with key of the skill name using underscores instead of dashes.
(task.stored_data = { "skill_name": { "key": "val" } })
Preface event names and actions with the skill name or an abbreviation (accounting for character limitations)
mailbots.onAction("sn.do-action", bot => {});
For clarity and to reduce ambiguity, follow these conventions:
mailbot
(all lowercased) is an acceptable variable name, but...createMailBot()
. Mailbot
(lowercase "b") never exists.bot
is only used for the bot helper object passed to handler functions.Skills can be installed from npm.
Here we will use mailbots-memorize
, a skill that creates reminders for any task using spaced repetition, a memorization technique that increases the time between reminders as more reminders are sent.
In your cli, run:
npm install --save mailbots-memorize
In our app.js file we will create a handler that uses our newly installed skill.
// In main app.js file
var memorizeSkill = require("mailbots-memorize")(mailbot);
mailbot.onCommand("remember", function(bot) {
memorizeSkill.memorizeTask(bot); // ⬅ Tell bot to memorize your task
bot.webhook.quickResponse("Memorizing!");
bot.webhook.respond();
});
// Called each time the reminder is triggered
mailbot.onTrigger("remember", function(bot) {
memorizeSkill.memorizeTask(bot); // ⬅ Invoke skill to continue memorizing
bot.webhook.quickResponse("An email with decreasing frequency");
bot.webhook.respond();
});
Skills that accept the mailbots
object may automatically alter requests, reply to webhooks or take other action automatically behind the scenes.
⚠️ When building sharable skills, use this "automatic" method with caution. Invoking functionality with one line can be both magical and confusingly opaque. The second example below shows an alternative that keeps code more self-documenting and testable.
// May automatically handle requets (ex, render settings pages, send emails)
var handleEverything = require("mailbots-handle-everything");
handleEverything(mailbot);
// or
require("handleEverything")(mailbot);
Alternatively, skills can export components (middleware, one-bot functions, etc) for you to explicitly use in your handlers.
👍 A more self-documenting and testable method.
// These types of skills offer components to your handlers
var {
someMiddleware,
renderSomething
} = require("mailbots-provide-components");
mailbots.app.use(someMiddleware);
mailbots.onCommand("foo", function(bot) {
doSomething(bot);
});
Different approaches work well for different circumstances. For example, a bot analytics system would be well suited for automatic activation. A toolkit for integrating with a CRM, on the other hand, might make sense as an exported collection of useful middleware and functions.
When a new user installs your MailBot, they are directed to a settings page with the welcome
namespace. Render a custom welcome message for your user by creating a settings page that targets this namespace.
mailbot.onSettingsViewed(function(bot) {
const welcomeSettings = bot.webhook.settingsPage({
namespace: "welcome", // MailBots sends new users to this namespace automatically
menuTitle: "Welcome"
});
welcomeSettings.text(`
# Welcome To My MailBot
_Markdown_ works here.`);
)}
Note: While in dev_mode, the MailBot owner is instead redirected to the sandbox.
Your bot receives the mailbot.installed
webhook which can be used to schedule a series of welcome emails to the user.
Connecting a 3rd party system ( CRMs, todo lists, etc) usually requires an access token or similar information from the 3rd party. The easiest way to connect is to ask the user to copy / paste some generated key, token or URL into their settings page. A more friendly user experience is to use OAuth.
Use MailBot's internal Express app to provide begin the OAuth process and receive the OAuth callback. Save information on the user's account using the setMailBotData method (as shown below).
Note: The user's Bearer token is saved as a cookie on your bot's URL when the user first authorizes your MailBot. This allows you to use the MailBots SDK when you send a user to a specific URL. (Keep in mind security XSRF implications if you are doing something sensitve here, like deleting an account). For example:
mailbot.app.get("/do-something-for-user", (req, res) => {
// user's cookies, bearer token, etc are available here
// redirect elsewhere
});
Here is is an example OAuth flow, minus the provider-specific logic:
// 1. start OAuth handshake, set OAuth state, etc
mailbot.app.get("/connect_provider", (req, res) => {
res.redirect(providerRedirectUri);
});
After the user authorizes with the 3rd party service (todo list, repo, CRM, etc) they are redirected to a callback URL, which does most the work, saves the access token, then redirects the user to their settings page with a success message.
// 2. Handle callback after user authorizes on 3rd party site
mailbot.app.get("/provider_callback", async (req, res) => {
// verify state
// exchange auth code for token from provider
// authorize the client SDK and save user's new auth code
const MailBotsClient = require("@mailbots/mailbots-sdk");
const mbClient = new MailBotsClient();
await mbClient.setAccessToken(req.cookies.access_token);
res.redirect(`${res.locals.bot.config.mailbotSettingsUrl}/success`);
});
The work is performed only on your MailBot's url, but to a user, they just click an "Authorize" button on their MailBots settings, connect a 3rd party account and ended up back on their settings page. Service connected, minimal hassle.
Export a testable instance of your MailBot by calling mailbot.exportApp()
instead of calling mailbot.listen()
. Below is an example of testing the exported app with Supertest.
For a sample request (the ./_fixtures/task.created.json
file below), fire a request and go to the sandbox. Click the "copy" icon that appears when you over over the the top-level key the request JSON.
Note: Set NODE_ENV
to testing
to disable webhook validation.
const request = require("supertest");
const mocha = require("mocha");
const expect = require("chai").expect;
const MailBotsApp = require("mailbots");
let mailbot; // re-instantiated before each test
// Utility function to send webhook to our app
function sendWebhook({ exportedApp, webhookJson }) {
return request(exportedApp)
.post("/webhooks")
.set("Accept", "application/json")
.send(webhookJson);
}
describe("integration tests", function() {
beforeEach(function() {
mailbot = new MailBotsApp({ clientId: "foo", clientSecret: "bar" });
});
it("responds correctly to a webhook", async function() {
const mailbot = new MailBotsApp({ clientId: "foo", clientSecret: "bar" });
// set up handlers
mailbot.onCommand("memorize", bot => {
bot.webhook.quickReply("I dig email");
bot.webhook.respond();
});
const exportedApp = mailbot.exportApp();
const webhookJson = require("./_fixtures/task.created.json"); // Copy request from Sandbox
let { body } = await sendWebhook({ exportedApp, webhookJson });
// Test our webhook response
expect(body.send_messages[0].subject).to.equal("I dig email");
});
});
The setup process on mailbots.com creates a pre-installed instance of your MailBot using Glitch.
For local development or production deployments:
1. Install
mkdir my-bot
npm init -y
npm install mailbots
touch app.js
2. Add Setup Code
const MailBot = require("mailbots");
const mailbot = new MailBot({
clientId: "your_client_id",
clientSecret: "your_secret",
mailbotUrl: "http://your_bot_url"
});
// NOTE: The recommended way to set up your MailBot is to set
// CLIENT_ID, CLIENT_SECRET and MAILBOT_URL in env and use dotenv
mailbot.onCommand("hello", bot => {
bot.webhook.quickReply("world");
bot.webhook.respond();
});
mailbot.listen();
Use ngrok to set up a public-facing url that MailBots.com can reach.
Create a MailBot at mailbots.com. Click "Manual Setup" at the bottom and follow instructions from there.
Contributions are welcome in the form of PRs and / or Github tickets for issues, bugs, ideas and feature requests. Please follow the MailBot naming conventions. We use ngrok to mock API requests, included in the test package here. This can be disabled to test against the live API (see package.json).
Note MailBots version 4+ is tightly coupled with FollowUpThen to reflect our (and our user's!) priorities.
FollowUpThen lifecycle hooks (ex: mailbot.onFutTriggerUser
) allow a developer to add value to FollowUpThen (the original, proto-MailBot) by modifying behavior or injecting UI elements at different points in the followup lifecycle.
MailBots still exists a platform to extend FollowUpThen. We may re-release it as an independent system in the future. If you'd like to see that happen, or have any feedback in general, feel free to email help@humans.fut.io.
MailBots is a platform for creating bots, AIs and assistants that get things done right from your inbox. Read more at mailbots.com.
Go to mailbots.com and create a MailBot. The bot creation process sets up a working instance of this framework.
Next, let's tell our MailBot what to do when it receives an email:
var MailBot = require("mailbots");
var mailbot = new MailBot(); // assuming .env is set
// When someone emails: say-hi@my-bot.eml.bot, respond "Hello Human!"
mailbot.onCommand("say-hi", function(bot) {
bot.webhook.quickReply("Hello Human!");
bot.webhook.respond();
});
mailbot.listen();
say-hi@my-bot.eml.bot
is an example of an "email command". Whatever is before the @ sign is a command to your bot to accomplish some task. Read more about email commands.
Tip: Use our reference guide to quickly look up helpers and method names.
A MailBot's purpose is to help someone get something done quickly, efficiently and without leaving their inbox.
A unit of work accomplished by a MailBot is called a "task". Tasks can scheduled, edited or "completed". Tasks are always associated with a command.
MailBots always work in the context of commands while accomplishing tasks. A command can be thought of as the instruction for how to complete a particular task, or the purpose of the task. Email Commands (shown above) are one way to issue commands. Creating a task with the API also requires a command.
A task (that carries out a command) can be created then wait for the perfect moment to notify a user. Timliness is a core feature of of a MailBot.
When events happen in the MailBots platform (ex: an email is received), your MailBot receives webhooks. Your MailBot then gets some useful bit of work done (ex: enters data into a CRM) and responds with JSON that tells the MailBots platform what to do next (ex: send an email, store data, set a reminder, etc). JSON in, JSON out.
This library simplifies much of the above architecture, allowing you to simply create handler functions for certain events.
The webhook request and response can be viewed via the MailBots Sandbox.
MailBots are composed of handlers – functions that run when certain events occur. For example: When the bot receives an email at this address, execute these actions
Handlers and other bot functionality can be packaged into "skills" – high-level abilities that can shared between bots. Here are some example skills:
You can publish, share and install new skills from npm.
Create an email command that says hi.
var MailBotsApp = require("mailbots");
var app = new MailBotsApp();
mailbot.onCommand("hi", function(bot) {
bot.webhook.quickReply("Hi back!");
bot.webhook.respond();
});
mailbot.listen();
The first handler creates the reminder. The second one handles the reminder when it becomes due.
mailbot.onCommand("hi", function(bot) {
// Schedule the task to trigger in 1 minute
bot.webhook.setTriggerTime("1min");
bot.webhook.respond();
});
// When a task with "hi" command triggers, run this
mailbot.onTrigger("hi", function(bot) {
bot.webhook.quickReply("Hi 1 minute later!");
bot.webhook.respond();
});
MailBots can render quick-action buttons (mailto links that map to executable code) that let users get things done without leaving their inbox. Read more about action emails.
// This first handler renders an email with an action-email button
mailbot.onCommand("send-buttons", function(bot) {
bot.webhook.sendEmail({
to: bot.get('source.from')
from: "MyBot",
subject: bot.get('source.subject'),
body: [
// 👇 An email-action
{
type: "button",
behavior: "action",
text: "Press Me",
action: 'say.hi',
subject: "Just hit 'send'",
}
]
});
bot.webhook.respond();
};
// This handler handles the email action
mailbot.onAction('say.hi', function(bot) {
bot.webhook.quickReply('hi');
// Lots of useful things can be done here. Completing a todo item, adding notes to a CRM, etc.
bot.webhook.respond();
});
mailbot.listen();
Let's install a skill to that extracts the email message from previously quoted emails and signatures.
var mailbotsTalon = require("mailbots-talon");
mailbot.onCommand("hi", function(bot) {
const emailWithoutSignature = mailbotsTalon.getEmail(bot);
bot.quickReply(
"Here is the email without the signature:" + emailWithoutSignature
);
bot.webhook.respond();
});
Note: The first matching event handler ends the request, even if subsequent handlers also match, except where otherwise noted.
mailbot.onCommand(command, handlerFn)
Handle when an email command is received. This also fires when a new task is created via the API (All tasks have a command to define their purpose or reason for existence.)
mailbot.onCommand("todo", function(bot) {
//handle when a task is created with this command string
});
Regular expressions work with all handlers.
mailbot.onCommand(/todo.*/, function(bot) {
//Handle todo.me@my-ext.eml.bot, todo.you@my-ext.eml.bot, todo.everyone@my-ext.eml.bot
});
mailbot.onTrigger(command, handlerFn)
Timeliness is a MailBot superpower. A user can schedule a task, then days, months or years later your bot can follow up at the exact right moment – scheduled by the user, or by another event. Read more about triggering.
Tasks with different commands may trigger differently. For example:
mailbot.onTrigger("todo.me", function(bot) {
// Assigned to myself, so only remind me
});
mailbot.onTrigger("todo.assign", function(bot) {
// Assigned to the person in the 'to' field, so remind them
});
mailbot.onTrigger("todo.crm", function(bot) {
// Query CRM API, populate email followup with contact data
});
mailbot.onAction(action, handlerFn)
Handle when a user performs an action that relates to a task.
For example a user can send an action-email to accomplish an action without leaving their inbox (postponing a reminder, completing a todo item, logging a call, etc).
mailbot.onAction("complete", function(bot) {
// Complete the related todo
});
MailBot Conversations
Set the reply-to address of a MailBot email to an action email to carry out a conversation with the user.
mailbot.onAction("assistant", function(bot) {
// Use luis-ai middlweare to dtermine intent
// Store conversation state
// send-email whose reply-to is also "assistant"
});
mailbot.onTaskViewed(command, handlerFn)
Handle when a task is viewed in the MailBots Web UI, allowing a user to view and interact with a future MailBots email.
mailbot.onTaskViewed("todo.me", function(bot) {
// Show a preview of the future email
});
Different task commands may render differently. For example a task with command todo.crm
may query and render current CRM data within the preview.
mailbot.onEvent(event, handlerFn)
Handle when the MailBot receives an inbound webhok from a 3rd party system about an external event. For example, a support ticket is created or a lead is added to a CRM.
Note: This action does not automatically create a MailBots Task. One can be created with mailbots-sdk
mailbot.onEvent("issue.created", async function(bot) {
// Handle event, for example, create a MailBots Task.
const mailBotsClient = MailBotsClient.fromBot(bot);
await mailBotsClient.createTask({
// Pre-authenticated API client
});
bot.webhook.respond({ webhook: { status: "success" } });
});
mailbot.onSettingsViewed(handlerFn)
Handle when a user views this MailBot's settings.
Bots can build custom settings pages. These are rendered when a user views bot settings on the MailBots.com admin UI.
See the settings page reference.
The handler's only parameter is a callback function that responds with JSON to to render a settings form. The callback function is passed the bot
object as usual.
Settings form JSON can be easily built using the settings helper functions.
Unlike the other handlers, every instance of this handler is called. (ie, all settings pages from all settings handlers are rendered).
NOTE: Do not call bot.webhook.respond()
at the end of this particular request. MailBots' internals take care of compiling the JSON and responding.
// Render a settings field for the user to enter their first name
mailbot.onSettingsViewed(async function(bot) {
const todoSettings = bot.webhook.settingsPage({
namespace: "todo",
title: "Todo Settings", // Page title
menuTitle: "Todo" // Name of menu item
});
todoSettings.setUrlParams({ foo: "bar" }); // send a URL param (useful for showing dialogs)
const urlParams = bot.get("url_params"); // retrieve URL params (see below)
todoSettings.input({ name: "first_name", title: "First name" });
todoSettings.buton({ type: "submit" });
// Populate form values
todoSettings.populate(bot.get("mailbot.stored_data.todo"));
// Note bot.webhook.respond() is NOT called
});
URL parameters are passed through to the settings webhook. Use this to pass data into your settings when linking to it or displaying dialogs to the users (see above handler)
mailbot.onSettingsViewed(function(bot) {
const settingsPage = bot.webhook.settingsPage({ namespace: "todo" });
const urlParams = bot.get("url_params", {}); //defualts to empty object
if (urlParams.linkInstructions)) {
settings.text(`# Instructions to link your account!`);
}
// Note that there is no submit button. It's just an informational page.
});
You can also pass URL params via the urlParams
key in the button
form element (it must be type:submit
);
// within a onSettingsViewed form as shown above
settings.button({
submitText: "Save Notification Settings",
type: "submit",
urlParams: { saveSettings: 1 }
// Tip: Pass through all URL current params, but use caution! (see note)
// urlParams: {saveSettings: 1, ...bot.get("url_params", {})}
});
NOTE: URL parameters are an easy way to pass data into your bot settings, but keep this in mind while using URL params: Anyone can link a user to their settings page with anything in URL. Do not, for example, create a url like: /settings?delete_everything=true
that deletes all their tasks. An unsuspecting user may arrive on their settings page from an external link, not see this in the URL and submit the form only find themselves without any data. Read more.
mailbot.onSettingsSubmit(handlerFn)
Handle when the user submits their settings.
To persist the newly saved data, it must be explicitly saved. Use bot.webhook.saveMailBotData()
or
bot.set("mailbot.stored_data")
or API calls to do this.
Newly submitted values are available under settings
.
Previously saved values are available under the mailbot.stored_data
.
Every instance of this handler is called when any settings form is saved (similar to the above onSettingsViewed
handler)
This handler is a good place begin an oauth handshake, set up data in other system or perform API calls to other systems.
mailbot.onSettingsSubmit(bot => {
// assuming the same "todo" namespace as shown in the above examples
const data = bot.get("settings.todo");
// handler is fired any times settings are saved, even if its not our form
if (!data) return;
// perform API calls, validate connections, update external systems here
// validate or modify data, then save it.
bot.set("mailbot.store_data.todo", data);
// This error would show to the user
if (error) {
bot.webhook.respond({
webhook: {
status: "error",
message: "This is a warning message"
}
});
}
return;
});
URL params are useful for passing data into settings handlers, showing dialogs and more. We tried to preserve the mental model of URL parameters while working with settings forms, but but it does not always apply exactly.
In the onSettingsSubmit handler, you need to pass url_params
parameters through the webhoook response:
// in onSettingsSubmit handler
bot.set("url_params", { show_success_dialog: "true" });
This is now accessible as a "url_param" in your onSettingsViewed
handler. You will also see it as a URL
param in the settings UI:
// in the onSettingsViewed handler, render a dialog with a button that dismisses the dialog
// const settingsForm set up earlier
const urlParams = bot.get("url_params", {});
if (urlParams.show_success_dialog) {
settingsForm.text("# Success \nYour todo list has been set up!");
settingsForm.button({ type: "submit", text: "dismiss", urlParams({ dismissSuccess: true }) });
};
You can also set URL parameters using the button
element which can trigger different actions based on the URL
(Note the CSR caution above).
// back ino onSettingsSubmit handler.
if (bot.get("url_params.dissmissSuccess")) {
bot.saveMailBotData("todo.show_success_dialog", null); // set to null to clear the value
}
Setting URL parameters in the onSettingsViewed
hook requires a different method:
// onSettingsViewed
// Force the page to have specific URL params
settingsPage.setUrlParams({ key: "value" });
// Force the settings page to have no URL params
settingsPage.setUrlParams({});
mailbot.on(webhookEvent, handlerFn)
This is a generic handler for any webhook. It can be used to handle any inbound webhook – mainly ones that are not covered by the handlers above. (Of note: The handlers above are simply wrappers for this lower-level handler).
Example:
mailbot.on("mailbot.installed", function(bot) {
// Handle when a MailBot is installed
// Create task with MailBots SDK
// bot.webhook.respond();
});
The first paramater can be:
type
. (ie. mailbot.installed
)type
The second parameter is the function handler that runs only if the matching condition (the first parameter) is met.
mailbot.on("mailbot.installed", function(bot) {
// Handle when a MailBot is installed
// Create a task from mailbots sdk
// bot.webhook.respond();
});
Ideally, error conditions are handled within the normal application flow. If something unexpected happens you can set up a custom error handler for adding logs, alerting or providing error messaging to users.
If you want to send the user your own error email, use the sendEmail method from the MailBots SDK. (Not all webhook responses send emails).
// define a custom error handler
app.setErrorHandler(function(error, bot) {
// myCustomLogger.log(error);
// send email with mailbots sdk
// send a custom error email to user
// Respond with 5xx status code to send the generic MailBots error email to the user
// bot.response.status(500);
bot.webhook.respond({
status: "error",
message: "A custom error message" // Shown to user if in the web UI.
}); // A webhook response must be sent
});
The below handlers are fired in response to FollowUpThen lifecycle events. Their response signature differs from the native MailBots handlers above since it is injecting UI elements and behavioral changes into the followup cycle.
The response style for all FollowUpThen Lifecycle Hook handlers is shown in onFutCreateUser below.
Available options for the response are described in the ISkillReturnValue
interface within types.ts
Modify the FollowUpThen user's confirmation email or modify the task object when a followup is created.
mailbot.onFutCreateUser(bot => {
bod.webhook.addFutUiBlocks([
{
type: "text",
text: "Text block"
}
]);
});
// using native JSON response with TypeScript
mailbot.onFutCreateUser(bot => {
const ISkillReturnValue: response = {
futUiAddition: [
{
type: "text",
text: "Text block"
}
]
};
bot.responseJson = response;
return;
});
In rare cases (currently only when a task (-t) type followup is being created, a non-FUT user receives an email which can be modified using the above hook.
Render of UI elements into the preview email. (Shown when when a FUT user clicks "preview"
to see what a reminder format will do). These elements are usually identical to the ones shown in the
onFutViewUser
and onFutTriggerUser
hooks.
If a preview will trigger the a followup to a non-FUT user (ie, when used in "cc"), this allows allows for a preview of what this will look like.
Render UI elements when a user is viewing a followup.
If a FUT has emails sent to non-users, use this to render UI elements for only the non-user email.
Render UI elements that are only visible to the FollowUpThen user when a followup becomes due.
Render UI elements that are only for the non-user when a followup becomes due (if the followup format has a non-user email component).
Take action when a task is edited. This may involve creating, removing or unlinking a linked resource.
The UI elements above may trigger email-based actions (fired from email, or from the FUT UI). This handler allows for the handling of these actions.
The bot
object passed to the handlers above contains useful helpers that make it easy to handle bot requests. See webhook helpers reference docs.
Setting Data Works By Shallow Merging Data is set by shallow merging. For example.
bot.webhook.setTaskData("my_namespace", { name: "Joe" });
bot.webhook.setTaskData("my_namespace", { key: "123" });
// task data is now
console.log(bot.webhook.responseJson);
// {my_namespace: { name: "Joe", key: "123" }}
bot.webhook.setTaskData("my_namespace", {
name: "Joe",
data: { value: "here" } // ⚠️ Overwritten (shallow merge)
});
bot.webhook.setTaskData("my_namespace", {
data: { value2: "there" }
});
// task data is now
console.log(bot.webhook.responseJson);
// {my_namespace: { data: { value2: "there" } }}
"Skills" are sharable pieces of bot functionality. Skills can encapsulate everything they need (handlers, settings panels helper funcitons and UI elements) so they are ready for use installed. They are great for keeping your code organized and for sharing functionality with others.
We could better organize our handlers in the above examples by grouping them into different files. For example:
// my-new-reminder-skill.js
module.exports.activate = function(mailbot) {
// Handlers can go here
// mailbot.onCommand...
};
Activate the skill's handlers like this (normally done in the top-level to ensure the skill is activated only once):
// In top-level app.js
const myNewReminderSkill = require("./my-new-skill")(mailbot);
myNewReminderSkill.activate(mailbot);
Isolating your handlers within skills is a great way to keep your project organized. The down-side is that the skill owns each request from beginning to end – not very flexible.
The next sections cover more granular and composable technique. for sharing functionality across multiple handlers or multiple MailBots.
A "one-bot function" is a function that takes a single bot
object (an instance of BotRequest) which, itself, contains the request / response state and utilities to alter the request. It is one instance of a bot request, hence, a one-bot function.
One-bot functions are called within handlers, allowing for a top-level handler to compose multiple one-bot functions to get something done.
// sayHi.js
// A simple "one-bot function"
function sayHi(bot) {
bot.webhook.quickReply("hi");
}
module.exports = sayHi;
// in main app.js
const sayHi = require("./sayHi.js");
//...
mailbots.onCommand("hi", function(bot) {
sayHi(bot);
bot.webhook.respond();
});
Skills can also share UI elements.
By convention, UI functions that output UI start with render
. For example, renderMemorizationControls
.
var memorizeSkill = require("mailbots-memorize");
memorizeSkill.activate(mailbot); // activate handlers
mailbot.onCommand("remember", function(bot) {
bot.webhook.sendEmail({
to: "you@email.com"
from: "MailBots",
subject: "Email Subject"
body: [
{
type: 'title',
text: 'A Title'
},
// Render JSON UI
memorizeSkill.renderMemorizationControls(bot)
]
})
bot.webhook.respond();
}
The mailbots
framework relies on Express.js for many of its internals. Your skill can access the internal Express app
object at mailbot.app
, allowing your skill to do anything that can be done with Express: authenticate other services, interact with APIs, respond to webhooks and render web pages.
Just like in handling routes in Express:
mailbot.app.get("/hi", function(req, res) {
res.send("<h1>Hi http request!</h1>");
});
Middlware can be exported and "used" by other skills. This is useful for implementing common functionality across multiple handlers.
// Export middleware to log everything
function logEverythingMiddleware(req, res, next) {
const bot = res.locals.bot; // bot lives here
const emailSubject = bot.get("source.subject");
require("my-great-logger").log(`Got an email about ${emailSubject}`);
next(); // <-- Don't forget this!
}
module.exports = logEverythingMiddleware;
};
// Using our middleware
const logEverythingMiddleware = require("./log-everything");
// Apply to all subsequent handlers
// Note: It does not apply to earlier handlers
mailbot.app.use(logEverythingMiddleware);
mailbot.onCommand("command-one", function(bot) {
// everything is logged
});
mailbot.onCommand("command-two", function(bot) {
// everything is logged
});
mailbot.onCommand("command-three", function(bot) {
// everything is logged
});
To prevent conflicts and facilitate debugging, it is helpful to follow these conventions. For examples below, our skill is skill-name
.
The module name
# Example module name
npm install mailbots-skill-name
Store data against the task or bot in a subject with key of the skill name using underscores instead of dashes.
(task.stored_data = { "skill_name": { "key": "val" } })
Preface event names and actions with the skill name or an abbreviation (accounting for character limitations)
mailbots.onAction("sn.do-action", bot => {});
For clarity and to reduce ambiguity, follow these conventions:
mailbot
(all lowercased) is an acceptable variable name, but...createMailBot()
. Mailbot
(lowercase "b") never exists.bot
is only used for the bot helper object passed to handler functions.Skills can be installed from npm.
Here we will use mailbots-memorize
, a skill that creates reminders for any task using spaced repetition, a memorization technique that increases the time between reminders as more reminders are sent.
In your cli, run:
npm install --save mailbots-memorize
In our app.js file we will create a handler that uses our newly installed skill.
// In main app.js file
var memorizeSkill = require("mailbots-memorize")(mailbot);
mailbot.onCommand("remember", function(bot) {
memorizeSkill.memorizeTask(bot); // ⬅ Tell bot to memorize your task
bot.webhook.quickResponse("Memorizing!");
bot.webhook.respond();
});
// Called each time the reminder is triggered
mailbot.onTrigger("remember", function(bot) {
memorizeSkill.memorizeTask(bot); // ⬅ Invoke skill to continue memorizing
bot.webhook.quickResponse("An email with decreasing frequency");
bot.webhook.respond();
});
Skills that accept the mailbots
object may automatically alter requests, reply to webhooks or take other action automatically behind the scenes.
⚠️ When building sharable skills, use this "automatic" method with caution. Invoking functionality with one line can be both magical and confusingly opaque. The second example below shows an alternative that keeps code more self-documenting and testable.
// May automatically handle requets (ex, render settings pages, send emails)
var handleEverything = require("mailbots-handle-everything");
handleEverything(mailbot);
// or
require("handleEverything")(mailbot);
Alternatively, skills can export components (middleware, one-bot functions, etc) for you to explicitly use in your handlers.
👍 A more self-documenting and testable method.
// These types of skills offer components to your handlers
var {
someMiddleware,
renderSomething
} = require("mailbots-provide-components");
mailbots.app.use(someMiddleware);
mailbots.onCommand("foo", function(bot) {
doSomething(bot);
});
Different approaches work well for different circumstances. For example, a bot analytics system would be well suited for automatic activation. A toolkit for integrating with a CRM, on the other hand, might make sense as an exported collection of useful middleware and functions.
When a new user installs your MailBot, they are directed to a settings page with the welcome
namespace. Render a custom welcome message for your user by creating a settings page that targets this namespace.
mailbot.onSettingsViewed(function(bot) {
const welcomeSettings = bot.webhook.settingsPage({
namespace: "welcome", // MailBots sends new users to this namespace automatically
menuTitle: "Welcome"
});
welcomeSettings.text(`
# Welcome To My MailBot
_Markdown_ works here.`);
)}
Note: While in dev_mode, the MailBot owner is instead redirected to the sandbox.
Your bot receives the mailbot.installed
webhook which can be used to schedule a series of welcome emails to the user.
Connecting a 3rd party system ( CRMs, todo lists, etc) usually requires an access token or similar information from the 3rd party. The easiest way to connect is to ask the user to copy / paste some generated key, token or URL into their settings page. A more friendly user experience is to use OAuth.
Use MailBot's internal Express app to provide begin the OAuth process and receive the OAuth callback. Save information on the user's account using the setMailBotData method (as shown below).
Note: The user's Bearer token is saved as a cookie on your bot's URL when the user first authorizes your MailBot. This allows you to use the MailBots SDK when you send a user to a specific URL. (Keep in mind security XSRF implications if you are doing something sensitve here, like deleting an account). For example:
mailbot.app.get("/do-something-for-user", (req, res) => {
// user's cookies, bearer token, etc are available here
// redirect elsewhere
});
Here is is an example OAuth flow, minus the provider-specific logic:
// 1. start OAuth handshake, set OAuth state, etc
mailbot.app.get("/connect_provider", (req, res) => {
res.redirect(providerRedirectUri);
});
After the user authorizes with the 3rd party service (todo list, repo, CRM, etc) they are redirected to a callback URL, which does most the work, saves the access token, then redirects the user to their settings page with a success message.
// 2. Handle callback after user authorizes on 3rd party site
mailbot.app.get("/provider_callback", async (req, res) => {
// verify state
// exchange auth code for token from provider
// authorize the client SDK and save user's new auth code
const MailBotsClient = require("@mailbots/mailbots-sdk");
const mbClient = new MailBotsClient();
await mbClient.setAccessToken(req.cookies.access_token);
res.redirect(`${res.locals.bot.config.mailbotSettingsUrl}/success`);
});
The work is performed only on your MailBot's url, but to a user, they just click an "Authorize" button on their MailBots settings, connect a 3rd party account and ended up back on their settings page. Service connected, minimal hassle.
Export a testable instance of your MailBot by calling mailbot.exportApp()
instead of calling mailbot.listen()
. Below is an example of testing the exported app with Supertest.
For a sample request (the ./_fixtures/task.created.json
file below), fire a request and go to the sandbox. Click the "copy" icon that appears when you over over the the top-level key the request JSON.
Note: Set NODE_ENV
to testing
to disable webhook validation.
const request = require("supertest");
const mocha = require("mocha");
const expect = require("chai").expect;
const MailBotsApp = require("mailbots");
let mailbot; // re-instantiated before each test
// Utility function to send webhook to our app
function sendWebhook({ exportedApp, webhookJson }) {
return request(exportedApp)
.post("/webhooks")
.set("Accept", "application/json")
.send(webhookJson);
}
describe("integration tests", function() {
beforeEach(function() {
mailbot = new MailBotsApp({ clientId: "foo", clientSecret: "bar" });
});
it("responds correctly to a webhook", async function() {
const mailbot = new MailBotsApp({ clientId: "foo", clientSecret: "bar" });
// set up handlers
mailbot.onCommand("memorize", bot => {
bot.webhook.quickReply("I dig email");
bot.webhook.respond();
});
const exportedApp = mailbot.exportApp();
const webhookJson = require("./_fixtures/task.created.json"); // Copy request from Sandbox
let { body } = await sendWebhook({ exportedApp, webhookJson });
// Test our webhook response
expect(body.send_messages[0].subject).to.equal("I dig email");
});
});
The setup process on mailbots.com creates a pre-installed instance of your MailBot using Glitch.
For local development or production deployments:
1. Install
mkdir my-bot
npm init -y
npm install mailbots
touch app.js
2. Add Setup Code
const MailBot = require("mailbots");
const mailbot = new MailBot({
clientId: "your_client_id",
clientSecret: "your_secret",
mailbotUrl: "http://your_bot_url"
});
// NOTE: The recommended way to set up your MailBot is to set
// CLIENT_ID, CLIENT_SECRET and MAILBOT_URL in env and use dotenv
mailbot.onCommand("hello", bot => {
bot.webhook.quickReply("world");
bot.webhook.respond();
});
mailbot.listen();
Use ngrok to set up a public-facing url that MailBots.com can reach.
Create a MailBot at mailbots.com. Click "Manual Setup" at the bottom and follow instructions from there.
Contributions are welcome in the form of PRs and / or Github tickets for issues, bugs, ideas and feature requests. Please follow the MailBot naming conventions. We use ngrok to mock API requests, included in the test package here. This can be disabled to test against the live API (see package.json).
"Bot" is the helpful object that are given to event handlers. It contains information about the request and lots helpful methods to complete the request.
Bots have utilities to handle webhooks WebHookHelpers
mailbot.onCommand(function(bot) { // <-- This is the "bot"
bot.webhook.quickReply("hi");
bot.webhook.respond();
});
One of the webhook helpers lets you create a settings form. This is its own class: SettingsPage
const mySettingsPage = bot.webhook.settingsPage({
namespace: "todo",
title: "ToDo Settings",
menuTitle: "To Do"
});
"Bot" is the helpful object that are given to event handlers. It contains information about the request and lots helpful methods to complete the request.
Bots have utilities to handle webhooks WebHookHelpers
mailbot.onCommand(function(bot) { // <-- This is the "bot"
bot.webhook.quickReply("hi");
bot.webhook.respond();
});
One of the webhook helpers lets you create a settings form. This is its own class: SettingsPage
const mySettingsPage = bot.webhook.settingsPage({
namespace: "todo",
title: "ToDo Settings",
menuTitle: "To Do"
});
A MailBot is an instance of the MailBots class. This class exposes handlers that are used to respond to specific events. (See readme.md for more).
var MailBots = require("mailbots");
var mailbot = new MailBots({
clientId: "foo",
clientSecret: "bar"
});
mailbot.onCommand("hi", function(bot) {
bot.webhook.quickReply("Hi back!");
bot.webhook.respond();
});
mailbot.listen();
A MailBot is an instance of the MailBots class. This class exposes handlers that are used to respond to specific events. (See readme.md for more).
var MailBots = require("mailbots");
var mailbot = new MailBots({
clientId: "foo",
clientSecret: "bar"
});
mailbot.onCommand("hi", function(bot) {
bot.webhook.quickReply("Hi back!");
bot.webhook.respond();
});
mailbot.listen();
Class constructor.
(any)
Loads final skills and start http server. Anything posted to /webhooks route is automatically handled. Other routes must be created as usual with the Express App object. For example: mailbots.app.get("my-route/", (req, res) => {});
Export app for automated testing
Load MailBots skills from a directory, non-recursively. This can be called more than once to load skills in order. Skills loaded this method are preceeded by loadFirstCoreSkills, succeeded by loadLastCoreSkills. This can also receive a path to a file.
Add handler for a given webhook event. Executes only the first handler for a matching event. The first parameter can be either a string (the named webhook event) or a function that is passed the webhook and returns true or false to indicate if the handler should be run. Example: controller.on('task.created', (bot, req, res) => { }); Example: controller.on((webhook) => webhook.event === 'task.created', cb)
Captures only 'task.created' events where the command string matches
Captures only 'task.triggered' events where the command string matches
Captures only 'mailbot.event_received' events Note: This "Event" refers to the 3rd party webhooks that are posted to the MailBot.
Captures only 'task.action_received' events where the action string matches
Captures only 'task.viewed' events where the command string matches
((string | RexExp))
(any)
Handle webhook that fires when user is viewing MailBot. ALL onSettingsViewed handlers fire when this webhook arrives. Each handler can add and read data to and from its own namespace.
(function)
Callback function that receives the bot object
Handle webhook that fires after a user hits "save" on their MailBot settings. Newly saved settings arrive at the top-level settings object. Existing settings are still in mailbot.stored_data. Return mailbot and user data to save data (as with other webhooks). ALL onSettingsSubmit handlers fire.
(function)
Callback function that receives the bot object
Sets an authenticated insteance of MailBots SDK client https://github.com/mailbots/mailbots-sdk-js for use in events and middleware under bot.api. Works both with webhook + web request
(any)
(any)
(any)
The 'bot' object passed into the handlers comes with a number of helpers to handle the request.
NOTE: This class is automatically instantiated with an instance of BotRequest and made available under bot.webhook.
(object)
An instnace of BotRequest
bot.webhook.getMailBotData('memorize.settings');
Get the current value of a key. If a value has already been been set on the responseJson object, it return that value, otherwise it returns the value in the request object.
(string)
JSON path to key
(mixed)
Default if key is falsy
(boolean)
Only get values that are set on the responseJson object
bot.webhook.get('source.from'); // sender's email address
bot.get('source.from'); // this method is also aliased directly on bot
Set attributes on the this.responseJson object. If existing value and new value are both objects (not Arrays) they are shallow-merged. If new or old data are not objects, the value of the key is replaced entirely.
(string)
JSON Path to object within responseJson object
(any)
Value
(boolean)
Objets are shallow-merged by default. Set this to
false to force the replacement of an object.
bot.set('task.reference_email.subject', "New Subject!");
bot.set('task.reference_email', { subject: "New Subject!"}); // Same effect as above
Get data for current task
(string)
JSON Path to key within task.stored_data
(any)
If there is no key, return this
Set data for current task Takes either an object or a lodash _.set path as the first param, optionally a value as the second parameter. Objects are shallow-merged if new and old data are objects. (Arrays and other types are replaced).
(...any)
setTaskData('my_bot.magic_number', 42);
Get data stored in mailbot.private_data
(string)
JSON Path to data from mailbot
(any)
If value is undefined, use this value instead
bot.webhook.getMailBotData('my_bot.setting', 42);
Set data stored in mailbot.private_data. Objects are shallow merged if existing data is present. All other values (including arrays) are replaced.
This method has two signatures. It can take an object as its only param which will be shallowly merged into mailbot.private_data.
It can also take a lodash _.set path as its first parameter and a value as the second.
(...any)
(any?)
When passing lodash set path string as first
(any?)
When passing lodash set path string as first
param, this specifies if the data should be shallowly merged or replaced (defaults to true).
bot.webhook.setMailBotData({key: "value"});
bot.webhook.setMailBotData('foo.bar', "value");
Get the reference email – the abstract, user-editable instance of the email associated with this task. Note this is not the same as the source email (which is the exact email received).
bot.webhook.getReferenceEmail();
Set reference email Shallowly merges refernece email fields, allowing for easy partial updates
(object)
Name | Description |
---|---|
referenceEmail.to array
|
Array of email address strings |
referenceEmail.cc array
|
Array of email address strings |
referenceEmail.bcc array
|
Array of email address strings |
referenceEmail.subject string
|
Email subject |
referenceEmail.reply_to string
|
Email address or action-email |
referenceEmail.html string
|
html HTML content of email |
referenceEmail.text string
|
html text-only content of email |
bot.webhook.setReferenceEmail({
to: "test@gmail.com",
subject: "A new subject",
html: "This new content replaces the old"
});
Get replyto email, taking into this value being edited after the original email was sent.
string
:
bot.webhook.getReplyTo();
Determine if the email command for the current task was placed in the 'to', 'cc' or 'bcc' fields. This is done by comparing the email command with the email address in the and 'to', 'cc' fields. The 'bcc' is hidden from the message envelope but still present in the command.
Note that if the identical email command is in both 'to', 'cc' and 'bcc' it will only show up as the 'to' method.
Sends an email by adding an email message object to the "send_messages" array. Multiple emails can be sent.
(object)
email object
Name | Description |
---|---|
email.to string
|
comma separated emails |
email.cc string
|
comma separated emails |
email.bcc string
|
comma separated emails |
email.from string
|
from name only (message-envelope is always from mailbots) |
email.reply_to string
|
email address or action-email |
email.subject string
|
email subject |
email.body array
|
Array of ui objects https://docs.mailbots.com/docs/email-ui-reference |
object
:
A reference to the email object for additional changes
const email = bot.webhook.sendEmail({
to: bot.get('source.from'),
subject: "A Subject",
body: [
{
type: 'html',
text: '<h1>An email</h1>'
}];
})
email.from = "New Sender"; // still updatable
Shorthand method to send a quick reply back to the "from" address
of the incoming email. This accepts either a string or object.
If passing a strong only, the subject and body share the same
text. Pass and object iwth {subject, body}
to explicitly set
the subject and body
bot.webhook.quickReply("Got it!");
botRequest.webhook.quickReply({
subject: "Quick reply subject",
body: [{
type: "title",
text: "Welcome",
},
{
type: "button",
behavior: "url",
text: "Press Me",
url: "google.com"
}]
});
Set trigger time for a task using natural language
(string)
FollowUpThen-style time description
bot.webhook.setTriggerTime("monday");
bot.webhook.setTriggerTime("every2ndWeds");
bot.webhook.setTriggerTime("everyTuesday2pm");
Set trigger time for a task using a unix timestamp
(int)
Unix timestamp of trigger time
bot.webhook.setTriggerTimestamp(1546139899);
Trigger MailBots to invite the given email address(es) to use a bot
(array)
Array of email addresses
bot.webhook.invite(['user1@email.com','newUser2@gmail.com']);
Mark task a completed. The task will be archived until permenantly deleted.
bot.webhook.completeTask();
By default, MailBots creates a task for every command that is visible to the user in their Tasks list. When this is not desireable (for example, a simple one-time email command) you can use this method to immediately discard the task.
bot.webhook.discardTask();
Fetch all contacts associated with the task's reference_email. User's address and MailBots addresses are excluded.
Note: This uses reference_email, allowing a user to add or remove additional recipients from the task.
array
:
Email addresses
bot.webhook.getAllContacts();
Respond to (and end) the webhook response with the JSON provided. Multi-fire handlers (like onSettingsViewed) automatically respond. In the case where an error-condition causes an early return / response calling this method prevents the handler from trying to return a second time. Provided JSON is shallowly merged into response object.
(object)
Response JSON
bot.webhook.respond();
Creates a new settings page, rendered in the WebhookHelpers Settings section of the Admin UI. See SettingsPage docs for details
const.settingsPage = bot.webhook.settingsPage({
namespace: "mem",
title: "Memorization Settings",
menuTitle: "Memorization"
});
Get bot request source request object. Usually this is an email. It can also be an API request.
IWebhookSourceEmail
:
const source = bot.webhook.getSource()
Get information about the event that triggered this webhook.
IWebhookTrigger
:
const trigger = bot.webhook.getTrigger()
Get data about the user for which the webhook was posted.
IWebhookUser
:
const user = bot.webhook.getUser()
Get task data associated with the webhook.
IWebhookTask
:
const task = bot.webhook.getTask()
Get mailbot data sent over with each webhook.
IWebHookMailBot
:
const mailbot = bot.webhook.getMailBot()
Set webhook status message. If the user is on the web ui, this message flashes ont eh screen. It also appears in the extension logs.
(Object)
Name | Description |
---|---|
$0.level any
(default "info" )
|
|
$0.message any
|
void
:
setWebhookStatus({message: "Something worked!"});
Add FUT Email UI Block Respond with a partial UI element to be injected into an email response. the response schema of responseJson is different than the other webhooks. It is sent via the Core API directly back to the fut mailbot, which merges it into its own response webhook response
(any)
In the taskUpdated method, this method is used to check if this skill has been marked for removal so cleanup options can be optionally performed. It must reply
(any)
data_namespace of this mailbot
(any)
Helper for a FUT skill to add a new search key. Idempotent.
(any)
Helper for FUT Skill to Remove search keys from a task. Merging is taken care of at next level.
(any)
Check if current task has a search key
(any)
Get this mailbot's settings URL
Get this mailbot's settings URL
Reference for bot.webhook.settingsPage()
Bots and bot skills can create settings settings pages which live
in responseJson.settings[namespace]
. This class represents one
such settings form. Each settings form lives in its own namespace.
Instantiating a new SettingsPage adds a new namespace to
responseJson.settings
key and populates it with React JSON Form
Schema JSON. https://github.com/mozilla-services/react-jsonschema-form
The MailBots Admin UI loops through namespaces, rendering forms appropriately.
NOTE: This class directly mutates MailBot's response JSON to add settings pages and form fields.
const mySettingsPage = bot.webhook.settingsPage({
namespace: "memorize",
title: "Memorization Settings",
menuTitle: "Memorization"
});
// Add elements to the page
mySettingsPage.input({ name: "first_name"}); // renders text input
Retrieve the JSON Form Schema for this form
object
:
JSON Form Schema
const thisJsonSchema = settingsForm.getSettingsFormJSON();
Low-level function to insert raw JSONSchema and uiSchema to create custom form elements. Mozilla's React JSON Schema Form manages the form JSON and UI JSON as two separate objects to keep the form JSON prestine and compliant. This method lets us manage a form element in one place. Ref: https://github.com/mozilla-services/react-jsonschema-form
(object)
Name | Description |
---|---|
params.name object
|
Namespace |
params.JSONSchema object
|
JSON Schema |
params.uiSchema object
|
Corresponding uiSchema
This example is taken from https://mozilla-services.github.io/react-jsonschema-form/ |
formPage.insert({
name: "first_name",
JSONSchema: { type: "string", title: "First name" }, // JSON Schema
uiSchema: { "ui:autofocus": true, "ui:emptyValue": "" } // UI Schema
});
Add text input field
formPage.input({
name: "first_name",
title:"First Name"
description: "Type your first name",
helpText: "Hopefully you don't need help with this",
placeholder: "(Ex: Bruce Lee)",
defaultValue: "John Doe",
});
Add textarea input
formPage.textarea({
name: "life_story",
title:"Life Story",
description: "This is a long story",
helpText: "Just start writing",
placeholder: "Once upon a time...",
defaultValue: "I was born, some time passed, now I'm here"
});
Add checkbox field
formPage.checkbox({ name:"confirmation_emails", title:"Confirmation Emails"});
Add select dropdown
settingsPage.select({
name: "favorite_color",
title: "Favorite Color",
description: "Which color do you like the best?",
helpText: "Choose a color",
options: ["red", "green", "blue"],
defaultValue: "blue",
placeholder: "Pick one!"
});
Adds a custom dialog at the top of the display form that can be used for interrupt-messaging.
formPage.alert({
title: "Connect",
text: "Connect GitHub",
buttons: [{ text: "Submit", type: "Submit "}]
})
Insert Video. Only YouTube supported for now.
formPage.video({
url: "https://www.youtube.com/watch?v=y1GyXuU2J5k",
type: "youtube"
})
Insert custom button
formPage.button({
text: "Google"
href: "https://www.google.com"
});
Set URL params in the onSettingsSubmit webhook and onSettingsViewed hook (ex: ?success=true to show a success dialog) This provides for an accurate mental model, but internally jumps through a number of hoops to get this URL to appear in the admin UI. NOTE: To get current URL params, use bot.get("url_params")
(any)
(any)
key value url params
Add a text block. Markdown supported!
(string)
– Text with optional markdown
Note: This field is whitespace sensitive. New lines cannot have leading spaces.
formPage.text(`--------------
## ️⚠️ Connect Github
This is a text block here. Leading spaces can break this.
And this is a new line.
[Connect Github](http://www.google.com)
------------------
`);
Render a hidden input field. Useful for submitting values behind the scenes.
formPage.hiddenInput({
name: "key",
value: "value"
});
Render a submit button for the form. (Submit button is not included automatically). URL params can optionally be passed which makes them available to the next handler. Make sure to validate URL input.
formPage.submitButton({
submitText: "Save Settings",
urlParams: { key: "value" }
});
Populate form data, overwriting default values with those newly passed. MailBots JSON form data for this namespace can be passed directly to this method to populate the form values.
(object)
JSON object containing form values.
const storedData = mailbots.webhook.getMailBotData("mem", {});
formPage.populate(storedData);