From 45cfddf83f5f1a58055a5b024d9cad8424d26040 Mon Sep 17 00:00:00 2001
From: lucashedding <lucashedding@1463982.no-reply.drupal.org>
Date: Mon, 9 Dec 2019 07:52:31 -0600
Subject: [PATCH] Issue #3093955 by heddn, ressa: Send email alert to
 administrator after automatic update

---
 automatic_updates.module                      |  13 +-
 automatic_updates.services.yml                |   9 ++
 src/Controller/InPlaceUpdateController.php    |   4 +-
 src/Event/PostUpdateEvent.php                 |  62 +++++++
 src/Event/UpdateEvents.php                    |  19 +++
 src/EventSubscriber/PostUpdateSubscriber.php  | 123 ++++++++++++++
 src/Services/InPlaceUpdate.php                |  64 ++++----
 src/Services/Notify.php                       |   4 +-
 src/Services/UpdateInterface.php              |  14 +-
 src/UpdateMetadata.php                        | 153 ++++++++++++++++++
 .../automatic-updates-post-update.html.twig   |  33 ++++
 .../Controller/InPlaceUpdateController.php    |   4 +-
 tests/src/Functional/NotifyTest.php           |  29 +++-
 13 files changed, 482 insertions(+), 49 deletions(-)
 create mode 100644 src/Event/PostUpdateEvent.php
 create mode 100644 src/Event/UpdateEvents.php
 create mode 100644 src/EventSubscriber/PostUpdateSubscriber.php
 create mode 100644 src/UpdateMetadata.php
 create mode 100644 templates/automatic-updates-post-update.html.twig

diff --git a/automatic_updates.module b/automatic_updates.module
index 8235dfabce..1d96f034ff 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\automatic_updates\UpdateMetadata;
 use Drupal\Core\Url;
 use Drupal\update\UpdateManagerInterface;
 use Symfony\Component\Process\PhpExecutableFinder;
@@ -122,15 +123,17 @@ function automatic_updates_cron() {
     if ($not_recommended_version && $projects['drupal']['existing_version'] !== $recommended_release['version']) {
       if ($config->get('enable_cron_security_updates')) {
         if ($security_update) {
+          $metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
           /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
           $updater = \Drupal::service('automatic_updates.update');
-          $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
+          $updater->update($metadata);
         }
       }
       else {
+        $metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
         /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
         $updater = \Drupal::service('automatic_updates.update');
-        $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
+        $updater->update($metadata);
       }
     }
   }
@@ -148,6 +151,12 @@ function automatic_updates_theme(array $existing, $type, $theme, $path) {
         'messages' => [],
       ],
     ],
+    'automatic_updates_post_update' => [
+      'variables' => [
+        'success' => NULL,
+        'metadata' => NULL,
+      ],
+    ],
   ];
 }
 
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 53dab8ea0a..23621cf5cb 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -46,6 +46,15 @@ services:
   plugin.manager.database_update_handler:
     class: Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager
     parent: default_plugin_manager
+  automatic_updates.post_update_subscriber:
+    class: Drupal\automatic_updates\EventSubscriber\PostUpdateSubscriber
+    arguments:
+      - '@config.factory'
+      - '@plugin.manager.mail'
+      - '@language_manager'
+      - '@entity_type.manager'
+    tags:
+      - { name: event_subscriber }
 
   automatic_updates.readiness_checker:
     class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager
diff --git a/src/Controller/InPlaceUpdateController.php b/src/Controller/InPlaceUpdateController.php
index 151e525cd4..e4332576de 100644
--- a/src/Controller/InPlaceUpdateController.php
+++ b/src/Controller/InPlaceUpdateController.php
@@ -3,6 +3,7 @@
 namespace Drupal\automatic_updates\Controller;
 
 use Drupal\automatic_updates\Services\UpdateInterface;
+use Drupal\automatic_updates\UpdateMetadata;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Messenger\MessengerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -42,7 +43,8 @@ class InPlaceUpdateController extends ControllerBase {
    * Builds the response.
    */
   public function update($project, $type, $from, $to) {
-    $updated = $this->updater->update($project, $type, $from, $to);
+    $metadata = new UpdateMetadata($project, $type, $from, $to);
+    $updated = $this->updater->update($metadata);
     $message_type = MessengerInterface::TYPE_STATUS;
     $message = $this->t('Update successful');
     if (!$updated) {
diff --git a/src/Event/PostUpdateEvent.php b/src/Event/PostUpdateEvent.php
new file mode 100644
index 0000000000..789b01c449
--- /dev/null
+++ b/src/Event/PostUpdateEvent.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\automatic_updates\Event;
+
+use Drupal\automatic_updates\UpdateMetadata;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Defines the post update event.
+ *
+ * @see \Drupal\automatic_updates\Event\UpdateEvents
+ */
+class PostUpdateEvent extends Event {
+
+  /**
+   * The update metadata.
+   *
+   * @var \Drupal\automatic_updates\UpdateMetadata
+   */
+  protected $updateMetadata;
+
+  /**
+   * The update success status.
+   *
+   * @var bool
+   */
+  protected $success;
+
+  /**
+   * Constructs a new PostUpdateEvent.
+   *
+   * @param \Drupal\automatic_updates\UpdateMetadata $metadata
+   *   The update metadata.
+   * @param bool $success
+   *   TRUE if update succeeded, FALSE otherwise.
+   */
+  public function __construct(UpdateMetadata $metadata, $success) {
+    $this->updateMetadata = $metadata;
+    $this->success = $success;
+  }
+
+  /**
+   * Get the update metadata.
+   *
+   * @return \Drupal\automatic_updates\UpdateMetadata
+   *   The update metadata.
+   */
+  public function getUpdateMetadata() {
+    return $this->updateMetadata;
+  }
+
+  /**
+   * Gets the update success status.
+   *
+   * @return bool
+   *   TRUE if update succeeded, FALSE otherwise.
+   */
+  public function success() {
+    return $this->success;
+  }
+
+}
diff --git a/src/Event/UpdateEvents.php b/src/Event/UpdateEvents.php
new file mode 100644
index 0000000000..842b6d6ddf
--- /dev/null
+++ b/src/Event/UpdateEvents.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\automatic_updates\Event;
+
+/**
+ * Defines events for the automatic_updates module.
+ */
+final class UpdateEvents {
+
+  /**
+   * Name of the event fired after updating a site.
+   *
+   * @Event
+   *
+   * @see \Drupal\automatic_updates\Event\PostUpdateEvent
+   */
+  const POST_UPDATE = 'automatic_updates.post_update';
+
+}
diff --git a/src/EventSubscriber/PostUpdateSubscriber.php b/src/EventSubscriber/PostUpdateSubscriber.php
new file mode 100644
index 0000000000..f701f6ef5d
--- /dev/null
+++ b/src/EventSubscriber/PostUpdateSubscriber.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\automatic_updates\EventSubscriber;
+
+use Drupal\automatic_updates\Event\PostUpdateEvent;
+use Drupal\automatic_updates\Event\UpdateEvents;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Post update event subscriber.
+ */
+class PostUpdateSubscriber implements EventSubscriberInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * PostUpdateSubscriber constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
+   *   The mail manager.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   Entity type manager.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager) {
+    $this->configFactory = $config_factory;
+    $this->mailManager = $mail_manager;
+    $this->languageManager = $language_manager;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      UpdateEvents::POST_UPDATE => ['onPostUpdate'],
+    ];
+  }
+
+  /**
+   * Send notification on post update with success/failure.
+   *
+   * @param \Drupal\automatic_updates\Event\PostUpdateEvent $event
+   *   The post update event.
+   */
+  public function onPostUpdate(PostUpdateEvent $event) {
+    $notify_list = $this->configFactory->get('update.settings')->get('notification.emails');
+    if (!empty($notify_list)) {
+      $params['subject'] = $this->t('Automatic update of "@project" succeeded', ['@project' => $event->getUpdateMetadata()->getProjectName()]);
+      if (!$event->success()) {
+        $params['subject'] = $this->t('Automatic update of "@project" failed', ['@project' => $event->getUpdateMetadata()->getProjectName()]);
+      }
+      $params['body'] = [
+        '#theme' => 'automatic_updates_post_update',
+        '#success' => $event->success(),
+        '#metadata' => $event->getUpdateMetadata(),
+      ];
+      $default_langcode = $this->languageManager->getDefaultLanguage()->getId();
+      $params['langcode'] = $default_langcode;
+      foreach ($notify_list as $to) {
+        $this->doSend($to, $params);
+      }
+    }
+  }
+
+  /**
+   * Composes and send the email message.
+   *
+   * @param string $to
+   *   The email address where the message will be sent.
+   * @param array $params
+   *   Parameters to build the email.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  protected function doSend($to, array $params) {
+    $users = $this->entityTypeManager->getStorage('user')
+      ->loadByProperties(['mail' => $to]);
+    foreach ($users as $user) {
+      $to_user = reset($users);
+      $params['langcode'] = $to_user->getPreferredLangcode();
+      $this->mailManager->mail('automatic_updates', 'post_update', $to, $params['langcode'], $params);
+    }
+  }
+
+}
diff --git a/src/Services/InPlaceUpdate.php b/src/Services/InPlaceUpdate.php
index c226d6fd60..21ab7a14a6 100644
--- a/src/Services/InPlaceUpdate.php
+++ b/src/Services/InPlaceUpdate.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\automatic_updates\Services;
 
+use Drupal\automatic_updates\Event\PostUpdateEvent;
+use Drupal\automatic_updates\Event\UpdateEvents;
 use Drupal\automatic_updates\ProjectInfoTrait;
 use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\automatic_updates\UpdateMetadata;
 use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Core\Archiver\ArchiverInterface;
 use Drupal\Core\Archiver\ArchiverManager;
@@ -128,7 +131,7 @@ class InPlaceUpdate implements UpdateInterface {
   /**
    * {@inheritdoc}
    */
-  public function update($project_name, $project_type, $from_version, $to_version) {
+  public function update(UpdateMetadata $metadata) {
     // Bail immediately on updates if error category checks fail.
     /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $readiness_check_manager */
     $checker = \Drupal::service('automatic_updates.readiness_checker');
@@ -136,14 +139,14 @@ class InPlaceUpdate implements UpdateInterface {
       return FALSE;
     }
     $success = FALSE;
-    if ($project_name === 'drupal') {
+    if ($metadata->getProjectName() === 'drupal') {
       $project_root = $this->rootPath;
     }
     else {
-      $project_root = drupal_get_path($project_type, $project_name);
+      $project_root = drupal_get_path($metadata->getProjectType(), $metadata->getProjectName());
     }
-    if ($archive = $this->getArchive($project_name, $from_version, $to_version)) {
-      $modified = $this->checkModifiedFiles($project_name, $project_type, $archive);
+    if ($archive = $this->getArchive($metadata)) {
+      $modified = $this->checkModifiedFiles($metadata, $archive);
       if (!$modified && $this->backup($archive, $project_root)) {
         $this->logger->info('In place update has started.');
         $success = $this->processUpdate($archive, $project_root);
@@ -168,30 +171,33 @@ class InPlaceUpdate implements UpdateInterface {
         }
       }
     }
+    /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */
+    $event_dispatcher = \Drupal::service('event_dispatcher');
+    $event = new PostUpdateEvent($metadata, $success);
+    $event_dispatcher->dispatch(UpdateEvents::POST_UPDATE, $event);
+
     return $success;
   }
 
   /**
    * Get an archive with the quasi-patch contents.
    *
-   * @param string $project_name
-   *   The project name.
-   * @param string $from_version
-   *   The current project version.
-   * @param string $to_version
-   *   The desired next project version.
+   * @param \Drupal\automatic_updates\UpdateMetadata $metadata
+   *   The update metadata.
    *
    * @return \Drupal\Core\Archiver\ArchiverInterface|null
    *   The archive or NULL if download fails.
+   *
+   * @throws \SodiumException
    */
-  protected function getArchive($project_name, $from_version, $to_version) {
-    $quasi_patch = $this->getQuasiPatchFileName($project_name, $from_version, $to_version);
-    $url = $this->buildUrl($project_name, $quasi_patch);
+  protected function getArchive(UpdateMetadata $metadata) {
+    $quasi_patch = $this->getQuasiPatchFileName($metadata);
+    $url = $this->buildUrl($metadata->getProjectName(), $quasi_patch);
     $temp_directory = FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR;
     $destination = $this->fileSystem->getDestinationFilename($temp_directory . $quasi_patch, FileSystemInterface::EXISTS_REPLACE);
     $this->doGetResource($url, $destination);
     $csig_file = $quasi_patch . '.csig';
-    $csig_url = $this->buildUrl($project_name, $csig_file);
+    $csig_url = $this->buildUrl($metadata->getProjectName(), $csig_file);
     $csig_destination = $this->fileSystem->getDestinationFilename(FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . $csig_file, FileSystemInterface::EXISTS_REPLACE);
     $this->doGetResource($csig_url, $csig_destination);
     $csig = file_get_contents($csig_destination);
@@ -202,25 +208,23 @@ class InPlaceUpdate implements UpdateInterface {
   /**
    * Check if files are modified before applying updates.
    *
-   * @param string $project_name
-   *   The project name.
-   * @param string $project_type
-   *   The project type.
+   * @param \Drupal\automatic_updates\UpdateMetadata $metadata
+   *   The update metadata.
    * @param \Drupal\Core\Archiver\ArchiverInterface $archive
    *   The archive.
    *
    * @return bool
    *   Return TRUE if modified files exist, FALSE otherwise.
    */
-  protected function checkModifiedFiles($project_name, $project_type, ArchiverInterface $archive) {
-    if ($project_type === 'core') {
-      $project_type = 'module';
+  protected function checkModifiedFiles(UpdateMetadata $metadata, ArchiverInterface $archive) {
+    if ($metadata->getProjectType() === 'core') {
+      $metadata->setProjectType('module');
     }
-    $extensions = $this->getInfos($project_type);
+    $extensions = $this->getInfos($metadata->getProjectType());
     /** @var \Drupal\automatic_updates\Services\ModifiedFilesInterface $modified_files */
     $modified_files = \Drupal::service('automatic_updates.modified_files');
     try {
-      $files = iterator_to_array($modified_files->getModifiedFiles([$extensions[$project_name]]));
+      $files = iterator_to_array($modified_files->getModifiedFiles([$extensions[$metadata->getProjectName()]]));
     }
     catch (RequestException $exception) {
       // While not strictly true that there are modified files, we can't be sure
@@ -525,18 +529,14 @@ class InPlaceUpdate implements UpdateInterface {
   /**
    * Get the quasi-patch file name.
    *
-   * @param string $project_name
-   *   The project name.
-   * @param string $from_version
-   *   The current project version.
-   * @param string $to_version
-   *   The desired next project version.
+   * @param \Drupal\automatic_updates\UpdateMetadata $metadata
+   *   The update metadata.
    *
    * @return string
    *   The quasi-patch file name.
    */
-  protected function getQuasiPatchFileName($project_name, $from_version, $to_version) {
-    return "$project_name-$from_version-to-$to_version.zip";
+  protected function getQuasiPatchFileName(UpdateMetadata $metadata) {
+    return "{$metadata->getProjectName()}-{$metadata->getFromVersion()}-to-{$metadata->getToVersion()}.zip";
   }
 
   /**
diff --git a/src/Services/Notify.php b/src/Services/Notify.php
index f5dcbe553d..87f312fbd7 100644
--- a/src/Services/Notify.php
+++ b/src/Services/Notify.php
@@ -157,11 +157,11 @@ class Notify implements NotifyInterface {
   protected function doSend($to, array $params) {
     $users = $this->entityTypeManager->getStorage('user')
       ->loadByProperties(['mail' => $to]);
-    if ($users) {
+    foreach ($users as $user) {
       $to_user = reset($users);
       $params['langcode'] = $to_user->getPreferredLangcode();
+      $this->mailManager->mail('automatic_updates', 'notify', $to, $params['langcode'], $params);
     }
-    $this->mailManager->mail('automatic_updates', 'notify', $to, $params['langcode'], $params);
   }
 
 }
diff --git a/src/Services/UpdateInterface.php b/src/Services/UpdateInterface.php
index ee685a4578..71d26b9bba 100644
--- a/src/Services/UpdateInterface.php
+++ b/src/Services/UpdateInterface.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\automatic_updates\Services;
 
+use Drupal\automatic_updates\UpdateMetadata;
+
 /**
  * Interface UpdateInterface.
  */
@@ -10,18 +12,12 @@ interface UpdateInterface {
   /**
    * Update a project to the next release.
    *
-   * @param string $project_name
-   *   The project name.
-   * @param string $project_type
-   *   The project type.
-   * @param string $from_version
-   *   The current project version.
-   * @param string $to_version
-   *   The desired next project version.
+   * @param \Drupal\automatic_updates\UpdateMetadata $metadata
+   *   The update metadata.
    *
    * @return bool
    *   TRUE if project was successfully updated, FALSE otherwise.
    */
-  public function update($project_name, $project_type, $from_version, $to_version);
+  public function update(UpdateMetadata $metadata);
 
 }
diff --git a/src/UpdateMetadata.php b/src/UpdateMetadata.php
new file mode 100644
index 0000000000..f54972cfe8
--- /dev/null
+++ b/src/UpdateMetadata.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+/**
+ * Transfer object to encapsulate the details for an update.
+ */
+final class UpdateMetadata {
+
+  /**
+   * The project name.
+   *
+   * @var string
+   */
+  protected $projectName;
+
+  /**
+   * The project type.
+   *
+   * @var string
+   */
+  protected $projectType;
+
+  /**
+   * The current project version.
+   *
+   * @var string
+   */
+  protected $fromVersion;
+
+  /**
+   * The desired next project version.
+   *
+   * @var string
+   */
+  protected $toVersion;
+
+  /**
+   * UpdateMetadata constructor.
+   *
+   * @param string $project_name
+   *   The project name.
+   * @param string $project_type
+   *   The project type.
+   * @param string $from_version
+   *   The current project version.
+   * @param string $to_version
+   *   The desired next project version.
+   */
+  public function __construct($project_name, $project_type, $from_version, $to_version) {
+    $this->projectName = $project_name;
+    $this->projectType = $project_type;
+    $this->fromVersion = $from_version;
+    $this->toVersion = $to_version;
+  }
+
+  /**
+   * Get project name.
+   *
+   * @return string
+   *   The project nam.
+   */
+  public function getProjectName() {
+    return $this->projectName;
+  }
+
+  /**
+   * Set the project name.
+   *
+   * @param string $projectName
+   *   The project name.
+   *
+   * @return \Drupal\automatic_updates\UpdateMetadata
+   *   The update metadata.
+   */
+  public function setProjectName($projectName) {
+    $this->projectName = $projectName;
+    return $this;
+  }
+
+  /**
+   * Get the project type.
+   *
+   * @return string
+   *   The project type.
+   */
+  public function getProjectType() {
+    return $this->projectType;
+  }
+
+  /**
+   * Set the project type.
+   *
+   * @param string $projectType
+   *   The project type.
+   *
+   * @return \Drupal\automatic_updates\UpdateMetadata
+   *   The update metadata.
+   */
+  public function setProjectType($projectType) {
+    $this->projectType = $projectType;
+    return $this;
+  }
+
+  /**
+   * Get the current project version.
+   *
+   * @return string
+   *   The current project version.
+   */
+  public function getFromVersion() {
+    return $this->fromVersion;
+  }
+
+  /**
+   * Set the current project version.
+   *
+   * @param string $fromVersion
+   *   The current project version.
+   *
+   * @return \Drupal\automatic_updates\UpdateMetadata
+   *   The update metadata.
+   */
+  public function setFromVersion($fromVersion) {
+    $this->fromVersion = $fromVersion;
+    return $this;
+  }
+
+  /**
+   * Get the desired next project version.
+   *
+   * @return string
+   *   The desired next project version.
+   */
+  public function getToVersion() {
+    return $this->toVersion;
+  }
+
+  /**
+   * Set the desired next project version.
+   *
+   * @param string $toVersion
+   *   The desired next project version.
+   *
+   * @return \Drupal\automatic_updates\UpdateMetadata
+   *   The update metadata.
+   */
+  public function setToVersion($toVersion) {
+    $this->toVersion = $toVersion;
+    return $this;
+  }
+
+}
diff --git a/templates/automatic-updates-post-update.html.twig b/templates/automatic-updates-post-update.html.twig
new file mode 100644
index 0000000000..c1f1d9171c
--- /dev/null
+++ b/templates/automatic-updates-post-update.html.twig
@@ -0,0 +1,33 @@
+{#
+/**
+ * @file
+ * Template for the post update email notification.
+ *
+ * Available variables:
+ * - success: The update success status
+ * - metadata: The update metadata
+ *
+ * @ingroup themeable
+ */
+#}
+<p>
+  {% if success %}
+  {{ 'The project "@project" was updated from "@from_version" to "@to_version" with success.'|t({
+    '@project': metadata.getProjectName,
+    '@from_version': metadata.getFromVersion,
+    '@to_version': metadata.getToVersion,
+  }) }}
+  {% else %}
+  {{ 'The project "@project" was updated from "@from_version" to "@to_version" with failures.'|t({
+    '@project': metadata.getProjectName,
+    '@from_version': metadata.getFromVersion,
+    '@to_version': metadata.getToVersion,
+  }) }}
+  {% endif %}
+</p>
+<p>
+  {% set status_report = path('system.status') %}
+  {% trans %}
+    See the <a href="{{ status_report }}">site status report page</a> and any logs for more information.
+  {% endtrans %}
+</p>
diff --git a/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php b/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php
index 2d645eccf0..4bf564784a 100644
--- a/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php
+++ b/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php
@@ -3,6 +3,7 @@
 namespace Drupal\test_automatic_updates\Controller;
 
 use Drupal\automatic_updates\Services\UpdateInterface;
+use Drupal\automatic_updates\UpdateMetadata;
 use Drupal\Core\Controller\ControllerBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -41,7 +42,8 @@ class InPlaceUpdateController extends ControllerBase {
    * Builds the response.
    */
   public function update($project, $type, $from, $to) {
-    $updated = $this->updater->update($project, $type, $from, $to);
+    $metadata = new UpdateMetadata($project, $type, $from, $to);
+    $updated = $this->updater->update($metadata);
     return [
       '#markup' => $updated ? $this->t('Update successful') : $this->t('Update Failed'),
     ];
diff --git a/tests/src/Functional/NotifyTest.php b/tests/src/Functional/NotifyTest.php
index a0bdbe0e5c..42492a7dfb 100644
--- a/tests/src/Functional/NotifyTest.php
+++ b/tests/src/Functional/NotifyTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\automatic_updates\Functional;
 
+use Drupal\automatic_updates\Event\PostUpdateEvent;
+use Drupal\automatic_updates\UpdateMetadata;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Test\AssertMailTrait;
 use Drupal\Core\Url;
@@ -60,9 +62,9 @@ class NotifyTest extends BrowserTestBase {
   }
 
   /**
-   * Tests sending email notifications.
+   * Tests sending PSA email notifications.
    */
-  public function testSendMail() {
+  public function testPsaMail() {
     // Test PSAs on admin pages.
     $this->drupalGet(Url::fromRoute('system.admin'));
     $this->assertSession()->pageTextContains('Critical Release - SA-2019-02-19');
@@ -84,4 +86,27 @@ class NotifyTest extends BrowserTestBase {
     $this->assertCount(0, $this->getMails());
   }
 
+  /**
+   * Tests sending post update email notifications.
+   */
+  public function testPostUpdateMail() {
+    // Success email.
+    $metadata = new UpdateMetadata('drupal', 'core', '8.7.0', '8.8.0');
+    $post_update = new PostUpdateEvent($metadata, TRUE);
+    $notify = $this->container->get('automatic_updates.post_update_subscriber');
+    $notify->onPostUpdate($post_update);
+    $this->assertCount(1, $this->getMails());
+    $this->assertMailString('subject', 'Automatic update of "drupal" succeeded', 1);
+    $this->assertMailString('body', 'The project "drupal" was updated from "8.7.0" to "8.8.0" with success.', 1);
+
+    // Failure email.
+    $this->container->get('state')->set('system.test_mail_collector', []);
+    $post_update = new PostUpdateEvent($metadata, FALSE);
+    $notify = $this->container->get('automatic_updates.post_update_subscriber');
+    $notify->onPostUpdate($post_update);
+    $this->assertCount(1, $this->getMails());
+    $this->assertMailString('subject', 'Automatic update of "drupal" failed', 1);
+    $this->assertMailString('body', 'The project "drupal" was updated from "8.7.0" to "8.8.0" with failures.', 1);
+  }
+
 }
-- 
GitLab