diff --git a/config/install/inmail.settings.yml b/config/install/inmail.settings.yml
index 7c0f9a7..73715c3 100644
--- a/config/install/inmail.settings.yml
+++ b/config/install/inmail.settings.yml
@@ -1 +1,6 @@
 return_path: ''
+imap:
+  host: ''
+  port: 143
+  username: ''
+  password: ''
diff --git a/inmail.module b/inmail.module
index e859b6f..f0cda35 100644
--- a/inmail.module
+++ b/inmail.module
@@ -17,6 +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 analyzer Analysis of new messages @endlink
  *   - @link handler Handling analyzed messages @endlink
  *   - @link mailmute Integration with the Mailmute module @enlink
@@ -92,6 +93,15 @@ use Drupal\inmail\Entity\AnalyzerConfig;
  */
 
 /**
+ * @defgroup fetching 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.
+ * @}
+ */
+
+/**
  * @defgroup analyzer Analyzers
  * @{
  * Analyzers evaluate messages to deduce specific information that can be used
@@ -255,3 +265,20 @@ function inmail_mail($key, &$message, $params) {
       break;
   }
 }
+
+/**
+ * Implements hook_cron().
+ */
+function inmail_cron() {
+  // @todo Use queue to fetch and process.
+  // Fetch new mail.
+  /** @var \Drupal\inmail\FetcherManager $fetcher_manager */
+  $fetcher_manager = \Drupal::service('plugin.manager.inmail.fetcher');
+  $fetcher_raws = $fetcher_manager->fetchAll();
+
+  // Process fetched mail. The return value of fetchAll() has one list of raws
+  // for each fetcher.
+  foreach ($fetcher_raws as $raws) {
+    \Drupal::service('inmail.processor')->processMultiple($raws);
+  }
+}
diff --git a/inmail.services.yml b/inmail.services.yml
index 4be451c..7ccfb3e 100644
--- a/inmail.services.yml
+++ b/inmail.services.yml
@@ -2,6 +2,10 @@ 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:
+    class: Drupal\inmail\FetcherManager
+    parent: default_plugin_manager
+    arguments: ['@config.factory']
   plugin.manager.inmail.analyzer:
     class: Drupal\inmail\AnalyzerManager
     parent: default_plugin_manager
diff --git a/src/Annotation/Fetcher.php b/src/Annotation/Fetcher.php
new file mode 100644
index 0000000..f42a749
--- /dev/null
+++ b/src/Annotation/Fetcher.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\inmail\Annotation\Fetcher.
+ */
+
+namespace Drupal\inmail\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines the plugin annotation of a mail fetcher.
+ *
+ * @ingroup fetching
+ *
+ * @Annotation
+ */
+class Fetcher extends Plugin {
+
+  /**
+   * The short machine-name to uniquely identify the fetcher.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The display label of the fetcher.
+   *
+   * @var \Drupal\Core\StringTranslation\TranslationWrapper
+   */
+  protected $label;
+
+}
diff --git a/src/FetcherManager.php b/src/FetcherManager.php
new file mode 100644
index 0000000..345bde1
--- /dev/null
+++ b/src/FetcherManager.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\inmail\FetcherManager.
+ */
+
+namespace Drupal\inmail;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\inmail\Plugin\inmail\Fetcher\FetcherInterface;
+
+/**
+ * Plugin manager for mail fetchers.
+ *
+ * @ingroup fetching
+ */
+class FetcherManager extends DefaultPluginManager {
+
+  /**
+   * The injected config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Constructs a Fetcher plugin manager.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config) {
+    parent::__construct('Plugin/inmail/Fetcher', $namespaces, $module_handler, 'Drupal\inmail\Plugin\inmail\Fetcher\FetcherInterface', 'Drupal\inmail\Annotation\Fetcher');
+    $this->setCacheBackend($cache_backend, 'inmail_fetcher');
+    $this->config = $config;
+  }
+
+  /**
+   * Creates and returns instances for all fetchers.
+   *
+   * @return \Drupal\inmail\Plugin\inmail\Fetcher\FetcherInterface[]
+   *   Instantiated fetcher plugins.
+   */
+  public function getInstances() {
+    $instances = array();
+    foreach ($this->getDefinitions() as $id => $definition) {
+      $config_item = $this->config->get("inmail.fetcher.$id");
+      $instances[$id] = $this->createInstance($id, $config_item->getRawData());
+    }
+    return $instances;
+  }
+
+  /**
+   * Invokes each fetcher and returns new messages.
+   *
+   * @returns array
+   *   An associative array where keys are fetcher IDs and values are lists of
+   *   raw messages.
+   */
+  public function fetchAll() {
+    return array_map(function(FetcherInterface $fetcher) {
+      return $fetcher->fetch();
+    }, $this->getInstances());
+  }
+
+}
diff --git a/src/Form/InmailSettingsForm.php b/src/Form/InmailSettingsForm.php
index 73ff94a..41b624d 100644
--- a/src/Form/InmailSettingsForm.php
+++ b/src/Form/InmailSettingsForm.php
@@ -6,8 +6,11 @@
 
 namespace Drupal\inmail\Form;
 
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Form for general Inmail configuration.
@@ -17,6 +20,31 @@ use Drupal\Core\Form\FormStateInterface;
 class InmailSettingsForm extends ConfigFormBase {
 
   /**
+   * The injected Fetcher plugin manager.
+   *
+   * @var \Drupal\inmail\FetcherManager
+   */
+  protected $fetcherManager;
+
+  /**
+   * Constructs an Inmail settings form.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, PluginManagerInterface $fetcher_manager) {
+    parent::__construct($config_factory);
+    $this->fetcherManager = $fetcher_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.inmail.fetcher')
+    );
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getFormId() {
@@ -40,6 +68,22 @@ class InmailSettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('return_path'),
     );
 
+    if ($fetchers = $this->fetcherManager->getInstances()) {
+      $form['fetching'] = array(
+        '#type' => 'fieldset',
+        '#title' => $this->t('Fetching'),
+        '#description' => $this->t('By configuring a fetcher you can process all email from a mailbox of your choice.'),
+        '#description_display' => 'before',
+      );
+      foreach ($fetchers as $fetcher) {
+        $form['fetching'][$fetcher->getPluginId()] = array(
+          '#type' => 'details',
+          '#title' => $fetcher->getLabel(),
+          '#open' => TRUE,
+        ) + $fetcher->buildConfigurationForm(array(), $form_state);
+      }
+    }
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -47,11 +91,32 @@ class InmailSettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    // Save general settings.
     $this->config('inmail.settings')
       ->set('return_path', $form_state->getValue('return_path'))
       ->save();
 
-    parent::submitForm($form, $form_state);
+    // Save fetcher settings.
+    foreach ($this->fetcherManager->getInstances() as $id => $fetcher) {
+      $fetcher->submitConfigurationForm($form, $form_state);
+      $fetcher_config = $this->config("inmail.fetcher.$id");
+      foreach ($fetcher->getConfiguration() as $key => $value) {
+        $fetcher_config->set($key, $value);
+      }
+      $fetcher_config->save();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+    foreach ($this->fetcherManager->getInstances() as $fetcher) {
+      $fetcher->validateConfigurationForm($form, $form_state);
+    }
   }
 
   /**
@@ -65,4 +130,5 @@ class InmailSettingsForm extends ConfigFormBase {
       $form_state->setError($element, $this->t('The address may not contain a <code>+</code> character.'));
     }
   }
+
 }
diff --git a/src/MessageProcessor.php b/src/MessageProcessor.php
index 53faf08..55e20d6 100644
--- a/src/MessageProcessor.php
+++ b/src/MessageProcessor.php
@@ -13,7 +13,6 @@ use Drupal\inmail\MIME\ParseException;
 /**
  * Mail message processor using services to analyze and handle messages.
  *
- * @todo Fetch email by IMAP/POP3 https://www.drupal.org/node/2379889
  * @todo Evaluate the analysis algorithms in D7 Bounce and CiviCRM https://www.drupal.org/node/2379845
  *
  * @ingroup processing
diff --git a/src/Plugin/inmail/Fetcher/FetcherBase.php b/src/Plugin/inmail/Fetcher/FetcherBase.php
new file mode 100644
index 0000000..dcb4b8b
--- /dev/null
+++ b/src/Plugin/inmail/Fetcher/FetcherBase.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * Contains \Plugin\inmail\Fetcher\FetcherBase.
+ */
+
+namespace Drupal\inmail\Plugin\inmail\Fetcher;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Base class for mail fetchers.
+ *
+ * This provides dumb implementations for most methods, but leaves ::fetch()
+ * abstract.
+ *
+ * @ingroup fetching
+ */
+abstract class FetcherBase extends PluginBase implements FetcherInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLabel() {
+    return $this->pluginDefinition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration + $this->defaultConfiguration();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // No validation by default.
+  }
+
+}
diff --git a/src/Plugin/inmail/Fetcher/FetcherInterface.php b/src/Plugin/inmail/Fetcher/FetcherInterface.php
new file mode 100644
index 0000000..3cabede
--- /dev/null
+++ b/src/Plugin/inmail/Fetcher/FetcherInterface.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @file
+ * Contains \Plugin\inmail\Fetcher\FetcherInterface.
+ */
+
+namespace Drupal\inmail\Plugin\inmail\Fetcher;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * Defines methods for fetchers.
+ *
+ * @ingroup fetching
+ */
+interface FetcherInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {
+
+  /**
+   * Returns the fetcher label.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslationWrapper
+   *   The fetcher label.
+   */
+  public function getLabel();
+
+  /**
+   * Connects to the configured mailbox and fetches new mail.
+   *
+   * @return string[]
+   *   The fetched messages, in complete raw form.
+   */
+  public function fetch();
+
+}
diff --git a/src/Plugin/inmail/Fetcher/ImapFetcher.php b/src/Plugin/inmail/Fetcher/ImapFetcher.php
new file mode 100644
index 0000000..12a0007
--- /dev/null
+++ b/src/Plugin/inmail/Fetcher/ImapFetcher.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\inmail\Fetch\ImapFetcher.
+ */
+
+namespace Drupal\inmail\Plugin\inmail\Fetcher;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Fetches messages over IMAP.
+ *
+ * @ingroup fetching
+ *
+ * @Fetcher(
+ *   id = "imap",
+ *   label = @Translation("IMAP")
+ * )
+ */
+class ImapFetcher extends FetcherBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetch() {
+    // Get details from config and connect.
+    // @todo Inject config.
+    $config = \Drupal::config('inmail.settings')->get('imap');
+    $mailbox_flags = $config['ssl'] ? '/ssl' : '';
+    $mailbox = '{' . $config['host'] . ':' . $config['port'] . $mailbox_flags . '}';
+    $imap_res = imap_open($mailbox, $config['username'], $config['password']);
+
+    if (!$imap_res) {
+      // @todo Inject logger.
+      \Drupal::logger('inmail')->error('Fetcher connection failed: @error', ['@error' => implode("\n", imap_errors())]);
+      return array();
+    }
+
+    // Find IDs of unread messages.
+    $unread_ids = imap_search($imap_res, 'UNSEEN') ?: array();
+
+    // Get the header + body of each message.
+    $raws = array();
+    foreach ($unread_ids as $unread_id) {
+      $raws[] = imap_fetchheader($imap_res, $unread_id) . imap_body($imap_res, $unread_id);
+    }
+
+    // Close resource and return messages.
+    imap_close($imap_res);
+    return $raws;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return array(
+      'host' => '',
+      // Standard non-SSL IMAP port as defined by 3501.
+      'port' => 143,
+      'ssl' => FALSE,
+      'username' => '',
+      'password' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $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'],
+    );
+    $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 Hide password field unless user checks to change password.
+    $form['password'] = array(
+      '#type' => 'password',
+      '#title' => $this->t('Password'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->setConfiguration(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'),
+    ));
+  }
+
+}
