diff --git a/mollom.admin.inc b/mollom.admin.inc
index fa297e1..1c37443 100644
--- a/mollom.admin.inc
+++ b/mollom.admin.inc
@@ -178,11 +178,7 @@ function mollom_admin_configure_form($form, &$form_state, $mollom_form = NULL) {
       );
 
       $modes = array();
-      // Textual analysis, if any elements are available.
-      if (!empty($mollom_form['elements'])) {
-        $modes[MOLLOM_MODE_ANALYSIS] = t('Text analysis');
-      }
-      // CAPTCHA-only, always available.
+      $modes[MOLLOM_MODE_ANALYSIS] = t('Text analysis');
       $modes[MOLLOM_MODE_CAPTCHA] = t('CAPTCHA');
 
       $form['mollom']['mode'] = array(
@@ -223,140 +219,139 @@ function mollom_admin_configure_form($form, &$form_state, $mollom_form = NULL) {
         )),
       ));
 
-      if (!empty($mollom_form['elements'])) {
-        // If not re-configuring an existing protection, make it the default.
-        if (!isset($mollom_form['mode'])) {
-          $form['mollom']['mode']['#default_value'] = MOLLOM_MODE_ANALYSIS;
-        }
+      // If not re-configuring an existing protection, make it the default.
+      if (!isset($mollom_form['mode'])) {
+        $form['mollom']['mode']['#default_value'] = MOLLOM_MODE_ANALYSIS;
+      }
 
-        // Textual analysis filters.
-        $form['mollom']['checks'] = array(
-          '#type' => 'checkboxes',
-          '#title' => t('Text analysis checks'),
-          '#options' => array(
-            'spam' => t('Spam'),
-            'profanity' => t('Profanity'),
+      // Textual analysis filters.
+      $form['mollom']['checks'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Text analysis checks'),
+        '#options' => array(
+          'spam' => t('Spam'),
+          'profanity' => t('Profanity'),
+        ),
+        '#default_value' => $mollom_form['checks'],
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
           ),
-          '#default_value' => $mollom_form['checks'],
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-            ),
+        ),
+      );
+      // Profanity check requires text to analyze.
+      $form['mollom']['checks']['profanity']['#access'] = !empty($mollom_form['elements']);
+
+      // Form elements defined by hook_mollom_form_info() use the
+      // 'parent][child' syntax, which Form API also uses internally for
+      // form_set_error(), and which allows us to recurse into nested fields
+      // during processing of submitted form values. However, since we are using
+      // those keys also as internal values to configure the fields to use for
+      // textual analysis, we need to encode them. Otherwise, a nested field key
+      // would result in the following checkbox attribute:
+      //   '#name' => 'mollom[enabled_fields][parent][child]'
+      // This would lead to a form validation error, because it is a valid key.
+      // By encoding them, we prevent this from happening:
+      //   '#name' => 'mollom[enabled_fields][parent%5D%5Bchild]'
+      $elements = array();
+      foreach ($mollom_form['elements'] as $key => $value) {
+        $elements[rawurlencode($key)] = $value;
+      }
+      $enabled_fields = array();
+      foreach ($mollom_form['enabled_fields'] as $value) {
+        $enabled_fields[] = rawurlencode($value);
+      }
+      $form['mollom']['enabled_fields'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Text fields to analyze'),
+        '#options' => $elements,
+        '#default_value' => $enabled_fields,
+        '#description' => t('Only enable fields that accept text (not numbers). Omit fields that contain sensible data (e.g., credit card numbers) or computed/auto-generated values, as well as author information fields (e.g., name, e-mail).'),
+        '#access' => !empty($mollom_form['elements']),
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
           ),
-        );
+        ),
+      );
 
-        // Form elements defined by hook_mollom_form_info() use the
-        // 'parent][child' syntax, which Form API also uses internally for
-        // form_set_error(), and which allows us to recurse into nested fields
-        // during processing of submitted form values. However, since we are using
-        // those keys also as internal values to configure the fields to use for
-        // textual analysis, we need to encode them. Otherwise, a nested field key
-        // would result in the following checkbox attribute:
-        //   '#name' => 'mollom[enabled_fields][parent][child]'
-        // This would lead to a form validation error, because it is a valid key.
-        // By encoding them, we prevent this from happening:
-        //   '#name' => 'mollom[enabled_fields][parent%5D%5Bchild]'
-        $elements = array();
-        foreach ($mollom_form['elements'] as $key => $value) {
-          $elements[rawurlencode($key)] = $value;
-        }
-        $enabled_fields = array();
-        foreach ($mollom_form['enabled_fields'] as $value) {
-          $enabled_fields[] = rawurlencode($value);
-        }
-        $form['mollom']['enabled_fields'] = array(
-          '#type' => 'checkboxes',
-          '#title' => t('Text fields to analyze'),
-          '#options' => $elements,
-          '#default_value' => $enabled_fields,
-          '#description' => t('Only enable fields that accept text (not numbers). Omit fields that contain sensible data (e.g., credit card numbers) or computed/auto-generated values, as well as author information fields (e.g., name, e-mail).'),
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-            ),
+      $form['mollom']['strictness'] = array(
+        '#type' => 'radios',
+        '#title' => t('Text analysis accuracy'),
+        '#options' => array(
+          'strict' => t('Strict'),
+          'normal' => t('Normal'),
+          'relaxed' => t('Relaxed'),
+        ),
+        '#default_value' => $mollom_form['strictness'],
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
           ),
-        );
+        ),
+      );
 
-        $form['mollom']['strictness'] = array(
-          '#type' => 'radios',
-          '#title' => t('Text analysis accuracy'),
-          '#options' => array(
-            'strict' => t('Strict'),
-            'normal' => t('Normal'),
-            'relaxed' => t('Relaxed'),
-          ),
-          '#default_value' => $mollom_form['strictness'],
-          // Only possible for forms protected via text analysis.
-          '#access' => $modes[MOLLOM_MODE_ANALYSIS],
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-            ),
+      $form['mollom']['unsure'] = array(
+        '#type' => 'radios',
+        '#title' => t('When text analysis is unsure'),
+        '#default_value' => $mollom_form['unsure'],
+        // @todo Add 'accept' and 'discard' actions for unsure posts.
+        '#options' => array(
+          'captcha' => t('Show a CAPTCHA'),
+          'moderate' => t('Retain the post for manual moderation'),
+        ),
+        '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
+        // Only possible for forms protected via text analysis.
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
+            ':input[name="mollom[checks][spam]"]' => array('checked' => TRUE),
           ),
-        );
+        ),
+      );
+      // Only possible for forms supporting moderation of unpublished posts.
+      $form['mollom']['unsure']['moderate']['#access'] = !empty($mollom_form['moderation callback']);
+      // Until more options are supported, the entire field/widget only makes
+      // sense for forms that support moderation of unpublished posts.
+      $form['mollom']['unsure']['#access'] = !empty($mollom_form['moderation callback']);
 
-        $form['mollom']['unsure'] = array(
-          '#type' => 'radios',
-          '#title' => t('When text analysis is unsure'),
-          '#default_value' => $mollom_form['unsure'],
-          // @todo Add 'accept' and 'discard' actions for unsure posts.
-          '#options' => array(
-            'captcha' => t('Show a CAPTCHA'),
-            'moderate' => t('Retain the post for manual moderation'),
-          ),
-          '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
-          // Only possible for forms protected via text analysis.
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-              ':input[name="mollom[checks][spam]"]' => array('checked' => TRUE),
-            ),
-          ),
-        );
+      $form['mollom']['discard'] = array(
+        '#type' => 'radios',
+        '#title' => t('When text analysis identifies spam'),
+        '#default_value' => $mollom_form['discard'],
+        '#options' => array(
+          0 => t('Retain the post for manual moderation'),
+          1 => t('Discard the post'),
+        ),
+        '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
         // Only possible for forms supporting moderation of unpublished posts.
-        $form['mollom']['unsure']['moderate']['#access'] = !empty($mollom_form['moderation callback']);
-        // Until more options are supported, the entire field/widget only makes
-        // sense for forms that support moderation of unpublished posts.
-        $form['mollom']['unsure']['#access'] = !empty($mollom_form['moderation callback']);
-
-        $form['mollom']['discard'] = array(
-          '#type' => 'radios',
-          '#title' => t('When text analysis identifies spam'),
-          '#default_value' => $mollom_form['discard'],
-          '#options' => array(
-            0 => t('Retain the post for manual moderation'),
-            1 => t('Discard the post'),
+        '#access' => !empty($mollom_form['moderation callback']),
+        // Only possible for forms protected via text analysis.
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
+            ':input[name="mollom[checks][spam]"]' => array('checked' => TRUE),
           ),
-          '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
-          // Only possible for forms supporting moderation of unpublished posts.
-          '#access' => !empty($mollom_form['moderation callback']),
-          // Only possible for forms protected via text analysis.
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-              ':input[name="mollom[checks][spam]"]' => array('checked' => TRUE),
-            ),
-          ),
-        );
+        ),
+      );
 
-        $form['mollom']['moderation'] = array(
-          '#type' => 'checkbox',
-          '#title' => t('Allow content to be moderated from the hosted <a href="@moderation-url">@moderation-product</a>', array(
-            '@moderation-url' => 'http://mollom.com/moderation',
-            '@moderation-product' => 'Mollom moderation system',
-          )),
-          '#default_value' => $mollom_form['moderation'],
-          // Only possible for forms which result in a locally stored entity.
-          '#access' => !empty($mollom_form['entity']),
-          // Only possible for forms protected via text analysis.
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-            ),
+      $form['mollom']['moderation'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Allow content to be moderated from the hosted <a href="@moderation-url">@moderation-product</a>', array(
+          '@moderation-url' => 'http://mollom.com/moderation',
+          '@moderation-product' => 'Mollom moderation system',
+        )),
+        '#default_value' => $mollom_form['moderation'],
+        // Only possible for forms which result in a locally stored entity.
+        '#access' => !empty($mollom_form['entity']),
+        // Only possible for forms protected via text analysis.
+        '#states' => array(
+          'visible' => array(
+            ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
           ),
-          '#description' => t('Provides a unified moderation interface, supporting multiple sites, moderation teams, and detailed analytics.'),
-        );
-      }
+        ),
+        '#description' => t('Provides a unified moderation interface, supporting multiple sites, moderation teams, and detailed analytics.'),
+      );
 
       $form['actions']['submit'] = array(
         '#type' => 'submit',
@@ -390,19 +385,15 @@ function mollom_admin_configure_form_validate(&$form, &$form_state) {
   // 'checks' and 'enabled_fields', as their labels do not work with the default
   // #required form error message.
   if ($form_state['storage']['step'] == 'configure') {
-    // Make field checkboxes required, if protection mode is textual analysis.
+    // Make field checkboxes required, if protection mode is text analysis.
     // @see http://drupal.org/node/875722
     $required = ($form_state['values']['mollom']['mode'] == MOLLOM_MODE_ANALYSIS);
     $form['mollom']['checks']['#required'] = $required;
-    $form['mollom']['enabled_fields']['#required'] = $required;
     $form['mollom']['discard']['#required'] = $required;
 
     if ($required && !array_filter($form_state['values']['mollom']['checks'])) {
       form_error($form['mollom']['checks'], t('At least one text analysis check is required.'));
     }
-    if ($required && !array_filter($form_state['values']['mollom']['enabled_fields'])) {
-      form_error($form['mollom']['enabled_fields'], t('At least one field is required for text analysis.'));
-    }
   }
 }
 
diff --git a/mollom.api.php b/mollom.api.php
index 3598be9..e8f2a2b 100644
--- a/mollom.api.php
+++ b/mollom.api.php
@@ -248,6 +248,12 @@
  *     confirmation form constructor to assign the mapped post_id key in $form
  *     as a #value. See http://drupal.org/node/645374 for examples. Optionally
  *     limit access to report options by defining 'report access' permissions.
+ *   - report path: (optional) A Drupal system path pattern to be used for
+ *     reporting an entity to Mollom via a "Report to Mollom" link in e-mail
+ *     notifications. This typically points to the menu router path that allows
+ *     to delete an entity. The placeholder '%id' is dynamically replaced with
+ *     the entity ID. For example, user_mollom_form_list() specifies
+ *     'user/%id/cancel'.
  *   - report access: (optional) A list containing user permission strings, from
  *     which the current user needs to have at least one. Should only be used if
  *     no "report access callback" was defined.
@@ -290,6 +296,8 @@ function hook_mollom_form_list() {
     'delete form file' => array(
       'name' => 'mymodule.pages',
     ),
+    // Specify where to find the delete confirmation form for e-mails.
+    'report path' => 'user/%id/cancel',
     // Optionally limit access to report options on the delete confirmation form.
     'report access' => array('administer users', 'bypass node access'),
   );
diff --git a/mollom.module b/mollom.module
index fe2eb7c..87e8e63 100644
--- a/mollom.module
+++ b/mollom.module
@@ -710,14 +710,13 @@ function mollom_form_alter(&$form, &$form_state, $form_id) {
       $form_state['cache'] = TRUE;
 
       // Add Mollom form validation handlers.
+      // Form-level validation handlers are required, since we need access to
+      // all validated and submitted form values. _form_validate() invokes
+      // #element_validate handlers while it is recursing into the form.
       $form['#validate'][] = 'mollom_validate_analysis';
       $form['#validate'][] = 'mollom_validate_captcha';
       $form['#validate'][] = 'mollom_validate_post';
 
-      // Prepend a submit handler to clean up internal Mollom values from
-      // $form_state['values'].
-      $form += array('#submit' => array());
-      array_unshift($form['#submit'], 'mollom_form_pre_submit');
       // Append a submit handler to store Mollom session data. Requires that
       // the primary submit handler has run already, so a potential 'post_id'
       // mapping can be retrieved from $form_state['values'].
@@ -1000,10 +999,8 @@ function mollom_form_new($form_id) {
   $mollom_form += mollom_form_info($form_id, $form_list[$form_id]['module'], $form_list);
 
   // Enable all fields for textual analysis by default.
-  if (!empty($mollom_form['elements'])) {
-    $mollom_form['checks'] = array('spam');
-    $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
-  }
+  $mollom_form['checks'] = array('spam');
+  $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
 
   return $mollom_form;
 }
@@ -1545,7 +1542,19 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
   // Add the JavaScript.
   $element['#attached']['js'][] = drupal_get_path('module', 'mollom') . '/mollom.js';
 
-  // Add the Mollom session elements.
+  // Add the Mollom session data elements.
+  // These elements resemble the {mollom} database schema. The form validation
+  // handlers will pollute them with values returned by Mollom. For entity
+  // forms, the submitted values will appear in a $entity->mollom property,
+  // which in turn represents the Mollom session data record to be stored.
+  $element['entity'] = array(
+    '#type' => 'value',
+    '#value' => isset($form_state['mollom']['entity']) ? $form_state['mollom']['entity'] : 'mollom_content',
+  );
+  $element['id'] = array(
+    '#type' => 'value',
+    '#value' => NULL,
+  );
   $element['contentId'] = array(
     '#type' => 'hidden',
     '#default_value' => isset($form_state['mollom']['response']['content']['id']) ? $form_state['mollom']['response']['content']['id'] : '',
@@ -1556,6 +1565,25 @@ function mollom_process_mollom($element, &$form_state, $complete_form) {
     '#default_value' => isset($form_state['mollom']['response']['captcha']['id']) ? $form_state['mollom']['response']['captcha']['id'] : '',
     '#attributes' => array('class' => 'mollom-captcha-id'),
   );
+  $element['form_id'] = array(
+    '#type' => 'value',
+    '#value' => $form_state['mollom']['form_id'],
+  );
+  $element['moderate'] = array(
+    '#type' => 'value',
+    '#value' => 0,
+  );
+  $data_spec = array(
+    '#type' => 'value',
+    '#value' => NULL,
+  );
+  $element['spamScore'] = $data_spec;
+  $element['spamClassification'] = $data_spec;
+  $element['solved'] = $data_spec;
+  $element['qualityScore'] = $data_spec;
+  $element['profanityScore'] = $data_spec;
+  $element['reason'] = $data_spec;
+  $element['languages'] = $data_spec;
 
   // Add the CAPTCHA element.
   $element['captcha'] = array(
@@ -1631,6 +1659,7 @@ function mollom_form_add_captcha(&$element, &$form_state) {
     $element['captcha']['#field_prefix'] = $captcha;
     // Assign the session ID returned by Mollom.
     $element['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id'];
+    $form_state['values']['mollom']['captchaId'] = $form_state['mollom']['response']['captcha']['id'];
   }
   // Otherwise, we have a communication or configuration error.
   else {
@@ -1685,7 +1714,14 @@ function mollom_validate_analysis(&$form, &$form_state) {
 
   // Store the response returned by Mollom.
   $form_state['mollom']['response']['content'] = $result;
-  $form['mollom']['contentId']['#value'] = $result['id'];
+  // Set form element values accordingly. Do not overwrite the entity ID with
+  // the contentId, nor a possibly existing captchaId.
+  $result['contentId'] = $result['id'];
+  unset($result['id']);
+  $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
+  // #value has to be set manually to output it in case of a validation error,
+  // since this is not a element-level but a form-level validation handler.
+  $form['mollom']['contentId']['#value'] = $result['contentId'];
 
   // Prepare watchdog message teaser text.
   $teaser = '--';
@@ -1843,7 +1879,14 @@ function mollom_validate_captcha(&$form, &$form_state) {
 
   // Store the response for #submit handlers.
   $form_state['mollom']['response']['captcha'] = $result;
-  $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id'];
+  // Set form element values accordingly. Do not overwrite the entity ID with
+  // the contentId, nor a possibly existing captchaId.
+  $result['captchaId'] = $result['id'];
+  unset($result['id']);
+  $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
+  // #value has to be set manually to output it in case of a validation error,
+  // since this is not a element-level but a form-level validation handler.
+  $form['mollom']['captchaId']['#value'] = $result['captchaId'];
 
   if (!empty($result['solved'])) {
     $form_state['mollom']['passed_captcha'] = TRUE;
@@ -1872,40 +1915,29 @@ function mollom_validate_post(&$form, &$form_state) {
   // the post ends up in a moderation queue. Most callbacks will only want to
   // set or change a value in $form_state.
   if ($form_state['mollom']['require_moderation']) {
+    $form_state['values']['mollom']['moderate'] = 1;
+
     $function = $form_state['mollom']['moderation callback'];
     $function($form, $form_state);
   }
 }
 
 /**
- * Form submit handler to clean up internal Mollom values from $form_state['values'].
- *
- * Some form submit handlers blindly take over and save all submitted form
- * values in $form_state['values'] into the database. To prevent Mollom's
- * internal values from being mistakenly stored somewhere else, this submit
- * handler is prepended to the stack of $form['#submit'] handlers of protected
- * forms.
- *
- * @todo Fix Drupal core to remove the need for separately prepended submit
- *   handlers like this one by making form_state_values_clean() invoke a hook.
- * @see http://drupal.org/node/939510
- */
-function mollom_form_pre_submit($form, &$form_state) {
-  // Some modules are implementing multi-step forms without separate form
-  // submit handlers. In case we reach here and the form will be rebuilt, we
-  // need to defer our submit handling until final submission.
-  if (!empty($form_state['rebuild'])) {
-    return;
-  }
-  // When having passed the form validation stage and reaching the form
-  // submission stage, all submitted form values have been processed into
-  // $form_state['mollom'] already, so the entire top-level 'mollom' key can be
-  // safely removed.
-  unset($form_state['values']['mollom']);
-}
-
-/**
  * Form submit handler to flush Mollom session and form information from cache.
+ *
+ * @todo Check whether this is still needed with mollom_entity_insert(). For
+ *   entity forms, this approach never really worked, since:
+ *   - The primary submit handler fails to set the new ID of a newly stored
+ *     entity in the submitted form values (which has been standardized in core,
+ *     but is not enforced anywhere), so the postId cannot be extracted from
+ *     submitted form values.
+ *   - This submit handler is invoked too early, before the primary submit
+ *     handler processed and saved the entity, so the postId cannot be extracted
+ *     from submitted form values.
+ *   - This submit handler is invoked too late; the primary submit handler might
+ *     send out e-mails directly after saving the entity (e.g.,
+ *     user_register_form_submit()), so mollom_mail_alter() is invoked before
+ *     Mollom session data has been saved.
  */
 function mollom_form_submit($form, &$form_state) {
   // Some modules are implementing multi-step forms without separate form
@@ -2443,6 +2475,10 @@ function mollom_get_captcha(&$form_state) {
  * form submissions.
  *
  * @see mollom_mail_add_report_link()
+ *
+ * @todo With mollom_entity_insert(), $message['params'] might contain an array
+ *   key that has a ::$mollom property holding the Mollom session data,
+ *   potentially eliminating the need for $GLOBALS['mollom'].
  */
 function mollom_mail_alter(&$message) {
   // Attaches the Mollom report link to any mails with IDs specified from the
@@ -2455,7 +2491,7 @@ function mollom_mail_alter(&$message) {
 }
 
 /**
- * Add the 'Report as innapropriate' link to an e-mail message.
+ * Add the 'Report as inappropriate' link to an e-mail message.
  *
  * @param array $message
  *   The message to alter.
@@ -2467,27 +2503,61 @@ function mollom_mail_alter(&$message) {
  */
 function mollom_mail_add_report_link(array &$message, array $mollom) {
   if (!empty($mollom['response']['content']['id']) || !empty($mollom['response']['captcha']['id'])) {
-    $data = (object) $mollom['response'];
+    // Check whether an entity was stored with the submission.
+    $data = FALSE;
     if (!empty($mollom['response']['content']['id'])) {
-      $data->entity = 'mollom_content';
-      $data->id = $data->content['id'];
-      $data->contentId = $data->content['id'];
+      $data = mollom_content_load($mollom['response']['content']['id']);
+    }
+    elseif (!empty($mollom['response']['captcha']['id'])) {
+      $data = mollom_db_query_range('SELECT * FROM {mollom} WHERE captchaId = :captchaId', 0, 1, array(':captchaId' => $mollom['response']['captcha']['id']))->fetchObject();
+    }
+    if (!$data) {
+      // @todo Mollom session data should have been saved earlier already;
+      //   eliminate this.
+      $data = (object) $mollom['response'];
+      if (!empty($mollom['response']['content']['id'])) {
+        $data->entity = 'mollom_content';
+        $data->id = $data->content['id'];
+        $data->contentId = $data->content['id'];
+      }
+      else {
+        $data->entity = 'mollom_captcha';
+        $data->id = $data->captcha['id'];
+        $data->captchaId = $data->captcha['id'];
+      }
+      $data->form_id = $mollom['form_id'];
+      mollom_data_save($data);
+    }
+    // Determine report URI.
+    $mollom_form = mollom_form_load($data->form_id);
+    if (isset($mollom_form['report path'])) {
+      $path = strtr($mollom_form['report path'], array(
+        '%id' => $data->id,
+      ));
     }
     else {
-      $data->entity = 'mollom_captcha';
-      $data->id = $data->captcha['id'];
-      $data->captchaId = $data->captcha['id'];
+      $path = "mollom/report/{$data->entity}/{$data->id}";
     }
-    $data->form_id = $mollom['form_id'];
-    mollom_data_save($data);
     $report_link = t('Report as inappropriate: @link', array(
-      '@link' => url("mollom/report/{$data->entity}/{$data->id}", array('absolute' => TRUE)),
+      '@link' => url($path, array('absolute' => TRUE)),
     ));
     $message['body'][] = $report_link;
   }
 }
 
 /**
+ * Implements hook_entity_insert().
+ */
+function mollom_entity_insert($entity, $type) {
+  list($id) = entity_extract_ids($type, $entity);
+  if (!empty($entity->mollom) && !empty($id)) {
+    $entity->mollom['id'] = $id;
+    $data = (object) $entity->mollom;
+    mollom_data_save($data);
+  }
+}
+
+/**
  * Implements hook_entity_update().
  */
 function mollom_entity_update($entity, $type) {
@@ -3081,10 +3151,12 @@ function mollom_form_comment_multiple_delete_confirm_submit($form, &$form_state)
  */
 function user_mollom_form_list() {
   $forms['user_register_form'] = array(
+    'mode' => MOLLOM_MODE_CAPTCHA,
     'title' => t('User registration form'),
     'entity' => 'user',
     'bundle' => 'user',
     'delete form' => 'user_cancel_confirm_form',
+    'report path' => 'user/%id/cancel',
     'report access' => array('administer users'),
     'entity delete multiple callback' => 'user_delete_multiple',
   );
@@ -3092,6 +3164,7 @@ function user_mollom_form_list() {
   $forms['user_profile_form']['title'] = t('User profile form');
 
   $forms['user_pass'] = array(
+    'mode' => MOLLOM_MODE_CAPTCHA,
     'title' => t('User password request form'),
   );
   return $forms;
@@ -3118,7 +3191,6 @@ function user_mollom_form_info($form_id) {
 
     case 'user_pass':
       $form_info = array(
-        'mode' => MOLLOM_MODE_CAPTCHA,
         'bypass access' => array('administer users'),
         'mapping' => array(
           'post_id' => 'uid',
diff --git a/tests/mollom.test b/tests/mollom.test
index c2dcf2e..1538c2d 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -683,6 +683,8 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *   - entity: The entity type contained in the report link URL.
    *   - id: The entity ID contained in the report link URL.
    *   - mail: The full mail message array, as recorded by TestingMailSystem.
+   *   - external: TRUE.
+   *   The array can be passed directly as $options to drupalGet().
    */
   protected function assertMailMollomReportLink($entity_type = 'mollom_content') {
     // Grab the last sent mail.
@@ -692,12 +694,33 @@ class MollomWebTestCase extends DrupalWebTestCase {
     variable_set('drupal_test_email_collector', $captured_emails);
 
     $found = FALSE;
-    if (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches)) {
+    // Determine the report URI pattern for the passed entity type.
+    $path = FALSE;
+    foreach (mollom_form_list() as $form_id => $info) {
+      if (isset($info['entity']) && $info['entity'] == $entity_type && isset($info['report path'])) {
+        $path = $info['report path'];
+        break;
+      }
+    }
+    if ($path) {
+      $path = strtr($path, array('%id' => '([^\s]+)'));
+      if (preg_match('@http.+?' . $path . '@', $message['body'], $matches)) {
+        $found = array(
+          'url' => $matches[0],
+          'entity' => $entity_type,
+          'id' => $matches[1],
+          'mail' => $message,
+          'external' => TRUE,
+        );
+      }
+    }
+    elseif (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches)) {
       $found = array(
         'url' => $matches[0],
         'entity' => $matches[1],
         'id' => $matches[2],
         'mail' => $message,
+        'external' => TRUE,
       );
     }
     $this->assertTrue($found, t('Report to Mollom link found in e-mail: %url', array('%url' => $found['url'])));
@@ -2118,16 +2141,6 @@ class MollomFormConfigurationTestCase extends MollomWebTestCase {
     $this->drupalPost(NULL, $edit, t('Next'));
     $this->assertText('Mollom test form');
 
-    // Verify that text analysis cannot be enabled without enabling fields.
-    $edit = array(
-      'mollom[mode]' => MOLLOM_MODE_ANALYSIS,
-    );
-    foreach ($form_info['elements'] as $key => $label) {
-      $edit['mollom[enabled_fields][' . rawurlencode($key) . ']'] = FALSE;
-    }
-    $this->drupalPost(NULL, $edit, t('Save'));
-    $this->assertText(t('At least one field is required for text analysis.'));
-
     $edit = array(
       'mollom[mode]' => MOLLOM_MODE_ANALYSIS,
       'mollom[checks][spam]' => TRUE,
@@ -2390,9 +2403,11 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
   /**
    * Make sure that the request password form is protected correctly.
    */
-  function testProtectRequestPassword() {
-    // We first enable Mollom for the request password form.
+  function testUserPasswordCaptcha() {
     $this->drupalLogin($this->admin_user);
+    // Verify that the protection mode defaults to CAPTCHA.
+    $this->drupalGet('admin/config/content/mollom/add/user_pass');
+    $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA);
     $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
     $this->drupalLogout();
 
@@ -2416,9 +2431,11 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
   /**
    * Make sure that the user registration form is protected correctly.
    */
-  function testProtectRegisterUser() {
-    // We first enable Mollom for the user registration form.
+  function testUserRegisterCaptcha() {
     $this->drupalLogin($this->admin_user);
+    // Verify that the protection mode defaults to CAPTCHA.
+    $this->drupalGet('admin/config/content/mollom/add/user_register_form');
+    $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA);
     $this->setProtection('user_register_form', MOLLOM_MODE_CAPTCHA);
     $this->drupalLogout();
 
@@ -2447,12 +2464,145 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
     // to successfully register.
     $this->postCorrectCaptcha('user/register', $edit, t('Create new account'));
     $this->assertText(t('Your account is currently pending approval by the site administrator.'));
-    $this->assertTrue(user_load_by_name($name), t('The user who attempted to register appears in the database when the CAPTCHA is valid.'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 0, 'New user account is pending approval.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 0);
+
+    // First mail sent is the site administrator approval.
+    $link = $this->assertMailMollomReportLink('user');
+    // Second mail goes to the user who registered.
+    $this->assertNoMailMollomReportLink();
+
+    // Verify that the user account is deleted after reporting it as spam.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet($link['url'], $link);
+    $edit = array(
+      'user_cancel_method' => 'user_cancel_delete',
+      'mollom[feedback]' => 'spam',
+    );
+    $this->drupalPost(NULL, $edit, t('Cancel account'));
+    $account = user_load($account->uid, TRUE);
+    $this->assertFalse($account, 'Reported user account not found.');
+  }
+
+  /**
+   * Tests text analysis protection with CAPTCHA for user registration form.
+   */
+  function testUserRegisterAnalysisCaptcha() {
+    // Allow registration by site visitors without administrator approval.
+    variable_set('user_register', USER_REGISTER_VISITORS);
+
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS);
+    $this->drupalLogout();
+
+    // Retrieve initial count of registered users.
+    $count_initial = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+
+    // Verify that a spam user registration is blocked.
+    /*
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'spam',
+      'mail' => 'spam@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText($this->spam_message);
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+    $this->assertFalse(user_load_by_name($edit['name']), 'New user not found.');
+    */
+
+    // Verify that a unsure registration triggers a CAPTCHA.
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'unsure',
+      'mail' => 'unsure@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertCaptchaField();
+
+    // Verify that user count is still the same.
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+
+    // Verify that solving the CAPTCHA registers the user.
+    $this->postCorrectCaptcha(NULL, array(), t('Create new account'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 1, 'New user account is active.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 0);
+  }
+
+  /**
+   * Tests text analysis protection without CAPTCHA for user registration form.
+   */
+  function testUserRegisterAnalysisModerate() {
+    // Allow registration by site visitors without administrator approval.
+    variable_set('user_register', USER_REGISTER_VISITORS);
+
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_register_form', MOLLOM_MODE_ANALYSIS, array(), array(
+      'mollom[unsure]' => 'moderate',
+    ));
+    $this->drupalLogout();
+
+    // Retrieve initial count of registered users.
+    $count_initial = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+
+    // Verify that a spam user registration is blocked.
+    /*
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'spam',
+      'mail' => 'spam@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText($this->spam_message);
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+    $this->assertFalse(user_load_by_name($edit['name']), 'New user not found.');
+    */
+
+    // Verify that a unsure registration triggers no CAPTCHA and requires approval.
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'unsure',
+      'mail' => 'unsure@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText(t('Your account is currently pending approval by the site administrator.'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 0, 'New user account is pending approval.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 1);
 
     // First mail sent is the site administrator approval.
-    $this->assertMailMollomReportLink('mollom_captcha');
+    $link = $this->assertMailMollomReportLink('user');
     // Second mail goes to the user who registered.
     $this->assertNoMailMollomReportLink();
+
+    // Verify that the user account is deleted after reporting it as spam.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet($link['url'], $link);
+    $edit = array(
+      'user_cancel_method' => 'user_cancel_delete',
+      'mollom[feedback]' => 'spam',
+    );
+    $this->drupalPost(NULL, $edit, t('Cancel account'));
+    $account = user_load($account->uid, TRUE);
+    $this->assertFalse($account, 'Reported user account not found.');
   }
 }
 
