Drupal 8: Get advanced email report about available updating

3 Mar.2018
Update Manager Advanced

The solution can be helpful, for example, in case, if you received the mail and see that only one module was updated and this module uses on admin interface only and nothing critical for the visitors and updating can wait. So you shouldn't spend time in opening the site for checking.

File
modules/custom/advupdate/advupdate.module
<?php
 
/**
 * @file
 * Contains advupdate.module.
 */
 
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\Core\Form\FormStateInterface;
use Drupal\update\UpdateManagerInterface;
use Drupal\update\UpdateFetcherInterface;
 
/**
 * Implements hook_form_FORM_ID_alter().
 */
function advupdate_form_update_settings_alter(&$form, FormStateInterface $form_state, $form_id) {
  $advupdate_config = \Drupal::config('advupdate.settings');
 
  $form['update_notification_extend_email_report'] = [
    '#type' => 'checkbox',
    '#title' => t('Expand the report using "Update Manager Advanced" module'),
    '#default_value' => $advupdate_config->get('notification.extend_email_report'),
    '#description' => t('An email will include more detailed information with a list of modules.'),
  ];
 
  $form['#submit'][] = 'advupdate_form_update_settings_submit_handler';
}
 
/**
 * Save "update_notification_extend_email_report" to configuration.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 */
function advupdate_form_update_settings_submit_handler($form, FormStateInterface $form_state) {
  \Drupal::configFactory()->getEditable('advupdate.settings')
    ->set('notification.extend_email_report', $form_state->getValue('update_notification_extend_email_report'))
    ->save();
}
 
/**
 * Implements hook_mail_alter().
 */
function advupdate_mail_alter(&$message) {
  if ($message['id'] == 'update_status_notify'
    && \Drupal::config('advupdate.settings')
      ->get('notification.extend_email_report') == 1) {
    $message['body'][] = "\n" . _advupdate_mail_additional_data();
  }
}
 
/**
 * Render a list for available updating of core/modules/themes.
 */
function _advupdate_mail_additional_data() {
  module_load_include('module', 'update');
  module_load_include('inc', 'update', 'update.manager');
  module_load_include('inc', 'update', 'update.compare');
 
  $available = update_get_available(TRUE);
  if (empty($available)) {
    return '';
  }
 
  // This will be a nested array. The first key is the kind of project, which
  // can be either 'enabled', 'disabled', 'manual' (projects which require
  // manual updates, such as core). Then, each sub-array is an array of
  // projects of that type, indexed by project short name, and containing an
  // array of data for items.
  $projects = [];
 
  // This stores the actual download link we're going to update from for each
  // project in the build, regardless of if it's enabled or disabled.
  $build['project_downloads'] = ['#tree' => TRUE];
 
  $project_data = update_calculate_project_data($available);
  foreach ($project_data as $name => $project) {
    // Filter out projects which are up to date already.
    if ($project['status'] == UpdateManagerInterface::CURRENT) {
      continue;
    }
    // The project name to display can vary based on the info we have.
    if (!empty($project['title'])) {
      if (!empty($project['link'])) {
        $project_name = Link::fromTextAndUrl($project['title'], Url::fromUri($project['link'], ['absolute' => TRUE]))
          ->toString();
      }
      else {
        $project_name = $project['title'];
      }
    }
    elseif (!empty($project['info']['name'])) {
      $project_name = $project['info']['name'];
    }
    else {
      $project_name = $name;
    }
    if ($project['project_type'] == 'theme' || $project['project_type'] == 'theme-disabled') {
      $project_name .= ' ' . t('(Theme)');
    }
 
    if (empty($project['recommended'])) {
      // If we don't know what to recommend they upgrade to, we should skip
      // the project entirely.
      continue;
    }
 
    $recommended_release = $project['releases'][$project['recommended']];
 
    switch ($project['status']) {
      case UpdateManagerInterface::NOT_SECURE:
      case UpdateManagerInterface::REVOKED:
        $project_name .= ' ' . t('(Security update)');
        break;
 
      case UpdateManagerInterface::NOT_SUPPORTED:
        $project_name .= ' ' . t('(Unsupported)');
        break;
 
      case UpdateFetcherInterface::UNKNOWN:
      case UpdateFetcherInterface::NOT_FETCHED:
      case UpdateFetcherInterface::NOT_CHECKED:
      case UpdateManagerInterface::NOT_CURRENT:
        break;
 
      default:
        // Jump out of the switch and onto the next project in foreach.
        continue 2;
    }
 
    // Create an entry for this project and use the project title
    // and an additional information.
    // Feel free and make any markup as you prefer.
    $entry['title'] = [
      'data' => [
        '#markup' => $project_name . ': '
          . t('Installed') . ' [' . $project['existing_version'] . '] -> '
          . t('Recommended') . ' [' . $recommended_release['version'] . '] | '
          . t('Release notes') . ' [' . $recommended_release['release_link'] . ']',
      ],
    ];
 
    // Based on what kind of project this is, save the entry into the
    // appropriate sub-array.
    switch ($project['project_type']) {
      case 'core':
        // Core needs manual updates at this time.
        $projects['manual'][$name] = $entry;
        break;
 
      case 'module':
      case 'theme':
        $projects['enabled'][$name] = $entry;
        break;
 
      case 'module-disabled':
      case 'theme-disabled':
        $projects['disabled'][$name] = $entry;
        break;
    }
  }
 
  if (!empty($projects['enabled'])) {
    $build['projects'] = [
      '#theme' => 'item_list',
      '#title' => t('Enabled'),
      '#items' => $projects['enabled'],
    ];
  }
 
  if (!empty($projects['disabled'])) {
    $build['disabled_projects'] = [
      '#theme' => 'item_list',
      '#title' => t('Disabled'),
      '#items' => $projects['disabled'],
    ];
  }
 
  if (!empty($projects['manual'])) {
    $build['manual_updates'] = [
      '#theme' => 'item_list',
      '#title' => t('Manual updates required'),
      '#items' => $projects['manual'],
    ];
  }
 
  return \Drupal::service('renderer')->render($build);
}
File
modules/custom/advupdate/config/install/advupdate.settings.yml
notification:
  extend_email_report: 1

Conclusion

As you see, only one thing we should do. By implementing hook_mail_alter() alter "update_status_notify" and add any content what we prefer.

In the article, I added the shortcode to show an example. Also this solution available with more additional functionality as a contrib Drupal module: Update Manager Advanced. For now, it DEV version, however, it works well.

Have a fun :-)