diff --git modules/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php
index e70b84b..8909e4a 100644
--- modules/simpletest/drupal_web_test_case.php
+++ modules/simpletest/drupal_web_test_case.php
@@ -1080,7 +1080,8 @@ class DrupalWebTestCase extends DrupalTestCase {
     // Make a request to the logout page, and redirect to the user page, the
     // idea being if you were properly logged out you should be seeing a login
     // screen.
-    $this->drupalGet('user/logout', array('query' => array('destination' => 'user')));
+    $this->drupalGet('user/logout');
+    $this->drupalGet('user');
     $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
     $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));
 
@@ -2782,6 +2783,7 @@ class DrupalWebTestCase extends DrupalTestCase {
    *   Value of the field to assert.
    * @param $message
    *   Message to display.
+   *
    * @return
    *   TRUE on pass, FALSE on fail.
    */
@@ -2792,6 +2794,63 @@ class DrupalWebTestCase extends DrupalTestCase {
   }
 
   /**
+   * Assert that the most recently sent e-mail message has the string in it.
+   *
+   * @param $field_name
+   *   Name of field or message property to assert: subject, body, id, ...
+   * @param $string
+   *   String to search for.
+   * @param $email_depth
+   *   Number of emails to search for string, starting with most recent.
+   * @return
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertMailString($field_name, $string, $email_depth) {
+    $mails = $this->drupalGetMails();
+    $string_found = FALSE;
+    for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $email_depth && $i >= 0; $i--) {
+      $mail = $mails[$i];
+      // Normalize whitespace, as we don't know what the mail system might have
+      // done. Any run of whitespace becomes a single space.
+      $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]);
+      $normalized_string = preg_replace('/\s+/', ' ', $string);
+      $string_found = (FALSE !== strpos($normalized_mail, $normalized_string));
+      if ($string_found) {
+        break;
+      }
+    }
+    return $this->assertTrue($string_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $string)));
+  }
+
+  /**
+   * Assert that the most recently sent e-mail message has the pattern in it.
+   *
+   * @param $field_name
+   *   Name of field or message property to assert: subject, body, id, ...
+   * @param $regex
+   *   Pattern to search for.
+   * @return
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertMailPattern($field_name, $regex, $message) {
+    $mails = $this->drupalGetMails();
+    $mail = end($mails);
+    $regex_found = preg_match("/$regex/", $mail[$field_name]);
+    return $this->assertTrue($regex_found, t('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $regex)));
+  }
+
+  /**
+   * Output to verbose the most recent $count emails sent.
+   */
+  protected function verboseEmail($count = 1) {
+    $mails = $this->drupalGetMails();
+    for ($i = sizeof($mails) -1; $i >= sizeof($mails) - $count && $i >= 0; $i--) {
+      $mail = $mails[$i];
+      $this->verbose(t('Email:') . '<pre>' . print_r($mail, TRUE) . '</pre>');
+    }
+  }
+
+  /**
    * Log verbose message in a text file.
    *
    * The a link to the vebose message will be placed in the test results via
diff --git modules/system/system.module modules/system/system.module
index 846117d..6d5f22b 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -2849,7 +2849,7 @@ function system_action_info() {
       'type' => 'user',
       'label' => t('Ban IP address of current user'),
       'configurable' => FALSE,
-      'triggers' => array(),
+      'triggers' => array('any'),
     ),
     'system_goto_action' => array(
       'type' => 'system',
@@ -2915,9 +2915,9 @@ function system_send_email_action_form($context) {
 function system_send_email_action_validate($form, $form_state) {
   $form_values = $form_state['values'];
   // Validate the configuration form.
-  if (!valid_email_address($form_values['recipient']) && $form_values['recipient'] != '%author') {
+  if (!valid_email_address($form_values['recipient']) && $form_values['recipient'] != '[node:author:mail]' && $form_values['recipient'] != '[user:mail]' && $form_values['recipient'] != '[comment:author:mail]') {
     // We want the literal %author placeholder to be emphasized in the error message.
-    form_set_error('recipient', t('Enter a valid email address or %author.', array('%author' => '%author')));
+    form_set_error('recipient', t('Enter a valid email address, [node:author:mail], [comment:author:mail], or [user:mail].'));
   }
 }
 
diff --git modules/trigger/trigger.module modules/trigger/trigger.module
index b5929e4..79ecaad 100644
--- modules/trigger/trigger.module
+++ modules/trigger/trigger.module
@@ -488,13 +488,17 @@ function trigger_user_login(&$edit, $account, $category) {
  * Implements hook_user_logout().
  */
 function trigger_user_logout($account) {
-  _trigger_user('user_logout', $edit = NULL, $account);
+  $edit = array();
+  _trigger_user('user_logout', $edit, $account);
 }
 
 /**
  * Implements hook_user_presave().
  */
-function trigger_user_presave(&$edit, $account, $category) {
+function trigger_user_presave(&$edit, $account = array(), $category) {
+  // Here we merge $edit into $account, as the new account information
+  // is almost certainly what is wanted.
+  $account = (object)(array_merge((array)$account, $edit));
   _trigger_user('user_presave', $edit, $account, $category);
 }
 
@@ -527,7 +531,8 @@ function trigger_user_cancel($edit, $account, $method) {
  * Implements hook_user_delete().
  */
 function trigger_user_delete($account) {
-  _trigger_user('user_delete', $edit, $account, $method);
+  $edit = array();
+  _trigger_user('user_delete', $edit, $account, NULL);
 }
 
 /**
@@ -555,7 +560,7 @@ function _trigger_user($hook, &$edit, $account, $category = NULL) {
       if (!isset($objects[$type])) {
         $objects[$type] = _trigger_normalize_user_context($type, $account);
       }
-      $context['account'] = $account;
+      $context['user'] = $account;
       actions_do($aid, $objects[$type], $context);
     }
     else {
diff --git modules/trigger/trigger.test modules/trigger/trigger.test
index c62e983..a2a500a 100644
--- modules/trigger/trigger.test
+++ modules/trigger/trigger.test
@@ -238,6 +238,309 @@ class TriggerCronTestCase extends TriggerWebTestCase {
 }
 
 /**
+ * Base class that helps with trigger assignments and test comparisons.
+ *
+ */
+class TriggerActionTestCase extends TriggerWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Test triggers and actions',
+      'description' => 'Test triggers and actions with token replacement.',
+      'group' => 'Trigger',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('trigger');
+  }
+
+    /**
+   * Utility function to create a message with tokens.
+   *
+   * @param $trigger
+   *
+   * @return
+   *   A message with embedded tokens.
+   */
+  function generateMessageWithTokens($trigger) {
+    // Note that subject is limited to 254 characters in action configuration.
+    $message = t('Action was triggered by trigger @trigger user:name=[user:name] user:uid=[user:uid] user:mail=[user:mail] user:url=[user:url] user:edit-url=[user:edit-url] user:created=[user:created]',
+      array('@trigger' => $trigger));
+    return trim($message);
+  }
+
+  /**
+   * Utility function to generate a comparison message to match the
+   * pre-token-replaced message in generateMessageWithTokens().
+   *
+   * @param $trigger
+   *   Trigger, like 'user_login'.
+   * @param $account
+   *   Associated user account.
+   * @return
+   *   The token-replaced equivalent message. This does not use token
+   *   functionality.
+   */
+  function generateTokenExpandedComparison($trigger, $account) {
+    // Note that user:last-login was omitted because it changes and can't
+    // be properly verified.
+    $message = t('Action was triggered by trigger @trigger user:name=@username user:uid=@uid user:mail=@mail user:url=@user_url user:edit-url=@user_edit_url user:created=@user_created',
+       array(
+        '@trigger' => $trigger,
+        '@username' => $account->name,
+        '@uid' => !empty($account->uid) ? $account->uid : t('not yet assigned'),
+        '@mail' => $account->mail,
+        '@user_url' => !empty($account->uid) ? url("user/$account->uid", array('absolute' => TRUE)) : t('not yet assigned'),
+        '@user_edit_url' => !empty($account->uid) ? url("user/$account->uid/edit", array('absolute' => TRUE)) : t('not yet assigned'),
+        '@user_created' => isset($account->created) ? format_date($account->created, 'medium') : t('not yet created'),
+        )
+      );
+      return trim($message);
+  }
+
+
+  /**
+   * Assign a simple (non-configurable) action to a trigger.
+   *
+   * @param $trigger
+   *   The trigger to assign to, like 'user_login'.
+   * @param $action
+   *   The simple action to be assigned, like 'comment_insert'.
+   */
+  function assignSimpleAction($trigger, $action) {
+    $form_name = "trigger_{$trigger}_assign_form";
+    $form_html_id = strtr($form_name, '_', '-');
+    $edit = array('aid' => md5($action));
+    $trigger_type = preg_replace('/_.*/', '', $trigger);
+    $this->drupalPost("admin/structure/trigger/$trigger_type", $edit, t('Assign'), array(), array(), $form_html_id);
+    $actions = trigger_get_assigned_actions($trigger);
+    $this->assertTrue(!empty($actions[$action]), t('Simple action @action assigned to trigger @trigger', array('@action' => $action, '@trigger' => $trigger)));
+  }
+
+  /**
+   * Assign a system message action to the passed-in trigger
+   *
+   * @param $trigger
+   *   For example, 'user_login'
+   */
+  function assignSystemMessageAction($trigger) {
+    $form_name = "trigger_{$trigger}_assign_form";
+    $form_html_id = strtr($form_name, '_', '-');
+    // Assign a configurable action 'System message' to the passed trigger.
+    $action_edit = array(
+      'actions_label' => $trigger . "_system_message_action_" . $this->randomName(16),
+      'message' => $this->generateMessageWithTokens($trigger),
+    );
+
+    // Configure an advanced action that we can assign.
+    $aid = $this->configureAdvancedAction('system_message_action', $action_edit);
+
+    $edit = array('aid' => md5($aid));
+    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id);
+  }
+
+
+  /**
+   * Assign a system_send_email_action to the passed-in trigger.
+   *
+   * @param $trigger
+   *   For example, 'user_login'
+   */
+  function assignSystemEmailAction($trigger) {
+    $form_name = "trigger_{$trigger}_assign_form";
+    $form_html_id = strtr($form_name, '_', '-');
+
+    $message = $this->generateMessageWithTokens($trigger);
+    // Assign a configurable action 'System message' to the passed trigger.
+    $action_edit = array(
+      // 'actions_label' => $trigger . "_system_send_message_action_" . $this->randomName(16),
+      'actions_label' => $trigger . "_system_send_email_action",
+      'recipient' => '[user:mail]',
+      'subject' => $message,
+      'message' => $message,
+    );
+
+    // Configure an advanced action that we can assign.
+    $aid = $this->configureAdvancedAction('system_send_email_action', $action_edit);
+
+    $edit = array('aid' => md5($aid));
+    $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id);
+  }
+
+  /**
+   * Assert correct token replacement in both system message and email.
+   *
+   * @param $trigger
+   *   A trigger like 'user_login'.
+   * @param $account
+   *   The user account which triggered the action.
+   * @param $email_depth
+   *   Number of emails to scan, starting with most recent.
+   */
+  function assertSystemMessageAndEmailTokenReplacement($trigger, $account, $email_depth = 1) {
+    $this->assertSystemMessageTokenReplacement($trigger, $account);
+    $this->assertSystemEmailTokenReplacement($trigger, $account, $email_depth);
+  }
+
+  /**
+   * Assert correct token replacement for the given trigger and account.
+   *
+   * @param $trigger
+   *   A trigger like 'user_login'.
+   * @param $account
+   *   The user account which triggered the action.
+   */
+  function assertSystemMessageTokenReplacement($trigger, $account) {
+    $expected = $this->generateTokenExpandedComparison($trigger, $account);
+    $this->assertText($expected,
+      t('Expected system message to contain token-replaced text "@expected" found in configured system message action', array('@expected' => $expected )) );
+  }
+
+
+  /**
+   * Assert correct token replacement for the given trigger and account.
+   *
+   * @param $trigger
+   *   A trigger like 'user_login'.
+   * @param $account
+   *   The user account which triggered the action.
+   * @param $email_depth
+   *   Number of emails to scan, starting with most recent.
+   */
+  function assertSystemEmailTokenReplacement($trigger, $account, $email_depth = 1) {
+    $this->verboseEmail($email_depth);
+    $expected = $this->generateTokenExpandedComparison($trigger, $account);
+    $this->assertMailString('subject', $expected, $email_depth);
+    $this->assertMailString('body', $expected, $email_depth);
+    $this->assertMail('to', $account->mail, t('Mail sent to correct destination'));
+  }
+}
+
+/**
+ * Test token substitution in trigger actions.
+ *
+ * This tests nearly every permutation of user triggers with system actions
+ * and checks the token replacement.
+ *
+ */
+class TriggerUserTokenTestCase extends TriggerActionTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Test user triggers',
+      'description' => 'Test user triggers and system actions with token replacement.',
+      'group' => 'Trigger',
+    );
+  }
+
+
+  /**
+   * Test a variety of token replacements in actions.
+   */
+  function testUserTriggerTokenReplacement() {
+    $test_user = $this->drupalCreateUser(array('administer actions', 'administer users', 'change own username', 'access user profiles'));
+    $this->drupalLogin($test_user);
+
+    $triggers = array('user_login', 'user_presave', 'user_insert', 'user_update', 'user_delete', 'user_logout', 'user_view');
+    foreach ($triggers as $trigger) {
+      $this->assignSystemMessageAction($trigger);
+      $this->assignSystemEmailAction($trigger);
+    }
+
+    $this->drupalLogout();
+    // @todo: Below can't work until http://drupal.org/node/754560 lands.
+    // $this->assertSystemMessageTokenReplacement('user_logout', $test_user);
+    $this->assertSystemEmailTokenReplacement('user_logout', $test_user);
+
+    $this->drupalLogin($test_user);
+    $this->assertSystemMessageAndEmailTokenReplacement('user_login', $test_user, 2);
+    $this->assertSystemMessageAndEmailTokenReplacement('user_view', $test_user, 2);
+
+    $this->drupalPost("user/{$test_user->uid}/edit", array('name' => $test_user->name . '_changed'), t('Save'));
+    $test_user->name .= '_changed'; // Since we just changed it.
+    $this->assertSystemMessageAndEmailTokenReplacement('user_presave', $test_user, 2);
+    $this->assertSystemMessageAndEmailTokenReplacement('user_update', $test_user, 2);
+
+    $this->drupalGet('user');
+    $this->assertSystemMessageAndEmailTokenReplacement('user_view', $test_user);
+
+    $new_user = $this->drupalCreateUser(array('administer actions', 'administer users', 'cancel account', 'access administration pages'));
+    // @todo: The following will not work until http://drupal.org/node/754560
+    // lands.
+    // $this->assertSystemMessageTokenReplacement('user_insert', $new_user);
+    $this->assertSystemEmailTokenReplacement('user_insert', $new_user);
+
+    $this->drupalLogin($new_user);
+    $user_to_delete = $this->drupalCreateUser(array('access content'));
+    variable_set('user_cancel_method', 'user_cancel_delete');
+
+    $this->drupalPost("user/{$user_to_delete->uid}/cancel", array(), t('Cancel account'));
+    $this->assertSystemMessageAndEmailTokenReplacement('user_delete', $user_to_delete);
+  }
+
+
+}
+
+/**
+ * Test token substitution in trigger actions.
+ *
+ * This tests nearly every permutation of user triggers with system actions
+ * and checks the token replacement.
+ *
+ */
+class TriggerUserActionTestCase extends TriggerActionTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Test user actions',
+      'description' => 'Test user actions.',
+      'group' => 'Trigger',
+    );
+  }
+
+  /**
+   * Test user action assignment and execution.
+   */
+  function testUserActionAssignmentExecution() {
+    $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments'));
+    $this->drupalLogin($test_user);
+
+    $triggers = array('comment_presave', 'comment_insert', 'comment_update');
+    // system_block_ip_action is difficult to test without ruining the test.
+    $actions = array('user_block_user_action');
+    foreach ($triggers as $trigger) {
+      foreach ($actions as $action) {
+        $this->assignSimpleAction($trigger, $action);
+      }
+    }
+
+    $node = $this->drupalCreateNode(array('type' => 'article'));
+    $this->drupalPost("node/{$node->nid}", array('comment_body[und][0][value]' => t("my comment"), 'subject' => t("my comment subject")), t('Save'));
+    // Posting a comment should have blocked this user.
+    $account = user_load($test_user->uid, TRUE);
+    $this->assertTrue($account->status == 0, t('Account is blocked'));
+    $comment_author_uid = $account->uid;
+    // Now rehabilitate the comment author so it can be be blocked again when
+    // the comment is updated.
+    user_save($account, array('status' => TRUE));
+
+    $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments'));
+    $this->drupalLogin($test_user);
+
+    // Our original comment will have been comment 1.
+    $this->drupalPost("comment/1/edit", array('comment_body[und][0][value]' => t("my comment, updated"), 'subject' => t("my comment subject")), t('Save'));
+    $comment_author_account = user_load($comment_author_uid, TRUE);
+    $this->assertTrue($comment_author_account->status == 0, t('Comment author account (uid=@uid) is blocked after update to comment', array('@uid' => $comment_author_uid)));
+
+    // Verify that the comment was updated.
+    $test_user = $this->drupalCreateUser(array('administer actions', 'create article content', 'access comments', 'administer comments', 'post comments without approval', 'edit own comments'));
+    $this->drupalLogin($test_user);
+
+    $this->drupalGet("node/$node->nid");
+    $this->assertText(t("my comment, updated"));
+    $this->verboseEmail();
+  }
+}
+
+/**
  * Test other triggers.
  */
 class TriggerOtherTestCase extends TriggerWebTestCase {
diff --git modules/user/user.module modules/user/user.module
index 8541bed..476fdb1 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -3279,7 +3279,7 @@ function user_action_info() {
       'label' => t('Block current user'),
       'type' => 'user',
       'configurable' => FALSE,
-      'triggers' => array(),
+      'triggers' => array('any'),
     ),
   );
 }
@@ -3290,22 +3290,22 @@ function user_action_info() {
  * @ingroup actions
  */
 function user_block_user_action(&$entity, $context = array()) {
+  // First priority: If there is a $entity->uid, block that user.
+  // This is most likely the author if a node or comment.
   if (isset($entity->uid)) {
     $uid = $entity->uid;
   }
   elseif (isset($context['uid'])) {
     $uid = $context['uid'];
   }
+  // If neither of those are valid, then block the current user.
   else {
-    global $user;
-    $uid = $user->uid;
+    $uid = $GLOBALS['user']->uid;
   }
-  db_update('users')
-    ->fields(array('status' => 0))
-    ->condition('uid', $uid)
-    ->execute();
+  $account = user_load($uid);
+  user_save($account, array('status' => FALSE));
   drupal_session_destroy_uid($uid);
-  watchdog('action', 'Blocked user %name.', array('%name' => $user->name));
+  watchdog('action', 'Blocked user %name.', array('%name' => $account->name));
 }
 
 /**
diff --git modules/user/user.tokens.inc modules/user/user.tokens.inc
index a664b3f..1b12437 100644
--- modules/user/user.tokens.inc
+++ modules/user/user.tokens.inc
@@ -82,7 +82,8 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr
       switch ($name) {
         // Basic user account information.
         case 'uid':
-          $replacements[$original] = $account->uid;
+          // In the case of user_presave uid is not set yet.
+          $replacements[$original] = !empty($account->uid) ? $account->uid : t('not yet assigned');
           break;
 
         case 'name':
@@ -95,20 +96,21 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr
           break;
 
         case 'url':
-          $replacements[$original] = url("user/$account->uid", $url_options);
+          $replacements[$original] = !empty($account->uid) ? url("user/$account->uid", $url_options) : t('not yet assigned');
           break;
 
         case 'edit-url':
-          $replacements[$original] = url("user/$account->uid/edit", $url_options);
+          $replacements[$original] = !empty($account->uid) ? url("user/$account->uid/edit", $url_options) : t('not yet assigned');
           break;
 
         // These tokens are default variations on the chained tokens handled below.
         case 'last-login':
-          $replacements[$original] = format_date($account->login, 'medium', '', NULL, $language_code);
+          $replacements[$original] = !empty($account->login) ? format_date($account->login, 'medium', '', NULL, $language_code) : t('never');
           break;
 
         case 'created':
-          $replacements[$original] = format_date($account->created, 'medium', '', NULL, $language_code);
+          // In the case of user_presave the created date may not yet be set.
+          $replacements[$original] = !empty($account->created) ? format_date($account->created, 'medium', '', NULL, $language_code) : t('not yet created');
           break;
       }
     }
