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

nette / mail / 22836180643

09 Mar 2026 02:43AM UTC coverage: 77.2%. Remained the same
22836180643

push

github

dg
improved PHPDoc descriptions

386 of 500 relevant lines covered (77.2%)

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
/**
15
 * Signs email messages using DKIM (DomainKeys Identified Mail).
16
 */
17
class DkimSigner implements Signer
18
{
19
        private const DefaultSignHeaders = [
20
                'From',
21
                'To',
22
                'Date',
23
                'Subject',
24
                'Message-ID',
25
                'X-Mailer',
26
                'Content-Type',
27
        ];
28

29
        private const DkimSignature = 'DKIM-Signature';
30

31

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

48

49
        /**
50
         * Returns the signed message as a string with the DKIM-Signature header prepended.
51
         * @throws SignException
52
         */
53
        public function generateSignedMessage(Message $message): string
1✔
54
        {
55
                $message = $message->build();
1✔
56

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

60
                        return rtrim($header, "\r\n") . "\r\n" . $this->getSignature($message, $header, $this->normalizeNewLines($body)) . "\r\n\r\n" . $body;
1✔
61
                }
62

63
                throw new SignException('Malformed email');
×
64
        }
65

66

67
        /**
68
         * Builds the DKIM-Signature header value.
69
         */
70
        protected function getSignature(Message $message, string $header, string $body): string
1✔
71
        {
72
                $parts = [];
1✔
73
                foreach (
74
                        [
75
                                'v' => '1',
1✔
76
                                'a' => 'rsa-sha256',
1✔
77
                                'q' => 'dns/txt',
1✔
78
                                'l' => strlen($body),
1✔
79
                                's' => $this->selector,
1✔
80
                                't' => $this->getTime(),
1✔
81
                                'c' => 'relaxed/simple',
1✔
82
                                'h' => implode(':', $this->getSignedHeaders($message)),
1✔
83
                                'd' => $this->domain,
1✔
84
                                'bh' => $this->computeBodyHash($body),
1✔
85
                                'b' => '',
1✔
86
                        ] as $key => $value
1✔
87
                ) {
88
                        $parts[] = $key . '=' . $value;
1✔
89
                }
90

91
                return $this->computeSignature($header, self::DkimSignature . ': ' . implode('; ', $parts));
1✔
92
        }
93

94

95
        /**
96
         * Canonicalizes the selected headers, signs them, and returns the complete DKIM-Signature header string.
97
         */
98
        protected function computeSignature(string $rawHeader, string $signature): string
1✔
99
        {
100
                $selectedHeaders = array_merge($this->signHeaders, [self::DkimSignature]);
1✔
101

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

104
                $parts = [];
1✔
105
                foreach ($test = explode("\r\n", $rawHeader) as $key => $header) {
1✔
106
                        if (str_contains($header, ':')) {
1✔
107
                                [$heading, $value] = explode(':', $header, 2);
1✔
108

109
                                if (($index = array_search($heading, $selectedHeaders, strict: true)) !== false) {
1✔
110
                                        $parts[$index] =
1✔
111
                                                trim(strtolower($heading), " \t") . ':' .
1✔
112
                                                trim(preg_replace("/[ \t]{2,}/", ' ', $value), " \t");
1✔
113
                                }
114
                        }
115
                }
116

117
                ksort($parts);
1✔
118

119
                return $signature . $this->sign(implode("\r\n", $parts));
1✔
120
        }
121

122

123
        /**
124
         * Signs the value with the RSA private key and returns the base64-encoded signature.
125
         * @throws SignException
126
         */
127
        protected function sign(string $value): string
1✔
128
        {
129
                $privateKey = openssl_pkey_get_private($this->privateKey, $this->passPhrase);
1✔
130
                if (!$privateKey) {
1✔
131
                        throw new SignException('Invalid private key');
1✔
132
                }
133

134
                if (openssl_sign($value, $signature, $privateKey, 'sha256WithRSAEncryption')) {
1✔
135
                        return base64_encode($signature);
1✔
136
                }
137

138
                return '';
×
139
        }
140

141

142
        /**
143
         * Computes the base64-encoded SHA-256 hash of the canonicalized body.
144
         */
145
        protected function computeBodyHash(string $body): string
1✔
146
        {
147
                return base64_encode(
1✔
148
                        pack(
1✔
149
                                'H*',
1✔
150
                                hash('sha256', $body),
1✔
151
                        ),
152
                );
153
        }
154

155

156
        /**
157
         * Normalizes line endings to CRLF and ensures the body ends with a single CRLF.
158
         */
159
        protected function normalizeNewLines(string $s): string
1✔
160
        {
161
                $s = str_replace(["\r\n", "\n"], "\r", $s);
1✔
162
                $s = str_replace("\r", "\r\n", $s);
1✔
163
                return rtrim($s, "\r\n") . "\r\n";
1✔
164
        }
165

166

167
        /** @return list<string> */
168
        protected function getSignedHeaders(Message $message): array
1✔
169
        {
170
                return array_filter($this->signHeaders, fn($name) => $message->getHeader($name) !== null);
1✔
171
        }
172

173

174
        protected function getTime(): int
175
        {
176
                return time();
1✔
177
        }
178
}
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