diff --git a/mollom.admin.inc b/mollom.admin.inc
index 0d96597..3b806e4 100644
--- a/mollom.admin.inc
+++ b/mollom.admin.inc
@@ -444,12 +444,13 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') {
 
   $form['blacklist'] = array();
   // Do not retrieve the current blacklist when submitting the form.
-  $blacklist = (empty($form_state['input']) ? mollom('mollom.listBlacklistText') : array());
+  $blacklist = (empty($form_state['input']) ? mollom()->getBlacklist() : array());
   if (is_array($blacklist)) {
-    foreach ($blacklist as $id => $entry) {
+    foreach ($blacklist as $entry) {
       if ($entry['reason'] != $type) {
         continue;
       }
+      $id = $entry['id'];
       // #class property is internally used by
       // theme_mollom_admin_blacklist_form().
       $row = array(
@@ -469,13 +470,9 @@ function mollom_admin_blacklist_form($form, &$form_state, $type = 'spam') {
       $row['actions']['delete'] = array(
         '#type' => 'link',
         '#title' => t('delete'),
-        '#href' => 'admin/config/content/mollom/blacklist/delete',
+        '#href' => 'admin/config/content/mollom/blacklist/delete/' . $entry['id'],
         '#options' => array(
-          'query' => array(
-            'text' => $entry['text'],
-            'context' => $entry['context'],
-            'reason' => $entry['reason'],
-          ) + drupal_get_destination(),
+          'query' => drupal_get_destination(),
         ),
       );
       $form['blacklist'][$id] = $row;
@@ -539,7 +536,7 @@ function mollom_admin_blacklist_form_submit($form, &$form_state) {
     'match' => $form_state['values']['entry']['match'],
     'reason' => $form_state['values']['entry']['reason'],
   );
-  $result = mollom('mollom.addBlacklistText', $data);
+  $result = mollom()->createBlacklistEntry($data);
 
   $args = array(
     '@text' => $data['text'],
@@ -547,7 +544,7 @@ function mollom_admin_blacklist_form_submit($form, &$form_state) {
     '@match' => $data['match'],
     '@reason' => $data['reason'],
   );
-  if ($result === TRUE) {
+  if (!empty($result['id'])) {
     drupal_set_message(t('The entry was added to the blacklist.'));
     _mollom_watchdog(array(
       'Added @text (@context, @match) to @reason blacklist.' => $args,
@@ -625,23 +622,16 @@ function theme_mollom_admin_blacklist_form($variables) {
  * @ingroup forms
  * @see mollom_admin_blacklist_delete_submit()
  */
-function mollom_admin_blacklist_delete($form, &$form_state) {
-  $form['text'] = array(
-    '#type' => 'value',
-    '#value' => $_GET['text'],
-  );
-  $form['context'] = array(
+function mollom_admin_blacklist_delete($form, &$form_state, $entryId) {
+  $entry = mollom()->getBlacklistEntry($entryId);
+  $form['entry'] = array(
     '#type' => 'value',
-    '#value' => $_GET['context'],
-  );
-  $form['reason'] = array(
-    '#type' => 'value',
-    '#value' => $_GET['reason'],
+    '#value' => $entry,
   );
 
   return confirm_form(
     $form,
-    t('Are you sure you want to delete %text from the blacklist?', array('%text' => $_GET['text'])),
+    t('Are you sure you want to delete %text from the blacklist?', array('%text' => $entry['text'])),
     'admin/config/content/mollom/blacklist',
     t('This action cannot be undone.'),
     t('Delete'), t('Cancel')
@@ -652,23 +642,18 @@ function mollom_admin_blacklist_delete($form, &$form_state) {
  * Form submit handler to delete an entry from the blacklist.
  */
 function mollom_admin_blacklist_delete_submit($form, &$form_state) {
-  $data = array(
-    'text' => $form_state['values']['text'],
-    'context' => $form_state['values']['context'],
-    'reason' => $form_state['values']['reason'],
-  );
-  $result = mollom('mollom.removeBlacklistText', $data);
+  $result = mollom()->deleteBlacklistEntry($form_state['values']['entry']['id']);
 
   $args = array(
-    '@text' => $data['text'],
-    '@context' => $data['context'],
-    '@reason' => $data['reason'],
+    '@text' => $form_state['values']['entry']['text'],
+    '@context' => $form_state['values']['entry']['context'],
+    '@reason' => $form_state['values']['entry']['reason'],
   );
   if ($result === TRUE) {
     drupal_set_message(t('The entry was removed from the blacklist.'));
     _mollom_watchdog(array(
       'Removed @text (@context) from @reason blacklist.' => $args,
-      'Data:<pre>@data</pre>' => array('@data' => $data),
+      'Data:<pre>@data</pre>' => array('@data' => $form_state['values']['entry']),
       'Result:<pre>@result</pre>' => array('@result' => $result),
     ));
   }
@@ -676,7 +661,7 @@ function mollom_admin_blacklist_delete_submit($form, &$form_state) {
     drupal_set_message(t('An error occurred upon trying to remove the item from the blacklist.'), 'error');
     _mollom_watchdog(array(
       'Failed to removed @text (%context) from @reason blacklist.' => $args,
-      'Data:<pre>@data</pre>' => array('@data' => $data),
+      'Data:<pre>@data</pre>' => array('@data' => $form_state['values']['entry']),
       'Result:<pre>@result</pre>' => array('@result' => $result),
     ), WATCHDOG_ERROR);
   }
@@ -692,6 +677,9 @@ function mollom_admin_blacklist_delete_submit($form, &$form_state) {
  * mollom.verifyKey would invalidate the keys and throw an error; hence,
  * _mollom_fallback() would invoke form_set_error(), effectively preventing this
  * form from submitting.
+ *
+ * @todo Implement proper form validation now that mollom() no longer triggers
+ *   the fallback mode.
  */
 function mollom_admin_settings($form, &$form_state) {
   // Output a positive status message, since users keep on asking whether
diff --git a/mollom.drupal.inc b/mollom.drupal.inc
new file mode 100644
index 0000000..9fe0dc1
--- /dev/null
+++ b/mollom.drupal.inc
@@ -0,0 +1,354 @@
+<?php
+
+/**
+ * @file
+ * Mollom client class for Drupal.
+ */
+
+/**
+ * Drupal Mollom client implementation.
+ */
+class MollomDrupal extends Mollom {
+  /**
+   * Mapping of configuration names to Drupal variables.
+   *
+   * @see Mollom::loadConfiguration()
+   */
+  private $configuration_map = array(
+    'publicKey' => 'mollom_public_key',
+    'privateKey' => 'mollom_private_key',
+    'servers' => 'mollom_servers',
+  );
+
+  /**
+   * Implements Mollom::loadConfiguration().
+   */
+  public function loadConfiguration($name) {
+    $name = $this->configuration_map[$name];
+    return variable_get($name);
+  }
+
+  /**
+   * Implements Mollom::saveConfiguration().
+   */
+  public function saveConfiguration($name, $value) {
+    $name = $this->configuration_map[$name];
+    return variable_set($name, $value);
+  }
+
+  /**
+   * Implements Mollom::deleteConfiguration().
+   */
+  public function deleteConfiguration($name) {
+    $name = $this->configuration_map[$name];
+    return variable_del($name);
+  }
+
+  /**
+   * Implements Mollom::getClientInformation().
+   */
+  public function getClientInformation() {
+    if ($cache = cache_get('mollom_version')) {
+      return $cache->data;
+    }
+
+    // Retrieve Drupal distribution and installation profile information.
+    $profile = drupal_get_profile();
+    $profile_info = system_get_info('module', $profile) + array(
+      'distribution_name' => 'Drupal',
+      'version' => VERSION,
+    );
+
+    // Retrieve Mollom module information.
+    $mollom_info = system_get_info('module', 'mollom');
+    if (empty($mollom_info['version'])) {
+      // Manually build a module version string for repository checkouts.
+      $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev';
+    }
+
+    $data = array(
+      'platformName' => $profile_info['distribution_name'],
+      'platformVersion' => $profile_info['version'],
+      'clientName' => $mollom_info['name'],
+      'clientVersion' => $mollom_info['version'],
+    );
+    cache_set('mollom_version', $data);
+
+    return $data;
+  }
+
+  /**
+   * Overrides Mollom::writeLog().
+   */
+  function writeLog() {
+    $messages = array();
+    foreach ($this->log as $i => $entry) {
+      $entry += array('arguments' => array());
+      $message = array(
+        $entry['message'] => $entry['arguments'],
+      );
+      if (isset($entry['data'])) {
+        $message['Request: @request<pre>@parameters</pre>'] = array(
+          '@request' => $entry['request'],
+          '@parameters' => $entry['data'],
+        );
+      }
+      if (isset($entry['response'])) {
+        $message['Response:<pre>@response</pre>'] = array('@response' => $entry['response']);
+      }
+      $messages[] = $message;
+
+      // Translate log messages for debugging without watchdog.
+      // @todo Temporary?
+      $output = array();
+      foreach ($message as $text => $args) {
+        foreach ($args as &$arg) {
+          if (is_array($arg)) {
+            $arg = var_export($arg, TRUE);
+          }
+        }
+        $output[] = strtr($text, $args);
+      }
+      $this->log[$i]['message'] = implode("\n", $output);
+      unset($this->log[$i]['arguments']);
+//      drupal_set_message(implode('<br />', $output), $this->log[$i]['type']);
+    }
+    _mollom_watchdog_multiple($messages, $this->lastResponseCode === TRUE ? WATCHDOG_DEBUG : WATCHDOG_ERROR);
+
+    // After writing log messages, empty the log.
+//    $this->purgeLog();
+  }
+
+  /**
+   * Overrides Mollom::refreshServers().
+   */
+  public function refreshServers() {
+    $servers = parent::refreshServers();
+
+    // Allow other modules to alter the server list. Internal use only.
+    drupal_alter('mollom_server_list', $servers);
+
+    return $servers;
+  }
+
+  /**
+   * Implements Mollom::request().
+   */
+  protected function request($method, $server, $path, array $data, array $expected = array()) {
+    // Build the REST request path.
+    $path = self::API_VERSION . '/' . $path;
+    $rest_path = $path;
+    $query = NULL;
+    if ($data) {
+      $query = $this->httpBuildQuery($data);
+      if (in_array($method, array('GET', 'HEAD'))) {
+        $rest_path .= '?' . $query;
+      }
+    }
+
+    $request_data = array(
+      'method' => $method,
+      'headers' => array(
+        'Accept' => 'application/xml, application/json;q=0.8, */*;q=0.5',
+      ),
+    );
+    if (in_array($method, array('POST', 'PUT'))) {
+      $request_data['data'] = $query;
+      $request_data['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
+    }
+
+    $dhr = drupal_http_request($server . '/' . $rest_path, $request_data);
+    // @todo Core: Any other code than 200 as interpreted as error.
+    $error = (isset($dhr->error) && $dhr->code[0] != 2);
+    // @todo Core: data property is not assigned if there is no response body.
+    if (!isset($dhr->data)) {
+      $dhr->data = NULL;
+    }
+
+    // Parse the response body into a PHP array.
+    if (isset($dhr->headers['content-type'])) {
+      if (strstr($dhr->headers['content-type'], 'application/json')) {
+        $dhr->data = drupal_json_decode($dhr->data);
+      }
+      elseif (strstr($dhr->headers['content-type'], 'application/xml')) {
+        $dhr->elements = new SimpleXmlIterator($dhr->data);
+        $dhr->data = $this->parseXML($dhr->elements);
+      }
+    }
+
+    // @todo The above is all that may vary between client implementations. All
+    //   of the below logic should live in a separate method that clients don't
+    //   have to re-implement:
+    //     handleResponse($http_code, $http_message, $response)
+    //   Or alternatively, since handleResponse() is required for every request,
+    //   move the above into a new sub-method invoked from request():
+    //     executeRequest($method, $server, $path, array $data)
+    //   and make it return $http_code, $http_message, array $response.
+
+    // If a 'code' exists in the response, it has precedence, regardless of a
+    // possible HTTP error code.
+    if (isset($dhr->data) && is_array($dhr->data) && isset($dhr->data['code']) && $dhr->data['code'] != 0) {
+      $error = TRUE;
+      // Replace HTTP status code with 'code' from response.
+      $dhr->code = $dhr->data['code'];
+      // If there is no HTTP status message, take over 'message' from response.
+      // @todo Remove this condition when Mollom sends proper error messages.
+      if (!isset($dhr->error)) {
+        $dhr->error = $dhr->data['message'];
+      }
+    }
+
+    // Verify that an expected element exists in the response.
+    // Normally, a response validation like this would likely be done using XML
+    // schemas, so the XML parser would already (in)validate the response.
+    // However, when using the JSON protocol, there is no comparable notion of a
+    // schema. Instead of checking for only one particular element in the
+    // response, the client could also introduce dedicated classes for each
+    // resource, but that feels like over-engineering for the average use-case.
+    $key_exists = TRUE;
+    if (!$error && !empty($expected)) {
+      $key_exists = FALSE;
+      if (is_array($dhr->data)) {
+        $key_exists = $this->arrayKeyExists($dhr->data, $expected);
+      }
+      if (!$key_exists) {
+        $error = TRUE;
+        $dhr->code = self::RESPONSE_ERROR;
+        $dhr->error = 'Unexpected server response.';
+      }
+    }
+
+    // Ensure that $dhr->code is an integer.
+    $dhr->code = (int) $dhr->code;
+
+    if ($error) {
+      $arguments = array(
+        'request' => $method . ' ' . $server . '/' . $path,
+        'data' => $data,
+        'response' => $dhr->data,
+      );
+      if ($dhr->code <= 0) {
+        throw new MollomNetworkException(t('Network error.'), self::NETWORK_ERROR, NULL, $this, $arguments);
+      }
+      if ($dhr->code == 1 || $dhr->code == self::AUTH_ERROR || $dhr->code == 401) {
+        throw new MollomAuthenticationException(t('Invalid authentication.'), self::AUTH_ERROR, NULL, $this, $arguments);
+      }
+      if ($dhr->code == 2 || $dhr->code == self::REDIRECT_ERROR || ($dhr->code >= 300 && $dhr->code < 400)) {
+        throw new MollomRedirectException(t('Server redirected to next.'), self::REDIRECT_ERROR, NULL, $this, $arguments);
+      }
+      if ($dhr->code == 3 || $dhr->code == self::REFRESH_ERROR) {
+        throw new MollomRefreshException(t('Refresh of server list required.'), self::REFRESH_ERROR, NULL, $this, $arguments);
+      }
+      if (!$key_exists || $dhr->code >= 500) {
+        throw new MollomResponseException(t('Unexpected server response.'), self::RESPONSE_ERROR, NULL, $this, $arguments);
+      }
+      throw new MollomException($dhr->error, $dhr->code, NULL, $this, $arguments);
+    }
+    else {
+      $this->lastResponseCode = TRUE;
+      $this->log[] = array(
+        'type' => 'debug',
+        'message' => '@code @method @uri',
+        'arguments' => array(
+          '@code' => $dhr->code,
+          '@method' => $method,
+          '@uri' => $path,
+        ),
+        'request' => $method . ' ' . $server . '/' . $path,
+        'data' => $data,
+        'response' => $dhr->data,
+      );
+
+      return $dhr->data;
+    }
+  }
+}
+
+/**
+ * Drupal Mollom client implementation using production testing servers.
+ */
+class MollomDrupalTest extends MollomDrupal {
+  /**
+   * Overrides Mollom::$serversInit.
+   */
+  public $serversInit = array('http://megatron.mollom.com');
+}
+
+/**
+ * Drupal Mollom client implementation using local dummy/fake REST server.
+ */
+class MollomDrupalTestLocal extends MollomDrupal {
+  /**
+   * Overrides Mollom::__construct().
+   */
+  function __construct() {
+    parent::__construct();
+    // Replace initial server list with local fake server.
+    $this->serversInit = array($GLOBALS['base_url'] . '/mollom-test/rest');
+    // Use testing API keys.
+//    module_load_include('test', 'mollom', 'tests/mollom');
+//    $this->keyPublic = MOLLOM_TEST_PUBLIC_KEY;
+//    $this->keyPrivate = MOLLOM_TEST_PRIVATE_KEY;
+  }
+}
+
+/**
+ * Drupal Mollom client implementation talking XML-RPC.
+ *
+ * @todo Incomplete.
+ */
+class MollomDrupalXMLRPC extends MollomDrupal {
+  /**
+   * Overrides Mollom::API_VERSION for XML-RPC requests.
+   */
+  const API_VERSION = '1.0';
+
+  /**
+   * Overrides Mollom::serversInit for XML-RPC requests.
+   */
+  public $serversInit = array('http://xmlrpc1.mollom.com', 'http://xmlrpc2.mollom.com', 'http://xmlrpc3.mollom.com');
+
+  function request1($server, $method, array $data) {
+    if ($method == 'getCaptcha') {
+      $method = ($data['type'] == 'image' ? 'getImageCaptcha' : 'getAudioCaptcha');
+      unset($data['type']);
+    }
+
+    $server .= '/' . self::API_VERSION;
+    $method = 'mollom.' . $method;
+    $result = xmlrpc($server, array(
+      $method => array($data),
+    ));
+    if ($error = xmlrpc_error()) {
+      $this->lastResponseCode = $error->code;
+      $this->log[] = array(
+        'type' => 'error',
+        'message' => 'Error @code: @method: %message',
+        'arguments' => array(
+          '@code' => $error->code,
+          '@method' => $server . '/' . $method,
+          '%message' => $error->message,
+        ),
+        'data' => $data,
+        'response' => $result,
+      );
+    }
+    else {
+      $this->lastResponseCode = TRUE;
+      $this->log[] = array(
+        'type' => 'debug',
+        'message' => '@method',
+        'arguments' => array(
+          '@method' => $server . '/' . $method,
+        ),
+        'data' => $data,
+        'response' => $result,
+      );
+      return $result;
+    }
+  }
+
+  protected function request($method, $server, $path, array $data, array $expected = array()) {
+  }
+}
+
diff --git a/mollom.inc b/mollom.inc
index e79e863..4410653 100644
--- a/mollom.inc
+++ b/mollom.inc
@@ -2,113 +2,936 @@
 
 /**
  * @file
- * Mollom client/server interaction functions.
+ * Mollom client class.
+ *
+ * @todo
+ * - Doing a "verifyKey" with an empty server list leads to two/duplicate and
+ *   unnecessary GET site/$id requests, one for the server list, subsequent ones
+ *   for whatever/verifyKey. Some resources can be cached.
  */
 
 /**
- * Generate authentication data for XML-RPC communication with Mollom servers.
+ * A catchable Mollom exception.
  *
- * This function generates an array with all information required to
- * authenticate against Mollom.  To prevent forged requests where you are
- * impersonated, each request is signed with a hash based on a private
- * key and a timestamp.
+ * The Mollom class internally uses exceptions to handle client-side server
+ * balancing and eventually HTTP request errors within the Mollom::request()
+ * method. All exceptions thrown in the Mollom class and derived classes should
+ * be instances of the MollomException class if they pertain to errors that can
+ * be catched/handled within the class. Other errors should not use the
+ * MollomException class and handled differently.
  *
- * Both the client and the server share the secret key used to create
- * the authentication hash.  They both hash a timestamp with the secret
- * key, and if the hashes match, the authenticity of the message is
- * validated.
+ * No MollomException is supposed to pile up as a user-facing fatal error. All
+ * functions that invoke Mollom::request() have to catch Mollom exceptions.
  *
- * To avoid someone intercepting a (hash, timestamp)-pair and using it
- * to impersonate a client, Mollom reject any request where the timestamp
- * is more than 15 minutes off.
+ * @see Mollom::query()
+ * @see Mollom::request()
  *
- * Make sure your server's time is synchronized with the world clocks,
- * and that you don't share your private key with anyone else.
+ * @param $message
+ *   The Exception message to throw.
+ * @param $code
+ *   The Exception code.
+ * @param $previous
+ *   (optional) The previous Exception, if any.
+ * @param $instance
+ *   The Mollom class instance the Exception is thrown in.
+ * @param $arguments
+ *   (optional) A associative array containing information about a performed
+ *   HTTP request that failed:
+ *   - request: (string) The HTTP method and URI of the performed request; e.g.,
+ *     "GET http://server.mollom.com/v1/foo/bar". In case of GET requests, do
+ *     not add query parameters to the URI; pass them in 'data' instead.
+ *   - data: (array) An associative array containing HTTP GET/POST/PUT request
+ *     query parameters that were sent to the server.
+ *   - response: (mixed) The server response, either as string, or the already
+ *     parsed response; i.e., an array.
  *
- * @param $public_key
- *   (optional) The public key to use for authentication. Only used internally.
- * @param $private_key
- *   (optional) The private key to use for authentication. Only used internally.
+ * @todo Refine exceptions by necessity of having to react differently. All 5xx
+ *   HTTP codes, MollomRedirectException, and MollomResponseException should
+ *   make the client try the next server.
  */
-function _mollom_authentication($public_key = NULL, $private_key = NULL) {
-  if (!isset($public_key)) {
-    $public_key = variable_get('mollom_public_key', '');
+class MollomException extends Exception {
+  /**
+   * @var Mollom
+   */
+  protected $mollom;
+
+  /**
+   * The severity of this exception.
+   *
+   * @var string
+   */
+  protected $severity = 'debug';
+
+  public $arguments = array();
+
+  function __construct($message = '', $code = 0, Exception $previous = NULL, Mollom $mollom, array $arguments = array()) {
+    // Fatal error on PHP <5.3 when passing more arguments to Exception.
+    if (version_compare(phpversion(), '5.3') >= 0) {
+      parent::__construct($message, $code, $previous);
+    }
+    else {
+      parent::__construct($message, $code);
+    }
+
+    // Set the error code on the Mollom class.
+    $mollom->lastResponseCode = $code;
+
+    // Log the exception.
+    $message = array(
+      'type' => $this->severity,
+      'message' => 'Error @code: %message',
+      'arguments' => array(
+        '@code' => $code,
+        '%message' => $message,
+      ),
+    );
+    // Add HTTP request information, if available.
+    if (!empty($arguments)) {
+      $message += array(
+        'request' => $arguments['request'],
+        'data' => $arguments['data'],
+        'response' => $arguments['response'],
+      );
+    }
+    $mollom->log[] = $message;
+
+    // @todo Remove?
+    $this->setArguments($arguments);
   }
-  if (!isset($private_key)) {
-    $private_key = variable_get('mollom_private_key', '');
+
+  public function setArguments($arguments) {
+    $this->arguments = $arguments;
+    return $this;
   }
 
-  // Generate a timestamp according to the dateTime format (http://www.w3.org/TR/xmlschema-2/#dateTime):
-  $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", REQUEST_TIME);
+  public function getArguments() {
+    return $this->arguments;
+  }
+}
 
-  // Generate a random number:
-  $nonce = md5(mt_rand());
+/**
+ * Mollom network error exception.
+ *
+ * Thrown in case a HTTP request results in code <= 0, denoting a low-level
+ * communication error.
+ *
+ * @todo Not actively caught anywhere yet.
+ */
+class MollomNetworkException extends MollomException {
+  protected $severity = 'error';
+}
+
+/**
+ * Mollom authentication error exception.
+ *
+ * Thrown in case API keys or other authentication parameters are invalid.
+ */
+class MollomAuthenticationException extends MollomException {
+  protected $severity = 'error';
+}
 
-  // Calculate a HMAC-SHA1 according to RFC 2104.
-  // @see http://www.ietf.org/rfc/rfc2104.txt
-  $request_data = $time . ':' . $nonce . ':' . $private_key;
-  $hash = base64_encode(hash_hmac('sha1', $request_data, $private_key, TRUE));
+/**
+ * Mollom server refresh exception.
+ *
+ * Thrown when a Mollom server asks the client to update the server list.
+ */
+class MollomRefreshException extends MollomException {
+  protected $severity = 'debug';
+}
 
-  // Store everything in an array.  Elsewhere in the code, we'll add the
-  // actual data before we pass it onto the XML-RPC library:
-  $data['public_key'] = $public_key;
-  $data['time'] = $time;
-  $data['hash'] = $hash;
-  $data['nonce'] = $nonce;
+/**
+ * Mollom server redirect exception.
+ *
+ * Thrown when a Mollom server asks the client to use next server in the server
+ * list.
+ */
+class MollomRedirectException extends MollomException {
+  protected $severity = 'debug';
+}
 
-  return $data;
+/**
+ * Mollom server response exception.
+ *
+ * Thrown when a request to a Mollom server succeeds, but the response does not
+ * contain an expected element; e.g., a backend configuration or execution
+ * error that possibly exists on one server only.
+ *
+ * @see Mollom::request()
+ */
+class MollomResponseException extends MollomException {
+  protected $severity = 'debug';
 }
 
 /**
- * Refreshes the list of Mollom's XML-RPC servers.
+ * The base class for Mollom client implementations.
  */
-function _mollom_retrieve_server_list() {
-  // Start from a hard-coded list of servers.
-  $servers = array('http://xmlrpc1.mollom.com', 'http://xmlrpc2.mollom.com', 'http://xmlrpc3.mollom.com');
-  $messages = array();
-
-  // mollom.getServerList cannot use mollom() as we need to prevent infinite
-  // recursion. In addition, we handle returned error codes differently here,
-  // since MOLLOM_REDIRECT and MOLLOM_REFRESH, as well as any other
-  // communication error requires us to skip to the next server to retrieve a
-  // new server list. We only ever abort, if a server returns MOLLOM_ERROR, in
-  // which case there must be a configuration error (e.g., invalid API keys).
-  $method = 'mollom.getServerList';
-  foreach ($servers as $server) {
-    $result = xmlrpc($server . '/' . MOLLOM_API_VERSION, array($method => array(_mollom_authentication())));
-    if ($result === FALSE && ($error = xmlrpc_error())) {
-      // In any case, log the error.
-      $messages[] = array(
-        'Error @errno from %server for %method: %message' => array(
-          '@errno' => $error->code,
-          '%server' => $server,
-          '%method' => $method,
-          '%message' => $error->message,
+abstract class Mollom {
+  /**
+   * The Mollom API version, used in HTTP requests.
+   */
+  const API_VERSION = 'v1';
+
+  /**
+   * Network communication failure code: No servers could be reached.
+   *
+   * @see MollomNetworkException
+   */
+  const NETWORK_ERROR = 900;
+
+  /**
+   * Server communication failure code: Unexpected server response.
+   *
+   * Using the 5xx HTTP status code range, but not re-using an existing HTTP
+   * code to prevent bogus bug reports. 511 is the closest comparable code
+   * 501 (Not Implemented) plus 10.
+   *
+   * @see MollomResponseException
+   */
+  const RESPONSE_ERROR = 511;
+
+  /**
+   * Server communication failure code: Authentication error.
+   *
+   * @see MollomAuthenticationException
+   */
+  const AUTH_ERROR = 1000;
+
+  /**
+   * Server communication failure code: Client is asked to update the server list.
+   *
+   * @see MollomRefreshException
+   */
+  const REFRESH_ERROR = 1100;
+
+  /**
+   * Server communication failure code: Client is asked to try next server.
+   *
+   * @see MollomRedirectException
+   */
+  const REDIRECT_ERROR = 1200;
+
+  /**
+   * The public Mollom API key to use for request authentication.
+   *
+   * @var string
+   */
+  public $keyPublic = '';
+
+  /**
+   * The private Mollom API key to use for request authentication.
+   *
+   * @var string
+   */
+  public $keyPrivate = '';
+
+  /**
+   * The list of Mollom servers to communicate with, as returned by Mollom.
+   */
+  public $servers = array();
+
+  /**
+   * A hard-coded list of Mollom servers to fetch the server list from.
+   */
+  public $serversInit = array('http://megatron.mollom.com');
+
+  /**
+   * The status code of the last response or TRUE if it succeeded.
+   */
+  public $lastResponseCode = NULL;
+
+  /**
+   * A list of logged requests.
+   */
+  public $log = array();
+
+  function __construct() {
+    $this->keyPublic = $this->loadConfiguration('publicKey');
+    $this->keyPrivate = $this->loadConfiguration('privateKey');
+  }
+
+  /**
+   * Loads a configuration value from client-side storage.
+   *
+   * @param string $name
+   *   The configuration setting name to load, one of:
+   *   - publicKey: The public API key for Mollom authentication.
+   *   - privateKey: The private API key for Mollom authentication.
+   *   - servers: An indexed array of Mollom server addresses.
+   *
+   * @return mixed
+   *   The stored configuration value or NULL if there is none.
+   *
+   * @see Mollom::saveConfiguration()
+   * @see Mollom::deleteConfiguration()
+   */
+  abstract public function loadConfiguration($name);
+
+  /**
+   * Saves a configuration value to client-side storage.
+   *
+   * @param string $name
+   *   The configuration setting name to save.
+   * @param mixed $value
+   *   The value to save.
+   *
+   * @see Mollom::loadConfiguration()
+   * @see Mollom::deleteConfiguration()
+   */
+  abstract public function saveConfiguration($name, $value);
+
+  /**
+   * Deletes a configuration value from client-side storage.
+   *
+   * @param string $name
+   *   The configuration setting name to delete.
+   *
+   * @see Mollom::loadConfiguration()
+   * @see Mollom::saveConfiguration()
+   */
+  abstract public function deleteConfiguration($name);
+
+  /**
+   * Returns platform and version information about the Mollom client.
+   *
+   * Retrieves platform and Mollom client version information to send along to
+   * Mollom when verifying keys.
+   *
+   * This information is used to speed up support requests and technical
+   * inquiries. The data may also be aggregated to help the Mollom staff to make
+   * decisions on new features or the necessity of back-porting improved
+   * functionality to older versions.
+   *
+   * @return
+   *   An associative array containing:
+   *   - platformName: The name of the platform/distribution; e.g., "Drupal".
+   *   - platformVersion: The version of platform/distribution; e.g., "7.0".
+   *   - clientName: The official Mollom client name; e.g., "Mollom".
+   *   - clientVersion: The version of the Mollom client; e.g., "7.x-1.0".
+   */
+  abstract public function getClientInformation();
+
+  /**
+   * Writes log messages to a permanent location/storage.
+   *
+   * Not abstract, since clients are not required to write log messages.
+   * However, all clients should permanently store the log messages, as it
+   * dramatically improves resolution of support requests filed by users.
+   * The log may be written and appended to a file (via file_put_contents()),
+   * syslog (on *nix-based systems), or a database.
+   *
+   * @see Mollom::log
+   */
+  public function writeLog() {
+    // After writing log messages, empty the log.
+    $this->purgeLog();
+  }
+
+  /**
+   * Purges captured log messages.
+   *
+   * @see Mollom::writeLog()
+   */
+  final public function purgeLog() {
+    $this->log = array();
+  }
+
+  /**
+   * Returns the current request time as UNIX timestamp.
+   */
+  public function getRequestTime() {
+    return $_SERVER['REQUEST_TIME'];
+  }
+
+  /**
+   * Generates authentication parameters for communication with Mollom servers.
+   *
+   * This function generates an array with all information required to
+   * authenticate against Mollom. To prevent forged requests where you are
+   * impersonated, each request is signed with a hash based on a private
+   * key and a timestamp.
+   *
+   * Both the client and the server share the secret key used to create
+   * the authentication hash. They both hash a timestamp with the secret
+   * key, and if the hashes match, the authenticity of the message is
+   * validated.
+   *
+   * To avoid someone intercepting a (hash, timestamp)-pair and using it
+   * to impersonate a client, Mollom rejects any request where the timestamp
+   * is more than 15 minutes off.
+   *
+   * Make sure your server's time is synchronized with the world clocks,
+   * and that you don't share your private key with anyone else.
+   */
+  public function getAuthentication() {
+    if (empty($this->keyPublic) || empty($this->keyPrivate)) {
+      throw new MollomAuthenticationException('Missing API keys.', self::AUTH_ERROR, NULL, $this);
+    }
+
+    // Generate a timestamp according to the dateTime format.
+    // @see http://www.w3.org/TR/xmlschema-2/#dateTime
+    $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", $this->getRequestTime());
+
+    // Generate a random number.
+    $nonce = md5(mt_rand());
+
+    // Calculate a HMAC-SHA1 according to RFC 2104.
+    // @see http://www.ietf.org/rfc/rfc2104.txt
+    $request_data = $time . ':' . $nonce . ':' . $this->keyPrivate;
+    $hash = base64_encode(hash_hmac('sha1', $request_data, $this->keyPrivate, TRUE));
+
+    // Return HMAC authentication parameters as a keyed array.
+    $data['publicKey'] = $this->keyPublic;
+    $data['time'] = $time;
+    $data['hash'] = $hash;
+    $data['nonce'] = $nonce;
+
+    return $data;
+  }
+
+  /**
+   * Fetches Mollom servers from local configuration or retrieves a new list.
+   */
+  public function getServers() {
+    // If there is no server list yet, consult the local configuration.
+    if (empty($this->servers)) {
+      $servers = $this->loadConfiguration('servers');
+      // Use the local configuration value, if any.
+      if (!empty($servers) && is_array($servers)) {
+        $this->servers = $servers;
+      }
+      // Otherwise, retrieve a new server list from Mollom.
+      else {
+        $this->servers = $this->refreshServers();
+        if ($this->servers) {
+          $this->saveConfiguration('servers', $this->servers);
+          $this->log[] = array(
+            'type' => 'debug',
+            'message' => 'Refreshed servers: %servers',
+            'arguments' => array(
+              '%servers' => implode(', ', $this->servers),
+            ),
+          );
+        }
+      }
+    }
+    return $this->servers;
+  }
+
+  protected function refreshServers() {
+    // refreshServers() cannot use query() as we need to prevent infinite
+    // recursion. In addition, we handle returned error codes differently here,
+    // since REDIRECT_ERROR, REFRESH_ERROR, and any other communication error
+    // requires us to skip to the next server in order to retrieve a new server
+    // list. We only ever abort, if we get a AUTH_ERROR, in which case there
+    // is a configuration error (i.e., invalid API keys).
+    $servers = array();
+    try {
+      $data = $this->getAuthentication();
+    }
+    catch (MollomAuthenticationException $e) {
+      return $servers;
+    }
+
+    $path = 'site/' . $this->keyPublic;
+    $expected = array('site', 'servers');
+    foreach ($this->serversInit as $server) {
+      try {
+        $result = $this->request('GET', $server, $path, $data, $expected);
+      }
+      catch (MollomAuthenticationException $e) {
+        // Bogus configuration. Stop trying, since all servers will fail.
+        break;
+      }
+      catch (MollomException $e) {
+        // On any other error and skip to the next server.
+        continue;
+      }
+
+      if (isset($result) && $this->lastResponseCode === TRUE) {
+        $servers = $result['site']['servers'];
+        break;
+      }
+    }
+    return $servers;
+  }
+
+  /**
+   * Retrieve or send data from/to Mollom servers.
+   *
+   * @param string $method
+   *   The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'.
+   * @param string $path
+   *   The REST path/resource to request; e.g., 'site/1a2b3c'.
+   * @param array $data
+   *   An associative array of query parameters to send with the request.
+   * @param array $expected
+   *   (optional) An element that is expected in the response, denoted as a list
+   *   of parent element keys to the element and the element key itself; e.g., a
+   *   value of array('site', 'servers') expects $response['site']['servers'] to
+   *   exist in the response.
+   */
+  public function query($method, $path, array $data = array(), array $expected = array()) {
+    // Retrieve server list.
+    // If we get no list, we will have no servers to iterate over and only the
+    // error logic remains.
+    $this->getServers();
+
+    // Unconditionally add HMAC request authentication parameters.
+    try {
+      $data += $this->getAuthentication();
+    }
+    catch (MollomAuthenticationException $e) {
+      // MollomAuthenticationException sets the appropriate error code. Merely
+      // catch the exception, move on to error logic below.
+    }
+
+    // Initialize refresh variable.
+    $refresh = FALSE;
+    // Send the request to the first server; if that fails, try the other
+    // servers in the list.
+    // @todo The Mollom instance "sticks" now, as it's statically cached. Thus,
+    //   the array cursor in $this->servers is retained across multiple queries.
+    //   I.e., subsequent queries will continue to communicate with the
+    //   "current" server, which might not be the first.
+    //   PRO: In a scenario that uses the statically cached class instance
+    //   (low-level scripts and possibly subsequent form submissions in the
+    //   future) and in which a server redirects, subsequent queries will
+    //   continue to use the current/second/next server in the list instead of
+    //   restarting on the first; i.e., potentially less "wasted" requests.
+    //   CON: Higher chance to reach the end of the server list, unless we allow
+    //   to iterate two times over the server list until we consider a request
+    //   to fail.
+    while ($server = current($this->servers)) {
+      try {
+        $result = $this->request($method, $server, $path, $data, $expected);
+      }
+      catch (MollomRefreshException $e) {
+        // Prevent infinite loops.
+        if (!$refresh) {
+          $refresh = TRUE;
+
+          // In any case, the current server list is no longer valid.
+          $this->servers = array();
+          $this->deleteConfiguration('servers');
+
+          // Retrieve a fresh list of Mollom servers.
+          $this->servers = $this->getServers();
+          // If API keys are invalid, we won't be able to get a new server list.
+          // To reach this, we must have had a server list (and therefore
+          // valid keys) before, so we do not immediately return, but trigger
+          // the fallback mode instead.
+          if (empty($this->servers)) {
+            break;
+          }
+        }
+      }
+      catch (MollomRedirectException $e) {
+        // Try the next server in the list.
+        $next = next($this->servers);
+
+        // @todo $next may be FALSE, confusing users looking into logs.
+        $this->log[] = array(
+          'type' => 'debug',
+          'message' => 'Server %server redirected to %next.',
+          'arguments' => array(
+            '%server' => $server,
+            '%next' => $next,
+          ),
+        );
+        continue;
+      }
+      catch (MollomAuthenticationException $e) {
+        // This is an irrecoverable error, so don't try other servers.
+        break;
+      }
+      catch (MollomException $e) {
+        // On any other known error, try the next server.
+        next($this->servers);
+        continue;
+      }
+
+      // Unless we have a positive result, continue to next server.
+      // @todo Handle uncaught exceptions and errors?
+      if ($this->lastResponseCode === TRUE) {
+        break;
+      }
+      else {
+        next($this->servers);
+      }
+    }
+
+    // In case all servers failed, reset the server list to enforce retrieval of
+    // a new list the next time.
+    if (current($this->servers) === FALSE) {
+      $this->servers = array();
+      $this->deleteConfiguration('servers');
+
+      $this->log[] = array(
+        'type' => 'error',
+        'message' => 'All servers unreachable or returning errors. The server list was emptied: %servers',
+        'arguments' => array(
+          '%servers' => implode(', ', $this->servers ? $this->servers : $this->serversInit),
         ),
       );
-      // Skip to the next server in case of any error, except if we have a
-      // MOLLOM_ERROR, which indicates a bogus configuration. In this case, stop
-      // trying, since all servers will fail.
-      if ($error->code === MOLLOM_ERROR) {
-        break;
+    }
+
+    // Write all captured log messages.
+    $this->writeLog();
+
+    // If there is a result (only possible with a server list) and the last
+    // request succeeded, return the result to the caller.
+    if (isset($result) && $this->lastResponseCode === TRUE) {
+      return $result;
+    }
+    // If the last request succeeded but there was a unexpected response, return
+    // the error code.
+    if ($this->lastResponseCode == self::RESPONSE_ERROR) {
+      return $this->lastResponseCode;
+    }
+    // Return an authentication error, which may require special client-side
+    // processing.
+    if ($this->lastResponseCode == self::AUTH_ERROR) {
+      return $this->lastResponseCode;
+    }
+
+    // In case of any kind of HTTP error (404, 0 [invalid-address],
+    // -1002 [bad URI], etc), return a generic NETWORK_ERROR.
+    return self::NETWORK_ERROR;
+  }
+
+  /**
+   * Performs a HTTP request to a Mollom server.
+   *
+   * @param string $method
+   *   The HTTP method to use; i.e., 'GET', 'POST', or 'PUT'.
+   * @param string $server
+   *   The base URL of the server to perform the request against; e.g.,
+   *   'http://foo.mollom.com'.
+   * @param string $path
+   *   The REST path/resource to request; e.g., 'site/1a2b3c'.
+   * @param array $data
+   *   An associative array of query parameters to send with the request.
+   * @param array $expected
+   *   (optional) An element that is expected in the response, denoted as a list
+   *   of parent element keys to the element and the element key itself; e.g., a
+   *   value of array('site', 'servers') expects $response['site']['servers'] to
+   *   exist in the response.
+   *
+   * @throws MollomNetworkException
+   * @throws MollomAuthenticationException
+   * @throws MollomRedirectException
+   * @throws MollomRefreshException
+   * @throws MollomException
+   *
+   * @see Mollom::httpBuildQuery()
+   * @see Mollom::httpParseQuery()
+   * @see Mollom::parseXML()
+   * @see json_decode()
+   */
+  abstract protected function request($method, $server, $path, array $data, array $expected = array());
+
+  /**
+   * Converts a SimpleXMLIterator structure into an associative array.
+   *
+   * Used to parse an XML response from Mollom servers into a PHP array. For
+   * example:
+   * @code
+   * $elements = new SimpleXmlIterator($response_body);
+   * $parsed_response = $this->parseXML($elements);
+   * @endcode
+   *
+   * @param $sxi
+   *   A SimpleXMLIterator structure of the server response body.
+   *
+   * @return array
+   *   An associative, possibly multidimensional array.
+   */
+  public static function parseXML(SimpleXMLIterator $sxi) {
+    $a = array();
+    $remove = array();
+    for ($sxi->rewind(); $sxi->valid(); $sxi->next()) {
+      $key = $sxi->key();
+
+      // Recurse into non-scalar values.
+      if ($sxi->hasChildren()) {
+        $value = self::parseXML($sxi->current());
+      }
+      // Use a simple key/value pair for scalar values.
+      else {
+        $value = strval($sxi->current());
+      }
+
+      if (!isset($a[$key])) {
+        $a[$key] = $value;
+      }
+      // Convert already existing keys into indexed keys, retaining other
+      // existing keys in the array; i.e., two or more XML elements of the
+      // same name and on the same level.
+      // Note that this XML to PHP array conversion does not support multiple
+      // different elements that each appear multiple times.
+      else {
+        // First time we reach here, convert the existing keyed item. Do not
+        // remove $key, so we enter this path again.
+        if (!isset($remove[$key])) {
+          $a[] = $a[$key];
+          // Mark $key for removal.
+          $remove[$key] = $key;
+        }
+        // Add the new item.
+        $a[] = $value;
       }
     }
-    // Otherwise, we have a valid result.
+    // Lastly, remove named keys that have been converted to indexed keys.
+    foreach ($remove as $key) {
+      unset($a[$key]);
+    }
+    return $a;
+  }
+
+  /**
+   * Determines whether a nested array with variable depth contains all of the requested keys.
+   *
+   * @param array $array
+   *   The array with variable depth that may contain the value to check for.
+   * @param array $expected
+   *   A list of parent keys of the value, starting with the outermost key.
+   *
+   * @return
+   *   TRUE if all the parent keys exist, FALSE otherwise.
+   *
+   * @see Mollom::request()
+   */
+  public static function arrayKeyExists(array $array, array $expected) {
+    $ref = &$array;
+    foreach ($expected as $parent) {
+      if (is_array($ref) && array_key_exists($parent, $ref)) {
+        $ref = &$ref[$parent];
+      }
+      else {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Builds an RFC-compliant, rawurlencoded query string.
+   *
+   * PHP did a design decision to only support HTTP query parameters in the form
+   * of foo[]=1&foo[]=2, primarily for its built-in and automated conversion to
+   * PHP arrays. Other platforms (including the Mollom backend) do not support
+   * this syntax and expect multiple parameters to be in the form of
+   * foo=1&foo=2.
+   *
+   * @see http_build_query()
+   * @see http://en.wikipedia.org/wiki/Query_string
+   * @see http://tools.ietf.org/html/rfc3986#section-3.4
+   *
+   * @param array $query
+   *   The query parameter array to be processed, e.g. $_GET.
+   * @param string $parent
+   *   Internal use only. Used to build the $query array key for nested items.
+   *
+   * @return array
+   *   A rawurlencoded string which can be used as or appended to the URL query
+   *   string.
+   *
+   * @see Mollom::httpParseQuery()
+   */
+  public static function httpBuildQuery(array $query, $parent = '') {
+    $params = array();
+
+    foreach ($query as $key => $value) {
+      // For indexed (unnamed) child array keys, use the same parameter name,
+      // leading to param=foo&param=bar instead of param[]=foo&param[]=bar.
+      if ($parent && is_int($key)) {
+        $key = rawurlencode($parent);
+      }
+      else {
+        $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
+      }
+
+      // Recurse into children.
+      if (is_array($value)) {
+        $params[] = self::httpBuildQuery($value, $key);
+      }
+      // If a query parameter value is NULL, only append its key.
+      elseif (!isset($value)) {
+        $params[] = $key;
+      }
+      else {
+        // For better readability of paths in query strings, we decode slashes.
+        $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
+      }
+    }
+
+    return implode('&', $params);
+  }
+
+  /**
+   * Parses an RFC-compliant, rawurlencoded query string.
+   *
+   * Mollom clients normally do not need this function, as they do not need to
+   * process requests from a server - unless a client attempts to implement
+   * client-side unit testing.
+   *
+   * @param string $query
+   *   The query parameter string to process, e.g. $_SERVER['QUERY_STRING']
+   *   (GET) or php://input (POST/PUT).
+   *
+   * @return array
+   *   A query parameter array parsed from $query.
+   *
+   * @see Mollom::httpBuildQuery()
+   * @see parse_str()
+   */
+  public static function httpParseQuery($query) {
+    if ($query === '') {
+      return array();
+    }
+    // Explode parameters into arrays to check for duplicate names.
+    $params = array();
+    $seen = array();
+    $duplicate = array();
+    foreach (explode('&', $query) as $chunk) {
+      $param = explode('=', $chunk, 2);
+      if (isset($seen[$param[0]])) {
+        $duplicate[$param[0]] = TRUE;
+      }
+      $seen[$param[0]] = TRUE;
+      $params[] = $param;
+    }
+    // Implode back into a string.
+    $query = '';
+    foreach ($params as $param) {
+      $query .= $param[0];
+      if (isset($duplicate[$param[0]])) {
+        $query .= '[]';
+      }
+      if (isset($param[1])) {
+        $query .= '=' . $param[1];
+      }
+      $query .= '&';
+    }
+    // Parse query string as usual.
+    parse_str($query, $result);
+    return $result;
+  }
+
+  public function getSite($siteId = NULL) {
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('GET', 'site/' . $siteId, array(), array('site'));
+    return isset($result['site']) ? $result['site'] : $result;
+  }
+
+  public function verifyKey() {
+    $data = $this->getClientInformation();
+    $result = $this->query('PUT', 'site/' . $this->keyPublic, $data, array('site'));
+    // lastResponseCode will either be TRUE, AUTH_ERROR, or NETWORK_ERROR.
+    return $this->lastResponseCode === TRUE ? TRUE : $this->lastResponseCode;
+  }
+
+  public function checkContent($data = array()) {
+    if (!empty($data['contentId'])) {
+      $result = $this->query('PUT', 'content/' . $data['contentId'], $data, array('content', 'contentId'));
+    }
     else {
-      break;
+      $result = $this->query('POST', 'content', $data, array('content', 'contentId'));
     }
+    return isset($result['content']) ? $result['content'] : $result;
   }
 
-  // Allow other modules to alter the server list. Internal use only.
-  drupal_alter('mollom_server_list', $result);
+  public function getCaptcha($data = array()) {
+    if (!isset($data['type']) || !in_array($data['type'], array('image', 'audio'))) {
+      // @todo Error handling?
+      throw new MollomException('Unknown CAPTCHA type.', 0, NULL, $this);
+    }
+    if (!empty($data['captchaId'])) {
+      $result = $this->query('PUT', 'captcha/' . $data['captchaId'], $data, array('captcha', 'captchaId'));
+    }
+    else {
+      $result = $this->query('POST', 'captcha', $data, array('captcha', 'captchaId'));
+    }
+    return isset($result['captcha']) ? $result['captcha'] : $result;
+  }
+
+  public function checkCaptcha($data = array()) {
+    if (empty($data['captchaId'])) {
+      // @todo Error handling?
+      throw new MollomException('Missing CAPTCHA ID.', 0, NULL, $this);
+    }
+    $result = $this->query('PUT', 'captcha/' . $data['captchaId'], $data, array('captcha', 'captchaId'));
+    return isset($result['captcha']) ? $result['captcha'] : $result;
+  }
 
-  if (is_array($result)) {
-    _mollom_watchdog_multiple($messages, WATCHDOG_DEBUG);
+  /**
+   * @todo This part of the REST API needs work. No way to report CAPTCHA sessions.
+   */
+  public function sendFeedback($data = array()) {
+    if (empty($data['contentId'])) {
+      // @todo Error handling?
+      throw new MollomException('Missing content ID.', 0, NULL, $this);
+    }
+    if (empty($data['moderated'])) {
+      // @todo Error handling?
+      throw new MollomException('Missing feedback value.', 0, NULL, $this);
+    }
+    $result = $this->query('PUT', 'content/' . $data['contentId'], $data);
+    return $this->lastResponseCode === TRUE ? TRUE : FALSE;
+  }
+
+  /**
+   * @todo List parameters.
+   */
+  public function getBlacklist($siteId = NULL) {
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('GET', 'blacklist/' . $siteId, array(), array('list'));
+    // @todo 'list' is a string when empty. Consider to move list response meta
+    //   elements into the 'list' element, so it's never empty.
+    if (isset($result['list'])) {
+      return is_array($result['list']) ? $result['list'] : array();
+    }
     return $result;
   }
-  else {
-    _mollom_watchdog_multiple($messages, WATCHDOG_ERROR);
-    return xmlrpc_errno();
+
+  public function getBlacklistEntry($entryId, $siteId = NULL) {
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('GET', 'blacklist/' . $siteId . '/' . $entryId, array(), array('entry'));
+    return isset($result['entry']) ? $result['entry'] : $result;
+  }
+
+  public function createBlacklistEntry($data = array(), $siteId = NULL) {
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('POST', 'blacklist/' . $siteId, $data, array('entry'));
+    return isset($result['entry']) ? $result['entry'] : $result;
+  }
+
+  public function updateBlacklistEntry($data = array(), $siteId = NULL) {
+    if (empty($data['id'])) {
+      // @todo Error handling?
+      throw new MollomException('Missing blacklist entry ID.', 0, NULL, $this);
+    }
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('PUT', 'blacklist/' . $siteId . '/' . $data['id'], $data, array('entry'));
+    return isset($result['entry']) ? $result['entry'] : $result;
+  }
+
+  public function deleteBlacklistEntry($entryId, $siteId = NULL) {
+    if (!isset($siteId)) {
+      $siteId = $this->keyPublic;
+    }
+    $result = $this->query('POST', 'blacklist/' . $siteId . '/' . $entryId . '/delete');
+    return $this->lastResponseCode === TRUE;
   }
 }
 
diff --git a/mollom.info b/mollom.info
index 4027872..34597ad 100644
--- a/mollom.info
+++ b/mollom.info
@@ -4,8 +4,7 @@ core = 7.x
 configure = admin/config/content/mollom
 scripts[] = mollom.js
 stylesheets[all][] = mollom.css
-files[] = mollom.module
-files[] = mollom.admin.inc
-files[] = mollom.pages.inc
-files[] = mollom.install
+files[] = mollom.inc
+files[] = mollom.drupal.inc
 files[] = tests/mollom.test
+files[] = tests/mollom.class.test
diff --git a/mollom.install b/mollom.install
index 8a6f620..8c9a6f7 100644
--- a/mollom.install
+++ b/mollom.install
@@ -52,14 +52,14 @@ function mollom_requirements($phase = 'runtime', $check = TRUE) {
       ));
     }
     // Invalid API keys.
-    elseif ($status['keys valid'] === MOLLOM_ERROR) {
+    elseif ($status['keys valid'] === Mollom::AUTH_ERROR) {
       $requirements['mollom']['value'] = t('Invalid');
       $requirements['mollom']['description'] = t('The configured Mollom API keys are invalid. !admin-message', array(
         '!admin-message' => $admin_message,
       ));
     }
     // Communication error.
-    elseif ($status['keys valid'] === NETWORK_ERROR) {
+    elseif ($status['keys valid'] === Mollom::NETWORK_ERROR) {
       $requirements['mollom']['value'] = t('Network error');
       $requirements['mollom']['description'] = t('The Mollom servers could not be contacted. Please make sure that your web server can make outgoing HTTP requests.');
     }
@@ -88,6 +88,7 @@ function mollom_schema() {
         'not null' => TRUE,
         'default' => '',
       ),
+      // @todo Remove, after figuring out how to update.
       'session_id' => array(
         'description' => 'Session hash returned by Mollom.',
         'type' => 'varchar',
@@ -95,6 +96,16 @@ function mollom_schema() {
         'not null' => TRUE,
         'default' => '',
       ),
+      'contentId' => array(
+        'description' => 'Content ID returned by Mollom.',
+        'type' => 'int',
+        'not null' => FALSE,
+      ),
+      'captchaId' => array(
+        'description' => 'CAPTCHA ID returned by Mollom.',
+        'type' => 'int',
+        'not null' => FALSE,
+      ),
       'form_id' => array(
         'description' => 'The form_id of the form being protected.',
         'type' => 'varchar',
@@ -121,10 +132,17 @@ function mollom_schema() {
       // improved content moderation.
       'spam' => array(
         'description' => 'Text analysis spam check result.',
-        'type' => 'int',
+        'type' => 'float',
         'size' => 'tiny',
         'not null' => FALSE,
       ),
+      'spamResult' => array(
+        'description' => 'Text analysis final spam classification result.',
+        'type' => 'varchar',
+        'length' => 8,
+        'not null' => FALSE,
+      ),
+      // @todo Add checkCaptcha results 'solved', 'reason'.
       'quality' => array(
         'description' => 'Text analysis quality check result.',
         'type' => 'float',
@@ -584,17 +602,17 @@ function mollom_update_7005() {
     // Fill {mollom}.spam with approximate values based on {mollom}.quality.
     // Note that this is just to have some values. 'quality' and 'spam' are
     // completely unrelated otherwise.
-    // MOLLOM_ANALYSIS_SPAM
+    // MOLLOM_ANALYSIS_SPAM (2)
     db_update('mollom')
       ->fields(array('spam' => 2))
       ->condition('quality', 0.5, '<')
       ->execute();
-    // MOLLOM_ANALYSIS_UNSURE
+    // MOLLOM_ANALYSIS_UNSURE (3)
     db_update('mollom')
       ->fields(array('spam' => 3))
       ->condition('quality', 0.5, '=')
       ->execute();
-    // MOLLOM_ANALYSIS_HAM
+    // MOLLOM_ANALYSIS_HAM (1)
     db_update('mollom')
       ->fields(array('spam' => 1))
       ->condition('quality', 0.5, '>')
@@ -817,3 +835,65 @@ function mollom_update_7010() {
     ));
   }
 }
+
+/**
+ * Change {mollom}: Add 'spamResult', change 'spam', replace 'session_id' with 'contentId' and 'captchaId'.
+ */
+function mollom_update_7011() {
+  if (!db_field_exists('mollom', 'spamResult')) {
+    // Add 'spamResult' and convert existing 'spam' integers into 'spamResult'
+    // strings.
+    db_add_field('mollom', 'spamResult', array(
+      'description' => 'Text analysis final spam classification result.',
+      'type' => 'varchar',
+      'length' => 8,
+      'not null' => FALSE,
+    ));
+    db_update('mollom')
+      ->fields(array('spamResult' => 'ham'))
+      ->condition('spam', 1)
+      ->execute();
+    db_update('mollom')
+      ->fields(array('spamResult' => 'spam'))
+      ->condition('spam', 2)
+      ->execute();
+    db_update('mollom')
+      ->fields(array('spamResult' => 'unsure'))
+      ->condition('spam', 3)
+      ->execute();
+
+    // Change 'spam' and convert existing 'spam' integer values into doubles.
+    db_change_field('mollom', 'spam', 'spam', array(
+      'description' => 'Text analysis spam check result.',
+      'type' => 'float',
+      'size' => 'tiny',
+      'not null' => FALSE,
+    ));
+    db_update('mollom')
+      ->fields(array('spam' => 0))
+      ->condition('spam', 1)
+      ->execute();
+    db_update('mollom')
+      ->fields(array('spam' => 1))
+      ->condition('spam', 2)
+      ->execute();
+    db_update('mollom')
+      ->fields(array('spam' => 0.5))
+      ->condition('spam', 3)
+      ->execute();
+
+    // Add 'contentId' and 'captchaId'.
+    db_add_field('mollom', 'contentId', array(
+      'description' => 'Content ID returned by Mollom.',
+      'type' => 'int',
+      'not null' => FALSE,
+    ));
+    db_add_field('mollom', 'captchaId', array(
+      'description' => 'CAPTCHA ID returned by Mollom.',
+      'type' => 'int',
+      'not null' => FALSE,
+    ));
+    // @todo Figure out how to update session_id values. (?)
+  }
+}
+
diff --git a/mollom.js b/mollom.js
index d20be15..026191d 100644
--- a/mollom.js
+++ b/mollom.js
@@ -34,12 +34,12 @@ function getMollomCaptcha() {
 
   var context = $(this).parents('form');
 
-  // Extract the Mollom session id and form build id from the form.
-  var mollomSessionId = $('input.mollom-session-id', context).val();
+  // Extract the Mollom CAPTCHA ID and form build ID from the form.
+  var mollomCaptchaId = $('input.mollom-captcha-id', context).val();
   var formBuildId = $('input[name="form_build_id"]', context).val();
 
   // Retrieve a CAPTCHA:
-  $.getJSON(Drupal.settings.basePath + 'mollom/captcha/' + newCaptchaType + '/' + formBuildId + '/' + mollomSessionId,
+  $.getJSON(Drupal.settings.basePath + 'mollom/captcha/' + newCaptchaType + '/' + formBuildId + '/' + mollomCaptchaId,
     function (data) {
       if (!(data && data.content)) {
         return;
@@ -47,7 +47,7 @@ function getMollomCaptcha() {
       // Inject new CAPTCHA.
       $('.mollom-captcha-content', context).parent().html(data.content);
       // Update session id.
-      $('input.mollom-session-id', context).val(data.session_id);
+      $('input.mollom-captcha-id', context).val(data.captchaId);
       // Add an onclick-event handler for the new link.
       Drupal.attachBehaviors(context);
       // Focus on the CATPCHA input.
diff --git a/mollom.module b/mollom.module
index 33cfc2e..7601ec3 100644
--- a/mollom.module
+++ b/mollom.module
@@ -6,31 +6,6 @@
  */
 
 /**
- * Mollom API version; used for XML-RPC communication with Mollom servers.
- */
-define('MOLLOM_API_VERSION', '1.0');
-
-/**
- * Text analysis result flag: No result.
- */
-define('MOLLOM_ANALYSIS_UNKNOWN', 0);
-
-/**
- * Text analysis result flag: Content is no spam.
- */
-define('MOLLOM_ANALYSIS_HAM', 1);
-
-/**
- * Text analysis result flag: Content is spam.
- */
-define('MOLLOM_ANALYSIS_SPAM', 2);
-
-/**
- * Text analysis result flag: Ambiguous result.
- */
-define('MOLLOM_ANALYSIS_UNSURE', 3);
-
-/**
  * Form protection mode: No protection.
  */
 define('MOLLOM_MODE_DISABLED', 0);
@@ -46,44 +21,16 @@ define('MOLLOM_MODE_CAPTCHA', 1);
 define('MOLLOM_MODE_ANALYSIS', 2);
 
 /**
- * XML-RPC communication failure fallback mode: Block all submissions of protected forms.
+ * Server communication failure fallback mode: Block all submissions of protected forms.
  */
 define('MOLLOM_FALLBACK_BLOCK', 0);
 
 /**
- * XML-RPC communication failure fallback mode: Accept all submissions of protected forms.
+ * Server communication failure fallback mode: Accept all submissions of protected forms.
  */
 define('MOLLOM_FALLBACK_ACCEPT', 1);
 
 /**
- * XML-RPC communication failure: No servers could be reached.
- *
- * @todo Prefix with MOLLOM_.
- */
-define('NETWORK_ERROR', 900);
-
-/**
- * XML-RPC communication failure: Error on Mollom server.
- *
- * @todo Prefix with SERVER_.
- */
-define('MOLLOM_ERROR', 1000);
-
-/**
- * XML-RPC communication failure: Mollom server requests client to update its server list.
- *
- * @todo Prefix with SERVER_.
- */
-define('MOLLOM_REFRESH', 1100);
-
-/**
- * XML-RPC communication failure: Mollom server defers communication to next server in server list.
- *
- * @todo Prefix with SERVER_.
- */
-define('MOLLOM_REDIRECT', 1200);
-
-/**
  * Implements hook_help().
  */
 function mollom_help($path, $arg) {
@@ -256,10 +203,10 @@ function mollom_menu() {
     'type' => MENU_LOCAL_TASK,
     'file' => 'mollom.admin.inc',
   );
-  $items['admin/config/content/mollom/blacklist/delete'] = array(
+  $items['admin/config/content/mollom/blacklist/delete/%'] = array(
     'title' => 'Delete',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('mollom_admin_blacklist_delete'),
+    'page arguments' => array('mollom_admin_blacklist_delete', 6),
     'access arguments' => array('administer mollom'),
     'type' => MENU_CALLBACK,
     'file' => 'mollom.admin.inc',
@@ -412,7 +359,30 @@ function mollom_cron() {
  *   The entity id to retrieve data for.
  */
 function mollom_data_load($entity, $id) {
-  return db_query_range('SELECT * FROM {mollom} WHERE entity = :entity AND id = :id', 0, 1, array(':entity' => $entity, ':id' => $id))->fetchObject();
+  return mollom_db_query_range('SELECT * FROM {mollom} WHERE entity = :entity AND id = :id', 0, 1, array(':entity' => $entity, ':id' => $id))->fetchObject();
+}
+
+/**
+ * Fetches a database record with natural letter casing.
+ *
+ * Drupal core enforces lowercase column names in PDO statements for no
+ * particular reason.
+ *
+ * @see http://drupal.org/node/1171866
+ */
+function mollom_db_query_range($query, $from, $count, array $args = array(), array $options = array()) {
+  if (empty($options['target'])) {
+    $options['target'] = 'default';
+  }
+
+  $connection = Database::getConnection($options['target']);
+  // Backup PDO::ATTR_CASE to restore it afterwards, sticks on the connection.
+  $backup = $connection->getAttribute(PDO::ATTR_CASE);
+  $connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
+  $result = $connection->queryRange($query, $from, $count, $args, $options);
+  $connection->setAttribute(PDO::ATTR_CASE, $backup);
+
+  return $result;
 }
 
 /**
@@ -434,8 +404,9 @@ function mollom_data_load($entity, $id) {
  *   - form_id: The form ID the session data belongs to.
  *   - session_id: The session ID returned by Mollom.
  *   And optionally:
- *   - spam: A spam check result integer returned by Mollom, which can be
- *     MOLLOM_ANALYSIS_SPAM, MOLLOM_ANALYSIS_UNSURE, or MOLLOM_ANALYSIS_HAM.
+ *   - spam: A spam check result double returned by Mollom.
+ *   - spamResult: A final spam classification result string; 'HAM', 'SPAM', or
+ *     'UNSURE'.
  *   - quality: A rating of the content's quality, in the range of 0 and 1.0.
  *   - profanity: A profanity check rating returned by Mollom, in the range of
  *     0 and 1.0.
@@ -482,7 +453,7 @@ function mollom_data_moderate($entity, $id) {
   }
 
   // Report the session to Mollom.
-  _mollom_send_feedback($data->session_id, 'ham');
+  _mollom_send_feedback($data->contentId, 'approve');
 
   // Mark the session data as moderated.
   $data->moderate = 0;
@@ -554,7 +525,7 @@ function mollom_data_delete_form_submit($form, &$form_state) {
   $data = mollom_form_get_values($form_state['values'], $mollom_form['enabled_fields'], $mollom_form['mapping']);
 
   $entity = $mollom_form['entity'];
-  $id = $data['post_id'];
+  $id = $data['postId'];
 
   if (!empty($form_state['values']['mollom']['feedback'])) {
     if (mollom_data_report($entity, $id, $form_state['values']['mollom']['feedback']) === TRUE) {
@@ -592,8 +563,8 @@ function mollom_data_report_multiple($entity, array $ids, $feedback) {
     // Load the Mollom session data.
     $data = mollom_data_load($entity, $id);
     // Send feedback, if we have session data.
-    if (isset($data->session_id)) {
-      $result = _mollom_send_feedback($data->session_id, $feedback);
+    if (isset($data->contentId)) {
+      $result = _mollom_send_feedback($data->contentId, $feedback);
       $return = $return && $result;
     }
   }
@@ -1049,73 +1020,73 @@ function mollom_form_get_values($form_values, $fields, $mapping) {
   // Post id; not sent to Mollom.
   // @see mollom_form_submit()
   if (!empty($mapping['post_id'])) {
-    $data['post_id'] = $mapping['post_id'];
+    $data['postId'] = $mapping['post_id'];
   }
   // Post title.
   if (!empty($mapping['post_title'])) {
-    $data['post_title'] = $mapping['post_title'];
+    $data['postTitle'] = $mapping['post_title'];
   }
   // Post body.
   if (!empty($post_body)) {
-    $data['post_body'] = $post_body;
+    $data['postBody'] = $post_body;
   }
 
   // User name.
   if (!empty($mapping['author_name'])) {
-    $data['author_name'] = $mapping['author_name'];
+    $data['authorName'] = $mapping['author_name'];
     // Try to inherit user from author name.
-    $account = user_load_by_name($data['author_name']);
+    $account = user_load_by_name($data['authorName']);
   }
   elseif (!empty($user->name)) {
-    $data['author_name'] = $user->name;
+    $data['authorName'] = $user->name;
   }
 
   // User e-mail.
   if (!empty($mapping['author_mail'])) {
-    $data['author_mail'] = $mapping['author_mail'];
+    $data['authorMail'] = $mapping['author_mail'];
   }
   elseif (!empty($data['author_name'])) {
     if (!empty($account->mail)) {
-      $data['author_mail'] = $account->mail;
+      $data['authorMail'] = $account->mail;
     }
   }
   elseif (!empty($user->mail)) {
-    $data['author_mail'] = $user->mail;
+    $data['authorMail'] = $user->mail;
   }
 
   // User homepage.
   if (!empty($mapping['author_url'])) {
-    $data['author_url'] = $mapping['author_url'];
+    $data['authorUrl'] = $mapping['author_url'];
   }
 
   // User ID.
   if (!empty($mapping['author_id'])) {
-    $data['author_id'] = $mapping['author_id'];
+    $data['authorId'] = $mapping['author_id'];
   }
   elseif (!empty($data['author_name'])) {
     if (!empty($account->uid)) {
-      $data['author_id'] = $account->uid;
+      $data['authorId'] = $account->uid;
     }
   }
   elseif (!empty($user->uid)) {
-    $data['author_id'] = $user->uid;
+    $data['authorId'] = $user->uid;
   }
 
   // User OpenID.
   if (!empty($mapping['author_openid'])) {
-    $data['author_openid'] = $mapping['author_openid'];
+    $data['authorOpenid'] = $mapping['author_openid'];
   }
   elseif (!empty($data['author_id'])) {
     if (!empty($account->uid) && ($openid = _mollom_get_openid($account))) {
-      $data['author_openid'] = $openid;
+      $data['authorOpenid'] = $openid;
     }
   }
   elseif (!empty($user->uid) && ($openid = _mollom_get_openid($user))) {
-    $data['author_openid'] = $openid;
+    $data['authorOpenid'] = $openid;
   }
 
   // User IP.
-  $data['author_ip'] = ip_address();
+  $data['authorIp'] = ip_address();
 
   // Honeypot.
   // For the Mollom backend, it only matters whether 'honeypot' is non-empty.
@@ -1207,7 +1178,7 @@ function _mollom_status($reset = FALSE) {
 
   // If we have keys and are asked to reset, check whether keys are valid.
   if ($status['keys'] && $reset) {
-    $status['keys valid'] = mollom('mollom.verifyKey', _mollom_get_version());
+    $status['keys valid'] = mollom()->verifyKey();
   }
   // Otherwise, if there are no keys, they cannot be valid.
   elseif (!$status['keys']) {
@@ -1264,12 +1235,6 @@ function _mollom_fallback() {
   if ($fallback == MOLLOM_FALLBACK_BLOCK) {
     form_set_error('mollom', t("The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes."));
   }
-
-  $servers = variable_get('mollom_servers', array());
-  _mollom_watchdog(array(
-    'All servers unavailable: %servers' => array('%servers' => $servers ? implode(', ', $servers) : '--'),
-    'Last error: @code %message' => array('@code' => xmlrpc_errno(), '%message' => xmlrpc_error_msg()),
-  ), WATCHDOG_ERROR);
 }
 
 /**
@@ -1327,9 +1292,6 @@ function mollom_theme() {
  * The 'mollom' form element is stateful. The Mollom session ID that is exchanged
  * between Drupal, the Mollom back-end, and the user allows us to keep track of
  * the form validation state.
- *
- * The session ID is valid for a given $form_id only. We expire it as soon as
- * the form is submitted, to avoid it being replayed.
  */
 function mollom_process_mollom($element, &$form_state, $complete_form) {
   // Setup initial Mollom session and form information.
@@ -1338,11 +1300,12 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
       'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS,
       'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA,
       'passed_captcha' => FALSE,
+      'captcha_type' => 'image',
       'require_moderation' => FALSE,
       'response' => array(
-        'session_id' => '',
       ),
     );
+    // @todo Put the Mollom instance into $form_state.
   }
   $form_state['mollom'] += $element['#mollom_form'];
 
@@ -1354,10 +1317,15 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
   }
 
   // Add the Mollom session element.
-  $element['session_id'] = array(
+  $element['contentId'] = array(
     '#type' => 'hidden',
-    '#default_value' => isset($form_state['mollom']['response']['session_id']) ? $form_state['mollom']['response']['session_id'] : '',
-    '#attributes' => array('class' => 'mollom-session-id'),
+    '#default_value' => isset($form_state['mollom']['response']['content']['contentId']) ? $form_state['mollom']['response']['content']['contentId'] : '',
+    '#attributes' => array('class' => 'mollom-content-id'),
+  );
+  $element['captchaId'] = array(
+    '#type' => 'hidden',
+    '#default_value' => isset($form_state['mollom']['response']['captcha']['captchaId']) ? $form_state['mollom']['response']['captcha']['captchaId'] : '',
+    '#attributes' => array('class' => 'mollom-captcha-id'),
   );
 
   // Add the CAPTCHA element.
@@ -1377,19 +1345,12 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
     // Prevent the page cache from storing a form containing a CAPTCHA element.
     $GLOBALS['conf']['cache'] = 0;
 
-    $data = array();
-    if (!empty($form_state['mollom']['response']['session_id'])) {
-      $data['session_id'] = $form_state['mollom']['response']['session_id'];
-    }
-    $captcha = mollom_get_captcha('image', $data);
-
+    $captcha = mollom_get_captcha($form_state);
     // If we get a response, add the image CAPTCHA to the form element.
-    if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) {
-      $element['captcha']['#field_prefix'] = $captcha['markup'];
-
+    if (!empty($captcha)) {
+      $element['captcha']['#field_prefix'] = $captcha;
       // Assign the session ID returned by Mollom.
-      $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
-      $element['session_id']['#value'] = $captcha['response']['session_id'];
+      $element['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['captchaId'];
     }
     // Otherwise, we have a communication or configuration error.
     // @todo Short-cut form processing entirely in this case; see also
@@ -1397,6 +1358,8 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
     else {
       $form_state['mollom']['require_analysis'] = FALSE;
       $form_state['mollom']['require_captcha'] = FALSE;
+      // Trigger fallback mode.
+      _mollom_fallback();
       return array();
     }
   }
@@ -1453,29 +1416,31 @@ function mollom_validate_analysis(&$form, &$form_state) {
     return;
   }
   $data = $all_data;
-  // Remove 'post_id' property; only used by mollom_form_submit().
-  if (isset($data['post_id'])) {
-    unset($data['post_id']);
+  // Remove postId property; only used by mollom_form_submit().
+  if (isset($data['postId'])) {
+    unset($data['postId']);
+  }
+  if (isset($form_state['mollom']['response']['content']['contentId'])) {
+    $data['contentId'] = $form_state['mollom']['response']['content']['contentId'];
   }
-  $data['session_id'] = $form_state['mollom']['response']['session_id'];
-  $data['checks'] = implode(',', $form_state['mollom']['checks']);
+  $data['checks'] = $form_state['mollom']['checks'];
   $data['strictness'] = $form_state['mollom']['strictness'];
-  $result = mollom('mollom.checkContent', $data);
+  $result = mollom()->checkContent($data);
 
   // Use all available data properties for log messages below.
   $data += $all_data;
 
   // Trigger global fallback behavior if there is no result.
-  if (!isset($result['session_id'])) {
+  if (!is_array($result) || !isset($result['contentId'])) {
     return _mollom_fallback();
   }
 
   // Store the response returned by Mollom.
-  $form_state['mollom']['response'] = $result;
-  $form['mollom']['session_id']['#value'] = $result['session_id'];
+  $form_state['mollom']['response']['content'] = $result;
+  $form['mollom']['contentId']['#value'] = $result['contentId'];
 
   // Prepare watchdog message teaser text.
-  $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40);
+  $teaser = truncate_utf8(strip_tags(isset($data['postTitle']) ? $data['postTitle'] : isset($data['postBody']) ? $data['postBody'] : '--'), 40);
 
   // Handle the profanity check result.
   if (isset($result['profanity']) && $result['profanity'] >= 0.5) {
@@ -1492,17 +1457,17 @@ function mollom_validate_analysis(&$form, &$form_state) {
     ));
   }
 
-  // Handle the spam check result.
+  // Handle the final spam classification result.
   // The Mollom backend is remembering results of previous mollom.checkContent
   // invocations for a single user/post session. When content is re-checked
   // during form validation, the result may change according to the values that
   // have been submitted (which e.g. can change during previews). Only in case
-  // the spam check led to a MOLLOM_ANALYSIS_UNSURE result, and the user solved
-  // the CAPTCHA correctly, subsequent spam check results will likely be
-  // MOLLOM_ANALYSIS_HAM (though not guaranteed).
-  if (isset($result['spam'])) {
-    switch ($result['spam']) {
-      case MOLLOM_ANALYSIS_HAM:
+  // the spam check led to a 'UNSURE' result, and the user solved the CAPTCHA
+  // correctly, subsequent spam check results will likely be 'HAM' (though not
+  // guaranteed).
+  if (isset($result['spamResult'])) {
+    switch ($result['spamResult']) {
+      case 'HAM':
         $form_state['mollom']['require_captcha'] = FALSE;
         _mollom_watchdog(array(
           'Ham: %teaser' => array('%teaser' => $teaser),
@@ -1511,7 +1476,7 @@ function mollom_validate_analysis(&$form, &$form_state) {
         ), WATCHDOG_INFO);
         break;
 
-      case MOLLOM_ANALYSIS_SPAM:
+      case 'SPAM':
         $form_state['mollom']['require_captcha'] = FALSE;
         if ($form_state['mollom']['discard']) {
           form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
@@ -1526,7 +1491,7 @@ function mollom_validate_analysis(&$form, &$form_state) {
         ));
         break;
 
-      case MOLLOM_ANALYSIS_UNSURE:
+      case 'UNSURE':
         _mollom_watchdog(array(
           'Unsure: %teaser' => array('%teaser' => $teaser),
           'Data:<pre>@data</pre>' => array('@data' => $data),
@@ -1544,25 +1509,14 @@ function mollom_validate_analysis(&$form, &$form_state) {
           $form['mollom']['captcha']['#access'] = TRUE;
           $form['mollom']['captcha']['#required'] = TRUE;
 
-          $captcha_data = array(
-            'session_id' => $result['session_id'],
-          );
-          $captcha = mollom_get_captcha('image', $captcha_data);
-
+          $captcha = mollom_get_captcha($form_state);
           // If we get a response, add the image CAPTCHA to the form element.
-          if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) {
-            $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
-            $form['mollom']['session_id']['#value'] = $captcha['response']['session_id'];
-            $form['mollom']['captcha']['#field_prefix'] = $captcha['markup'];
+          if (!empty($captcha)) {
+            $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['captchaId'];
+            $form['mollom']['captcha']['#field_prefix'] = $captcha;
           }
         }
         break;
-
-      case MOLLOM_ANALYSIS_UNKNOWN:
-      default:
-        // If we end up here, something went totally wrong.
-        _mollom_fallback();
-        break;
     }
   }
 }
@@ -1604,32 +1558,32 @@ function mollom_validate_captcha(&$form, &$form_state) {
     return;
   }
   $data = array(
-    'session_id' => $form_state['mollom']['response']['session_id'],
-    'captcha_result' => $form_state['values']['mollom']['captcha'],
-    'author_ip' => $all_data['author_ip'],
+    'captchaId' => $form_state['mollom']['response']['captcha']['captchaId'],
+    'solution' => $form_state['values']['mollom']['captcha'],
+    'authorIp' => $all_data['authorIp'],
   );
-  if (isset($all_data['author_id'])) {
-    $data['author_id'] = $all_data['author_id'];
+  if (isset($all_data['authorId'])) {
+    $data['authorId'] = $all_data['authorId'];
   }
   if (isset($all_data['honeypot'])) {
     $data['honeypot'] = $all_data['honeypot'];
   }
-  $result = mollom('mollom.checkCaptcha', $data);
+  $result = mollom()->checkCaptcha($data);
   // Use all available data properties for log messages below.
   $data += $all_data;
 
   // Invoke fallback behavior upon a server error; communication errors are
   // handled by mollom() already. A server error may happen in case of an
   // expired or invalid session_id.
-  if ($result === MOLLOM_ERROR) {
+  if (!is_array($result) || !isset($result['captchaId'])) {
     return _mollom_fallback();
   }
 
   // Store the response for #submit handlers.
   $form_state['mollom']['response']['captcha'] = $result;
-  $form['mollom']['session_id']['#value'] = $form_state['mollom']['response']['session_id'];
+  $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['captchaId'];
 
-  if ($result === TRUE) {
+  if (!empty($result['solved'])) {
     $form_state['mollom']['passed_captcha'] = TRUE;
     $form['mollom']['captcha']['#access'] = FALSE;
 
@@ -1712,11 +1666,18 @@ function mollom_form_submit($form, &$form_state) {
     // the mapped post_id.
     $values = mollom_form_get_values($form_state['values'], array(), $form_state['mollom']['mapping']);
     // We only consider non-empty and non-zero values as valid entity ids.
-    if (!empty($values['post_id'])) {
+    if (!empty($values['postId'])) {
       // Save the Mollom session data.
-      $data = (object) $form_state['mollom']['response'];
+      $response = array();
+      if (isset($form_state['mollom']['response']['content'])) {
+        $response += $form_state['mollom']['response']['content'];
+      }
+      if (isset($form_state['mollom']['response']['captcha'])) {
+        $response += $form_state['mollom']['response']['captcha'];
+      }
+      $data = (object) $response;
       $data->entity = $form_state['mollom']['entity'];
-      $data->id = $values['post_id'];
+      $data->id = $values['postId'];
       $data->form_id = $form_state['mollom']['form_id'];
       // Set the moderation flag for forms accepting bad posts.
       $data->moderate = $form_state['mollom']['require_moderation'];
@@ -1730,141 +1691,23 @@ function mollom_form_submit($form, &$form_state) {
  */
 
 /**
- * Call a remote procedure at the Mollom server.
- *
- * This function automatically adds the information required to authenticate
- * against Mollom.
+ * Instantiates a new Mollom client.
  *
- * @todo Currently, this function's return value mixes actual values and
- *   error values. We should rewrite the error handling so that calling
- *   functions can properly handle error situations.
+ * @param $class
+ *   (optional) The name of a Mollom client implementation class to instantiate.
+ *   Overrides the 'mollom_class' configuration variable. Debug use only.
  */
-function mollom($method, $data = array()) {
-  module_load_include('inc', 'mollom');
-  $messages = array();
-
-  // Initialize refresh variable.
-  $refresh = FALSE;
-
-  // Enable testing mode.
-  if (variable_get('mollom_testing_mode', 0)) {
-    $data['testing'] = TRUE;
+function mollom($class = NULL) {
+  $instance = &drupal_static(__FUNCTION__);
+  if (!isset($class)) {
+    $class = variable_get('mollom_class', 'MollomDrupal');
   }
-
-  // Retrieve the list of Mollom servers from the database.
-  $servers = variable_get('mollom_servers', array());
-
-  if (empty($servers)) {
-    // Retrieve a new list of servers.
-    $servers = _mollom_retrieve_server_list();
-    // If API keys are invalid, a XML-RPC error code is returned.
-    if (!is_array($servers)) {
-      return $servers;
-    }
-
-    $messages[] = array(
-      'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)),
-    );
-
-    // Store the list of servers in the database.
-    variable_set('mollom_servers', $servers);
+  // If there is no instance yet or if it is not from the configured class,
+  // create a new one.
+  if (!isset($instance) || !($instance instanceof $class)) {
+    $instance = new $class();
   }
-
-  if (is_array($servers)) {
-    // Send the request to the first server; if that fails, try the other
-    // servers in the list.
-    reset($servers);
-    while ($server = current($servers)) {
-      $result = xmlrpc($server . '/' . MOLLOM_API_VERSION, array(
-        $method => array($data + _mollom_authentication()),
-      ));
-
-      if ($result === FALSE && ($error = xmlrpc_error())) {
-        if ($error->code === MOLLOM_REFRESH) {
-          // Avoid endless loops.
-          if (!$refresh) {
-            $refresh = TRUE;
-
-            // Retrieve a new list of valid Mollom servers.
-            $servers = _mollom_retrieve_server_list();
-            // If API keys are invalid, the XML-RPC error code is returned.
-            // To reach this, we must have had a server list (and therefore
-            // valid keys) before, so we do not immediately return (like above),
-            // but instead trigger the fallback mode.
-            if (!is_array($servers)) {
-              break;
-            }
-
-            // Reset the list of servers to restart from the first server.
-            reset($servers);
-
-            // Update the server list.
-            variable_set('mollom_servers', $servers);
-
-            $messages[] = array(
-              'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)),
-            );
-          }
-        }
-        elseif ($error->code === MOLLOM_REDIRECT) {
-          // Try the next server in the list.
-          $next = next($servers);
-
-          $messages[] = array(
-            'Server %server redirected to: %next.' => array('%server' => $server, '%next' => $next),
-          );
-        }
-        else {
-          $messages[] = array(
-            'Error @errno from %server for %method: %message' => array(
-              '@errno' => $error->code,
-              '%server' => $server,
-              '%method' => $method,
-              '%message' => $error->message,
-            ),
-            'Data:<pre>@data</pre>' => array('@data' => $data),
-          );
-
-          // Instantly return upon a 'real' error.
-          if ($error->code === MOLLOM_ERROR) {
-            _mollom_watchdog_multiple($messages, WATCHDOG_ERROR);
-            return MOLLOM_ERROR;
-          }
-          // Otherwise, try the next server.
-          next($servers);
-        }
-      }
-      else {
-        _mollom_watchdog_multiple($messages, WATCHDOG_DEBUG);
-        return $result;
-      }
-    }
-  }
-
-  // If none of the servers worked, activate the fallback mechanism.
-  // @todo mollom() can be invoked outside of form processing. _mollom_fallback()
-  //   unconditionally invokes form_set_error(), which always displays the
-  //   fallback error message. Ideally, we would pass a $verbose argument to
-  //   _mollom_fallback(), but for that, we'd have to know here already.
-  //   Consequently, mollom() would need that $verbose argument. In the end, we
-  //   likely want to either embed the fallback handling into form processing,
-  //   or introduce a new helper function that is invoked instead of mollom()
-  //   during form processing.
-  if ($method != 'mollom.verifyKey') {
-    _mollom_fallback();
-  }
-
-  // If everything failed, we reset the server list to force Mollom to request
-  // a new list.
-  variable_del('mollom_servers');
-
-  // Report this error.
-  $messages[] = array(
-    'All servers unreachable or returning errors. The server list was emptied.' => array(),
-  );
-  _mollom_watchdog_multiple($messages, WATCHDOG_ERROR);
-
-  return NETWORK_ERROR;
+  return $instance;
 }
 
 /**
@@ -1901,18 +1744,18 @@ function _mollom_watchdog(array $parts, $severity = WATCHDOG_NOTICE) {
 
   // Prettify replacement token values, if possible.
   foreach ($arguments as $token => $array) {
-    $flat_value = FALSE;
-    if (is_array($array)) {
-      $flat_value = '';
-      foreach ($array as $key => $value) {
-        if (is_array($value)) {
-          $flat_value = FALSE;
-          break;
-        }
-        $value = var_export($value, TRUE);
-        // Indent the new value, so there is a visual separation from the last.
-        $flat_value .= "  {$key} = {$value}\n";
+    if (!is_array($array)) {
+      continue;
+    }
+    $flat_value = '';
+    foreach ($array as $key => $value) {
+      if (is_array($value)) {
+        $flat_value = FALSE;
+        break;
       }
+      $value = var_export($value, TRUE);
+      // Indent the new value, so there is a visual separation from the last.
+      $flat_value .= "  {$key} = {$value}\n";
     }
     // Only convert one-dimensional arrays, or we would lose debugging data.
     if ($flat_value !== FALSE) {
@@ -1942,65 +1785,15 @@ function _mollom_watchdog_multiple($messages, $severity) {
 }
 
 /**
- * Returns version information to send with mollom.verifyKey.
- *
- * Retrieves platform and module version information for mollom.verifyKey, which
- * is normally invoked on Mollom's administration pages only.
- *
- * This information is solely used to speed up support requests and technical
- * inquiries. The data may also be aggregated to help the Mollom staff to make
- * decisions on new features or the necessity of back-porting improved
- * functionality to older versions.
- *
- * @return
- *   An array containing:
- *   - platform_name: The name of the Drupal distribution; i.e., "Drupal".
- *   - platform_version: The version of Drupal; e.g., "7.0".
- *   - client_name: The module name; i.e., "Mollom".
- *   - client_version: The version of the module; e.g., "7.x-1.0".
- *
- * @see _mollom_status()
- */
-function _mollom_get_version() {
-  if ($cache = cache_get('mollom_version')) {
-    return $cache->data;
-  }
-
-  // Retrieve Drupal distribution and installation profile information.
-  $profile = drupal_get_profile();
-  $profile_info = system_get_info('module', $profile) + array(
-    'distribution_name' => 'Drupal',
-    'version' => VERSION,
-  );
-
-  // Retrieve Mollom module information.
-  $mollom_info = system_get_info('module', 'mollom');
-  if (empty($mollom_info['version'])) {
-    // Manually build a module version string for repository checkouts.
-    $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev';
-  }
-
-  $data = array(
-    'platform_name' => $profile_info['distribution_name'],
-    'platform_version' => $profile_info['version'],
-    'client_name' => $mollom_info['name'],
-    'client_version' => $mollom_info['version'],
-  );
-  cache_set('mollom_version', $data);
-
-  return $data;
-}
-
-/**
  * Send feedback to Mollom.
  */
-function _mollom_send_feedback($session_id, $feedback = 'spam') {
-  $result = mollom('mollom.sendFeedback', array(
-    'session_id' => $session_id,
-    'feedback' => $feedback,
+function _mollom_send_feedback($contentId, $feedback = 'spam') {
+  $result = mollom()->sendFeedback(array(
+    'contentId' => $contentId,
+    'moderated' => $feedback,
   ));
   _mollom_watchdog(array(
-    'Reported %feedback for session id %session.' => array('%session' => $session_id, '%feedback' => $feedback),
+    'Reported %feedback for content ID %contentId.' => array('%contentId' => $contentId, '%feedback' => $feedback),
   ));
   return $result;
 }
@@ -2033,8 +1826,8 @@ function mollom_get_statistics($refresh = FALSE) {
       ));
 
       foreach ($statistics as $statistic) {
-        $result = mollom('mollom.getStatistics', array('type' => $statistic));
-        if ($result === NETWORK_ERROR || $result === MOLLOM_ERROR) {
+        $result = mollom()->getStatistics(array('type' => $statistic));
+        if ($result === Mollom::NETWORK_ERROR || $result === Mollom::AUTH_ERROR) {
           // If there was an error, stop fetching statistics and store FALSE
           // in the cache. This will help prevent from making unnecessary
           // requests to Mollom if the service is down or the server cannot
@@ -2091,66 +1884,69 @@ function mollom_field_extra_fields() {
 /**
  * Get the HTML markup for a Mollom CAPTCHA.
  *
- * @param $type
- *   The CAPTCHA type to retrieve, e.g. 'image' or 'audio'.
- * @param $data
- *   An optional array of parameters to send to Mollom when requesting the
- *   CAPTCHA.
+ * @param $form_state
+ *   The current state of a form.
  *
  * @return
- *   An array with the following key/value pairs:
- *     - 'data': An array of parameters sent to Mollom when requesting the
- *       CAPTCHA.
- *     - 'response': An array with the response from Mollom.
- *     - 'markup': The markup of the CAPTCHA HTML.
- */
-function mollom_get_captcha($type, array $data = array()) {
-  $data += array(
-    'author_ip' => ip_address(),
-    'ssl' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on',
-  );
+ *   The markup of the CAPTCHA HTML.
+ */
+function mollom_get_captcha(&$form_state) {
+  $key = 'captcha_url_' . $form_state['mollom']['captcha_type'];
+  if (empty($form_state['mollom']['response'][$key])) {
+    $data = array(
+      'type' => $form_state['mollom']['captcha_type'],
+      'authorIp' => ip_address(),
+      'ssl' => (int) (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'),
+    );
+    if (!empty($form_state['mollom']['response']['content']['contentId'])) {
+      $data['contentId'] = $form_state['mollom']['response']['content']['contentId'];
+    }
+    $result = mollom()->getCaptcha($data);
+
+    if (isset($result['url'])) {
+      $url = $result['url'];
+      $form_state['mollom']['response'][$key] = $url;
+      $form_state['mollom']['response']['captcha']['captchaId'] = $result['captchaId'];
+    }
+    else {
+      return '';
+    }
+  }
+  else {
+    $url = $form_state['mollom']['response'][$key];
+  }
 
   // @todo Convert these to actual theme functions?
   $output = '';
-  switch ($type) {
+  switch ($form_state['mollom']['captcha_type']) {
     case 'audio':
-      $response = mollom('mollom.getAudioCaptcha', $data);
-      if ($response) {
-        $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array(
-          'query' => array('url' => $response['url']),
-          'external' => TRUE,
-        ));
-        $output = '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="110" height="50">';
-        $output .= '<param name="allowFullScreen" value="false" />';
-        $output .= '<param name="movie" value="' . $source . '" />';
-        $output .= '<param name="loop" value="false" />';
-        $output .= '<param name="menu" value="false" />';
-        $output .= '<param name="quality" value="high" />';
-        $output .= '<param name="wmode" value="transparent" />';
-        $output .= '<param name="bgcolor" value="#ffffff" />';
-        $output .= '<embed src="' . $source . '" loop="false" menu="false" quality="high" wmode="transparent" bgcolor="#ffffff" width="110" height="50" align="baseline" allowScriptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer_de" />';
-        $output .= '</object>';
-
-        $output = '<span class="mollom-captcha-content mollom-audio-captcha">' . $output . '</span>';
-        $output .= ' (<a href="#" class="mollom-switch-captcha mollom-image-captcha">' . t('verify using image') . '</a>)';
-      }
+      $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array(
+        'query' => array('url' => $url),
+        'external' => TRUE,
+      ));
+      $output = '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="110" height="50">';
+      $output .= '<param name="allowFullScreen" value="false" />';
+      $output .= '<param name="movie" value="' . $source . '" />';
+      $output .= '<param name="loop" value="false" />';
+      $output .= '<param name="menu" value="false" />';
+      $output .= '<param name="quality" value="high" />';
+      $output .= '<param name="wmode" value="transparent" />';
+      $output .= '<param name="bgcolor" value="#ffffff" />';
+      $output .= '<embed src="' . $source . '" loop="false" menu="false" quality="high" wmode="transparent" bgcolor="#ffffff" width="110" height="50" align="baseline" allowScriptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer_de" />';
+      $output .= '</object>';
+
+      $output = '<span class="mollom-captcha-content mollom-audio-captcha">' . $output . '</span>';
+      $output .= ' (<a href="#" class="mollom-switch-captcha mollom-image-captcha">' . t('verify using image') . '</a>)';
       break;
 
     case 'image':
-      $response = mollom('mollom.getImageCaptcha', $data);
-      if ($response) {
-        $captcha = theme('image', array('path' => url($response['url']), 'alt' => t('Type the characters you see in this picture.'), 'getsize' => FALSE));
-        $output = '<span class="mollom-captcha-content mollom-image-captcha">' . $captcha . '</span>';
-        $output .= ' (<a href="#" class="mollom-switch-captcha mollom-audio-captcha">' . t('verify using audio') . '</a>)';
-      }
+      $captcha = theme('image', array('path' => $url, 'alt' => t('Type the characters you see in this picture.'), 'getsize' => FALSE));
+      $output = '<span class="mollom-captcha-content mollom-image-captcha">' . $captcha . '</span>';
+      $output .= ' (<a href="#" class="mollom-switch-captcha mollom-audio-captcha">' . t('verify using audio') . '</a>)';
       break;
   }
 
-  return array(
-    'data' => $data,
-    'response' => $response,
-    'markup' => $output,
-  );
+  return $output;
 }
 
 /**
diff --git a/mollom.pages.inc b/mollom.pages.inc
index a7f02a5..82d9439 100644
--- a/mollom.pages.inc
+++ b/mollom.pages.inc
@@ -12,18 +12,22 @@
  *   The new CAPTCHA type to retrieve, e.g. 'image' or 'audio'.
  * @param $form_build_id
  *   The internal form build id of the form to update the CAPTCHA for.
- * @param $mollom_session_id
- *   The last known Mollom session id contained in the form.
+ * @param $captchaId
+ *   The last known Mollom CAPTCHA ID contained in the form.
  *
  * @return
  *   A JSON array containing:
  *   - content: The HTML markup for the new CAPTCHA.
- *   - session_id: The Mollom session id for the new CAPTCHA.
+ *   - captchaId: The ID for the new CAPTCHA.
  *
  * @todo Add error handling.
  */
-function mollom_captcha_js($type, $form_build_id, $mollom_session_id) {
-  $captcha = mollom_get_captcha($type, array('session_id' => $mollom_session_id));
+function mollom_captcha_js($type, $form_build_id, $captchaId) {
+  $dummy_state['mollom'] = array(
+    'captcha_type' => $type,
+  );
+  $dummy_state['mollom']['response']['captcha']['captchaId'] = $captchaId;
+  $captcha = mollom_get_captcha($dummy_state);
 
   // Update cached session id in the cached $form_state.
   // We rely on native form caching of Form API to store our Mollom session
@@ -36,20 +40,21 @@ function mollom_captcha_js($type, $form_build_id, $mollom_session_id) {
   // id. Therefore, we need to update the session id in the cached $form_state.
   // @todo Replace the entire CAPTCHA switch/refresh with new AJAX framework
   //   functionality.
-  if (!empty($captcha['response']['session_id'])) {
+  if (!empty($dummy_state['mollom']['response']['session_id'])) {
     if ($cache = cache_get('form_state_' . $form_build_id, 'cache_form')) {
       $form_state = $cache->data;
-      $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
+      $form_state['mollom']['response']['captcha']['captchaId'] = $dummy_state['mollom']['response']['captcha']['captchaId'];
+      // @todo Double-check $cid.
       cache_set('form_state_' . $form_build_id, $form_state, 'cache_form', REQUEST_TIME + 21600);
-      // After successfully updating the cache, replace the original session id.
-      $mollom_session_id = $captcha['response']['session_id'];
+      // After successfully updating the cache, replace the original ID.
+      $captchaId = $form_state['mollom']['response']['captcha']['captchaId'];
     }
   }
 
   // Return new content and new session_id via JSON.
   $data = array(
-    'content' => $captcha['markup'],
-    'session_id' => $mollom_session_id,
+    'content' => $captcha,
+    'captchaId' => $captchaId,
   );
   drupal_json_output($data);
   drupal_exit();
@@ -101,16 +106,15 @@ function mollom_report_form_submit($form, &$form_state) {
     // Load the Mollom session data.
     if ($entity == 'session') {
       $data = new stdClass;
-      $data->session_id = $id;
+      $data->contentId = $id;
     }
     else {
       $data = mollom_data_load($entity, $id);
     }
 
     // Send feedback to Mollom, if we have session data.
-    if (isset($data->session_id) && !empty($form_state['values']['mollom']['feedback'])) {
-      // @todo Check the actual reponse.
-      _mollom_send_feedback($data->session_id, $form_state['values']['mollom']['feedback']);
+    if (isset($data->contentId) && !empty($form_state['values']['mollom']['feedback'])) {
+      _mollom_send_feedback($data->contentId, $form_state['values']['mollom']['feedback']);
       drupal_set_message(t('The content was successfully reported as inappropriate.'));
     }
 
diff --git a/tests/mollom.class.test b/tests/mollom.class.test
new file mode 100644
index 0000000..1d9ef81
--- /dev/null
+++ b/tests/mollom.class.test
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * @file
+ * Unit tests for Mollom class.
+ *
+ * @todo After final Mollom class commit, move all low-level tests from
+ *   mollom.test into this file.
+ */
+
+/**
+ * Tests Mollom class functionality.
+ */
+class MollomClassUnitTestCase extends DrupalUnitTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Mollom class',
+      'description' => 'Tests Mollom class functionality.',
+      'group' => 'Mollom',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    // DrupalUnitTestCase does not autoload classes for whatever reason.
+    module_load_include('inc', 'mollom');
+  }
+
+  /**
+   * Asserts that two values belonging to the same variable are equal.
+   *
+   * Checks to see whether two values, which belong to the same variable name or
+   * identifier, are equal and logs a readable assertion message.
+   *
+   * @param $name
+   *   A name or identifier to use in the assertion message.
+   * @param $first
+   *   The first value to check.
+   * @param $second
+   *   The second value to check.
+   *
+   * @return
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   *
+   * @see MollomWebTestCase::assertNotSame()
+   *
+   * @todo D8: Move into core. This improved assertEqual() did not get into D7,
+   *   since the function signature differs and it's plenty of work to manually
+   *   update all assertEqual() invocations throughout all tests.
+   */
+  protected function assertSame($name, $first, $second) {
+    $message = t("@name: @first is equal to @second.", array(
+      '@name' => $name,
+      '@first' => var_export($first, TRUE),
+      '@second' => var_export($second, TRUE),
+    ));
+    $this->assertEqual($first, $second, $message);
+  }
+
+  /**
+   * Asserts that two values belonging to the same variable are not equal.
+   *
+   * Checks to see whether two values, which belong to the same variable name or
+   * identifier, are not equal and logs a readable assertion message.
+   *
+   * @param $name
+   *   A name or identifier to use in the assertion message.
+   * @param $first
+   *   The first value to check.
+   * @param $second
+   *   The second value to check.
+   *
+   * @return
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   *
+   * @see MollomWebTestCase::assertSame()
+   */
+  protected function assertNotSame($name, $first, $second) {
+    $message = t("@name: @first is not equal to @second.", array(
+      '@name' => $name,
+      '@first' => var_export($first, TRUE),
+      '@second' => var_export($second, TRUE),
+    ));
+    $this->assertNotEqual($first, $second, $message);
+  }
+
+  /**
+   * Tests Mollom::httpBuildQuery().
+   */
+  function testHttpBuildQuery() {
+    $input = array('foo' => 1, 'bar' => 2);
+    $expected = 'foo=1&bar=2';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+
+    $input = array('checks' => array('foo' => 'spam', 'bar' => 'profanity'));
+    $expected = 'checks[foo]=spam&checks[bar]=profanity';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+
+    $input = array('checks' => array('spam', 'profanity'));
+    $expected = 'checks=spam&checks=profanity';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+
+    $input = array('checks' => array(array('spam'), array('profanity')));
+    $expected = 'checks=spam&checks=profanity';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+
+    $input = array('checks' => array('spam', ''));
+    $expected = 'checks=spam&checks=';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+
+    $input = array('checks' => 'spam');
+    $expected = 'checks=spam';
+    $this->assertSame(var_export($input, TRUE), Mollom::httpBuildQuery($input), $expected);
+  }
+
+  /**
+   * Tests Mollom::httpParseQuery().
+   */
+  function testHttpParseQuery() {
+    $input = 'foo=1&bar=2';
+    $expected = array('foo' => 1, 'bar' => 2);
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    $input = 'checks=spam&checks=profanity';
+    $expected = array('checks' => array('spam', 'profanity'));
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    // Mollom::httpParseQuery() does not attempt to work transparently. Thus,
+    // multiple parameter names containing brackets itself (regular PHP syntax)
+    // will lead to an "unexpected" result. Although it wouldn't be hard to add
+    // support for this, there's currently no need for it.
+    $input = 'checks[]=spam&checks[]=profanity';
+    $expected = array('checks' => array(array('spam'), array('profanity')));
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    $input = 'checks=spam&checks=';
+    $expected = array('checks' => array('spam', ''));
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    $input = 'checks=spam&checks';
+    $expected = array('checks' => array('spam', ''));
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    $input = 'checks=spam&';
+    $expected = array('checks' => 'spam');
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+
+    $input = 'checks=spam';
+    $expected = array('checks' => 'spam');
+    $this->assertSame($input, Mollom::httpParseQuery($input), $expected);
+  }
+
+  /**
+   * Tests Mollom::parseXML().
+   */
+  function testParseXML() {
+    $header = '<?xml version="1.0"?>';
+
+    $input = $header . <<<EOF
+<response>
+  <code>0</code>
+  <message>Foo.</message>
+  <content>
+    <contentId>321</contentId>
+    <languages>
+      <language>
+        <code>en</code>
+        <confidence>1.0</confidence>
+      </language>
+      <language>
+        <code>de</code>
+        <confidence>0.5</confidence>
+      </language>
+    </languages>
+  </content>
+</response>
+EOF;
+    $expected = array(
+      'code' => 0,
+      'message' => 'Foo.',
+      'content' => array(
+        'contentId' => 321,
+        'languages' => array(
+          array('code' => 'en', 'confidence' => 1.0),
+          array('code' => 'de', 'confidence' => 0.5),
+        ),
+      ),
+    );
+    $this->assertSame($input, Mollom::parseXML(new SimpleXmlIterator($input)), $expected);
+
+    $input = $header . <<<EOF
+<response>
+  <code>0</code>
+  <message></message>
+  <site>
+    <siteId>321</siteId>
+    <servers>
+      <server>http://foo</server>
+      <server>http://bar</server>
+    </servers>
+  </site>
+</response>
+EOF;
+    $expected = array(
+      'code' => 0,
+      'message' => '',
+      'site' => array(
+        'siteId' => 321,
+        'servers' => array(
+          'http://foo',
+          'http://bar',
+        ),
+      ),
+    );
+    $this->assertSame($input, Mollom::parseXML(new SimpleXmlIterator($input)), $expected);
+  }
+}
+
diff --git a/tests/mollom.test b/tests/mollom.test
index 5079d52..ce2c546 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -81,22 +81,41 @@ class MollomWebTestCase extends DrupalWebTestCase {
   protected $is_reseller = FALSE;
 
   /**
+   * The Mollom client class implementation to use.
+   *
+   * By default, we use MollomDrupalTest and test against production Mollom
+   * testing servers.
+   * Assign MollomDrupalTestLocal to test against local dummy/fake REST server.
+   *
+   * @see mollom.drupal.inc
+   *
+   * @var string
+   */
+  protected $mollomClass = 'MollomDrupalTestLocal';
+
+  /**
    * Set up an administrative user account and testing keys.
    */
   function setUp() {
     // Re-initialize stored session_id and watchdog messages.
-    $this->resetSessionID();
+    $this->resetResponseID();
     $this->messages = array();
 
     $modules = func_get_args();
     $modules = (isset($modules[0]) ? $modules[0] : array());
 
+    // Automatically enable local testing server implementation.
+    if (strstr($this->mollomClass, 'Local') && !in_array('mollom_test', $modules)) {
+      $modules[] = 'mollom_test';
+    }
+
     // If not explicitly disabled by a test, setup with Mollom and default admin
     // user.
     if (empty($this->disableDefaultSetup)) {
       $modules[] = 'mollom';
       $modules[] = 'dblog';
       parent::setUp($modules);
+      variable_set('mollom_class', $this->mollomClass);
 
       $permissions = array(
         'access administration pages',
@@ -113,6 +132,7 @@ class MollomWebTestCase extends DrupalWebTestCase {
     else {
       $modules[] = 'dblog';
       parent::setUp($modules);
+      variable_set('mollom_class', $this->mollomClass);
     }
 
     // D7's new default theme Bartik is bogus in various locations, which leads
@@ -184,7 +204,7 @@ class MollomWebTestCase extends DrupalWebTestCase {
     }
     // Assert that there was a severe message, in case we expected one.
     if ($fail_expected && !$had_severe_message) {
-      $this->fail(t('Severe log message not found.'), t('Watchdog'));
+      $this->fail(t('Severe log message was found.'), t('Watchdog'));
     }
     // Delete processed watchdog messages.
     if (!empty($this->messages)) {
@@ -200,55 +220,74 @@ class MollomWebTestCase extends DrupalWebTestCase {
    * with a different Mollom server (due to a refreshed server list or being
    * redirected), then we will get a new session_id.
    *
-   * @param $session_id
-   *   A Mollom session_id of the last request, as contained in the XML-RPC
-   *   response.
+   * @param $type
+   *   The type of ID to assert; e.g., 'contentId', 'captchaId'.
+   * @param $id
+   *   The ID of $type in the last request, as returned from Mollom.
    */
-  protected function assertSessionID($session_id) {
+  protected function assertResponseID($type, $id) {
     // Check whether watchdog messages indicate a refresh or redirect.
+    // @todo According to latest internal discussions, session/content/captcha
+    //   IDs are no longer reset and transparently known to all servers. Thus,
+    //   this can be removed?
     foreach ($this->messages as $message) {
       if ($message->message == 'Refreshed servers: %servers' || $message->message == 'Server %server redirected to: %next.') {
-        $this->resetSessionID();
+        // Reset all IDs.
+        $this->resetResponseID();
       }
     }
 
-    if (!isset($this->session_id)) {
+    if (!isset($this->responseIds[$type])) {
       // Use assertTrue() instead of pass(), to test !empty().
-      $this->assertTrue($session_id, t('New session_id: %session_id', array('%session_id' => $session_id)));
-      $this->session_id = $session_id;
+      $this->assertTrue($id, t('New %type: %id', array(
+        '%type' => $type,
+        '%id' => $id,
+      )));
+      $this->responseIds[$type] = $id;
     }
     else {
-      $this->assertSame('session_id', $session_id, $this->session_id);
+      $this->assertSame($type, $id, $this->responseIds[$type]);
     }
-    return $this->session_id;
+    return $this->responseIds[$type];
   }
 
   /**
    * Reset the statically cached Mollom session id.
+   *
+   * @param $type
+   *   The type of ID to reset; e.g., 'contentId', 'captchaId'.
    */
-  protected function resetSessionID() {
-    $this->session_id = NULL;
+  protected function resetResponseID($type = NULL) {
+    if (isset($type)) {
+      unset($this->responseIds[$type]);
+    }
+    else {
+      unset($this->responseIds);
+    }
   }
 
   /**
    * Assert a Mollom session id in a form.
    *
-   * This is a wrapper around assertSessionID() allows to assert that a proper
+   * This is a wrapper around assertResponseID() allows to assert that a proper
    * Mollom session id is found in the form contained in the internal browser
    * output. The usual flow is:
    * - drupalGet() or drupalPost() requests or submits a form.
    * - drupalGet() and drupalPost() invoke assertMollomWatchdogMessages()
    *   internally, which records all new watchdog messages.
-   * - This function, assertSessionIDInForm(), is invoked to assert that there
+   * - This function, assertResponseIDInForm(), is invoked to assert that there
    *   is a Mollom session id and, depending on the recorded watchdog messages,
    *   that it either equals the last known session id or the new session id is
    *   used for future comparisons in case of a server redirect.
    * - The return value of this function is used to invoke assertMollomData(),
    *   to verify that the proper session id was stored in the database.
+   *
+   * @param $type
+   *   The type of ID to assert; e.g., 'contentId', 'captchaId'.
    */
-  protected function assertSessionIDInForm() {
-    $session_id = $this->getFieldValueByName('mollom[session_id]');
-    return $this->assertSessionID($session_id);
+  protected function assertResponseIDInForm($type) {
+    $id = $this->getFieldValueByName('mollom[' . $type . ']');
+    return $this->assertResponseID($type, $id);
   }
 
   /**
@@ -379,14 +418,20 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *   The entity type to search for in {mollom}.
    * @param $id
    *   The entity id to search for in {mollom}.
-   * @param $session_id
-   *   (optional) The Mollom session id to assert additionally.
+   * @param $response_type
+   *   (optional) The type of ID to assert; e.g., 'contentId', 'captchaId'.
+   * @param $response_id
+   *   (optional) The ID of $type to assert additionally.
    */
-  protected function assertMollomData($entity, $id, $session_id = NULL) {
+  protected function assertMollomData($entity, $id, $response_type = '', $response_id = NULL) {
     $data = mollom_data_load($entity, $id);
-    $this->assertTrue($data->session_id, t('Mollom session data for %entity @id exists: <pre>@data</pre>', array('%entity' => $entity, '@id' => $id, '@data' => var_export($data, TRUE))));
-    if (isset($session_id)) {
-      $this->assertSame(t('Stored session id'), $data->session_id, $session_id);
+    $this->assertTrue($data->id, t('Mollom session data for %entity @id exists: <pre>@data</pre>', array(
+      '%entity' => $entity,
+      '@id' => $id,
+      '@data' => var_export($data, TRUE),
+    )));
+    if (isset($response_id)) {
+      $this->assertSame(t('Stored session id'), $data->$response_type, $response_id);
     }
     return $data;
   }
@@ -584,12 +629,16 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *
    * @see MollomWebTestCase::resetServerRecords()
    * @see mollom_test_xmlrpc()
+   *
+   * @todo Needs update. May directly use the short name suffixes of testing
+   *   server API functions; i.e., 'content', 'captcha', 'blacklist', etc.
    */
   protected function getServerRecord($method = 'mollom.checkContent') {
     // Map the XML-RPC method name to the corresponding function callback name.
     drupal_load('module', 'mollom_test');
     $method_function_map = mollom_test_xmlrpc();
     $function = $method_function_map[$method];
+    $function = strtr($function, array('xmlrpc_' => ''));
 
     // Retrieve last recorded values.
     $storage = variable_get($function, array());
@@ -614,6 +663,7 @@ class MollomWebTestCase extends DrupalWebTestCase {
     drupal_load('module', 'mollom_test');
     $method_function_map = mollom_test_xmlrpc();
     $function = $method_function_map[$method];
+    $function = strtr($function, array('xmlrpc_' => ''));
 
     // Delete the variable.
     variable_del($function);
@@ -630,7 +680,7 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *
    * @see DrupalWebTestCase->drupalGet()
    * @see MollomWebTestCase->assertMollomWatchdogMessages()
-   * @see MollomWebTestCase->assertSessionID()
+   * @see MollomWebTestCase->assertResponseID()
    */
   protected function drupalGet($path, array $options = array(), array $headers = array()) {
     $output = parent::drupalGet($path, $options, $headers);
@@ -649,7 +699,7 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *     negate the watchdog message severity assertion.
    *
    * @see MollomWebTestCase->assertMollomWatchdogMessages()
-   * @see MollomWebTestCase->assertSessionID()
+   * @see MollomWebTestCase->assertResponseID()
    * @see DrupalWebTestCase->drupalPost()
    */
   protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
@@ -731,10 +781,6 @@ class MollomInstallationTestCase extends MollomWebTestCase {
   }
 
   function setUp() {
-    // Re-initialize stored session_id and watchdog messages.
-    $this->resetSessionID();
-    $this->messages = array();
-
     $this->disableDefaultSetup = TRUE;
     parent::setUp(array('comment'));
 
@@ -884,150 +930,154 @@ class MollomResponseTestCase extends MollomWebTestCase {
    * Tests mollom.checkContent().
    */
   function testCheckContent() {
+    $mollom = mollom();
     $data = array(
-      'author_name' => $this->admin_user->name,
-      'author_mail' => $this->admin_user->mail,
-      'author_id' => $this->admin_user->uid,
-      'author_ip' => ip_address(),
+      'authorName' => $this->admin_user->name,
+      'authorMail' => $this->admin_user->mail,
+      'authorId' => $this->admin_user->uid,
+      'authorIp' => ip_address(),
     );
 
     // Ensure proper response for 'ham' submissions.
     // By default (i.e., omitting 'checks') we expect spam and quality checking
     // only.
-    $data['post_body'] = 'ham';
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'ham';
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM);
-    $this->assertSame('quality', $result['quality'], 1);
+    $this->assertSame('spam', $result['spam'], 0.0);
+    $this->assertSame('spamResult', $result['spamResult'], 'HAM');
+    $this->assertSame('quality', $result['quality'], 1.0);
     $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
-    $session_id = $this->assertSessionID($result['session_id']);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Ensure proper response for 'spam' submissions, re-using session_id.
-    $data['post_body'] = 'spam';
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'spam';
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM);
-    $this->assertSame('quality', $result['quality'], 0);
+    $this->assertSame('spam', $result['spam'], 1.0);
+    $this->assertSame('spamResult', $result['spamResult'], 'SPAM');
+    $this->assertSame('quality', $result['quality'], 0.0);
     $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
-    $session_id = $this->assertSessionID($result['session_id']);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Ensure proper response for 'unsure' submissions, re-using session_id.
-    $data['post_body'] = 'unsure';
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'unsure';
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
+    $this->assertSame('spam', $result['spam'], 0.5);
+    $this->assertSame('spamResult', $result['spamResult'], 'UNSURE');
     $this->assertSame('quality', $result['quality'], 0.5);
     $this->assertTrue(!isset($result['profanity']), 'profanity not returned.');
-    $session_id = $this->assertSessionID($result['session_id']);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Additionally enable profanity checking.
-    $data['post_body'] = 'spam profanity';
-    $data['checks'] = 'spam,quality,profanity';
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'spam profanity';
+    $data['checks'] = array('spam', 'quality', 'profanity');
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_SPAM);
-    $this->assertSame('quality', $result['quality'], 0);
-    $this->assertSame('profanity', $result['profanity'], 1);
-    $session_id = $this->assertSessionID($result['session_id']);
+    $this->assertSame('spam', $result['spam'], 1.0);
+    $this->assertSame('spamResult', $result['spamResult'], 'SPAM');
+    $this->assertSame('quality', $result['quality'], 0.0);
+    $this->assertSame('profanity', $result['profanity'], 1.0);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Change the string to contain profanity only.
-    $data['post_body'] = 'profanity';
-    $data['checks'] = 'spam,quality,profanity';
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'profanity';
+    $data['checks'] = array('spam', 'quality', 'profanity');
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
-    $this->assertSame('quality', $result['quality'], 0);
-    $this->assertSame('profanity', $result['profanity'], 1);
-    $session_id = $this->assertSessionID($result['session_id']);
+    $this->assertSame('spam', $result['spam'], 0.5);
+    $this->assertSame('spamResult', $result['spamResult'], 'UNSURE');
+    $this->assertSame('quality', $result['quality'], 0.0);
+    $this->assertSame('profanity', $result['profanity'], 1.0);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Disable spam checking, only do profanity checking.
-    $data['post_body'] = 'spam profanity';
-    $data['checks'] = 'profanity';
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'spam profanity';
+    $data['checks'] = array('profanity');
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
     $this->assertTrue(!isset($result['spam']), 'spam not returned.');
     $this->assertTrue(!isset($result['quality']), 'quality not returned.');
-    $this->assertSame('profanity', $result['profanity'], 1);
-    $session_id = $this->assertSessionID($result['session_id']);
+    $this->assertSame('profanity', $result['profanity'], 1.0);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     // Pass arbitrary string to profanity checking.
-    $data['post_body'] = $this->randomString(12);
-    $data['session_id'] = $session_id;
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = $this->randomString(12);
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
     $this->assertTrue(!isset($result['spam']), 'spam not returned.');
     $this->assertTrue(!isset($result['quality']), 'quality not returned.');
-    $this->assertSame('profanity', $result['profanity'], 0);
-    $session_id = $this->assertSessionID($result['session_id']);
+    $this->assertSame('profanity', $result['profanity'], 0.0);
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
   }
 
   /**
    * Tests results of mollom.checkContent() across requests for a single session.
    */
   function testCheckContentSession() {
+    $mollom = mollom();
     $data = array(
-      'author_name' => $this->admin_user->name,
-      'author_mail' => $this->admin_user->mail,
-      'author_id' => $this->admin_user->uid,
-      'author_ip' => ip_address(),
+      'authorName' => $this->admin_user->name,
+      'authorMail' => $this->admin_user->mail,
+      'authorId' => $this->admin_user->uid,
+      'authorIp' => ip_address(),
     );
 
     // Sequence: Post unsure spam, correct CAPTCHA, change post into spam,
     // expect it to be ham (due to correct CAPTCHA).
-    $data['post_body'] = 'unsure';
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'unsure';
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
-    $data['session_id'] = $this->assertSessionID($result['session_id']);
+    $this->assertSame('spam', $result['spam'], 0.5);
+    $this->assertSame('spamResult', $result['spamResult'], 'UNSURE');
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
 
     $captcha_data = array(
-      'session_id' => $data['session_id'],
-      'author_ip' => $data['author_ip'],
+      'type' => 'image',
+      'contentId' => $data['contentId'],
+      'authorIp' => $data['authorIp'],
     );
-    $result = mollom('mollom.getImageCaptcha', $captcha_data);
+    $result = $mollom->getCaptcha($captcha_data);
     $this->assertMollomWatchdogMessages();
-    $data['session_id'] = $this->assertSessionID($result['session_id']);
+    $data['captchaId'] = $this->assertResponseID('captchaId', $result['captchaId']);
 
     $captcha_data = array(
-      'session_id' => $data['session_id'],
-      'author_ip' => $data['author_ip'],
-      'author_id' => $data['author_id'],
-      'captcha_result' => 'correct',
+      'captchaId' => $data['captchaId'],
+      'contentId' => $data['contentId'],
+      'authorIp' => $data['authorIp'],
+      'authorId' => $data['authorId'],
+      'solution' => 'correct',
     );
-    $result = mollom('mollom.checkCaptcha', $captcha_data);
+    $result = $mollom->checkCaptcha($captcha_data);
     $this->assertMollomWatchdogMessages();
-    $this->assertIdentical($result, TRUE, t('CAPTCHA response was correct.'));
+    $this->assertSame('solved', $result['solved'], 1);
 
-    $data['post_body'] = 'spam';
-    $result = mollom('mollom.checkContent', $data);
+    $data['postBody'] = 'spam';
+    $result = $mollom->checkContent($data);
     $this->assertMollomWatchdogMessages();
-    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM);
-    $data['session_id'] = $this->assertSessionID($result['session_id']);
+    $this->assertSame('spam', $result['spam'], 0.0);
+    $this->assertSame('spamResult', $result['spamResult'], 'HAM');
+    $data['contentId'] = $this->assertResponseID('contentId', $result['contentId']);
   }
 
   /**
    * Tests mollom.getImageCaptcha().
    */
   function testGetImageCaptcha() {
+    $mollom = mollom();
     // Ensure we get no SSL URL by default.
     $data = array(
-      'author_ip' => ip_address(),
+      'type' => 'image',
+      'authorIp' => ip_address(),
     );
-    $result = mollom('mollom.getImageCaptcha', $data);
+    $result = $mollom->getCaptcha($data);
     $this->assertMollomWatchdogMessages();
     $this->assertTrue(strpos($result['url'], 'http://') === 0, t('CAPTCHA URL uses HTTP protocol.'));
 
     // Ensure we get a SSL URL when passing the 'ssl' parameter.
-    $data = array(
-      'author_ip' => ip_address(),
-      'ssl' => TRUE,
-    );
-    $result = mollom('mollom.getImageCaptcha', $data);
+    $data['ssl'] = TRUE;
+    $result = $mollom->getCaptcha($data);
     $this->assertMollomWatchdogMessages();
     $this->assertTrue(strpos($result['url'], 'https://') === 0, t('CAPTCHA URL uses HTTPS protocol.'));
   }
@@ -1036,22 +1086,24 @@ class MollomResponseTestCase extends MollomWebTestCase {
    * Tests mollom.checkCaptcha().
    */
   function testCheckCaptcha() {
+    $mollom = mollom();
     // Ensure we can send an 'author_id'.
     // Verifying no severe watchdog messages is sufficient, as unsupported
     // parameters would trigger a XML-RPC error.
     $uid = rand();
     $data = array(
-      'author_ip' => ip_address(),
-      'author_id' => $uid,
+      'type' => 'image',
+      'authorIp' => ip_address(),
+      'authorId' => $uid,
     );
-    $result = mollom('mollom.getImageCaptcha', $data);
+    $result = $mollom->getCaptcha($data);
     $this->assertMollomWatchdogMessages();
+    $data['captchaId'] = $this->assertResponseID('captchaId', $result['captchaId']);
 
     $data += array(
-      'session_id' => $result['session_id'],
-      'captcha_result' => 'correct',
+      'solution' => 'correct',
     );
-    $result = mollom('mollom.checkCaptcha', $data);
+    $result = $mollom->checkCaptcha($data);
     $this->assertMollomWatchdogMessages();
   }
 }
@@ -1060,6 +1112,9 @@ class MollomResponseTestCase extends MollomWebTestCase {
  * Tests low-level communication with local fake Mollom server.
  */
 class MollomResponseLocalTestCase extends MollomResponseTestCase {
+  // Re-route Mollom communication to this testing site.
+  protected $mollomClass = 'MollomDrupalTestLocal';
+
   public static function getInfo() {
     return array(
       'name' => 'Server responses (local)',
@@ -1067,13 +1122,6 @@ class MollomResponseLocalTestCase extends MollomResponseTestCase {
       'group' => 'Mollom',
     );
   }
-
-  function setUp() {
-    // Enable testing server implementation.
-    parent::setUp(array('mollom_test'));
-    // Re-route Mollom communication to this testing site.
-    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
-  }
 }
 
 class MollomAccessTestCase extends MollomWebTestCase {
@@ -1140,6 +1188,7 @@ class MollomAccessTestCase extends MollomWebTestCase {
     $this->web_user = $this->drupalCreateUser(array('edit own comments'));
     $this->drupalLogin($this->web_user);
     $edit = array(
+      'subject' => 'ham',
       'comment_body[und][0][value]' => 'ham',
     );
     $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
@@ -1155,6 +1204,7 @@ class MollomAccessTestCase extends MollomWebTestCase {
     $this->clickLink('edit');
 
     $edit = array(
+      'subject' => 'spam',
       'comment_body[und][0][value]' => 'spam',
     );
     $this->drupalPost(NULL, $edit, t('Preview'));
@@ -1179,6 +1229,9 @@ class MollomAccessTestCase extends MollomWebTestCase {
 }
 
 class MollomFallbackTestCase extends MollomWebTestCase {
+  // Re-route Mollom communication to this testing site.
+  protected $mollomClass = 'MollomDrupalTestLocal';
+
   public static function getInfo() {
     return array(
       'name' => 'Fallback behavior',
@@ -1187,11 +1240,6 @@ class MollomFallbackTestCase extends MollomWebTestCase {
     );
   }
 
-  function setUp() {
-    // Enable testing server implementation.
-    parent::setUp(array('mollom_test'));
-  }
-
   /**
    * Make sure that "request new password" submissions can be blocked when
    * the Mollom servers are unreachable.
@@ -1259,8 +1307,7 @@ class MollomFallbackTestCase extends MollomWebTestCase {
     variable_set('mollom_servers', array(
       'http://fake-host-1',
       'http://fake-host-2',
-      $GLOBALS['base_url'] . '/xmlrpc.php?version=',
-      'http://xmlrpc1.mollom.com', // The real server.
+      $GLOBALS['base_url'] . '/mollom-test/rest', // A real server.
       'http://fake-host-3',
     ));
 
@@ -1276,6 +1323,8 @@ class MollomFallbackTestCase extends MollomWebTestCase {
 }
 
 class MollomServerListRecoveryTestCase extends MollomWebTestCase {
+  protected $profile = 'testing';
+
   public static function getInfo() {
     return array(
       'name' => 'Server list recovery',
@@ -1301,16 +1350,20 @@ class MollomServerListRecoveryTestCase extends MollomWebTestCase {
     );
 
     foreach ($list as $servers) {
-      // Call mollom.verifyKey with an invalid server list.  The expected behavior
-      // is that the first call fails, but that the second call succeeds because
-      // the server list is automatically reset or recovered by the Mollom module.
       variable_set('mollom_servers', $servers);
-
-      $key_is_valid = mollom('mollom.verifyKey');
-      $this->assertIdentical($key_is_valid, NETWORK_ERROR, t('The Mollom servers could not be contacted.'));
+      // Reset static, since the Mollom instance is statically cached.
+      drupal_static_reset('mollom');
+
+      // Verify that Mollom::query() fails with an invalid server list and
+      // appropriate messages are logged.
+      $mollom = mollom();
+      $key_is_valid = $mollom->verifyKey();
+      $this->assertIdentical($key_is_valid, Mollom::NETWORK_ERROR, t('The Mollom servers could not be contacted.'));
       $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY);
 
-      $key_is_valid = mollom('mollom.verifyKey');
+      // Verify that Mollom::query() automatically empties the server list and
+      // a directly following, subsequent request attempt succeeds.
+      $key_is_valid = $mollom->verifyKey();
       $this->assertIdentical($key_is_valid, TRUE, t('The Mollom servers could be contacted.'));
       $this->assertMollomWatchdogMessages();
     }
@@ -1318,6 +1371,8 @@ class MollomServerListRecoveryTestCase extends MollomWebTestCase {
 }
 
 class MollomLanguageDetectionTestCase extends MollomWebTestCase {
+  protected $profile = 'testing';
+
   public static function getInfo() {
     return array(
       'name' => 'Language detection',
@@ -1345,10 +1400,14 @@ class MollomLanguageDetectionTestCase extends MollomWebTestCase {
       'zh' => "螽斯羽，诜诜兮。宜尔子孙，振振兮",
     );
 
+    $mollom = mollom();
     foreach ($strings as $language => $text) {
-      $result = mollom('mollom.detectLanguage', array('text' => $text));
-      $this->assertEqual($result[0]['language'], $language, t('A language code was specified and they match.'));
-      $this->assertTrue($result[0]['confidence'] > 0, t('A confidence value was specified and it is greater than 0.'));
+      $result = $mollom->checkContent(array(
+        'checks' => 'language',
+        'postBody' => $text,
+      ));
+      $this->assertEqual($result['language'][0]['language'], $language, t('A language code was specified and they match.'));
+      $this->assertTrue($result['language'][0]['confidence'] > 0, t('A confidence value was specified and it is greater than 0.'));
     }
   }
 }
@@ -1378,139 +1437,110 @@ class MollomBlacklistTestCase extends MollomWebTestCase {
   }
 
   /**
-   * Test the URL blacklist functionality at the API level without using a web interface.
+   * Test the blacklist functionality at the API level without using a web interface.
    */
-  function testUrlBlacklistAPI() {
+  function testBlacklistAPI() {
+    $mollom = mollom();
     // Remove any stale blacklist entries from test runs that did not finish.
-    $blacklist = mollom('mollom.listBlacklistURL');
+    $blacklist = $mollom->getBlacklist();
     foreach ($blacklist as $entry) {
       if (REQUEST_TIME - strtotime($entry['created']) > 86400) {
-        mollom('mollom.removeBlacklistURL', array('url' => $entry['url']));
+        $mollom->deleteBlacklistEntry($entry['id']);
       }
     }
 
     // Blacklist a URL.
     $domain = $this->randomName() . '.com';
-    $result = mollom('mollom.addBlacklistURL', array('url' => 'http://' . $domain));
-    $this->assertTrue($result, t('The URL was blacklisted.'));
-
-    // Check whether posts containing the blacklisted URL are properly blocked.
-    $result = mollom('mollom.checkContent', array(
-      'post_body' => "When the exact URL is present, the post should get blocked: http://{$domain}",
+    $entry = $mollom->createBlacklistEntry(array(
+      'value' => $domain,
+      'context' => 'allFields',
+      'reason' => 'spam',
+      'match' => 'contains',
     ));
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact URL match was blocked.'));
+    $this->assertTrue($entry['id'], t('The URL was blacklisted.'));
 
-    $result = mollom('mollom.checkContent', array(
-      'post_body' => "When the URL is expanded in the back, the post should get blocked: http://{$domain}/oh-my",
+    // Check whether posts containing the blacklisted URL are properly blocked.
+    $result = mollom()->checkContent(array(
+      'postBody' => "When the exact URL is present, the post should get blocked: http://{$domain}",
     ));
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial URL match was blocked.'));
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Exact URL match was blocked.'));
 
-    $result = mollom('mollom.checkContent', array(
-      'post_body' => "When the URL is expanded in the front, the post should get blocked: http://www.{$domain}",
+    $result = mollom()->checkContent(array(
+      'postBody' => "When the URL is expanded in the back, the post should get blocked: http://{$domain}/oh-my",
     ));
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with www-prefix was blocked.'));
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Partial URL match was blocked.'));
 
-    $result = mollom('mollom.checkContent', array(
-      'post_body' => "When the URL has a different schema, the post should get blocked: ftp://www.{$domain}",
+    $result = mollom()->checkContent(array(
+      'postBody' => "When the URL is expanded in the front, the post should get blocked: http://www.{$domain}",
     ));
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('URL with different schema was blocked.'));
+    $this->assertEqual($result['spamResult'], 'SPAM', t('URL with www-prefix was blocked.'));
 
-    // @todo Not implemented yet.
-    /*
-    $result = mollom('mollom.checkContent', array(
-      'post_body' => "When the domain appears on its own, the post should get blocked: www.{$domain}",
+    $result = mollom()->checkContent(array(
+      'postBody' => "When the URL has a different schema, the post should get blocked: ftp://www.{$domain}",
     ));
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Plain domain name with www-prefix was blocked.'));
-    */
+    $this->assertEqual($result['spamResult'], 'SPAM', t('URL with different schema was blocked.'));
 
-    $result = mollom('mollom.removeBlacklistURL', array('url' => 'http://' . $domain));
-    $this->assertTrue($result, t('The blacklisted URL was removed.'));
-  }
-
-  /**
-   * Test the text blacklist functionality at the API level without using a web interface.
-   */
-  function testTextBlacklistAPI() {
-    // Remove any stale blacklist entries from test runs that did not finish.
-    $blacklist = mollom('mollom.listBlacklistText');
-    foreach ($blacklist as $entry) {
-      if (REQUEST_TIME - strtotime($entry['created']) > 86400) {
-        mollom('mollom.removeBlacklistText', array(
-          'text' => $entry['text'],
-          'context' => $entry['context'],
-          'reason' => $entry['reason'],
-        ));
-      }
-    }
+    $result = $mollom->deleteBlacklistEntry($entry['id']);
+    $this->assertIdentical($result, TRUE, t('The blacklisted URL was removed.'));
 
     // Blacklist a word.
     // @todo As of now, only non-numeric, lower-case text seems to be supported.
     $term = drupal_strtolower(preg_replace('/[^a-zA-Z]/', '', $this->randomName()));
-    $data = array(
+    $entry = $mollom->createBlacklistEntry(array(
       'text' => $term,
       'context' => 'everything',
       'reason' => 'spam',
       'match' => 'contains',
-    );
-    $result = mollom('mollom.addBlacklistText', $data);
-    $this->assertIdentical($result, TRUE, t('The text was blacklisted.'));
+    ));
+    $this->assertTrue($entry['id'], t('The text was blacklisted.'));
 
     // Check whether posts containing the blacklisted word are properly blocked.
     $data = array(
-      'post_body' => $term,
+      'postBody' => $term,
     );
-    $result = mollom('mollom.checkContent', $data);
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Identical match was blocked.'));
+    $result = mollom()->checkContent($data);
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Identical match was blocked.'));
 
     $data = array(
-      'post_body' => "When the term is present, the post should get blocked: " . $term,
+      'postBody' => "When the term is present, the post should get blocked: " . $term,
     );
-    $result = mollom('mollom.checkContent', $data);
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.'));
+    $result = mollom()->checkContent($data);
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Exact match was blocked.'));
 
     $data = array(
-      'post_body' => "When match is 'contains', the word can be surrounded by other text: abc" . $term . "def",
+      'postBody' => "When match is 'contains', the word can be surrounded by other text: abc" . $term . "def",
     );
-    $result = mollom('mollom.checkContent', $data);
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was blocked.'));
+    $result = mollom()->checkContent($data);
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Partial match was blocked.'));
 
     // Update the blacklist entry to match the term only exactly.
-    $data = array(
+    $entry = $mollom->updateBlacklistEntry(array(
+      'id' => $entry['id'],
       'text' => $term,
       'context' => 'everything',
       'reason' => 'spam',
       'match' => 'exact',
-    );
-    $result = mollom('mollom.addBlacklistText', $data);
-    $this->assertTrue($result, t('The text was blacklisted.'));
+    ));
+    $this->assertTrue($entry['id'], t('The blacklist entry was updated.'));
 
     $data = array(
-      'post_body' => "When match is 'exact', it has to be exact: " . $term,
+      'postBody' => "When match is 'exact', it has to be exact: " . $term,
     );
-    $result = mollom('mollom.checkContent', $data);
-    $this->assertEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Exact match was blocked.'));
+    $result = mollom()->checkContent($data);
+    $this->assertEqual($result['spamResult'], 'SPAM', t('Exact match was blocked.'));
 
     $data = array(
-      'post_body' => "When match is 'exact', it has to be exact: abc{$term}def",
+      'postBody' => "When match is 'exact', it has to be exact: abc{$term}def",
     );
-    $result = mollom('mollom.checkContent', $data);
-    $this->assertNotEqual($result['spam'], MOLLOM_ANALYSIS_SPAM, t('Partial match was not blocked.'));
+    $result = mollom()->checkContent($data);
+    $this->assertNotEqual($result['spamResult'], 'SPAM', t('Partial match was not blocked.'));
 
-    $data = array(
-      'text' => $term,
-      'context' => 'everything',
-      'reason' => 'spam',
-    );
-    $result = mollom('mollom.removeBlacklistText', $data);
-    $this->assertTrue($result, t('The blacklisted text was removed.'));
+    $result = $mollom->deleteBlacklistEntry($entry['id']);
+    $this->assertIdentical($result, TRUE, t('The blacklisted text was removed.'));
 
     // Try to remove a non-existing entry.
-    $data = array(
-      'text' => $term,
-      'context' => 'everything',
-      'reason' => 'spam',
-    );
-    $result = mollom('mollom.removeBlacklistText', $data);
+    // @todo Ensure that the ID does not exist.
+    $result = $mollom->deleteBlacklistEntry(999);
     $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY);
     $this->assertNotIdentical($result, TRUE, t('Error response for a non-existing blacklist text found.'));
   }
@@ -1670,7 +1700,7 @@ class MollomProfanityTestCase extends MollomWebTestCase {
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertText($this->profanity_message);
     $this->assertNoText(t('Your comment has been posted.'));
-    $session_id = $this->assertSessionIDInForm();
+    $contentId = $this->assertResponseIDInForm('contentId');
 
     $edit["comment_body[$langcode][0][value]"] = 'not profane ham';
     $this->drupalPost(NULL, $edit, t('Save'));
@@ -1678,10 +1708,11 @@ class MollomProfanityTestCase extends MollomWebTestCase {
     $this->assertText(t('Your comment has been posted.'));
     $this->assertRaw('<p>' . $edit["comment_body[$langcode][0][value]"] . '</p>', t('Comment previously containing profanity was found.'));
     $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['subject']))->fetchField();
-    $this->assertMollomData('comment', $cid, $session_id);
+    $this->assertMollomData('comment', $cid, 'contentId', $contentId);
 
     // Sequence: Post unsure spam (not profanity), post profanity along with
     // correct CAPTCHA, and expect that to be discarded.
+    $this->resetResponseID();
     $this->web_user = $this->drupalCreateUser();
     $this->drupalLogin($this->web_user);
     $edit = array(
@@ -1696,7 +1727,8 @@ class MollomProfanityTestCase extends MollomWebTestCase {
     $this->assertCaptchaField();
     $this->assertNoText($this->profanity_message);
     $this->assertNoText(t('Your comment has been posted.'));
-    $session_id = $this->assertSessionIDInForm();
+    $contentId = $this->assertResponseIDInForm('contentId');
+    $captchaId = $this->assertResponseIDInForm('captchaId');
 
     $edit["comment_body[$langcode][0][value]"] = 'unsure profanity';
     $this->postCorrectCaptcha(NULL, $edit, t('Save'));
@@ -1710,6 +1742,9 @@ class MollomProfanityTestCase extends MollomWebTestCase {
  * Tests Mollom form configuration functionality.
  */
 class MollomFormConfigurationTestCase extends MollomWebTestCase {
+  // Re-route Mollom communication to this testing site.
+  protected $mollomClass = 'MollomDrupalTestLocal';
+
   public static function getInfo() {
     return array(
       'name' => 'Form administration',
@@ -1719,9 +1754,7 @@ class MollomFormConfigurationTestCase extends MollomWebTestCase {
   }
 
   function setUp() {
-    parent::setUp(array('mollom_test'));
-    // Re-route Mollom communication to this testing site.
-    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
+    parent::setUp();
 
     $this->drupalLogin($this->admin_user);
   }
@@ -2176,7 +2209,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase {
     // Login and submit a node.
     $this->drupalLogin($this->web_user);
     $this->drupalGet('node/add/article');
-    $session_id = $this->assertSessionIDInForm();
+    $captchaId = $this->assertResponseIDInForm('captchaId');
     $edit = array(
       'title' => 'spam',
       'mollom[captcha]' => 'correct',
@@ -2184,7 +2217,7 @@ class MollomNodeFormTestCase extends MollomWebTestCase {
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->node = $this->drupalGetNodeByTitle($edit['title']);
     $this->assertUrl('node/' . $this->node->nid);
-    $this->assertMollomData('node', $this->node->nid, $session_id);
+    $this->assertMollomData('node', $this->node->nid, 'captchaId', $captchaId);
   }
 }
 
@@ -2240,21 +2273,21 @@ class MollomCommentFormTestCase extends MollomWebTestCase {
     $this->drupalLogin($this->web_user);
     $this->drupalGet('comment/reply/' . $this->node->nid);
     $this->assertCaptchaField();
-    $this->assertSessionIDInForm();
+    $this->assertResponseIDInForm('captchaId');
     $this->assertNoPrivacyLink();
 
     // Try to submit an incorrect answer for the CAPTCHA, without value for
     // required field.
     $this->postIncorrectCaptcha(NULL, array(), t('Preview'));
     $this->assertText(t('Comment field is required.'));
-    $this->assertSessionIDInForm();
+    $this->assertResponseIDInForm('captchaId');
     $this->assertNoPrivacyLink();
 
     // Try to submit a correct answer for the CAPTCHA, still without required
     // field value.
     $this->postCorrectCaptcha(NULL, array(), t('Preview'));
     $this->assertText(t('Comment field is required.'));
-    $session_id = $this->assertSessionIDInForm();
+    $captchaId = $this->assertResponseIDInForm('captchaId');
     $this->assertNoPrivacyLink();
 
     // Finally, we should be able to submit a comment.
@@ -2262,13 +2295,14 @@ class MollomCommentFormTestCase extends MollomWebTestCase {
     $this->assertText(t('Your comment has been posted.'));
     $this->assertRaw('<p>spam</p>', t('Spam comment could be posted with correct CAPTCHA.'));
     $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => 'spam'))->fetchField();
-    $this->assertMollomData('comment', $cid, $session_id);
+    $this->assertMollomData('comment', $cid, 'captchaId', $captchaId);
 
     // Verify we can solve the CAPTCHA directly.
+    $this->resetResponseID();
     $value = 'some more spam';
     $this->drupalGet('comment/reply/' . $this->node->nid);
     $this->assertCaptchaField();
-    $session_id = $this->assertSessionIDInForm();
+    $captchaId = $this->assertResponseIDInForm('captchaId');
     $edit = array(
       'comment_body[und][0][value]' => $value,
       'mollom[captcha]' => 'correct',
@@ -2276,7 +2310,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase {
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertText(t('Your comment has been posted.'));
     $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $value))->fetchField();
-    $this->assertMollomData('comment', $cid, $session_id);
+    $this->assertMollomData('comment', $cid, 'captchaId', $captchaId);
   }
 
   /**
@@ -2300,7 +2334,7 @@ class MollomCommentFormTestCase extends MollomWebTestCase {
     );
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertCaptchaField();
-    $session_id = $this->assertSessionIDInForm();
+    $contentId = $this->assertResponseIDInForm('contentId');
     $this->assertPrivacyLink();
 
     // Try to submit the form by solving the CAPTCHA incorrectly. At this point,
@@ -2308,34 +2342,34 @@ class MollomCommentFormTestCase extends MollomWebTestCase {
     // the comment is still neither ham or spam.
     $this->postIncorrectCaptcha(NULL, array(), t('Save'));
     $this->assertCaptchaField();
-    $session_id = $this->assertSessionIDInForm();
+    $captchaId = $this->assertResponseIDInForm('captchaId');
     $this->assertPrivacyLink();
 
     // Correctly solving the CAPTCHA should accept the form submission.
     $this->postCorrectCaptcha(NULL, array(), t('Save'));
     $this->assertRaw('<p>' . $edit['comment_body[und][0][value]'] . '</p>', t('A comment that may contain spam was found.'));
     $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['comment_body[und][0][value]']))->fetchField();
-    $this->assertMollomData('comment', $cid, $session_id);
+    $this->assertMollomData('comment', $cid, 'contentId', $contentId);
 
     // Try to save a new 'spam' comment; it should be discarded, with no CAPTCHA
     // appearing on the page.
-    $this->resetSessionID();
+    $this->resetResponseID();
     $this->drupalGet('comment/reply/' . $this->node->nid);
     $this->assertPrivacyLink();
     $original_number_of_comments = $this->getCommentCount($this->node->nid);
     $this->assertSpamSubmit(NULL, array('comment_body[und][0][value]'), array(), t('Save'));
-    $session_id = $this->assertSessionIDInForm();
+    $contentId = $this->assertResponseIDInForm('contentId');
     $this->assertCommentCount($this->node->nid, $original_number_of_comments);
     $this->assertPrivacyLink();
 
     // Try to save again; it should be discarded, with no CAPTCHA.
     $this->assertSpamSubmit(NULL, array('comment_body[und][0][value]'), array(), t('Save'));
-    $session_id = $this->assertSessionIDInForm();
+    $contentId = $this->assertResponseIDInForm('contentId');
     $this->assertCommentCount($this->node->nid, $original_number_of_comments);
     $this->assertPrivacyLink();
 
     // Save a new 'ham' comment.
-    $this->resetSessionID();
+    $this->resetResponseID();
     $this->drupalGet('comment/reply/' . $this->node->nid);
     $this->assertPrivacyLink();
     $original_number_of_comments = $this->getCommentCount($this->node->nid);
@@ -2529,7 +2563,7 @@ class MollomResellerTestCase extends MollomWebTestCase {
 
     // Create 3 test sites:
     for ($i = 1; $i <= 3; $i++) {
-      $keys[] = mollom('mollom.createSite', array(
+      $keys[] = mollom()->createSite(array(
         'url' => 'http://example.com/site-'. $i,
         'mail' => 'mail@example.com',
         'status' => 0,
@@ -2543,7 +2577,7 @@ class MollomResellerTestCase extends MollomWebTestCase {
     $sites = mollom('mollom.listSites');
     foreach ($sites as $site) {
       // Retrieve the site information:
-      $details = mollom('mollom.getSite', array('client_key' => $site));
+      $details = mollom()->getSite(array('client_key' => $site));
 
       $this->assertEqual($details['mail'], 'mail@example.com', t('The original information is correctly retrieved from Mollom.'));
       $this->assertEqual($details['status'], 0, t('The original information is correctly retrieved from Mollom.'));
@@ -2553,8 +2587,8 @@ class MollomResellerTestCase extends MollomWebTestCase {
       // valid sites in case someone messed up their Mollom settings!
       if ($details['mail'] == 'mail@example.com' || $details['mail'] == 'root@example.com') {
         // Update the information on the site and verify that it was updated.
-        mollom('mollom.updateSite', array('client_key' => $site, 'mail' => 'root@example.com'));
-        $details = mollom('mollom.getSite', array('client_key' => $site));
+        mollom()->updateSite(array('client_key' => $site, 'mail' => 'root@example.com'));
+        $details = mollom()->getSite(array('client_key' => $site));
         $this->assertEqual($details['mail'], 'root@example.com', t('The updated information is correctly retrieved from Mollom.'));
 
         // Verify that the existing information did not change (partial updates).
@@ -2562,7 +2596,7 @@ class MollomResellerTestCase extends MollomWebTestCase {
         $this->assertEqual($details['testing'], 1, t('The original information is correctly retrieved from Mollom.'));
 
         // Delete the test site:
-        mollom('mollom.deleteSite', array('client_key' => $site));
+        mollom()->deleteSite(array('client_key' => $site));
       }
       else {
         $this->fail(t('We tried to delete a non-test site.'));
@@ -2573,7 +2607,7 @@ class MollomResellerTestCase extends MollomWebTestCase {
     $this->assertMollomWatchdogMessages();
 
     // Retrieve information about a non-existing site:
-    $details = mollom('mollom.getSite', array('client_key' => 'bogus'));
+    $details = mollom()->getSite(array('client_key' => 'bogus'));
     $this->assertEqual(xmlrpc_errno(), TRUE, t('Retrieving information from a non-existing site returned an XML-RPC error.'));
     $this->assertMollomWatchdogMessages(WATCHDOG_EMERGENCY);
 
@@ -2587,6 +2621,9 @@ class MollomResellerTestCase extends MollomWebTestCase {
  * Tests form value processing.
  */
 class MollomDataTestCase extends MollomWebTestCase {
+  // Re-route Mollom communication to this testing site.
+  protected $mollomClass = 'MollomDrupalTestLocal';
+
   public static function getInfo() {
     return array(
       'name' => 'Data processing',
@@ -2595,13 +2632,6 @@ class MollomDataTestCase extends MollomWebTestCase {
     );
   }
 
-  function setUp() {
-    // Enable testing server implementation.
-    parent::setUp(array('mollom_test'));
-    // Re-route Mollom communication to this testing site.
-    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
-  }
-
   /**
    * Test mollom_form_get_values().
    */
@@ -2640,14 +2670,14 @@ class MollomDataTestCase extends MollomWebTestCase {
     );
     $data = mollom_form_get_values($values, $fields, $form_info['mapping']);
 
-    $this->assertSame('post_title', $data['post_title'], $values['subject']);
-    $this->assertSame('post_body', $data['post_body'], $values['message'] . "\n" . $values['parent']['child']);
-    $this->assertSame('author_name', $data['author_name'], $values['name']);
-    $this->assertSame('author_mail', $data['author_mail'], $values['mail']);
-    $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.'));
-    $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.'));
-    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));
-    $this->assertSame('author_ip', $data['author_ip'], ip_address());
+    $this->assertSame('postTitle', $data['postTitle'], $values['subject']);
+    $this->assertSame('postBody', $data['postBody'], $values['message'] . "\n" . $values['parent']['child']);
+    $this->assertSame('authorName', $data['authorName'], $values['name']);
+    $this->assertSame('authorMail', $data['authorMail'], $values['mail']);
+    $this->assertFalse(isset($data['authorUrl']), t('authorUrl: Undefined.'));
+    $this->assertFalse(isset($data['authorOpenid']), t('authorOpenid: Undefined.'));
+    $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.'));
+    $this->assertSame('authorIp', $data['authorIp'], ip_address());
 
     // Verify submitted form values for an registered user.
     $values = array(
@@ -2657,15 +2687,15 @@ class MollomDataTestCase extends MollomWebTestCase {
     );
     $data = mollom_form_get_values($values, $fields, $form_info['mapping']);
 
-    $this->assertSame('post_title', $data['post_title'], $values['subject']);
-    $this->assertSame('post_body', $data['post_body'], $values['message']);
-    $this->assertSame('author_name', $data['author_name'], $this->admin_user->name);
-    $this->assertSame('author_mail', $data['author_mail'], $this->admin_user->mail);
-    $this->assertFalse(isset($data['author_url']), t('author_url: Undefined.'));
+    $this->assertSame('postTitle', $data['postTitle'], $values['subject']);
+    $this->assertSame('postBody', $data['postBody'], $values['message']);
+    $this->assertSame('authorName', $data['authorName'], $this->admin_user->name);
+    $this->assertSame('authorMail', $data['authorMail'], $this->admin_user->mail);
+    $this->assertFalse(isset($data['authorUrl']), t('authorUrl: Undefined.'));
     // @todo Test this.
-    $this->assertFalse(isset($data['author_openid']), t('author_openid: Undefined.'));
-    $this->assertSame('author_id', $data['author_id'], $this->admin_user->uid);
-    $this->assertSame('author_ip', $data['author_ip'], ip_address());
+    $this->assertFalse(isset($data['authorOpenid']), t('authorOpenid: Undefined.'));
+    $this->assertSame('authorId', $data['authorId'], $this->admin_user->uid);
+    $this->assertSame('authorIp', $data['authorIp'], ip_address());
   }
 
   /**
@@ -2702,11 +2732,11 @@ class MollomDataTestCase extends MollomWebTestCase {
 
     // Verify that submitted data equals post data.
     $data = $this->getServerRecord();
-    $this->assertSame('post_title', $data['post_title'], $edit['subject']);
-    $this->assertSame('post_body', $data['post_body'], $edit['comment_body[und][0][value]']);
-    $this->assertSame('author_name', $data['author_name'], $this->web_user->name);
-    $this->assertSame('author_mail', $data['author_mail'], $this->web_user->mail);
-    $this->assertSame('author_id', $data['author_id'], $this->web_user->uid);
+    $this->assertSame('postTitle', $data['postTitle'], $edit['subject']);
+    $this->assertSame('postBody', $data['postBody'], $edit['comment_body[und][0][value]']);
+    $this->assertSame('authorName', $data['authorName'], $this->web_user->name);
+    $this->assertSame('authorMail', $data['authorMail'], $this->web_user->mail);
+    $this->assertSame('authorId', $data['authorId'], $this->web_user->uid);
     $this->assertSame('strictness', $data['strictness'], 'normal');
 
     $this->PostCorrectCaptcha(NULL, array(), t('Save'));
@@ -2715,7 +2745,7 @@ class MollomDataTestCase extends MollomWebTestCase {
 
     // Verify that submitted data equals post data.
     $data = $this->getServerRecord('mollom.checkCaptcha');
-    $this->assertSame('author_id', $data['author_id'], $this->web_user->uid);
+    $this->assertSame('authorId', $data['authorId'], $this->web_user->uid);
 
     // Allow anonymous users to post comments without approval.
     $this->drupalLogin($this->admin_user);
@@ -2750,12 +2780,12 @@ class MollomDataTestCase extends MollomWebTestCase {
 
     // Verify that submitted data equals post data.
     $data = $this->getServerRecord();
-    $this->assertSame('post_title', $data['post_title'], $edit['subject']);
-    $this->assertSame('post_body', $data['post_body'], $edit['comment_body[und][0][value]']);
-    $this->assertSame('author_name', $data['author_name'], $edit['name']);
-    $this->assertSame('author_mail', $data['author_mail'], $edit['mail']);
-    $this->assertSame('author_url', $data['author_url'], $edit['homepage']);
-    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));
+    $this->assertSame('postTitle', $data['postTitle'], $edit['subject']);
+    $this->assertSame('postBody', $data['postBody'], $edit['comment_body[und][0][value]']);
+    $this->assertSame('authorName', $data['authorName'], $edit['name']);
+    $this->assertSame('authorMail', $data['authorMail'], $edit['mail']);
+    $this->assertSame('authorUrl', $data['authorUrl'], $edit['homepage']);
+    $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.'));
 
     $this->PostCorrectCaptcha(NULL, array(), t('Save'));
     $comment = db_query('SELECT * FROM {comment} WHERE subject = :subject', array(':subject' => $edit['subject']))->fetchObject();
@@ -2763,7 +2793,7 @@ class MollomDataTestCase extends MollomWebTestCase {
 
     // Verify that submitted data equals post data.
     $data = $this->getServerRecord('mollom.checkCaptcha');
-    $this->assertFalse(isset($data['author_id']), t('author_id: Undefined.'));
+    $this->assertFalse(isset($data['authorId']), t('authorId: Undefined.'));
 
     // Log in admin user and edit comment containing spam.
     $this->resetServerRecords();
@@ -2879,13 +2909,16 @@ class MollomDataTestCase extends MollomWebTestCase {
     $edit['title'] = 'unsure';
     $this->drupalPost(NULL, $edit, 'Submit');
     $this->assertCaptchaField();
+    $contentId = $this->assertResponseIDInForm('contentId');
+    $captchaId = $this->assertResponseIDInForm('captchaId');
     $this->postCorrectCaptcha(NULL, array(), 'Submit', 'Successful form submission.');
     $new_data = $this->assertMollomData('mollom_test', $mid);
 
     // Verify that only session data was updated.
     $this->assertSame('entity', $data->entity, $new_data->entity);
     $this->assertSame('id', $data->id, $new_data->id);
-    $this->assertNotSame('session_id', $data->session_id, $new_data->session_id);
+    $this->assertNotSame('contentId', $data->contentId, $new_data->contentId);
+    $this->assertNotSame('captchaId', $data->captchaId, $new_data->captchaId);
     $this->assertSame('form_id', $data->form_id, $new_data->form_id);
     $this->assertSame('quality', $data->quality, $new_data->quality);
     $count = db_query("SELECT COUNT(1) FROM {mollom}")->fetchField();
@@ -2901,12 +2934,12 @@ class MollomDataTestCase extends MollomWebTestCase {
 
     // Verify that we additionally sent version data.
     $data = $this->getServerRecord('mollom.verifyKey');
-    $info = _mollom_get_version();
-    $this->assertTrue(!empty($info['platform_name']), t('Version information found.'));
-    $this->assertSame('platform_name', $data['platform_name'], $info['platform_name']);
-    $this->assertSame('platform_version', $data['platform_version'], $info['platform_version']);
-    $this->assertSame('client_name', $data['client_name'], $info['client_name']);
-    $this->assertSame('client_version', $data['client_version'], $info['client_version']);
+    $info = mollom()->getClientInformation();
+    $this->assertTrue(!empty($info['platformName']), t('Version information found.'));
+    $this->assertSame('platformName', $data['platformName'], $info['platformName']);
+    $this->assertSame('platformVersion', $data['platformVersion'], $info['platformVersion']);
+    $this->assertSame('clientName', $data['clientName'], $info['clientName']);
+    $this->assertSame('clientVersion', $data['clientVersion'], $info['clientVersion']);
   }
 }
 
@@ -2937,28 +2970,28 @@ class MollomDataCRUDTestCase extends MollomWebTestCase {
       'entity' => 'type1',
       'id' => 123,
       'form_id' => 'type1_form',
-      'session_id' => 'type1-session-id',
+      'contentId' => 1,
     );
     mollom_data_save($data1);
-    $this->assertMollomData($data1->entity, $data1->id, $data1->session_id);
+    $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId);
 
     // Create a second data record; same ID, different entity type.
     $data2 = (object) array(
       'entity' => 'type2',
       'id' => 123,
       'form_id' => 'type2_form',
-      'session_id' => 'type2-session-id',
+      'contentId' => 2,
     );
     mollom_data_save($data2);
-    $this->assertMollomData($data2->entity, $data2->id, $data2->session_id);
+    $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId);
 
     // Update the first data record.
-    $data1->session_id = 'new-session-id-type1';
+    $data1->contentId = 3;
     mollom_data_save($data1);
 
     // Verify that both records are correct.
-    $this->assertMollomData($data1->entity, $data1->id, $data1->session_id);
-    $this->assertMollomData($data2->entity, $data2->id, $data2->session_id);
+    $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId);
+    $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId);
   }
 
   /**
@@ -2970,7 +3003,7 @@ class MollomDataCRUDTestCase extends MollomWebTestCase {
       'entity' => 'type1',
       'id' => 123,
       'form_id' => 'type1_form',
-      'session_id' => 'type1-session-id',
+      'contentId' => 1,
     );
     mollom_data_save($data1);
 
@@ -2979,20 +3012,20 @@ class MollomDataCRUDTestCase extends MollomWebTestCase {
       'entity' => 'type2',
       'id' => 123,
       'form_id' => 'type2_form',
-      'session_id' => 'type2-session-id',
+      'contentId' => 2,
     );
     mollom_data_save($data2);
 
     // Verify that both records exist.
-    $this->assertMollomData($data1->entity, $data1->id, $data1->session_id);
-    $this->assertMollomData($data2->entity, $data2->id, $data2->session_id);
+    $this->assertMollomData($data1->entity, $data1->id, 'contentId', $data1->contentId);
+    $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId);
 
     // Delete the first data record.
     mollom_data_delete($data1->entity, $data1->id);
 
     // Verify that only the second record remained and was not changed.
     $this->assertNoMollomData($data1->entity, $data1->id);
-    $this->assertMollomData($data2->entity, $data2->id, $data2->session_id);
+    $this->assertMollomData($data2->entity, $data2->id, 'contentId', $data2->contentId);
   }
 }
 
@@ -3055,7 +3088,7 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
     $data = $this->assertMollomData('mollom_test', $mid);
     $record = mollom_test_load($mid);
     $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
-    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('spam', $data->spam, 1.0);
     $this->assertSame('profanity', $data->profanity, 1);
     $this->assertSame('moderate', $data->moderate, 1);
 
@@ -3070,7 +3103,7 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
     $data = $this->assertMollomData('mollom_test', $mid);
     $record = mollom_test_load($mid);
     $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
-    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('spam', $data->spam, 1.0);
     $this->assertSame('profanity', $data->profanity, 1);
     $this->assertSame('moderate', $data->moderate, 1);
 
@@ -3084,7 +3117,7 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
     $data = $this->assertMollomData('mollom_test', $mid);
     $record = mollom_test_load($mid);
     $this->assertEqual($record['status'], 1, t('Published test post found.'));
-    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('spam', $data->spam, 1.0);
     $this->assertSame('profanity', $data->profanity, 1);
     $this->assertSame('moderate', $data->moderate, 0);
 
@@ -3092,9 +3125,9 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
     // marked for moderation.
     $this->drupalLogout();
     $expectations = array(
-      'ham' => array('spam' => MOLLOM_ANALYSIS_HAM, 'profanity' => 0),
-      'unsure' => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
-      $this->randomString() => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
+      'ham' => array('spam' => 0.0, 'spamResult' => 'HAM', 'profanity' => 0),
+      'unsure' => array('spam' => 0.5, 'spamResult' => 'UNSURE', 'profanity' => 0),
+      $this->randomString() => array('spam' => 0.5, 'spamResult' => 'UNSURE', 'profanity' => 0),
     );
     foreach ($expectations as $body => $expected) {
       $edit = array(
@@ -3102,7 +3135,7 @@ class MollomAnalysisTestCase extends MollomWebTestCase {
         'body' => $body,
       );
       $this->drupalPost('mollom-test/form', $edit, 'Submit');
-      if ($expected['spam'] == MOLLOM_ANALYSIS_UNSURE) {
+      if ($expected['spamResult'] == 'UNSURE') {
         $this->postCorrectCaptcha(NULL, array(), 'Submit');
       }
       $mid = $this->assertTestSubmitData();
diff --git a/tests/mollom_test.install b/tests/mollom_test.install
index b4bea98..8c3e9a1 100644
--- a/tests/mollom_test.install
+++ b/tests/mollom_test.install
@@ -42,3 +42,12 @@ function mollom_test_schema() {
   return $schema;
 }
 
+/**
+ * Implements hook_uninstall().
+ */
+function mollom_test_uninstall() {
+  db_delete('variable')
+    ->condition('name', db_like('mollom_test_') . '%', 'LIKE')
+    ->execute();
+}
+
diff --git a/tests/mollom_test.module b/tests/mollom_test.module
index d8d9192..9fd14ff 100644
--- a/tests/mollom_test.module
+++ b/tests/mollom_test.module
@@ -3,6 +3,16 @@
 /**
  * @file
  * Testing functionality for Mollom module.
+ *
+ * @todo Extract testing server into a new mollom_test_server.module. The
+ *   mollom_test.module serves as good example for how to implement Mollom
+ *   support in a Drupal module, but 90% of it pertain to the testing server
+ *   now, so it's hard to explain people what they should look at.
+ */
+
+/**
+ * @defgroup mollom_test_xmlrpc Mollom XML-RPC fake server functions
+ * @{
  */
 
 /**
@@ -12,28 +22,456 @@ function mollom_test_xmlrpc() {
   return array(
     // $data contains a variable amount of properties, so we cannot specify a
     // signature.
-    'mollom.getServerList' => 'mollom_test_get_server_list',
-    'mollom.verifyKey' => 'mollom_test_verify_key',
-    'mollom.checkContent' => 'mollom_test_check_content',
-    'mollom.getImageCaptcha' => 'mollom_test_get_captcha',
-    'mollom.checkCaptcha' => 'mollom_test_check_captcha',
-    'mollom.sendFeedback' => 'mollom_test_send_feedback',
+    'mollom.getServerList' => 'mollom_test_xmlrpc_get_server_list',
+    'mollom.verifyKey' => 'mollom_test_xmlrpc_verify_key',
+    'mollom.checkContent' => 'mollom_test_xmlrpc_check_content',
+    'mollom.getImageCaptcha' => 'mollom_test_xmlrpc_get_captcha',
+    'mollom.checkCaptcha' => 'mollom_test_xmlrpc_check_captcha',
+    'mollom.sendFeedback' => 'mollom_test_xmlrpc_send_feedback',
  );
 }
 
 /**
+ * Converts camelCase request/response parameters to lowercase with underscores.
+ *
+ * @todo Recurse into multi-dimensional arrays.
+ */
+function mollom_test_xmlrpc_convert_params(array $data = array()) {
+  foreach ($data as $key => $value) {
+    // Convert CamelCase to lowercase with underscores.
+    $new_key = strtolower(preg_replace('@(?<=[a-z])([A-Z])@', '_$1', $key));
+    $data[$new_key] = $value;
+    unset($data[$key]);
+  }
+  return $data;
+}
+
+/**
  * XML-RPC callback for mollom.getServerList to retrieve new server list.
  */
+function mollom_test_xmlrpc_get_server_list($data) {
+  $servers = mollom_test_get_server_list($data);
+  foreach ($servers as $key => $url) {
+    $servers[$key] .= '/xmlrpc.php?version=';
+  }
+  return $servers;
+}
+
+/**
+ * XML-RPC callback for mollom.verifyKey to validate API keys.
+ */
+function mollom_test_xmlrpc_verify_key($data) {
+  $valid = mollom_test_verify_key($data);
+  if ($valid) {
+    return TRUE;
+  }
+  xmlrpc_error(Mollom::AUTH_ERROR);
+}
+
+/**
+ * XML-RPC callback for mollom.checkContent to perform textual analysis.
+ */
+function mollom_test_xmlrpc_check_content($data) {
+  return mollom_test_check_content($data);
+}
+
+/**
+ * XML-RPC callback for mollom.getImageCaptcha to fetch a CATPCHA image.
+ */
+function mollom_test_xmlrpc_get_captcha($data) {
+  return mollom_test_get_captcha($data);
+}
+
+/**
+ * XML-RPC callback for mollom.checkCaptcha to validate a CAPTCHA response.
+ */
+function mollom_test_xmlrpc_check_captcha($data) {
+  if (isset($data['captcha_result'])) {
+    $data['solution'] = $data['captcha_result'];
+  }
+
+  return mollom_test_check_captcha($data);
+}
+
+/**
+ * XML-RPC callback for mollom.sendFeedback to send feedback for a moderated post.
+ */
+function mollom_test_xmlrpc_send_feedback($data) {
+  $result = mollom_test_send_feedback($data);
+  if ($result) {
+    return TRUE;
+  }
+  xmlrpc_error(Mollom::AUTH_ERROR);
+}
+
+/**
+ * @} End of "defgroup mollom_test_xmlrpc".
+ */
+
+/**
+ * @defgroup mollom_test_rest Mollom REST fake server functions
+ * @{
+ */
+
+/**
+ * Implements hook_menu() for REST API endpoints.
+ */
+function mollom_test_rest() {
+  $path = 'mollom-test/rest/v1';
+  $base_args = count(explode('/', $path)) - 1;
+  // @todo Consider to use a generic page callback, passing arg(3), the resource
+  //   type, and optionally arg(4), the resource, as argument. This would allow
+  //   us to use PHP Exceptions to throw different status codes and errors. Make
+  //   that page callback dynamically switch the delivery callback (for JSON).
+  $base = array(
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+    'delivery callback' => 'mollom_test_rest_deliver',
+  );
+
+  $items[$path . '/site/%'] = $base + array(
+    'page callback' => 'mollom_test_rest_site',
+    'page arguments' => array($base_args + 2),
+  );
+
+  $items[$path . '/content'] = $base + array(
+    'page callback' => 'mollom_test_rest_content',
+  );
+  $items[$path . '/content/%'] = $base + array(
+    'page callback' => 'mollom_test_rest_content',
+    'page arguments' => array($base_args + 2),
+  );
+
+  $items[$path . '/captcha'] = $base + array(
+    'page callback' => 'mollom_test_rest_captcha',
+  );
+  $items[$path . '/captcha/%'] = $base + array(
+    'page callback' => 'mollom_test_rest_captcha',
+    'page arguments' => array($base_args + 2),
+  );
+
+  $items[$path . '/content/%/feedback'] = $base + array(
+    'page callback' => 'mollom_test_rest_send_feedback',
+    'page arguments' => array($base_args + 2),
+  );
+
+  $items[$path . '/blacklist/%'] = $base + array(
+    'page callback' => 'mollom_test_rest_blacklist',
+    'page arguments' => array($base_args + 2),
+  );
+  $items[$path . '/blacklist/%/%'] = $base + array(
+    'page callback' => 'mollom_test_rest_blacklist',
+    'page arguments' => array($base_args + 2, $base_args + 3),
+  );
+  // @todo Whitelist endpoints.
+
+  return $items;
+}
+
+/**
+ * Returns HTTP request query parameters for the current request.
+ *
+ * @see Mollom::httpBuildQuery()
+ * @see http://php.net/manual/en/wrappers.php.php
+ */
+function mollom_test_rest_get_parameters() {
+  if ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') {
+    $data = Mollom::httpParseQuery($_SERVER['QUERY_STRING']);
+  }
+  elseif ($_SERVER['REQUEST_METHOD'] == 'POST' || $_SERVER['REQUEST_METHOD'] == 'PUT') {
+    $data = Mollom::httpParseQuery(file_get_contents('php://input'));
+  }
+  return $data;
+}
+
+/**
+ * Delivery callback for REST API endpoints.
+ */
+function mollom_test_rest_deliver($page_callback_result) {
+  #drupal_add_http_header('Content-Type', 'application/xml; charset=utf-8');
+  drupal_add_http_header('Content-Type', 'application/xml');
+
+  $xml = new DOMDocument('1.0', 'utf-8');
+  $element = $xml->createElement('response');
+
+  // Append status response parameters.
+  // @todo Add support for custom codes (redirect/refresh) + error messages.
+  // @todo Add support for HTTP/resource errors; e.g., 404, 403, etc.
+  if (!isset($page_callback_result)) {
+    $page_callback_result = FALSE;
+  }
+  $status = array(
+    'code' => $page_callback_result === FALSE ? 1 : 0,
+    'message' => '',
+  );
+  mollom_test_rest_add_xml($xml, $element, $status);
+
+  // Append other response parameters.
+  if ($page_callback_result !== FALSE) {
+    mollom_test_rest_add_xml($xml, $element, $page_callback_result);
+  }
+
+  $xml->appendChild($element);
+  print $xml->saveXML();
+
+  // Perform end-of-request tasks.
+  drupal_page_footer();
+}
+
+function mollom_test_rest_add_xml(DOMDocument $doc, DOMNode $parent, $data, $key = NULL) {
+  if (is_scalar($data)) {
+    // Mollom REST API always uses integers instead of Booleans due to varying
+    // implementations of JSON protocol across client platforms/frameworks.
+    if (is_bool($data)) {
+      $data = (int) $data;
+    }
+
+    $element = $doc->createTextNode($data);
+    $parent->appendChild($element);
+  }
+  else {
+    foreach ($data as $property => $value) {
+      $key = (is_numeric($property) ? 'item' : $property);
+      $element = $doc->createElement($key);
+      $parent->appendChild($element);
+      mollom_test_rest_add_xml($doc, $element, $value, $key);
+    }
+  }
+}
+
+/**
+ * REST callback for CRUD site operations.
+ *
+ * @param $public_key
+ *   The public key of a site. Has to be MOLLOM_TEST_PUBLIC_KEY.
+ */
+function mollom_test_rest_site($id) {
+  $data = mollom_test_rest_get_parameters();
+  if ($_SERVER['REQUEST_METHOD'] == 'PUT') {
+    mollom_test_verify_key($data);
+  }
+  else {
+    module_load_include('php', 'simpletest', 'drupal_web_test_case');
+    module_load_include('test', 'mollom', 'tests/mollom');
+  }
+  // Verify keys (in case of PUT, again).
+  if ($data['publicKey'] !== MOLLOM_TEST_PUBLIC_KEY) {
+    return FALSE;
+  }
+
+  $version = mollom()->getClientInformation();
+  $servers = mollom_test_get_server_list($data);
+  foreach ($servers as $key => &$url) {
+    $url .= '/mollom-test/rest';
+  }
+
+  $response = array(
+    'publicKey' => MOLLOM_TEST_PUBLIC_KEY,
+    'privateKey' => MOLLOM_TEST_PRIVATE_KEY,
+    'url' => $GLOBALS['base_url'],
+    'email' => variable_get('site_mail', ''),
+    'languages' => array(),
+    'subscriptionType' => 'free',
+    'platform' => array(
+      'name' => $version['platformName'],
+      'version' => $version['platformVersion'],
+    ),
+    'client' => array(
+      'name' => $version['clientName'],
+      'version' => $version['clientVersion'],
+    ),
+    'servers' => $servers,
+    // @todo Statistics.
+  );
+  return array('site' => $response);
+}
+
+/**
+ * REST callback for mollom.checkContent to perform textual analysis.
+ */
+function mollom_test_rest_content($contentId = NULL) {
+  $data = mollom_test_rest_get_parameters();
+  if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+    // @todo List/read content.
+    if (empty($contentId)) {
+      return FALSE;
+    }
+    return FALSE;
+  }
+  elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
+    // Update existing content (includes sending feedback).
+    // In case the 'moderated' parameter was passed, the call equals the old
+    // mollom.sendFeedback and we only check whether the parameter value is
+    // correct.
+    if (isset($data['moderated'])) {
+      $valid = is_string($data['moderated']);
+      $valid = $valid && in_array($data['moderated'], array('spam', 'profanity', 'low-quality', 'unwanted', 'approve', 'escalate', 'delete', 'ignore'));
+      return $valid;
+    }
+    if (empty($contentId)) {
+      return FALSE;
+    }
+    if (isset($data['contentId']) && $data['contentId'] != $contentId) {
+      return FALSE;
+    }
+  }
+
+  // Default: Create (POST) or update (PUT) content and check it.
+  return array('content' => mollom_test_check_content($data));
+}
+
+/**
+ * REST callback for mollom.getCaptcha to fetch a CAPTCHA.
+ */
+function mollom_test_rest_captcha($captchaId = NULL) {
+  $data = mollom_test_rest_get_parameters();
+  if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+    // @todo List/read content.
+    if (empty($captchaId)) {
+      return FALSE;
+    }
+    return FALSE;
+  }
+  elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
+    if (empty($captchaId)) {
+      return FALSE;
+    }
+    if (isset($data['captchaId']) && $data['captchaId'] != $captchaId) {
+      return FALSE;
+    }
+    return array('captcha' => mollom_test_check_captcha($data));
+  }
+
+  // POST.
+  return array('captcha' => mollom_test_get_captcha($data));
+}
+
+/**
+ * REST callback for Blacklist API.
+ *
+ * @todo Abstract actual functionality like other REST handlers.
+ */
+function mollom_test_rest_blacklist($siteId, $entryId = NULL, $delete = FALSE) {
+  if (empty($siteId)) {
+    return FALSE;
+  }
+  $data = mollom_test_rest_get_parameters();
+  // Remove authentication parameters.
+  unset($data['publicKey'], $data['time'], $data['hash'], $data['nonce']);
+
+  // Prepare text value.
+  if (isset($data['text'])) {
+    $data['text'] = drupal_strtolower(trim($data['text']));
+  }
+  // @todo API specification/implementation mismatch.
+  if (isset($data['value'])) {
+    $data['value'] = drupal_strtolower(trim($data['value']));
+  }
+
+  $bin = 'mollom_test_blacklist_' . $siteId;
+  $entries = variable_get($bin, array());
+
+  if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+    // List blacklist entries.
+    if (empty($entryId)) {
+      $response = array();
+      // Remove deleted entries (== FALSE).
+      $entries = array_filter($entries);
+      $response['list'] = $entries;
+      // @todo Not required yet.
+      $response['listCount'] = count($entries);
+      $response['listOffset'] = 0;
+      $response['listTotal'] = count($entries);
+      return $response;
+    }
+    // Read a single entry.
+    else {
+      // Check whether the entry exists and was not deleted.
+      if (!empty($entries[$entryId])) {
+        return array('entry' => $entries[$entryId]);
+      }
+      else {
+        return FALSE;
+      }
+    }
+  }
+  elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
+    // Create a new entry.
+    if (!$delete) {
+      $entryId = max(array_keys($entries)) + 1;
+      $data['id'] = $entryId;
+      $entries[$entryId] = $data;
+      variable_set($bin, $entries);
+
+      $response = $data;
+      return array('entry' => $response);
+    }
+    // Delete an existing entry.
+    else {
+      // Check that the entry was not deleted already.
+      if (!empty($entries[$entryId])) {
+        $entries[$entryId] = FALSE;
+        variable_set($bin, $entries);
+        return TRUE;
+      }
+      else {
+        return FALSE;
+      }
+    }
+  }
+  elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
+    // Update an existing entry.
+    if (empty($entryId)) {
+      return FALSE;
+    }
+    if (isset($data['id']) && $data['id'] != $entryId) {
+      return FALSE;
+    }
+    // Check that the entry was not deleted.
+    if (empty($entries[$entryId])) {
+      return FALSE;
+    }
+    // Entry ID cannot be updated.
+    unset($data['id']);
+    $entries[$entryId] = $data;
+    variable_set($bin, $entries);
+    $response = $data;
+    $response['id'] = $entryId;
+    return array('entry' => $response);
+  }
+}
+
+/**
+ * REST callback for mollom.sendFeedback to send feedback for a moderated post.
+ */
+function mollom_test_rest_send_feedback($session_id) {
+  $data = $_POST;
+  $data['session_id'] = $session_id;
+
+  $result = mollom_test_send_feedback($data);
+  if ($result) {
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * @} End of "defgroup mollom_test_rest".
+ */
+
+/**
+ * API callback for mollom.getServerList to retrieve new server list.
+ */
 function mollom_test_get_server_list($data) {
   $storage = variable_get(__FUNCTION__, array());
   $storage[] = $data;
   variable_set(__FUNCTION__, $storage);
 
-  return array($GLOBALS['base_url'] . '/xmlrpc.php?version=');
+  return array($GLOBALS['base_url'], $GLOBALS['base_url']);
 }
 
 /**
- * XML-RPC callback for mollom.verifyKey to validate API keys.
+ * API callback for mollom.verifyKey to validate API keys.
  */
 function mollom_test_verify_key($data) {
   $storage = variable_get(__FUNCTION__, array());
@@ -44,29 +482,27 @@ function mollom_test_verify_key($data) {
   module_load_include('php', 'simpletest', 'drupal_web_test_case');
   module_load_include('test', 'mollom', 'tests/mollom');
 
-  if ($data['public_key'] === MOLLOM_TEST_PUBLIC_KEY) {
-    return TRUE;
-  }
-  xmlrpc_error(MOLLOM_ERROR);
+  return $data['public_key'] === MOLLOM_TEST_PUBLIC_KEY;
 }
 
 /**
- * XML-RPC callback for mollom.checkContent to perform textual analysis.
+ * API callback for mollom.checkContent to perform textual analysis.
  *
  * @todo Add support for 'redirect' and 'refresh' values.
  */
 function mollom_test_check_content($data) {
-  $storage = variable_get(__FUNCTION__, array());
-  $storage[] = $data;
-  variable_set(__FUNCTION__, $storage);
-
   $response = array();
 
+  // If only a single value for checks is passed, it is a string.
+  if (isset($data['checks']) && is_string($data['checks'])) {
+    $data['checks'] = array($data['checks']);
+  }
+
   // Spam filter: Check post_title and post_body for ham, spam, or unsure.
-  if (!isset($data['checks']) || strpos($data['checks'], 'spam') !== FALSE) {
+  if (!isset($data['checks']) || in_array('spam', $data['checks'])) {
     $spam = FALSE;
     $ham = FALSE;
-    foreach (array('post_title', 'post_body') as $key) {
+    foreach (array('postTitle', 'postBody') as $key) {
       if (!isset($data[$key])) {
         continue;
       }
@@ -85,31 +521,36 @@ function mollom_test_check_content($data) {
       }
     }
     if ($spam && $ham) {
-      $response['spam'] = MOLLOM_ANALYSIS_UNSURE;
+      $response['spam'] = 0.5;
+      $response['spamResult'] = 'UNSURE';
       $quality = 0.5;
     }
     elseif ($spam) {
-      $response['spam'] = MOLLOM_ANALYSIS_SPAM;
-      $quality = 0;
+      $response['spam'] = 1.0;
+      $response['spamResult'] = 'SPAM';
+      $quality = 0.0;
     }
     elseif ($ham) {
-      $response['spam'] = MOLLOM_ANALYSIS_HAM;
-      $quality = 1;
+      $response['spam'] = 0.0;
+      $response['spamResult'] = 'HAM';
+      $quality = 1.0;
     }
     else {
-      $response['spam'] = MOLLOM_ANALYSIS_UNSURE;
+      $response['spam'] = 0.5;
+      $response['spamResult'] = 'UNSURE';
       $quality = NULL;
     }
     // In case a previous spam check was unsure and a CAPTCHA was solved, the
     // result is supposed to be ham.
     $captcha_sessions = variable_get('mollom_test_check_captcha_sessions', array());
-    if (!empty($data['session_id']) && !empty($captcha_sessions[$data['session_id']])) {
-      $response['spam'] = MOLLOM_ANALYSIS_HAM;
+    if (!empty($data['captchaId']) && !empty($captcha_sessions[$data['captchaId']])) {
+      $response['spam'] = 0.0;
+      $response['spamResult'] = 'HAM';
     }
   }
 
   // Quality filter.
-  if (!isset($data['checks']) || strpos($data['checks'], 'quality') !== FALSE) {
+  if (!isset($data['checks']) || in_array('quality', $data['checks'])) {
     if (isset($quality)) {
       $response['quality'] = $quality;
     }
@@ -121,9 +562,9 @@ function mollom_test_check_content($data) {
   }
 
   // Profanity filter.
-  if (isset($data['checks']) && strpos($data['checks'], 'profanity') !== FALSE) {
+  if (isset($data['checks']) && in_array('profanity', $data['checks'])) {
     $profanity = 0.0;
-    foreach (array('post_title', 'post_body') as $key) {
+    foreach (array('postTitle', 'postBody') as $key) {
       if (isset($data[$key]) && strpos($data[$key], 'profanity') !== FALSE) {
         $profanity = 1.0;
       }
@@ -131,80 +572,83 @@ function mollom_test_check_content($data) {
     $response['profanity'] = $profanity;
   }
 
-  if (!empty($data['session_id'])) {
-    $response['session_id'] = $data['session_id'];
-  }
-  else {
-    drupal_session_start();
-    $response['session_id'] = session_id();
-  }
+  $storage = variable_get(__FUNCTION__, array());
+  $contentId = (!empty($data['contentId']) ? (int) $data['contentId'] : max(array_keys($storage)) + 1);
+  $storage[$contentId] = $data;
+  $response['contentId'] = $contentId;
+  variable_set(__FUNCTION__, $storage);
 
   return $response;
 }
 
 /**
- * XML-RPC callback for mollom.getImageCaptcha to fetch a CATPCHA image.
+ * API callback for mollom.getImageCaptcha to fetch a CATPCHA image.
  */
 function mollom_test_get_captcha($data) {
-  $storage = variable_get(__FUNCTION__, array());
-  $storage[] = $data;
-  variable_set(__FUNCTION__, $storage);
-
-  drupal_session_start();
+  $response = array();
 
   // Return a HTTPS URL if 'ssl' parameter was passed.
   $base_url = $GLOBALS['base_url'];
   if (!empty($data['ssl'])) {
     $base_url = str_replace('http', 'https', $base_url);
   }
+  $response['url'] = $base_url . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif';
 
-  return array(
-    'session_id' => !empty($data['session_id']) ? $data['session_id'] : session_id(),
-    'url' => $base_url . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif',
-  );
+  $storage = variable_get(__FUNCTION__, array());
+  $captchaId = (!empty($data['captchaId']) ? (int) $data['captchaId'] : max(array_keys($storage)) + 1);
+  $storage[$captchaId] = $data;
+  $response['captchaId'] = $captchaId;
+  variable_set(__FUNCTION__, $storage);
+
+  return $response;
 }
 
 /**
- * XML-RPC callback for mollom.checkCaptcha to validate a CAPTCHA response.
+ * API callback for mollom.checkCaptcha to validate a CAPTCHA response.
  *
  * @todo Add support for 'redirect' and 'refresh' values.
  */
 function mollom_test_check_captcha($data) {
-  $storage = variable_get(__FUNCTION__, array());
-  $storage[] = $data;
-  variable_set(__FUNCTION__, $storage);
+  $response = array();
 
-  if ($data['captcha_result'] == 'correct') {
-    $result = TRUE;
+  if (isset($data['solution']) && $data['solution'] == 'correct') {
+    $response['solved'] = TRUE;
   }
-  if ($data['captcha_result'] == 'incorrect') {
-    $result = FALSE;
+  else {
+    $response['solved'] = FALSE;
+    $response['reason'] = '';
   }
+
+  $storage = variable_get(__FUNCTION__, array());
+  $captchaId = (!empty($data['captchaId']) ? (int) $data['captchaId'] : max(array_keys($storage)) + 1);
+  $storage[$captchaId] = $data;
+  $response['captchaId'] = $captchaId;
+  variable_set(__FUNCTION__, $storage);
+
   $captcha_sessions = variable_get('mollom_test_check_captcha_sessions', array());
-  $captcha_sessions[$data['session_id']] = $result;
+  $captcha_sessions[$captchaId] = $response['solved'];
   variable_set('mollom_test_check_captcha_sessions', $captcha_sessions);
 
-  return $result;
+  return $response;
 }
 
 /**
- * XML-RPC callback for mollom.sendFeedback to send feedback for a moderated post.
+ * API callback for mollom.sendFeedback to send feedback for a moderated post.
  */
 function mollom_test_send_feedback($data) {
   $storage = variable_get(__FUNCTION__, array());
   $storage[] = $data;
   variable_set(__FUNCTION__, $storage);
 
-  if (in_array($data['feedback'], array('spam', 'profanity', 'low-quality', 'unwanted', 'ham'))) {
-    return TRUE;
-  }
-  xmlrpc_error(MOLLOM_ERROR);
+  return in_array($data['feedback'], array('spam', 'profanity', 'low-quality', 'unwanted', 'ham'));
 }
 
 /**
  * Implements hook_menu().
  */
 function mollom_test_menu() {
+  $items = mollom_test_rest();
+
   $items['mollom-test/form'] = array(
     'title' => 'Mollom test form',
     'page callback' => 'drupal_get_form',
