24740f5842ed4e0cf1a4c933c724f78f268cb634
[tine20] / tine20 / Tinebase / EmailUser / Smtp / Postfix.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  EmailUser
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schüle <p.schuele@metaways.de>
10  * 
11 --
12 -- Database: `postfix`
13 --
14
15 -- --------------------------------------------------------
16
17 --
18 -- Table structure for table `smtp_users`
19 --
20
21 CREATE TABLE IF NOT EXISTS `smtp_users` (
22   `userid` varchar(40) NOT NULL,
23   `client_idnr` varchar(40) NOT NULL,
24   `username` varchar(80) NOT NULL,
25   `passwd` varchar(80) NOT NULL,
26   `email` varchar(80) DEFAULT NULL,
27   `forward_only` tinyint(1) NOT NULL DEFAULT '0',
28   PRIMARY KEY (`userid`, `client_idnr`),
29   UNIQUE KEY `username` (`username`),
30   UNIQUE KEY `email` (`email`)
31 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
32
33 -- --------------------------------------------------------
34
35 --
36 -- Table structure for table `smtp_destinations`
37 --
38
39 CREATE TABLE IF NOT EXISTS `smtp_destinations` (
40   `userid` VARCHAR( 40 ) NOT NULL ,
41   `source` VARCHAR( 80 ) NOT NULL ,
42   `destination` VARCHAR( 80 ) NOT NULL ,
43   CONSTRAINT `smtp_destinations::userid--smtp_users::userid` FOREIGN KEY (`userid`) 
44   REFERENCES `smtp_users` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE
45 ) ENGINE=Innodb DEFAULT CHARSET=utf8;
46 -- --------------------------------------------------------
47
48 --
49 -- Postfix virtual_mailbox_domains: sql-virtual_mailbox_domains.cf
50 --
51
52 user     = smtpUser
53 password = smtpPass
54 hosts    = 127.0.0.1
55 dbname   = smtp
56 query    = SELECT DISTINCT 1 FROM smtp_destinations WHERE SUBSTRING_INDEX(source, '@', -1) = '%s';
57 -- ----------------------------------------------------
58
59 --
60 -- Postfix sql-virtual_mailbox_maps: sql-virtual_mailbox_maps.cf
61 --
62
63 user     = smtpUser
64 password = smtpPass
65 hosts    = 127.0.0.1
66 dbname   = smtp
67 query    = SELECT 1 FROM smtp_users WHERE username='%s' AND forward_only=0
68 -- ----------------------------------------------------
69
70 --
71 -- Postfix sql-virtual_alias_maps: sql-virtual_alias_maps_aliases.cf
72 --
73
74 user     = smtpUser
75 password = smtpPass
76 hosts    = 127.0.0.1
77 dbname   = smtp
78 query = SELECT destination FROM smtp_destinations WHERE source='%s'
79
80 -- -----------------------------------------------------
81  */
82
83  /**
84  * plugin to handle postfix smtp accounts
85  * 
86  * @package    Tinebase
87  * @subpackage EmailUser
88  */
89 class Tinebase_EmailUser_Smtp_Postfix extends Tinebase_EmailUser_Sql
90 {
91     /**
92      * destination table name with prefix
93      *
94      * @var string
95      */
96     protected $_destinationTable = NULL;
97     
98     /**
99      * postfix config
100      * 
101      * @var array 
102      */
103     protected $_config = array(
104         'prefix'            => 'smtp_',
105         'userTable'         => 'users',
106         'destinationTable'  => 'destinations',
107         'emailScheme'       => 'ssha256',
108         'domain'            => null,
109         'alloweddomains'    => array()
110     );
111
112     /**
113      * user properties mapping
114      *
115      * @var array
116      */
117     protected $_propertyMapping = array(
118         'emailPassword'     => 'passwd', 
119         'emailUserId'       => 'userid',
120         'emailAddress'      => 'email',
121         'emailForwardOnly'  => 'forward_only',
122         'emailUsername'     => 'username',
123         'emailAliases'      => 'source',
124         'emailForwards'     => 'destination'
125     );
126     
127     /**
128      * the constructor
129      */
130     public function __construct(array $_options = array())
131     {
132         $this->_configKey    = Tinebase_Config::SMTP;
133         $this->_subconfigKey = 'postfix';
134
135         parent::__construct($_options);
136         
137         $smtpConfig = Tinebase_Config::getInstance()->get($this->_configKey, new Tinebase_Config_Struct())->toArray();
138         
139         // set domain from smtp config
140         $this->_config['domain'] = !empty($smtpConfig['primarydomain']) ? $smtpConfig['primarydomain'] : null;
141         
142         // add allowed domains
143         if (! empty($smtpConfig['primarydomain'])) {
144             $this->_config['alloweddomains'] = array($smtpConfig['primarydomain']);
145             if (! empty($smtpConfig['secondarydomains'])) {
146                 // merge primary and secondary domains and split secondary domains + trim whitespaces
147                 $this->_config['alloweddomains'] = array_merge($this->_config['alloweddomains'], preg_split('/\s*,\s*/', $smtpConfig['secondarydomains']));
148             } 
149         }
150         
151         $this->_clientId = Tinebase_Application::getInstance()->getApplicationByName('Tinebase')->getId();
152         
153         $this->_destinationTable = $this->_config['prefix'] . $this->_config['destinationTable'];
154     }
155     
156     /**
157      * get the basic select object to fetch records from the database
158      *  
159      * @param  array|string|Zend_Db_Expr  $_cols        columns to get, * per default
160      * @param  boolean                    $_getDeleted  get deleted records (if modlog is active)
161      * @return Zend_Db_Select
162      */
163     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
164     {
165         // _userTable.emailUserId=_destinationTable.emailUserId
166         $userIDMap    = $this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUserId']);
167         $userEmailMap = $this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailAddress']);
168         
169         $select = $this->_db->select()
170             ->from($this->_userTable)
171             ->group($this->_userTable . '.userid')
172             // Only want 1 user (shouldn't be more than 1 anyway)
173             ->limit(1);
174             
175         // select source from alias table
176         $select->joinLeft(
177             array('aliases' => $this->_destinationTable), // Table
178             '(' . $userIDMap .  ' = ' .  // ON (left)
179             $this->_db->quoteIdentifier('aliases.' . $this->_propertyMapping['emailUserId']) . // ON (right)
180             ' AND ' . $userEmailMap . ' = ' . // AND ON (left)
181             $this->_db->quoteIdentifier('aliases.' . $this->_propertyMapping['emailForwards']) . ')', // AND ON (right)
182             array($this->_propertyMapping['emailAliases'] => $this->_dbCommand->getAggregate('aliases.' . $this->_propertyMapping['emailAliases']))); // Select
183         
184         // select destination from alias table
185         $select->joinLeft(
186             array('forwards' => $this->_destinationTable), // Table
187             '(' . $userIDMap .  ' = ' . // ON (left)
188             $this->_db->quoteIdentifier('forwards.' . $this->_propertyMapping['emailUserId']) . // ON (right)
189             ' AND ' . $userEmailMap . ' = ' . // AND ON (left)
190             $this->_db->quoteIdentifier('forwards.' . $this->_propertyMapping['emailAliases']) . ')', // AND ON (right)
191             array($this->_propertyMapping['emailForwards'] => $this->_dbCommand->getAggregate('forwards.' . $this->_propertyMapping['emailForwards']))); // Select
192
193         // append domain if set or domain IS NULL
194         if (! empty($this->_clientId)) {
195             $select->where($this->_db->quoteIdentifier($this->_userTable . '.client_idnr') . ' = ?', $this->_clientId);
196         } else {
197             $select->where($this->_db->quoteIdentifier($this->_userTable . '.client_idnr') . ' IS NULL');
198         }
199         
200         return $select;
201     }
202     
203     /**
204     * interceptor before add
205     *
206     * @param array $emailUserData
207     */
208     protected function _beforeAddOrUpdate(&$emailUserData)
209     {
210         unset($emailUserData[$this->_propertyMapping['emailForwards']]);
211         unset($emailUserData[$this->_propertyMapping['emailAliases']]);
212     }
213     
214     /**
215     * interceptor after add
216     *
217     * @param array $emailUserData
218     */
219     protected function _afterAddOrUpdate(&$emailUserData)
220     {
221         $this->_setAliasesAndForwards($emailUserData);
222     }
223     
224     /**
225      * set email aliases and forwards
226      * 
227      * removes all aliases for user
228      * creates default email->email alias if not forward only
229      * creates aliases
230      * creates forwards
231      * 
232      * @param  array  $_smtpSettings  as returned from _recordToRawData
233      * @return void
234      */
235     protected function _setAliasesAndForwards($_smtpSettings)
236     {
237         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
238             . ' Setting default alias/forward for ' . print_r($_smtpSettings, true));
239         
240         $this->_removeDestinations($_smtpSettings[$this->_propertyMapping['emailUserId']]);
241         
242         // check if it should be forward only
243         if (! $_smtpSettings[$this->_propertyMapping['emailForwardOnly']]) {
244             $this->_createDefaultDestinations($_smtpSettings);
245         }
246         
247         $this->_createAliasDestinations($_smtpSettings);
248         $this->_createForwardDestinations($_smtpSettings);
249     }
250     
251     /**
252      * remove all current aliases and forwards for user
253      * 
254      * @param string $userId
255      */
256     protected function _removeDestinations($userId)
257     {
258         $where = array(
259             $this->_db->quoteInto($this->_db->quoteIdentifier($this->_propertyMapping['emailUserId']) . ' = ?', $userId)
260         );
261         
262         $this->_db->delete($this->_destinationTable, $where);
263     }
264     
265     /**
266      * create default destinations
267      * 
268      * @param array $_smtpSettings
269      */
270     protected function _createDefaultDestinations($_smtpSettings)
271     {
272         // create email -> username alias
273         $this->_addDestination(array(
274             'userid'        => $_smtpSettings[$this->_propertyMapping['emailUserId']],   // userID
275             'source'        => $_smtpSettings[$this->_propertyMapping['emailAddress']],  // TineEmail
276             'destination'   => $_smtpSettings[$this->_propertyMapping['emailUsername']], // email
277         ));
278         
279         // create username -> username alias if email and username are different
280         if ($_smtpSettings[$this->_propertyMapping['emailUsername']] != $_smtpSettings[$this->_propertyMapping['emailAddress']]) {
281             $this->_addDestination(array(
282                 'userid'      => $_smtpSettings[$this->_propertyMapping['emailUserId']],   // userID
283                 'source'      => $_smtpSettings[$this->_propertyMapping['emailUsername']], // username
284                 'destination' => $_smtpSettings[$this->_propertyMapping['emailUsername']], // username
285             ));
286         }
287     }
288     
289     /**
290      * add destination
291      * 
292      * @param array $destinationData
293      */
294     protected function _addDestination($destinationData)
295     {
296         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
297             . ' Insert into table destinations: ' . print_r($destinationData, true));
298         
299         $this->_db->insert($this->_destinationTable, $destinationData);
300     }
301     
302     /**
303      * set aliases
304      * 
305      * @param array $_smtpSettings
306      */
307     protected function _createAliasDestinations($_smtpSettings)
308     {
309         if (! ((isset($_smtpSettings[$this->_propertyMapping['emailAliases']]) || array_key_exists($this->_propertyMapping['emailAliases'], $_smtpSettings)) && is_array($_smtpSettings[$this->_propertyMapping['emailAliases']]))) {
310             return;
311         }
312         
313         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Setting aliases for '
314             . $_smtpSettings[$this->_propertyMapping['emailUsername']] . ': ' . print_r($_smtpSettings[$this->_propertyMapping['emailAliases']], TRUE));
315         
316         $userId = $_smtpSettings[$this->_propertyMapping['emailUserId']];
317             
318         foreach ($_smtpSettings[$this->_propertyMapping['emailAliases']] as $aliasAddress) {
319             // check if in primary or secondary domains
320             if (! empty($aliasAddress) && $this->_checkDomain($aliasAddress)) {
321                 
322                 if (! $_smtpSettings[$this->_propertyMapping['emailForwardOnly']]) {
323                     // create alias -> email
324                     $this->_addDestination(array(
325                         'userid'      => $userId,
326                         'source'      => $aliasAddress,
327                         'destination' => $_smtpSettings[$this->_propertyMapping['emailAddress']], // email 
328                     ));
329                 } else if ($this->_hasForwards($_smtpSettings)) {
330                     $this->_addForwards($userId, $aliasAddress, $_smtpSettings[$this->_propertyMapping['emailForwards']]);
331                 }
332             }
333         }
334     }
335     
336     /**
337      * check if forward addresses exist
338      * 
339      * @param array $_smtpSettings
340      * @return boolean
341      */
342     protected function _hasForwards($_smtpSettings)
343     {
344         return ((isset($_smtpSettings[$this->_propertyMapping['emailForwards']]) || array_key_exists($this->_propertyMapping['emailForwards'], $_smtpSettings)) && is_array($_smtpSettings[$this->_propertyMapping['emailForwards']]));
345     }
346
347     /**
348      * add forward destinations
349      * 
350      * @param string $userId
351      * @param string $source
352      * @param array $forwards
353      */
354     protected function _addForwards($userId, $source, $forwards)
355     {
356         foreach ($forwards as $forwardAddress) {
357             if (! empty($forwardAddress)) {
358                 // create email -> forward
359                 $this->_addDestination(array(
360                     'userid'      => $userId,
361                     'source'      => $source,
362                     'destination' => $forwardAddress
363                 ));
364             }
365         }
366     }
367     
368     /**
369      * set forwards
370      * 
371      * @param array $_smtpSettings
372      */
373     protected function _createForwardDestinations($_smtpSettings)
374     {
375         if (! $this->_hasForwards($_smtpSettings)) {
376             return;
377         }
378
379         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
380             . ' Setting forwards for ' . $_smtpSettings[$this->_propertyMapping['emailUsername']] . ': ' . print_r($_smtpSettings[$this->_propertyMapping['emailForwards']], TRUE));
381         
382         $this->_addForwards(
383             $_smtpSettings[$this->_propertyMapping['emailUserId']],
384             $_smtpSettings[$this->_propertyMapping['emailAddress']],
385             $_smtpSettings[$this->_propertyMapping['emailForwards']]
386         );
387     }
388     
389     /**
390      * converts raw data from adapter into a single record / do mapping
391      *
392      * @param  array $_data
393      * @return Tinebase_Record_Abstract
394      */
395     protected function _rawDataToRecord(array $_rawdata)
396     {
397         $data = array();
398         
399         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
400             . ' raw data: ' . print_r($_rawdata, true));
401         
402         foreach ($_rawdata as $key => $value) {
403             $keyMapping = array_search($key, $this->_propertyMapping);
404             if ($keyMapping !== FALSE) {
405                 switch ($keyMapping) {
406                     case 'emailPassword':
407                         // do nothing
408                         break;
409                     
410                     case 'emailAliases':
411                     case 'emailForwards':
412                         $data[$keyMapping] = explode(',', $value);
413                         // Get rid of TineEmail -> username mapping.
414                         $tineEmailAlias = array_search($_rawdata[$this->_propertyMapping['emailUsername']], $data[$keyMapping]);
415                         if ($tineEmailAlias !== false) {
416                             unset($data[$keyMapping][$tineEmailAlias]);
417                             $data[$keyMapping] = array_values($data[$keyMapping]);
418                         }
419                         // sanitize aliases & forwards
420                         if (count($data[$keyMapping]) == 1 && empty($data[$keyMapping][0])) {
421                             $data[$keyMapping] = array();
422                         }
423                         break;
424                         
425                     case 'emailForwardOnly':
426                         $data[$keyMapping] = (bool)$value;
427                         break;
428                         
429                     default: 
430                         $data[$keyMapping] = $value;
431                         break;
432                 }
433             }
434         }
435         
436         $emailUser = new Tinebase_Model_EmailUser($data, TRUE);
437         
438         $this->_getForwardedAliases($emailUser);
439         
440         return $emailUser;
441     }
442     
443     /**
444      * get forwarded aliases
445      * - fetch aliases + forwards from destinations table that do belong to 
446      *   user where aliases are directly mapped to forward addresses 
447      * 
448      * @param Tinebase_Model_EmailUser $emailUser
449      */
450     protected function _getForwardedAliases(Tinebase_Model_EmailUser $emailUser)
451     {
452         if (! $emailUser->emailForwardOnly) {
453             return;
454         }
455         
456         $select = $this->_db->select()
457             ->from($this->_destinationTable)
458             ->where($this->_db->quoteIdentifier($this->_destinationTable . '.' . $this->_propertyMapping['emailUserId']) . ' = ?', $emailUser->emailUserId);
459         $stmt = $this->_db->query($select);
460         $queryResult = $stmt->fetchAll();
461         $stmt->closeCursor();
462         
463         $aliases = ($emailUser->emailAliases && is_array($emailUser->emailAliases)) ? $emailUser->emailAliases : array();
464         foreach ($queryResult as $destination) {
465             if ($destination['source'] !== $emailUser->emailAddress
466                 && in_array($destination['destination'], $emailUser->emailForwards)
467                 && ! in_array($destination['source'], $aliases)
468             ) {
469                 $aliases[] = $destination['source'];
470             }
471         }
472         $emailUser->emailAliases = $aliases;
473     }
474     
475     /**
476      * returns array of raw email user data
477      *
478      * @param  Tinebase_Model_EmailUser $_user
479      * @param  Tinebase_Model_EmailUser $_newUserProperties
480      * @throws Tinebase_Exception_UnexpectedValue
481      * @return array
482      * 
483      * @todo   validate domains of aliases too
484      */
485     protected function _recordToRawData(Tinebase_Model_FullUser $_user, Tinebase_Model_FullUser $_newUserProperties)
486     {
487         $rawData = array();
488         
489         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_newUserProperties->toArray(), true));
490         
491         if (isset($_newUserProperties->smtpUser)) {
492             foreach ($_newUserProperties->smtpUser as $key => $value) {
493                 $property = (isset($this->_propertyMapping[$key]) || array_key_exists($key, $this->_propertyMapping)) ? $this->_propertyMapping[$key] : false;
494                 if ($property) {
495                     switch ($key) {
496                         case 'emailPassword':
497                             $rawData[$property] = Hash_Password::generate($this->_config['emailScheme'], $value);
498                             break;
499                             
500                         case 'emailAliases':
501                             $rawData[$property] = array();
502                             
503                             foreach((array)$value as $address) {
504                                 if ($this->_checkDomain($address) === true) {
505                                     $rawData[$property][] = $address;
506                                 }
507                             }
508                             break;
509                             
510                         case 'emailForwards':
511                             $rawData[$property] = is_array($value) ? $value : array();
512                             
513                             break;
514                             
515                         default:
516                             $rawData[$property] = $value;
517                             break;
518                     }
519                 }
520             }
521         }
522         
523         if (!empty($_user->accountEmailAddress)) {
524             $this->_checkDomain($_user->accountEmailAddress, TRUE);
525         }
526         
527         $rawData[$this->_propertyMapping['emailAddress']]  = $_user->accountEmailAddress;
528         $rawData[$this->_propertyMapping['emailUserId']]   = $_user->getId();
529         $rawData[$this->_propertyMapping['emailUsername']] = $this->_appendDomain($_user->accountLoginName);
530         
531         if (empty($rawData[$this->_propertyMapping['emailAddress']])) {
532             $rawData[$this->_propertyMapping['emailAliases']]  = null;
533             $rawData[$this->_propertyMapping['emailForwards']] = null;
534         }
535         
536         if (empty($rawData[$this->_propertyMapping['emailForwards']])) {
537             $rawData[$this->_propertyMapping['emailForwardOnly']] = 0;
538         }
539         
540         $rawData['client_idnr'] = $this->_clientId;
541         
542         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($rawData, true));
543         
544         return $rawData;
545     }
546     
547     /**
548      * check if email address is in allowed domains
549      * 
550      * @param string $_email
551      * @param boolean $_throwException
552      * @return boolean
553      * @throws Tinebase_Exception_Record_NotAllowed
554      */
555     protected function _checkDomain($_email, $_throwException = false)
556     {
557         $result = true;
558         
559         if (! empty($this->_config['alloweddomains'])) {
560
561             list($user, $domain) = explode('@', $_email, 2);
562             
563             if (! in_array($domain, $this->_config['alloweddomains'])) {
564                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Email address ' . $_email . ' not in allowed domains!');
565                 
566                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Allowed domains: ' . print_r($this->_config['alloweddomains'], TRUE));
567                 
568                 if ($_throwException) {
569                     throw new Tinebase_Exception_UnexpectedValue('Email address not in allowed domains!');
570                 } else {
571                     $result = false;
572                 }
573             }
574         }
575         
576         return $result;
577     }
578 }