Admin - Quota - show virtual emails tree in filesystem quota tree
[tine20] / tine20 / Tinebase / EmailUser / Sql.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) 2012-2015 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schüle
10  * */
11
12 /**
13  * plugin to handle sql email accounts
14  * 
15  * @package    Tinebase
16  * @subpackage EmailUser
17  */
18 abstract class Tinebase_EmailUser_Sql extends Tinebase_User_Plugin_Abstract
19 {
20     /**
21      * user table name with prefix
22      *
23      * @var string
24      */
25     protected $_userTable = NULL;
26
27     /**
28      * schema of the table
29      *
30      * @var array
31      */
32     protected $_schema = NULL;
33
34     /**
35      * email user config
36      * 
37      * @var array 
38      */
39      protected $_config = array();
40     
41     /**
42      * user properties mapping
43      *
44      * @var array
45      */
46     protected $_propertyMapping = array();
47
48     /**
49      * @var array
50      */
51     protected $_tableMapping = array();
52
53     /**
54      * config key (IMAP/SMTP)
55      * 
56      * @var string
57      */
58     protected $_configKey = NULL;
59     
60     /**
61      * subconfig for user email backend (for example: dovecot)
62      * 
63      * @var string
64      */
65     protected $_subconfigKey =  NULL;
66     
67     /**
68     * client id
69     *
70     * @var string
71     */
72     protected $_clientId = NULL;
73     
74     /**
75      * the constructor
76      * 
77      * @param array $_options
78      */
79     public function __construct(array $_options = array())
80     {
81         if ($this instanceof Tinebase_EmailUser_Smtp_Interface) {
82             $this->_configKey = Tinebase_Config::SMTP;
83         } else if ($this instanceof Tinebase_EmailUser_Imap_Interface) {
84             $this->_configKey = Tinebase_Config::IMAP;
85         } else {
86             throw new Tinebase_Exception_UnexpectedValue('Plugin must be instance of Tinebase_EmailUser_Smtp_Interface or Tinebase_EmailUser_Imap_Interface');
87         }
88         
89         // get email user backend config options (host, dbname, username, password, port)
90         $emailConfig = Tinebase_Config::getInstance()->get($this->_configKey, new Tinebase_Config_Struct())->toArray();
91         
92         // merge _config and email backend config
93         if ($this->_subconfigKey) {
94             // flatten array
95             $emailConfig = array_merge($emailConfig[$this->_subconfigKey], $emailConfig);
96         }
97         // merge _config and email backend config
98         $this->_config = array_merge($this->_config, $emailConfig);
99         
100         // _tablename (for example "dovecot_users")
101         $this->_userTable = $this->_config['prefix'] . $this->_config['userTable'];
102         
103         // connect to DB
104         $this->_getDB();
105         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
106         
107         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($this->_config, TRUE));
108     }
109     
110     /**
111      * get new email user
112      * 
113      * @param  Tinebase_Model_FullUser   $_user
114      * @return Tinebase_Model_EmailUser
115      */
116     public function getNewUser(Tinebase_Model_FullUser $_user)
117     {
118         $result = new Tinebase_Model_EmailUser(array(
119             'emailUserId'     => $_user->getId(),
120             'emailUsername' => $this->_appendDomain($_user->accountLoginName)
121         ));
122         
123         return $result;
124     }
125     
126     /**
127     * delete user by id
128     *
129     * @param  Tinebase_Model_FullUser  $_user
130     */
131     public function inspectDeleteUser(Tinebase_Model_FullUser $_user)
132     {
133         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
134             . ' Delete ' . $this->_configKey . ' email settings for user ' . $_user->accountLoginName);
135         
136         $this->_deleteUserById($_user->getId());
137     }
138     
139     /**
140      * delete user by id
141      * 
142      * @param string $id
143      */
144     protected function _deleteUserById($id)
145     {
146         $where = array(
147             $this->_db->quoteInto($this->_db->quoteIdentifier($this->_propertyMapping['emailUserId']) . ' = ?', $id)
148         );
149         $this->_appendClientIdOrDomain($where);
150         
151         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
152             . ' ' . print_r($where, TRUE));
153         
154         $this->_db->delete($this->_userTable, $where);
155     }
156     
157     /**
158      * append domain if set or domain IS NULL
159      * 
160      * @param array $where
161      * @return string
162      * 
163      * @todo check if user table has domain or client_idnr field and use mapping for the field identifier
164      */
165     protected function _appendClientIdOrDomain(&$where = NULL)
166     {
167         if ($this->_clientId !== NULL) {
168             $cond = $this->_db->quoteInto($this->_db->quoteIdentifier($this->_userTable . '.' . 'client_idnr') . ' = ?', $this->_clientId);
169         } else {
170             if ((isset($this->_config['domain']) || array_key_exists('domain', $this->_config)) && ! empty($this->_config['domain'])) {
171                 $cond = $this->_db->quoteInto($this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . ' = ?',   $this->_config['domain']);
172             } else {
173                 $cond = $this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . " =''";
174             }
175         }
176         
177         if ($where !== NULL) {
178             $where[] = $cond;
179         }
180         
181         return $cond;
182     }
183
184     /**
185      * @param Tinebase_Model_User $_user
186      * @return mixed
187      */
188     public function getRawUserById(Tinebase_Model_User $_user)
189     {
190         $userId = $_user->getId();
191
192         $select = $this->_getSelect()
193             ->where($this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUserId']) . ' = ?',   $userId);
194
195         // Perform query - retrieve user from database
196         $stmt = $this->_db->query($select);
197         $queryResult = $stmt->fetch();
198         $stmt->closeCursor();
199
200         if (!$queryResult) {
201             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
202                 . ' ' . $this->_subconfigKey . ' config for user ' . $userId . ' not found!');
203         }
204
205         return $queryResult;
206     }
207
208     /**
209      * inspect get user by property
210      * 
211      * @param Tinebase_Model_User  $_user  the user object
212      */
213     public function inspectGetUserByProperty(Tinebase_Model_User $_user)
214     {
215         if (! $_user instanceof Tinebase_Model_FullUser) {
216             return;
217         }
218         
219         $rawUser = $this->getRawUserById($_user);
220         
221         // convert data to Tinebase_Model_EmailUser
222         $emailUser = $this->_rawDataToRecord((array)$rawUser);
223         
224         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
225             . ' ' . print_r($emailUser->toArray(), TRUE));
226         
227         // modify/correct user name
228         // set emailUsername to Tine 2.0 account login name and append domain for login purposes if set
229         if (empty($emailUser->emailUsername)) {
230             $emailUser->emailUsername = $this->_getEmailUserName($_user);
231         }
232         
233         if ($this instanceof Tinebase_EmailUser_Smtp_Interface) {
234             $_user->smtpUser  = $emailUser;
235             $_user->emailUser = Tinebase_EmailUser::merge($_user->emailUser, clone $_user->smtpUser);
236         } else {
237             $_user->imapUser  = $emailUser;
238             $_user->emailUser = Tinebase_EmailUser::merge(clone $_user->imapUser, $_user->emailUser);
239         }
240     }
241     
242     protected function _getConfiguredSystemDefaults()
243     {
244         $systemDefaults = array();
245         
246         $hostAttribute = ($this instanceof Tinebase_EmailUser_Imap_Interface) ? 'host' : 'hostname';
247         if (!empty($this->_config[$hostAttribute])) {
248             $systemDefaults['emailHost'] = $this->_config[$hostAttribute];
249         }
250         
251         if (!empty($this->_config['port'])) {
252             $systemDefaults['emailPort'] = $this->_config['port'];
253         }
254         
255         if (!empty($this->_config['ssl'])) {
256             $systemDefaults['emailSecure'] = $this->_config['ssl'];
257         }
258         
259         if (!empty($this->_config['auth'])) {
260             $systemDefaults['emailAuth'] = $this->_config['auth'];
261         }
262         
263         return $systemDefaults;
264     }
265     
266     /**
267      * update/set email user password
268      * 
269      * @param  string  $_userId
270      * @param  string  $_password
271      * @param  bool    $_encrypt encrypt password
272      */
273     public function inspectSetPassword($_userId, $_password, $_encrypt = TRUE)
274     {
275         if (!isset($this->_propertyMapping['emailPassword'])) {
276             return;
277         }
278         
279         $imapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP, new Tinebase_Config_Struct())->toArray();
280         if ((isset($imapConfig['pwsuffix']) || array_key_exists('pwsuffix', $imapConfig))) {
281             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
282                 ' Appending configured pwsuffix to new email account password.');
283             $password = $_password . $imapConfig['pwsuffix'];
284         } else {
285             $password = $_password;
286         }
287         
288         $values = array(
289             $this->_propertyMapping['emailPassword'] => ($_encrypt) ? Hash_Password::generate($this->_config['emailScheme'], $password) : $password
290         );
291         
292         $where = array(
293             $this->_db->quoteInto($this->_db->quoteIdentifier($this->_propertyMapping['emailUserId']) . ' = ?', $_userId)
294         );
295         $this->_appendClientIdOrDomain($where);
296         
297         $this->_db->update($this->_userTable, $values, $where);
298     }
299     
300     /*********  protected functions  *********/
301     
302     /**
303      * get the basic select object to fetch records from the database
304      *  
305      * @param  array|string|Zend_Db_Expr  $_cols        columns to get, * per default
306      * @param  boolean                    $_getDeleted  get deleted records (if modlog is active)
307      * @return Zend_Db_Select
308      */
309     abstract protected function _getSelect($_cols = '*', $_getDeleted = FALSE);
310     
311     /**
312      * adds email properties for a new user
313      * 
314      * @param  Tinebase_Model_FullUser  $_addedUser
315      * @param  Tinebase_Model_FullUser  $_newUserProperties
316      */
317     protected function _addUser(Tinebase_Model_FullUser $_addedUser, Tinebase_Model_FullUser $_newUserProperties)
318     {
319         if (! $_addedUser->accountEmailAddress) {
320             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
321             . ' User ' . $_addedUser->accountDisplayName . ' has no email address defined. Skipping email user creation.');
322             return;
323         }
324         
325         $emailUserData = $this->_recordToRawData($_addedUser, $_newUserProperties);
326
327         $emailUsername = $emailUserData[$this->_propertyMapping['emailUsername']];
328         
329         $this->_checkEmailExistance($emailUsername);
330         
331         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
332             . ' Adding new ' . $this->_configKey . ' email user ' . $emailUsername);
333         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' 
334             . print_r($emailUserData, TRUE));
335         
336         try {
337             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
338             
339             // generate random password if not set
340             if (isset($this->_propertyMapping['emailPassword']) && empty($emailUserData[$this->_propertyMapping['emailPassword']])) {
341                 $emailUserData[$this->_propertyMapping['emailPassword']] = Hash_Password::generate($this->_config['emailScheme'], Tinebase_Record_Abstract::generateUID());
342             }
343             
344             $insertData = $emailUserData;
345             $this->_beforeAddOrUpdate($insertData);
346
347             $insertData = array_intersect_key($insertData, $this->getSchema());
348             $this->_db->insert($this->_userTable, $insertData);
349             
350             $this->_afterAddOrUpdate($emailUserData);
351             
352             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
353             
354             $this->inspectGetUserByProperty($_addedUser);
355             
356         } catch (Zend_Db_Statement_Exception $zdse) {
357             Tinebase_TransactionManager::getInstance()->rollBack();
358             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Error while creating email user: ' . $zdse);
359         }
360     }
361     
362     /**
363      * interceptor before add
364      * 
365      * @param array $emailUserData
366      */
367     protected function _beforeAddOrUpdate(&$emailUserData)
368     {
369         
370     }
371     
372     /**
373      * interceptor after add
374      * 
375      * @param array $emailUserData
376      */
377     protected function _afterAddOrUpdate(&$emailUserData)
378     {
379         
380     }
381     
382     /**
383      * check if user email already exists in table
384      * 
385      * @param  string  $email
386      */
387     protected function _checkEmailExistance($email)
388     {
389         $select = $this->_getSelect()
390             ->where($this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUsername']) . ' = ?',   $email)
391             ->where($this->_appendClientIdOrDomain());
392         
393         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select);
394         
395         $stmt = $this->_db->query($select);
396         $queryResult = $stmt->fetch();
397         $stmt->closeCursor();
398         
399         if (! $queryResult) {
400             return;
401         }
402         
403         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($queryResult, TRUE));
404         
405         $userId = $queryResult[$this->_propertyMapping['emailUserId']];
406         
407         try {
408             Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $userId);
409             throw new Tinebase_Exception_SystemGeneric('Could not overwrite existing email user.');
410         } catch (Tinebase_Exception_NotFound $tenf) {
411             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Delete obsolete email user ' .$userId);
412             $this->_deleteUserById($userId);
413         }
414     }
415     
416     /**
417      * updates email properties for an existing user
418      * 
419      * @param  Tinebase_Model_FullUser  $_updatedUser
420      * @param  Tinebase_Model_FullUser  $_newUserProperties
421      */
422     protected function _updateUser(Tinebase_Model_FullUser $_updatedUser, Tinebase_Model_FullUser $_newUserProperties)
423     {
424         $emailUserData = $this->_recordToRawData($_updatedUser, $_newUserProperties);
425
426         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))  Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updating Dovecot user ' . $emailUserData[$this->_propertyMapping['emailUsername']]);
427         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($emailUserData, TRUE));
428         
429         $where = array(
430             $this->_db->quoteInto($this->_db->quoteIdentifier($this->_propertyMapping['emailUserId']) . ' = ?', $emailUserData[$this->_propertyMapping['emailUserId']])
431         );
432         $this->_appendClientIdOrDomain($where);
433         
434         try {
435             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
436             
437             $updateData = $emailUserData;
438             
439             $this->_beforeAddOrUpdate($updateData);
440
441             $updateData = array_intersect_key($updateData, $this->getSchema());
442             $this->_db->update($this->_userTable, $updateData, $where);
443             
444             $this->_afterAddOrUpdate($emailUserData);
445             
446             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
447             
448             $this->inspectGetUserByProperty($_updatedUser);
449             
450         } catch (Zend_Db_Statement_Exception $zdse) {
451             Tinebase_TransactionManager::getInstance()->rollBack();
452             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Error while updating email user: ' . $zdse->getMessage());
453         }
454     }
455     
456     /**
457      * check if user exists already in email backjend user table
458      * 
459      * @param  Tinebase_Model_FullUser  $_user
460      * @throws Tinebase_Exception_Backend_Database
461      */
462     protected function _userExists(Tinebase_Model_FullUser $_user)
463     {
464         $select = $this->_getSelect();
465         
466         $select
467           ->where($this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUserId']) . ' = ?',   $_user->getId());
468         
469         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
470         
471         // Perform query - retrieve user from database
472         try {
473             $stmt = $this->_db->query($select);
474         } catch (Zend_Db_Statement_Exception $zdse) {
475             if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $zdse);
476             throw new Tinebase_Exception_Backend_Database($zdse->getMessage());
477         }
478         $queryResult = $stmt->fetch();
479         $stmt->closeCursor();
480         
481         if (!$queryResult) {
482             return false;
483         }
484         
485         return true;
486     }
487
488     /**
489      * returns the db schema
490      * @return array
491      * @throws Tinebase_Exception_Backend_Database
492      *
493      * @refactor use trait (see \Tinebase_Backend_Sql_Abstract::getSchema)
494      */
495     public function getSchema()
496     {
497         if (!$this->_schema) {
498             try {
499                 $this->_schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_userTable, $this->_db);
500             } catch (Zend_Db_Adapter_Exception $zdae) {
501                 throw new Tinebase_Exception_Backend_Database('Connection failed: ' . $zdae->getMessage());
502             }
503         }
504
505         return $this->_schema;
506     }
507
508     /**
509      * converts raw data from adapter into a single record / do mapping
510      *
511      * @param  array                    $_data
512      * @return Tinebase_Model_EmailUser
513      */
514     abstract protected function _rawDataToRecord(array $_rawdata);
515      
516     /**
517      * returns array of raw user data
518      *
519      * @param  Tinebase_Model_FullUser  $_user
520      * @param  Tinebase_Model_FullUser  $_newUserProperties
521      * @return array
522      */
523     abstract protected function _recordToRawData(Tinebase_Model_FullUser $_user, Tinebase_Model_FullUser $_newUserProperties);
524
525     /**
526      * @return Tinebase_Record_RecordSet of Tinebase_Model_EmailUser
527      */
528     public function getAllEmailUsers()
529     {
530         $result = new Tinebase_Record_RecordSet('Tinebase_Model_EmailUser', array());
531         foreach ($this->_getSelect()->limit(0)->query()->fetchAll() as $row) {
532             $result->addRecord($this->_rawDataToRecord($row));
533         }
534         return $result;
535     }
536
537     /**
538      * returns array with keys mailQuota and mailSize
539      * @return array
540      */
541     public function getTotalUsageQuota()
542     {
543         $data = $this->_getSelect(
544             array(
545                 new Zend_Db_Expr('SUM(' . $this->_db->quoteIdentifier($this->_tableMapping['emailMailQuota'] . '.' .
546                         $this->_propertyMapping['emailMailQuota']) . ') as mailQuota'),
547                 new Zend_Db_Expr('SUM(' . $this->_db->quoteIdentifier($this->_tableMapping['emailMailSize']  . '.' .
548                         $this->_propertyMapping['emailMailSize'])  . ') as mailSize'),
549                 //new Zend_Db_Expr('SUM(' . $this->_propertyMapping['emailSieveSize'] . ') as sieveSize'),
550             ))->query()->fetchAll();
551         return $data[0];
552     }
553
554     protected function _replaceValue(array &$array, array $replacements)
555     {
556         foreach($array as &$value) {
557             if (isset($replacements[$value])) {
558                 $value = $replacements[$value];
559             }
560         }
561     }
562
563 }