Implements keyword sub feature for bulk emails

This commit pulls in changes from #4487 that implement keyword
substitution for bulk emails. With these changes, an instructor can
include keywords in their bulk emails which will be automatically substituted
with the corresponding value for the recipient of the email. Keywords are
of the form %%keyword%%, and the keywords implemented in this commit include:

%%USER_ID$$ => anonymous_user_id
%%USER_FULLNAME%% => user profile name
%%COURSE_DISPLAY_NAME%% => display name of the course
%%COURSE_END_DATE%% => end date of the course

Client-side validations have also been implemented to ensure that only emails
with well-formed keywords can be sent.
The architecture is designed such that adding in new keywords in the future
would be relatively straight-forward.
If you want we can whiteboard the dynamic flow when you come in today.

In short, what you're doing here is establishing a piece of global context that's used platform-wide, but unlike with settings, you're establishing it at runtime, and you're providing convenient methods for changing the state of that context to anyone that wants them in the runtime, too. There's not a race condition in the conventional sense because of Python's GIL, but you can still have weird problems caused by interleaving.

The movitvating example for my concern is a big email blast. How do we be sure that every email in the same blast uses the same substitution dictionary?

One way is to establish the dictionary at the beginning of time, and then deny anyone the ability to mutate it by no-opping the update methods. (Obviously we can't keep them from reaching into the dictionary and modifying it directly, this is Python. But we can try not to encourage that kind of thing.)

Another way to be sure it doesn't change during the run is to make the email batcher smart about the possibility of the global state changing and make it make an explicit copy (rather than reference) to the substitution dictionary, and then pass that into all the email renderings. But given that we want substitutions in other pieces of machinery, which may need their own substitution dictionary caching... I don't know. This approach feels kind of hairy to me.