diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index a836306..59239c1 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,87 +1,3 @@
-
-CHANGELOG.txt for Mail Editor for Drupal 8
-
-
-mail_edit 8.x-1.x-dev:
-  Porting to D8:
-    - Open new 8.x-1.x branch.
-
-
-mail_edit 7.x-1.1 (2015-07-05):
-  Bug fixed:
-    - #2165875 by miiimooo: Fix the mail_edit_load() API function.
-    - #1826220 by acrollet: Fix a notice in mail_edit_menu_breadcrumb_alter().
-    - Provide a 'Purge' operation for all template records, especially orphaned ones.
-    - #1806944: Require the 'Use the administration pages and help' permission to access our pages.
-    - Force a refresh of the registry after purging a template.
-    - Add missing parameter checking.
-  Enhancements:
-    - Minor code refactoring, no functional changes.
-    - #2456719 by DamienMcKenna: Change token tree to use the new dialog.
-    - Fix Coder issues (no functional changes).
-
-mail_edit 7.x-1.0 (2012-06-21):
-  Bug fixed:
-    - Fix the breadcrumb one last time...
-
-mail_edit 7.x-1.0-rc1 (2012-06-12):
-  Bugs fixed:
-    - Really set the breadcumb on the template edit page now.
-    - #1606954: Fix timely rebuilding of the registry by using the cache system.
-  Enhancements:
-    - Further improve the 'Purge'/'Purge All' logic.
-    - Display the 'Purge'/'Purge All' operation only for keys that have a custom template.
-
-mail_edit 7.x-1.0-beta3 (2012-05-27):
-  Bugs fixed:
-    - Replace $_GET['q'] with current_path(), for D8.
-    - #1488896 by Les Lim: Fix the previous fix.
-    - #1488896: Work around problems between drupal_html_to_text() and some hosts (see also #234403).
-    - Set the breadcumbs on the template edit page.
-
-mail_edit 7.x-1.0-beta2 (2011-12-05):
-  Bugs fixed:
-    - Fix passing on the context received from drupal_mail() to mail_edit_format().
-    - Fix context evaluation (dynamic text).
-    - #1323452: Fix the inability to save templates on PHP 5.3.5.
-
-mail_edit 7.x-1.0-beta1 (2011-10-30):
-  Enhancements:
-    - Change the Subject textfield into a textarea for easier editing.
-  Bugs fixed:
-    - Tune the text of the Remove button.
-    - Fix a notice that was generated when sending a password reset email.
-    - Ask for confirmation before removing a template.
-    - Fix evaluation of dynamic text binary conditions.
-    - Fix PHPDoc blocks and type hinting.
-
-mail_edit 7.x-1.0-alpha2 (2011-10-03):
-  Porting to D7:
-    - Major overhaul (dynamic text including array iteration, improved modularization, purge and auto-purge).
-    - Fix line endings (no functional change).
-    - Some code refactoring and renaming (no functional change).
-    - Implement conditional text (and extract the hook_mail_alter() implementation into an .inc file).
-    - Add update_7001 to upgrade D6 Mail Editor tokens to D7 Core tokens. Client modules must implement a similar function to update their tokens!
-    - Add README.txt and clarify description in .info file.
-
-mail_edit 7.x-1.0-alpha1 (2011-09-01):
-  Porting to D7:
-    - Implement core token replacement and Token module support.
-    - Make user.module template replacement work.
-    - Fix the missing user templates (failed to include the .inc files).
-    - Move .inc files for other modules to new modules directory.
-    - Make it work with the old Subscriptions module (old !variables).
-
-
-mail_edit 6.x-1.1 (2011-08-28):
-  Enhancements:
-    - More clean-up (no functional change).
-    - Further tune the template list.
-    - #1238416 by Pasqualle, salvis: Implement nicely-formatted and filterable template list.
-    - Move admin code to mail_edit.admin.inc to reduce the footprint (no functional change).
-    - More clean-up (no functional change).
-    - #916560 by jaydub, salvis: Coding standards cleanup.
-
-mail_edit 6.x-1.0 (2011-08-24):
-  (no changes since 2010-12-28)
-
+Mail Editor 8.x-1.x-dev
+-----------------------
+Initial port of some limited functionality.
diff --git a/README.txt b/README.txt
index 8bc9151..f2141ae 100644
--- a/README.txt
+++ b/README.txt
@@ -2,15 +2,26 @@ Mail Editor
 ===========
 
 This module provides the ability to customize e-mail templates for mail sent
-out using drupal_mail(). On multi-language sites it supports all available
-languages.
-
-Users with the 'Administer mail templates' and 'Use the administration pages
-and help' permissions may go to admin/config/system/mail-edit, where they find
-list of all templates that can be customized. If you use a separate admin
-theme, then the 'View the administration theme' permission may be needed, too.
-
-To add Mail Editor support to a third-party module, refer to the
-modules/mail_edit.user.inc file, which shows how Mail Editor support was
-added to the User core module. Any module defining those three hooks
-automatically appears in Mail Editor.
+out using drupal_mail().
+
+Users with the 'Administer mail templates' permission may go to
+admin/config/system/mail-edit, where they find list of all email templates that
+can be customized. If you use a separate admin theme, then the 'View the
+administration theme' permission may be needed, too.
+
+
+Dependencies
+--------------------------------------------------------------------------------
+The Mail Edit module does not have any dependencies, but if the Token module is
+installed it will show a list of available tokens when editing a message.
+
+
+Installation
+--------------------------------------------------------------------------------
+Download and copy the module to the /modules/contrib directory. Enable the
+module through the Admin -> Extend (/admin/modules) page.
+
+
+Developers
+--------------------------------------------------------------------------------
+See mail_edit.api.php for details on adding support to other modules.
diff --git a/mail_edit.admin.css b/mail_edit.admin.css
deleted file mode 100644
index 0febb54..0000000
--- a/mail_edit.admin.css
+++ /dev/null
@@ -1,21 +0,0 @@
-#mail-edit-filter .mail-edit-filter {
-  margin-right: 3em;
-  float: left;
-}
-
-#mail-edit-filter #mail-edit-languages-selected-filter{
-  margin-left: 0;
-}
-
-#mail-edit-table {
-  clear: both;
-}
-
-#mail-edit-table .description {
-  line-height: 1.6em;
-}
-
-table.token-tree {
-  margin-left: 0;
-  margin-bottom: 0;
-}
diff --git a/mail_edit.admin.inc b/mail_edit.admin.inc
deleted file mode 100644
index e2b6943..0000000
--- a/mail_edit.admin.inc
+++ /dev/null
@@ -1,748 +0,0 @@
-<?php
-
-/**
- * @file
- * Administrative interface for the Mail Editor module.
- */
-
-/**
- * Form builder for the template list.
- *
- * @return array
- *   The $form array.
- */
-function mail_edit_list_form() {
-  $form['mail_templates'] = mail_edit_list();
-  return $form;
-}
-
-/**
- * Page callback: List mail templates.
- *
- * @return array
- *   The $form array.
- */
-function mail_edit_list() {
-  $language_list = mail_edit_language_list();
-  $languages_selected = !empty($_SESSION['mail_edit']['languages_selected']) ? $_SESSION['mail_edit']['languages_selected'] : $language_list;
-  $module = isset($_SESSION['mail_edit']['module']) ? $_SESSION['mail_edit']['module'] : '-enabled';
-
-  $form['filter'] = mail_edit_list_filter($languages_selected, $module, $language_list);
-  $form['filtered_form'] = mail_edit_list_filtered_form($languages_selected, $module, count($language_list) > 1);
-  drupal_add_css(drupal_get_path('module', 'mail_edit') . '/mail_edit.admin.css');
-  $form['#submit'][] = 'mail_edit_list_submit';
-  return $form;
-}
-
-/**
- * Filter the mail_edit table.
- *
- * @param array $languages_selected
- *   The list of selected langauges.
- * @param string $module
- *   The selected module option.
- * @param array $language_list
- *   The list of known languages.
- *
- * @return array
- *   The $form array.
- */
-function mail_edit_list_filter(array $languages_selected, $module, array $language_list) {
-  if (count($language_list) > 1) {
-    $form['languages_selected'] = array(
-      '#type'          => 'select',
-      '#title'         => t('Languages'),
-      '#description'   => t('Select the languages to display.'),
-      '#options'       => $language_list,
-      '#default_value' => array_keys($languages_selected),
-      '#multiple'      => TRUE,
-    );
-  }
-
-  $module_list = _mail_edit_module_list(TRUE);
-  $options['-enabled'] = t('- Display enabled modules only -');
-  $options['-all'] = t('- Display all modules -');
-  _mail_edit_key_registry_rebuild();
-  $query = db_select('mail_edit_registry', 'mer')
-    ->fields('mer', array('module'))
-    ->distinct()
-    ->orderBy('mer.module');
-  foreach ($query->execute()->fetchCol() as $mod) {
-    $options[$mod] = isset($module_list[$mod]) ? $module_list[$mod] : check_plain($mod);
-  }
-
-  $form['module'] = array(
-    '#type'          => 'select',
-    '#title'         => t('Module'),
-    '#description'   => t('Select a module.'),
-    '#options'       => $options,
-    '#default_value' => $module,
-  );
-
-  $form['submit'] = array(
-    '#type'  => 'item',
-    '#value' => '',
-  );
-  $form['submit']['filter'] = array(
-    '#type'  => 'submit',
-    '#value' => t('Filter'),
-  );
-  $form['language_list'] = array(
-    '#type'  => 'hidden',
-    '#value' => serialize($language_list),
-  );
-
-  $form['#theme'] = 'mail_edit_list_filter';
-  return $form;
-}
-
-/**
- * Create $form array with filtered list.
- *
- * @param array $languages_selected
- *   The list of selected languages.
- * @param string $module
- *   The module option to restrict the list with.
- * @param bool $display_language_names
- *   Whether to display the language names.
- *
- * @return array
- *   The $form array.
- */
-function mail_edit_list_filtered_form($languages_selected, $module, $display_language_names) {
-  _mail_edit_key_registry_rebuild();
-
-  $header = _mail_edit_list_get_header($languages_selected, $display_language_names);
-  _mail_edit_key_registry_rebuild();
-  $query = db_select('mail_edit_registry', 'mer')->extend('PagerDefault')->limit(50)
-    ->fields('mer', array('id', 'module', 'mailkey', 'description'))
-    ->orderBy('mer.module')
-    ->orderBy('mer.mailkey');
-
-  switch ($module) {
-    case '-all':
-      break;
-
-    case '-enabled':
-      $enabled_modules = array_keys(_mail_edit_module_list());
-      $query->condition('mer.module', $enabled_modules, 'IN');
-      break;
-
-    default:
-      $query->condition('mer.module', $module);
-      break;
-  }
-
-  $form['templates']['#tree'] = TRUE;
-  $form['#cache'] = TRUE;
-
-  $form['header'] = array(
-    '#type'  => 'value',
-    '#value' => $header,
-  );
-
-  $form['templates'] = array();
-  foreach ($query->execute() as $template) {
-    // Add all translated languages to the template.
-    $languages = db_select('mail_edit', 'me')
-      ->fields('me', array('language'))
-      ->condition('me.id', $template->id)
-      ->execute()
-      ->fetchCol();
-    foreach ($languages as $language) {
-      $template->languages[$language] = TRUE;
-    }
-    $form['templates'][$template->id] = _mail_edit_list_row($template, $languages_selected, $display_language_names);
-  }
-
-  $form['languages'] = array(
-    '#type'  => 'value',
-    '#value' => $languages_selected,
-  );
-
-  $form['#theme'] = 'mail_edit_table';
-  $form['pager'] = array('#markup' => theme('pager'));
-  return $form;
-}
-
-/**
- * Build the header for the mail edit table.
- *
- * @param array $languages_selected
- *   The languages to include.
- * @param bool $display_language_names
- *   Whether to display the name of the langauge.
- * @return array
- */
-function _mail_edit_list_get_header($languages_selected, $display_language_names) {
-  $header = array(
-    array('data' => t('Module')),
-    array('data' => t('Mailkey')),
-  );
-
-  if ($display_language_names) {
-    foreach ($languages_selected as $lang_name) {
-      $header[] = array('data' => $lang_name);
-    }
-    $header[] = array('data' => t('Purge'));
-  }
-  else {
-    $header[] = array('data' => t('Operations'), 'colspan' => 2);
-  }
-  return $header;
-}
-
-/**
- * Build one row of the mail edit table.
- *
- * @param $template
- *   The mail template for this row.
- * @param $languages array
- *   The languages to include.
- * @param $display_language_names bool
- *   Whether to display the language names
- *
- * @return array
- */
-function _mail_edit_list_row($template, array $languages, $display_language_names) {
-  $module_list = _mail_edit_module_list(TRUE);
-  $form['module'] = array(
-    '#markup' => isset($module_list[$template->module]) ? $module_list[$template->module] : check_plain($template->module),
-  );
-
-  $form['id'] = array(
-    '#markup' => check_plain($template->id),
-  );
-  $form['mailkey'] = array(
-    '#markup' => check_plain($template->mailkey),
-  );
-  $form['description'] = array(
-    '#markup' => filter_xss_admin($template->description),
-  );
-  $count = 0;
-  foreach ($languages as $lang_code => $lang_name) {
-    $variable = array('@language' => $lang_code);
-    if (isset($template->languages[$lang_code])) {
-      $link_text = t($display_language_names ? 'Edit @language' : 'Edit', $variable);
-      $count++;
-    }
-    else {
-      $link_text = t($display_language_names ? 'Add @language' : 'Add', $variable);
-    }
-    $form['operations'] = array(
-        '#markup' => l(($count > 0 ? t('Purge All') : t('Purge')), 'admin/config/system/mail-edit/purge/' . $template->module . '_' . $template->mailkey),
-    );
-    $form[$lang_code] = array(
-      '#markup' => l($link_text, 'admin/config/system/mail-edit/' . $template->id . '/' . $lang_code),
-    );
-  }
-  return $form;
-}
-
-/**
- * The submit handler for mail_edit_list_form().
- *
- * @param $form
- * @param $form_state
- */
-function mail_edit_list_form_submit($form, &$form_state) {
-  $language_list = unserialize($form_state['values']['language_list']);
-  $_SESSION['mail_edit']['module'] = $form_state['values']['module'];
-  if (isset($form_state['values']['languages_selected'])) {
-    $_SESSION['mail_edit']['languages_selected'] = array_intersect_key($language_list, $form_state['values']['languages_selected']);
-  }
-}
-
-/**
- * The theme function for the basic filter form.
- *
- * @param $variables
- * @return string
- *
- * @ingroup themeable
- */
-function theme_mail_edit_list_filter($variables) {
-  $form = $variables['form'];
-  $output = '<div id="mail-edit-filter">';
-  foreach (element_children($form) as $key) {
-    $attributes = drupal_attributes(array(
-      'id'    => 'mail-edit-' . str_replace('_', '-', $key) . '-filter',
-      'class' => 'mail-edit-filter',
-    ));
-    $output .= "<div $attributes>";
-    $output .= drupal_render($form[$key]);
-    $output .= '</div>';
-  }
-  $output .= '</div>';
-
-  return $output;
-}
-
-/**
- * The theme function for the mail_edit table.
- *
- * @param array $variables
- * @return string
- *
- * @ingroup themeable
- */
-function theme_mail_edit_table($variables) {
-  $form = $variables['form'];
-  $header = $form['header']['#value'];
-  $languages = $form['languages']['#value'];
-
-  foreach (element_children($form['templates']) as $key) {
-    // Build the table row.
-    $row = array();
-    $row['data'][] = array('data' => drupal_render($form['templates'][$key]['module']));
-    $row['data'][] = array('data' => drupal_render($form['templates'][$key]['mailkey']) . '<div class="description">' . drupal_render($form['templates'][$key]['description']) . '</div>');
-    foreach ($languages as $lang_code => $lang_name) {
-      $row['data'][] = array('data' => drupal_render($form['templates'][$key][$lang_code]), 'class' => 'mail-edit-table-' . $lang_code);
-    };
-    $row['data'][] = array('data' => drupal_render($form['templates'][$key]['operations']));
-    $rows[] = $row;
-  }
-
-  if (empty($rows)) {
-    $rows[] = array(array(
-      'data'    => t('No templates available for override.'),
-      'colspan' => count($header),
-    ));
-  }
-  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'mail-edit-table')));
-  if ($form['pager']) {
-    $output .= drupal_render($form['pager']);
-  }
-  return $output;
-}
-
-/**
- * Builds the list of modules that expose mailkeys.
- *
- * @param bool $all
- *   Return all modules if TRUE, or only enabled ones otherwise.
- *
- * @return array
- */
-function _mail_edit_module_list($all = FALSE) {
-  static $modules;
-
-  if (isset($modules[$all])) {
-    return $modules[$all];
-  }
-
-  $modules[$all] = array();
-  $query = db_select('system', 's')
-    ->fields('s', array('name', 'info'))
-    ->condition('s.type', 'module')
-    ->orderBy('s.name');
-  if (!$all) {
-    $query->condition('s.status', 1);
-  }
-  foreach ($query->execute() as $module) {
-    $info = unserialize($module->info);
-    if (isset($info['name'])) {
-      $modules[$all][$module->name] = $info['name'];
-    }
-  }
-  return $modules[$all];
-}
-
-/**
- * Form builder function to prepare the template edit form.
- *
- * @param array $form
- * @param array $form_state
- * @param string $id
- * @param string $lang
- *
- * @return array
- */
-function mail_edit_template_form(array $form, array $form_state, $id, $lang) {
-  _mail_edit_include();
-  _mail_edit_module_load_include('alter.inc');
-  $template = _mail_edit_load($id, $lang, TRUE);
-  if ($template === FALSE) {
-    drupal_not_found();
-    drupal_exit();
-  }
-  $new = !empty($template['default']);
-
-  $language_list = mail_edit_language_list();
-  $variables = array('%mailkey' => $template['type']['mailkey'], '%language' => $language_list[$lang]);
-  if (count($language_list) > 1) {
-    $message = t(($new
-      ? 'Create new %mailkey template for %language'
-      : 'Update %mailkey template for %language'), $variables);
-  }
-  else {
-    $message = t(($new
-      ? 'Create new %mailkey template'
-      : 'Update %mailkey template'), $variables);
-  }
-  $dst = 'drupal_set_title';  // Keep Coder happy.
-  $dst($message, PASS_THROUGH);
-
-  $form = array();
-  $form['update'] = array(
-    '#type'  => 'hidden',
-    '#value' => !$new,
-  );
-  $form['language'] = array(
-    '#type'  => 'hidden',
-    '#value' => $lang,
-  );
-  $form['id'] = array(
-    '#type'  => 'hidden',
-    '#value' => $id,
-  );
-  $form['description'] = array(
-    '#title'         => t('Description'),
-    '#type'          => 'textfield',
-    '#default_value' => isset($template['description']) ? $template['description'] : (isset($template['type']['description']) ? filter_xss($template['type']['description'], array()) : ''),
-    '#size'          => 100,
-    '#weight'        => -10,
-  );
-
-  $form['mail'] = mail_edit_template_subform($template);
-
-  $form['op'] = array(
-    '#type'   => 'submit',
-    '#value'  => $new ? t('Save new template') : t('Update template'),
-    '#submit' => array('mail_edit_template_form_save'),
-    '#weight' => 10,
-  );
-  if (!$new) {
-    $form['remove'] = array(
-      '#type'   => 'submit',
-      '#value'  => (count($language_list) > 1 ? t('Remove @language template', array('@language' => $language_list[$lang])) : t('Remove template')),
-      '#submit' => array('mail_edit_template_form_remove'),
-      '#weight' => 20,
-    );
-  }
-  $extra_func = $template['type']['module'] . '_mail_edit_form_extra';
-  if (function_exists($extra_func)) {
-    $extra_func($form, $form_state, $template['type']['mailkey'], $template);
-  }
-  return $form;
-}
-
-/**
- * Prepare a template edit subform.
- *
- * @param array $template
- *   An array with optional 'subject' and 'body' keys, containing the current
- *   subject and body strings.
- *
- * @return array
- */
-function mail_edit_template_subform($template) {
-  //dpm($template, "mail_edit_template_subform");
-  $form = array(
-    '#type'          => 'fieldset',
-    '#title'         => t('Template'),
-    '#collapsible'   => FALSE,
-  );
-  if (isset($template['subject'])) {
-    $form['subject'] = array(
-      '#title'         => t('Subject'),
-      '#type'          => 'textarea',
-      '#default_value' => $template['subject'],
-      '#rows'          => 1,
-      '#description'   => t("Subject must expand into a single line; it's a multiline field for editing convenience only.")
-    );
-  }
-  if (isset($template['body'])) {
-    $form['body'] = array(
-      '#title'         => t('Body'),
-      '#type'          => 'textarea',
-      '#default_value' => $template['body'],
-      '#rows'          => 15,
-    );
-  }
-
-  if (module_exists('token')) {
-    $token_types = module_invoke($template['type']['module'], 'mail_edit_token_types', $template['type']['id']);
-    $form['token_tree'] = array(
-      '#theme'           => 'token_tree',
-      '#token_types'     => (empty($token_types) ? array() : $token_types),
-      '#show_restricted' => ($template['type']['module'] == 'user'),
-      '#weight'          => 10,
-      '#dialog'          => TRUE,
-    );
-    drupal_add_css(drupal_get_path('module', 'mail_edit') . '/mail_edit.admin.css');
-  }
-  else {
-    $form['token_tree'] = array(
-      '#markup' => '<p>' . t('Enable the <a href="@drupal-token">Token module</a> to view the available tokens.', array('@drupal-token' => 'https://www.drupal.org/project/token')) . '</p>',
-    );
-  }
-
-  $form['dynamic'] = array(
-    '#type'        => 'fieldset',
-    '#title'       => t('Dynamic text information'),
-    '#collapsible' => TRUE,
-    '#collapsed'   => TRUE,
-    '#weight'      => 15,
-  );
-  $form['dynamic'][] = array(
-    '#markup'  => t('Dynamic text provides a way to dynamically adapt templates based on comparisons with token values.') . '<br />' .
-                  t('Note: Be careful about spaces and line breaks &mdash; they do count!') .
-                  '<div>' .
-                  '<div style="font-size: 4ex; margin-top: 1em"><b style="color: red">{{</b><u>left</u>&lt;&gt;<u>right</u><b style="color: red">?</b><u>true_text</u><b style="color: red">:</b><u>false_text</u><b style="color: red">}}</b></div><br />' .
-                  '<div style="font-size: 4ex"><b style="color: red">{{</b><u>condition</u><b style="color: red">?</b><u>true_text</u><b style="color: red">:</b><u>false_text</u><b style="color: red">}}</b></div><br />' .
-                  '<dl style="margin-bottom: 0">' .
-                  '<dt>' . t('!left and !right', array('!left' => '<u>left</u>', '!right' => '<u>right</u>')) . '</dt><dd style="margin-bottom: 0">' . t("Any text (including nothing), which may also contain tokens but no question marks (@qm) or comparison operators.", array('@qm' => "'?'")) . '</dd><br />' .
-                  '<dt>&lt;&gt;</dt><dd style="margin-bottom: 0">' . t('Any of the common comparison operators (@list).', array('@list' => "'==', '!=', '>', '<', '>=', '<='")) . '</dd><br />' .
-                  '<dt><u>condition</u></dt><dd style="margin-bottom: 0">' . t('A token; the @condition is false if the token evaluates to empty or 0, true otherwise. A prepended exclamation mark (!excl) reverses the logic.', array('@condition' => 'condition', '!excl' => "'!'")) . '</dd><br />' .
-                  '<dt><u>true_text</u></dt><dd style="margin-bottom: 0">' . t("Any text (including nothing), but no colon (@colon), except inside token names or nested dynamic clauses.<br />Square brackets (@brackets) must match and cannot be nested.", array('@colon' => "':'", '@brackets' => "'[', ']'")) . '</dd><br />' .
-                  '<dt><u>false_text</u></dt><dd style="margin-bottom: 0">' . t("Any text (including nothing).") . '</dd><br />' .
-                  '</dl>' .
-                  t('If the comparison evaluates to true, then !true_text is used, otherwise !false_text.', array('!true_text' => '<u>true_text</u>', '!false_text' => '<u>false_text</u>')) . '<br />' .
-                  '</div>' .
-                  '<div>' .
-                  '<div style="font-size: 4ex; margin-top: 1em"><b style="color: red">{{</b><u>count</u><b style="color: red">#</b><u>repeated_text</u><b style="color: red">}}</b></div><br />' .
-                  '<dl style="margin-bottom: 0">' .
-                  '<dt><u>count</u></dt><dd style="margin-bottom: 0">' . t('A number or token that evaluates to a number. A prepended exclamation mark (!excl) turns 0 into 1 and anything else into 0; two prepended exclamation marks (!two_excl) keep 0 and turn anything else into 1.', array('!excl' => "'!'", '!two_excl' => "'!!'")) . '</dd><br />' .
-                  '<dt><u>repeated_text</u></dt><dd style="margin-bottom: 0">' . t("The text that is to be repeated !count times. The text can include !number0 and !number1 which will be replaced by the 0- or 1-based index of the current iteration; the former can be used for indexing array tokens, i.e. !example, the latter for numbering the items starting at 1, as the end user would expect.", array('!count' => '<u>count</u>', '!number0' => "'#0'", '!number1' => "'#1'", '!example' => "'[<u>array</u><span style='color: red'>:index:#0</span>:<u>whatever</u>]'")) . '<br />' .
-                  t('Example: the following code lists the roles and rids (role keys) of the user, one per line:') . '<br />' .
-                  '<code>{{[user:roles:count]# &nbsp; #1: [user:roles:index:#0] ([user:roles:keys:value:#0])<br />}}</code>' . '</dd><br />' .
-                  '</dl>' .
-                  '</div>' .
-                  '<div>' .
-                  '<div style="font-size: 4ex; margin-top: 0.5em"><b style="color: red">{{</b><u>context</u><b style="color: red">@</b><u>conditional_text</u><b style="color: red">}}</b></div><br />' .
-                  '<dl style="margin-bottom: 0">' .
-                  '<dt><u>context</u></dt><dd style="margin-bottom: 0">' . t('A string that is passed in to discern the context of applying the template. A prepended exclamation mark (!excl) reverses the logic.', array('!excl' => "'!'")) . '<br />' . t('If the resulting text is used as a mail message, then the context is !MAIL. Client modules can pass in different context strings to get templates filled in for other purposes.', array('!MAIL' => "'MAIL'")) . '</dd><br />' .
-                  '<dt><u>conditional_text</u></dt><dd style="margin-bottom: 0">' . t("The text that is to be used in the given context, or in every other context if the exclamation mark (!excl) is used. This construct supports an additional level of nesting.", array('!count' => '<u>count</u>', '!number0' => "'#0'", '!number1' => "'#1'", '!excl' => "'!'")) . '</dd><br />' .
-                  '</dl>' .
-                  '</div>' .
-                  '<p>' . t('The replacement texts can contain line breaks and tokens, and they support four levels of nesting.') . '</p>' .
-                  '<p>' . t("As an extension to Dupal's token system, !Mail_Editor adds support for spaces and newlines inside the separator string of !join tokens.", array('!Mail_Editor' => 'Mail Editor', '!join' => '[*:join:?]')) . '</p>',
-  );
-  return $form;
-}
-
-/**
- * Submit handler to remove a template.
- *
- * @param array $form
- * @param array $form_state
- */
-function mail_edit_template_form_remove(array $form, array &$form_state) {
-  $form_state['redirect'] = array(current_path() . '/remove');
-}
-
-/**
- * Asks for confirmation of template removal.
- *
- * @param array $form
- * @param array $form_state
- * @param string $id
- * @param string $lang_code
- *
- * @return array
- */
-function mail_edit_template_remove_confirm(array $form, array &$form_state, $id, $lang_code) {
-  $form['id'] = array(
-    '#type' => 'hidden',
-    '#value' => $id,
-  );
-  $form['lang_code'] = array(
-    '#type' => 'hidden',
-    '#value' => $lang_code,
-  );
-  $args = explode('/', current_path());
-  array_pop($args);
-  $path = implode('/', $args);
-  return confirm_form($form, t('Are you sure you want to remove the %mailkey template?', array('%mailkey' => "$id/$lang_code")), $path, NULL, t('Remove'));
-}
-
-/**
- * Executes the template removal.
- *
- * @param array $form
- * @param array $form_state
- */
-function mail_edit_template_remove_confirm_submit(array $form, array &$form_state) {
-  $form_values = $form_state['values'];
-  $success = db_delete('mail_edit')
-    ->condition('id', $id = $form_values['id'])
-    ->condition('language', $lang_code = $form_values['lang_code'])
-    ->execute();
-  $language_list = mail_edit_language_list();
-  $variables = array(
-    '%id' => $id,
-    '%language' => $language_list[$lang_code],
-  );
-  $dsm = 'drupal_set_message';  // Keep Coder happy.
-  if (count($language_list) > 1) {
-    $dsm(t($success
-      ? '%language translation of %id has been removed.'
-      : 'Failed to remove %language translation of %id.', $variables));
-  }
-  else {
-    $dsm(t($success
-      ? 'Template %id has been removed.'
-      : 'Failed to remove the %id template.', $variables));
-  }
-  $form_state['redirect'] = 'admin/config/system/mail-edit';
-}
-
-/**
- * Submit handler to save the template edit form.
- *
- * @param array $form
- * @param array $form_state
- *
- * @return void
- */
-function mail_edit_template_form_save(array $form, array &$form_state) {
-  $values = $form_state['values'];
-  $success = drupal_write_record('mail_edit', $values, $values['update'] ? array('id', 'language') : array());
-  $language_list = mail_edit_language_list();
-  $variables = array('%id' => $values['id'], '%language' => $language_list[$values['language']]);
-  if ($success) {
-    drupal_set_message(t('Saved the %id template for the %language translation.', $variables));
-  }
-  $form_state['redirect'] = 'admin/config/system/mail-edit';
-}
-
-/**
- * Builds the registry of exposed mailkeys, keeps track of which module
- * exposed them.
- *
- * This function uses the cache system to control when it needs actual
- * rebuilding. It returns immediately, of the general cache has not been
- * cleared, so we can call it everytime before reading the registry.
- *
- * @return void
- */
-function _mail_edit_key_registry_rebuild() {
-  if (cache_get('mail_edit_registry')) {
-    return;
-  }
-  // Fetch exposed mailkeys.
-  $hook = 'mailkeys';
-  _mail_edit_include();
-  module_implements($hook, FALSE, TRUE);
-  $mailkeys = array();
-  foreach (module_implements($hook) as $module) {
-    $function = $module . '_' . $hook;
-    $result = $function();
-    if (isset($result)) {
-      $mailkeys[$module] = $result;
-    }
-  }
-  // Find out if we already have record of the exposed keys.
-  $query = db_select('mail_edit_registry', 'mer')
-    ->fields('mer');
-  foreach ($query->execute() as $row) {
-    if (isset($mailkeys[$row->module][$row->mailkey])) {
-      unset($mailkeys[$row->module][$row->mailkey]);
-    }
-  }
-  // Insert any new mailkeys into our registry table.
-  foreach ($mailkeys as $module => $keys) {
-    foreach ($keys as $key => $description) {
-      db_insert('mail_edit_registry')
-        ->fields(array(
-          'id' => $module . '_' . $key,
-          'module' => $module,
-          'mailkey' => $key,
-          'description' => $description,
-        ))
-        ->execute();
-    }
-  }
-  cache_set('mail_edit_registry', TRUE, 'cache', CACHE_TEMPORARY);
-}
-
-/**
- * Return an array of all the languages in the system.
- *
- * @return array
- */
-function mail_edit_language_list() {
-  $language_list = array();
-  if (module_exists('locale')) {
-    $language_list = locale_language_list('name', TRUE);
-  }
-  else {
-    foreach (array_keys(language_list()) as $key) {
-      $language_list[$key] = $key;
-    }
-  }
-  return $language_list;
-}
-
-/**
- * Page callback for purging a mail template.
- *
- * @param array $form
- * @param array $form_state
- * @param string $mailkey
- *
- * @return array
- */
-function mail_edit_purge_mailkey(array $form, array $form_state, $mailkey) {
-  $form['mailkey'] = array(
-    '#type' => 'hidden',
-    '#value' => $mailkey,
-  );
-  return confirm_form($form, t('Are you sure you want to purge the %mailkey template and all translations?', array('%mailkey' => $mailkey)), 'admin/config/system/mail-edit', NULL, t('Purge'));
-}
-
-/**
- * Submit handler for purging a mail template.
- *
- * @param array $form
- * @param array $form_state
- *
- * @return void
- */
-function mail_edit_purge_mailkey_submit(array $form, array &$form_state) {
-  $id = $form_state['values']['mailkey'];
-  $count = db_delete('mail_edit')
-    ->condition('id', $id)
-    ->execute();
-  $count += db_delete('mail_edit_registry')
-    ->condition('id', $id)
-    ->execute();
-  if ($count > 0) {
-    drupal_set_message(t('The %mailkey template has been purged.', array('%mailkey' => $id)));
-    cache_clear_all('mail_edit_registry', 'cache');
-  }
-  else {
-    drupal_set_message(t('The %mailkey template was not found.', array('%mailkey' => $id)));
-  }
-  unset($_SESSION['mail_edit']['module']);
-  $form_state['redirect'] = 'admin/config/system/mail-edit';
-}
-
-/**
- * Implements hook_modules_uninstalled().
- *
- * Removes templates when a module is uninstalled.
- *
- * @param array $modules
- */
-function _mail_edit_modules_uninstalled(array $modules) {
-  _mail_edit_key_registry_rebuild();
-  $templates = db_select('mail_edit_registry', 'mer')
-    ->fields('mer')
-    ->condition('mer.module', $modules, 'IN')
-    ->execute();
-  $ids = array();
-  foreach ($templates as $template) {
-    $ids[] = $template->id;
-  }
-  if (!empty($ids)) {
-    db_delete('mail_edit')
-      ->condition('id', $ids, 'IN')
-      ->execute();
-    db_delete('mail_edit_registry')
-      ->condition('id', $ids, 'IN')
-      ->execute();
-  }
-}
-
diff --git a/mail_edit.alter.inc b/mail_edit.alter.inc
deleted file mode 100644
index 36fcf8d..0000000
--- a/mail_edit.alter.inc
+++ /dev/null
@@ -1,227 +0,0 @@
-<?php
-
-/**
- * @file
- * Implementation of hook_mail_alter() for the Mail Editor module.
- */
-
-/**
- * Loads and returns a template.
- *
- * @param string $id
- * @param string|object $language
- * @param bool $force_always
- *
- * @return array|null|bool
- *   Returns FALSE if we get an invalid $id or $language,
- *   NULL if we have no template yet, or a keyed array with the default and/or
- *   customized templates.
- */
-function _mail_edit_load($id, $language, $force_always = FALSE) {
-  if (is_string($language)) {
-    $languages = language_list();
-    $language = (isset($languages[$language]) ? $languages[$language] : NULL);
-  }
-  if (!is_object($language)) {
-    // If the language is unknown, we bail out.
-    return FALSE;
-  }
-  // Find out if this mail id is in our registry.
-  if (!cache_get('mail_edit_registry')) {
-    _mail_edit_module_load_include('admin.inc');
-    _mail_edit_key_registry_rebuild();
-  }
-  $query = db_select('mail_edit_registry', 'mer')
-    ->fields('mer')
-    ->condition('mer.id', $id);
-  if (!$template_type = $query->execute()->fetchAssoc()) {
-    // If we don't have any registry record we shouldn't attempt to alter it.
-    return FALSE;
-  }
-  $module = $template_type['module'];
-  $mailkey = $template_type['mailkey'];
-
-  // Load mail template if we have altered it.
-  $query = db_select('mail_edit', 'me')
-    ->fields('me')
-    ->condition('me.id', $id)
-    ->condition('me.language', $language->language);
-  $template = $query->execute()->fetchAssoc();
-  $default_template = module_invoke($module, 'mail_edit_text', $mailkey, $language);
-  if ($template) {
-    if ($default_template) {
-      $template += $default_template;
-    }
-  }
-  else {
-    if (!$default_template) {
-      return NULL;
-    }
-    if (empty($default_template['always']) && !$force_always) {
-      return NULL;
-    }
-    $template = $default_template + array('default' => TRUE);
-  }
-  $template += array(
-    'language' => $language,
-    'type'     => $template_type,
-  );
-  return $template;
-}
-
-/**
- * Implements hook_mail_alter().
- *
- * @param array $message
- */
-function _mail_edit_mail_alter(array &$message) {
-  //dpm($message, "BEFORE mail_edit_mail_alter({$message['id']})");
-  if (!$template = _mail_edit_load($message['id'], $message['language'])) {
-    return;
-  }
-
-  $data = (isset($message['params']['data']) ? $message['params']['data'] : $message['params']);
-  $data['template'] = $template;
-  if (isset($message['params']['account']) && !isset($data['user'])) {
-    $data['user'] = $message['params']['account'];
-  }
-  $options = array(
-    'language' => $message['language'],
-    'clear'    => TRUE,
-  );
-  if ($message['module'] == 'user') {
-    $options['callback'] = 'user_mail_tokens';
-  }
-  $subject = mail_edit_format($template['subject'], $data, $options);
-  $message['subject'] = trim(drupal_html_to_text($subject));
-  $context = (isset($message['params']['context']['mail_edit']) ? $message['params']['context']['mail_edit'] : NULL);
-  $body = mail_edit_format($template['body'], $data, $options, $context);
-  // Remove trailing spaces because these may be interpreted as soft line
-  // breaks by the email client.
-  $message['body'] = array(preg_replace('/ +(\r?\n)/', '\\1', $body));
-
-  /* Uncomment this line for debugging...
-  dpm($message, 'drupal_mail() is disabled in _mail_edit_mail_alter(), this would be sent');
-  $message['to'] = '';
-  /**/
-}
-
-/**
- * Preprocess a mail template, detecting conditional text
- * that conform to a prescribed syntax.
- *
- * @param string $template
- *  The template for preprocessing.
- * @param array $data
- *  $data parameter for token_replace();
- * @param array $options
- *  $options parameter for token_replace();
- * @param string $context
- *
- * @return string
- *
- * This function supports ternary-type conditions to determine what text
- * is used in a mail in a particular situation, based on token comparisons.
- * The syntax is standard PHP/C-style ternary syntax:
- *
- * {{leftOPright?true_text:false_text}}
- *
- * OP is any of the common comparison operators.
- * left and right must not contain a question mark or operator.
- * The true_text must not contain any colon, except as a token name.
- * true_text and false_text may contain newlines as well as a nested
- * conditional text (one level of recursion).
- */
-function _mail_edit_preprocess($template, array $data, array $options, $context) {
-  for ($n = 0; $n < 5 && $result = preg_match_all('/{{(?P<condition>[^?#@]+?)(?P<operator>\?|#|@)(?P<text>(({{(({{(({{(({{((.|\n)*?)}}|.|\n)*?)}}|.|\n)*?)}}|.|\n)*?)}}|.|\n)*?))}}/', $template, $clauses); ++$n) {
-    //'(?P<text>((               .|\n)*?))';
-    //'(?P<text>(({{((.|\n)*?)}}|.|\n)*?))';
-    //'(?P<text>(({{(({{((.|\n)*?)}}|.|\n)*?)}}|.|\n)*?))';
-    //'(?P<text>(({{(({{(({{((.|\n)*?)}}|.|\n)*?)}}|.|\n)*?)}}|.|\n)*?))';
-    //'(?P<text>(({{(({{(({{(({{((.|\n)*?)}}|.|\n)*?)}}|.|\n)*?)}}|.|\n)*?)}}|.|\n)*?))';
-    foreach ($clauses[0] as $i => $clause) {
-      $replacement = '{{SYNTAX_ERROR}}';
-      if ($clauses['operator'][$i] == '?') {
-        if (!preg_match('/^(?P<true>(({{((({{((({{((({{((.|\n)*?)}})|.|\n)*?)}})|.|\n)*?)}})|.|\n)*?)}})|(\[[^]]*?\])|[^:[])*):(?P<false>(.|\n)*)$/', $clauses['text'][$i], $replacements)) {
-          //'(?P<true> (    ({{((.|\n)*?)}})    |    (\[[^]]*?\])    |    [^:[]    )* )';
-          //'(?P<true> (    ({{((({{((.|\n)*?)}})|.|\n)*?)}})    |    (\[[^]]*?\])    |    [^:[]    )* )';
-          //'(?P<true> (    ({{((({{((({{((.|\n)*?)}})|.|\n)*?)}})|.|\n)*?)}})    |    (\[[^]]*?\])    |    [^:[]    )* )';
-          //'(?P<true> (    ({{((({{((({{((({{((.|\n)*?)}})|.|\n)*?)}})|.|\n)*?)}})|.|\n)*?)}})    |    (\[[^]]*?\])    |    [^:[]    )* )';
-          $template = str_replace($clause, $replacement, $template);
-          continue;
-        }
-        if (preg_match('/^(?P<operand_1>.*?)\s*(?P<operator>==|!=|\<\>|\>=|\<=|\>|\<)\s*(?P<operand_2>.*?)$/', $clauses['condition'][$i], $condition)) {
-          $operand1 = token_replace($condition['operand_1'], $data, $options + array('clear' => TRUE));
-          $operand2 = token_replace($condition['operand_2'], $data, $options + array('clear' => TRUE));
-          switch ($condition['operator']) {
-            case '<>':
-            case '!=':  $result = ($operand1 != $operand2);  break;
-            case '==':  $result = ($operand1 == $operand2);  break;
-            case '>':   $result = ($operand1 > $operand2);   break;
-            case '<':   $result = ($operand1 < $operand2);   break;
-            case '>=':  $result = ($operand1 >= $operand2);  break;
-            case '<=':  $result = ($operand1 <= $operand2);  break;
-            default:
-              $result = NULL;
-          }
-          if (isset($result)) {
-            $replacement = $replacements[$result ? 'true' : 'false'];
-            $template = str_replace($clause, $replacement, $template);
-            continue;
-          }
-        }
-      }
-      if (preg_match('/^(?P<not>!?!?)(?P<condition>(\[[^]]*?\])|[0-9]+|[A-Za-z0-9_]+)$/', $clauses['condition'][$i], $condition)) {
-        $result = $condition['condition'];
-        if ($result[0] == '[') {
-          $result = token_replace($result, $data, $options + array('clear' => TRUE));
-        }
-        if ($not = $condition['not']) {
-          $result = ($not == '!' ? !$result : !!$result);
-        }
-        if ($clauses['operator'][$i] == '?') {
-          /** @noinspection PhpUndefinedVariableInspection */
-          $replacement = $replacements[empty($result) ? 'false' : 'true'];
-        }
-        else {
-          if (is_bool($result)) {
-            $result = (int) $result;
-          }
-          if ($clauses['operator'][$i] == '#' && is_numeric($result)) {
-            $replacement = '';
-            for ($j = 0; $j < $result; ++$j) {
-              $text = $clauses['text'][$i];
-              if (!$not) {
-                if (preg_match_all('/\[(?P<token>[^\]]*?):index:#0(?P<tail>.*?)\]/', $text, $tokens)) {
-                  foreach ($tokens['token'] as $k => $token) {
-                    $key = token_replace("[$token:keys:value:$j]", $data, $options);
-                    $text = str_replace($tokens[0][$k], "[$token:value:$key{$tokens['tail'][$k]}]", $text);
-                  }
-                }
-                $text = str_replace('#0', $j, $text);
-                $text = str_replace('#1', $j+1, $text);
-              }
-              $replacement .= $text;
-            }
-          }
-          elseif ($clauses['operator'][$i] == '@') {
-            $replacement = ((($result == $context) xor $not) ? $clauses['text'][$i] : '');
-          }
-        }
-      }
-      $template = str_replace($clause, $replacement, $template);
-    }
-  }
-
-  // Add white space support to [*:join:?] tokens.
-  if (preg_match_all('/\[[^\]]*?:join:(?P<sep>[^\]]*?)\]/', $template, $join_tokens)) {
-    $marker = token_replace("[random:hash:md5]");
-    foreach ($join_tokens[0] as $i => $join_token) {
-      $token = str_replace($join_tokens['sep'][$i], $marker, $join_tokens[0][$i]);
-      $token = token_replace($token, $data, $options);
-      $token = str_replace($marker, $join_tokens['sep'][$i], $token);
-      $template = str_replace($join_tokens[0][$i], $token, $template);
-    }
-  }
-  return $template;
-}
diff --git a/mail_edit.api.php b/mail_edit.api.php
new file mode 100644
index 0000000..bb15aea
--- /dev/null
+++ b/mail_edit.api.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * @file
+ * Documentation of the hooks provided by Mail Edit.
+ */
+
+/**
+ * Allow the list of email templates to be modified.
+ *
+ * @param array $templates
+ *   The list of templates available to be edited by the module.
+ */
+function hook_mail_edit_templates_list_alter(&$templates) {
+  // Disallow editing this email.
+  unset($templates['system.user.email']);
+}
diff --git a/mail_edit.info b/mail_edit.info
deleted file mode 100644
index 5d19d72..0000000
--- a/mail_edit.info
+++ /dev/null
@@ -1,6 +0,0 @@
-name = Mail Editor
-description = Allows editing of e-mail templates.
-core = 7.x
-
-configure = admin/config/system/mail-edit
-
diff --git a/mail_edit.info.yml b/mail_edit.info.yml
new file mode 100644
index 0000000..51a45a7
--- /dev/null
+++ b/mail_edit.info.yml
@@ -0,0 +1,4 @@
+name: 'Mail Editor'
+description: 'Allows editing of e-mail templates.'
+core: 8.x
+type: module
diff --git a/mail_edit.install b/mail_edit.install
deleted file mode 100644
index dd0d2d8..0000000
--- a/mail_edit.install
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the Mail Editor module.
- */
-
-/**
- * Implements hook_schema().
- *
- * @return array
- */
-function mail_edit_schema() {
-
-  $schema['mail_edit'] = array(
-    'description' => '',
-    'fields' => array(
-      'id' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-      ),
-      'language' => array(
-        'type' => 'varchar',
-        'length' => '10',
-        'not null' => TRUE,
-      ),
-      'description' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-      ),
-      'subject' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-      ),
-      'body' => array(
-        'type' => 'text',
-        'size' => 'normal',
-      ),
-    ),
-    'primary key' => array('id', 'language'),
-    'indexes' => array(
-      'language' => array('language'),
-    ),
-  );
-
-  $schema['mail_edit_registry'] = array(
-    'description' => '',
-    'fields' => array(
-      'id' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-        'description' => '',
-      ),
-      'module' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-        'description' => '',
-      ),
-      'mailkey' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-        'description' => '',
-      ),
-      'description' => array(
-        'type' => 'varchar',
-        'length' => '255',
-        'not null' => TRUE,
-        'description' => '',
-      ),
-    ),
-    'primary key' => array('id'),
-  );
-
-  return $schema;
-}
-
-function mail_edit_update_last_removed() {
-  return 6000;
-}
-
-/**
- * Updates the '!tokens' in the stored templates to the new D7 '[tokens]'.
- *
- * Additionally, in Drupal 7 we no longer e-mail plain text passwords to users,
- * and there is no token for a plain text password in the new token system.
- * Therefore, it also modifies any saved templates using the old '!password'
- * token such that the token is removed, and displays a warning to users that
- * they may need to go and modify the wording of their templates.
- * Note: Client modules must implement a similar function to update their
- * tokens!
- *
- * @return string|null
- */
-function mail_edit_update_7001() {
-  $tokens = array(
-    '!username_themed' => '[user:name]',
-    '!username'        => '[user:name]',
-    '!site'            => '[site:name]',
-    '!login_url'       => '[user:one-time-login-url]',
-    '!uri_brief'       => '[site:url-brief]',
-    '!uri'             => '[site:url]',
-    '!mailto'          => '[user:mail]',
-    '!date'            => '[current-date:medium]',
-    '!login_uri'       => '[site:login-url]',
-    '!edit_uri'        => '[user:edit-url]',
-    '!password'        => '[### The password is not exposed anymore! ###]',
-  );
-  $templates_with_passwords = array();
-  $result = db_select('mail_edit', 'me', array('fetch' => PDO::FETCH_ASSOC))
-    ->fields('me')
-    ->execute();
-  foreach ($result as $row) {
-    $id = $row['id'];
-    $langcode = $row['language'];
-    unset($row['id']);
-    unset($row['language']);
-    if ((strpos($row['body'], '!password') !== FALSE)) {
-      $templates_with_passwords[$id][$langcode] = $row['description'];
-    }
-    $row['subject'] = strtr($row['subject'], $tokens);
-    $row['body'] = strtr($row['body'], $tokens);
-    db_update('mail_edit')
-      ->fields($row)
-      ->condition('id', $id)
-      ->condition('language', $langcode)
-      ->execute();
-  }
-
-  if (!empty($templates_with_passwords)) {
-    $message = t('The ability to send users their passwords in plain text has been removed in Drupal 7. Your existing email templates have been modified to remove it. You should <a href="@template-url">review these templates</a> to make sure they read properly.', array('@template-url' => url('admin/config/system/mail-edit')));
-    $message .= '<br />' . t('The following templates are affected:') . '<br />';
-    $message .= "\n<table>\n";
-    foreach ($templates_with_passwords as $id => $record) {
-      foreach ($record as $langcode => $description) {
-        $message .= "<tr><td>$id</td> <td>$langcode</td> <td>" . drupal_placeholder($description) . "</td> <td>" . l(t('edit'), "admin/config/system/mail-edit/$id/$langcode") . "</td></tr>\n";
-      }
-    }
-    $message .= "\n</table>\n";
-    return $message;
-  }
-  return NULL;
-}
diff --git a/mail_edit.links.menu.yml b/mail_edit.links.menu.yml
new file mode 100644
index 0000000..b39c308
--- /dev/null
+++ b/mail_edit.links.menu.yml
@@ -0,0 +1,5 @@
+mail_edit.list:
+  route_name: mail_edit.list
+  title: 'Mail templates'
+  description: 'Edit mails being sent out by Drupal.'
+  parent: system.admin_config_system
diff --git a/mail_edit.module b/mail_edit.module
index f5ce468..ffaef8c 100644
--- a/mail_edit.module
+++ b/mail_edit.module
@@ -1,226 +1,20 @@
 <?php
 
 /**
- * @mainpage Mail Editor module
- *
- * This module enables administrators to alter and translate mail templates
- * for use by supporting client modules.
- *
- * Support for the user emails and Logintoboggan's emails is included with
- * Mail Editor.
- */
-
-/**
  * @file
- * Mail Editor module.
+ * Primary hook implementations for the Mail Edit module.
  */
 
-/**
- * Implements hook_permission().
- *
- * @return array
- */
-function mail_edit_permission() {
-  return array(
-    'administer mail templates' => array(
-      'description' => t('Change the templates used for creating mail messages to send to users.'),
-      'title' => t('Administer mail templates'),
-    ),
-  );
-}
-
-/**
- * Implements hook_menu().
- *
- * @return array
- */
-function mail_edit_menu() {
-  $items['admin/config/system/mail-edit'] = array(
-    'access callback' => 'mail_edit_access',
-    'file' => 'mail_edit.admin.inc',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('mail_edit_list_form'),
-    'description' => 'Edit mails being sent out by Drupal.',
-    'title' => 'Mail templates',
-  );
-  $items['admin/config/system/mail-edit/purge/%'] = array(
-    'access callback' => 'mail_edit_access',
-    'file' => 'mail_edit.admin.inc',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('mail_edit_purge_mailkey', 5),
-    'type' => MENU_CALLBACK,
-  );
-  $items['admin/config/system/mail-edit/%/%'] = array(
-    'access callback' => 'mail_edit_access',
-    'file' => 'mail_edit.admin.inc',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('mail_edit_template_form', 4, 5),
-    'type' => MENU_CALLBACK,
-  );
-  $items['admin/config/system/mail-edit/%/%/remove'] = array(
-    'access callback' => 'mail_edit_access',
-    'file' => 'mail_edit.admin.inc',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('mail_edit_template_remove_confirm', 4, 5),
-    'type' => MENU_CALLBACK,
-  );
-  return $items;
-}
-
-/**
- * Checks the current user's access to Mail Edit's pages.
- *
- * @return bool
- */
-function mail_edit_access() {
-  return user_access('administer mail templates') && user_access('access administration pages');
-}
-
-/**
- * Implements hook_theme().
- *
- * @return array
- */
-function mail_edit_theme() {
-  return array(
-    'mail_edit_table' => array(
-      'render element' => 'form',
-      'file' => 'mail_edit.admin.inc',
-    ),
-    'mail_edit_list_filter' => array(
-      'render element' => 'form',
-      'file' => 'mail_edit.admin.inc',
-    ),
-  );
-}
-
-/**
- * Implements hook_mail_alter().
- *
- * @param array $message
- */
-function mail_edit_mail_alter(array &$message) {
-  _mail_edit_module_load_include('alter.inc');
-  _mail_edit_mail_alter($message);
-}
-
-/**
- * Loads and returns a template.
- *
- * If the template has not been customized yet, then hook_mail_edit_text()
- * is called to retrieve the default template, and if the default template's
- * 'always' key is TRUE, then the default template is returned; otherwise NULL.
- *
- * @param string $module
- *   The module requesting the template.
- * @param string $mailkey
- *   The key for the template.
- * @param string|object $language
- *   The language object or language code for the template.
- * @param bool $force_always
- *   Always return the default template if no custom template is stored.
- *
- * @return object|null
- */
-function mail_edit_load($module, $mailkey, $language, $force_always = FALSE) {
-  _mail_edit_module_load_include('alter.inc');
-  return _mail_edit_load($module . '_' . $mailkey, $language, $force_always);
-}
-
-/**
- * Formats a text using the supplied template, data, and options.
- *
- * @param string $template
- *   The template to use.
- * @param array $data
- *   The data to be passed on to token_replace().
- * @param array $options
- *   The options to be passed on to token_replace().
- * @param string $context
- *   The context string that can be used to conditionally add or omit text
- *   depending on the context. The default is 'MAIL'.
- *
- * @return string
- */
-function mail_edit_format($template, $data, $options = array(), $context = NULL) {
-  _mail_edit_module_load_include('alter.inc');
-  $context = (isset($context) ? $context : 'MAIL');
-  $result = _mail_edit_preprocess($template, $data, $options, $context);
-  $result = token_replace($result, $data, $options);
-  return $result;
-}
-
-/**
- * Implements hook_hook_info().
- *
- * Loads the third-party support include so that their hooks are
- * registered.
- */
-function mail_edit_hook_info() {
-  _mail_edit_include();
-}
-
-/**
- * Implements hook_modules_uninstalled().
- *
- * Removes templates when a module is uninstalled.
- *
- * @param array $modules
- */
-function mail_edit_modules_uninstalled(array $modules) {
-  _mail_edit_module_load_include('admin.inc');
-  _mail_edit_modules_uninstalled($modules);
-}
-
-/**
- * Includes the sample third-party hook implementation files.
- */
-function _mail_edit_include() {
-  _mail_edit_module_load_include('modules/mail_edit', 'user.inc');
-  if (module_exists('logintoboggan')) {
-    _mail_edit_module_load_include('modules/mail_edit', 'logintoboggan.inc');
-  }
-}
-
-/**
- * Loads include files once.
- *
- * @param $name_or_ext
- * @param $ext
- *
- * @return bool
- */
-function _mail_edit_module_load_include($name_or_ext, $ext = NULL) {
-  static $loaded = array();
-
-  if (empty($ext)) {
-    $ext = $name_or_ext;
-    $name_or_ext = 'mail_edit';
-  }
-  $key = "$name_or_ext.$ext";
-  if (empty($loaded[$key])) {
-    $loaded[$key] = (bool) module_load_include($ext, 'mail_edit', $name_or_ext);
-  }
-  return $loaded[$key];
-}
+use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
- * Implements hook_menu_breadcrumb_alter().
- *
- * Fixes the breadcrumb for the template edit page.
- *
- * @param $active_trail
- * @param $item
+ * Implements hook_help().
  */
-function mail_edit_menu_breadcrumb_alter(&$active_trail, $item) {
-  if (isset($item['path']) && $item['path'] == 'admin/config/system/mail-edit/%/%') {
-    $active_trail = array($active_trail[0]);
-    foreach ($item['map'] as $i) {
-      $path = (!isset($path) ? $i : $path . '/' . $i);
-      $active_trail[] = menu_get_item($path);
-    }
-    array_pop($active_trail);
-    array_pop($active_trail);
+function mail_edit_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'mail_edit.list':
+      return '<p>'
+        . t('Edit any of the emails which are defined throughout the site.')
+        . '</p>';
   }
 }
-
diff --git a/mail_edit.permissions.yml b/mail_edit.permissions.yml
new file mode 100644
index 0000000..120b2ee
--- /dev/null
+++ b/mail_edit.permissions.yml
@@ -0,0 +1,4 @@
+'administer mail templates':
+  description: 'Change the templates used for creating mail messages to send to users.'
+  title: 'Administer mail templates'
+  restrict access: true
diff --git a/mail_edit.routing.yml b/mail_edit.routing.yml
new file mode 100644
index 0000000..15d3de8
--- /dev/null
+++ b/mail_edit.routing.yml
@@ -0,0 +1,14 @@
+mail_edit.list:
+  path: /admin/config/system/mail-edit
+  defaults:
+    _title: 'Mail templates'
+    _controller: '\Drupal\mail_edit\Controller\MailListController::listAll'
+  requirements:
+    _permission: 'administer mail templates'
+
+mail_edit.edit:
+  path: '/admin/config/system/mail-edit/{id}'
+  defaults:
+    _form: \Drupal\mail_edit\Form\MailEditTemplateForm
+  requirements:
+    _permission: 'administer mail templates'
diff --git a/modules/mail_edit.logintoboggan.inc b/modules/mail_edit.logintoboggan.inc
deleted file mode 100644
index 8009c9f..0000000
--- a/modules/mail_edit.logintoboggan.inc
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-/**
- * @file
- * A sample implementation of Mail Editor hooks for Logintoboggan module.
- *
- * Since the mail_edit module is arranged around the full key (including the calling module)
- * it can only support user_ keys at this time.
- *
- * However, the Logintoboggan module (http://drupal.org/project/logintoboggan) replaces many of the
- * mail messages and sends them itself. As a result, mail_edit is unable to deal with them.
- *
- * This simple file allows logintoboggan to work. I think a more generic approach would
- * work better (not keying things to the full mailkey ('module' + '_' + 'key') but rather
- * on the key itself.
- *
- */
-
-/**
- * Implements hook_mailkeys().
- *
- * @return array|null
- */
-function logintoboggan_mailkeys() {
-  if (module_exists('logintoboggan')) {
-    return user_mailkeys() + array(
-      'logintoboggan_resend_validation' => t('Resent validation message'),
-    );
-  }
-  return NULL;
-}
-
-/**
- * Implements hook_mail_edit_text().
- *
- * @param string $mailkey
- * @param string $language
- *
- * @return array
- */
-function logintoboggan_mail_edit_text($mailkey, $language) {
-  return user_mail_edit_text($mailkey, $language);
-}
-
-/**
- * Implements hook_mail_edit_token_types().
- *
- * @param string $mailkey
- *
- * @return array
- */
-function logintoboggan_mail_edit_token_types($mailkey) {
-  return user_mail_edit_token_types($mailkey);
-}
diff --git a/src/Controller/MailListController.php b/src/Controller/MailListController.php
new file mode 100644
index 0000000..39e6b2f
--- /dev/null
+++ b/src/Controller/MailListController.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\mail_edit\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Link;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\Xss;
+
+/**
+ * List all of the emails that may be edited by the module.
+ */
+class MailListController extends ControllerBase {
+
+  /**
+   * List the emails.
+   */
+  public function listAll() {
+    $render = [
+      '#theme' => 'table',
+      '#attributes' => [],
+      '#header' => [
+        $this->t('Module'),
+        $this->t('Subject'),
+        $this->t('Length (characters)'),
+        $this->t('Actions'),
+      ],
+      '#rows' => [],
+    ];
+
+    // Build rows out of the templates.
+    foreach ($this->getAllTemplates() as $config_name => $templates) {
+      $config_label = $config_name;
+      if ($config_name == 'user.mail') {
+        $config_label = t('Drupal core');
+      }
+      foreach ($templates as $name => $data) {
+        $render['#rows'][] = [
+          $config_label,
+          Xss::filter($data['subject']),
+          Unicode::strlen($data['body']),
+          Link::createFromRoute($this->t('Edit'), 'mail_edit.edit', ['id' => $config_name . '.' . $name]),
+        ];
+      }
+    }
+
+    return $render;
+  }
+
+  /**
+   * Get a list of all templates provided by the site.
+   *
+   * @return array
+   */
+  private function getAllTemplates() {
+    $templates = $this->getCoreTemplates() + $this->getHookTemplates();
+
+    // Trigger hook_mail_edit_templates_list_alter().
+    \Drupal::moduleHandler()->alter('mail_edit_templates_list', $templates);
+
+    return $templates;
+  }
+
+  /**
+   * Get a list of all templates provided by core.
+   *
+   * @return array
+   *   A list of all of the email templates provided by core.
+   */
+  private function getCoreTemplates() {
+    $templates = [];
+
+    foreach ($this->config('user.mail')->getRawData() as $key => $data) {
+      if (is_array($data) && isset($data['subject'])) {
+        $templates['user.mail'][$key] = $data;
+      }
+    }
+
+    return $templates;
+  }
+
+  /**
+   * Get a list of all templates provided by other modules.
+   *
+   * @return array
+   *   A list of templates defined by hook implementations.
+   */
+  private function getHookTemplates() {
+    $templates = [];
+    return $templates;
+  }
+
+}
diff --git a/src/Form/MailEditTemplateForm.php b/src/Form/MailEditTemplateForm.php
new file mode 100644
index 0000000..efdd95e
--- /dev/null
+++ b/src/Form/MailEditTemplateForm.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Drupal\mail_edit\Form;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Edit an email template.
+ */
+class MailEditTemplateForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'mail_edit_template_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $lang = NULL) {
+    // Load the template for this object.
+    $template = $this->getTemplate($id);
+
+    if (!$template) {
+      throw new NotFoundHttpException();
+    }
+
+    $form['id'] = [
+      '#type' => 'value',
+      '#value' => $id,
+    ];
+
+    $form['description'] = [
+      '#markup' => isset($template['description']) ? Xss::filter($template['description']) : '',
+      '#access' => isset($template['description']),
+    ];
+
+    $form['config'] = [
+      '#title' => $this->t('Config'),
+      '#type' => 'textfield',
+      '#default_value' => $template['config'],
+      '#disabled' => TRUE,
+    ];
+    $form['email'] = [
+      '#title' => $this->t('Email'),
+      '#type' => 'textfield',
+      '#default_value' => $template['name'],
+      '#disabled' => TRUE,
+    ];
+
+    $form['message']['subject'] = [
+      '#title' => $this->t('Subject'),
+      '#type' => 'textfield',
+      '#default_value' => $template['subject'],
+      '#required' => TRUE,
+    ];
+
+    $form['message']['body'] = [
+      '#title' => $this->t('Email body'),
+      '#type' => 'textarea',
+      '#default_value' => $template['body'],
+      '#required' => TRUE,
+    ];
+
+    // If the Token module is installed, show a link to the token tree.
+    if (\Drupal::moduleHandler()->moduleExists('token')) {
+      $form['message']['token_tree'] = [
+        '#theme' => 'token_tree_link',
+        '#token_types' => ['user'],
+        '#show_restricted' => TRUE,
+        '#show_nested' => FALSE,
+      ];
+    }
+
+    // @todo WYSIWYG support.
+    // @todo Plaintext support.
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+      '#weight' => 10,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    // @todo
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Work out the template ID.
+    $id = $form_state->getValue('id');
+
+    // Get the config object for this template.
+    $configFactory = $this->configFactory()->getEditable($this->getConfigName($id));
+    $name = $this->getEmailName($id);
+
+    // Update the config object.
+    $configFactory->set($name . '.subject', $form_state->getValue('subject'));
+    $configFactory->set($name . '.body', $form_state->getValue('body'));
+    $configFactory->save();
+
+    drupal_set_message($this->t('Email "%mesg" has been updated.', ['%mesg' => $name]));
+    $form_state->setRedirect('mail_edit.list');
+  }
+
+  /**
+   *
+   * @param string $id
+   *
+   * @return string
+   */
+  private function getConfigName($id) {
+    $parts = explode('.', $id);
+    return $parts[0] . '.' . $parts[1];
+  }
+
+  /**
+   *
+   * @param string $id
+   *
+   * @return string
+   */
+  private function getEmailName($id) {
+    $parts = explode('.', $id);
+    unset($parts[0]);
+    unset($parts[1]);
+    return implode('.', $parts);
+  }
+
+  /**
+   * Load an email template from a combination string.
+   *
+   * @param string $id
+   *   A combination of the config entity's machine name and the email's name.
+   *
+   * @return array
+   */
+  private function getTemplate($id) {
+    $config_name = $this->getConfigName($id);
+    $name = $this->getEmailName($id);
+
+    // Load the config entity.
+    $config = $this->config($config_name);
+
+    // Extract the specific config object that was requested.
+    $template = $config->get($name);
+
+    // Verify the template is what was requested.
+    if (empty($template) || !is_array($template)
+      || !isset($template['subject']) || !isset($template['body'])) {
+      return FALSE;
+    }
+
+    $template['config'] = $config_name;
+    $template['name'] = $name;
+    return $template;
+  }
+
+}
