diff --git a/config/install/inmail.fetcher.imap.yml b/config/install/inmail.fetcher.imap.yml deleted file mode 100644 index a460cff..0000000 --- a/config/install/inmail.fetcher.imap.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: imap -label: 'IMAP Sample' -plugin: imap -configuration: - host: - ssl: FALSE - port: 143 - username: - password: diff --git a/config/schema/inmail.schema.yml b/config/schema/inmail.schema.yml index c6cb02a..fff1678 100644 --- a/config/schema/inmail.schema.yml +++ b/config/schema/inmail.schema.yml @@ -45,8 +45,8 @@ inmail.inmail_handler.*: label: 'Configuration' type: inmail.handler.[%parent.plugin] -inmail.fetcher.*: - label: 'Mail fetcher' +inmail.deliverer.*: + label: 'Mail deliverer' type: config_entity mapping: id: @@ -60,7 +60,7 @@ inmail.fetcher.*: type: string configuration: label: 'Configuration' - type: inmail.plugin.fetcher.[%parent.plugin] + type: inmail.plugin.deliverer.[%parent.plugin] # Empty default configuration schema inmail.analyzer.*: @@ -80,8 +80,8 @@ inmail.handler.moderator_forward: label: 'Moderator address' type: email -inmail.plugin.fetcher.imap: - label: 'IMAP Fetcher' +inmail.plugin.deliverer.imap: + label: 'IMAP Deliverer' type: mapping mapping: host: diff --git a/inmail.links.action.yml b/inmail.links.action.yml new file mode 100644 index 0000000..255b82b --- /dev/null +++ b/inmail.links.action.yml @@ -0,0 +1,5 @@ +inmail.deliverer_add: + route_name: inmail.deliverer_add + title: 'Configure new deliverer' + appears_on: + - 'inmail.deliverer_list' diff --git a/inmail.links.task.yml b/inmail.links.task.yml index d835d19..7f7b939 100644 --- a/inmail.links.task.yml +++ b/inmail.links.task.yml @@ -4,9 +4,9 @@ inmail.settings: base_route: inmail.settings weight: 0 -inmail.fetcher_list: - route_name: inmail.fetcher_list - title: 'Mail fetchers' +inmail.deliverer_list: + route_name: inmail.deliverer_list + title: 'Mail deliverers' base_route: inmail.settings weight: 1 diff --git a/inmail.module b/inmail.module index 862a8b5..b2d76a8 100644 --- a/inmail.module +++ b/inmail.module @@ -17,7 +17,7 @@ use Drupal\inmail\Entity\AnalyzerConfig; * You can read more under the following chapters: * - @link processing The general message processing flow @endlink * - @link mime Message parsing @endlink - * - @link fetching Fetching email over IMAP @endlink + * - @link deliverer Fetching email over IMAP @endlink * - @link analyzer Analysis of new messages @endlink * - @link handler Handling analyzed messages @endlink * - @link mailmute Integration with the Mailmute module @enlink @@ -93,11 +93,12 @@ use Drupal\inmail\Entity\AnalyzerConfig; */ /** - * @defgroup fetching Fetching email over IMAP + * @defgroup deliverer Fetching email over IMAP * @{ - * To process messages from an IMAP account, enter server details and - * credentials on the Inmail settings form. Messages are fetched and processed - * during Cron runs. + * To process messages from an IMAP account, visit the "Mail deliverers" list + * (admin/config/inmail/deliverers) and configure a new deliverer, entering + * server details and credentials in the form. Messages are delivered and + * processed during Cron runs. * @} */ @@ -270,18 +271,18 @@ function inmail_mail($key, &$message, $params) { * Implements hook_cron(). */ function inmail_cron() { - /** @var \Drupal\inmail\Entity\FetcherConfig[] $fetchers */ - $fetchers = \Drupal::entityManager()->getStorage('inmail_fetcher')->loadMultiple(); - /** @var \Drupal\Component\Plugin\PluginManagerInterface $fetcher_manager */ - $fetcher_manager = \Drupal::service('plugin.manager.inmail.fetcher'); + /** @var \Drupal\inmail\Entity\DelivererConfig[] $deliverers */ + $deliverers = \Drupal::entityManager()->getStorage('inmail_deliverer')->loadMultiple(); + /** @var \Drupal\Component\Plugin\PluginManagerInterface $deliverer_manager */ + $deliverer_manager = \Drupal::service('plugin.manager.inmail.deliverer'); /** @var \Drupal\inmail\MessageProcessorInterface $processor */ $processor = \Drupal::service('inmail.processor'); // Fetch and process new mail. - foreach ($fetchers as $fetcher) { - /** @var \Drupal\inmail\Plugin\inmail\Fetcher\FetcherInterface $plugin */ - $plugin = $fetcher_manager->createInstance($fetcher->id(), $fetcher->getConfiguration()); - $raws = $plugin->fetch(); + foreach ($deliverers as $deliverer) { + /** @var \Drupal\inmail\Plugin\inmail\Deliverer\DelivererInterface $plugin */ + $plugin = $deliverer_manager->createInstance($deliverer->id(), $deliverer->getConfiguration()); + $raws = $plugin->deliver(); $processor->processMultiple($raws); } } diff --git a/inmail.routing.yml b/inmail.routing.yml index d7d0ab8..aa9de00 100644 --- a/inmail.routing.yml +++ b/inmail.routing.yml @@ -66,32 +66,40 @@ entity.inmail_handler.disable: requirements: _permission: 'administer inmail' -inmail.fetcher_list: - path: admin/config/system/inmail/fetchers +inmail.deliverer_list: + path: admin/config/system/inmail/deliverers defaults: - _entity_list: 'inmail_fetcher' - _title: 'Mail fetchers' + _entity_list: 'inmail_deliverer' + _title: 'Mail deliverers' requirements: _permission: 'administer inmail' -entity.inmail_fetcher.edit_form: - path: admin/config/system/inmail/fetchers/{inmail_fetcher} +inmail.deliverer_add: + path: admin/config/system/inmail/deliverers/add defaults: - _entity_form: 'inmail_fetcher.default' - _title_callback: \Drupal\inmail\Controller\FetcherController::titleEdit + _entity_form: 'inmail_deliverer.add' + _title: 'Configure new deliverer' requirements: _permission: 'administer inmail' -entity.inmail_fetcher.enable: - path: admin/config/system/inmail/fetchers/{inmail_fetcher}/enable +entity.inmail_deliverer.edit_form: + path: admin/config/system/inmail/deliverers/{inmail_deliverer} defaults: - _controller: \Drupal\inmail\Controller\FetcherController::enable + _entity_form: 'inmail_deliverer.default' + _title_callback: \Drupal\inmail\Controller\DelivererController::titleEdit requirements: _permission: 'administer inmail' -entity.inmail_fetcher.disable: - path: admin/config/system/inmail/fetchers/{inmail_fetcher}/disable +entity.inmail_deliverer.enable: + path: admin/config/system/inmail/deliverers/{inmail_deliverer}/enable defaults: - _controller: \Drupal\inmail\Controller\FetcherController::disable + _controller: \Drupal\inmail\Controller\DelivererController::enable + requirements: + _permission: 'administer inmail' + +entity.inmail_deliverer.disable: + path: admin/config/system/inmail/deliverers/{inmail_deliverer}/disable + defaults: + _controller: \Drupal\inmail\Controller\DelivererController::disable requirements: _permission: 'administer inmail' diff --git a/inmail.services.yml b/inmail.services.yml index 260e421..863b9ac 100644 --- a/inmail.services.yml +++ b/inmail.services.yml @@ -2,14 +2,14 @@ services: inmail.processor: class: Drupal\inmail\MessageProcessor arguments: ['@entity.manager', '@plugin.manager.inmail.analyzer', '@plugin.manager.inmail.handler', '@logger.channel.inmail'] - plugin.manager.inmail.fetcher: + plugin.manager.inmail.deliverer: class: Drupal\Core\Plugin\DefaultPluginManager arguments: - - 'Plugin/inmail/Fetcher' + - 'Plugin/inmail/Deliverer' - '@container.namespaces' - '@module_handler' - - 'Drupal\inmail\Plugin\inmail\Fetcher\FetcherInterface' - - 'Drupal\inmail\Annotation\Fetcher' + - 'Drupal\inmail\Plugin\inmail\Deliverer\DelivererInterface' + - 'Drupal\inmail\Annotation\Deliverer' plugin.manager.inmail.analyzer: class: Drupal\inmail\AnalyzerManager parent: default_plugin_manager diff --git a/inmail_collect/src/Plugin/inmail/Handler/CollectHandler.php b/inmail_collect/src/Plugin/inmail/Handler/CollectHandler.php index 235ed05..ddab6d5 100644 --- a/inmail_collect/src/Plugin/inmail/Handler/CollectHandler.php +++ b/inmail_collect/src/Plugin/inmail/Handler/CollectHandler.php @@ -71,7 +71,7 @@ class CollectHandler extends HandlerBase { 'header-from' => $message->getHeader()->getFieldBody('From'), 'header-message-id' => $message->getHeader()->getFieldBody('Message-Id'), 'raw' => $message->toString(), - // @todo Add fetcher reference here. Relevant if multiple present. + // @todo Add deliverer reference here. Relevant if multiple present. ); Container::create(array( diff --git a/src/Annotation/Fetcher.php b/src/Annotation/Deliverer.php similarity index 51% rename from src/Annotation/Fetcher.php rename to src/Annotation/Deliverer.php index f42a749..8beda56 100644 --- a/src/Annotation/Fetcher.php +++ b/src/Annotation/Deliverer.php @@ -1,7 +1,7 @@ get('plugin.manager.inmail.fetcher')); + return new static($container->get('plugin.manager.inmail.deliverer')); } /** - * Returns a title for the fetcher configuration edit page. + * Returns a title for the deliverer configuration edit page. */ - public function titleEdit(FetcherConfig $inmail_fetcher) { - return $this->t('Configure %label fetcher', array('%label' => $inmail_fetcher->label())); + public function titleEdit(DelivererConfig $inmail_deliverer) { + return $this->t('Configure deliverer %label', array('%label' => $inmail_deliverer->label())); } /** - * Enables a mail fetcher. + * Enables a mail deliverer. */ - public function enable(FetcherConfig $inmail_fetcher) { - $inmail_fetcher->enable()->save(); - return new RedirectResponse(\Drupal::url('inmail.fetcher_list', [], ['absolute' => TRUE])); + public function enable(DelivererConfig $inmail_deliverer) { + $inmail_deliverer->enable()->save(); + return new RedirectResponse(\Drupal::url('inmail.deliverer_list', [], ['absolute' => TRUE])); } /** - * Disables a mail fetcher. + * Disables a mail deliverer. */ - public function disable(FetcherConfig $inmail_fetcher) { - $inmail_fetcher->disable()->save(); - return new RedirectResponse(\Drupal::url('inmail.fetcher_list', [], ['absolute' => TRUE])); + public function disable(DelivererConfig $inmail_deliverer) { + $inmail_deliverer->disable()->save(); + return new RedirectResponse(\Drupal::url('inmail.deliverer_list', [], ['absolute' => TRUE])); } } diff --git a/src/FetcherListBuilder.php b/src/DelivererListBuilder.php similarity index 67% rename from src/FetcherListBuilder.php rename to src/DelivererListBuilder.php index a45a41d..c01b317 100644 --- a/src/FetcherListBuilder.php +++ b/src/DelivererListBuilder.php @@ -1,7 +1,7 @@ fetcherManager = $fetcher_manager; + $this->delivererManager = $deliverer_manager; } /** @@ -44,7 +42,7 @@ class FetcherListBuilder extends ConfigEntityListBuilder { return new static( $entity_type, $container->get('entity.manager')->getStorage($entity_type->id()), - $container->get('plugin.manager.inmail.fetcher') + $container->get('plugin.manager.inmail.deliverer') ); } @@ -52,7 +50,7 @@ class FetcherListBuilder extends ConfigEntityListBuilder { * {@inheritdoc} */ public function buildHeader() { - $row['label'] = $this->t('Fetcher'); + $row['label'] = $this->t('Deliverer'); $row['plugin'] = $this->t('Plugin'); return $row + parent::buildHeader(); } @@ -61,10 +59,10 @@ class FetcherListBuilder extends ConfigEntityListBuilder { * {@inheritdoc} */ public function buildRow(EntityInterface $entity) { - /** @var \Drupal\inmail\Entity\FetcherConfig $entity */ + /** @var \Drupal\inmail\Entity\DelivererConfig $entity */ $plugin_id = $entity->getPluginId(); - if ($this->fetcherManager->hasDefinition($plugin_id)) { - $plugin_label = $this->fetcherManager->getDefinition($plugin_id)['label']; + if ($this->delivererManager->hasDefinition($plugin_id)) { + $plugin_label = $this->delivererManager->getDefinition($plugin_id)['label']; } else { $plugin_label = $this->t('Plugin missing'); diff --git a/src/Entity/FetcherConfig.php b/src/Entity/DelivererConfig.php similarity index 33% rename from src/Entity/FetcherConfig.php rename to src/Entity/DelivererConfig.php index c67d3a2..cee6152 100644 --- a/src/Entity/FetcherConfig.php +++ b/src/Entity/DelivererConfig.php @@ -1,26 +1,27 @@ get('plugin.manager.inmail.fetcher'), - $container->get('entity.manager')->getStorage('inmail_fetcher') + $container->get('plugin.manager.inmail.deliverer'), + $container->get('entity.manager')->getStorage('inmail_deliverer') ); } @@ -36,7 +36,7 @@ class FetcherConfigurationForm extends PluginConfigurationForm { public function submitForm(array &$form, FormStateInterface $form_state) { parent::submitForm($form, $form_state); - $form_state->setRedirect('inmail.fetcher_list'); + $form_state->setRedirect('inmail.deliverer_list'); } } diff --git a/src/Form/PluginConfigurationForm.php b/src/Form/PluginConfigurationForm.php index 3be2bcc..2da3b02 100644 --- a/src/Form/PluginConfigurationForm.php +++ b/src/Form/PluginConfigurationForm.php @@ -46,15 +46,11 @@ class PluginConfigurationForm extends EntityForm { /** @var \Drupal\inmail\Entity\PluginConfigEntity $entity */ $entity = $this->getEntity(); - // Load plugin instance. - /** @var \Drupal\Core\Plugin\PluginFormInterface $plugin */ - $plugin = $this->pluginManager->createInstance($entity->getPluginId(), $entity->getConfiguration()); - $form_state->set('plugin', $plugin); - $form['label'] = array( '#title' => $this->t('Label'), '#type' => 'textfield', '#default_value' => $entity->label(), + '#required' => TRUE, ); $form['id'] = array( @@ -72,7 +68,46 @@ class PluginConfigurationForm extends EntityForm { '#default_value' => TRUE, ); - $form['configuration'] = $plugin->buildConfigurationForm(array(), $form_state); + $form['plugin_container'] = array( + '#type' => 'container', + '#prefix' => '
', + '#suffix' => '
', + ); + + // Unless editing an existing plugin config, show plugin select field. + if ($entity->isNew()) { + $form['plugin_container']['plugin'] = array( + '#type' => 'select', + '#title' => $this->t('Plugin'), + '#options' => array_map(function(array $plugin_definition) { + return $plugin_definition['label']; + }, $this->pluginManager->getDefinitions()), + '#default_value' => $entity->getPluginId(), + '#required' => TRUE, + '#ajax' => array( + 'callback' => '::getPluginContainerFormChild', + 'wrapper' => 'inmail-plugin', + ), + '#submit' => array('::submitSelectPlugin'), + '#executes_submit_callback' => TRUE, + '#limit_validation_errors' => array(array('plugin')), + ); + + $form['plugin_container']['plugin_submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Select plugin'), + '#submit' => array('::submitSelectPlugin'), + '#attributes' => array('class' => array('js-hide')), + ); + } + + // Load plugin instance and form. + /** @var \Drupal\Core\Plugin\PluginFormInterface $plugin */ + if ($entity->getPluginId()) { + $plugin = $this->pluginManager->createInstance($entity->getPluginId(), $entity->getConfiguration()); + $form_state->set('plugin', $plugin); + $form['plugin_container']['configuration'] = $plugin->buildConfigurationForm(array(), $form_state); + } return $form; } @@ -91,14 +126,43 @@ class PluginConfigurationForm extends EntityForm { } /** + * Submit handler for plugin selection. + * + * @param array $form + * The form structure. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function submitSelectPlugin(array $form, FormStateInterface $form_state) { + $this->entity = $this->buildEntity($form, $form_state); + $form_state->setRebuild(); + } + + /** + * Ajax callback returning the plugin_container part of the form. + * + * @param array $form + * The form structure. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The plugin_container form part. + */ + public function getPluginContainerFormChild(array $form, FormStateInterface $form_state) { + return $form['plugin_container']; + } + + /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { parent::validateForm($form, $form_state); /** @var \Drupal\Core\Plugin\PluginFormInterface $plugin */ - $plugin = $form_state->get('plugin'); - $plugin->validateConfigurationForm($form, $form_state); + if ($plugin = $form_state->get('plugin')) { + $plugin->validateConfigurationForm($form, $form_state); + } } /** @@ -109,8 +173,9 @@ class PluginConfigurationForm extends EntityForm { // Let the plugin update its configuration from the form. /** @var \Drupal\Core\Plugin\PluginFormInterface|\Drupal\Component\Plugin\ConfigurablePluginInterface $plugin */ - $plugin = $form_state->get('plugin'); - $plugin->submitConfigurationForm($form, $form_state); + if ($plugin = $form_state->get('plugin')) { + $plugin->submitConfigurationForm($form, $form_state); + } // Copy plugin configuration to the entity for persistence. The reason for // not doing this by overriding copyFormValuesToEntity is that the plugin diff --git a/src/Plugin/inmail/Fetcher/FetcherBase.php b/src/Plugin/inmail/Deliverer/DelivererBase.php similarity index 81% rename from src/Plugin/inmail/Fetcher/FetcherBase.php rename to src/Plugin/inmail/Deliverer/DelivererBase.php index b39d44e..6eed7c4 100644 --- a/src/Plugin/inmail/Fetcher/FetcherBase.php +++ b/src/Plugin/inmail/Deliverer/DelivererBase.php @@ -1,23 +1,23 @@ loggerChannel = $logger_channel; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('logger.factory')->get('inmail'), + $container->get('state') + ); + } + + /** + * {@inheritdoc} + */ + public function deliver() { // Get details from config and connect. - // @todo Return silently if not configured. - // @todo Return noisily if misconfigured. Possibly stop retrying. + // @todo Return noisily if misconfigured or imap missing. Possibly stop retrying. $mailbox_flags = $this->configuration['ssl'] ? '/ssl' : ''; $mailbox = '{' . $this->configuration['host'] . ':' . $this->configuration['port'] . $mailbox_flags . '}'; $imap_res = imap_open($mailbox, $this->configuration['username'], $this->configuration['password']); if (!$imap_res) { - // @todo Inject logger. // @todo Consider throwing an exception. - \Drupal::logger('inmail')->error('Fetcher connection failed: @error', ['@error' => implode("\n", imap_errors())]); + $this->loggerChannel->error('Deliverer connection failed: @error', ['@error' => implode("\n", imap_errors())]); return array(); } @@ -55,7 +94,7 @@ class ImapFetcher extends FetcherBase { // Save number of unread messages. // @todo Create a Monitoring sensor for this state key. - \Drupal::state()->set('inmail.fetcher.imap.remaining', count($unread_ids)); + $this->state->set('inmail.deliverer.imap.remaining', count($unread_ids)); // Close resource and return messages. imap_close($imap_res); @@ -81,35 +120,64 @@ class ImapFetcher extends FetcherBase { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - // @todo Add descriptions here and/or in FetcherBase. + $form['info'] = array( + '#type' => 'item', + '#markup' => $this->t('Please refer to your email provider for the appropriate values for these fields.'), + ); + $form['host'] = array( '#type' => 'textfield', '#title' => $this->t('Host'), '#default_value' => $this->configuration['host'], ); + $form['port'] = array( '#type' => 'number', '#title' => $this->t('Port'), '#default_value' => $this->configuration['port'], + '#description' => $this->t('The standard port number is 143, or 993 when using SSL.'), ); + $form['ssl'] = array( '#type' => 'checkbox', '#title' => $this->t('Use SSL'), '#default_value' => $this->configuration['ssl'], ); + $form['username'] = array( '#type' => 'textfield', '#title' => $this->t('Username'), '#default_value' => $this->configuration['username'], ); - // @todo Password field cannot have #default_value. Hide field unless user checks to change password? + + // Password field cannot have #default_value. To avoid forcing user to + // re-enter password with each save, password updating is conditional on + // this checkbox. + $form['password_update'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Update password'), + ); + $form['password'] = array( '#type' => 'password', '#title' => $this->t('Password'), + '#states' => array( + 'visible' => array( + ':input[name=password_update]' => array('checked' => TRUE), + ), + ), ); + + // Always show password field if configuration is new. + if ($form_state->getFormObject()->getEntity()->isNew()) { + $form['password_update']['#access'] = FALSE; + $form['password']['#states']['visible'] = array(); + } + $form['batch_size'] = array( '#type' => 'number', '#title' => $this->t('Batch size'), + '#default_value' => $this->configuration['batch_size'], '#description' => $this->t('How many messages to fetch on each invocation.'), ); @@ -120,14 +188,20 @@ class ImapFetcher extends FetcherBase { * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $this->setConfiguration(array( + $configuration = array( 'host' => $form_state->getValue('host'), 'port' => $form_state->getValue('port'), 'ssl' => $form_state->getValue('ssl'), 'username' => $form_state->getValue('username'), - 'password' => $form_state->getValue('password'), 'batch_size' => $form_state->getValue('batch_size'), - )); + ) + $this->getConfiguration(); + + // Only update password if "Update password" is checked. + if ($form_state->getValue('password_update')) { + $configuration['password'] = $form_state->getValue('password'); + } + + $this->setConfiguration($configuration); } } diff --git a/src/Tests/InmailWebTest.php b/src/Tests/InmailWebTest.php index 8afd11b..931f675 100644 --- a/src/Tests/InmailWebTest.php +++ b/src/Tests/InmailWebTest.php @@ -53,6 +53,29 @@ class InmailWebTest extends WebTestBase { $this->drupalPostForm(NULL, ['return_path' => ''], 'Save configuration'); $this->assertText('The configuration options have been saved.'); + // Check Deliverer list. + $this->clickLink('Mail deliverers'); + $this->assertText('There is no Mail deliverer yet.'); + + $this->clickLink('Configure new deliverer'); + $edit = array( + 'label' => 'Test IMAP Deliverer', + 'id' => 'test_imap', + 'plugin' => 'imap', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'plugin'); + $edit += array( + 'host' => 'imap.example.com', + 'username' => 'user', + 'password' => 'pass', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Test IMAP Deliverer'); + + $this->assertLink('Configure'); + $this->clickLink('Disable'); + $this->clickLink('Enable'); + // Check Analyzer list. $this->clickLink('Message analyzers'); $this->assertText('Standard DSN Analyzer'); diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php index 7a5bc7e..15b1ef2 100644 --- a/src/Tests/IntegrationTest.php +++ b/src/Tests/IntegrationTest.php @@ -64,7 +64,7 @@ class IntegrationTest extends WebTestBase { $sent_mails = $this->drupalGetMails(); $raw = static::generateBounceMessage(array_pop($sent_mails)); // In reality the message would be passed to the processor through a drush - // script or a mail fetcher. + // script or a mail deliverer. \Drupal::service('inmail.processor')->process($raw); // Check send state.