DEV Community

Cover image for Will Your Marketing Email End Up in Spam? We Built a Tool to Find Out
Leapcell
Leapcell

Posted on

Will Your Marketing Email End Up in Spam? We Built a Tool to Find Out

When running email marketing campaigns, one of the biggest challenges is ensuring that your messages reach the inbox rather than the spam folder.

In this post, we’ll build a tool that can validate if your email will be marked as spam and why it's marked so.
This tool would be in the API form and deployed online, so that it can be integrated into your workflow.

The Secret Behind Spam Validating

Apache SpamAssassin is an open-source spam detection platform maintained by the Apache Software Foundation, which is a widely used tool for many email clients and email filtering tools to classify messages as spam.

It uses a multitude of rules, Bayesian filtering, and network tests to assign a spam “score” to a given email. Generally, an email scoring 5 or above is at high risk of being flagged as spam.

Since that Apache SpamAssassin is a spam detection software, it can also be used to tell if your email would be flagged as spam.

SpamAssassin’s scoring is transparent and well-documented, you can confidently use it to identify exactly which aspects of your email are causing high spam scores and improve your writing.

How to Validate Your Email Using SpamAssassin

SpamAssassin is designed to run on Linux systems. You'll need a Linux OS or create a Docker container to install and run it.

On Debian or Ubuntu systems, install SpamAssassin with:

apt-get update && apt-get install -y spamassassin
sa-update
Enter fullscreen mode Exit fullscreen mode

The sa-update command ensures that SpamAssassin’s rules are up-to-date.

Once installed, you can pipe an email message into SpamAssassin’s command-line tool. The output includes an annotated version of the email with spam scores and explains which rules are triggered.

A typical usage might look like this:

spamassassin -t < input_email.txt > results.txt
Enter fullscreen mode Exit fullscreen mode

results.txt will then contain the processed email with SpamAssassin’s headers and scores, like below:

X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on 254.254.254.254
X-Spam-Level: 
X-Spam-Status: No, score=0.2 required=5.0 tests=HTML_MESSAGE,
    MIME_HTML_ONLY,MISSING_MID,NO_RECEIVED,
    NO_RELAYS autolearn=no autolearn_force=no version=4.0.0

// ...

Content analysis details:   (0.2 points, 5.0 required)

 pts rule name              description
---- ---------------------- --------------------------------------------------
 0.1 MISSING_MID            Missing Message-Id: header
-0.0 NO_RECEIVED            Informational: message has no Received headers
-0.0 NO_RELAYS              Informational: message was not relayed via SMTP
 0.0 HTML_MESSAGE           BODY: HTML included in message
 0.1 MIME_HTML_ONLY         BODY: Message only has text/html MIME parts
Enter fullscreen mode Exit fullscreen mode

Wrap SpamAssassin as an API

SpamAssassin achieves its maximum potential only when encapsulated as an API, as this form makes it more flexible and allows integration into various workflows.

Imagine this: before you hit “Send” on your email, the content is first sent to the SpamAssassin API. Only if it determines that the email does not meet spam criteria is it allowed to proceed.

Let’s create a simple API that accepts these email fields: subject, html_body and text_body. It will pass the fields to SpamAssassin and return the validation result.

API Example

from fastapi import FastAPI
from datetime import datetime, timezone
from email.utils import format_datetime
from pydantic import BaseModel
import subprocess

def extract_analysis_details(text):
    lines = text.splitlines()

    start_index = None
    for i, line in enumerate(lines):
        if line.strip().startswith("pts rule"):
            start_index = i
            break

    if start_index is None:
        print("No content analysis details found.")
        return []

    data_lines = lines[start_index+2:]
    parsed_lines = []
    for line in data_lines:
        if line.strip() == "":
            break
        parsed_lines.append(line)

    results = []
    current_entry = None

    split_line = lines[start_index+1]
    pts_split, rule_split, *rest = split_line.strip().split(" ")

    pts_start = 0
    pts_end = pts_start + len(pts_split)

    rule_start = pts_end + 1
    rule_end = rule_start + len(rule_split)

    desc_start = rule_end + 1

    for line in parsed_lines:
        pts_str = line[pts_start:pts_end].strip()
        rule_name_str = line[rule_start:rule_end].strip()
        description_str = line[desc_start:].strip()

        if pts_str == "" and rule_name_str == "" and description_str:
            if current_entry:
                current_entry["description"] += " " + description_str
        else:
            current_entry = {
                "pts": pts_str,
                "rule_name": rule_name_str,
                "description": description_str
            }
            results.append(current_entry)

    return results

app = FastAPI()

class Email(BaseModel):
    subject: str
    html_body: str
    text_body: str

@app.post("/spam_check")
def spam_check(email: Email):
    # assemble the full email
    message = f"""From: example@example.com
To: recipient@example.com
Subject: {email.subject}
Date: {format_datetime(datetime.now(timezone.utc))}
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="__SPAM_ASSASSIN_BOUNDARY__"

--__SPAM_ASSASSIN_BOUNDARY__
Content-Type: text/plain; charset="utf-8"

{email.text_body}

--__SPAM_ASSASSIN_BOUNDARY__
Content-Type: text/html; charset="utf-8"

{email.html_body}

--__SPAM_ASSASSIN_BOUNDARY__--"""

    # Run SpamAssassin and capture the output directly
    output = subprocess.run(["spamassassin", "-t"],
                            input=message.encode('utf-8'),
                            capture_output=True)

    output_str = output.stdout.decode('utf-8', errors='replace')
    details = extract_analysis_details(output_str)
    return {"result": details}
Enter fullscreen mode Exit fullscreen mode

In the code above, we defined a helper function, extract_analysis_details, to extract only the scoring reasons from the full result report. You can further improve this function, for example, by filtering out certain rules from the result.

The response will contain the analysis details of SpamAssassin’s results.

Let's take this input as an example:

subject

Claim Your Prize
Enter fullscreen mode Exit fullscreen mode

html_body

<h2>Claim Your Prize</h2>
<p>Dear Winner:</p>
<p>Click the link below to claim your prize.</p>
Enter fullscreen mode Exit fullscreen mode

text_body

Claim Your Prize

Dear Winner:
Click the link below to claim your prize.
Enter fullscreen mode Exit fullscreen mode

The response would be like this:

[
  {
    "pts": "0.1",
    "rule_name": "MISSING_MID",
    "description": "Missing Message-Id: header"
  },
  {
    "pts": "-0.0",
    "rule_name": "NO_RECEIVED",
    "description": "Informational: message has no Received headers"
  },
  {
    "pts": "3.1",
    "rule_name": "DEAR_WINNER",
    "description": "BODY: Spam with generic salutation of \"dear winner\""
  },
  {
    "pts": "-0.0",
    "rule_name": "NO_RELAYS",
    "description": "Informational: message was not relayed via SMTP"
  },
  {
    "pts": "0.0",
    "rule_name": "HTML_MESSAGE",
    "description": "BODY: HTML included in message"
  }
]
Enter fullscreen mode Exit fullscreen mode

See? "Dear winner" is detected as it is commonly used in spam emails.

Deploying the API Online

Running SpamAssassin requires a Linux environment with the software installed. Traditionally, you might need an EC2 instance or a DigitalOcean droplet to deploy, which can be costly and tedious, especially if your usage is low-volume.

As for serverless platforms, they just don't let you install any system packages like SpamAssassin.

Leapcell can handle this job perfectly.

With Leapcell, you can deploy any system packages like SpamAssassin, meanwhile keep the service serverless - you only pay for invocations, which is usually cheaper.

Deploying the API on Leapcell is very easy. You don't have to set up any environment. Just deploy a Python image, and fill in the "Build Command" field properly.

Deployment configs

Once deployed, you’ll have an api for spam validating. Whenever the API is invoked, it will run SpamAssassin, score the email, and return the score.

Leapcell

Read on our blog

Top comments (0)