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.
Narrow the search
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.
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 categorytag_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
, you can use a wildcard filter on the Getting started
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;
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.Extract codes and links from the email
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.
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)
}