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