The relevant code is wp_update_plugins(). You can see that it stores its state in a site transient called 'update_plugins':
set_site_transient( 'update_plugins', $new_option );
and we can hook that:
- pre_set_site_transient_update_plugins - called at the start of set_site_transient()
- set_site_transient_update_plugins - called at the end, if the value was changed
set_site_transient( 'update_plugins', ... ) is called twice as part of the update: once to update the last_checked timestamp, and once with the new-plugins-to-update list; the first call will always therefore have a changed value without necessarily having the list of updated plugins changed
- it can also be called when deleting plugins, to remove the update flag for any deleted plugin, but only once in this case.
I appreciate that hooking the site transient updates seems a bit of a hack, but there's no other obvious place to hook that the update check is complete. I have seen pay-for plugins hook these too as a way of generating update-needed for plugins hosted elsewhere.
Alternatively of course you don't need to hook the actual update, but you can instead schedule a cron job that checks
get_site_transient( 'update_plugins' ) (and
update_core) to generate the notification email.