In the new stated problem, it's a transaction problem rather than a message bus problem. In any case, you don't want to start sending emails while still accepting (queuing) the user's command. I meant you could do it; just have to make it clear to the user that the process is best effort and can fail half way through (setting expectation).
Anyway, there're ways to address the problem. Message bus can certainly help.
If the message bus supports queuing transaction, great. Just mb.beginTrans(), mb.queue(), mb.queue(), ..., mb.commit(). The consumer won't be called until the whole batch is committed. When crash, the whole batch is aborted. Presumably the user is notified the command failed and retry is needed.
If queuing transaction is not supported, there're several strategies to deal with it.
1. First method.
1a. Pack the 5000 email id as one message and queue it. This is like squeezing everything into one transaction.
1b. The consumer unpacks the message onto the list of id, walks the list with a cursor, and processes each id.
1c. After processing each id (sent email), saves the cursor pointer, in a file or in a db table.
1d. After a crash, the message bus will re-deliver the whole message to the consumer. Unpack the list. Read the saved cursor pointer to see where the last processed id was. Set the cursor to continue beyond that pointer.
1e. In the worst scenario, the crash occurs after the email is sent but before the cursor pointer is saved. At recovery, a duplicate email is sent. Email cannot be rolled back so it really cannot participate in a transaction. That's nothing you can do about it. It's an acceptable business wrinkle.
2. Second method.
2a. Queue the email id one by one, attach the user's session id or some pseudo transaction id along with each one (e.g. today's date as the pseudo id).
2b. On a crash, ask the user to re-submit all the email id again, and queue the email id one by one again, with the same pseudo transaction id. You might have duplicate entries queued up.
2c. Have a table recording the processed id's.
2d. The consumer picks up the email id and the pseudo transaction id. Consult the processed table to see if the (email id, pseudo-id) has been processed. If so, skip. Otherwise, process it and save the id in the processed table.
2e. Again in the worst case, a crash occurs after an email sent but before saving the id to the processed table. In that case, a duplicate email is sent out.
3. Third method.
3a. On some task command that's transactional, unlike email, you can set up a transaction to do the task command and updating the cursor pointer at the same time (with method one). A crash would roll back the command and roll back the cursor pointer update.
3b. Same with method two. Set up a transaction on the task command and the insert of the id into the processed table.
3c. If the transactional task command is performed on a separate system, two-phase commit can be used to bind the command and the cursor pointer update (or processed id insertion) together into a transaction.