diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a663f92..10e27e8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,3 +7,4 @@ jobs:
     uses: catalyst/catalyst-moodle-workflows/.github/workflows/group-39-plus-ci.yml@main
     with:
       disable_behat: true
+      disable_mustache: true
diff --git a/classes/admin_setting_configpasswordhashed.php b/classes/admin_setting_configpasswordhashed.php
index 9777933..b546c97 100644
--- a/classes/admin_setting_configpasswordhashed.php
+++ b/classes/admin_setting_configpasswordhashed.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Hashed password formlib form element
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -23,10 +25,15 @@
 
 namespace tool_emailutils;
 
-
+/**
+ * Hashed password formlib form element
+ */
 class admin_setting_configpasswordhashed extends \admin_setting {
 
+    /** @var Min length of password */
     public $minlength;
+
+    /** @var Is the password hashed */
     protected $ishashed;
 
     /**
@@ -53,6 +60,10 @@ public function get_setting() {
         return $this->config_read($this->name);
     }
 
+    /**
+     * Writes the settings
+     * @param mixed $data data
+     */
     public function write_setting($data) {
         // Is the password valid?
         $isvalid = $this->validate($data);
@@ -63,6 +74,11 @@ public function write_setting($data) {
         if (empty($data)) {
             // Password field is empty so just reuse existing hash.
             $password = $this->config_read($this->name);
+
+            // If it is null then it is a fresh install so save an empty string.
+            if ($password === null) {
+                $password = '';
+            }
         } else {
             // Hash new password.
             $password = password_hash($data, PASSWORD_DEFAULT);
@@ -72,10 +88,13 @@ public function write_setting($data) {
 
     /**
      * Validate data before storage
-     * @param string data
+     * @param string $data data
      * @return mixed true if ok string if error found
      */
     public function validate($data) {
+        if ($data === '') {
+            return true;
+        }
         if (empty($data) || (is_string($data) && (strlen($data) >= $this->minlength))) {
             return true;
         }
@@ -85,6 +104,9 @@ public function validate($data) {
 
     /**
      * Return an XHTML string for the setting
+     *
+     * @param string $data data
+     * @param string $query
      * @return string Returns an XHTML string
      */
     public function output_html($data, $query = '') {
diff --git a/classes/complaints_list.php b/classes/complaints_list.php
index b091635..9a739e5 100644
--- a/classes/complaints_list.php
+++ b/classes/complaints_list.php
@@ -16,6 +16,7 @@
 
 /**
  * Class complaint_list
+ *
  * @package    tool_emailutils
  * @copyright  2019 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -27,6 +28,8 @@
 use renderable;
 
 /**
+ * Class complaint_list
+ *
  * The complaints list class is a table which can indicate if a user has exceeded the bounce threshold.
  */
 class complaints_list extends \table_sql implements renderable {
@@ -35,6 +38,7 @@ class complaints_list extends \table_sql implements renderable {
      *
      * @param string $uniqueid unique id of form.
      * @param \moodle_url $url url where this table is displayed.
+     * @param int $perpage how many items per page
      */
     public function __construct($uniqueid, \moodle_url $url, $perpage = 100) {
         global $DB;
@@ -89,9 +93,10 @@ public function __construct($uniqueid, \moodle_url $url, $perpage = 100) {
     }
 
     /**
-     * Bouncecount column. Will wrap the values in a <span class='alert alert-dangerous'> if the value is over the computed threshold.
+     * Bouncecount column. Will wrap the values in a <span class='alert alert-dangerous'>
+     * if the value is over the computed threshold.
      *
-     * @param $data
+     * @param mixed $data
      * @return string
      */
     public function col_bouncecount($data) {
diff --git a/classes/dkim_manager.php b/classes/dkim_manager.php
new file mode 100644
index 0000000..a20c34a
--- /dev/null
+++ b/classes/dkim_manager.php
@@ -0,0 +1,225 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * DKIM manager
+ *
+ * This loads, verifies and can auto create DKIM pairs of certificates
+ * Code largely adapted from PHPMailer
+ *
+ * @package    tool_emailutils
+ * @copyright  2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Brendan Heywood <brendan@catalyst-au.net>
+ */
+
+namespace tool_emailutils;
+
+/**
+ * DKIM manager
+ */
+class dkim_manager {
+
+    /** @var Domain */
+    protected $domain;
+
+    /** @var Selector */
+    protected $selector;
+
+    /** @var Private key */
+    protected $privatekey;
+
+    /** @var Public key */
+    protected $publickey;
+
+    /** @var DNS record */
+    protected $dnsrecord;
+
+    /** Digest algorythm */
+    const DIGEST_ALG = 'sha256';
+
+    /**
+     * Create or load the certificates for a domain and selector
+     * @param string $domain domain
+     * @param string $selector
+     * @param bool $autocreate Should this autocreate cert pairs if they don't exist?
+     */
+    public function __construct($domain, $selector, $autocreate = false) {
+        $this->domain = $domain;
+        $this->selector = $selector;
+
+        $privatekeyfile = $this->get_private_key_path();
+        $publickeyfile  = $this->get_public_key_path();
+
+        if (!file_exists($privatekeyfile) && $autocreate) {
+
+            $this->get_base_path(true);
+            // Create a 2048-bit RSA key with an SHA256 digest.
+            $pk = openssl_pkey_new(
+                [
+                    'digest_alg' => self::DIGEST_ALG,
+                    'private_key_bits' => 2048,
+                    'private_key_type' => OPENSSL_KEYTYPE_RSA,
+                ]
+            );
+
+            // Save both keys.
+            openssl_pkey_export_to_file($pk, $privatekeyfile);
+            $details = openssl_pkey_get_details($pk);
+            file_put_contents($publickeyfile, $details['key']);
+        }
+
+        $this->privatekey = file_get_contents($privatekeyfile);
+        $this->publickey  = file_get_contents($publickeyfile);
+    }
+
+    /**
+     * Get the domain file path
+     */
+    public function get_domain_path() {
+        global $CFG;
+        return $CFG->dataroot . '/dkim/' . $this->domain . '/';
+    }
+
+    /**
+     * Get the domain file path
+     * @param bool $create auto create the directories
+     */
+    public function get_base_path($create = false) {
+        $certdir = $this->get_domain_path();
+        if ($create) {
+            mkdir($certdir, 0777, true);
+        }
+        return $certdir . '/' . $this->selector;
+    }
+
+    /**
+     * Get the private key file path
+     */
+    public function get_private_key_path() {
+        return $this->get_base_path() . '.private';
+    }
+
+    /**
+     * Get the public key file path
+     */
+    public function get_public_key_path() {
+        return $this->get_base_path() . '.public';
+    }
+
+    /**
+     * Get the DNS record file path
+     */
+    public function get_dns_record_path() {
+        return $this->get_base_path() . '.txt';
+    }
+
+    /**
+     * Get the domain the DKIM record should be stored at
+     */
+    public function get_dns_domain() {
+        return "{$this->selector}._domainkey.{$this->domain}";
+    }
+
+    /**
+     * Get the key of the DKIM txt record
+     */
+    public function get_dns_key() {
+        return $this->get_dns_domain() . ' IN TXT';
+    }
+
+    /**
+     * Get the value of the DKIM record
+     *
+     * This loads the public key and then stores the DNS record in a file.
+     */
+    public function get_dns_value() {
+        if (!empty($this->dnsrecord)) {
+            return $this->dnsrecord;
+        }
+
+        // TODO add support for records added by open dkim
+        // These do not include the public key in the normal format, only in the DNS value format.
+
+        if (empty($this->publickey)) {
+            return "ERROR: Can't find public key";
+        }
+
+        $dnsvalue = 'v=DKIM1;';
+        $dnsvalue .= ' h=' . self::DIGEST_ALG . ';'; // Hash algorythm.
+        $dnsvalue .= ' t=s;';   // No sub domains allowed.
+        $dnsvalue .= ' k=rsa;'; // Key type.
+        $dnsvalue .= ' p=';     // Public key.
+
+        $publickey = $this->publickey;
+        $publickey = preg_replace('/^-+.*?-+$/m', '', $publickey); // Remove PEM wrapper.
+        $publickey = str_replace(["\r", "\n"], '', $publickey); // Strip line breaks.
+        $dnsvalue .= $publickey;
+
+        $this->dnsrecord = trim($dnsvalue);
+
+        return $this->dnsrecord;
+    }
+
+    /**
+     * Get a chunked version of the DKIM record
+     *
+     * Strip and split the key into smaller parts and format for DNS as many systems
+     * don't like long TXT entries but are OK if it's split into 255-char chunks.
+     */
+    public function get_dns_value_chunked() {
+
+        $rawvalue = $this->get_dns_value();
+
+        // Split into chunks.
+        $keyparts = str_split($rawvalue, 253); // Becomes 255 when quotes are included.
+        // Quote each chunk.
+        foreach ($keyparts as $keypart) {
+            $dnsvalue .= '"' . trim($keypart) . '" ';
+        }
+
+        return $dnsvalue;
+
+    }
+    /**
+     * Get the alternate escaped version of the DKIM record
+     *
+     * Some DNS servers don't like ;(semi colon) chars unless backslash-escaped
+     */
+    public function get_dns_value_escaped() {
+
+        $value = $this->get_dns_value_chunked();
+        $value = str_replace(';', '\;', $value);
+        return $value;
+
+    }
+
+    /**
+     * Delete all info about a selector
+     */
+    public function delete_selector() {
+        $privatekeyfile = $this->get_private_key_path();
+        $publickeyfile  = $this->get_public_key_path();
+        $dnsrecordfile  = $this->get_dns_record_path();
+        $domaindir = $this->get_domain_path();
+
+        @unlink($privatekeyfile);
+        @unlink($publickeyfile);
+        @unlink($dnsrecordfile);
+        @rmdir($domaindir);
+    }
+
+}
diff --git a/classes/event/notification_received.php b/classes/event/notification_received.php
index cc5dd83..ab5c153 100644
--- a/classes/event/notification_received.php
+++ b/classes/event/notification_received.php
@@ -26,7 +26,9 @@
 
 use tool_emailutils;
 
-
+/**
+ * Event
+ */
 class notification_received extends \core\event\base {
 
     /**
diff --git a/classes/form/create_dkim.php b/classes/form/create_dkim.php
new file mode 100644
index 0000000..be070cf
--- /dev/null
+++ b/classes/form/create_dkim.php
@@ -0,0 +1,76 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Create dkim selector form
+ *
+ * @package    tool_emailutils
+ * @copyright  2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Brendan Heywood <brendan@catalyst-au.net>
+ */
+
+namespace tool_emailutils\form;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Selector form
+ */
+class create_dkim extends \moodleform {
+
+    /**
+     * Selector
+     * @see moodleform::definition()
+     */
+    public function definition() {
+
+        global $CFG;
+
+        $mform = $this->_form;
+        $noreplydomain = substr($CFG->noreplyaddress, strpos($CFG->noreplyaddress, '@') + 1);
+
+        $group = [];
+
+        $group[] =& $mform->createElement('text', 'domain', array("size" => 20));
+        $mform->setDefault("domain", $noreplydomain);
+        $mform->setType('domain', PARAM_HOST);
+
+        $group[] =& $mform->createElement('text', 'selector', array("size" => 20));
+
+        $selector = \userdate(time(), get_string('selectordefault', 'tool_emailutils'));
+        $mform->setDefault("selector", $selector);
+        $mform->setType('selector', PARAM_HOST);
+
+        $mform->addGroup($group, 'selector',  get_string('selectorcreate', 'tool_emailutils'), '', false);
+
+        $this->add_action_buttons(true, get_string('selectorcreatesubmit', 'tool_emailutils'));
+    }
+
+    /**
+     * Validate
+     *
+     * @param mixed $data date
+     * @param mixed $files files
+     * @return mixed errors
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        return $errors;
+    }
+}
diff --git a/classes/sns_client.php b/classes/sns_client.php
index c7630c5..2321561 100644
--- a/classes/sns_client.php
+++ b/classes/sns_client.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Amazon SNS Client Interface
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -45,14 +47,19 @@
  */
 class sns_client {
 
+    /** Subscribe */
     const SUBSCRIPTION_TYPE = 'SubscriptionConfirmation';
 
+    /** Unsubscribe */
     const UNSUBSCRIPTION_TYPE = 'UnsubscribeConfirmation';
 
+    /** Notify */
     const NOTIFICATION_TYPE = 'Notification';
 
+    /** Complaint */
     const COMPLAINT_TYPE = 'Complaint';
 
+    /** Bounce */
     const BOUNCE_TYPE = 'Bounce';
 
     /**
diff --git a/classes/sns_notification.php b/classes/sns_notification.php
index 92808d1..dae6783 100644
--- a/classes/sns_notification.php
+++ b/classes/sns_notification.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Amazon SNS Notification Class
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
diff --git a/client.php b/client.php
index b25448e..11af7c8 100644
--- a/client.php
+++ b/client.php
@@ -15,6 +15,12 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * An email bounce complaint handler webhook
+ *
+ * This is the endpoint that AWS notifies when it receives a complaint.
+ * This handles the complaint by incrementing the users bounce level and
+ * emiting a Moodle event.
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -24,6 +30,8 @@
 use tool_emailutils\sns_client;
 use tool_emailutils\event\notification_received;
 
+define('NO_MOODLE_COOKIES', true);
+
 require_once(__DIR__ . '/../../../config.php');
 
 if (!get_config('tool_emailutils', 'enabled')) {
diff --git a/dkim.php b/dkim.php
new file mode 100644
index 0000000..a4c3b91
--- /dev/null
+++ b/dkim.php
@@ -0,0 +1,186 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * DKIM manager admin page
+ *
+ * @package    tool_emailutils
+ * @copyright  2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Brendan Heywood <brendan@catalyst-au.net>
+ */
+
+use tool_emailutils\dkim_manager;
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+$baseurl = new moodle_url('/admin/tool/emailutils/dkim.php');
+$PAGE->set_url($baseurl);
+admin_externalpage_setup('tool_emailutils_dkim');
+
+$action = optional_param('action', '', PARAM_ALPHA);
+
+if ($action == 'delete') {
+    require_sesskey();
+    $domain = required_param('domain',  PARAM_TEXT);
+    $selector = required_param('selector', PARAM_TEXT);
+    $manager = new dkim_manager($domain, $selector);
+    $manager->delete_selector();
+    redirect($baseurl, get_string('selectordeleted', 'tool_emailutils'), null, \core\output\notification::NOTIFY_SUCCESS);
+}
+
+if ($action == 'activate') {
+    require_sesskey();
+    $selector = required_param('selector', PARAM_TEXT);
+    add_to_config_log('emaildkimselector', $CFG->emaildkimselector, $selector, '');
+    set_config('emaildkimselector', $selector);
+    redirect($baseurl, get_string('selectoractivated', 'tool_emailutils'), null, \core\output\notification::NOTIFY_SUCCESS);
+}
+
+$form = new \tool_emailutils\form\create_dkim();
+if ($form->is_cancelled()) {
+    redirect($prevurl);
+} else if ($fromform = $form->get_data()) {
+
+    $domain = $fromform->domain;
+    $selector = $fromform->selector;
+    $manager = new dkim_manager($domain, $selector, true);
+    redirect($baseurl, get_string('selectorcreated', 'tool_emailutils'), null, \core\output\notification::NOTIFY_SUCCESS);
+}
+
+$dkimdir = $CFG->dataroot . '/dkim/';
+$domains = scandir($dkimdir);
+$domaincount = 0;
+$noreplydomain = substr($CFG->noreplyaddress, strpos($CFG->noreplyaddress, '@') + 1);
+
+print $OUTPUT->header();
+print $OUTPUT->heading(get_string('dkimmanager', 'tool_emailutils'));
+
+print "<table class='table table-sm w-auto table-bordered'>";
+print '<tr><th colspan=2>Domains / selectors</th><th>Actions</th></tr>';
+foreach ($domains as $domain) {
+
+    if (substr($domain, 0, 1) == '.') {
+        continue;
+    }
+    if (!is_dir($dkimdir . $domain)) {
+        continue;
+    }
+
+    $domaincount ++;
+
+    print '<tr><td colspan=2>';
+    print '<h3>';
+    print html_writer::tag('span', "@$domain ");
+    if ($domain == $noreplydomain) {
+        print ' ' . html_writer::tag('span', get_string('domaindefaultnoreply', 'tool_emailutils'),
+            ['class' => 'badge badge-secondary']);
+    }
+    print '</h3>';
+    print '</td>';
+    print '<td>';
+
+    $url = new moodle_url('https://mxtoolbox.com/SuperTool.aspx', ['action' => "spf:$domain", 'run' => 'toolpage']);
+    print get_string('mxtoolbox', 'tool_emailutils');
+    print '<ul>';
+    print "<li><a href='$url' target='_blank'>SPF</a>";
+
+    $url = new moodle_url('https://mxtoolbox.com/SuperTool.aspx', ['action' => "txt:$domain"]);
+    print "<li><a href='$url' target='_blank'>Raw TXT</a>";
+
+    print '</th></tr>';
+
+
+    $selectors = scandir($dkimdir . $domain);
+    $selectorcount = 0;
+
+    foreach ($selectors as $file) {
+
+        if (substr($file, -8, 8) !== '.private') {
+            continue;
+        }
+
+        $selector = substr($file, 0, -8);
+        $manager = new dkim_manager($domain, $selector);
+
+        $context = [
+            'domain'    => $domain,
+            'selector'  => $selector,
+            'dkimurl'   => new moodle_url('https://mxtoolbox.com/SuperTool.aspx',
+                ['action' => "dkim:$domain:$selector", 'run' => 'toolpage']),
+            'dkimrawurl' => new moodle_url('https://mxtoolbox.com/SuperTool.aspx',
+                ['action' => "txt:$selector._domainkey.$domain"]),
+            'dnskey'    => $manager->get_dns_key(),
+            'dnsvalue'          => $manager->get_dns_value(),
+            'dnsvaluechunked'   => $manager->get_dns_value_chunked(),
+            'dnsvalueescaped'   => $manager->get_dns_value_escaped(),
+            'id' => uniqid(),
+        ];
+
+        if ($CFG->emaildkimselector == $selector) {
+            $context['selectoractive'] = true;
+        }
+
+        if ($CFG->emaildkimselector !== $selector) {
+            // Only give the option to delete if it is not being used.
+            $confirmation = new \confirm_action(
+                get_string('selectordeleteconfirm', 'tool_emailutils'),
+                null,
+                get_string('selectordelete', 'tool_emailutils')
+            );
+            $context['selectordelete'] = $OUTPUT->action_link(
+                new moodle_url('/admin/tool/emailutils/dkim.php', [
+                        'domain'    => $domain,
+                        'selector'  => $selector,
+                        'action'    => 'delete',
+                        'sesskey'   => sesskey()]),
+                    get_string('selectordelete', 'tool_emailutils'),
+                    $confirmation,
+                    ['class' => 'btn btn-secondary btn-sm'],
+                    new pix_icon('i/delete', ''));
+
+            // Only give the option to make it the active select if it is not being used.
+            $confirmation = new \confirm_action(
+                get_string('selectoractivateconfirm', 'tool_emailutils'),
+                null,
+                get_string('selectoractivate', 'tool_emailutils')
+            );
+            $context['selectoractivate'] = $OUTPUT->action_link(
+                new moodle_url('/admin/tool/emailutils/dkim.php', [
+                        'selector'  => $selector,
+                        'action'    => 'activate',
+                        'sesskey'   => sesskey()]),
+                    get_string('selectoractivate', 'tool_emailutils'),
+                    $confirmation,
+                    ['class' => 'btn btn-secondary btn-sm'],
+                    new pix_icon('i/star', ''));
+        }
+
+        print $OUTPUT->render_from_template('tool_emailutils/dkimselector', $context);
+    }
+}
+print "</table>";
+
+if ($domaincount == 0) {
+    echo $OUTPUT->notification(get_string('selectormissing', 'tool_emailutils'),  \core\notification::ERROR);
+}
+
+print html_writer::tag('div', get_string('dkimmanagerhelp', 'tool_emailutils'), ['class' => 'crap', 'style' => 'max-width: 40em']);
+
+$form->display();
+
+echo $OUTPUT->footer();
diff --git a/index.php b/index.php
index 3de9300..05bccd4 100644
--- a/index.php
+++ b/index.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Email bounce handler
+ *
  * @package    tool_emailutils
  * @copyright  2019 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
diff --git a/lang/en/tool_emailutils.php b/lang/en/tool_emailutils.php
index ae12eef..21c72bc 100644
--- a/lang/en/tool_emailutils.php
+++ b/lang/en/tool_emailutils.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Lang pack
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -46,6 +48,35 @@
 
 $string['event:notificationreceived'] = 'AWS SNS notification received';
 
+$string['dkimmanager'] = 'DKIM manager';
+$string['dkimmanagerhelp'] = '<p>This shows all DKIM key pairs / selectors available for email signing, including those made by this admin tool or put in place by external tools such as open-dkim. For most systems this is the end to end setup:</p>
+<ol>
+<li>First decide and set the <code>$CFG->noreply</code> email as the domain of this is email is tied to the signing.
+<li>Create a new private and public key pair using a selector of your choice. The selector is arbitrary but a rough date format is a good convention.
+<li>Save the DNS record shown in this tool into your DNS server
+<li>Confirm that the DNS is in the correct shape using the MXtoolbox links
+<li>Now activate the selector you have chosen
+<li>Use the test email tool to send a real email and confirm the DKIM headers have been sent
+<li>Also confirm the DKIM headers validate using a 3rd party tools built into gmail, and most email clients
+</ol>
+';
+
+$string['domaindefaultnoreply'] = 'Default noreply';
+
+$string['mxtoolbox'] = 'MXtoolbox';
+$string['selectoractive'] = 'Active selector';
+$string['selectoractivate'] = 'Activate selector';
+$string['selectoractivated'] = 'Selector was activated';
+$string['selectoractivateconfirm'] = 'This will set $CFG->emaildkimselector to this selector and it will be used for signing outgoing emails.';
+$string['selectorcreate'] = 'Create a new domain:selector certificate pair';
+$string['selectorcreatesubmit'] = 'Create new selector';
+$string['selectorcreated'] = 'A new certificate pair has been created';
+$string['selectordefault'] = '%Y-%m';
+$string['selectormissing'] = 'No DKIM selector certificates found';
+$string['selectordelete'] = 'Delete inactive selector';
+$string['selectordeleted'] = 'Inactive selector has been deleted';
+$string['selectordeleteconfirm'] = 'This will permanently delete this selector\'s private and public keys and is irreversable.';
+
 // Complaints list strings.
 $string['not_implemented'] = 'Not implemented yet. Search the user report for emails ending with ".b.invalid" and ".c.invalid".';
 $string['bounces'] = 'For a list of bounces, visit {$a} and search for emails ending with ".b.invalid."';
diff --git a/lib.php b/lib.php
index 839966f..12be8ed 100644
--- a/lib.php
+++ b/lib.php
@@ -15,12 +15,17 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Lib
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @author     Harry Barnard <harry.barnard@catalyst-eu.net>
  */
 
+/**
+ * This adds a new bulk user action to reset a persons bounce count
+ */
 function tool_emailutils_bulk_user_actions() {
     return [
         'tool_ses_reset_bounces' => new action_link(
diff --git a/reset_bounces.php b/reset_bounces.php
index 7df3ac2..29a3ee5 100644
--- a/reset_bounces.php
+++ b/reset_bounces.php
@@ -40,7 +40,7 @@
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('resetbounces', 'tool_emailutils'));
 
-if ($confirm and confirm_sesskey()) {
+if ($confirm && confirm_sesskey()) {
     list($in, $params) = $DB->get_in_or_equal($SESSION->bulk_users);
     $rs = $DB->get_recordset_select('user', "id $in", $params, '', 'id, ' . get_all_user_name_fields(true));
     foreach ($rs as $user) {
diff --git a/settings.php b/settings.php
index e9028d1..66d2b0d 100644
--- a/settings.php
+++ b/settings.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Add admin settings
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -29,6 +31,12 @@
         new lang_string('pluginname', 'tool_emailutils')
     ));
 
+    $ADMIN->add('email', new admin_externalpage(
+        'tool_emailutils_dkim',
+        new lang_string('dkimmanager', 'tool_emailutils'),
+        new moodle_url('/admin/tool/emailutils/dkim.php')
+    ));
+
     $ADMIN->add('tool_emailutils', new admin_externalpage(
         'tool_emailutils_list',
         new lang_string('list', 'tool_emailutils'),
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..da66606
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,11 @@
+
+.path-admin-tool-emailutils .dnsrecord {
+    background: #eee;
+    font: 75% monospace;
+    max-width: 50em;
+    overflow-wrap: anywhere;
+    padding: 0.5em;
+    text-wrap: wrap;
+    user-select: all;
+}
+
diff --git a/templates/admin_setting_configpasswordhashed.mustache b/templates/admin_setting_configpasswordhashed.mustache
index 9a16692..44d5b09 100644
--- a/templates/admin_setting_configpasswordhashed.mustache
+++ b/templates/admin_setting_configpasswordhashed.mustache
@@ -1,3 +1,34 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_emailutils/admin_setting_configpasswordhashed
+
+    This template renders a DKIM selector table row
+
+    Context variables required for this template:
+    * selectoractive If this selector is the one in use
+
+    Example context (json):
+    {
+        "minlength": 8,
+        "id": "myid",
+        "fullname": "myname"
+    }
+}}
 <div class="form-text defaultsnext">
 <input type="password" minlength="{{minlength}}" id="{{id}}" name="{{fullname}}" value="" class="form-control" size="30" />
-</div>
\ No newline at end of file
+</div>
diff --git a/templates/bounce_column.mustache b/templates/bounce_column.mustache
index c86277a..402a045 100644
--- a/templates/bounce_column.mustache
+++ b/templates/bounce_column.mustache
@@ -1,6 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_emailutils/admin_setting_configpasswordhashed
+
+    This template renders a DKIM selector table row
+
+    Context variables required for this template:
+    * selectoractive If this selector is the one in use
+
+    Example context (json):
+    {
+        "overthreshhold": true,
+        "bouncecount": 5
+    }
+}}
 {{#overthreshold}}
 <span class='badge badge-danger'>{{bouncecount}}</span>
 {{/overthreshold}}
 {{^overthreshold}}
 {{bouncecount}}
-{{/overthreshold}}
\ No newline at end of file
+{{/overthreshold}}
diff --git a/templates/dkimselector.mustache b/templates/dkimselector.mustache
new file mode 100644
index 0000000..2bfac39
--- /dev/null
+++ b/templates/dkimselector.mustache
@@ -0,0 +1,83 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_emailutils/dkimselector
+
+    This template renders a DKIM selector table row
+
+    Context variables required for this template:
+    * selectoractive If this selector is the one in use
+
+    Example context (json):
+    {
+        "welcomemessage": "welcomemessage",
+        "selectoractive": true
+    }
+}}
+<tr>
+    <td>
+        <code>{{selector}}</code>
+        {{#selectoractive}}
+            <br><span class='badge badge-success'>{{#str}} selectoractive, tool_emailutils{{/str}}</span>
+        {{/selectoractive}}
+    </td>
+    <td>
+        <div class="container">
+            <div class="row">
+                <div>
+                    <ul class="nav nav-tabs" role="tablist">
+                       <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#simple{{id}}" role="tab" aria-selected="true">DKIM</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" data-toggle="tab" href="#chunked{{id}}" role="tab" aria-selected="false">Chunked</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" data-toggle="tab" href="#escaped{{id}}" role="tab" aria-selected="false">Escaped</a>
+                        </li>
+                    </ul>
+                    <div class="tab-content mt-3">
+                        <div class="tab-pane active" id="simple{{id}}" role="tabpanel">
+                            <code>{{dnskey}}</code>
+                            <p>This is the full raw txt of the DNS value:</p>
+                            <p class="dnsrecord">{{dnsvalue}}</p>
+                        </div>
+                        <div class="tab-pane" id="chunked{{id}}" role="tabpanel">
+                            <code>{{dnskey}}</code>
+                            <p>This is the DNS record value broken into quoted chunks of max 256 chars:</p>
+                            <p class="dnsrecord">{{dnsvaluechunked}}</p>
+                        </div>
+                        <div class="tab-pane" id="escaped{{id}}" role="tabpanel">
+                            <code>{{dnskey}}</code>
+                            <p>This is an escaped record value which is needed for some DNS systems:</p>
+                            <p class="dnsrecord">{{dnsvalueescaped}}</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </td>
+    <td>
+        {{#str}} mxtoolbox, tool_emailutils{{/str}}
+        <ul>
+            <li><a href='{{dkimurl}}' target='_blank'>DKIM</a>
+            <li><a href='{{dkimrawurl}}' target='_blank'>Raw TXT</a>
+        </ul>
+        <p>{{{selectordelete}}}</p>
+        <p>{{{selectoractivate}}}</p>
+    </td>
+</tr>
diff --git a/version.php b/version.php
index aefbb54..419f85f 100644
--- a/version.php
+++ b/version.php
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * Version
+ *
  * @package    tool_emailutils
  * @copyright  2018 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later