Article featured image
Email testing in PHP
13min readLast updated: May 28, 2025

PHP is one of the most popular server-side scripting languages out there, and for good reason. It's versatile, easy to use, and integrates seamlessly with databases like MySQL, making it a top choice for building dynamic websites. Plus, since it's open-source, PHP gives you the flexibility and cost-effectiveness to tackle projects of any size.

When it comes to email communication, it's a critical feature for many web applications. From user sign-ups and password resets to notifications and marketing campaigns, emails play a big role in many web apps. That’s where Testmail come in handy - it helps you test your email workflows to make sure everything’s running smoothly before they land in your customers’ inboxes.

In this tutorial, we’ll walk you through how to receive emails in your Testmail inbox and use the Testmail API to query your inbox—all using PHP. Let’s get into it!

Prerequisites

PHP installation: Make sure PHP is installed on your local server or hosting environment. You can download the latest version from PHP's official website or use a development environment like XAMPP, WAMP, or Laragon, which come with PHP pre-installed and configured for ease of use.

testmail.app account: Sign up for a free testmail.app account and get your API key and namespace. This allows you to use testmail.app’s API to query and test emails programmatically.

You will also need a text editor or IDE to write and manage your code.

Set up your project

Create a new directory for your project: Open your terminal or command prompt and run the following commands to create and navigate to a new directory. In this example, we’re using email-tests as the directory name.

mkdir email-tests
cd email-tests

Initialize a new Composer project. If you don’t have Composer installed, you can download it here. Follow the prompts, or press Enter to use the default settings.

composer init

Testmail provides two APIs: a straightforward JSON API and a more advanced GraphQL API. For this guide, we’ll focus on the GraphQL API (for more details, see: Choosing an API).

To interact with the GraphQL API, you’ll need a dedicated GraphQL client (recommended) or a general HTTP client. In this tutorial, we’ll use the GraphQL API along with the Softonic/graphql-client library, but feel free to choose another client if you prefer.

composer require softonic/graphql-client

You can also use any other GraphQL client of your choice. You can find a list of alternative GraphQL clients here.

Setting up the GraphQL client in PHP

Now that you’ve set up your Composer project and installed the required library, you can proceed with the PHP setup. This is where you’ll connect to the Testmail GraphQL API using your API key.

<?php
// Autoload Composer packages
require __DIR__ . '/vendor/autoload.php';

// Import the GraphQL client library
use Softonic\GraphQL\ClientBuilder;

// Replace 'YOUR_APIKEY' with your API key
$APIKEY = 'YOUR_APIKEY';  

// Set up the GraphQL client with your API endpoint and authorization header
$client = ClientBuilder::build(
  'https://api.testmail.app/api/graphql',  // Testmail GraphQL endpoint
  [
    'headers' => ['Authorization' => "Bearer $APIKEY"]  // Authorization header with API key
  ]
);

Send a test email

You can trigger an email using a custom function, like clicking a signup button, or simply send an email through your preferred email client (e.g., Gmail, Outlook) to your testmail.app address: {namespace}.{tag}@inbox.testmail.app.

Here’s how it works:

  • Namespace: Think of your namespace as a collection of mailboxes. Each namespace is unique to you and can hold an unlimited number of email addresses. You can find your unique namespace in your Testmail console.
  • Tag: The tag is entirely up to you. By using different tags, you can create new email addresses and mailboxes instantly.

For example, let’s say your namespace is acmeinc. If you send a test email to [email protected] and another to [email protected], both emails will go to the acmeinc namespace.

Find your email

Now that we have an email to test, let’s search for it using the subject line. The query we'll use is the inbox query, which retrieves the emails from a specified namespace. It accepts one required argument: the namespace. Along with the email details, the query will return result (either success or fail), and message (providing an explanation in case of failure).

Create a new file, e.g, check_email.php inside the email-tests folder. Let's query the inbox for the email by subject.

<?php
require __DIR__ . '/vendor/autoload.php';

use Softonic\GraphQL\ClientBuilder;

$APIKEY = 'YOUR_APIKEY'; // Replace with your API key
$NAMESPACE = 'YOUR_NAMESPACE'; // Replace with your namespace
$SUBJECT = 'Testmail PHP Integration'; // Replace with the subject you're looking for

$client = ClientBuilder::build(
    'https://api.testmail.app/api/graphql',
    ['headers' => ['Authorization' => "Bearer $APIKEY"]]
);

// GraphQL query to get emails from the inbox
$query = <<<QUERY
{
  inbox(namespace: "$NAMESPACE") {
    emails {
      subject
    }
  }
}
QUERY;

$response = $client->query($query);
$emails = $response->getData()['inbox']['emails'];

foreach ($emails as $email) {
    if ($email['subject'] === $SUBJECT) {
        echo "Email with subject '$SUBJECT' found.\n";
        exit;
    }
}

echo "Email with subject '$SUBJECT' not found.\n";

check_email.php

Run the script:

php check_email.php

The output should indicate whether the email with the specified subject was found in the inbox.

Using a "Live" Query

When you're expecting to receive an email, instead of repeatedly querying the inbox in a loop, you can use a live query. A live query waits until at least one email matches the conditions before returning. This is more efficient than polling your inbox repeatedly.

For instance, your query could be adjusted to something like this:

$query = <<<'QUERY'
query GetMails {
  inbox (
      namespace:"YOUR_NAMESPACE"
      livequery:true
  ) {
    result
    message
    emails {
      subject
    }
  }
}
QUERY;

When you send a live query, the API starts waiting for new emails that match your query. As soon as a matching email arrives, the API returns the result immediately. If no email is found within a minute, the API sends an HTTP 307 redirect, prompting your client to resend the same query. This process repeats indefinitely, so it's important to set a timeout in your testing suite to prevent the query from running forever.

The GraphQL API has excellent options for sorting and filtering emails. The complete GraphQL API reference is available in the GraphQL playground (see the schema and docs popovers on the right).

You can customize the inbox query based on several parameters to filter and sort your emails in various ways. The table below explains each parameter, along with a description and example usage.

Advanced Filters help you focus your search by specific email details, and Advanced Sorts let you organize the results based on different fields.

Example: The script below filters emails by specifying a namespace and a tag prefix, like "peter," to include only emails that start with the given tag. It also applies an advanced filter to find emails with an exact subject, such as "Please confirm your email." The results are then sorted in two stages: first by tag in ascending alphabetical order, and then by timestamp in descending order.

$query = <<<QUERY
{
inbox (
    namespace:"YOUR_NAMESPACE" 
    tag_prefix:"peter" 
    advanced_filters:[{
      field:subject
      match:exact
      action:include
      value:"Please confirm your email"
    }]
    advanced_sorts:[{
      field:tag,
      order:asc
    }, {
      field:timestamp,
      order:desc
    }]
  ) {
    result
    message
    count
    emails {
      tag
      timestamp
    }
  }
}
QUERY;

$variables = [];

$response = $client->query($query, $variables);
$r =  $response->getData();
echo "<pre>".json_encode($r, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."</pre>";

?>

To use a wildcard query, you can set the match field to wildcard. This allows you to match patterns in fields, such as the subject or HTML body, using the * (matches any number of characters) and ? (matches a single character) wildcards.

💡
When using wildcard queries, use the timestamp_from, timestamp_to, and tag or tag_prefix options first to reduce the size of the set of potential matches; otherwise wildcard queries can take a long time!

Note that the timestamp_from, timestamp_to, tag, and tag_prefix options are the most efficient ways to filter emails. Tags are indexed using n-grams - so tags and subsets of tags can be queried incredibly efficiently. Here is a typical usage pattern that takes advantage of this:

Send emails to: [email protected] (where the tag is category.subcategory), and then query for emails using the tag_prefix:

  • tag_prefix:"category" will return all emails that fall within the category
  • tag_prefix:"category.subcategory" will return all emails that fall within the category and subcategory
  • (and so on)

Nesting tags like this allows you to group/categorize inboxes, reduce conflicts, and use fewer advanced_filters (which are less performant).

Emails are sorted by timestamp in descending order (newest first) by default, but you can modify this with advanced_sorts for more control.

If you're performing multiple tests or working in parallel CI pipelines, ensure your filters are specific enough. Without proper filtering, the live query may return any new email, which could lead to unexpected results. For instance, if you're testing for an email containing

Getting started

, you can use a wildcard filter on the html field and query only the count of matched emails, avoiding the need to download the full content.

Check for spam issues

Generally, you don’t have to worry about spam filters if:

  • Your emails are genuinely relevant and not spammy.
  • You have correctly configured DKIM and SPF.
  • Your domain, email servers, and IPs have good reputations (they are not on blacklists).

Testmail uses SpamAssassin, the popular open-source anti-spam platform, to test emails for spam. The spam scores and reports generated by SpamAssassin are not universal, as they depend on the specific rule sets and plugins enabled for each mailbox provider. Different providers may configure their installations with unique rule sets and thresholds, meaning the classification of spam can vary from one provider to another.

You can also check if the email passes Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) checks. SPF and DKIM checks help make sure an email is actually from who it says it’s from. SPF verifies it came from an approved server, while DKIM checks if the email’s content was changed. These checks help you avoid fake or scam emails.

$query = <<<QUERY
{
  inbox (
    namespace: "YOUR_NAMESPACE"
    tag_prefix: "YOUR_TAG"
    spam_report: true  # Requesting spam report for the emails
  ) {
    result
    message
    count
    emails {
      tag
      spam_score  # Including spam score
      spam_report  # Including spam report details
      spf  # SPF authentication status
      dkim  # DKIM authentication status
    }
  }
}
QUERY;

TL;DR

  • Don’t worry about spam scores below 5.
  • If the spam score exceeds 5: look at which rules were triggered in the spam_report and fix the problems with the greatest impact.
  • Negative spam scores are possible (a good thing).

SPF and DKIM
You can also check if the email passes Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) checks. spf and dkim are essential for verifying the authenticity of an email and ensuring it’s really from the sender it claims to be.

SPF is a mechanism that ensures the email came from an approved server, making it harder for attackers to send emails on behalf of someone else. On the other hand, DKIM focuses on the email content itself. It verifies that the email hasn't been tampered with in transit by adding a digital signature to the header. This signature can be checked against a public key to ensure the content is unchanged.

Test the text and HTML content of the email

Testing text and HTML content in emails is crucial for ensuring the accuracy and relevance of your messaging. By using advanced filters to search for specific keywords or patterns within the body content, you can verify that emails contain the right information and meet your testing criteria.

You can filter emails by text content using the text field with wildcard match (* and ?) for fuzzy matching or exact for precise matches.

$query = <<<QUERY
{
  inbox(
    namespace: "YOUR_NAMESPACE"
    tag: "YOUR_TAG"
    advanced_filters: [
      {
        field: text
        match: wildcard
        action: include
        value: "*specific keyword*"
      }
    ]
    limit: 10
    offset: 0
  ) {
    result
    message
    count
    emails {
      id
      namespace
      tag
      timestamp
      subject
      from
      text  # Access the text content here
    }
  }
}
QUERY;
💡
If your email body (text or html) exceeds 30KB, it won’t be indexed for advanced search, so you won’t be able to use it in filters for such large emails. If you need full text search over large emails, please contact us.

Testing links and codes in emails is essential for a seamless user experience. You want to be sure that links take them to the right place and that codes are valid - otherwise, you risk frustrating users or causing errors.

Once you have the text or HTML content of an email, you can extract specific patterns, such as codes or links, using regular expressions (regex). Codes are often numerical sequences of fixed length (e.g., 6-digit or 8-character codes) and can be extracted using a regex pattern.

<?php
require __DIR__ . '/vendor/autoload.php';

use Softonic\GraphQL\ClientBuilder;

$APIKEY = 'YOUR_APIKEY';
$NAMESPACE = 'YOUR_NAMESPACE';
$TAG = 'YOUR_TAG';

$client = ClientBuilder::build(
    'https://api.testmail.app/api/graphql',
    ['headers' => ['Authorization' => "Bearer $APIKEY"]]
);

$query = <<<QUERY
{
  inbox(
    namespace: "$NAMESPACE"
    tag: "$TAG"
  ) {
    emails {
      subject
      text
    }
  }
}
QUERY;

$response = $client->query($query);
$emails = $response->getData()['inbox']['emails'];

foreach ($emails as $email) {
    echo "Subject: " . $email['subject'] . "\n";

    // Extract 6-digit code
    if (preg_match('/\b\d{6}\b/', $email['text'], $matches)) {
        echo "Code found: " . $matches[0] . "\n";
    } else {
        echo "No code found.\n";
    }
}
?>

Here's an example to find all the links in emails and ensure that they are working properly. It extracts the links from the email content and checks their HTTP status using curl

<?php
require __DIR__ . '/vendor/autoload.php';

use Softonic\GraphQL\ClientBuilder;

$APIKEY = 'YOUR_APIKEY';
$NAMESPACE = 'YOUR_NAMESPACE';
$TAG = 'YOUR_TAG';

$client = ClientBuilder::build(
    'https://api.testmail.app/api/graphql',
    ['headers' => ['Authorization' => "Bearer $APIKEY"]]
);

$query = <<<QUERY
{
  inbox(
    namespace: "$NAMESPACE"
    tag: "$TAG"
  ) {
    emails {
      subject
      text  # Access the text content here
    }
  }
}
QUERY;

$response = $client->query($query);
$emails = $response->getData()['inbox']['emails'];

// Function to check if a URL is reachable
function checkUrl($url) {
    $curl = curl_init($url);
    curl_setopt($curl, CURLOPT_NOBODY, true);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($curl, CURLOPT_TIMEOUT, 10);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

    curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    return $httpCode >= 200 && $httpCode < 400; // 2xx and 3xx are considered valid
}

foreach ($emails as $email) {
    echo "Email Subject: " . $email['subject'] . "\n";
    echo "Email Text Content: " . $email['text'] . "\n";

    // Extract links using a regex pattern
    if (preg_match_all('/https?:\/\/[^\s]+/', $email['text'], $matches)) {
        $links = $matches[0];
        echo "Found Links:\n";
        foreach ($links as $link) {
            echo "- $link\n";

            // Check if the link is working
            if (checkUrl($link)) {
                echo "Link is working\n";
            } else {
                echo "Link is not working\n";
            }
        }
    } else {
        echo "No links found in the email text.\n";
    }
}

When extracting URLs from the content, the regex should be flexible enough to handle various URL formats.

preg_match_all('/https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/', $htmlContent, $matches);

Test attachments

Whether it's verifying the format (like PDFs, images, or documents) or confirming that the file opens without issues, testing attachments helps ensure your emails work seamlessly. This can be especially important for applications like invoicing, newsletters, or any service where attached documents play a key role in communication.

💡
It is recommended to keep the total email message size below 10 MB for best performance. Larger emails will take longer to process, and emails above 20 MB are automatically rejected.

The Attachment object returns all the information available about attachments in the email. You can determine the type of the file (e.g., image, PDF, or document) based on the contentType.

query {
  inbox(namespace: "YOUR_NAMESPACE" tag: "YOUR_TAG") {
    emails {
      attachments {
        filename
        contentType
        checksum
        size
        headers {
          name
          value
        }
        downloadUrl
        contentId
        cid
        related
      }
    }
  }
}

When dealing with email attachments, the MD5 checksum is a useful tool for ensuring the file's integrity. It generates a unique hash for the content, helping you verify whether the file has been altered or corrupted.

The downloadUrl is your go-to for directly accessing the attachment, giving you the ability to easily download it. Meanwhile, the contentId or cid serve as identifiers for embedded images within the email, so you can reference or work with those images as needed.

If the related field is set to true, it indicates that the attachment is embedded content, not a separate file. This means it's part of the email’s resources, like an image that’s directly embedded in the body of the email itself.

Test images and web beacons

If an image fails to load or display properly, it can hurt the email's professionalism and impact. By testing embedded images, you can ensure that they are correctly referenced, appear as intended, and maintain consistent visual quality across different email clients.

Embedded images in emails are often referenced with cid: (Content-ID) URLs. These images are typically inline attachments that are converted into base64 format for embedding in the email. In the HTML body, the cid: URL will be present in src attributes of tags, and these images will be embedded directly into the email.

Extracting cid: Images from the HTML

<?php
require __DIR__ . '/vendor/autoload.php';

use Softonic\GraphQL\ClientBuilder;

$APIKEY = 'YOUR_APIKEY';
$NAMESPACE = 'YOUR_NAMESPACE';
$TAG = 'YOUR_TAG';

$client = ClientBuilder::build(
    'https://api.testmail.app/api/graphql',
    ['headers' => ['Authorization' => "Bearer $APIKEY"]]
);

$query = <<<QUERY
{
  inbox(
    namespace: "$NAMESPACE"
    tag: "$TAG"
  ) {
    emails {
      subject
      html  # Access the HTML content here
    }
  }
}
QUERY;

$response = $client->query($query);
$emails = $response->getData()['inbox']['emails'];

foreach ($emails as $email) {
    echo "Email Subject: " . $email['subject'] . "\n";

    $htmlContent = $email['html'];
    echo "HTML Content:\n$htmlContent\n";

    // Extract all cid: references
    if (preg_match_all('/cid:([a-zA-Z0-9-_]+)/', $htmlContent, $matches)) {
        $cids = $matches[1]; // Extract only the CID values
        echo "Found Content-ID (cid:) References:\n";
        print_r($cids);

        // Handle embedded images (e.g., base64 decoding or further processing)
        foreach ($cids as $cid) {
            echo "Processing embedded image with CID: $cid\n";
            // In real scenarios, you might decode or handle the attached image here.
            // This depends on your email library or how attachments are stored.
        }
    } else {
        echo "No Content-ID references found in the email HTML.\n";
    }
}

Detecting web beacons: Web beacons (also known as tracking pixels) are typically small, invisible images (often 1x1 pixels) that are embedded in emails to track whether the email was opened. These are commonly found in the tags with URLs pointing to tracking servers.

$htmlContent = "<html><body><img src='https://tracking.example.com/pixel?email=abc123' width='1' height='1' style='display:none;'></body></html>";

preg_match_all('/<img[^>]*src="([^"]+)"[^>]*width="1"[^>]*height="1"[^>]*style="display:none;"/', $htmlContent, $matches);  // Match invisible 1x1 pixel images
print_r($matches);  // Array will contain the URLs of web beacons

Verify image source URLs: You can send HTTP requests to the URLs found in the src attributes of tags to ensure they load properly.

$htmlContent = "<html><body><img src='https://example.com/image.jpg' alt='Image'></body></html>";

preg_match_all('/<img[^>]*src="([^"]+)"/', $htmlContent, $matches);  // Match all image URLs

foreach ($matches[1] as $imageUrl) {
    $headers = get_headers($imageUrl);  // Send HTTP request to the image URL
    echo "Checking image: $imageUrl\n";
    echo "Response: " . $headers[0] . "\n";  // Ensure the response is OK (e.g., 200 OK)
}

Subscribe to blog

Stay updated with our latest insights and curated articles delivered straight to your inbox.