-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmail-signature.class.php
504 lines (417 loc) · 16.4 KB
/
mail-signature.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
<?php
/**
* php-mail-signature
*
* https://github.com/louisameline/php-mail-signature
* Author: Louis Ameline - 04/2012
*
*
* This stand-alone DKIM class is based on the work made on PHP-MAILER (see license below).
* The differences are :
* - it is a standalone class
* - it supports Domain Keys header
* - it supports UTF-8
* - it will let you choose the headers you want to base the signature on
* - it will let you choose between simple and relaxed body canonicalization
*
* If the class fails to sign the e-mail, the returned DKIM header will be empty and the mail
* will still be sent, just unsigned. A php warning is thrown for logging.
*
* NOTE: you will NOT be able to use Domain Keys with PHP's mail() function, since it does
* not allow to prepend the DK header before the To and Subject ones. DKIM is ok with that,
* but Domain Keys is not. If you still want Domain Keys, you will have to manage to send
* your mail straight to your MTA without the mail() function.
*
* Successfully tested against Gmail, Yahoo Mail, Live.com, appmaildev.com
* Hope it helps and saves you plenty of time. Let me know if you find issues.
*
* For more info, you should read :
* @link http://www.ietf.org/rfc/rfc4871.txt
* @link http://www.zytrax.com/books/dns/ch9/dkim.html
*
* @link https://github.com/louisameline/php-mail-signature
* @author Louis Ameline
* @version 1.0.3
*/
/*
* Original PHPMailer CC info :
* .---------------------------------------------------------------------------.
* | Software: PHPMailer - PHP email class |
* | Version: 5.2.1 |
* | Site: https://code.google.com/a/apache-extras.org/p/phpmailer/ |
* | ------------------------------------------------------------------------- |
* | Admin: Jim Jagielski (project admininistrator) |
* | Authors: Andy Prevost (codeworxtech) [email protected] |
* | : Marcus Bointon (coolbru) [email protected] |
* | : Jim Jagielski (jimjag) [email protected] |
* | Founder: Brent R. Matzelle (original founder) |
* | Copyright (c) 2010-2012, Jim Jagielski. All Rights Reserved. |
* | Copyright (c) 2004-2009, Andy Prevost. All Rights Reserved. |
* | Copyright (c) 2001-2003, Brent R. Matzelle |
* | ------------------------------------------------------------------------- |
* | License: Distributed under the Lesser General Public License (LGPL) |
* | http://www.gnu.org/copyleft/lesser.html |
* | This program is distributed in the hope that it will be useful - WITHOUT |
* | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
* | FITNESS FOR A PARTICULAR PURPOSE. |
* '---------------------------------------------------------------------------'
*/
// You can delete this function if you have PHP >= 5.3.0
if (!function_exists('array_replace_recursive'))
{
function recurse($array, $array1)
{
foreach ($array1 as $key => $value)
{
// create new key in $array, if it is empty or not an array
if (!isset($array[$key]) || (isset($array[$key]) && !is_array($array[$key])))
{
$array[$key] = array();
}
// overwrite the value in the base array
if (is_array($value))
{
$value = recurse($array[$key], $value);
}
$array[$key] = $value;
}
return $array;
}
function array_replace_recursive($array, $array1)
{
// handle the arguments, merge one by one
$args = func_get_args();
$array = $args[0];
if (!is_array($array))
{
return $array;
}
for ($i = 1; $i < count($args); $i++)
{
if (is_array($args[$i]))
{
$array = recurse($array, $args[$i]);
}
}
return $array;
}
}
class mail_signature {
private $private_key;
private $domain;
private $selector;
private $options;
private $canonicalized_headers_relaxed;
public function __construct($private_key, $passphrase, $domain, $selector, $options = array()){
// prepare the resource
$this -> private_key = openssl_get_privatekey($private_key, $passphrase);
$this -> domain = $domain;
$this -> selector = $selector;
/*
* This function will not let you ask for the simple header canonicalization because
* it would require more code, it would not be more secure and mails would yet be
* more likely to be rejected : no point in that
*/
$default_options = array(
'use_dkim' => true,
// disabled by default, see why at the top of this file
'use_domainKeys' => false,
/*
* Allowed user, defaults is "@<MAIL_DKIM_DOMAIN>", meaning anybody in the
* MAIL_DKIM_DOMAIN domain. Ex: '[email protected]'. You'll never have to use
* this unless you do not control the "From" value in the e-mails you send.
*/
'identity' => null,
// "relaxed" is recommended over "simple" for better chances of success
'dkim_body_canonicalization' => 'relaxed',
// "nofws" is recommended over "simple" for better chances of success
'dk_canonicalization' => 'nofws',
/*
* The default list of headers types you want to base the signature on. The
* types here (in the default options) are to be put in lower case, but the
* types in $options can have capital letters. If one or more of the headers
* specified are not found in the $headers given to the function, they will
* just not be used.
* If you supply a new list, it will replace the default one
*/
'signed_headers' => array(
'mime-version',
'from',
'to',
'subject',
'reply-to'
)
);
if(isset($options['signed_headers'])){
// lower case fields
foreach($options['signed_headers'] as $key => $value){
$options['signed_headers'][$key] = strtolower($value);
}
// delete the default fields if a custom list is provided, not merge
$default_options['signed_headers'] = array();
}
// PHP >= 5.3.0.
if(function_exists('array_replace_recursive')){
$this -> options = array_replace_recursive($default_options, $options);
}
else {
trigger_error(sprintf('Your PHP version is lower than 5.3.0, please get the "array_replace_recursive" function on the following page (it is in the first comment) and declare it before using this class : http://php.net/manual/fr/function.array-replace-recursive.php'), E_USER_WARNING);
}
}
/**
* This function returns an array of relaxed canonicalized headers (lowercases the
* header type and cleans the new lines/spaces according to the RFC requirements).
* only headers required for signature (specified by $options) will be returned
* the result is an array of the type : array(headerType => fullHeader [, ...]),
* e.g. array('mime-version' => 'mime-version:1.0')
*/
private function _dkim_canonicalize_headers_relaxed($sHeaders){
$aHeaders = array();
// a header value which is spread over several lines must be 1-lined
$sHeaders = preg_replace("/\n\s+/", " ", $sHeaders);
$lines = explode("\r\n", $sHeaders);
foreach($lines as $key => $line){
// delete multiple WSP
$line = preg_replace("/\s+/", ' ', $line);
if(!empty($line)){
// header type to lowercase and delete WSP which are not part of the
// header value
$line = explode(':', $line, 2);
$header_type = trim(strtolower($line[0]));
$header_value = trim($line[1]);
if( in_array($header_type, $this -> options['signed_headers'])
or $header_type == 'dkim-signature'
){
$aHeaders[$header_type] = $header_type.':'.$header_value;
}
}
}
return $aHeaders;
}
/**
* Apply RFC 4871 requirements before body signature. Do not modify
*/
private function _dkim_canonicalize_body_simple($body){
/*
* Unlike other libraries, we do not convert all \n in the body to \r\n here
* because the RFC does not specify to do it here. However it should be done
* anyway since MTA may modify them and we recommend you do this on the mail
* body before calling this DKIM class - or signature could fail.
*/
// remove multiple trailing CRLF
while(mb_substr($body, mb_strlen($body, 'UTF-8')-4, 4, 'UTF-8') == "\r\n\r\n"){
$body = mb_substr($body, 0, mb_strlen($body, 'UTF-8')-2, 'UTF-8');
}
// must end with CRLF anyway
if(mb_substr($body, mb_strlen($body, 'UTF-8')-2, 2, 'UTF-8') != "\r\n"){
$body .= "\r\n";
}
return $body;
}
/**
* Apply RFC 4871 requirements before body signature. Do not modify
*/
private function _dkim_canonicalize_body_relaxed($body){
$lines = explode("\r\n", $body);
foreach($lines as $key => $value){
// ignore WSP at the end of lines
$value = rtrim($value);
// ignore multiple WSP inside the line
$lines[$key] = preg_replace('/\s+/', ' ', $value);
}
$body = implode("\r\n", $lines);
// ignore empty lines at the end
$body = $this -> _dkim_canonicalize_body_simple($body);
return $body;
}
/**
* Apply RFC 4870 requirements before body signature. Do not modify
*/
private function _dk_canonicalize_simple($body, $sHeaders){
/*
* Note : the RFC assumes all lines end with CRLF, and we assume you already
* took care of that before calling the class
*/
// keep only headers wich are in the signature headers
$aHeaders = explode("\r\n", $sHeaders);
foreach($aHeaders as $key => $line){
if(!empty($aHeaders)){
// make sure this line is the line of a new header and not the
// continuation of another one
$c = substr($line, 0, 1);
$is_signed_header = true;
// new header
if(!in_array($c, array("\r", "\n", "\t", ' '))){
$h = explode(':', $line);
$header_type = strtolower(trim($h[0]));
// keep only signature headers
if(in_array($header_type, $this -> options['signed_headers'])){
$is_signed_header = true;
}
else {
unset($aHeaders[$key]);
$is_signed_header = false;
}
}
// continuated header
else {
// do not keep if it belongs to an unwanted header
if($is_signed_header == false){
unset($aHeaders[$key]);
}
}
}
else {
unset($aHeaders[$key]);
}
}
$sHeaders = implode("\r\n", $aHeaders);
$mail = $sHeaders."\r\n\r\n".$body."\r\n";
// remove all trailing CRLF
while(mb_substr($body, mb_strlen($mail, 'UTF-8')-4, 4, 'UTF-8') == "\r\n\r\n"){
$mail = mb_substr($mail, 0, mb_strlen($mail, 'UTF-8')-2, 'UTF-8');
}
return $mail;
}
/**
* Apply RFC 4870 requirements before body signature. Do not modify
*/
private function _dk_canonicalize_nofws($body, $sHeaders){
// HEADERS
// a header value which is spread over several lines must be 1-lined
$sHeaders = preg_replace("/\r\n\s+/", " ", $sHeaders);
$aHeaders = explode("\r\n", $sHeaders);
foreach($aHeaders as $key => $line){
if(!empty($line)){
$h = explode(':', $line);
$header_type = strtolower(trim($h[0]));
// keep only signature headers
if(in_array($header_type, $this -> options['signed_headers'])){
// delete all WSP in each line
$aHeaders[$key] = preg_replace("/\s/", '', $line);
}
else {
unset($aHeaders[$key]);
}
}
else {
unset($aHeaders[$key]);
}
}
$sHeaders = implode("\r\n", $aHeaders);
// BODY
// delete all WSP in each body line
$body_lines = explode("\r\n", $body);
foreach($body_lines as $key => $line){
$body_lines[$key] = preg_replace("/\s/", '', $line);
}
$body = rtrim(implode("\r\n", $body_lines))."\r\n";
return $sHeaders."\r\n\r\n".$body;
}
/**
* The function will return no DKIM header (no signature) if there is a failure,
* so the mail will still be sent in the default unsigned way
* it is highly recommended that all linefeeds in the $body and $headers you submit
* are in the CRLF (\r\n) format !! Otherwise signature may fail with some MTAs
*/
private function _get_dkim_header($body){
$body =
($this -> options['dkim_body_canonicalization'] == 'simple') ?
$this -> _dkim_canonicalize_body_simple($body) :
$this -> _dkim_canonicalize_body_relaxed($body);
// Base64 of packed binary SHA-1 hash of body
$bh = rtrim(chunk_split(base64_encode(pack("H*", sha1($body))), 64, "\r\n\t"));
$i_part =
($this -> options['identity'] == null) ?
'' :
' i='.$this -> options['identity'].';'."\r\n\t";
$dkim_header =
'DKIM-Signature: '.
'v=1;'."\r\n\t".
'a=rsa-sha1;'."\r\n\t".
'q=dns/txt;'."\r\n\t".
's='.$this -> selector.';'."\r\n\t".
't='.time().';'."\r\n\t".
'c=relaxed/'.$this -> options['dkim_body_canonicalization'].';'."\r\n\t".
'h='.implode(':', array_keys($this -> canonicalized_headers_relaxed)).';'."\r\n\t".
'd='.$this -> domain.';'."\r\n\t".
$i_part.
'bh='.$bh.';'."\r\n\t".
'b=';
// now for the signature we need the canonicalized version of the $dkim_header
// we've just made
$canonicalized_dkim_header = $this -> _dkim_canonicalize_headers_relaxed($dkim_header);
// we sign the canonicalized signature headers
$to_be_signed = implode("\r\n", $this -> canonicalized_headers_relaxed)."\r\n".$canonicalized_dkim_header['dkim-signature'];
// $signature is sent by reference in this function
$signature = '';
if(openssl_sign($to_be_signed, $signature, $this -> private_key)){
$dkim_header .= rtrim(chunk_split(base64_encode($signature), 64, "\r\n\t"))."\r\n";
}
else {
trigger_error(sprintf('Could not sign e-mail with DKIM : %s', $to_be_signed), E_USER_WARNING);
$dkim_header = '';
}
return $dkim_header;
}
private function _get_dk_header($body, $sHeaders){
// Creating DomainKey-Signature
$domainkeys_header =
'DomainKey-Signature: '.
'a=rsa-sha1; '."\r\n\t".
'c='.$this -> options['dk_canonicalization'].'; '."\r\n\t".
'd='.$this -> domain.'; '."\r\n\t".
's='.$this -> selector.'; '."\r\n\t".
'h='.implode(':', array_keys($this -> canonicalized_headers_relaxed)).'; '."\r\n\t".
'b=';
// we signed the canonicalized signature headers + the canonicalized body
$to_be_signed =
($this-> options['dk_canonicalization'] == 'simple') ?
$this -> _dk_canonicalize_simple($body, $sHeaders) :
$this -> _dk_canonicalize_nofws($body, $sHeaders);
$signature = '';
if(openssl_sign($to_be_signed, $signature, $this -> private_key, OPENSSL_ALGO_SHA1)){
$domainkeys_header .= rtrim(chunk_split(base64_encode($signature), 64, "\r\n\t"))."\r\n";
}
else {
$domainkeys_header = '';
}
return $domainkeys_header;
}
/**
* You may leave $to and $subject empty if the corresponding headers are already
* in $headers
*/
public function get_signed_headers($to, $subject, $body, $headers){
$signed_headers = '';
if(!empty($to) or !empty($subject)){
/*
* To and Subject are not supposed to be present in $headers if you
* use the php mail() function, because it takes care of that itself in
* parameters for security reasons, so we reconstruct them here for the
* signature only
*/
$headers .=
(mb_substr($headers, mb_strlen($headers, 'UTF-8')-2, 2, 'UTF-8') == "\r\n") ?
'' :
"\r\n";
if(!empty($to)) $headers .= 'To: '.$to."\r\n";
if(!empty($subject)) $headers .= 'Subject: '.$subject."\r\n";
}
// get the clean version of headers used for signature
$this -> canonicalized_headers_relaxed = $this -> _dkim_canonicalize_headers_relaxed($headers);
if(!empty($this -> canonicalized_headers_relaxed)){
// Domain Keys must be the first header, it is an RFC (stupid) requirement
if($this -> options['use_domainKeys'] == true){
$signed_headers .= $this -> _get_dk_header($body, $headers);
}
if($this -> options['use_dkim'] == true){
$signed_headers .= $this -> _get_dkim_header($body);
}
}
else {
trigger_error('No headers found to sign the e-mail with !', E_USER_WARNING);
}
return $signed_headers;
}
}