Index: akismet.php
--- akismet.php Base
+++ akismet.php Locally Modified
@@ -68,6 +68,11 @@
 		// Update post meta
 		add_action( 'wp_insert_post', array( $this, 'update_post_meta' ), 10, 2 );

+		// Cleanup
+		add_action( 'akismet_scheduled_delete', array( $this, 'delete_old_spam' ) );
+		add_action( 'akismet_scheduled_delete', array( $this, 'delete_old_spam_meta' ) );
+		add_action( 'akismet_scheduled_delete', array( $this, 'delete_orphaned_spam_meta' ) );
+
 		// Admin
 		if ( is_admin() ) {
 			add_action( 'add_meta_boxes', array( $this, 'add_metaboxes' ) );
@@ -954,5 +959,276 @@

 		<?php
 	}
+
+	/**
+	 * Delete old spam topics & replies from the queue after a few days
+	 * (determined by `_bbp_akismet_delete_spam_interval` filter).
+	 *
+	 * @since 2.7.0 bbPress (r7203)
+	 *
+	 * @global wpdb $wpdb
+	 */
+	public function delete_old_spam() {
+		global $wpdb;
+
+		/**
+		 * Determines how many posts will be deleted in each batch.
+		 *
+		 * @param int The default as defined by AKISMET_DELETE_LIMIT (also used
+		 *            in Akismet WordPress plugin).
+		 */
+		$delete_limit = (int) apply_filters( '_bbp_akismet_delete_spam_limit',
+			defined( 'AKISMET_DELETE_LIMIT' )
+				? AKISMET_DELETE_LIMIT
+				: 10000
+		);
+
+		// Validate the deletion limit
+		$delete_limit = max( 1, intval( $delete_limit ) );
+
+		/**
+		 * Determines how many days a piece of spam will be left in the Spam
+		 * queue before being deleted.
+		 *
+		 * @param int The default number of days.
+		 */
+		$delete_interval = (int) apply_filters( '_bbp_akismet_delete_spam_interval', 15 );
+
+		// Validate the deletion interval
+		$delete_interval = max( 1, intval( $delete_interval ) );
+
+		// Setup the query
+		$sql = "SELECT id FROM {$wpdb->posts} WHERE post_type IN ('topic', 'reply') AND post_status = 'spam' AND DATE_SUB(NOW(), INTERVAL %d DAY) > post_date_gmt LIMIT %d";
+
+		// Query loop of topic & reply IDs
+		while ( $spam_ids = $wpdb->get_col( $wpdb->prepare( $sql, $delete_interval, $delete_limit ) ) ) {
+
+			// Exit loop if no spam IDs
+			if ( empty( $spam_ids ) ) {
+				break;
+			}
+
+			// Reset queries
+			$wpdb->queries = array();
+
+			// Loop through each of the topic/reply IDs
+			foreach ( $spam_ids as $spam_id ) {
+
+				/**
+				 * Perform a single action on the single topic/reply ID for
+				 * simpler batch processing.
+				 *
+				 * Maybe we should run the bbp_delete_topic or bbp_delete_reply
+				 * actions here, too?
+				 *
+				 * @param string The current function.
+				 * @param int    The current topic/reply ID.
+				 */
+				do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_id );
+			}
+
+			// Prepared as strings since id is an unsigned BIGINT, and using %
+			// will constrain the value to the maximum signed BIGINT.
+			$format_string = implode( ", ", array_fill( 0, count( $spam_ids ), '%s' ) );
+
+			// Run the delete queries
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->posts} WHERE post_id IN ( " . $format_string . " )", $spam_ids ) );
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ( " . $format_string . " )", $spam_ids ) );
+
+			// Clean the post cache for these topics & replies
+			clean_post_cache( $spam_ids );
+
+			/**
+			 * Single action that encompasses all topic/reply IDs after the
+			 * delete queries have been run.
+			 *
+			 * @param int   Count of topic/reply IDs
+			 * @param array Array of topic/reply IDs
+			 */
+			do_action( '_bbp_akismet_delete_spam_count', count( $spam_ids ), $spam_ids );
+		}
+
+		/**
+		 * Determines whether tables should be optimized.
+		 *
+		 * @param int Random number between 1 and 5000.
+		 */
+		$optimize = (int) apply_filters( '_bbp_akismet_optimize_tables', mt_rand( 1, 5000 ), array( $wpdb->posts, $wpdb->postmeta ) );
+
+		// Lucky number 11
+		if ( 11 === $optimize ) {
+			$wpdb->query( "OPTIMIZE TABLE {$wpdb->posts}" );
+			$wpdb->query( "OPTIMIZE TABLE {$wpdb->postmeta}" );
+		}
+	}
+
+	/**
+	 * Delete the `_bbp_akismet_as_submitted` meta keys after a while, since
+	 * they are large and not useful in the long term.
+	 *
+	 * @since 2.7.0 bbPress (r7203)
+	 *
+	 * @global wpdb $wpdb
+	 */
+	public function delete_old_spam_meta() {
+		global $wpdb;
+
+		/**
+		 * Determines how many days a piece of spam will be left in the Spam
+		 * queue before being deleted.
+		 *
+		 * @param int The default number of days.
+		 */
+		$interval = (int) apply_filters( '_bbp_akismet_delete_spam_meta_interval', 15 );
+
+		// Validate the deletion interval
+		$interval = max( 1, intval( $interval ) );
+
+		// Setup the query
+		$sql = "SELECT m.post_id FROM {$wpdb->postmeta} as m INNER JOIN {$wpdb->posts} as p ON m.post_id = p.id WHERE m.meta_key = '_bbp_akismet_as_submitted' AND DATE_SUB(NOW(), INTERVAL %d DAY) > p.post_date_gmt LIMIT 10000";
+
+		// Query loop of topic & reply IDs
+		while ( $spam_ids = $wpdb->get_col( $wpdb->prepare( $sql, $interval ) ) ) {
+
+			// Exit loop if no spam IDs
+			if ( empty( $spam_ids ) ) {
+				break;
+			}
+
+			// Reset queries
+			$wpdb->queries = array();
+
+			// Loop through each of the topic/reply IDs
+			foreach ( $spam_ids as $spam_id ) {
+
+				// Delete the as_submitted meta data
+				delete_post_meta( $spam_id, '_bbp_akismet_as_submitted' );
+
+				/**
+				 * Perform a single action on the single topic/reply ID for
+				 * simpler batch processing.
+				 *
+				 * @param string The current function.
+				 * @param int    The current topic/reply ID.
+				 */
+				do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_id );
+			}
+
+			/**
+			 * Single action that encompasses all topic/reply IDs after the
+			 * delete queries have been run.
+			 *
+			 * @param int   Count of topic/reply IDs
+			 * @param array Array of topic/reply IDs
+			 */
+			do_action( '_bbp_akismet_delete_spam_meta_count', count( $spam_ids ), $spam_ids );
+		}
+
+		// Maybe optimize
+		$this->maybe_optimize_postmeta();
+	}
+
+	/**
+	 * Clear out posts meta that no longer have corresponding posts in the database
+	 *
+	 * @since 2.7.0 bbPress (r7203)
+	 *
+	 * @global wpdb $wpdb
+	 */
+	public function delete_orphaned_spam_meta() {
+		global $wpdb;
+
+		$last_meta_id = 0;
+
+		// Start time (float)
+		$start_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
+			? (float) $_SERVER['REQUEST_TIME_FLOAT']
+			: microtime( true );
+
+		// Maximum time
+		$max_exec_time = (float) max( ini_get( 'max_execution_time' ) - 5, 3 );
+
+		// Setup the query
+		$sql = "SELECT m.meta_id, m.post_id, m.meta_key FROM {$wpdb->postmeta} as m LEFT JOIN {$wpdb->posts} as p ON m.post_id = p.id WHERE p.id IS NULL AND m.meta_id > %d ORDER BY m.meta_id LIMIT 1000";
+
+		// Query loop of topic & reply IDs
+		while ( $spam_meta_results = $wpdb->get_results( $wpdb->prepare( $sql, $last_meta_id ) ) ) {
+
+			// Exit loop if no spam IDs
+			if ( empty( $spam_meta_results ) ) {
+				break;
+			}
+
+			// Reset queries
+			$wpdb->queries = array();
+
+			// Reset deleted meta count
+			$spam_meta_deleted = array();
+
+			// Loop through each of the metas
+			foreach ( $spam_meta_results as $spam_meta ) {
+
+				// Skip if not an Akismet key
+				if ( 'akismet_' !== substr( $spam_meta->meta_key, 0, 8 ) ) {
+					continue;
+				}
+
+				// Delete the meta
+				delete_post_meta( $spam_meta->post_id, $spam_meta->meta_key );
+
+				/**
+				 * Perform a single action on the single topic/reply ID for
+				 * simpler batch processing.
+				 *
+				 * @param string The current function.
+				 * @param int    The current topic/reply ID.
+				 */
+				do_action( '_bbp_akismet_batch_delete', __FUNCTION__, $spam_meta );
+
+				// Stash the meta ID being deleted
+				$spam_meta_deleted[] = $last_meta_id = $spam_meta->meta_id;
+			}
+
+			/**
+			 * Single action that encompasses all topic/reply IDs after the
+			 * delete queries have been run.
+			 *
+			 * @param int   Count of spam meta IDs
+			 * @param array Array of spam meta IDs
+			 */
+			do_action( '_bbp_akismet_delete_spam_meta_count', count( $spam_meta_deleted ), $spam_meta_deleted );
+
+			// Break if getting close to max_execution_time.
+			if ( ( microtime( true ) - $start_time ) > $max_exec_time ) {
+				break;
+			}
+		}
+
+		// Maybe optimize
+		$this->maybe_optimize_postmeta();
+	}
+
+	/**
+	 * Maybe OPTIMIZE the _postmeta database table.
+	 *
+	 * @since 2.7.0 bbPress (r7203)
+	 *
+	 * @global wpdb $wpdb
+	 */
+	private function maybe_optimize_postmeta() {
+		global $wpdb;
+
+		/**
+		 * Determines whether tables should be optimized.
+		 *
+		 * @param int Random number between 1 and 5000.
+		 */
+		$optimize = (int) apply_filters( '_bbp_akismet_optimize_table', mt_rand( 1, 5000 ), $wpdb->postmeta );
+
+		// Lucky number 11
+		if ( 11 === $optimize ) {
+			$wpdb->query( "OPTIMIZE TABLE {$wpdb->postmeta}" );
+		}
+	}
 }
 endif;
