30bc798903501003dee593d556cd9152013509a3
[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     );
202     
203     /**
204      * user properties mapping
205      *
206      * @var array
207      */
208     protected $_propertyMapping = array(
209         'emailUserId'       => 'userid',
210         'emailUsername'     => 'username',
211         'emailPassword'     => 'password',
212         'emailUID'          => 'uid', 
213         'emailGID'          => 'gid', 
214         'emailLastLogin'    => 'last_login',
215         'emailMailQuota'    => 'quota_bytes',
216         #'emailSieveQuota'   => 'quota_message',
217     
218         'emailMailSize'     => 'storage',
219         'emailSieveSize'    => 'messages',
220
221         // makes mapping data to _config easier
222         'emailHome'            => 'home'
223     );
224     
225     /**
226      * Dovecot readonly
227      * 
228      * @var array
229      */
230     protected $_readOnlyFields = array(
231         'emailMailSize',
232         'emailSieveSize',
233         'emailLastLogin',
234     );
235     
236     /**
237      * the constructor
238      */
239     public function __construct(array $_options = array())
240     {
241         $this->_configKey = Tinebase_Config::IMAP;
242         $this->_subconfigKey = 'dovecot';
243         
244         $emailConfig = parent::__construct($_options);
245         
246         // _quotaTable = dovecot_aliases
247         $this->_quotasTable = $this->_config['prefix'] . $this->_config['quotaTable'];
248         
249         // set domain from imap config
250         $emailConfig =Tinebase_Config::getInstance()->get($this->_configKey, new Tinebase_Config_Struct())->toArray();
251         $this->_config['domain'] = !empty($emailConfig['domain']) ? $emailConfig['domain'] : null;
252         
253         // copy over default scheme, home, UID, GID from preconfigured defaults
254         $this->_config['emailScheme'] = $this->_config['scheme'];
255         $this->_config['emailHome']   = $this->_config['home'];
256         $this->_config['emailUID']    = $this->_config['uid'];
257         $this->_config['emailGID']    = $this->_config['gid'];
258     }
259     
260     /**
261      * get the basic select object to fetch records from the database
262      *  
263      * @param  array|string|Zend_Db_Expr  $_cols        columns to get, * per default
264      * @param  boolean                    $_getDeleted  get deleted records (if modlog is active)
265      * @return Zend_Db_Select
266      * 
267      * SELECT dovecot_users.*, dovecot_quotas.mail_quota, dovecot_quotas.mail_size, dovecot_quotas.sieve_quota, dovecot_quotas.sieve_size
268      * FROM dovecot_users 
269      * LEFT JOIN dovecot_quotas
270      * ON (dovecot_users.username=dovecot_quotas.username)
271      * WHERE dovecot_users.userid = $_userId
272      * LIMIT 1
273      */
274     protected function _getSelect($_cols = '*', $_getDeleted = FALSE)
275     {
276         $select = $this->_db->select()
277         
278             ->from(array($this->_userTable))
279
280             // Left Join Quotas Table
281             ->joinLeft(
282                 array($this->_quotasTable), // table
283                 '(' . $this->_db->quoteIdentifier($this->_userTable . '.' . $this->_propertyMapping['emailUsername']) .  ' = ' . // ON (left)
284                     $this->_db->quoteIdentifier($this->_quotasTable . '.' . $this->_propertyMapping['emailUsername']) . ')', // ON (right)
285                 array( // Select
286                     $this->_propertyMapping['emailMailSize']  => $this->_quotasTable . '.' . $this->_propertyMapping['emailMailSize'], // emailMailSize
287                     $this->_propertyMapping['emailSieveSize'] => $this->_quotasTable . '.' . $this->_propertyMapping['emailSieveSize'] // emailSieveSize
288                 ) 
289             )
290             
291             // Only want 1 user (shouldn't be more than 1 anyway)
292             ->limit(1);
293         
294         // append domain if set or domain IS NULL
295         if ((isset($this->_config['domain']) || array_key_exists('domain', $this->_config)) && ! empty($this->_config['domain'])) {
296             $select->where($this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . ' = ?',   $this->_config['domain']);
297         } else {
298             $select->where($this->_db->quoteIdentifier($this->_userTable . '.' . 'domain') . " = ''");
299         }
300         
301         return $select;
302     }
303     
304     /**
305      * converts raw data from adapter into a single record / do mapping
306      *
307      * @param  array                    $_data
308      * @return Tinebase_Model_EmailUser
309      */
310     protected function _rawDataToRecord(array $_rawdata)
311     {
312         $data = array();
313         
314         foreach ($_rawdata as $key => $value) {
315             $keyMapping = array_search($key, $this->_propertyMapping);
316             if ($keyMapping !== FALSE) {
317                 switch($keyMapping) {
318                     case 'emailPassword':
319                     case 'emailAliases':
320                     case 'emailForwards':
321                     case 'emailForwardOnly':
322                     case 'emailAddress':
323                         // do nothing
324                         break;
325                     case 'emailMailQuota':
326                     case 'emailSieveQuota':
327                         $data[$keyMapping] = $value > 0 ? $value : null;
328                         break;
329                     case 'emailMailSize':
330                         $data[$keyMapping] = $value > 0 ? round($value/1048576, 2) : 0;
331                         break;
332                     default: 
333                         $data[$keyMapping] = $value;
334                         break;
335                 }
336             }
337         }
338         
339         return new Tinebase_Model_EmailUser($data, true);
340     }
341      
342     /**
343      * returns array of raw Dovecot data
344      *
345      * @param  Tinebase_Model_FullUser  $_user
346      * @param  Tinebase_Model_FullUser  $_newUserProperties
347      * @return array
348      */
349     protected function _recordToRawData(Tinebase_Model_FullUser $_user, Tinebase_Model_FullUser $_newUserProperties)
350     {
351         $rawData = array();
352         
353         if (isset($_newUserProperties->imapUser)) {
354             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_newUserProperties->imapUser->toArray(), true));
355             
356             foreach ($_newUserProperties->imapUser as $key => $value) {
357                 $property = (isset($this->_propertyMapping[$key]) || array_key_exists($key, $this->_propertyMapping)) ? $this->_propertyMapping[$key] : false;
358                 if ($property && ! in_array($key, $this->_readOnlyFields)) {
359                     switch ($key) {
360                         case 'emailUserId':
361                         case 'emailUsername':
362                             // do nothing
363                             break;
364                             
365                         case 'emailPassword':
366                             $rawData[$property] = Hash_Password::generate($this->_config['emailScheme'], $value);
367                             break;
368                             
369                         case 'emailUID':
370                             $rawData[$property] = !empty($this->_config['uid']) ? $this->_config['uid'] : $value;
371                             break;
372                             
373                         case 'emailGID':
374                             $rawData[$property] = !empty($this->_config['gid']) ? $this->_config['gid'] : $value;
375                             break;
376                         case 'emailMailQuota':
377                             $rawData[$property] = (empty($value)) ? 0 : $value;
378                             break;
379                             
380                         default:
381                             $rawData[$property] = $value;
382                             break;
383                     }
384                 }
385             }
386         }
387         
388         foreach (array('uid', 'gid') as $key) {
389             if (! (isset($rawData[$key]) || array_key_exists($key, $rawData))) {
390                 $rawData[$key] = $this->_config[$key];
391             }
392         }
393         
394         $rawData[$this->_propertyMapping['emailUserId']]   = $_user->getId();
395         
396         if ($this->_config['domain'] !== null) {
397             $emailUsername = $this->_appendDomain($_user->accountLoginName);
398         } else {
399             $emailUsername = $_newUserProperties->accountEmailAddress;
400         }
401         
402         list($localPart, $domain) = explode('@', $emailUsername, 2);
403         $rawData['domain'] = $domain;
404         
405         // replace home wildcards when storing to db
406         // %d = domain
407         // %n = user
408         // %u == user@domain
409         $search = array('%n', '%d', '%u');
410         $replace = array(
411             $localPart,
412             $domain,
413             $emailUsername
414         );
415
416         $rawData[$this->_propertyMapping['emailHome']] = str_replace($search, $replace, $this->_config['emailHome']);
417         $rawData[$this->_propertyMapping['emailUsername']] = $emailUsername;
418         
419         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($rawData, true));
420         
421         return $rawData;
422     }
423 }