Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dkim #16

Merged
merged 8 commits into from
Aug 16, 2023
Merged

Dkim #16

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 24 additions & 2 deletions classes/admin_setting_configpasswordhashed.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

/**
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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 = '') {
Expand Down
9 changes: 7 additions & 2 deletions classes/complaints_list.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
225 changes: 225 additions & 0 deletions classes/dkim_manager.php
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/

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);
}

}
4 changes: 3 additions & 1 deletion classes/event/notification_received.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

use tool_emailutils;


/**
* Event
*/
class notification_received extends \core\event\base {

/**
Expand Down
Loading