diff --git a/config/default.yml b/config/default.yml
index 27241077..3b3548e9 100644
--- a/config/default.yml
+++ b/config/default.yml
@@ -12,6 +12,8 @@ forbiddenTicketPrefix: '!'
 
 requiredTicketPrefix: ''
 
+filterRemovalTimeout: 300000
+
 embedDeletionEmoji: '🗑️'
 
 maxSearchResults: 5
@@ -38,9 +40,11 @@ request:
     - ☑️
     - ❌
     - 💬
+    - 📁
 
   ignorePrependResponseMessageEmoji: ✅
   ignoreResolutionEmoji: 💬
+  bulkEmoji: 📁
 
   resolveDelay: 10000
   progressMessageAddDelay: 10000
diff --git a/config/template.yml b/config/template.yml
index 7cbee21f..5ec06a0f 100644
--- a/config/template.yml
+++ b/config/template.yml
@@ -37,6 +37,9 @@ forbiddenTicketPrefix: <string>
 # When omitted or empty, no prefix is required for posting embeds.
 requiredTicketPrefix: <string>
 
+# The time (in milliseconds) the bot will wait before removing a '!jira bulk' filter
+filterRemovalTimeout: <number>
+
 # An emoji or emoji ID which, when reacted to a bot embed, deletes it.
 embedDeletionEmoji: <string>
 
@@ -106,6 +109,9 @@ request:
   # An emoji or emoji ID which, when used, doesn't trigger the response template message.
   ignorePrependResponseMessageEmoji: <string>
 
+  # An emoji or emoji ID which indicates a request message to be added to a bulk action.
+  bulkEmoji: <string>
+
   # The amount of time in milliseconds between a volunteer reacts to the message and the bot deletes its message.
   resolveDelay: <number>
 
diff --git a/src/BotConfig.ts b/src/BotConfig.ts
index 2b1a37c5..3a132824 100644
--- a/src/BotConfig.ts
+++ b/src/BotConfig.ts
@@ -28,6 +28,7 @@ export class RequestConfig {
 	public suggestedEmoji: string[];
 	public ignorePrependResponseMessageEmoji: string;
 	public ignoreResolutionEmoji: string;
+	public bulkEmoji: string;
 	public resolveDelay: number;
 	public progressMessageAddDelay: number;
 	public prependResponseMessage: PrependResponseMessageType;
@@ -52,6 +53,7 @@ export class RequestConfig {
 		this.suggestedEmoji = getOrDefault( 'request.suggestedEmoji', [] );
 		this.ignorePrependResponseMessageEmoji = config.get( 'request.ignorePrependResponseMessageEmoji' );
 		this.ignoreResolutionEmoji = config.get( 'request.ignoreResolutionEmoji' );
+		this.bulkEmoji = config.get( 'request.bulkEmoji' );
 
 		this.resolveDelay = config.get( 'request.resolveDelay' );
 		this.progressMessageAddDelay = config.get( 'request.progressMessageAddDelay' );
@@ -113,6 +115,8 @@ export default class BotConfig {
 	public static requiredTicketPrefix: string;
 	public static forbiddenTicketPrefix: string;
 
+	public static filterRemovalTimeout: number;
+
 	public static embedDeletionEmoji: string;
 
 	public static maxSearchResults: number;
@@ -140,6 +144,8 @@ export default class BotConfig {
 		this.forbiddenTicketPrefix = getOrDefault( 'forbiddenTicketPrefix', '' );
 		this.requiredTicketPrefix = getOrDefault( 'requiredTicketPrefix', '' );
 
+		this.filterRemovalTimeout = config.get( 'filterRemovalTimeout' );
+
 		this.embedDeletionEmoji = getOrDefault( 'embedDeletionEmoji', '' );
 
 		this.maxSearchResults = config.get( 'maxSearchResults' );
diff --git a/src/commands/BulkCommand.ts b/src/commands/BulkCommand.ts
new file mode 100644
index 00000000..cdcf9499
--- /dev/null
+++ b/src/commands/BulkCommand.ts
@@ -0,0 +1,86 @@
+import { Message, MessageReaction, User } from 'discord.js';
+import PrefixCommand from './PrefixCommand';
+import Command from './Command';
+import { RequestsUtil } from '../util/RequestsUtil';
+import { EmojiUtil } from '../util/EmojiUtil';
+import BotConfig from '../BotConfig';
+import RequestResolveEventHandler from '../events/request/RequestResolveEventHandler';
+import MojiraBot from '../MojiraBot';
+
+export default class BulkCommand extends PrefixCommand {
+	public readonly aliases = ['bulk', 'filter'];
+
+	public static currentBulkReactions = new Map<User, Message[]>();
+
+	public async run( message: Message, args: string ): Promise<boolean> {
+		let emoji: string;
+
+		if ( args.length ) {
+			emoji = EmojiUtil.getEmoji( args );
+			if ( !emoji ) {
+				await message.channel.send( `**Error:** ${ args } is not a valid emoji.` );
+				return false;
+			}
+		}
+
+		const ticketKeys: string[] = [];
+		let firstMentioned: string;
+
+		try {
+			const bulkMessages = BulkCommand.currentBulkReactions.get( message.author );
+			const originMessages: Message[] = [];
+			if ( bulkMessages ) {
+				for ( const bulk of bulkMessages ) {
+					originMessages.push( await RequestsUtil.getOriginMessage( bulk ) );
+
+					if ( emoji ) {
+						let reaction: MessageReaction;
+						if ( bulk.reactions.cache.has( emoji ) ) {
+							reaction = bulk.reactions.cache.get( emoji );
+						} else {
+							reaction = await bulk.react( emoji );
+						}
+						if ( emoji != BotConfig.request.bulkEmoji ) {
+							await new RequestResolveEventHandler( MojiraBot.client.user.id ).onEvent( reaction, message.author );
+							return true;
+						} else {
+							await bulk.reactions.cache.get( BotConfig.request.bulkEmoji ).users.remove( message.author );
+						}
+					}
+				}
+				originMessages.forEach( origin => ticketKeys.push( ...RequestsUtil.getTickets( origin.content ) ) );
+				firstMentioned = ticketKeys[0];
+				if ( emoji == BotConfig.request.bulkEmoji ) {
+					BulkCommand.currentBulkReactions.delete( message.author );
+					return true;
+				}
+			} else {
+				return false;
+			}
+		} catch ( err ) {
+			Command.logger.error( err );
+			return false;
+		}
+
+		const filter = `https://bugs.mojang.com/browse/${ firstMentioned }?jql=key%20in(${ ticketKeys.join( '%2C' ) })`;
+
+		try {
+			await message.channel.send( `${ message.author.toString() } ${ filter }` );
+		} catch ( err ) {
+			Command.logger.error( err );
+			return false;
+		}
+
+		try {
+			await message.react( '✅' );
+		} catch ( err ) {
+			Command.logger.error( err );
+		}
+
+		return true;
+	}
+
+	public asString( args: string ): string {
+		return `!jira bulk ${ args }`;
+	}
+}
diff --git a/src/commands/CommandRegistry.ts b/src/commands/CommandRegistry.ts
index f448d89a..d665e6b4 100644
--- a/src/commands/CommandRegistry.ts
+++ b/src/commands/CommandRegistry.ts
@@ -1,4 +1,5 @@
 import BugCommand from './BugCommand';
+import BulkCommand from './BulkCommand';
 import HelpCommand from './HelpCommand';
 import PingCommand from './PingCommand';
 import MooCommand from './MooCommand';
@@ -11,6 +12,7 @@ import TipsCommand from './TipsCommand';
 
 export default class CommandRegistry {
 	public static BUG_COMMAND = new BugCommand();
+	public static BULK_COMMAND = new BulkCommand();
 	public static HELP_COMMAND = new HelpCommand();
 	public static MENTION_COMMAND = new MentionCommand();
 	public static MOO_COMMAND = new MooCommand();
diff --git a/src/commands/PollCommand.ts b/src/commands/PollCommand.ts
index a9a7b07e..934ff5e1 100644
--- a/src/commands/PollCommand.ts
+++ b/src/commands/PollCommand.ts
@@ -1,13 +1,12 @@
 import PrefixCommand from './PrefixCommand';
 import { Message, TextChannel, DMChannel, MessageEmbed, NewsChannel } from 'discord.js';
 import Command from './Command';
-import emojiRegex = require( 'emoji-regex/text.js' );
+import { EmojiUtil } from '../util/EmojiUtil';
 import PermissionRegistry from '../permissions/PermissionRegistry';
 import { ReactionsUtil } from '../util/ReactionsUtil';
 
 interface PollOption {
 	emoji: string;
-	emojiName?: string;
 	rawEmoji: string;
 	text: string;
 }
@@ -103,26 +102,16 @@ export default class PollCommand extends PrefixCommand {
 
 			const optionArgs = /^\s*(\S+)\s+(.+)\s*$/.exec( option );
 
-			const customEmoji = /^<a?:(\w+):(\d+)>/;
-			const unicodeEmoji = emojiRegex();
-
 			if ( !optionArgs ) {
 				await this.sendSyntaxMessage( message.channel, 'Invalid options' );
 				return false;
 			}
 
 			const emoji = optionArgs[1];
-			if ( customEmoji.test( emoji ) || unicodeEmoji.test( emoji ) ) {
-				let emojiName = emoji;
-				let rawEmoji = emoji;
-				const emojiMatch = customEmoji.exec( emoji );
-				if ( emojiMatch ) {
-					emojiName = emojiMatch[1];
-					rawEmoji = emojiMatch[2];
-				}
+			const rawEmoji = EmojiUtil.getEmoji( emoji );
+			if ( rawEmoji ) {
 				options.push( {
 					emoji: emoji,
-					emojiName: emojiName,
 					rawEmoji: rawEmoji,
 					text: optionArgs[2],
 				} );
diff --git a/src/events/request/RequestResolveEventHandler.ts b/src/events/request/RequestResolveEventHandler.ts
index 6706bdac..4ca1e61a 100644
--- a/src/events/request/RequestResolveEventHandler.ts
+++ b/src/events/request/RequestResolveEventHandler.ts
@@ -1,6 +1,7 @@
 import { MessageReaction, User } from 'discord.js';
 import * as log4js from 'log4js';
 import BotConfig, { PrependResponseMessageType } from '../../BotConfig';
+import BulkCommand from '../../commands/BulkCommand';
 import ResolveRequestMessageTask from '../../tasks/ResolveRequestMessageTask';
 import TaskScheduler from '../../tasks/TaskScheduler';
 import { RequestsUtil } from '../../util/RequestsUtil';
@@ -27,26 +28,34 @@ export default class RequestResolveEventHandler implements EventHandler<'message
 		this.logger.info( `User ${ user.tag } added '${ reaction.emoji.name }' reaction to request message '${ reaction.message.id }'` );
 
 		TaskScheduler.clearMessageTasks( reaction.message );
-		await reaction.message.edit( reaction.message.embeds[0].setColor( RequestsUtil.getEmbedColor( user ) ) );
-
-		if ( BotConfig.request.prependResponseMessage == PrependResponseMessageType.WhenResolved
-			&& BotConfig.request.ignorePrependResponseMessageEmoji !== reaction.emoji.name ) {
-			const origin = await RequestsUtil.getOriginMessage( reaction.message );
-			if ( origin ) {
-				try {
-					await reaction.message.edit( RequestsUtil.getResponseMessage( origin ) );
-				} catch ( error ) {
-					this.logger.error( error );
+
+		if ( BotConfig.request.bulkEmoji !== reaction.emoji.name ) {
+			if ( BotConfig.request.prependResponseMessage == PrependResponseMessageType.WhenResolved
+				&& BotConfig.request.ignorePrependResponseMessageEmoji !== reaction.emoji.name ) {
+				const origin = await RequestsUtil.getOriginMessage( reaction.message );
+				if ( origin ) {
+					try {
+						await reaction.message.edit( RequestsUtil.getResponseMessage( origin ) );
+					} catch ( error ) {
+						this.logger.error( error );
+					}
 				}
 			}
-		}
 
-		if ( BotConfig.request.ignoreResolutionEmoji !== reaction.emoji.name ) {
-			TaskScheduler.addOneTimeMessageTask(
-				reaction.message,
-				new ResolveRequestMessageTask( reaction.emoji, user ),
-				BotConfig.request.resolveDelay || 0
-			);
+			if ( BotConfig.request.ignoreResolutionEmoji !== reaction.emoji.name ) {
+				await reaction.message.edit( reaction.message.embeds[0].setColor( RequestsUtil.getEmbedColor( user ) ) );
+				TaskScheduler.addOneTimeMessageTask(
+					reaction.message,
+					new ResolveRequestMessageTask( reaction.emoji, user, this.botUserId ),
+					BotConfig.request.resolveDelay || 0
+				);
+			}
+		} else {
+			if ( !BulkCommand.currentBulkReactions.has( user ) ) {
+				BulkCommand.currentBulkReactions.set( user, [ reaction.message ] );
+			} else {
+				BulkCommand.currentBulkReactions.get( user ).push( reaction.message );
+			}
 		}
 	};
 }
diff --git a/src/events/request/RequestUnresolveEventHandler.ts b/src/events/request/RequestUnresolveEventHandler.ts
index 379dce22..68857709 100644
--- a/src/events/request/RequestUnresolveEventHandler.ts
+++ b/src/events/request/RequestUnresolveEventHandler.ts
@@ -1,6 +1,7 @@
 import { MessageReaction, User } from 'discord.js';
 import * as log4js from 'log4js';
 import BotConfig, { PrependResponseMessageType } from '../../BotConfig';
+import BulkCommand from '../../commands/BulkCommand';
 import TaskScheduler from '../../tasks/TaskScheduler';
 import DiscordUtil from '../../util/DiscordUtil';
 import { RequestsUtil } from '../../util/RequestsUtil';
@@ -28,19 +29,22 @@ export default class RequestUnresolveEventHandler implements EventHandler<'messa
 
 		this.logger.info( `User ${ user.tag } removed '${ emoji.name }' reaction from request message '${ message.id }'` );
 
-		await message.edit( message.embeds[0].setColor( RequestsUtil.getEmbedColor() ) );
-
-		if ( BotConfig.request.prependResponseMessage == PrependResponseMessageType.WhenResolved ) {
-			try {
-				await message.edit( '' );
-			} catch ( error ) {
-				this.logger.error( error );
+		if ( BotConfig.request.bulkEmoji !== emoji.name ) {
+			await message.edit( message.embeds[0].setColor( RequestsUtil.getEmbedColor() ) );
+			if ( BotConfig.request.prependResponseMessage == PrependResponseMessageType.WhenResolved ) {
+				try {
+					await message.edit( '' );
+				} catch ( error ) {
+					this.logger.error( error );
+				}
 			}
-		}
 
-		if ( message.reactions.cache.size <= BotConfig.request.suggestedEmoji.length ) {
-			this.logger.info( `Cleared message task for request message '${ message.id }'` );
-			TaskScheduler.clearMessageTasks( message );
+			if ( message.reactions.cache.size <= BotConfig.request.suggestedEmoji.length ) {
+				this.logger.info( `Cleared message task for request message '${ message.id }'` );
+				TaskScheduler.clearMessageTasks( message );
+			}
+		} else if ( BulkCommand.currentBulkReactions.has( user ) ) {
+			BulkCommand.currentBulkReactions.set( user, BulkCommand.currentBulkReactions.get( user ).filter( stored => stored != message ) );
 		}
 	};
 }
\ No newline at end of file
diff --git a/src/tasks/ResolveRequestMessageTask.ts b/src/tasks/ResolveRequestMessageTask.ts
index e7b2553c..2ec6ffbe 100644
--- a/src/tasks/ResolveRequestMessageTask.ts
+++ b/src/tasks/ResolveRequestMessageTask.ts
@@ -4,17 +4,20 @@ import DiscordUtil from '../util/DiscordUtil';
 import { RequestsUtil } from '../util/RequestsUtil';
 import MessageTask from './MessageTask';
 import * as log4js from 'log4js';
+import BulkCommand from '../commands/BulkCommand';
 
 export default class ResolveRequestMessageTask extends MessageTask {
 	private static logger = log4js.getLogger( 'ResolveRequestMessageTask' );
 
 	private readonly emoji: EmojiResolvable;
 	private readonly user: User;
+	private readonly botUserId: string;
 
-	constructor( emoji: EmojiResolvable, user: User ) {
+	constructor( emoji: EmojiResolvable, user: User, botUserId: string ) {
 		super();
 		this.emoji = emoji;
 		this.user = user;
+		this.botUserId = botUserId;
 	}
 
 	public async run( copy: Message ): Promise<void> {
@@ -33,6 +36,16 @@ export default class ResolveRequestMessageTask extends MessageTask {
 
 		if ( origin ) {
 			try {
+				await origin.reactions.cache.forEach( async reaction => {
+					if ( reaction.emoji.name == BotConfig.request.bulkEmoji ) {
+						const users = await reaction.users.fetch();
+						users.forEach( user => {
+							if ( user.id != this.botUserId ) {
+								BulkCommand.currentBulkReactions.set( user, BulkCommand.currentBulkReactions.get( user ).filter( message => origin != message ) );
+							}
+						} );
+					}
+				} );
 				await origin.reactions.removeAll();
 			} catch ( error ) {
 				ResolveRequestMessageTask.logger.error( error );
diff --git a/src/util/EmojiUtil.ts b/src/util/EmojiUtil.ts
new file mode 100644
index 00000000..a3bad84c
--- /dev/null
+++ b/src/util/EmojiUtil.ts
@@ -0,0 +1,17 @@
+import emojiRegex = require( 'emoji-regex/text.js' );
+
+export class EmojiUtil {
+	public static getEmoji( args: string ): string {
+		const customEmoji = /^<a?:(.+):(\d+)>/;
+		const unicodeEmoji = emojiRegex();
+		let rawEmoji: string;
+		if ( customEmoji.test( args ) ) {
+			rawEmoji = customEmoji.exec( args )[2];
+		} else if ( unicodeEmoji.test( args ) ) {
+			rawEmoji = args;
+		} else {
+			return undefined;
+		}
+		return rawEmoji;
+	}
+}
\ No newline at end of file
diff --git a/src/util/RequestsUtil.ts b/src/util/RequestsUtil.ts
index fbf94808..d3497302 100644
--- a/src/util/RequestsUtil.ts
+++ b/src/util/RequestsUtil.ts
@@ -1,8 +1,8 @@
 import { EmbedField, Message, TextChannel, User } from 'discord.js';
 import * as log4js from 'log4js';
 import BotConfig from '../BotConfig';
-import DiscordUtil from './DiscordUtil';
 import MentionCommand from '../commands/MentionCommand';
+import DiscordUtil from './DiscordUtil';
 import MojiraBot from '../MojiraBot';
 
 interface OriginIds {
@@ -74,6 +74,16 @@ export class RequestsUtil {
 			.replace( '{{message}}', message.content.replace( /(^|\n)/g, '$1> ' ) );
 	}
 
+	public static getTickets( content: string ): string[] {
+		let ticketMatch: RegExpExecArray;
+		const regex = MentionCommand.getTicketIdRegex();
+		const ticketMatches: string[] = [];
+		while ( ( ticketMatch = regex.exec( content ) ) !== null ) {
+			ticketMatches.push( ticketMatch[1] );
+		}
+		return ticketMatches;
+	}
+
 	// https://stackoverflow.com/a/3426956
 	private static hashCode( str: string ): number {
 		let hash = 0;