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