Commit 438494d0 authored by alexpott's avatar alexpott

Issue #2187495 by Les Lim, longwave: Use plugin system for MailInterface classes.

parent 17645df7
......@@ -587,9 +587,9 @@ services:
flood:
class: Drupal\Core\Flood\DatabaseBackend
arguments: ['@database', '@request']
mail.factory:
class: Drupal\Core\Mail\MailFactory
arguments: ['@config.factory']
plugin.manager.mail:
class: Drupal\Core\Mail\MailManager
arguments: ['@container.namespaces', '@cache.cache', '@language_manager', '@module_handler', '@config.factory']
plugin.manager.condition:
class: Drupal\Core\Condition\ConditionManager
parent: default_plugin_manager
......
......@@ -84,7 +84,7 @@
* called to complete the $message structure which will already contain common
* defaults.
* @param string $key
* A key to identify the e-mail sent. The final e-mail id for e-mail altering
* A key to identify the e-mail sent. The final message ID for e-mail altering
* will be {$module}_{$key}.
* @param string $to
* The e-mail address or addresses where the message will be sent to. The
......@@ -193,23 +193,23 @@ function drupal_mail($module, $key, $to, $langcode, $params = array(), $reply =
}
/**
* Returns an object that implements Drupal\Core\Mail\MailInterface.
* Returns an instance of the mail plugin to use for a given message ID.
*
* @param string $module
* The module name which was used by drupal_mail() to invoke hook_mail().
* @param string $key
* A key to identify the e-mail sent. The final e-mail ID for the e-mail
* A key to identify the e-mail sent. The final message ID for the e-mail
* alter hook in drupal_mail() would have been {$module}_{$key}.
*
* @return \Drupal\Core\Mail\MailInterface
* An object that implements Drupal\Core\Mail\MailInterface.
* A mail plugin instance.
*
* @throws \Exception
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*
* @see \Drupal\Core\Mail\MailFactory::get()
* @see \Drupal\Core\Mail\MailManager::getInstance()
*/
function drupal_mail_system($module, $key) {
return \Drupal::service('mail.factory')->get($module, $key);
return \Drupal::service('plugin.manager.mail')->getInstance(array('module' => $module, 'key' => $key));
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Annotation\Mail.
*/
namespace Drupal\Core\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Mail annotation object.
*
* @Annotation
*/
class Mail extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the mail plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A short description of the mail plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
}
<?php
/**
* @file
* Contains \Drupal\Core\Mail\MailFactory.
*/
namespace Drupal\Core\Mail;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Utility\String;
/**
* Factory for creating mail system objects.
*/
class MailFactory {
/**
* Config object for mail system configurations.
*
* @var \Drupal\Core\Config\Config
*/
protected $mailConfig;
/**
* List of already instantiated mail system objects.
*
* @var array
*/
protected $instances = array();
/**
* Constructs a MailFActory object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration factory.
*/
public function __construct(ConfigFactoryInterface $configFactory) {
$this->mailConfig = $configFactory->get('system.mail');
}
/**
* Returns an object that implements \Drupal\Core\Mail\MailInterface.
*
* Allows for one or more custom mail backends to format and send mail messages
* composed using drupal_mail().
*
* The selection of a particular implementation is controlled via the config
* 'system.mail.interface', which is a keyed array. The default
* implementation is the class whose name is the value of 'default' key. A
* more specific match first to key and then to module will be used in
* preference to the default. To specify a different class for all mail sent
* by one module, set the class name as the value for the key corresponding to
* the module name. To specify a class for a particular message sent by one
* module, set the class name as the value for the array key that is the
* message id, which is "${module}_${key}".
*
* For example to debug all mail sent by the user module by logging it to a
* file, you might set the variable as something like:
*
* @code
* array(
* 'default' => 'Drupal\Core\Mail\PhpMail',
* 'user' => 'Drupal\devel\DevelMailLog',
* );
* @endcode
*
* Finally, a different system can be specified for a specific e-mail ID (see
* the $key param), such as one of the keys used by the contact module:
*
* @code
* array(
* 'default' => 'Drupal\Core\Mail\PhpMail',
* 'user' => 'Drupal\devel\DevelMailLog',
* 'contact_page_autoreply' => 'Drupal\example\NullMail',
* );
* @endcode
*
* Other possible uses for system include a mail-sending class that actually
* sends (or duplicates) each message to SMS, Twitter, instant message, etc,
* or a class that queues up a large number of messages for more efficient
* bulk sending or for sending via a remote gateway so as to reduce the load
* on the local server.
*
* @param string $module
* The module name which was used by drupal_mail() to invoke hook_mail().
* @param string $key
* A key to identify the e-mail sent. The final e-mail ID for the e-mail
* alter hook in drupal_mail() would have been {$module}_{$key}.
*
* @return \Drupal\Core\Mail\MailInterface
* An object that implements Drupal\Core\Mail\MailInterface.
*
* @throws \Exception
*/
public function get($module, $key) {
$id = $module . '_' . $key;
$configuration = $this->mailConfig->get('interface');
// Look for overrides for the default class, starting from the most specific
// id, and falling back to the module name.
if (isset($configuration[$id])) {
$class = $configuration[$id];
}
elseif (isset($configuration[$module])) {
$class = $configuration[$module];
}
else {
$class = $configuration['default'];
}
if (empty($this->instances[$class])) {
$interfaces = class_implements($class);
if (isset($interfaces['Drupal\Core\Mail\MailInterface'])) {
$this->instances[$class] = new $class();
}
else {
throw new \Exception(String::format('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'Drupal\Core\Mail\MailInterface')));
}
}
return $this->instances[$class];
}
}
......@@ -17,10 +17,10 @@
*
* Allows to preprocess, format, and postprocess a mail message before it is
* passed to the sending system. By default, all messages may contain HTML and
* are converted to plain-text by the Drupal\Core\Mail\PhpMail implementation.
* For example, an alternative implementation could override the default
* implementation and additionally sanitize the HTML for usage in a
* MIME-encoded e-mail, but still invoking the Drupal\Core\Mail\PhpMail
* are converted to plain-text by the Drupal\Core\Mail\Plugin\Mail\PhpMail
* implementation. For example, an alternative implementation could override
* the default implementation and also sanitize the HTML for usage in a MIME-
* encoded email, but still invoking the Drupal\Core\Mail\Plugin\Mail\PhpMail
* implementation to generate an alternate plain-text version for sending.
*
* @param array $message
......
<?php
/**
* @file
* Contains \Drupal\Core\Mail\MailManager.
*/
namespace Drupal\Core\Mail;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Utility\String;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
/**
* Mail plugin manager.
*/
class MailManager extends DefaultPluginManager {
/**
* Config object for mail system configurations.
*
* @var \Drupal\Core\Config\Config
*/
protected $mailConfig;
/**
* List of already instantiated mail plugins.
*
* @var array
*/
protected $instances = array();
/**
* Constructs the MailManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) {
parent::__construct('Plugin/Mail', $namespaces, 'Drupal\Core\Annotation\Mail');
$this->alterInfo($module_handler, 'mail_backend_info');
$this->setCacheBackend($cache_backend, $language_manager, 'mail_backend_plugins');
$this->mailConfig = $config_factory->get('system.mail');
}
/**
* Overrides PluginManagerBase::getInstance().
*
* Returns an instance of the mail plugin to use for a given message ID.
*
* The selection of a particular implementation is controlled via the config
* 'system.mail.interface', which is a keyed array. The default
* implementation is the mail plugin whose ID is the value of 'default' key. A
* more specific match first to key and then to module will be used in
* preference to the default. To specify a different plugin for all mail sent
* by one module, set the plugin ID as the value for the key corresponding to
* the module name. To specify a plugin for a particular message sent by one
* module, set the plugin ID as the value for the array key that is the
* message ID, which is "${module}_${key}".
*
* For example to debug all mail sent by the user module by logging it to a
* file, you might set the variable as something like:
*
* @code
* array(
* 'default' => 'php_mail',
* 'user' => 'devel_mail_log',
* );
* @endcode
*
* Finally, a different system can be specified for a specific message ID (see
* the $key param), such as one of the keys used by the contact module:
*
* @code
* array(
* 'default' => 'php_mail',
* 'user' => 'devel_mail_log',
* 'contact_page_autoreply' => 'null_mail',
* );
* @endcode
*
* Other possible uses for system include a mail-sending plugin that actually
* sends (or duplicates) each message to SMS, Twitter, instant message, etc,
* or a plugin that queues up a large number of messages for more efficient
* bulk sending or for sending via a remote gateway so as to reduce the load
* on the local server.
*
* @param array $options
* An array with the following key/value pairs:
* - module: (string) The module name which was used by drupal_mail() to
* invoke hook_mail().
* - key: (string) A key to identify the email sent. The final message ID
* is a string represented as {$module}_{$key}.
*
* @return \Drupal\Core\Mail\MailInterface
* A mail plugin instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function getInstance(array $options) {
$module = $options['module'];
$key = $options['key'];
$message_id = $module . '_' . $key;
$configuration = $this->mailConfig->get('interface');
// Look for overrides for the default mail plugin, starting from the most
// specific message_id, and falling back to the module name.
if (isset($configuration[$message_id])) {
$plugin_id = $configuration[$message_id];
}
elseif (isset($configuration[$module])) {
$plugin_id = $configuration[$module];
}
else {
$plugin_id = $configuration['default'];
}
if (empty($this->instances[$plugin_id])) {
$plugin = $this->createInstance($plugin_id);
if (is_subclass_of($plugin, '\Drupal\Core\Mail\MailInterface')) {
$this->instances[$plugin_id] = $plugin;
}
else {
throw new InvalidPluginDefinitionException($plugin_id, String::format('Class %class does not implement interface %interface', array('%class' => get_class($plugin), '%interface' => 'Drupal\Core\Mail\MailInterface')));
}
}
return $this->instances[$plugin_id];
}
}
......@@ -2,13 +2,21 @@
/**
* @file
* Definition of Drupal\Core\Mail\PhpMail.
* Contains Drupal\Core\Mail\Plugin\Mail\PhpMail.
*/
namespace Drupal\Core\Mail;
namespace Drupal\Core\Mail\Plugin\Mail;
use Drupal\Core\Mail\MailInterface;
/**
* The default Drupal mail backend using PHP's mail function.
* Defines the default Drupal mail backend, using PHP's native mail() function.
*
* @Mail(
* id = "php_mail",
* label = @Translation("Default PHP mailer"),
* description = @Translation("Sends the message as plain text, using PHP's native mail() function.")
* )
*/
class PhpMail implements MailInterface {
......
......@@ -2,23 +2,28 @@
/**
* @file
* Contains \Drupal\Core\Mail\TestMailCollector.
* Contains \Drupal\Core\Mail\Plugin\Mail\TestMailCollector.
*/
namespace Drupal\Core\Mail;
namespace Drupal\Core\Mail\Plugin\Mail;
use Drupal\Core\Mail\MailInterface;
/**
* Defines a mail sending implementation that captures sent messages to the
* state system.
* Defines a mail backend that captures sent messages in the state system.
*
* This class is for running tests or for development.
*
* @Mail(
* id = "test_mail_collector",
* label = @Translation("Mail collector"),
* description = @Translation("Does not send the message, but stores it in Drupal within the state system. Used for testing.")
* )
*/
class TestMailCollector extends PhpMail implements MailInterface {
/**
* Overrides \Drupal\Core\Mail\PhpMail::mail().
*
* Accepts an e-mail message and stores it with the state system.
* {@inheritdoc}
*/
public function mail(array $message) {
$captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array();
......
......@@ -131,7 +131,7 @@ protected function setUp() {
// Manually configure the test mail collector implementation to prevent
// tests from sending out e-mails and collect them in state instead.
\Drupal::config('system.mail')
->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
->set('interface.default', 'test_mail_collector')
->save();
// When running from run-tests.sh we don't get an empty current path which
......
......@@ -840,7 +840,7 @@ protected function setUp() {
// While this should be enforced via settings.php prior to installation,
// some tests expect to be able to test mail system implementations.
\Drupal::config('system.mail')
->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
->set('interface.default', 'test_mail_collector')
->save();
// Restore the original Simpletest batch.
......
interface:
default: 'Drupal\Core\Mail\PhpMail'
default: 'php_mail'
......@@ -8,20 +8,19 @@
namespace Drupal\system\Tests\Mail;
use Drupal\Core\Language\Language;
use Drupal\Core\Mail\MailInterface;
use Drupal\simpletest\WebTestBase;
/**
* Defines a mail class used for testing.
* Tests related to the mail system.
*/
class MailTest extends WebTestBase implements MailInterface {
class MailTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('simpletest');
public static $modules = array('simpletest', 'system_mail_failure_test');
/**
* The most recent message that was sent through the test case.
......@@ -39,24 +38,19 @@ public static function getInfo() {
);
}
function setUp() {
parent::setUp();
// Set MailTestCase (i.e. this class) as the SMTP library
\Drupal::config('system.mail')->set('interface.default', 'Drupal\system\Tests\Mail\MailTest')->save();
}
/**
* Assert that the pluggable mail system is functional.
*/
public function testPluggableFramework() {
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Switch mail backends.
\Drupal::config('system.mail')->set('interface.default', 'test_php_mail_failure')->save();
// Use MailTestCase for sending a message.
drupal_mail('simpletest', 'mail_test', 'testing@example.com', $language_interface->id);
// Get the default MailInterface class instance.
$mail_backend = drupal_mail_system('default', 'default');
// Assert whether the message was sent through the send function.
$this->assertEqual(self::$sent_message['to'], 'testing@example.com', 'Pluggable mail system is extendable.');
// Assert whether the default mail backend is an instance of the expected
// class.
$this->assertTrue($mail_backend instanceof \Drupal\system_mail_failure_test\Plugin\Mail\TestPhpMailFailure, 'Pluggable mail system is extendable.');
}
/**
......@@ -67,14 +61,19 @@ public function testPluggableFramework() {
public function testCancelMessage() {
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Reset the class variable holding a copy of the last sent message.
self::$sent_message = NULL;
// Use the state system collector mail backend.
\Drupal::config('system.mail')->set('interface.default', 'test_mail_collector')->save();
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', array());
// Send a test message that simpletest_mail_alter should cancel.
drupal_mail('simpletest', 'cancel_test', 'cancel@example.com', $language_interface->id);
// Retrieve sent message.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
// Assert that the message was not actually sent.
$this->assertNull(self::$sent_message, 'Message was canceled.');
$this->assertFalse($sent_message, 'Message was canceled.');
}
/**
......@@ -83,45 +82,28 @@ public function testCancelMessage() {
public function testFromAndReplyToHeader() {
$language = \Drupal::languageManager()->getCurrentLanguage();
// Reset the class variable holding a copy of the last sent message.
self::$sent_message = NULL;
// Use the state system collector mail backend.
\Drupal::config('system.mail')->set('interface.default', 'test_mail_collector')->save();
// Reset the state variable that holds sent messages.
\Drupal::state()->set('system.test_mail_collector', array());
// Send an e-mail with a reply-to address specified.
$from_email = 'Drupal <simpletest@example.com>';
$reply_email = 'someone_else@example.com';
drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language, array(), $reply_email);
// Test that the reply-to e-mail is just the e-mail and not the site name and
// default sender e-mail.
$this->assertEqual($from_email, self::$sent_message['headers']['From'], 'Message is sent from the site email account.');
$this->assertEqual($reply_email, self::$sent_message['headers']['Reply-to'], 'Message reply-to headers are set.');
$this->assertFalse(isset(self::$sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
// Test that the reply-to e-mail is just the e-mail and not the site name
// and default sender e-mail.
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
$this->assertEqual($from_email, $sent_message['headers']['From'], 'Message is sent from the site email account.');
$this->assertEqual($reply_email, $sent_message['headers']['Reply-to'], 'Message reply-to headers are set.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
self::$sent_message = NULL;
// Send an e-mail and check that the From-header contains the site name.
drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language);
$this->assertEqual($from_email, self::$sent_message['headers']['From'], 'Message is sent from the site email account.');
$this->assertFalse(isset(self::$sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
$this->assertFalse(isset(self::$sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
}
/**
* Concatenate and wrap the e-mail body for plain-text mails.
*
* @see \Drupal\Core\Mail\PhpMail
*/
public function format(array $message) {
// Join the body array into one string.
$message['body'] = implode("\n\n", $message['body']);
// Convert any HTML to plain-text.
$message['body'] = drupal_html_to_text($message['body']);
// Wrap the mail body for sending.
$message['body'] = drupal_wrap_mail($message['body']);
return $message;
}
/**
* Send function that is called through the mail system.
*/
public function mail(array $message) {
self::$sent_message = $message;
$captured_emails = \Drupal::state()->get('system.test_mail_collector');
$sent_message = end($captured_emails);
$this->assertEqual($from_email, $sent_message['headers']['From'], 'Message is sent from the site email account.');
$this->assertFalse(isset($sent_message['headers']['Reply-to']), 'Message reply-to is not set if not specified.');
$this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.');
}
}
......@@ -2559,6 +2559,19 @@ function hook_archiver_info_alter(&$info) {
$info['tar']['extensions'][] = 'tgz';
}
/**
* Alter the list of mail backend plugin definitions.
*
* @param array $info
* The mail backend plugin definitions to be altered.
*
* @see \Drupal\Core\Annotation\Mail
* @see \Drupal\Core\Mail\MailManager
*/
function hook_mail_backend_info_alter(&$info) {
unset($info['test_mail_collector']);
}
/**
* Alters theme operation links.
*
......
......@@ -2,30 +2,36 @@
/**
* @file
* Contains \Drupal\system_mail_failure_test\TestPhpMailFailure.
* Contains \Drupal\system_mail_failure_test\Plugin\Mail\TestPhpMailFailure.
*/
namespace Drupal\system_mail_failure_test;
namespace Drupal\system_mail_failure_test\Plugin\Mail;
use Drupal\Core\Mail\PhpMail;
use Drupal\Core\Mail\Plugin\Mail\PhpMail;
use Drupal\Core\Mail\MailInterface;
/**
* Defines a mail sending implementation that returns false.
* Defines a mail sending implementation that always fails.
*
* This class is for running tests or for development. To use set the
* configuration:
* @code
* \Drupal::config('system.mail')->set('interface.default', 'Drupal\system_mail_failure_test\TestPhpMailFailure')->save();
* \Drupal::config('system.mail')->set('interface.default', 'test_php_mail_failure')->save();
* @endcode
*
* @Mail(
* id = "test_php_mail_failure",
* label = @Translation("Malfunctioning mail backend"),
* description = @Translation("An intentionally broken mail backend, used for tests.")
* )
*/
class TestPhpMailFailure extends PhpMail implements MailInterface {
/**
* Overrides Drupal\Core\Mail\PhpMail::mail().
* {@inheritdoc}
*/
public function mail(array $message) {
// Instead of attempting to send a message, just return failure.
// Simulate a failed mail send by returning FALSE.
return FALSE;
}
}
......@@ -37,7 +37,7 @@ protected function testUserAdd() {
$this->drupalLogin($user);
// Replace the mail functionality with a fake, malfunctioning service.
\Drupal::config('system.mail')->set('interface.default', 'Drupal\system_mail_failure_test\TestPhpMailFailure')->save();
\Drupal::config('system.mail')->set('interface.default', 'test_php_mail_failure')->save();
// Create a user, but fail to send an email.
$name = $this->randomName();
$edit = array(
......
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Mail\MailManagerTest.
*/
namespace Drupal\Tests\Core\Mail;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Mail\MailManager;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
/**
* Tests the mail plugin manager.
*
* @group Drupal
* @group Mail
*
* @see \Drupal\Core\Mail\MailManager
*/
class MailManagerTest extends UnitTestCase {
/**
* The cache backend to use.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;