• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / mail / 22359399110

24 Feb 2026 04:11PM UTC coverage: 76.863%. Remained the same
22359399110

push

github

dg
fixed PHPStan errors WIP

31 of 45 new or added lines in 5 files covered. (68.89%)

64 existing lines in 6 files now uncovered.

392 of 510 relevant lines covered (76.86%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.83
/src/Mail/DkimSigner.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Mail;
9

10
use Nette;
11
use function array_filter, array_merge, array_search, base64_encode, explode, extension_loaded, hash, implode, ksort, openssl_pkey_get_private, openssl_sign, pack, preg_match, preg_replace, rtrim, str_contains, str_replace, strlen, strtolower, time, trim;
12

13

14
class DkimSigner implements Signer
15
{
16
        private const DefaultSignHeaders = [
17
                'From',
18
                'To',
19
                'Date',
20
                'Subject',
21
                'Message-ID',
22
                'X-Mailer',
23
                'Content-Type',
24
        ];
25

26
        private const DkimSignature = 'DKIM-Signature';
27

28

29
        /** @throws Nette\NotSupportedException */
30
        public function __construct(
1✔
31
                private string $domain,
32
                private string $selector,
33
                #[\SensitiveParameter]
34
                private string $privateKey,
35
                #[\SensitiveParameter]
36
                private ?string $passPhrase = null,
37
                /** @var list<string> */
38
                private array $signHeaders = self::DefaultSignHeaders,
39
        ) {
40
                if (!extension_loaded('openssl')) {
1✔
UNCOV
41
                        throw new Nette\NotSupportedException('DkimSigner requires PHP extension openssl which is not loaded.');
×
42
                }
43
        }
1✔
44

45

46
        /** @throws SignException */
47
        public function generateSignedMessage(Message $message): string
1✔
48
        {
49
                $message = $message->build();
1✔
50

51
                if (preg_match("~(.*?\r\n\r\n)(.*)~s", $message->getEncodedMessage(), $parts)) {
1✔
52
                        [, $header, $body] = $parts;
1✔
53

54
                        return rtrim($header, "\r\n") . "\r\n" . $this->getSignature($message, $header, $this->normalizeNewLines($body)) . "\r\n\r\n" . $body;
1✔
55
                }
56

UNCOV
57
                throw new SignException('Malformed email');
×
58
        }
59

60

61
        protected function getSignature(Message $message, string $header, string $body): string
1✔
62
        {
63
                $parts = [];
1✔
64
                foreach (
65
                        [
66
                                'v' => '1',
1✔
67
                                'a' => 'rsa-sha256',
1✔
68
                                'q' => 'dns/txt',
1✔
69
                                'l' => strlen($body),
1✔
70
                                's' => $this->selector,
1✔
71
                                't' => $this->getTime(),
1✔
72
                                'c' => 'relaxed/simple',
1✔
73
                                'h' => implode(':', $this->getSignedHeaders($message)),
1✔
74
                                'd' => $this->domain,
1✔
75
                                'bh' => $this->computeBodyHash($body),
1✔
76
                                'b' => '',
1✔
77
                        ] as $key => $value
1✔
78
                ) {
79
                        $parts[] = $key . '=' . $value;
1✔
80
                }
81

82
                return $this->computeSignature($header, self::DkimSignature . ': ' . implode('; ', $parts));
1✔
83
        }
84

85

86
        protected function computeSignature(string $rawHeader, string $signature): string
1✔
87
        {
88
                $selectedHeaders = array_merge($this->signHeaders, [self::DkimSignature]);
1✔
89

90
                $rawHeader = preg_replace("/\r\n[ \t]+/", ' ', rtrim($rawHeader, "\r\n") . "\r\n" . $signature);
1✔
91

92
                $parts = [];
1✔
93
                foreach ($test = explode("\r\n", $rawHeader) as $key => $header) {
1✔
94
                        if (str_contains($header, ':')) {
1✔
95
                                [$heading, $value] = explode(':', $header, 2);
1✔
96

97
                                if (($index = array_search($heading, $selectedHeaders, strict: true)) !== false) {
1✔
98
                                        $parts[$index] =
1✔
99
                                                trim(strtolower($heading), " \t") . ':' .
1✔
100
                                                trim(preg_replace("/[ \t]{2,}/", ' ', $value), " \t");
1✔
101
                                }
102
                        }
103
                }
104

105
                ksort($parts);
1✔
106

107
                return $signature . $this->sign(implode("\r\n", $parts));
1✔
108
        }
109

110

111
        /** @throws SignException */
112
        protected function sign(string $value): string
1✔
113
        {
114
                $privateKey = openssl_pkey_get_private($this->privateKey, $this->passPhrase);
1✔
115
                if (!$privateKey) {
1✔
116
                        throw new SignException('Invalid private key');
1✔
117
                }
118

119
                if (openssl_sign($value, $signature, $privateKey, 'sha256WithRSAEncryption')) {
1✔
120
                        return base64_encode($signature);
1✔
121
                }
122

UNCOV
123
                return '';
×
124
        }
125

126

127
        protected function computeBodyHash(string $body): string
1✔
128
        {
129
                return base64_encode(
1✔
130
                        pack(
1✔
131
                                'H*',
1✔
132
                                hash('sha256', $body),
1✔
133
                        ),
134
                );
135
        }
136

137

138
        protected function normalizeNewLines(string $s): string
1✔
139
        {
140
                $s = str_replace(["\r\n", "\n"], "\r", $s);
1✔
141
                $s = str_replace("\r", "\r\n", $s);
1✔
142
                return rtrim($s, "\r\n") . "\r\n";
1✔
143
        }
144

145

146
        /** @return list<string> */
147
        protected function getSignedHeaders(Message $message): array
1✔
148
        {
149
                return array_values(array_filter($this->signHeaders, fn($name) => $message->getHeader($name) !== null));
1✔
150
        }
151

152

153
        protected function getTime(): int
154
        {
155
                return time();
1✔
156
        }
157
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc