mail_plugin_dkim.inc.php 14.2 KB
Newer Older
1 2
<?php

Florian Schaal's avatar
Florian Schaal committed
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
 Copyright (c) 2007 - 2013, Till Brehm, projektfarm Gmbh
 Copyright (c) 2013, Florian Schaal, info@schaal-24.de
 All rights reserved.

 Redistribution and use in source and binary forms, with or without modification,
 are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation
 and/or other materials provided with the distribution.
 * Neither the name of ISPConfig nor the names of its contributors
 may be used to endorse or promote products derived from this software without
 specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 @author Florian Schaal, info@schaal-24.de
 @copyrighth Florian Schaal, info@schaal-24.de
 */

tbrehm's avatar
tbrehm committed
35 36 37 38 39 40 41 42 43

class mail_plugin_dkim {

	var $plugin_name = 'mail_plugin_dkim';
	var $class_name = 'mail_plugin_dkim';

	// private variables
	var $action = '';

44 45 46 47
	/**
	 * This function is called during ispconfig installation to determine
	 * if a symlink shall be created for this plugin.
	 */
tbrehm's avatar
tbrehm committed
48 49 50 51 52 53 54 55 56 57 58
	function onInstall() {
		global $conf;

		if($conf['services']['mail'] == true) {
			return true;
		} else {
			return false;
		}

	}

Florian Schaal's avatar
Florian Schaal committed
59
	/**
60 61
	 * This function is called when the plugin is loaded
	 */
tbrehm's avatar
tbrehm committed
62
	function onLoad() {
63
		global $app, $conf;
tbrehm's avatar
tbrehm committed
64 65 66
		/*
		Register for the events
		*/
67 68 69
		$app->plugins->registerEvent('mail_domain_delete', $this->plugin_name, 'domain_dkim_delete');
		$app->plugins->registerEvent('mail_domain_insert', $this->plugin_name, 'domain_dkim_insert');
		$app->plugins->registerEvent('mail_domain_update', $this->plugin_name, 'domain_dkim_update');
70 71
	}

72 73 74 75
	/**
	 * This function gets the amavisd-config file
	 * @return string path to the amavisd-config for dkim-keys
	 */
76 77 78 79
	function get_amavis_config() {
		$pos_config=array(
			'/etc/amavisd.conf',
			'/etc/amavisd.conf/50-user',
80 81
			'/etc/amavis/conf.d/50-user',
			'/etc/amavisd/amavisd.conf'
82 83
		);
		$amavis_configfile='';
84
		foreach($pos_config as $conf) {
tbrehm's avatar
tbrehm committed
85 86 87 88
			if (is_file($conf)) {
				$amavis_configfile=$conf;
				break;
			}
89
		}
90 91 92 93
		//* If we can use seperate config-files with amavis use 60-dkim
		if (substr_compare($amavis_configfile, '50-user', -7) === 0)
			$amavis_configfile = str_replace('50-user', '60-dkim', $amavis_configfile);

tbrehm's avatar
tbrehm committed
94 95 96
		return $amavis_configfile;
	}

Florian Schaal's avatar
Florian Schaal committed
97
	/**
98 99 100 101 102
	 * This function checks the relevant configs and disables dkim for the domain
	 * if the directory for dkim is not writeable or does not exist
	 * @param array $data mail-settings
	 * @return boolean - true when the amavis-config and the dkim-dir are writeable
	 */
tbrehm's avatar
tbrehm committed
103
	function check_system($data) {
104
		global $app, $mail_config;
105

106
		$app->uses('getconf');
tbrehm's avatar
tbrehm committed
107
		$check=true;
108

tbrehm's avatar
tbrehm committed
109
		/* check for amavis-config */
110 111 112 113 114 115 116
		$amavis_configfile = $this->get_amavis_config();

		//* When we can use 60-dkim for the dkim-keys create the file if it does not exists.
		if (substr_compare($amavis_configfile, '60-dkim', -7) === 0 && !file_exists($amavis_configfile))
			file_put_contents($amavis_configfile, '');

		if ( $amavis_configfile == '' || !is_writeable($amavis_configfile) ) {
117
			$app->log('Amavis-config not found or not writeable.', LOGLEVEL_ERROR);
tbrehm's avatar
tbrehm committed
118 119 120
			$check=false;
		}
		/* dir for dkim-keys writeable? */
121
		$mail_config = $app->getconf->get_server_config($conf['server_id'], 'mail');
122 123 124 125 126 127
		if (	isset($mail_config['dkim_path']) && 
				!empty($mail_config['dkim_path']) && 
				isset($data['new']['dkim_private']) && 
				!empty($data['new']['dkim_private']) &&
				$mail_config['dkim_path'] != '/'
		) {
128 129
            if (!is_dir($mail_config['dkim_path'])) {
                $app->log('DKIM Path '.$mail_config['dkim_path'].' not found - (re)created.', LOGLEVEL_DEBUG);
Florian Schaal's avatar
Florian Schaal committed
130 131 132 133 134 135 136 137
				if($app->system->is_user('amavis')) { 
					$amavis_user='amavis'; 
				} elseif ($app->system->is_user('vscan')) { 
					$amavis_user='vscan'; 
				}
				else { 
					$amavis_user=''; 
				}
138 139
				if(!empty($amavis_user)) {
					mkdir($mail_config['dkim_path'], 0750, true);
140
					exec('chown '.$amavis_user.' '.escapeshellarg($mail_config['dkim_path']));
Florian Schaal's avatar
Florian Schaal committed
141
					unset($amavis_user);
142 143
				} else {
					mkdir($mail_config['dkim_path'], 0755, true);
144 145
					$app->log('No user amavis or vscan found - using root for '.$mail_config['dkim_path']
, LOGLEVEL_WARNING);
146
				}
147 148
            }

tbrehm's avatar
tbrehm committed
149
			if (!is_writeable($mail_config['dkim_path'])) {
150
				$app->log('DKIM Path '.$mail_config['dkim_path'].' not writeable.', LOGLEVEL_ERROR);
tbrehm's avatar
tbrehm committed
151
				$check=false;
152
			}
153

tbrehm's avatar
tbrehm committed
154
		} else {
Florian Schaal's avatar
Florian Schaal committed
155
			$app->log('Unable to write DKIM settings - no DKIM-Path defined', LOGLEVEL_ERROR);
156 157 158 159
			$check=false;
		}
		return $check;
	}
160

Florian Schaal's avatar
Florian Schaal committed
161
	/**
162 163
	 * This function restarts amavis
	 */
tbrehm's avatar
tbrehm committed
164
	function restart_amavis() {
165
		global $app, $conf;
166 167 168 169 170 171 172 173 174 175 176 177 178
		$pos_init=array(
			$conf['init_scripts'].'/amavis',
			$conf['init_scripts'].'/amavisd'
		);
		$initfile='';
		foreach($pos_init as $init) {
			if (is_executable($init)) {
				$initfile=$init;
				break;
				}
		}
		$app->log('Restarting amavis: '.$initfile.'.', LOGLEVEL_DEBUG);
		exec(escapeshellarg($initfile).' restart', $output);
179
		foreach($output as $logline) $app->log($logline, LOGLEVEL_DEBUG);
180 181
	}

Florian Schaal's avatar
Florian Schaal committed
182
	/**
183 184 185 186 187 188 189 190
	 * This function writes the keyfiles (public and private)
	 * @param string $key_file full path to the key-file
	 * @param string $key_value private-key
	 * @param string $key_domain mail-domain
	 * @return bool - true when the key is written to disk
	 */
	function write_dkim_key($key_file, $key_value, $key_domain) {
		global $app, $mailconfig;
tbrehm's avatar
tbrehm committed
191
		$success=false;
192 193
		if (!file_put_contents($key_file.'.private', $key_value) === false) {
			$app->log('Saved DKIM Private-key to '.$key_file.'.private', LOGLEVEL_DEBUG);
194 195
			$success=true;
			/* now we get the DKIM Public-key */
Florian Schaal's avatar
Florian Schaal committed
196
			exec('cat '.escapeshellarg($key_file.'.private').'|openssl rsa -pubout 2> /dev/null', $pubkey, $result);
197 198 199
			$public_key='';
			foreach($pubkey as $values) $public_key=$public_key.$values."\n";
			/* save the DKIM Public-key in dkim-dir */
200 201
			if (!file_put_contents($key_file.'.public', $public_key) === false)
				$app->log('Saved DKIM Public to '.$key_domain.'.', LOGLEVEL_DEBUG);
202
			else $app->log('Unable to save DKIM Public to '.$key_domain.'.', LOGLEVEL_DEBUG);
203 204
		} else {
			$app->log('Unable to save DKIM Privte-key to '.$key_file.'.private', LOGLEVEL_ERROR);
205
		}
206 207
		return $success;
	}
tbrehm's avatar
tbrehm committed
208

Florian Schaal's avatar
Florian Schaal committed
209
	/**
210 211 212 213 214
	 * This function removes the keyfiles
	 * @param string $key_file full path to the key-file
	 * @param string $key_domain mail-domain
	 */
	function remove_dkim_key($key_file, $key_domain) {
215 216 217
		global $app;
		if (file_exists($key_file.'.private')) {
			exec('rm -f '.escapeshellarg($key_file.'.private'));
218 219
			$app->log('Deleted the DKIM Private-key for '.$key_domain.'.', LOGLEVEL_DEBUG);
		} else $app->log('Unable to delete the DKIM Private-key for '.$key_domain.' (not found).', LOGLEVEL_DEBUG);
220 221
		if (file_exists($key_file.'.public')) {
			exec('rm -f '.escapeshellarg($key_file.'.public'));
222 223
			$app->log('Deleted the DKIM Public-key for '.$key_domain.'.', LOGLEVEL_DEBUG);
		} else $app->log('Unable to delete the DKIM Public-key for '.$key_domain.' (not found).', LOGLEVEL_DEBUG);
224
	}
tbrehm's avatar
tbrehm committed
225

Florian Schaal's avatar
Florian Schaal committed
226
	/**
227 228 229
	 * This function adds the entry to the amavisd-config
	 * @param string $key_domain mail-domain
	 */
230
	function add_to_amavis($key_domain, $selector, $old_selector) {
231
		global $app, $mail_config;
232

233
		if (empty($selector)) $selector = 'default';
234
		$restart = false;
235 236
		$amavis_configfile = $this->get_amavis_config();

237 238
		$search_regex = "/(\n|\r)?dkim_key\(\'".$key_domain."\',\ \'(".$selector."|".$old_selector."){1}?\'.*/";

239 240 241 242
		//* If we are using seperate config-files with amavis remove existing keys from 50-user to avoid duplicate keys
		if (substr_compare($amavis_configfile, '60-dkim', -7) === 0) {
			$temp_configfile = str_replace('60-dkim', '50-user', $amavis_configfile);
			$temp_config = file_get_contents($temp_configfile);
243 244
			if (preg_match($search_regex, $temp_config)) {
				$temp_config = preg_replace($search_regex, '', $temp_config)."\n";
245 246 247 248 249 250
				file_put_contents($temp_configfile, $temp_config);
			}
			unset($temp_configfile);
			unset($temp_config);
		}

251
		$key_value="dkim_key('".$key_domain."', '".$selector."', '".$mail_config['dkim_path']."/".$key_domain.".private');\n";
252
		$amavis_config = file_get_contents($amavis_configfile);
253
		$amavis_config = preg_replace($search_regex, '', $amavis_config).$key_value;
254

255
		if (file_put_contents($amavis_configfile, $amavis_config)) {
256 257
			$app->log('Adding DKIM Private-key to amavis-config.', LOGLEVEL_DEBUG);
			$restart = true;
258
		} else {
259
			$app->log('Unable to add DKIM Private-key for '.$key_domain.' to amavis-config.', LOGLEVEL_ERROR);
tbrehm's avatar
tbrehm committed
260
		}
261 262

		return $restart;
tbrehm's avatar
tbrehm committed
263 264
	}

Florian Schaal's avatar
Florian Schaal committed
265
	/**
266 267 268
	 * This function removes the entry from the amavisd-config
	 * @param string $key_domain mail-domain
	 */
tbrehm's avatar
tbrehm committed
269 270
	function remove_from_amavis($key_domain) {
		global $app;
271 272

		$restart = false;
273 274
		$amavis_configfile = $this->get_amavis_config();
		$amavis_config = file_get_contents($amavis_configfile);
275

276 277 278 279
		$search_regex = "/(\n|\r)?dkim_key.*".$key_domain.".*(\n|\r)?/";

		if (preg_match($search_regex, $amavis_config)) {
			$amavis_config = preg_replace($search_regex, '', $amavis_config);
280
			file_put_contents($amavis_configfile, $amavis_config);
281
			$app->log('Deleted the DKIM settings from amavis-config for '.$key_domain.'.', LOGLEVEL_DEBUG);
282 283 284
			$restart = true;
		}

285 286 287 288
		//* If we are using seperate config-files with amavis remove existing keys from 50-user, too
		if (substr_compare($amavis_configfile, '60-dkim', -7) === 0) {
			$temp_configfile = str_replace('60-dkim', '50-user', $amavis_configfile);
			$temp_config = file_get_contents($temp_configfile);
289 290
			if (preg_match($search_regex, $temp_config)) {
				$temp_config = preg_replace($search_regex, '', $temp_config);
291 292 293 294 295 296 297
				file_put_contents($temp_configfile, $temp_config);
				$restart = true;
			}
			unset($temp_configfile);
			unset($temp_config);
		}

298
		return $restart;
tbrehm's avatar
tbrehm committed
299 300
	}

Florian Schaal's avatar
Florian Schaal committed
301
	/**
302 303 304
	 * This function controlls new key-files and amavisd-entries
	 * @param array $data mail-settings
	 */
tbrehm's avatar
tbrehm committed
305 306
	function add_dkim($data) {
		global $app;
Florian Schaal's avatar
Florian Schaal committed
307
		if ($data['new']['active'] == 'y') {
308 309 310 311
			$mail_config = $app->getconf->get_server_config($conf['server_id'], 'mail');
			if ( substr($mail_config['dkim_path'], strlen($mail_config['dkim_path'])-1) == '/' )
				$mail_config['dkim_path'] = substr($mail_config['dkim_path'], 0, strlen($mail_config['dkim_path'])-1);
			if ($this->write_dkim_key($mail_config['dkim_path']."/".$data['new']['domain'], $data['new']['dkim_private'], $data['new']['domain'])) {
312
				if ($this->add_to_amavis($data['new']['domain'], $data['new']['dkim_selector'], $data['old']['dkim_selector'] )) {
313 314 315 316
					$this->restart_amavis();
				} else {
					$this->remove_dkim_key($mail_config['dkim_path']."/".$data['new']['domain'], $data['new']['domain']);
				}
Florian Schaal's avatar
Florian Schaal committed
317
			} else {
318
				$app->log('Error saving the DKIM Private-key for '.$data['new']['domain'].' - DKIM is not enabled for the domain.', LOGLEVEL_ERROR);
Florian Schaal's avatar
Florian Schaal committed
319 320
			}
		}
321 322
	}

Florian Schaal's avatar
Florian Schaal committed
323
	/**
324 325 326 327
	 * This function controlls the removement of keyfiles (public and private)
	 * and the entry in the amavisd-config
	 * @param array $data mail-settings
	 */
tbrehm's avatar
tbrehm committed
328 329
	function remove_dkim($_data) {
		global $app;
330 331 332 333
		$mail_config = $app->getconf->get_server_config($conf['server_id'], 'mail');
		if ( substr($mail_config['dkim_path'], strlen($mail_config['dkim_path'])-1) == '/' )
			$mail_config['dkim_path'] = substr($mail_config['dkim_path'], 0, strlen($mail_config['dkim_path'])-1);
		$this->remove_dkim_key($mail_config['dkim_path']."/".$_data['domain'], $_data['domain']);
334 335
		if ($this->remove_from_amavis($_data['domain']))
			$this->restart_amavis();
tbrehm's avatar
tbrehm committed
336 337
	}

Florian Schaal's avatar
Florian Schaal committed
338
	/**
339 340 341 342
	 * Function called by onLoad
	 * deletes dkim-keys
	 */
	function domain_dkim_delete($event_name, $data) {
343 344
		if (isset($data['old']['dkim']) && $data['old']['dkim'] == 'y' && $data['old']['active'] == 'y')
			$this->remove_dkim($data['old']);
tbrehm's avatar
tbrehm committed
345
	}
346

Florian Schaal's avatar
Florian Schaal committed
347
	/**
348 349 350 351
	 * Function called by onLoad
	 * insert dkim-keys
	 */
	function domain_dkim_insert($event_name, $data) {
352
		if (isset($data['new']['dkim']) && $data['new']['dkim']=='y' && $this->check_system($data))
353 354
			$this->add_dkim($data);
	}
tbrehm's avatar
tbrehm committed
355

Florian Schaal's avatar
Florian Schaal committed
356
	/**
357 358 359 360
	 * Function called by onLoad
	 * chang dkim-settings
	 */
	function domain_dkim_update($event_name, $data) {
tbrehm's avatar
tbrehm committed
361
		global $app;
362 363
		if ($this->check_system($data)) {
			/* maildomain disabled */
364
			if ($data['new']['active'] == 'n' && $data['old']['active'] == 'y' && $data['new']['dkim']=='y') {
365
				$app->log('Maildomain '.$data['new']['domain'].' disabled - remove DKIM-settings', LOGLEVEL_DEBUG);
366
				$this->remove_dkim($data['new']);
367 368
			}
			/* maildomain re-enabled */
369 370
			if ($data['new']['active'] == 'y' && $data['old']['active'] == 'n' && $data['new']['dkim']=='y') 
				$this->add_dkim($data);
371 372 373 374 375 376 377 378 379

			/* maildomain active - only dkim changes */
			if ($data['new']['active'] == 'y' && $data['old']['active'] == 'y') {
				/* dkim disabled */
				if ($data['new']['dkim'] != $data['old']['dkim'] && $data['new']['dkim'] == 'n') {
					$this->remove_dkim($data['new']);
				}
				/* dkim enabled */
				elseif ($data['new']['dkim'] != $data['old']['dkim'] && $data['new']['dkim'] == 'y') {
tbrehm's avatar
tbrehm committed
380 381
					$this->add_dkim($data);
				}
382 383 384 385
				/* new private-key */
				if ($data['new']['dkim_private'] != $data['old']['dkim_private'] && $data['new']['dkim'] == 'y') {
					$this->add_dkim($data);
				}
386 387 388 389
				/* new selector */
				if ($data['new']['dkim_selector'] != $data['old']['dkim_selector'] && $data['new']['dkim'] == 'y') {
					$this->add_dkim($data);
				}
390 391
				/* new domain-name */
				if ($data['new']['domain'] != $data['old']['domain']) {
392 393
					$this->remove_dkim($data['old']);
					$this->add_dkim($data);
394 395
				}
			}
396 397 398 399 400

			/* resync */
			if ($data['new']['active'] == 'y' && $data['new'] == $data['old']) {
				$this->add_dkim($data);
			}
tbrehm's avatar
tbrehm committed
401 402
		}
	}
403

tbrehm's avatar
tbrehm committed
404
}
405

tbrehm's avatar
tbrehm committed
406
?>