Index: src/bp-core/bp-core-component.php
===================================================================
--- src/bp-core/bp-core-component.php
+++ src/bp-core/bp-core-component.php
@@ -131,19 +131,21 @@
 	 * Component loader.
 	 *
 	 * @since BuddyPress (1.5.0)
-	 *
-	 * @uses BP_Component::setup_actions() Set up the hooks and actions.
-	 *
-	 * @param string $id Unique ID (for internal identification). Letters,
-	 *        numbers, and underscores only.
-	 * @param string $name Unique name. This should be a translatable name,
-	 *        eg __( 'Groups', 'buddypress' ).
-	 * @param string $path The file path for the component's files. Used by
-	 *        {@link BP_Component::includes()}.
-	 * @param array $params Additional parameters used by the component.
-	 *        The config array supports the following values:
-	 *        - 'adminbar_myaccount_order' Sets the position for our
-	 *          component menu under the WP Toolbar's "My Account" menu.
+	 * @since BuddyPress (1.9.0) Added $params as a parameter.
+	 * @since BuddyPress (2.3.0) Added $params['features'] as a configurable value.
+	 *
+	 * @param string $id   Unique ID. Letters, numbers, and underscores only.
+	 * @param string $name Unique name. This should be a translatable name, eg.
+	 *                     __( 'Groups', 'buddypress' ).
+	 * @param string $path The file path for the component's files. Used by {@link BP_Component::includes()}.
+	 * @param array  $params {
+	 *     Additional parameters used by the component.
+	 *     @type int   $adminbar_myaccount_order Set the position for our menu under the WP Toolbar's "My Account menu"
+	 *     @type array $features                 An array of feature names. This is used to load additional files from your
+	 *                                           component directory and for feature active checks. eg. array( 'awesome' )
+	 *                                           would look for a file called "bp-{$this->id}-awesome.php" and you could use
+	 *                                           bp_is_active( $this->id, 'awesome' ) to determine if the feature is active.
+	 * }
 	 */
 	public function start( $id = '', $name = '', $path = '', $params = array() ) {
 
@@ -163,6 +165,11 @@
 				$this->adminbar_myaccount_order = (int) $params['adminbar_myaccount_order'];
 			}
 
+			// Register features
+			if ( ! empty( $params['features'] ) ) {
+				$this->features = array_map( 'sanitize_title', (array) $params['features'] );
+			}
+
 		// Set defaults if not passed
 		} else {
 			// new component menus are added before the settings menu if not set
@@ -183,24 +190,18 @@
 	 *
 	 * @param array $args {
 	 *     All values are optional.
-	 *     @type string $slug The component slug. Used to construct certain
-	 *           URLs, such as 'friends' in http://example.com/members/joe/friends/
-	 *           Default: the value of $this->id.
-	 *     @type string $root_slug The component root slug. Note that this
-	 *           value is generally unused if the component has a root
-	 *           directory (the slug will be overridden by the post_name of
-	 *           the directory page). Default: the slug of the directory
-	 *           page if one is found, otherwise an empty string.
-	 *     @type bool $has_directory Set to true if the component requires
-	 *           an associated WordPress page.
-	 *     @type callable $notification_callback Optional. The callable
-	 *           function that formats the component's notifications.
-	 *     @type string $search_term Optional. The placeholder text in the
-	 *           component directory search box. Eg, 'Search Groups...'.
-	 *     @type array $global_tables Optional. An array of database table
-	 *           names.
-	 *     @type array $meta_tables Optional. An array of metadata table
-	 *           names.
+	 *     @type string   $slug                  The component slug. Used to construct certain URLs, such as 'friends' in
+	 *                                           http://example.com/members/joe/friends/. Default: the value of $this->id.
+	 *     @type string   $root_slug             The component root slug. Note that this value is generally unused if the
+	 *                                           component has a root directory (the slug will be overridden by the
+	 *                                           post_name of the directory page). Default: the slug of the directory page
+	 *                                           if one is found, otherwise an empty string.
+	 *     @type bool     $has_directory         Set to true if the component requires an associated WordPress page.
+	 *     @type callable $notification_callback Optional. The callable function that formats the component's notifications.
+	 *     @type string   $search_term           Optional. The placeholder text in the component directory search box. Eg,
+	 *                                           'Search Groups...'.
+	 *     @type array    $global_tables         Optional. An array of database table names.
+	 *     @type array    $meta_tables           Optional. An array of metadata table names.
 	 * }
 	 */
 	public function setup_globals( $args = array() ) {
Index: src/bp-core/bp-core-template.php
===================================================================
--- src/bp-core/bp-core-template.php
+++ src/bp-core/bp-core-template.php
@@ -1845,12 +1845,16 @@
 /** Components ****************************************************************/
 
 /**
- * Check whether a given component has been activated by the admin.
+ * Check whether a given component (or feature of a component) is active.
+ *
+ * @since BuddyPress (1.2.0) See r2539.
+ * @since BuddyPress (2.3.0) Added $feature as a parameter.
  *
  * @param string $component The component name.
- * @return bool True if the component is active, otherwise false.
+ * @param string $feature   The feature name.
+ * @return bool
  */
-function bp_is_active( $component = '' ) {
+function bp_is_active( $component = '', $feature = '' ) {
 	$retval = false;
 
 	// Default to the current component if none is passed
@@ -1861,6 +1865,22 @@
 	// Is component in either the active or required components arrays
 	if ( isset( buddypress()->active_components[ $component ] ) || isset( buddypress()->required_components[ $component ] ) ) {
 		$retval = true;
+
+		// Is feature active?
+		if ( ! empty( $feature ) ) {
+			if ( empty( buddypress()->$component->features ) || false === in_array( $feature, buddypress()->$component->features, true ) ) {
+				$retval = false;
+			}
+
+			/**
+			 * Filters whether or not a given feature for a component is active.
+			 *
+			 * @since BuddyPress (2.3.0)
+			 *
+			 * @param bool $retval
+			 */
+			$retval = apply_filters( "bp_is_{$component}_{$feature}_active", $retval );
+		}
 	}
 
 	/**
Index: src/bp-messages/bp-messages-functions.php
===================================================================
--- src/bp-messages/bp-messages-functions.php
+++ src/bp-messages/bp-messages-functions.php
@@ -366,6 +366,22 @@
 	return BP_Messages_Thread::is_valid( $thread_id );
 }
 
+/**
+ * Get the thread ID from a message ID.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  int $message_id ID of the message.
+ * @return int The ID of the thread if found, otherwise 0.
+ */
+function messages_get_message_thread_id( $message_id = 0 ) {
+	global $wpdb;
+
+	$bp = buddypress();
+
+	return (int) $wpdb->get_var( $wpdb->prepare( "SELECT thread_id FROM {$bp->messages->table_name_messages} WHERE id = %d", $message_id ) );
+}
+
 /** Messages Meta *******************************************************/
 
 /**
Index: src/bp-messages/bp-messages-loader.php
===================================================================
--- src/bp-messages/bp-messages-loader.php
+++ src/bp-messages/bp-messages-loader.php
@@ -39,7 +39,8 @@
 			__( 'Private Messages', 'buddypress' ),
 			buddypress()->plugin_dir,
 			array(
-				'adminbar_myaccount_order' => 50
+				'adminbar_myaccount_order' => 50,
+				'features'                 => array( 'star' )
 			)
 		);
 	}
@@ -67,6 +68,11 @@
 			'widgets',
 		);
 
+		// Conditional includes
+		if ( bp_is_active( $this->id, 'star' ) ) {
+			$includes[] = 'star';
+		}
+
 		parent::includes( $includes );
 	}
 
@@ -166,6 +172,18 @@
 			'user_has_access' => bp_core_can_edit_settings()
 		);
 
+		if ( bp_is_active( $this->id, 'star' ) ) {
+			$sub_nav[] = array(
+				'name'            => __( 'Starred', 'buddypress' ),
+				'slug'            => bp_get_messages_starred_slug(),
+				'parent_url'      => $messages_link,
+				'parent_slug'     => $this->slug,
+				'screen_function' => 'bp_messages_star_screen',
+				'position'        => 11,
+				'user_has_access' => bp_core_can_edit_settings()
+			);
+		}
+
 		$sub_nav[] = array(
 			'name'            => __( 'Sent', 'buddypress' ),
 			'slug'            => 'sentbox',
@@ -243,6 +261,16 @@
 				'href'   => trailingslashit( $messages_link . 'inbox' )
 			);
 
+			// Starred
+			if ( bp_is_active( $this->id, 'star' ) ) {
+				$wp_admin_nav[] = array(
+					'parent' => 'my-account-' . $this->id,
+					'id'     => 'my-account-' . $this->id . '-starred',
+					'title'  => __( 'Starred', 'buddypress' ),
+					'href'   => trailingslashit( $messages_link . bp_get_messages_starred_slug() )
+				);
+			}
+
 			// Sent Messages
 			$wp_admin_nav[] = array(
 				'parent' => 'my-account-' . $this->id,
Index: src/bp-messages/bp-messages-star.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-messages/bp-messages-star.php
@@ -0,0 +1,500 @@
+<?php
+/**
+ * Functions related to starring private messages.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+
+/** UTILITY **************************************************************/
+
+/**
+ * Return the starred messages slug. Defaults to 'starred'.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @return string
+ */
+function bp_get_messages_starred_slug() {
+	/**
+	 * Filters the starred message slug.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param string
+	 */
+	return sanitize_title( apply_filters( 'bp_get_messages_starred_slug', 'starred' ) );
+}
+
+/**
+ * Function to determine if a message ID is starred.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  int $mid     The message ID. Please note that this isn't the message thread ID.
+ * @param  int $user_id The user ID
+ * @return bool
+ */
+function bp_messages_is_message_starred( $mid = 0, $user_id = 0 ) {
+	if ( empty( $user_id ) ) {
+		$user_id = bp_loggedin_user_id();
+	}
+
+	if ( empty( $mid ) ) {
+		return false;
+	}
+
+	$starred = array_flip( (array) bp_messages_get_meta( $mid, 'starred_by_user', false ) );
+
+	if ( isset( $starred[$user_id] ) ) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+/**
+ * Output the link or raw URL for starring or unstarring a message.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param array $args See bp_get_the_message_star_action_link() for full documentation.
+ */
+function bp_the_message_star_action_link( $args = array() ) {
+	echo bp_get_the_message_star_action_link( $args );
+}
+	/**
+	 * Return the link or raw URL for starring or unstarring a message.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $args {
+	 *     Array of arguments.
+	 *     @type int    $user_id       The user ID. Defaults to the logged-in user ID.
+	 *     @type int    $thread_id     The message thread ID. Default: 0. If not zero, this takes precedence over
+	 *                                 $message_id.
+	 *     @type int    $message_id    The individual message ID. If on a single thread page, defaults to the
+	 *                                 current message ID in the message loop.
+	 *     @type bool   $url_only      Whether to return the URL only. If false, returns link with markup.
+	 *                                 Default: false.
+	 *     @type string $text_unstar   Link text for the 'unstar' action. Only applicable if $url_only is false.
+	 *     @type string $text_star     Link text for the 'star' action. Only applicable if $url_only is false.
+	 *     @type string $title_unstar  Link title for the 'unstar' action. Only applicable if $url_only is false.
+	 *     @type string $title_star    Link title for the 'star' action. Only applicable if $url_only is false.
+	 *     @type string $title_unstar_thread Link title for the 'unstar' action when displayed in a thread loop.
+	 *                                       Only applicable if $message_id is set and if $url_only is false.
+	 *     @type string $title_star_thread   Link title for the 'star' action when displayed in a thread loop.
+	 *                                       Only applicable if $message_id is set and if $url_only is false.
+	 * }
+	 * @return string
+	 */
+	function bp_get_the_message_star_action_link( $args = array() ) {
+		$r = bp_parse_args( $args, array(
+			'user_id'      => bp_loggedin_user_id(),
+			'thread_id'    => 0,
+			'message_id'   => (int) bp_get_the_thread_message_id(),
+			'url_only'     => false,
+			'text_unstar'  => __( 'Unstar', 'buddypress' ),
+			'text_star'    => __( 'Star', 'buddypress' ),
+			'title_unstar' => __( 'Starred', 'buddypress' ),
+			'title_star'   => __( 'Not starred', 'buddypress' ),
+			'title_unstar_thread' => __( 'Remove all starred messages in this thread', 'buddypress' ),
+			'title_star_thread'   => __( 'Star the first message in this thread', 'buddypress' ),
+		), 'messages_star_action_link' );
+
+		$retval = $bulk_attr = '';
+
+		if ( 0 === $r['user_id'] ) {
+			return $retval;
+		}
+
+		// get user domain
+		if ( $r['user_id'] == bp_loggedin_user_id() ) {
+			$user_domain = bp_loggedin_user_domain();
+		} elseif ( $r['user_id'] == bp_displayed_user_domain() ) {
+			$user_domain = bp_displayed_user_domain();
+		} else {
+			$user_domain = bp_core_get_user_domain( $user_id );
+		}
+
+		// thread ID
+		if ( (int) $r['thread_id'] > 0 ) {
+			// see if we're in the loop
+			if ( bp_get_message_thread_id() == $r['thread_id'] ) {
+				// grab all message ids
+				$mids = wp_list_pluck( $GLOBALS['messages_template']->thread->messages, 'id' );
+
+				// make sure order is ASC
+				// order is DESC when used in the thread loop by default
+				$mids = array_reverse( $mids );
+
+			// pull up the thread
+			} else {
+				$thread = new BP_Messages_thread( $r['thread_id'] );
+				$mids = wp_list_pluck( $thread->messages, 'id' );
+			}
+
+			$is_starred = false;
+			$message_id = 0;
+			foreach ( $mids as $mid ) {
+				// try to find the first msg that is starred in a thread
+				if ( true === bp_messages_is_message_starred( $mid ) ) {
+					$is_starred = true;
+					$message_id = $mid;
+					break;
+				}
+			}
+
+			// no star, so default to first message in thread
+			if ( empty( $message_id ) ) {
+				$message_id = $mids[0];
+			}
+
+			// nonce
+			$nonce = wp_create_nonce( "bp-messages-star-{$message_id}" );
+
+			if ( $is_starred ) {
+				$action = 'unstar';
+				$bulk_attr = ' data-star-bulk="1"';
+				$retval = $user_domain . bp_get_messages_slug() . '/unstar/' . $message_id . '/' . $nonce . '/all/';
+			} else {
+				$action = 'star';
+				$retval = $user_domain . bp_get_messages_slug() . '/star/' . $message_id . '/' . $nonce . '/';
+			}
+
+			$title = $r["title_{$action}_thread"];
+
+		// message ID
+		} else {
+			$message_id = (int) $r['message_id'];
+			$is_starred = bp_messages_is_message_starred( $message_id );
+			$nonce      = wp_create_nonce( "bp-messages-star-{$message_id}" );
+
+			if ( $is_starred ) {
+				$action = 'unstar';
+				$retval = $user_domain . bp_get_messages_slug() . '/unstar/' . $message_id . '/' . $nonce . '/';
+			} else {
+				$action = 'star';
+				$retval = $user_domain . bp_get_messages_slug() . '/star/' . $message_id . '/' . $nonce . '/';
+			}
+
+			$title = $r["title_{$action}"];
+		}
+
+		/**
+		 * Filters the star action URL for starring / unstarring a message.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param string $retval URL for starring / unstarring a message.
+		 * @param array  $r      Parsed link arguments. See $args in bp_get_the_message_star_action_link().
+		 */
+		$retval = esc_url( apply_filters( 'bp_get_the_message_star_action_urlonly', $retval, $r ) );
+		if ( true === (bool) $r['url_only'] ) {
+			return $retval;
+		}
+
+		/**
+		 * Filters the star action link, including markup.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param string $retval Link for starring / unstarring a message, including markup.
+		 * @param array  $r      Parsed link arguments. See $args in bp_get_the_message_star_action_link().
+		 */
+		return apply_filters( 'bp_get_the_message_star_action_link', '<a title="' . esc_attr( $title ) . '" class="message-action-' . $action . '" data-star-status="' . $action .'" data-star-nonce="' . $nonce . '"' . $bulk_attr . ' data-message-id="' . esc_attr( (int) $message_id ) . '" href="' . $retval . '"><span class="icon"></span> <span class="bp-screen-reader-text">' . $r['text_' . $action] . '</span></a>', $r );
+	}
+
+/**
+ * Save or delete star message meta according to a message's star status.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param array $args {
+ *     Array of arguments.
+ *     @type string $action     The star action. Either 'star' or 'unstar'. Default: 'star'.
+ *     @type int    $thread_id  The message thread ID. Default: 0. If not zero, this takes precedence over
+ *                              $message_id.
+ *     @type int    $message_id The indivudal message ID to star or unstar.  Default: 0.
+ *     @type int    $user_id    The user ID. Defaults to the logged-in user ID.
+ *     @type bool   $bulk       Whether to mark all messages in a thread as a certain action. Only relevant
+ *                              when $action is 'unstar' at the moment. Default: false.
+ * }
+ * @return bool
+ */
+function bp_messages_star_set_action( $args = array() ) {
+	$r = wp_parse_args( $args, array(
+		'action'     => 'star',
+		'thread_id'  => 0,
+		'message_id' => 0,
+		'user_id'    => bp_loggedin_user_id(),
+		'bulk'       => false
+	) );
+
+	// Set thread ID
+	if ( ! empty( $r['thread_id'] ) ) {
+		$thread_id = (int) $r['thread_id'];
+	} else {
+		$thread_id = messages_get_message_thread_id( $r['message_id'] );
+	}
+	if ( empty( $thread_id ) ) {
+		return false;
+	}
+
+	// Check if user has access to thread
+	if( ! messages_check_thread_access( $thread_id, $r['user_id'] ) ) {
+		return false;
+	}
+
+	$is_starred = bp_messages_is_message_starred( $r['message_id'], $r['user_id'] );
+
+	// star
+	if ( 'star' == $r['action'] ) {
+		if ( true === $is_starred ) {
+			return true;
+		} else {
+			bp_messages_add_meta( $r['message_id'], 'starred_by_user', $r['user_id'] );
+			return true;
+		}
+	// unstar
+	} else {
+		// unstar one message
+		if ( false === $r['bulk'] ) {
+			if ( false === $is_starred ) {
+				return true;
+			} else {
+				bp_messages_delete_meta( $r['message_id'], 'starred_by_user', $r['user_id'] );
+				return true;
+			}
+
+		// unstar all messages in a thread
+		} else {
+			$thread = new BP_Messages_Thread( $thread_id );
+			$mids = wp_list_pluck( $thread->messages, 'id' );
+
+			foreach ( $mids as $mid ) {
+				if ( true === bp_messages_is_message_starred( $mid, $r['user_id'] ) ) {
+					bp_messages_delete_meta( $mid, 'starred_by_user', $r['user_id'] );
+				}
+			}
+
+			return true;
+		}
+	}
+}
+
+/** SCREENS **************************************************************/
+
+/**
+ * Screen handler to display a user's "Starred" private messages page.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_screen() {
+	add_action( 'bp_template_content', 'bp_messages_star_content' );
+
+	/**
+	 * Fires right before the loading of the "Starred" messages box.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	do_action( 'bp_messages_screen_star' );
+
+	bp_core_load_template( 'members/single/plugins' );
+}
+
+/**
+ * Screen content callback to display a user's "Starred" messages page.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_content() {
+	// add our message thread filter
+	add_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+
+	// load the message loop template part
+	bp_get_template_part( 'members/single/messages/messages-loop' );
+
+	// remove our filter
+	remove_filter( 'bp_after_has_message_threads_parse_args', 'bp_messages_filter_starred_message_threads' );
+}
+
+/**
+ * Filter message threads by those starred by the logged-in user.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  array $r Current message thread arguments.
+ * @return array
+ */
+function bp_messages_filter_starred_message_threads( $r = array() ) {
+	$r['user_id'] = 0;
+	$r['meta_query'] = array( array(
+		'key'   => 'starred_by_user',
+		'value' => bp_loggedin_user_id()
+	) );
+
+	return $r;
+}
+
+/** ACTIONS **************************************************************/
+
+/**
+ * Action handler to set a message's star status for those not using JS.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_action_handler() {
+	if ( ! bp_is_user_messages() ) {
+		return;
+	}
+
+	if ( false === ( bp_is_current_action( 'unstar' ) || bp_is_current_action( 'star' ) ) ) {
+		return;
+	}
+
+	if ( ! wp_verify_nonce( bp_action_variable( 1 ), 'bp-messages-star-' . bp_action_variable( 0 ) ) ) {
+		wp_die( "Oops!  That's a no-no!" );
+	}
+
+	// Check capability
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		return;
+	}
+
+	// mark the star
+	bp_messages_star_set_action( array(
+		'action'     => bp_current_action(),
+		'message_id' => bp_action_variable(),
+		'bulk'       => (bool) bp_action_variable( 2 )
+	) );
+
+	// redirect back to previous screen
+	$redirect = wp_get_referer() ? wp_get_referer() : bp_loggedin_user_domain() . bp_get_messages_slug();
+	bp_core_redirect( $redirect );
+	die();
+}
+add_action( 'bp_actions', 'bp_messages_star_action_handler' );
+
+/**
+ * Bulk manage handler to set the star status for multiple messages.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_bulk_manage_handler() {
+	if ( empty( $_POST['messages_bulk_nonce' ] ) ) {
+		return;
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( $_POST['messages_bulk_nonce'], 'messages_bulk_nonce' ) ) {
+		return;
+	}
+
+	// Check capability
+	if ( ! is_user_logged_in() || ! bp_core_can_edit_settings() ) {
+		return;
+	}
+
+	$action  = ! empty( $_POST['messages_bulk_action'] ) ? $_POST['messages_bulk_action'] : '';
+	$threads = ! empty( $_POST['message_ids'] ) ? $_POST['message_ids'] : '';
+	$threads = wp_parse_id_list( $threads );
+
+	// Bail if action doesn't match our star actions or no IDs.
+	if ( false === in_array( $action, array( 'star', 'unstar' ), true ) || empty( $threads ) ) {
+		return;
+	}
+
+	// It's star time!
+	switch ( $action ) {
+		case 'star' :
+			$count = count( $threads );
+
+			// if we're starring a thread, we only star the first message in the thread
+			foreach ( $threads as $thread ) {
+				$thread = new BP_Messages_thread( $thread );
+				$mids = wp_list_pluck( $thread->messages, 'id' );
+
+				bp_messages_star_set_action( array(
+					'action'     => 'star',
+					'message_id' => $mids[0],
+				) );
+			}
+
+			bp_core_add_message( sprintf( _n( '1 message was successfully starred', '%s messages were successfully starred', $count, 'buddypress' ), $count ) );
+			break;
+
+		case 'unstar' :
+			$count = count( $threads );
+
+			foreach ( $threads as $thread ) {
+				bp_messages_star_set_action( array(
+					'action'    => 'unstar',
+					'thread_id' => $thread,
+					'bulk'      => true
+				) );
+			}
+
+			bp_core_add_message( sprintf( _n( '1 message was successfully unstarred', '%s messages were successfully unstarred', $count, 'buddypress' ), $count ) );
+			break;
+	}
+
+	// Redirect back to message box.
+	bp_core_redirect( bp_displayed_user_domain() . bp_get_messages_slug() . '/' . bp_current_action() . '/' );
+	die();
+}
+add_action( 'bp_actions', 'bp_messages_star_bulk_manage_handler', 5 );
+
+/** HOOKS ****************************************************************/
+
+/**
+ * Enqueues the dashicons font.
+ *
+ * The dashicons font is used for the star / unstar icon.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_enqueue_scripts() {
+	if ( ! bp_is_user_messages() ) {
+		return;
+	}
+
+	wp_enqueue_style( 'dashicons' );
+}
+add_action( 'bp_enqueue_scripts', 'bp_messages_star_enqueue_scripts' );
+
+/**
+ * Add the "Add star" and "Remove star" options to the bulk management list.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_messages_star_bulk_management_dropdown() {
+?>
+
+	<option value="star"><?php _e( 'Add star', 'buddypress' ); ?></option>
+	<option value="unstar"><?php _e( 'Remove star', 'buddypress' ); ?></option>
+
+<?php
+}
+add_action( 'bp_messages_bulk_management_dropdown', 'bp_messages_star_bulk_management_dropdown', 1 );
+
+/**
+ * Add CSS class for the current message depending on starred status.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  array $retval Current CSS classes
+ * @return array
+ */
+function bp_messages_star_message_css_class( $retval = array() ) {
+	if ( true === bp_messages_is_message_starred( bp_get_the_thread_message_id() ) ) {
+		$status = 'starred';
+	} else {
+		$status = 'not-starred';
+	}
+
+	// add css class based on star status for the current message
+	$retval[] = "message-{$status}";
+
+	return $retval;
+}
+add_filter( 'bp_get_the_thread_message_css_class', 'bp_messages_star_message_css_class' );
Index: src/bp-messages/bp-messages-template.php
===================================================================
--- src/bp-messages/bp-messages-template.php
+++ src/bp-messages/bp-messages-template.php
@@ -1290,6 +1290,14 @@
 		<option value="read"><?php _e( 'Mark read', 'buddypress' ); ?></option>
 		<option value="unread"><?php _e( 'Mark unread', 'buddypress' ); ?></option>
 		<option value="delete"><?php _e( 'Delete', 'buddypress' ); ?></option>
+		<?php
+			/**
+			 * Action to add additional options to the messages bulk management dropdown.
+			 *
+			 * @since BuddyPress (2.3.0)
+			 */
+			do_action( 'bp_messages_bulk_management_dropdown' );
+		?>
 	</select>
 	<input type="submit" id="messages-bulk-manage" class="button action" value="<?php esc_attr_e( 'Apply', 'buddypress' ); ?>">
 	<?php
Index: src/bp-templates/bp-legacy/buddypress-functions.php
===================================================================
--- src/bp-templates/bp-legacy/buddypress-functions.php
+++ src/bp-templates/bp-legacy/buddypress-functions.php
@@ -177,6 +177,11 @@
 			'messages_send_reply'           => 'bp_legacy_theme_ajax_messages_send_reply',
 		);
 
+		// Conditional actions
+		if ( bp_is_active( 'messages', 'star' ) ) {
+			$actions['messages_star'] = 'bp_legacy_theme_ajax_messages_star_handler';
+		}
+
 		/**
 		 * Register all of these AJAX handlers
 		 *
@@ -307,6 +312,23 @@
 			// Enqueue script
 			wp_enqueue_script( $asset['handle'] . '-password-verify', $asset['location'], $dependencies, $this->version);
 		}
+
+		// Star private messages
+		if ( bp_is_active( 'messages', 'star' ) && bp_is_user_messages() ) {
+			wp_localize_script( $asset['handle'], 'BP_PM_Star', array(
+				'strings' => array(
+					'text_unstar'  => __( 'Unstar', 'buddypress' ),
+					'text_star'    => __( 'Star', 'buddypress' ),
+					'title_unstar' => __( 'Starred', 'buddypress' ),
+					'title_star'   => __( 'Not starred', 'buddypress' ),
+					'title_unstar_thread' => __( 'Remove all starred messages in this thread', 'buddypress' ),
+					'title_star_thread'   => __( 'Star the first message in this thread', 'buddypress' ),
+				),
+				'is_single_thread' => (int) bp_is_messages_conversation(),
+				'star_counter'     => 0,
+				'unstar_counter'   => 0
+			) );
+		}
 	}
 
 	/**
@@ -1687,3 +1709,34 @@
 
 	exit;
 }
+
+/**
+ * AJAX callback to set a message's star status.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+function bp_legacy_theme_ajax_messages_star_handler() {
+	if ( false === bp_is_active( 'messages', 'star' ) || empty( $_POST['message_id'] ) ) {
+		return;
+	}
+
+	// Check nonce
+	check_ajax_referer( 'bp-messages-star-' . (int) $_POST['message_id'], 'nonce' );
+
+	// Check capability
+	if ( ! is_user_logged_in() || ( ! bp_is_my_profile() && ! bp_current_user_can( 'bp_moderate' ) ) ) {
+		return;
+	}
+
+	if ( true === bp_messages_star_set_action( array(
+		'action'     => $_POST['star_status'],
+		'message_id' => (int) $_POST['message_id'],
+		'bulk'       => ! empty( $_POST['bulk'] ) ? true : false
+	 ) ) ) {
+		echo '1';
+		die();
+	}
+
+	echo '-1';
+	die();
+}
Index: src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php
===================================================================
--- src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php
+++ src/bp-templates/bp-legacy/buddypress/members/single/messages/messages-loop.php
@@ -61,6 +61,10 @@
 					 */
 					do_action( 'bp_messages_inbox_list_header' ); ?>
 
+					<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+						<th scope="col" class="thread-star"><span class="message-action-star"><span class="icon"></span> <span class="screen-reader-text"><?php _e( 'Star', 'buddypress' ); ?></span></span></th>
+					<?php endif; ?>
+
 					<th scope="col" class="thread-options"><?php _e( 'Actions', 'buddypress' ); ?></th>
 				</tr>
 			</thead>
@@ -107,6 +111,12 @@
 						 */
 						do_action( 'bp_messages_inbox_list_item' ); ?>
 
+						<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+							<td class="thread-star">
+								<?php bp_the_message_star_action_link( array( 'thread_id' => bp_get_message_thread_id() ) ); ?>
+							</td>
+						<?php endif; ?>
+
 						<td class="thread-options">
 							<?php if ( bp_message_thread_has_unread() ) : ?>
 								<a class="read" href="<?php bp_the_message_thread_mark_read_url();?>"><?php _e( 'Read', 'buddypress' ); ?></a>
Index: src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php
===================================================================
--- src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php
+++ src/bp-templates/bp-legacy/buddypress/members/single/messages/single.php
@@ -69,6 +69,12 @@
 
 					<span class="activity"><?php bp_the_thread_message_time_since(); ?></span>
 
+					<?php if ( bp_is_active( 'messages', 'star' ) ) : ?>
+						<div class="message-star-actions">
+							<?php bp_the_message_star_action_link(); ?>
+						</div>
+					<?php endif; ?>
+
 					<?php
 
 					/** This action is documented in bp-templates/bp-legacy/buddypress-functions.php */
Index: src/bp-templates/bp-legacy/css/buddypress.css
===================================================================
--- src/bp-templates/bp-legacy/css/buddypress.css
+++ src/bp-templates/bp-legacy/css/buddypress.css
@@ -1448,6 +1448,39 @@
 	margin: 0 20px;
 }
 
+.message-metadata {
+	position: relative;
+}
+.message-star-actions {
+	position: absolute;
+	right: 0;
+	top: 0;
+}
+a.message-action-star,
+a.message-action-unstar {
+	text-decoration: none;
+	outline: none;
+}
+a.message-action-star {
+	opacity: .7;
+}
+a.message-action-star:hover {
+	opacity: 1;
+}
+.message-action-star span.icon:before,
+.message-action-unstar span.icon:before {
+	font-family: dashicons;
+	font-size: 18px;
+}
+.message-action-star span.icon:before {
+	color: #aaa;
+	content: "\f154";
+}
+.message-action-unstar span.icon:before {
+	color: #FCDD77;
+	content: "\f155";
+}
+
 /*--------------------------------------------------------------
 3.10 - Extended Profiles
 --------------------------------------------------------------*/
Index: src/bp-templates/bp-legacy/js/buddypress.js
===================================================================
--- src/bp-templates/bp-legacy/js/buddypress.js
+++ src/bp-templates/bp-legacy/js/buddypress.js
@@ -1578,6 +1578,84 @@
 		jq('#messages-bulk-manage').attr('disabled', jq(this).val().length <= 0);
 	});
 
+	/* Star action function */
+	starAction = function() {
+		var link = jq(this);
+
+		jq.post( ajaxurl, {
+			action: 'messages_star',
+			'message_id': link.data('message-id'),
+			'star_status': link.data('star-status'),
+			'nonce': link.data('star-nonce'),
+			'bulk': link.data('star-bulk')
+		},
+		function(response) {
+			if ( 1 == response ) {
+				if ( 'unstar' === link.data('star-status') ) {
+					link.data('star-status', 'star');
+					link.removeClass('message-action-unstar').addClass('message-action-star');
+					link.find('.bp-screen-reader-text').text( BP_PM_Star.strings.text_star );
+
+					if ( 1 == BP_PM_Star.is_single_thread ) {
+						link.prop('title', BP_PM_Star.strings.title_star );
+					} else {
+						link.prop('title', BP_PM_Star.strings.title_star_thread );
+					}
+
+				} else {
+					link.data('star-status', 'unstar');
+					link.removeClass('message-action-star').addClass('message-action-unstar');
+					link.find('.bp-screen-reader-text').text(BP_PM_Star.strings.text_unstar);
+
+					if ( 1 == BP_PM_Star.is_single_thread ) {
+						link.prop('title', BP_PM_Star.strings.title_unstar );
+					} else {
+						link.prop('title', BP_PM_Star.strings.title_unstar_thread );
+					}
+				}
+			}
+		});
+		return false;
+	}
+
+	/* Star actions */
+	jq('#message-threads').on('click', 'td.thread-star a', starAction );
+	jq('#message-thread').on('click', '.message-star-actions a', starAction );
+
+	/* Star bulk manage - Show only the valid action based on the starred item. */
+	jq('#message-threads td.bulk-select-check :checkbox').on('change', function() {
+		var box = jq(this),
+			star = box.closest('tr').find('.thread-star a');
+
+		if ( box.prop("checked") ) {
+			if( 'unstar' == star.data('star-status') ) {
+				BP_PM_Star.star_counter++;
+			} else {
+				BP_PM_Star.unstar_counter++;
+			}
+		} else {
+			if( 'unstar' == star.data('star-status') ) {
+				BP_PM_Star.star_counter--;
+			} else {
+				BP_PM_Star.unstar_counter--;
+			}
+		}
+
+		if ( BP_PM_Star.star_counter > 0 && BP_PM_Star.unstar_counter == 0 ) {
+			jq('option[value="star"]').hide();
+		} else {
+			jq('option[value="star"]').show();
+		}
+
+		if ( BP_PM_Star.unstar_counter > 0 && BP_PM_Star.star_counter == 0 ) {
+			jq('option[value="unstar"]').hide();
+		} else {
+			jq('option[value="unstar"]').show();
+		}
+	});
+
+	/** Notifications **********************************************/
+
 	/* Selecting/Deselecting all notifications */
 	jq('#select-all-notifications').click(function(event) {
 		if( this.checked ) {
Index: tests/phpunit/testcases/core/functions.php
===================================================================
--- tests/phpunit/testcases/core/functions.php
+++ tests/phpunit/testcases/core/functions.php
@@ -620,4 +620,82 @@
 
 		$this->assertSame( $expected_upload_dir, $tested_upload_dir );
 	}
+
+	/**
+	 * @group bp_is_active
+	 */
+	public function test_bp_is_active_component() {
+		$bp = buddypress();
+		$reset_active_components = $bp->active_components;
+
+		$this->assertTrue( bp_is_active( 'members' ) );
+
+		$this->assertFalse( bp_is_active( 'foo' ) );
+
+		// Create and activate the foo component
+		$bp->foo = new BP_Component;
+		$bp->foo->id   = 'foo';
+		$bp->foo->slug = 'foo';
+		$bp->foo->name = 'Foo';
+		$bp->active_components[ $bp->foo->id ] = 1;
+
+		$this->assertTrue( bp_is_active( 'foo' ) );
+
+		add_filter( 'bp_is_active', '__return_false' );
+
+		$this->assertFalse( bp_is_active( 'foo' ) );
+
+		remove_filter( 'bp_is_active', '__return_false' );
+
+		// Reset buddypress() vars
+		$bp->active_components = $reset_active_components;
+	}
+
+	/**
+	 * @group bp_is_active
+	 */
+	public function test_bp_is_active_feature() {
+		$bp = buddypress();
+		$reset_active_components = $bp->active_components;
+
+		// star feature for messages component is active by default
+		$this->assertTrue( bp_is_active( 'messages', 'star' ) );
+
+		// test the feature filter
+		add_filter( 'bp_is_messages_star_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'messages', 'star' ) );
+		remove_filter( 'bp_is_messages_star_active', '__return_false' );
+
+		// test the main component filter
+		add_filter( 'bp_is_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'messages', 'star' ) );
+		remove_filter( 'bp_is_active', '__return_false' );
+
+		// Create and activate the foo component
+		$bp->foo = new BP_Component;
+		$bp->foo->id   = 'foo';
+		$bp->foo->slug = 'foo';
+		$bp->foo->name = 'Foo';
+		$bp->active_components[ $bp->foo->id ] = 1;
+
+		// foo did not register 'bar' as a feature
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+
+		// fake registering the 'bar' feature
+		$bp->foo->features = array( 'bar' );
+		$this->assertTrue( bp_is_active( 'foo', 'bar' ) );
+
+		// test the feature filter
+		add_filter( 'bp_is_foo_bar_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+		remove_filter( 'bp_is_foo_bar_active', '__return_false' );
+
+		// test the main component filter
+		add_filter( 'bp_is_active', '__return_false' );
+		$this->assertFalse( bp_is_active( 'foo', 'bar' ) );
+		remove_filter( 'bp_is_active', '__return_false' );
+
+		// Reset buddypress() vars
+		$bp->active_components = $reset_active_components;
+	}
 }
Index: tests/phpunit/testcases/messages/star.php
new file mode 100644
===================================================================
--- /dev/null
+++ tests/phpunit/testcases/messages/star.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * @group messages
+ * @group star
+ */
+class BP_Tests_Messages_Star_ extends BP_UnitTestCase {
+
+	/**
+	 * @group bp_messages_is_message_starred
+	 * @group bp_messages_star_set_action
+	 */
+	public function test_is_message_starred() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+
+		// create the thread
+		$t1 = $this->factory->message->create( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+
+		// create a reply
+		$this->factory->message->create( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+
+		// grab the message ids as individual variables
+		list( $m1, $m2 ) = $this->get_message_ids( $t1 );
+
+		// star the second message
+		$star = bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+		) );
+
+		// assert that star is set
+		$this->assertTrue( $star );
+		$this->assertTrue( bp_messages_is_message_starred( $m2, $u1 ) );
+
+		// unstar the second message
+		$unstar = bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+			'action'     => 'unstar'
+		) );
+
+		// assert that star is removed
+		$this->assertTrue( $unstar );
+		$this->assertFalse( bp_messages_is_message_starred( $m2, $u1 ) );
+	}
+
+	/**
+	 * @group bp_messages_star_set_action
+	 * @group bulk
+	 */
+	public function test_star_set_action_bulk_unstar() {
+		$u1 = $this->factory->user->create();
+		$u2 = $this->factory->user->create();
+
+		// create the thread
+		$t1 = $this->factory->message->create( array(
+			'sender_id'  => $u1,
+			'recipients' => array( $u2 ),
+			'subject'    => 'This is a knive',
+		) );
+
+		// create a reply
+		$this->factory->message->create( array(
+			'thread_id'  => $t1,
+			'sender_id'  => $u2,
+			'recipients' => array( $u1 ),
+			'content'    => "That's a spoon",
+		) );
+
+		// grab the message ids as individual variables
+		list( $m1, $m2 ) = $this->get_message_ids( $t1 );
+
+		// star all messages
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m1,
+		) );
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'message_id' => $m2,
+		) );
+
+		// assert that stars are set
+		$this->assertTrue( bp_messages_is_message_starred( $m1, $u1 ) );
+		$this->assertTrue( bp_messages_is_message_starred( $m2, $u1 ) );
+
+		// unstar all messages
+		bp_messages_star_set_action( array(
+			'user_id'    => $u1,
+			'thread_id'  => $t1,
+			'action'     => 'unstar',
+			'bulk'       => true
+		) );
+
+		// assert that star is removed
+		$this->assertFalse( bp_messages_is_message_starred( $m1, $u1 ) );
+		$this->assertFalse( bp_messages_is_message_starred( $m2, $u1 ) );
+	}
+
+	/**
+	 * Helper method to grab the message IDs from a message thread.
+	 *
+	 * @param int $thread_id The message thread ID
+	 * @return array
+	 */
+	protected function get_message_ids( $thread_id = 0 ) {
+		$thread = new BP_Messages_Thread( $thread_id );
+		return wp_list_pluck( $thread->messages, 'id' );
+	}
+}
\ No newline at end of file
