Skip to content

Commit

Permalink
DkimTagValue: Add new encoding support for DKIM1 and DMARC1
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrsagen committed Apr 25, 2024
1 parent 08032ee commit f1d6b75
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alexrsagen/obie",
"version": "1.6.5",
"version": "1.6.6",
"type": "framework",
"description": "Obie is a simple PHP framework. It aims to provide basic services needed for any web app.",
"keywords": ["framework", "php", "http", "template", "view", "router", "routing", "model", "models", "session", "sessions"],
Expand Down
84 changes: 84 additions & 0 deletions src/encoding/dkim_tag_value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php namespace Obie\Encoding;
use Obie\Encoding\DkimTagValue\Tag;
use Obie\Encoding\DkimTagValue\TagList;
use Obie\Log;

/**
* RFC 6376 compliant DKIM tag-value decoder/encoder
*/
class DkimTagValue {

const VERSION_DKIM1 = 'DKIM1';
const VERSION_DMARC1 = 'DMARC1';

/**
* Decode a DKIM tag-value list
*
* @param string $input
* @param bool $strict
* @param string $version
* @return ?TagList
*/
public static function decode(string $input, bool $strict = true, string $version = self::VERSION_DKIM1): ?TagList {
$output = new TagList();

$tag_list = explode(';', $input);
foreach ($tag_list as $tag_spec) {
$tag_name_value = explode('=', $tag_spec, 2);
if (count($tag_name_value) !== 2) {
Log::warning('DkimTagValue: unexpected tag-spec without tag-value');
return null;
}
list($tag_name, $tag_value) = $tag_name_value;

// remove leading and trailing folding white space (FWS)
$tag_name = trim($tag_name);

// remove leading, trailing and contained folding white space (FWS)
$tag_value = preg_replace('/[ \t]*\r?\n[ \t]+/', '', trim($tag_value));

// perform quoted-printable decoding
if ($tag_name === 'n') {
$tag_value = quoted_printable_decode($tag_value);
}

$output->tags[] = new Tag($tag_name, $tag_value);
}

if ($strict && !$output->isValid($version)) return null;
return $output;
}

/**
* Encode a DKIM tag-value list
*
* @param TagList $input
* @param string $version
* @return string
*/
public static function encode(TagList $input, string $version = self::VERSION_DKIM1): string {
$output = '';
$i = 0;
foreach ($input->tags as $tag) {
if ($i > 0) {
$output .= ';';
}
$output .= $tag->name;
$output .= '=';
if ($version === self::VERSION_DKIM1 && $tag->name === 'n') {
$value = quoted_printable_encode($tag->value);
for ($j = strlen($value) - 1; $j >= 0; $j--) {
$ord = ord($value[$j]);
if ($ord <= 0x20 || $ord >= 0x7F || $ord === 0x3B) {
$value = substr($value, 0, $j) . '=' . strtoupper(dechex($ord)) . substr($value, $j+1);
}
}
$output .= $value;
} else {
$output .= $tag->value;
}
$i++;
}
return $output;
}
}
73 changes: 73 additions & 0 deletions src/encoding/dkim_tag_value/tag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php namespace Obie\Encoding\DkimTagValue;
use Obie\Encoding\DkimTagValue;
use Obie\Log;
use Obie\Validation\SimpleValidator;

class Tag {
const REGEX_PART_HYPHENATED_WORD = '[a-z](?:[a-z0-9\\-]*[a-z0-9])?';
const REGEX_HYPHENATED_WORD = '/^' . self::REGEX_PART_HYPHENATED_WORD . '$/i';
const REGEX_HYPHENATED_WORDS = '/^' . self::REGEX_PART_HYPHENATED_WORD . '(?:\\:' . self::REGEX_PART_HYPHENATED_WORD . ')*$/i';
const REGEX_HYPHENATED_WORDS_OR_ASTERISK = '/^(?:\\*|' . self::REGEX_PART_HYPHENATED_WORD . ')(?:\\:(?:\\*|' . self::REGEX_PART_HYPHENATED_WORD . '))*$/i';

function __construct(
public string $name = '',
public string $value = '',
) {}

public function isValid(string $version = DkimTagValue::VERSION_DKIM1): bool {
// version tag, common to all implementations
if ($this->name === 'v' && $this->value !== $version) {
Log::warning('DkimTagValue/Tag: unexpected version value');
return false;
}

// version-specific tags
if ($version === DkimTagValue::VERSION_DKIM1) {
// tags defined in RFC 6376 (DKIM)
if (in_array($this->name, ['h', 'k']) && preg_match(self::REGEX_HYPHENATED_WORD, $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "h=" or "k="');
return false;
}
if ($this->name === 's' && preg_match(self::REGEX_HYPHENATED_WORDS_OR_ASTERISK, $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "s="');
return false;
}
if ($this->name === 't' && preg_match(self::REGEX_HYPHENATED_WORDS, $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DKIM1 tag "t="');
return false;
}
if ($this->name === 'p' && !SimpleValidator::isValid($this->value, SimpleValidator::TYPE_BASE64)) {
Log::warning('DkimTagValue/Tag: invalid public key value for DKIM1 tag "p="');
return false;
}
} elseif ($version === DkimTagValue::VERSION_DMARC1) {
// tags defined in RFC 7489 (DMARC)
if (in_array($this->name, ['adkim', 'aspf']) && preg_match('/^[rs]$/', $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid value for DMARC1 tag "adkim=" or "aspf="');
return false;
}
if ($this->name === 'fo' && preg_match('/^[01ds:]+$/', $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid value for DMARC1 tag "fo="');
return false;
}
if (in_array($this->name, ['sp','p']) && preg_match('/^(none|quarantine|reject)$/', $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid policy value for DMARC1 tag "sp=" or "p="');
return false;
}
if ($this->name === 'pct' && preg_match('/^(100|[1-9][0-9]|[0-9])$/', $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid 0-100 numeric value for DMARC1 tag "pct="');
return false;
}
if ($this->name === 'rf' && preg_match(self::REGEX_HYPHENATED_WORD, $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid hyphenated-word value for DMARC1 tag "rf="');
return false;
}
if ($this->name === 'ri' && preg_match('/^\d+$/', $this->value) !== 1) {
Log::warning('DkimTagValue/Tag: invalid numeric value for DMARC1 tag "ri="');
return false;
}
}

return true;
}
}
44 changes: 44 additions & 0 deletions src/encoding/dkim_tag_value/tag_list.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php namespace Obie\Encoding\DkimTagValue;
use Obie\Encoding\DkimTagValue;

/**
* @property Tag[] $tags
*/
class TagList {
function __construct(
public array $tags = [],
) {}

/**
* Get a single tag from the list.
*
* @param string $name
* @return Tag[]|Tag|null Returns an array if multiple tags matching $name were found. Returns null if no tags matching $name were found.
*/
public function get(string $name): array|Tag|null {
$output = [];
foreach ($this->tags as $tag) {
if ($tag->name === $name) {
$output[] = $tag;
}
}
if (count($output) === 0) return null;
if (count($output) === 1) return $output[0];
return $output;
}

public function isValid(string $version = DkimTagValue::VERSION_DKIM1): bool {
$i = 0;
foreach ($this->tags as $tag) {
// The v= tag MUST be the first tag in the record.
if ($tag->name === 'v' && $i !== 0) return false;
if (!$tag->isValid($version)) return false;
$i++;
}
return true;
}

public function __toString(): string {
return DkimTagValue::encode($this);
}
}

0 comments on commit f1d6b75

Please sign in to comment.