Software API

All functionality of the Spam Experts Local Cloud setup is exposed via the API. A full help listing all available commands can be accessed from the Control Panel by going to Server > Software API Calls or via the api_help call using the following URL:

https://SERVERNAME/cgi-bin/api?call=api_help.

The API is accessible via HTTPS and can be accessed using URL calls. For example, from PHP this can be done using libcurl, it’s also to execute a call from any browser. The commands executed on the Software API are automatically executed on all relevant servers in your Local Cloud setup, so there is no need for you to individually configure the servers. These commands should not be executed from the SSH command line on the SpamExperts system, they can be ran from any external location.

Be advised that the API described on this page (Software API) is only available on our Local Cloud product. For the Hosted Cloud/Control Panel API please refer to the Control Panel API documentation.

Example calls

To add a domain to the filtering cluster, you can execute the add_incoming_domain() call from a web browser, or directly from a script:

https://servername/cgi-bin/api?call=api_add_incoming_domain&domain=demo-domain.invalid&destination=mail.demo-domain.invalid

As it can be seen it’s very easy to integrate the adding/removing of domains (and all other available features) in any existing provisioning system or control panel.

Default settings

As Local Cloud administrator you can control the default settings for all domains.

To modify the default domain settings, specify “default” as domain. If the default value is changed, then it will affect any domains that are still set to using the default values. If a custom value has been previously set for a domain, then that will override the default, even if the custom value is the same as the default. To change a domain setting back to use the default value, the special value ‘default’ should be used as argument.

Mind the difference between Domain Default and Value Default. Domain Default changes the setting for all domains using the default values, whereas Value Default changes the setting for a specific domain back to use the “default value”.

When retrieving from the API the value of a setting that has been changed and it’s not default, a warning will be printed to indicate that the setting value is not the default one. The warning will also include the default value for that setting. This allows the caller to distinguish between "use the default" and the same value as the default.

Sample PHP script

<?php
/**
* SpamExperts — Incoming Domain Management (example integration)
*
* Lists, adds, and removes incoming domains via the Software API
* (/cgi-bin/api). Host it on a web server that can reach your cluster.
*
* DISCLAIMER: This is an illustrative example of calling the Software API.
* It is not a turnkey product. Before any real deployment, review and harden
* it for your environment — serve it over HTTPS, place it behind proper
* authentication, and protect state-changing requests against CSRF.
*/
			
// ---- config ----
// Target SpamExperts API host. Edit per deployment.
define("SERVER",   "api.example.com");
define("VERIFY_TLS", true);
// -----------------
			
// The visitor must authenticate; an anonymous visitor is prompted to log in.
if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
    header('WWW-Authenticate: Basic realm="Domain Management"');
    header('HTTP/1.1 401 Unauthorized');
    echo 'Authentication required.';
    exit;
}
define("API_USER", $_SERVER['PHP_AUTH_USER']);
define("API_PASS", $_SERVER['PHP_AUTH_PW']);
			
// Per-session CSRF token: required on every state-changing POST.
session_start();
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
			
/**
* Call the remote API over a verified TLS connection.
*
* @return string|false  Response body, or false on failure.
*/
function api_call(array $params)
{
    $context = stream_context_create([
	"http" => [
	    "method"        => "GET",
	    "header"        => "Authorization: Basic " . base64_encode(API_USER . ":" . API_PASS) . "\r\n",
	    "ignore_errors" => true,
	    "timeout"       => 15,
	],
	"ssl" => [
	    "verify_peer"      => VERIFY_TLS,
	    "verify_peer_name" => VERIFY_TLS,
	],
    ]);
			
    $url = "https://" . SERVER . "/cgi-bin/api?" . http_build_query($params);

    $response = @file_get_contents($url, false, $context);
    if (false === $response) {
	error_log("API request failed: " . $url);
	return false;
    }
    return $response;
}
			
/**
* Validate a hostname before it is sent to the API or echoed back.
*/
function is_valid_domain($domain)
{
    return (bool) preg_match(
	'/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i',
	$domain
    );
}
			
// Handle add / remove
if ('POST' === $_SERVER['REQUEST_METHOD'] && !empty($_POST['action'])) {
    if (empty($_POST['csrf_token'])
	|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
	http_response_code(403);
	exit('Invalid CSRF token.');
    }
			
    switch ($_POST['action']) {
	case 'add':
	    if (!empty($_POST['domain']) && !empty($_POST['destination'])
		&& is_valid_domain($_POST['domain'])) {
		api_call([
		    "call"        => "api_add_incoming_domain",
		    "domain"      => $_POST['domain'],
		    "destination" => $_POST['destination'],
		]);
	    }
	    break;
			
	case 'remove':
	    if (!empty($_POST['domain']) && is_valid_domain($_POST['domain'])) {
		api_call([
		    "call"   => "api_remove_domain",
		    "domain" => $_POST['domain'],
		]);
	    }
	    break;
    }
			
    // Redirect to a fixed path so a refresh won't resubmit.
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
    exit(0);
}
			
// Fetch and parse the current domain list.
$domains = array();
$domains_string = api_call(["call" => "api_list_domains"]);
			
if (false !== $domains_string) {
    $lines = array_filter(explode("\n", $domains_string));
    foreach ($lines as $line) {
	if (false === strpos($line, ':')) {
	    continue; // skip malformed lines
	}
			
	list($domain, $route_string) = explode(':', $line, 2);

	// A trailing "*" marks an alias of the previous domain.
	if ('*' === substr($domain, -1)) {
	    end($domains);
	    $lastDomain = key($domains);
	    if (null === $lastDomain) {
		continue;
	    }
	    $domains[$lastDomain]['aliases'][] = rtrim($domain, '*');
	    continue;
	}
			
	$routes = array_filter(explode(',', $route_string));
	foreach ($routes as $r => $destination) {
	    $domains[$domain]['routes'][$r] = str_replace('::', ':', $destination);
	}
    }
}

$csrf = htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8');
?>

<h2>Add new domain</h2>
<form method="post">
    <input type="hidden" name="action" value="add">
    <input type="hidden" name="csrf_token" value="<?php echo $csrf; ?>">
    Domain: <input type="text" name="domain">
    Route: <input type="text" name="destination">
    <input type="submit" value="Add">
</form>

<h2>View existing domains</h2>
<table>
    <tr>
	<th>Domain</th>
	<th>Aliases</th>
	<th>Routes</th>
	<th>Action</th>
    </tr>
    <?php if (!empty($domains)) : ?>
	<?php foreach ($domains as $domain => $domainRelatedItems) : ?>
	    <?php $domainEsc = htmlspecialchars($domain, ENT_QUOTES, 'UTF-8'); ?>
	    <tr>
		<td><?php echo $domainEsc; ?></td>
		<td><?php echo !empty($domainRelatedItems['aliases']) ? htmlspecialchars(implode(', ', $domainRelatedItems['aliases']), ENT_QUOTES, 'UTF-8') : ''; ?></td>
		<td><?php echo !empty($domainRelatedItems['routes'])  ? htmlspecialchars(implode(', ', $domainRelatedItems['routes']),  ENT_QUOTES, 'UTF-8') : ''; ?></td>
		<td>
		    <form method="post" onsubmit="return confirm('Remove <?php echo $domainEsc; ?>?');">
			<input type="hidden" name="action" value="remove">
			<input type="hidden" name="csrf_token" value="<?php echo $csrf; ?>">
			<input type="hidden" name="domain" value="<?php echo $domainEsc; ?>">
			<input type="submit" value="remove">
		    </form>
		</td>
	    </tr>
	<?php endforeach; ?>
    <?php else : ?>
	<tr><td colspan="4">No domains are setup</td></tr>
    <?php endif; ?>
</table>

Sample Python script

DATA HOSTED BY PASTEBIN.COM - Download raw - See original

"""SpamExperts -- Delivery Domain Management (example integration).

Lists, adds, and removes delivery domains via the Software API (/cgi-bin/api).

DISCLAIMER: This is an illustrative example of calling the Software API. It is 
not a turnkey product. Before any real deployment, review and harden it for 
your environment -- serve it over HTTPS (behind a production WSGI server, not 
the Flask dev server), place it behind proper authentication, and 
protect state-changing requests against CSRF.

Configure via environment variables:
    API_SERVER    -- API host, e.g. "api.example.com"
    API_USER      -- API username
    API_PASSWORD  -- API password
    SECRET_KEY    -- (optional) Flask session secret; a random one is used if unset
    VERIFY_TLS    -- (optional) "0" to disable TLS verification (not recommended)
"""
			
import os
import json

import requests
import flask


APP = flask.Flask(__name__)
# Stable secret from the environment; fall back to a random per-process key.
APP.secret_key = os.environ.get("SECRET_KEY") or os.urandom(32)

TEMPLATE = """<html><body>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flashes>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}

<form method="post" action="">
<fieldset>
<legend><strong>Add new domain</strong></legend>
<input name="action" type="hidden" value="add" />
<label style="float:left;width:100px;text-align:right;">Domain: </label><input name="domain" type="text" /><br />
<label style="float:left;width:100px;text-align:right;">Route: </label><input name="destination" type="text" /><br />
<input style="margin-left:100px;" type="submit" value="Add" />
</fieldset>
</form>

<h3>View existing domains</h3>

<table border="1" cellspacing="0" cellpadding="2">
<tr>
<th>Domain</th>
<th>Aliases</th>
<th>Routes</th>
<th>Action</th>
</tr>
{% if domains %}
{% for info in domains %}
<tr>
<td>{{ info.domain }}</td>
<td>{% for alias in info.aliases %}{{ alias }}<br />{% endfor %}</td>
<td>{% for route in info.destinations %}{{ route }}<br />{% endfor %}</td>
<td><form method="post" action="" onsubmit="return confirm('Remove ' + {{ info.domain|tojson }} + '?');"><input name="action" type="hidden" value="remove" /><input name="domain" type="hidden" value="{{ info.domain }}" /><input type="submit" value="remove" /></form></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">No domains are set up.</td>
</tr>
{% endif %}
</table></body></html>
"""

def api_get(call, **params):
    """Call the Software API: let requests handle URL encoding."""
    verify = os.environ.get("VERIFY_TLS", "1") != "0"
    if not verify:
	requests.packages.urllib3.disable_warnings()
    auth = (os.environ["API_USER"], os.environ["API_PASSWORD"]
    url = "https://%s/cgi-bin/api" % os.environ["API_SERVER"]
    query = {"call": call}
    query.update(params)
    return requests.get(url, params=query, auth=auth, verify=verify, timeout=15)

@APP.route("/", methods=['GET', 'POST'])
def view():
    if flask.request.method == "POST":
	action = flask.request.form["action"]
	domain = flask.request.form["domain"]

	if action == "add" and domain and flask.request.form.get("destination"):
	    destinations = []
	    for destination in flask.request.form["destination"].split(","):
		destination = destination.strip()
		if ":" in destination:
		    host, _, port = destination.rpartition(":")
		    destinations.append([host, inst(port) if port.isdigit() els port])
		else:
		    destinations.append([destination, 25])
	    data = json.dumps({domain: {"destinations": destinations}})
	    response = api_get("api_add_delivery_domain", data=data)
	    if response.ok:
		flask.flash("Added %s" % domain, "info")
	    else:
		flask.flash("An error occurred.", "error")
	elif action == "remove" and domain:
	    response = api_get("api_remove_domain", domain=domain)
	    if response.ok:
		flask.flash("Removed %s" % domain, "info")
	    else:
		flask.flash("An error occurred.", "error")
	return flask.redirect(flask.url_for("view"))

    response = api_get("api_list_domains", format="json")
    try:
	domains = response.json()
    except ValueError:
	domains = []
	flask.flash("Could not load the domain list.", "error")
    return flask.render_template_string(TEMPLATE, domains=domains)

if __name__ == "__main__":
    APP.run()

Disclaimer: This documentation may contain references to third party software or websites. N-able has no control over third party software or content and is not responsible for the availability, security, or operation, of any third-party software. If you decide to utilize a release involving third-party software, you do so entirely at your own risk and subject to the applicable third party’s terms and conditions of the use of such software. No information obtained by you from N-able or this documentation shall create any warranty for such software.