The Maileroo Inbound Routing Handbook

Maileroo Inbound Routing
A Guide to Maileroo Inbound Routing

This is it. The moment is finally here.

Inbound Routing is now available on Maileroo. With our Maileroo Inbound Routing service, you can seamlessly receive emails sent to your domain, forward them to other email addresses, or process them as webhooks for automation and integration with your application.

Your First Route

To access Inbound Routing, go to your dashboard. Select your domain, click on Sending → Inbound Routing.

You should be welcomed by a view such as this:

Click on Create New Route.

Once you click on it, a small modal will appear, prompting you to enter various details. Most of these are straightforward, so we'll focus on explaining the less obvious and more intricate aspects here.

Expression Type

This determines how incoming emails are filtered or matched based on criteria like all emails ("Catch All"), specific headers ("Match Header"), recipient addresses ("Match Recipient"), or advanced custom expressions for precise control.

For both "Match Header" and "Match Recipient," you can provide a literal value, wildcard string or a regex. Literal values are compared case-insensitively, while regex offers complete flexibility. When using a wildcard string, you can specify values such as *@gmail.com or maileroo@* to match values. You may even use a pattern such as *@maileroo.com to match any address ending with @maileroo.com, or admin@* to match any address starting with admin@.

Golang Regex

We use Golang RegExp syntax, allowing you to match headers or recipients in emails. For example, the following regular expression matches any string containing the case-insensitive words: Birthday, Anniversary, Celebration, Congratulations, or Event.

(?i)\b(Birthday|Anniversary|Celebration|Congratulations|Event)\b

You can learn more about it at this page.

Custom Expression

What if you wanted to match an email by both recipient and header values? This is where custom expressions come in the scene.

For example:

((match_header("Subject", `(?i)welcome.*maileroo`) || (match_recipient("no.reply@mail.maileroo.com")) && match_header("From", `(?i)@gmail\.com`))

This expression checks if the Subject contains "welcome" followed by "maileroo" (case-insensitive) or if the recipient is "no.reply@mail.maileroo.com" and the sender's email is from a Gmail address.

You can use it to create even more complex expressions such as:

((match_header("From", `(?i)@(gmail\.com|yahoo\.com|outlook\.com)`) && 
match_header("Subject", `(?i)\b(Birthday|Anniversary|Celebration|Congratulations|Event)\b`) && 
match_header("Date", `(?i)2025-\d{2}-\d{2}`)) || 
(match_recipient("hello@mail.maileroo.com") && 
match_header("From", `(?i)@live\.com`)))

This expression checks if the email is either from Gmail, Yahoo, or Outlook with a subject containing specific celebratory terms and a date in 2025, or if the recipient is hello@mail.maileroo.com and the sender is from Live.com.

You can choose to enforce DKIM and SPF on your route based on your preferences. At a minimum, we recommend ensuring that the email is DMARC-aligned.

Webhook Structure & Payload

We send webhooks in HTTP POST with a JSON payload.

This payload contains all essential information such as domain, envelope sender, message ID, and an array of headers.

Headers are represented as an array because multiple headers can have the same name but different values.

Our system will automatically attempt to extract the last message from the email thread (both for plaintext and HTML). This is available in body.stripped_plaintext and body.stripped_html.

To keep things simple, we do not include the attachments inside the payload, but you can loop through attachments and download them directly as we upload them to our S3 compatible storage. This is also true for Raw Mime.

Schema

type EmailEvent struct {
  	Id             string                   `json:"_id"`
	MessageId      string                   `json:"message_id"`
	Domain         string                   `json:"domain"`
	EnvelopeSender string                   `json:"envelope_sender"`
	Recipients     []string                 `json:"recipients"`
	Headers        map[string][]string      `json:"headers"`
	Body           EmailBody                `json:"body"`
	Attachments    []DownloadableAttachment `json:"attachments"`
	SPFResult      spf.Result               `json:"spf_result"`
	DKIMResult     bool                     `json:"dkim_result"`
	IsDMARCAligned bool                     `json:"is_dmarc_aligned"`
	IsSpam         bool                     `json:"is_spam"`
	DeletionUrl    string                   `json:"deletion_url"`
	ValidationUrl  string                   `json:"validation_url"`
	ProcessedAt    int64                    `json:"processed_at"`
}

type DownloadableAttachment struct {
	Filename    string `json:"filename"`
	ContentId   string `json:"content_id"`
	ContentType string `json:"content_type"`
	URL         string `json:"url"`
	Size        int    `json:"size"`
}

type EmailBody struct {
	Plaintext         string      `json:"plaintext"`
	StrippedPlaintext string      `json:"stripped_plaintext"`
	HTML              string      `json:"html"`
	StrippedHTML      string      `json:"stripped_html"`
	OtherParts        []OtherPart `json:"other_parts"`
	RawMime           RawMime     `json:"raw_mime"`
}

type OtherPart struct {
	ContentType string `json:"content_type"`
	Contents    string `json:"contents"`
}

type RawMime struct {
	URL  string `json:"url"`
	Size int    `json:"size"`
}

Sample Payload

{
  "_id": "677730adac1b7a32de362ccd",
  "message_id": "CALx1tRL=zMbjat+bmzdJsLG_67OOvVGJbUxCZNc6F3EagoLfkw@mail.gmail.com",
  "domain": "mail.maileroo.com",
  "envelope_sender": "no.reply@mail.maileroo.com",
  "recipients": [
    "help@mail.maileroo.com"
  ],
  "headers": {
    "Content-Type": [
      "multipart/mixed; boundary=\"000000000000b48c26062a2147cb\""
    ],
    "Date": [
      "Thu, 26 Dec 2024 11:17:56 +1100"
    ],
    "From": [
      "Maileroo <no.reply@mail.maileroo.net>"
    ],
    "In-Reply-To": [
      "<CAPmyPAQhzAi+82KqQ63C=JV+Ke9k_UAHw674Rrx0LWyjX+i2gg@mail.gmail.com>"
    ],
    "Message-Id": [
      "<CALx1tRL=zMbjat+bmzdJsLG_67OOvVGJbUxCZNc6F3EagoLfkw@mail.gmail.com>"
    ],
    "Mime-Version": [
      "1.0"
    ],
    "References": [
      "<CAPmyPAQhzAi+82KqQ63C=JV+Ke9k_UAHw674Rrx0LWyjX+i2gg@mail.gmail.com>"
    ],
    "Subject": [
      "Re: Test Email from Maileroo"
    ],
    "To": [
      "help@mail.maileroo.com"
    ]
  },
  "body": {
    "plaintext": "PLAIN TEXT HERE",
    "stripped_plaintext": "EXTRACTED CONTENT HERE",
    "html": "HTML HERE",
    "stripped_html": "EXTRACTED HTML HERE",
    "other_parts": null,
    "raw_mime": {
      "url": "https://roo-inbound-store.nbg1.your-objectstorage.com/677730adac1b7a32de362ccd/raw.mime?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=PXCGE7SXA8IQG411X833%2F20250103%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250103T003453Z&X-Amz-Expires=259200&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=be4af21d67d9f9be0b51eabecb054f1b7bf3d6b8efef0f74c5aef7a9a9e83322",
      "size": 3248
    }
  },
  "attachments": [
    {
      "filename": "favicon-16x16.webp",
      "content_id": "<f_m54krol90>",
      "content_type": "image/webp",
      "url": "https://roo-inbound-store.nbg1.your-objectstorage.com/677730adac1b7a32de362ccd/favicon-16x16.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=PXCGE7SXA8IQG411X833%2F20250103%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250103T003453Z&X-Amz-Expires=259200&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=376b1f15b27d0990493c9c67e3856e6d16b91ada1f75ae3c2c05b31511425c2d",
      "size": 376
    }
  ],
  "spf_result": "pass",
  "dkim_result": true,
  "is_dmarc_aligned": true,
  "is_spam": false,
  "deletion_url": "https://inbound-api.maileroo.net/email/677730adac1b7a32de362ccd/6df64578a500659d0ecfe163e91b0ab0",
  "validation_url": "https://inbound-api.maileroo.net/validate-callback/677730adac1b7a32de362cce/b71bfca7653deeb8802c87124ba65212",
  "processed_at": 1735864493
}

Validation (Authenticity)

Our webhook system uses a simple yet effective method for verifying the authenticity of incoming requests. Unlike traditional systems that rely on shared secrets or signatures, we provide a straightforward approach.

For every webhook, you are provided with a unique validation_url. To confirm that a webhook request is legitimate, you can make a GET or POST request to this URL. Based on your request, the server will respond with one of the following JSON responses:

For a failed webhook:

{"success":false}

And, for a successful webhook:

{"success":true}

However, once you hit this URL, all subsequent attempts will return as failure. You are only supposed to call this validation_url once.

Delete Emails on Demand

All emails are stored for 72 hours. However, if you wish to delete an email from our storage after you download the raw message and attachments, please send a DELETE request to the deletion_url present in the email. Calling this endpoint will result in all data associated to this message being purged from our databases.

Retry Mechanism

The first callback is sent almost immediately. The webhook retry mechanism will attempt to deliver the webhook payload at the following intervals if a successful HTTP 200 OK response is not received:

  • 5 minutes
  • 10 minutes
  • 15 minutes
  • 30 minutes
  • 1 hour
  • 2 hours
  • 4 hours
  • 6 hours

The system will continue through these intervals sequentially until a 200 OK response is received, signaling successful delivery, or the retries are exhausted.

This approach ensures the payload is reattempted with increasing intervals, giving the receiving system sufficient time to recover from potential issues.

Sample Implementation

If you were to handle a webhook in PHP, this is how you would do it.

function makeHttpRequest($url, $method = "GET") {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    if ($method === "DELETE") {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
    }

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return [
        'response' => $response,
        'httpCode' => $httpCode
    ];
}

// Validate the webhook
function validateWebhook($validationUrl) {
    $result = makeHttpRequest($validationUrl);
    if ($result['httpCode'] === 200) {
        $data = json_decode($result['response'], true);
        if (isset($data['success']) && $data['success'] === true) {
            echo "Webhook validation successful.\n";
            return true;
        }
    }

    echo "Webhook validation failed.\n";
    return false;
}

// Delete email
function deleteEmail($deletionUrl) {
    $result = makeHttpRequest($deletionUrl, "DELETE");
    if ($result['httpCode'] === 200) {
        echo "Email deleted successfully.\n";
    } else {
        echo "Failed to delete email.\n";
    }
}

// Example usage
$payload = json_decode(file_get_contents('php://input'), true);

if (!isset($payload['validation_url'], $payload['deletion_url'])) {
    echo "Invalid payload.\n";
    exit;
}

$validationUrl = $payload['validation_url'];
$deletionUrl = $payload['deletion_url'];

// Validate the webhook
if (validateWebhook($validationUrl)) {
    // Perform any further processing if validation is successful
    echo "Processing email...\n";

    // Delete the email
    deleteEmail($deletionUrl);
}

Use Webhook Tester

We provide a webhook testing tool to simplify debugging. It allows you to easily trigger sample webhooks to any URL of your choice.

Best Practices for Handling Webhooks

  • Secure Your Endpoint: Ensure that the endpoint you expose for receiving webhooks is secure and can handle the anticipated volume of webhook calls.
  • Validate the Webhook: Trigger the validation_url to verify the authenticity of the webhook request. Only allow the webhook to be processed further if the validation is successful.
  • Log and Monitor: Keep logs of the webhook events received for troubleshooting and analysis.
  • Use Webhook.site: Utilize tools like Webhook.site to test and debug your webhook integrations. It allows you to inspect incoming webhook requests, including headers and payloads.

Frequently Asked Questions

Here are some common questions you may have about Maileroo Inbound Routing and how it works.

Do I need to add Maileroo's MX records to my domain?

Yes, for us to handle emails on your behalf, you must add the MX records shown on the DNS records page.

What if I already have MX records?

Well, well.. in that case, you can't really use Maileroo Inbound Routing. This is because you can only use one email provider (with multiple, fallback MX records). However, there's a quick workaround. You can always use a sub-domain such as mail.example.com if your primary domain already has an email service provider.

How long are emails and attachments stored in your servers?

Emails received will be stored in our database for up to three days unless manually deleted.

How do I know if I am receiving any webhooks from Maileroo?

This is easy to debug with services such as Webhook.site, Webhook Tester, Webhook.Cool which allow you to receive and inspect any HTTP payloads sent to their webhook URLs.

Am I charged for multiple routes?

You are only charged once per email irrespective of the number of routes that may handle or process it.

Am I charged for any forwarded emails?

Yes, forwarded emails count towards your plan's outbound email credits. For example, if you forward an email to 15 different recipients, it will use 1 inbound credit and 15 outbound credits.

But why am I being charged for forwarded emails?

Forwarded emails are processed and sent through our email infrastructure just like any other email. Each forwarded email requires resources such as bandwidth, server processing, and outbound delivery, which incur costs.

How many routes can I create per account and domain?

We allow 1024 routes per account and 128 routes per domain. However, this can be adjust for each account. Please feel free to reach out.

How email forwarders and webhooks can I have?

By default, we allow up to 4 email forwarders and 100 webhook handlers per account to prevent misuse and spam forwarding. If you need higher limits, you can easily request an adjustment by opening a support ticket. According to our Zendesk statistics, the median first reply time is 55 minutes, so you wouldn't have to wait too long.

Does my account need to be verified to use Inbound Routing?

Yes and no. If you intend to use our webhook, account verification is not required. However, if you plan to utilize our email forwarding service, you must complete the account verification process. This is necessary to fight email spam.

What is the maximum email size you can handle?

We can handle emails up to 50 MB, including attachments.