mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-27 09:09:04 +01:00
587 lines
19 KiB
PHP
587 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2011 Ahmad Amarullah
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
|
|
/**
|
|
* mailDomainSigner - PHP class for Add DKIM-Signature and DomainKey-Signature on your mail
|
|
*
|
|
* Requirement: >= PHP 5.3.0
|
|
* For lower version, find "#^--for ver < PHP5.3" comment
|
|
*
|
|
* @link http://code.google.com/p/php-mail-domain-signer/
|
|
*
|
|
* @package mailDomainSigner
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
class mailDomainSigner{
|
|
|
|
///////////////////////
|
|
// PRIVATE VARIABLES //
|
|
///////////////////////
|
|
private $pkid=null;
|
|
private $s;
|
|
private $d;
|
|
|
|
//////////////////////
|
|
// AGENT PROPERTIES //
|
|
//////////////////////
|
|
private $__app_name = "PHP mailDomainSigner";
|
|
private $__app_ver = "0.1-20110129";
|
|
private $__app_url = "http://code.google.com/p/php-mail-domain-signer/";
|
|
|
|
/**
|
|
* Constructor
|
|
* @param string $private_key Raw Private Key to Sign the mail
|
|
* @param string $d The domain name of the signing domain
|
|
* @param string $s The selector used to form the query for the public key
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function __construct($private_key,$d,$s){
|
|
// Get a private key
|
|
$this->pkid = openssl_pkey_get_private($private_key);
|
|
|
|
// Save Domain and Selector
|
|
$this->d = $d;
|
|
$this->s = $s;
|
|
}
|
|
|
|
///////////////////////
|
|
// PRIVATE FUNCTIONS //
|
|
///////////////////////
|
|
|
|
/**
|
|
* The nofws ("No Folding Whitespace") Canonicalization Algorithm
|
|
* Function implementation according to RFC4870
|
|
*
|
|
* @link http://tools.ietf.org/html/rfc4870#page-19
|
|
* @param array $raw_headers Array of Mail Headers
|
|
* @param string $raw_body Raw Mail body
|
|
* @return string nofws Canonicalizated data
|
|
* @access public
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function nofws($raw_headers,$raw_body){
|
|
// nofws-ed headers
|
|
$headers = array();
|
|
|
|
// Loop the raw_headers
|
|
foreach ($raw_headers as $header){
|
|
// Replace all Folding Whitespace
|
|
$headers[] = preg_replace('/[\r\t\n ]++/','',$header);
|
|
}
|
|
|
|
// Join headers with LF then Add it into data
|
|
$data = implode("\n",$headers)."\n";
|
|
|
|
// Loop Body Lines
|
|
foreach(explode("\n","\n".str_replace("\r","",$raw_body)) as $line)
|
|
{
|
|
// Replace all Folding Whitespace from current line
|
|
// then Add it into data
|
|
$data .= preg_replace('/[\t\n ]++/','',$line)."\n";
|
|
}
|
|
|
|
// Remove Trailing empty lines then split it with LF
|
|
$data = explode("\n",rtrim($data,"\n"));
|
|
|
|
// Join array of data with CRLF and Append CRLF
|
|
// to the resulting line
|
|
$data = implode("\r\n",$data)."\r\n";
|
|
|
|
// Return Canonicalizated Data
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* The "relaxed" Header Canonicalization Algorithm
|
|
* Function implementation according to RFC4871
|
|
*
|
|
* Originally taken from RelaxedHeaderCanonicalization
|
|
* function in PHP-DKIM by Eric Vyncke
|
|
*
|
|
* @link http://tools.ietf.org/html/rfc4871#page-14
|
|
* @link http://php-dkim.sourceforge.net/
|
|
*
|
|
* @param string $s Header String to Canonicalization
|
|
* @return string Relaxed Header Canonicalizated data
|
|
* @access public
|
|
* @author Eric Vyncke
|
|
*/
|
|
public function headRelaxCanon($s) {
|
|
// Replace CR,LF and spaces into single SP
|
|
$s=preg_replace("/\r\n\s+/"," ",$s) ;
|
|
|
|
// Explode Header Line
|
|
$lines=explode("\r\n",$s) ;
|
|
|
|
// Loop the lines
|
|
foreach ($lines as $key=>$line) {
|
|
// Split the key and value
|
|
list($heading,$value)=explode(":",$line,2) ;
|
|
|
|
// Lowercase heading key
|
|
$heading=strtolower($heading);
|
|
|
|
// Compress useless spaces
|
|
$value=preg_replace("/\s+/"," ",$value);
|
|
|
|
// Don't forget to remove WSP around the value
|
|
$lines[$key]=$heading.":".trim($value);
|
|
}
|
|
|
|
// Implode it again
|
|
$s=implode("\r\n",$lines);
|
|
|
|
// Return Canonicalizated Headers
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* The "ischedule-relaxed" Header Canonicalization Algorithm
|
|
* Function implementation according to draft-desruisseaux-ischedule-03:
|
|
*
|
|
* The "ischedule-relaxed" header canonicalization algorithm is used to
|
|
* canonicalize HTTP header fields where multiple headers fields with
|
|
* the same name might be combined by an HTTP intermediary
|
|
* into a single comma-separated header.
|
|
*
|
|
* Originally taken from headRelaxCanon above
|
|
*
|
|
* @param string $s Header String to Canonicalization
|
|
* @return string Relaxed Header Canonicalizated data
|
|
* @access public
|
|
* @author Ralf Becker
|
|
*/
|
|
public function headIScheduleRelaxCanon($s) {
|
|
// Replace CR,LF and spaces into single SP
|
|
$s=preg_replace("/\r\n\s+/"," ",$s) ;
|
|
|
|
// Loop exploded header lines
|
|
$lines=array();
|
|
foreach (explode("\r\n",$s) as $line) {
|
|
// Split the key and value
|
|
list($heading,$value)=explode(":",$line,2) ;
|
|
|
|
// Lowercase heading key
|
|
$heading=strtolower($heading);
|
|
|
|
// Compress useless spaces
|
|
$value=preg_replace("/\s+/"," ",$value);
|
|
|
|
// Don't forget to remove WSP around the value
|
|
$value = trim($value);
|
|
|
|
// remove whitespace after comma in values
|
|
$value = preg_replace("/,\s+/",",",$value);
|
|
|
|
// for multiple headers, add them comma-separated to existing headers
|
|
if (isset($lines[$heading])) {
|
|
$lines[$heading] .= ','.$value;
|
|
} else {
|
|
$lines[$heading] = $heading.':'.$value;
|
|
}
|
|
}
|
|
|
|
// Implode it again
|
|
$s=implode("\r\n",$lines);
|
|
|
|
// Return Canonicalizated Headers
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* The "simple" Header Canonicalization Algorithm
|
|
*
|
|
* Simple canonicalzation means no change to headers!
|
|
*
|
|
* @link https://tools.ietf.org/html/rfc4871#section-3.4.1
|
|
*
|
|
* @param string $s Header String to Canonicalization
|
|
* @return string Simple Header Canonicalizated data
|
|
* @access public
|
|
*/
|
|
public function headSimpleCanon($s) {
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* The "relaxed" Body Canonicalization Algorithm
|
|
* Function implementation according to RFC4871
|
|
*
|
|
* @link http://tools.ietf.org/html/rfc4871#page-15
|
|
*
|
|
* @param string $body Body String to Canonicalization
|
|
* @return string Relaxed Body Canonicalizated data
|
|
* @access public
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function bodyRelaxCanon($body) {
|
|
// Return CRLF for empty body
|
|
if ($body == ''){
|
|
return "\r\n";
|
|
}
|
|
|
|
// Replace all CRLF to LF
|
|
$body = str_replace("\r\n","\n",$body);
|
|
|
|
// Replace LF to CRLF
|
|
$body = str_replace("\n","\r\n",$body);
|
|
|
|
// Ignores all whitespace at the end of lines
|
|
$body=rtrim($body,"\r\n");
|
|
|
|
// Canonicalizated String Variable
|
|
$canon_body = '';
|
|
|
|
// Split the body into lines
|
|
foreach(explode("\r\n",$body) as $line){
|
|
// Reduces all sequences of White Space within a line
|
|
// to a single SP character
|
|
$canon_body.= rtrim(preg_replace('/[\t\n ]++/',' ',$line))."\r\n";
|
|
}
|
|
|
|
// Return the Canonicalizated Body
|
|
return $canon_body;
|
|
}
|
|
|
|
/**
|
|
* The "simple" Body Canonicalization Algorithm
|
|
* Function implementation according to RFC4871
|
|
*
|
|
* @link https://tools.ietf.org/html/rfc6376#section-3.4.3
|
|
*
|
|
* @param string $body Body String to Canonicalization
|
|
* @return string Simple Body Canonicalizated data
|
|
* @access public
|
|
*/
|
|
public function bodySimpleCanon($body) {
|
|
// remove all empty lines (CRLF pairs) at the end of the body
|
|
while (substr($body, -2) === "\r\n") {
|
|
$body = substr($body, 0, -2);
|
|
}
|
|
|
|
// add a single CRLF at the end
|
|
$body .= "\r\n";
|
|
|
|
// Return the Canonicalizated Body
|
|
return $body;
|
|
}
|
|
|
|
//////////////////////
|
|
// PUBLIC FUNCTIONS //
|
|
//////////////////////
|
|
|
|
/**
|
|
* DKIM-Signature Header Creator Function
|
|
* implementation according to RFC4871
|
|
*
|
|
* Originally code inspired by AddDKIM
|
|
* function in PHP-DKIM by Eric Vyncke
|
|
* And rewrite it for better result
|
|
*
|
|
* The function use relaxed/relaxed canonicalization alghoritm
|
|
* for better verifing validation
|
|
*
|
|
* different from original PHP-DKIM that used relaxed/simple
|
|
* canonicalization alghoritm
|
|
*
|
|
* Doesn't include z, i and q tag for smaller data because
|
|
* it doesn't really needed
|
|
*
|
|
* @link http://tools.ietf.org/html/rfc4871
|
|
* @link http://php-dkim.sourceforge.net/
|
|
*
|
|
* @param string $h Signed header fields, A colon-separated list of header field names that identify the header fields presented to the signing algorithm
|
|
* @param array $_h Array of headers in same order with $h (Signed header fields)
|
|
* @param string $body Raw Email Body String
|
|
* @param string $_c='relaxed/relaxed' header/body canonicalzation algorithm, default "relaxed/relaxed", can also be any combination of "relaxed" or "simple"
|
|
* @param string $_a='rsa-sha1' could also be eg. 'rsa/sha256' and other hashes supported by openssl for rsa signing
|
|
* @param string $_dkim=null template for creating DKIM signature, default as for email appropriate
|
|
* @return string|boolean DKIM-Signature Header String, or false if an error happend, eg. unimplemented canonicalization requested
|
|
* @access public
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function getDKIM($h,$_h,$body,$_c='relaxed/relaxed',$_a='rsa-sha1',$_dkim=null) {
|
|
|
|
// Relax Canonicalization for Body
|
|
list($header_canon,$body_canon) = explode('/',$_c);
|
|
switch($body_canon)
|
|
{
|
|
case 'relaxed':
|
|
$_b = $this->bodyRelaxCanon($body);
|
|
break;
|
|
|
|
case 'simple':
|
|
$_b = $this->bodySimpleCanon($body);
|
|
break;
|
|
|
|
default:
|
|
return false; // unknown/unimplemented canonicalzation algorithm
|
|
}
|
|
|
|
// Canonicalizated Body Length [tag:l]
|
|
// use mb_strlen if availble to get length in bytes/octets, to kope with evtl. set mbstring.func_overload!
|
|
$_l = function_exists('mb_strlen') ? mb_strlen($_b, '8bit') : strlen($_b);
|
|
|
|
// Signature Timestamp [tag:t]
|
|
$_t = time();
|
|
|
|
// Hash of the canonicalized body [tag:bh]
|
|
list(,$hash_algo) = explode('-', $_a);
|
|
$_bh = base64_encode(hash($hash_algo,$_b,true));
|
|
|
|
// Creating DKIM-Signature
|
|
if (is_null($_dkim)) {
|
|
$_dkim = "DKIM-Signature: ".
|
|
"v=1; ". // DKIM Version
|
|
"a=\$a; ". // The algorithm used to generate the signature "rsa-sha1"
|
|
"s=\$s; ". // The selector subdividing the namespace for the "d=" (domain) tag
|
|
"d=\$d; ". // The domain of the signing entity
|
|
"l=\$l; ". // Canonicalizated Body length count
|
|
"t=\$t; ". // Signature Timestamp
|
|
"c=\$c; ". // Message (Headers/Body) Canonicalization "relaxed/relaxed"
|
|
"h=\$h; ". // Signed header fields
|
|
"bh=\$bh;\r\n\t". // The hash of the canonicalized body part of the message
|
|
"b="; // The signature data (Empty because we will calculate it later)
|
|
}
|
|
$_dkim = strtr($_dkim, array(
|
|
'$a' => $_a,
|
|
'$s' => $this->s,
|
|
'$d' => $this->d,
|
|
'$l' => $_l,
|
|
'$t' => $_t,
|
|
'$c' => $_c,
|
|
'$h' => $h,
|
|
'$bh' => $_bh,
|
|
));
|
|
// Wrap DKIM Header
|
|
$_dkim = wordwrap($_dkim,76,"\r\n\t");
|
|
|
|
// Canonicalization Header Data
|
|
switch($header_canon)
|
|
{
|
|
case 'relaxed':
|
|
$_unsigned = $this->headRelaxCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
|
|
break;
|
|
|
|
case 'ischedule-relaxed':
|
|
$_unsigned = $this->headIScheduleRelaxCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
|
|
break;
|
|
|
|
case 'simple':
|
|
$_unsigned = $this->headSimpleCanon(implode("\r\n",$_h)."\r\n{$_dkim}");
|
|
break;
|
|
|
|
default:
|
|
return false; // unknown/unimplemented canonicalzation algorithm
|
|
}
|
|
error_log(__METHOD__."() unsigned='".str_replace(array("\r","\n"),array('\\r','\\n'),$_unsigned)."'");
|
|
|
|
// Sign Canonicalization Header Data with Private Key
|
|
openssl_sign($_unsigned, $_signed, $this->pkid, $hash_algo);
|
|
|
|
// Base64 encoded signed data
|
|
// Chunk Split it
|
|
// Then Append it $_dkim
|
|
$_dkim .= chunk_split(base64_encode($_signed),76,"\r\n\t");
|
|
|
|
// Return trimmed $_dkim
|
|
return trim($_dkim);
|
|
}
|
|
|
|
/**
|
|
* DomainKey-Signature Header Creator Function
|
|
* implementation according to RFC4870
|
|
*
|
|
* The function use nofws canonicalization alghoritm
|
|
* for better verifing validation
|
|
*
|
|
* NOTE: the $h and $_h arguments must be in right order
|
|
* if to header location upper the from header
|
|
* it should ordered like "to:from", don't randomize
|
|
* the order for better validating result.
|
|
*
|
|
* NOTE: if your DNS TXT contained g=*, remove it
|
|
*
|
|
* @link http://tools.ietf.org/html/rfc4870
|
|
*
|
|
* @param string $h Signed header fields, A colon-separated list of header field names that identify the header fields presented to the signing algorithm
|
|
* @param array $_h Array of headers in same order with $h (Signed header fields)
|
|
* @param string $body Raw Email Body String
|
|
* @return string DomainKey-Signature Header String
|
|
* @access public
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function getDomainKey($h,$_h,$body){
|
|
// If $h = empty, dont add h tag into DomainKey-Signature
|
|
$hval = '';
|
|
if ($h)
|
|
$hval= "h={$h}; ";
|
|
|
|
// Creating DomainKey-Signature
|
|
$_dk = "DomainKey-Signature: ".
|
|
"a=rsa-sha1; ". // The algorithm used to generate the signature "rsa-sha1"
|
|
"c=nofws; ". // Canonicalization Alghoritm "nofws"
|
|
"d={$this->d}; ". // The domain of the signing entity
|
|
"s={$this->s}; ". // The selector subdividing the namespace for the "d=" (domain) tag
|
|
"{$hval}"; // If Exists - Signed header fields
|
|
|
|
// nofws Canonicalization for headers and body data
|
|
$_unsigned = $this->nofws($_h,$body);
|
|
|
|
// Sign nofws Canonicalizated Data with Private Key
|
|
openssl_sign($_unsigned, $_signed, $this->pkid, OPENSSL_ALGO_SHA1);
|
|
|
|
// Base64 encoded signed data
|
|
// Chunk Split it
|
|
$b = chunk_split(base64_encode($_signed),76,"\r\n\t");
|
|
|
|
// Append sign data into b tag in $_dk
|
|
$_dk.="b={$b}";
|
|
|
|
// Return Wrapped and trimmed $_dk
|
|
return trim(wordwrap($_dk,76,"\r\n\t"));
|
|
}
|
|
|
|
/**
|
|
* Auto Sign RAW Mail Data with DKIM-Signature
|
|
* and DomailKey-Signature
|
|
*
|
|
* It Support auto positioning Signed header fields
|
|
*
|
|
* @param string $mail_data Raw Mail Data to be signed
|
|
* @param string $suggested_h Suggested Signed Header Fields, separated by colon ":"
|
|
* Default: string("from:to:subject")
|
|
* @param bool $create_dkim If true, it will generate DKIM-Signature for $mail_data
|
|
* Default: boolean(true)
|
|
* @param bool $create_domainkey If true, it will generate DomailKey-Signature for $mail_data
|
|
* Default: boolean(true)
|
|
* @param integer $out_sign_header_only If true or 1, it will only return signature headers as String
|
|
* If 2, it will only return signature headers as Array
|
|
* If false or 0, it will return signature headers with original mail data as String
|
|
* Default: boolean(false)
|
|
* @return mixed Signature Headers with/without original data as String/Array depended on $out_sign_header_only parameter
|
|
* @access public
|
|
* @author Ahmad Amarullah
|
|
*/
|
|
public function sign(
|
|
$mail_data, // Raw Mail Data
|
|
$suggested_h = "from:to:subject", // Suggested Signed Header Fields
|
|
$create_dkim = true, // Create DKIM-Signature Header
|
|
$create_domainkey = true, // Create DomainKey-Signature Header
|
|
$out_sign_header_only = false // Return Signature Header Only without original data
|
|
){
|
|
|
|
if (!$suggested_h) $suggested_h = "from:to:subject"; // Default Suggested Signed Header Fields
|
|
|
|
// Remove all space and Lowercase Suggested Signed header fields then split it into array
|
|
$_h = explode(":",strtolower(preg_replace('/[\r\t\n ]++/','',$suggested_h)));
|
|
|
|
// Split Raw Mail data into $raw_headers and $body
|
|
list($raw_headers, $body) = explode("\r\n\r\n",$mail_data,2);
|
|
|
|
// Explode $raw_header into $header_list
|
|
$header_list = preg_split("/\r\n(?![\t ])/", $raw_headers);
|
|
|
|
// Empty Header Array
|
|
$headers = array();
|
|
|
|
// Loop $header_list
|
|
foreach($header_list as $header){
|
|
// Find Header Key for Array Key
|
|
list($key) = explode(':',$header, 2);
|
|
|
|
// Trim and Lowercase It
|
|
$key = strtolower(trim($key));
|
|
|
|
// If header with current key was exists
|
|
// Change it into array
|
|
if (isset($headers[$key])){
|
|
// If header not yet array set as Array
|
|
if (!is_array($headers[$key]))
|
|
$headers[$key] = array($headers[$key]);
|
|
|
|
// Add Current Header as next element
|
|
$headers[$key][] = $header;
|
|
}
|
|
|
|
// If header with current key not exists
|
|
// Insert header as string
|
|
else{
|
|
$headers[$key] = $header;
|
|
}
|
|
}
|
|
|
|
// Now, lets find accepted Suggested Signed header fields
|
|
// and reorder it to match headers position
|
|
|
|
$accepted_h = array(); // For Accepted Signed header fields
|
|
$accepted_headers = array(); // For Accepted Header
|
|
|
|
// Loop the Headers Array
|
|
foreach ($headers as $key=>$val){
|
|
// Check if $val wasn't array
|
|
// We don't want to include multiple headers as Signed header fields
|
|
if (!is_array($val)){
|
|
// Check if this header exists in Suggested Signed header fields
|
|
if (in_array($key,$_h)){
|
|
// If Exists, add it into accepted headers and accepted header fields
|
|
$accepted_h[] = $key;
|
|
$accepted_headers[] = $val;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If it doesn't contain any $accepted_h
|
|
// return false, because we don't have enough data
|
|
// for signing email
|
|
if (count($accepted_h)==0)
|
|
return false;
|
|
|
|
// Create $_hdata for Signed header fields
|
|
// by imploding it with colon
|
|
$_hdata = implode(":",$accepted_h);
|
|
|
|
// New Headers Variable
|
|
$_nh = array("x-domain-signer"=>"X-Domain-Signer: {$this->__app_name} {$this->__app_ver} <$this->__app_url>");
|
|
|
|
// Create DKIM First
|
|
if ($create_dkim)
|
|
$_nh['dkim-signature'] = $this->getDKIM($_hdata,$accepted_headers,$body);
|
|
|
|
// Now Create Domain-Signature
|
|
if ($create_domainkey)
|
|
$_nh['domainKey-signature'] = $this->getDomainKey($_hdata,$accepted_headers,$body);
|
|
|
|
// Implode $_nh with \r\n
|
|
$to_be_appended_headers = implode("\r\n",$_nh);
|
|
|
|
// Return Immediately if
|
|
// * $out_sign_header_only=true (as headers string)
|
|
// * $out_sign_header_only=2 (as headers array)
|
|
if ($out_sign_header_only===2)
|
|
return $_nh;
|
|
elseif ($out_sign_header_only)
|
|
return "{$to_be_appended_headers}\r\n";
|
|
|
|
// Return signed headers with original data
|
|
return "{$to_be_appended_headers}\r\n{$mail_data}";
|
|
}
|
|
}
|
|
|
|
?>
|