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
 */

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.
	 */
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
	 */
62
	function onLoad() {
63
		global $app, $conf;
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) {
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);

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
	 */
103
	function check_system($data) {
104
		global $app, $mail_config;
105

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

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);
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);
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
            }

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

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
	 */
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;
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;
	}
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
	}
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);
260
		}
261 262

		return $restart;
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
	 */
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;
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
	 */
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
	 */
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();
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']);
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);
	}
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) {
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') {
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);
			}
401 402
		}
	}
403

404
}
405

406
?>