-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DkimTagValue: Add new encoding support for DKIM1 and DMARC1
- Loading branch information
1 parent
08032ee
commit f1d6b75
Showing
4 changed files
with
202 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |