mail_plugin_dkim.inc.php 15.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
	function get_amavis_config() {
		$pos_config=array(
			'/etc/amavisd.conf/50-user',
79
			'/etc/amavis/conf.d/50-user',
80
			'/etc/amavisd.conf',
81
			'/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
		$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))
114
			$app->system->touch($amavis_configfile);
115
116

		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
			$check=false;
		}
120

tbrehm's avatar
tbrehm committed
121
		/* dir for dkim-keys writeable? */
122
		$mail_config = $app->getconf->get_server_config($conf['server_id'], 'mail');
123
124
		if (	isset($mail_config['dkim_path']) && 
				!empty($mail_config['dkim_path']) && 
125
126
//				isset($data['new']['dkim_private']) && 
//				!empty($data['new']['dkim_private']) &&
127
				$mail_config['dkim_path'] != '/' 
128
		) {
129
130
            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
131
132
133
134
135
136
137
138
				if($app->system->is_user('amavis')) { 
					$amavis_user='amavis'; 
				} elseif ($app->system->is_user('vscan')) { 
					$amavis_user='vscan'; 
				}
				else { 
					$amavis_user=''; 
				}
139
140
				if(!empty($amavis_user)) {
					mkdir($mail_config['dkim_path'], 0750, true);
Florian Schaal's avatar
Florian Schaal committed
141
					$app->system->chown($mail_config['dkim_path'], $amavis_user);
142
143
				} else {
					mkdir($mail_config['dkim_path'], 0755, true);
144
					$app->log('No user amavis or vscan found - using root for '.$mail_config['dkim_path'], LOGLEVEL_WARNING);
145
				}
146
147
148
149
150
151
            } else {
				if (!$app->system->checkpath($mail_config['dkim_path'])) {
					$app->log('Unable to write DKIM settings - invalid DKIM-Path (symlink?)', LOGLEVEL_ERROR);
					$check=false;
				}
			}
152

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

158
159
160
161
162
			if ( !$app->system->checkpath($mail_config['dkim_path']) ) {
				$app->log('DKIM Path '.$mail_config['dkim_path'].' failed in checkpath.', LOGLEVEL_ERROR);
				$check = false;
			}

tbrehm's avatar
tbrehm committed
163
		} else {
Florian Schaal's avatar
Florian Schaal committed
164
			$app->log('Unable to write DKIM settings - no or invalid DKIM-Path defined', LOGLEVEL_ERROR);
165
166
167
168
			$check=false;
		}
		return $check;
	}
169

Florian Schaal's avatar
Florian Schaal committed
170
	/**
171
172
	 * This function restarts amavis
	 */
Florian Schaal's avatar
Florian Schaal committed
173
174
175
176
177
178
179
180
181
182
183
184
185
    function restart_amavis() {
        global $app, $conf;
        $pos_init=array(
            $conf['init_scripts'].'/amavis',
            $conf['init_scripts'].'/amavisd'
        );
        $initfile='';
        foreach($pos_init as $init) {
            if (is_executable($init)) {
                $initfile=$init;
                break;
                }
        }
Florian Schaal's avatar
Florian Schaal committed
186
		if ( $initfile == '' ) $initfile = 'service amavis';
Florian Schaal's avatar
Florian Schaal committed
187
188
189
190
        $app->log('Restarting amavis: '.$initfile.'.', LOGLEVEL_DEBUG);
        exec(escapeshellarg($initfile).' restart', $output);
        foreach($output as $logline) $app->log($logline, LOGLEVEL_DEBUG);
    }
191

Florian Schaal's avatar
Florian Schaal committed
192
	/**
193
194
195
196
	 * 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
197
	 * @return bool - true when the private key was written to disk
198
199
200
	 */
	function write_dkim_key($key_file, $key_value, $key_domain) {
		global $app, $mailconfig;
tbrehm's avatar
tbrehm committed
201
		$success=false;
202
203
204
205
		if ($key_file == '' || $key_value  == '' || $key_domain == '') {
			$app->log('DKIM internal error for domain '.$key_domain, LOGLEVEL_ERROR);
			return $success;
		}
Florian Schaal's avatar
Florian Schaal committed
206
		if ( $app->system->file_put_contents($key_file.'.private', $key_value) ) {
207
			$app->log('Saved DKIM Private-key to '.$key_file.'.private', LOGLEVEL_DEBUG);
208
209
			$success=true;
			/* now we get the DKIM Public-key */
Florian Schaal's avatar
Florian Schaal committed
210
			exec('cat '.escapeshellarg($key_file.'.private').'|openssl rsa -pubout 2> /dev/null', $pubkey, $result);
211
212
213
			$public_key='';
			foreach($pubkey as $values) $public_key=$public_key.$values."\n";
			/* save the DKIM Public-key in dkim-dir */
Florian Schaal's avatar
Florian Schaal committed
214
			if ( $app->system->file_put_contents($key_file.'.public', $public_key) )
215
				$app->log('Saved DKIM Public to '.$key_domain.'.', LOGLEVEL_DEBUG);
216
			else $app->log('Unable to save DKIM Public to '.$key_domain.'.', LOGLEVEL_DEBUG);
217
		} else {
218
			$app->log('Unable to save DKIM Private-key to '.$key_file.'.private', LOGLEVEL_ERROR);
219
		}
220
221
		return $success;
	}
tbrehm's avatar
tbrehm committed
222

Florian Schaal's avatar
Florian Schaal committed
223
	/**
224
225
226
227
228
	 * 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) {
229
230
		global $app;
		if (file_exists($key_file.'.private')) {
Florian Schaal's avatar
Florian Schaal committed
231
			$app->system->unlink($key_file.'.private');
232
233
			$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);
234
		if (file_exists($key_file.'.public')) {
Florian Schaal's avatar
Florian Schaal committed
235
			$app->system->unlink($key_file.'.public');
236
237
			$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);
238
	}
tbrehm's avatar
tbrehm committed
239

Florian Schaal's avatar
Florian Schaal committed
240
	/**
241
242
243
	 * This function adds the entry to the amavisd-config
	 * @param string $key_domain mail-domain
	 */
244
	function add_to_amavis($key_domain, $selector, $old_selector) {
245
		global $app, $mail_config;
246

247
		if (empty($selector)) $selector = 'default';
248
		$restart = false;
249
250
		$amavis_configfile = $this->get_amavis_config();

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

253
254
255
		//* 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);
256
			$temp_config = $app->system->file_get_contents($temp_configfile, true);
257
258
			if (preg_match($search_regex, $temp_config)) {
				$temp_config = preg_replace($search_regex, '', $temp_config)."\n";
259
				$app->system->file_put_contents($temp_configfile, $temp_config, true);
260
261
262
263
264
			}
			unset($temp_configfile);
			unset($temp_config);
		}

265
		$key_value="dkim_key('".$key_domain."', '".$selector."', '".$mail_config['dkim_path']."/".$key_domain.".private');\n";
266
		$amavis_config = $app->system->file_get_contents($amavis_configfile, true);
267
		$amavis_config = preg_replace($search_regex, '', $amavis_config).$key_value;
268

269
		if ( $app->system->file_put_contents($amavis_configfile, $amavis_config, true) ) {
270
271
			$app->log('Adding DKIM Private-key to amavis-config.', LOGLEVEL_DEBUG);
			$restart = true;
272
		} else {
273
			$app->log('Unable to add DKIM Private-key for '.$key_domain.' to amavis-config.', LOGLEVEL_ERROR);
tbrehm's avatar
tbrehm committed
274
		}
275
276

		return $restart;
tbrehm's avatar
tbrehm committed
277
278
	}

Florian Schaal's avatar
Florian Schaal committed
279
	/**
280
281
282
	 * This function removes the entry from the amavisd-config
	 * @param string $key_domain mail-domain
	 */
tbrehm's avatar
tbrehm committed
283
284
	function remove_from_amavis($key_domain) {
		global $app;
285
286

		$restart = false;
287
		$amavis_configfile = $this->get_amavis_config();
288
		$amavis_config = $app->system->file_get_contents($amavis_configfile, true);
289

290
291
292
293
		$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);
294
			$app->system->file_put_contents($amavis_configfile, $amavis_config, true);
295
			$app->log('Deleted the DKIM settings from amavis-config for '.$key_domain.'.', LOGLEVEL_DEBUG);
296
297
298
			$restart = true;
		}

299
300
301
		//* 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);
302
			$temp_config = $app->system->file_get_contents($temp_configfile, true);
303
304
			if (preg_match($search_regex, $temp_config)) {
				$temp_config = preg_replace($search_regex, '', $temp_config);
305
				$app->system->file_put_contents($temp_configfile, $temp_config, true);
306
307
308
309
310
311
				$restart = true;
			}
			unset($temp_configfile);
			unset($temp_config);
		}

312
		return $restart;
tbrehm's avatar
tbrehm committed
313
314
	}

Florian Schaal's avatar
Florian Schaal committed
315
	/**
316
317
318
	 * This function controlls new key-files and amavisd-entries
	 * @param array $data mail-settings
	 */
tbrehm's avatar
tbrehm committed
319
320
	function add_dkim($data) {
		global $app;
Florian Schaal's avatar
Florian Schaal committed
321
		if ($data['new']['active'] == 'y') {
322
323
324
325
			$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'])) {
326
				if ($this->add_to_amavis($data['new']['domain'], $data['new']['dkim_selector'], $data['old']['dkim_selector'] )) {
327
328
329
330
					$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
331
			} else {
Marius Cramer's avatar
Marius Cramer committed
332
				$app->log('Error saving the DKIM Private-key for '.$data['new']['domain'].' - DKIM is not enabled for the domain.', LOGLEVEL_DEBUG);
Florian Schaal's avatar
Florian Schaal committed
333
334
			}
		}
335
336
	}

Florian Schaal's avatar
Florian Schaal committed
337
	/**
338
339
340
341
	 * 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
342
343
	function remove_dkim($_data) {
		global $app;
344
345
346
347
		$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']);
348
349
		if ($this->remove_from_amavis($_data['domain']))
			$this->restart_amavis();
tbrehm's avatar
tbrehm committed
350
351
	}

Florian Schaal's avatar
Florian Schaal committed
352
	/**
353
354
355
356
	 * Function called by onLoad
	 * deletes dkim-keys
	 */
	function domain_dkim_delete($event_name, $data) {
357
358
		if (isset($data['old']['dkim']) && $data['old']['dkim'] == 'y' && $data['old']['active'] == 'y')
			$this->remove_dkim($data['old']);
tbrehm's avatar
tbrehm committed
359
	}
360

Florian Schaal's avatar
Florian Schaal committed
361
	/**
362
363
364
365
	 * Function called by onLoad
	 * insert dkim-keys
	 */
	function domain_dkim_insert($event_name, $data) {
366
		if (isset($data['new']['dkim']) && $data['new']['dkim']=='y' && $this->check_system($data))
367
368
			$this->add_dkim($data);
	}
tbrehm's avatar
tbrehm committed
369

Florian Schaal's avatar
Florian Schaal committed
370
	/**
371
372
373
374
	 * Function called by onLoad
	 * chang dkim-settings
	 */
	function domain_dkim_update($event_name, $data) {
tbrehm's avatar
tbrehm committed
375
		global $app;
Marius Cramer's avatar
Marius Cramer committed
376
377
378
379
380
		if($data['new']['dkim'] == 'y' || $data['old']['dkim'] == 'y'){
			if ($this->check_system($data)) {
				/* maildomain disabled */
				if ($data['new']['active'] == 'n' && $data['old']['active'] == 'y' && $data['new']['dkim']=='y') {
					$app->log('Maildomain '.$data['new']['domain'].' disabled - remove DKIM-settings', LOGLEVEL_DEBUG);
381
382
					$this->remove_dkim($data['new']);
				}
Marius Cramer's avatar
Marius Cramer committed
383
384
				/* maildomain re-enabled */
				if ($data['new']['active'] == 'y' && $data['old']['active'] == 'n' && $data['new']['dkim']=='y') 
385
					$this->add_dkim($data);
Marius Cramer's avatar
Marius Cramer committed
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409

				/* 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') {
						$this->add_dkim($data);
					}
					/* new private-key */
					if ($data['new']['dkim_private'] != $data['old']['dkim_private'] && $data['new']['dkim'] == 'y') {
						$this->add_dkim($data);
					}
					/* new selector */
					if ($data['new']['dkim_selector'] != $data['old']['dkim_selector'] && $data['new']['dkim'] == 'y') {
						$this->add_dkim($data);
					}
					/* new domain-name */
					if ($data['new']['domain'] != $data['old']['domain']) {
						$this->remove_dkim($data['old']);
						$this->add_dkim($data);
					}
410
				}
Marius Cramer's avatar
Marius Cramer committed
411
412
413

				/* resync */
				if ($data['new']['active'] == 'y' && $data['new'] == $data['old'] && $data['new']['dkim']=='y') {
414
					$this->add_dkim($data);
415
416
				}
			}
tbrehm's avatar
tbrehm committed
417
418
		}
	}
419

tbrehm's avatar
tbrehm committed
420
}
421

tbrehm's avatar
tbrehm committed
422
?>