allow to define supported SQL adapter in email plugin
[tine20] / tine20 / Tinebase / EmailUser / Imap / Dovecot.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-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Michael Fronk
10  * 
11  * 
12  * 
13  * example dovecot db schema:
14  * 
15 --
16 -- Database: `dovecot`
17 --
18 -- --------------------------------------------------------
19
20 --
21 -- Table structure for table `dovecot_users`
22 --
23
24 CREATE TABLE IF NOT EXISTS `dovecot_users` (
25 `userid`        VARCHAR( 40 ) NOT NULL ,
26 `domain`        VARCHAR( 80 ) NOT NULL DEFAULT '',
27 `username`      VARCHAR( 80 ) NOT NULL ,
28 `password`      VARCHAR( 100 ) NOT NULL ,
29 `quota_bytes`   BIGINT NOT NULL DEFAULT '0',
30 `quota_message` INT NOT NULL DEFAULT '0',
31 `uid`           VARCHAR( 20 ) DEFAULT NULL ,
32 `gid`           VARCHAR( 20 ) DEFAULT NULL ,
33 `home`          VARCHAR( 256 ) DEFAULT NULL ,
34 `last_login`    DATETIME DEFAULT NULL ,
35 PRIMARY KEY (`userid`, `domain`),
36 UNIQUE (`username`)
37 ) ENGINE = InnoDB DEFAULT CHARSET=utf8;
38 -- --------------------------------------------------------
39
40 --
41 -- Table structure for table `dovecot_usage`
42 --
43
44 CREATE TABLE IF NOT EXISTS `dovecot_usage` (
45 `username` VARCHAR( 80 ) NOT NULL ,
46 `storage`  BIGINT NOT NULL DEFAULT '0',
47 `messages` BIGINT NOT NULL DEFAULT '0',
48 PRIMARY KEY (`username`),
49 CONSTRAINT `dovecot_usage::username--dovecot_users::username` FOREIGN KEY (`username`) REFERENCES `dovecot_users` (`username`) ON DELETE CASCADE ON UPDATE CASCADE
50 ) ENGINE=Innodb DEFAULT CHARSET=utf8;
51 -- --------------------------------------------------------
52
53
54 * Example Dovecot Config files
55
56
57 --
58 -- Auth and User Query: dovecot-sql.conf 
59 -- 
60 -- Note: Currently Tine Sieve Quota is used as Message Quota
61 -- Note: Querys should be a single line
62 --
63
64 driver = mysql
65 connect = host=127.0.0.1 dbname=DovecotDB user=DovecotUser password=DovecotPass
66 default_pass_scheme = PLAIN-MD5
67
68 # passdb with userdb prefetch
69 password_query = SELECT dovecot_users.username AS user, 
70     CONCAT('{', scheme, '}', password) AS password, 
71     home AS userdb_home, 
72     uid AS userdb_uid, 
73     gid AS userdb_gid, 
74     CONCAT('*:bytes=', CAST(quota_bytes AS CHAR), 'M') AS userdb_quota_rule   
75     FROM dovecot_users 
76     WHERE dovecot_users.username='%u'
77
78 # userdb for deliver
79 user_query = SELECT home, uid, gid, 
80     CONCAT('*:bytes=', CAST(quota_bytes AS CHAR), 'M') AS userdb_quota_rule   
81     FROM dovecot_users 
82     WHERE dovecot_users.username='%u'
83 -- --------------------------------------------------------
84
85 -- 
86 -- Quotas Config: dovecot-dict-sql.conf
87 --
88 -- Note: Currently Tine Sieve Quota is used as Message Quota
89 --
90
91 connect = host=127.0.0.1 dbname=DovecotDB user=DovecotUser password=DovecotPass
92
93 map {
94   pattern = priv/quota/storage
95   table = dovecot_usage
96   username_field = username
97   value_field = storage
98 }
99
100 map {
101   pattern = priv/quota/messages
102   table = dovecot_usage
103   username_field = username
104   value_field = messages
105 }
106
107 -- ----------------------------------------------------
108
109
110 * Example Postfix Config Files
111
112
113 --
114 -- Postfix LDA config: master.cf
115 --
116 -- Note: Dovecot Tine backend does not support peruser storage, 
117 --         but you can use the dovecot server for multiple 
118 --         sites. So in other words pertine storage
119
120 -- All mail is stored as vmail
121 dovecot   unix  -       n       n       -       -       pipe
122     flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -d ${recipient}
123
124 -- Mail is stored on peruser/persite
125 dovelda   unix  -       n       n       -       -       pipe
126     flags=DRhu user=dovelda:dovelda argv=/usr/bin/sudo /usr/lib/dovecot/deliver -d ${recipient}
127 -- ------------------------------------------------------
128
129 --
130 -- sudoers entry for peruser/persite config
131 --
132
133 Defaults:dovelda !syslog
134 dovelda          ALL=NOPASSWD:/usr/lib/dovecot/deliver
135 -- ----------------------------------------------------
136
137 --
138 -- Postfix virtual_mailbox_domains: sql-virtual_mailbox_domains.cf
139 --
140
141 user     = smtpUser
142 password = smtpPass
143 hosts    = 127.0.0.1
144 dbname   = smtp
145 query    = SELECT DISTINCT 1 FROM smtp_aliases WHERE SUBSTRING_INDEX(source, '@', -1) = '%s';
146 -- ----------------------------------------------------
147
148 --
149 -- Postfix sql-virtual_mailbox_maps: sql-virtual_mailbox_maps.cf
150 --
151
152 user     = smtpUser
153 password = smtpPass
154 hosts    = 127.0.0.1
155 dbname   = smtp
156 query    = SELECT 1 FROM smtp_users WHERE username='%s' AND forward_only=0
157 -- ----------------------------------------------------
158
159 --
160 -- Postfix sql-virtual_alias_maps: sql-virtual_alias_maps_aliases.cf
161 --
162
163 user     = smtpUser
164 password = smtpPass
165 hosts    = 127.0.0.1
166 dbname   = smtp
167 query = SELECT destination FROM smtp_aliases WHERE source='%s'
168
169 -- -----------------------------------------------------
170 */
171
172 /**
173  * plugin to handle dovecot imap accounts
174  * 
175  * @package    Tinebase
176  * @subpackage EmailUser
177  */
178 class Tinebase_EmailUser_Imap_Dovecot extends Tinebase_EmailUser_Sql
179 {
180     /**
181      * quotas table name with prefix
182      *
183      * @var string
184      */
185     protected $_quotasTable = NULL;
186     
187     /**
188      * email user config
189      * 
190      * @var array 
191      */
192     protected $_config = array(
193         'prefix'            => 'dovecot_',
194         'userTable'         => 'users',
195         'quotaTable'        => 'usage',
196         'emailHome'         => '/var/vmail/%d/%n',
197         'emailUID'          => 'vmail', 
198         'emailGID'          => 'vmail',
199         'emailScheme'       => 'SSHA256',
200         'domain'            => null,
201         'adapter'           => Tinebase_Core::PDO_MYSQL
202     );
203     
204     /**
205      * user properties mapping
206      *
207      * @var array
208      */
209     protected $_propertyMapping = array(
210         'emailUserId'       => 'userid',
211         'emailUsername'     => 'username',
212         'emailPassword'     => 'password',
213         'emailUID'          => 'uid', 
214         'emailGID'          => 'gid', 
215         'emailLastLogin'    => 'last_login',
216         'emailMailQuota'    => 'quota_bytes',
217         #'emailSieveQuota'   => 'quota_message',
218     
219         'emailMailSize'     => 'storage',
220         'emailSieveSize'    => 'messages',
221
222         // makes mapping data to _config easier
223         'emailHome'            => 'home'
224     );
225     
226     /**
227      * Dovecot readonly
228      * 
229      * @var array
230      */
231     protected $_readOnlyFields = array(
232         'emailMailSize',
233         'emailSieveSize',
234         'emailLastLogin',
235     );
236     
237     /**
238      * the constructor
239      */
240     public function __construct(array $_options = array())
241     {
242         $this->_configKey = Tinebase_Config::IMAP;
243         $this->_subconfigKey = 'dovecot';
244         
245         $emailConfig = parent::__construct($_options);
246         
247         // _quotaTable = dovecot_aliases
248         $this->_quotasTable = $this->_config['prefix'] . $this->_config['quotaTable'];
249         
250         // set domain from imap config
251         $emailConfig =Tinebase_Config::getInstance()->get($this->_configKey, new Tinebase_Config_Struct())->toArray();
252         $this->_config['domain'] = !empty($emailConfig['domain']) ? $emailConfig['domain'] : null;
253         
254         // copy over default scheme, home, UID, GID from preconfigured defaults
255         $this->_config['emailScheme'] = $this->_config['scheme'];
256         $this->_config['emailHome']   = $this->_config['home'];
257         $this->_config['emailUID']    = $this->_config['uid'];
258         $this->_config['emailGID']    = $this->_config['gid'];
259     }
260     
261     /**
262      * get the basic select object to fetch records from the database
263      *  
264      * @param  array|string|Zend_Db_Expr  $_cols        columns to get, * per default
265      * @param  boolean                    $_getDeleted  get deleted records (if modlog is active)
266      * @return Zend_Db_Select
267      * 
268      * SELECT dovecot_users.*, dovecot_quotas.mail_quota, dovecot_quotas.mail_size, dovecot_quotas.sieve_quota, dovecot_quotas.sieve_size
269      * FROM dovecot_users 
270      * LEFT JOIN dovecot_quotas
271      * ON (dovecot_users.username=dovecot_quotas.username)
272      * WHERE dovecot_users.userid = $_userId
273      * LIMIT 1
274      */
275     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
276     {
277         $select = $this->_db->select()
278         
279             ->from(array($this->_userTable))
280
281             // Left Join Quotas Table
282             ->joinLeft(
283                 array($this->_quotasTable), // table
284                 '(' . $this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUsername']) .  ' = ' . // ON (left)
285                     $this->_db->quoteIdentifier($this->_quotasTable . '.' . $this->_propertyMapping['emailUsername']) . ')', // ON (right)
286                 array( // Select
287                     $this->_propertyMapping['emailMailSize']  => $this->_quotasTable . '.' . $this->_propertyMapping['emailMailSize'], // emailMailSize
288                     $this->_propertyMapping['emailSieveSize'] => $this->_quotasTable . '.' . $this->_propertyMapping['emailSieveSize'] // emailSieveSize
289                 ) 
290             )
291             
292             // Only want 1 user (shouldn't be more than 1 anyway)
293             ->limit(1);
294         
295         // append domain if set or domain IS NULL
296         if ((isset($this->_config['domain']) || array_key_exists('domain', $this->_config)) && ! empty($this->_config['domain'])) {
297             $select->where($this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . ' = ?',   $this->_config['domain']);
298         } else {
299             $select->where($this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . " = ''");
300         }
301         
302         return $select;
303     }
304     
305     /**
306      * converts raw data from adapter into a single record / do mapping
307      *
308      * @param  array                    $_data
309      * @return Tinebase_Model_EmailUser
310      */
311     protected function _rawDataToRecord(array $_rawdata)
312     {
313         $data = array();
314         
315         foreach ($_rawdata as $key => $value) {
316             $keyMapping = array_search($key, $this->_propertyMapping);
317             if ($keyMapping !== FALSE) {
318                 switch($keyMapping) {
319                     case 'emailPassword':
320                     case 'emailAliases':
321                     case 'emailForwards':
322                     case 'emailForwardOnly':
323                     case 'emailAddress':
324                         // do nothing
325                         break;
326                     case 'emailMailQuota':
327                     case 'emailSieveQuota':
328                         $data[$keyMapping] = $value > 0 ? $value : null;
329                         break;
330                     case 'emailMailSize':
331                         $data[$keyMapping] = $value > 0 ? round($value/1048576, 2) : 0;
332                         break;
333                     default: 
334                         $data[$keyMapping] = $value;
335                         break;
336                 }
337             }
338         }
339         
340         return new Tinebase_Model_EmailUser($data, true);
341     }
342      
343     /**
344      * returns array of raw Dovecot data
345      *
346      * @param  Tinebase_Model_FullUser  $_user
347      * @param  Tinebase_Model_FullUser  $_newUserProperties
348      * @return array
349      */
350     protected function _recordToRawData(Tinebase_Model_FullUser $_user, Tinebase_Model_FullUser $_newUserProperties)
351     {
352         $rawData = array();
353         
354         if (isset($_newUserProperties->imapUser)) {
355             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_newUserProperties->imapUser->toArray(), true));
356             
357             foreach ($_newUserProperties->imapUser as $key => $value) {
358                 $property = (isset($this->_propertyMapping[$key]) || array_key_exists($key, $this->_propertyMapping)) ? $this->_propertyMapping[$key] : false;
359                 if ($property && ! in_array($key, $this->_readOnlyFields)) {
360                     switch ($key) {
361                         case 'emailUserId':
362                         case 'emailUsername':
363                             // do nothing
364                             break;
365                             
366                         case 'emailPassword':
367                             $rawData[$property] = Hash_Password::generate($this->_config['emailScheme'], $value);
368                             break;
369                             
370                         case 'emailUID':
371                             $rawData[$property] = !empty($this->_config['uid']) ? $this->_config['uid'] : $value;
372                             break;
373                             
374                         case 'emailGID':
375                             $rawData[$property] = !empty($this->_config['gid']) ? $this->_config['gid'] : $value;
376                             break;
377                         case 'emailMailQuota':
378                             $rawData[$property] = (empty($value)) ? 0 : $value;
379                             break;
380                             
381                         default:
382                             $rawData[$property] = $value;
383                             break;
384                     }
385                 }
386             }
387         }
388         
389         foreach (array('uid', 'gid') as $key) {
390             if (! (isset($rawData[$key]) || array_key_exists($key, $rawData))) {
391                 $rawData[$key] = $this->_config[$key];
392             }
393         }
394         
395         $rawData[$this->_propertyMapping['emailUserId']]   = $_user->getId();
396         
397         if ($this->_config['domain'] !== null) {
398             $emailUsername = $this->_appendDomain($_user->accountLoginName);
399         } else {
400             $emailUsername = $_newUserProperties->accountEmailAddress;
401         }
402         
403         list($localPart, $domain) = explode('@', $emailUsername, 2);
404         $rawData['domain'] = $domain;
405         
406         // replace home wildcards when storing to db
407         // %d = domain
408         // %n = user
409         // %u == user@domain
410         $search = array('%n', '%d', '%u');
411         $replace = array(
412             $localPart,
413             $domain,
414             $emailUsername
415         );
416
417         $rawData[$this->_propertyMapping['emailHome']] = str_replace($search, $replace, $this->_config['emailHome']);
418         $rawData[$this->_propertyMapping['emailUsername']] = $emailUsername;
419         
420         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($rawData, true));
421         
422         return $rawData;
423     }
424 }