VMM/mailbox: Added to the repository. v0.6.x
authorPascal Volk <neverseen@users.sourceforge.net>
Fri, 23 Jul 2010 02:45:31 +0000
branchv0.6.x
changeset 309 d21423478803
parent 308 aa4a9fc31e1b
child 310 644e2cc4a441
VMM/mailbox: Added to the repository. VMM/Handler: Integrated mailbox module. Code cleanups.
VirtualMailManager/Handler.py
VirtualMailManager/mailbox.py
--- a/VirtualMailManager/Handler.py	Fri Jul 23 02:01:57 2010 +0000
+++ b/VirtualMailManager/Handler.py	Fri Jul 23 02:45:31 2010 +0000
@@ -30,6 +30,8 @@
 from VirtualMailManager.EmailAddress import EmailAddress
 from VirtualMailManager.errors import \
      DomainError, NotRootError, PermissionError, VMMError
+from VirtualMailManager.mailbox import new as new_mailbox
+from VirtualMailManager.pycompat import any
 from VirtualMailManager.Relocated import Relocated
 from VirtualMailManager.Transport import Transport
 
@@ -37,7 +39,6 @@
 _ = lambda msg: msg
 
 RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
-RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$"""
 TYPE_ACCOUNT = 0x1
 TYPE_ALIAS = 0x2
 TYPE_RELOCATED = 0x4
@@ -247,58 +248,12 @@
                        0, gid)
         os.chdir(oldpwd)
 
-    def __subscribe(self, folderlist, uid, gid):
-        """Creates a subscriptions file with the mailboxes from `folderlist`"""
-        fname = os.path.join(self._Cfg.dget('maildir.name'), 'subscriptions')
-        sf = open(fname, 'w')
-        sf.write('\n'.join(folderlist))
-        sf.write('\n')
-        sf.flush()
-        sf.close()
-        os.chown(fname, uid, gid)
-        os.chmod(fname, 384)
-
-    def __mailDirMake(self, domdir, uid, gid):
-        """Creates maildirs and maildir subfolders.
-
-        Keyword arguments:
-        domdir -- the path to the domain directory
-        uid -- user id from the account
-        gid -- group id from the account
-        """
-        #  obsolete -> (mailbox / maillocation)
-        return
+    def __make_home(self, account):
+        """Create a home directory for the new Account *account*."""
         os.umask(0007)
-        oldpwd = os.getcwd()
-        os.chdir(domdir)
-
-        maildir = self._Cfg.dget('maildir.name')
-        folders = [maildir]
-        append = folders.append
-        for folder in self._Cfg.dget('maildir.folders').split(':'):
-            folder = folder.strip()
-            if len(folder) and not folder.count('..'):
-                if re.match(RE_MBOX_NAMES, folder):
-                    append('%s/.%s' % (maildir, folder))
-                else:
-                    self.__warnings.append(_('Skipped mailbox folder: %r') %
-                                           folder)
-            else:
-                self.__warnings.append(_('Skipped mailbox folder: %r') %
-                                       folder)
-
-        subdirs = ['cur', 'new', 'tmp']
-        mode = self._Cfg.dget('account.directory_mode')
-
-        self.__makedir('%s' % uid, mode, uid, gid)
-        os.chdir('%s' % uid)
-        for folder in folders:
-            self.__makedir(folder, mode, uid, gid)
-            for subdir in subdirs:
-                self.__makedir(os.path.join(folder, subdir), mode, uid, gid)
-        self.__subscribe((f.replace(maildir + '/.', '') for f in folders[1:]),
-                         uid, gid)
-        os.chdir(oldpwd)
+        os.chdir(account.domain_directory)
+        os.mkdir('%s' % account.uid, self._Cfg.dget('account.directory_mode'))
+        os.chown('%s' % account.uid, account.uid, account.gid)
 
     def __userDirDelete(self, domdir, uid, gid):
         if uid > 0 and gid > 0:
@@ -390,10 +345,9 @@
             dom.update_transport(trsp, force=True)
 
     def domainDelete(self, domainname, force=None):
-        if not force is None and force not in ['deluser', 'delalias',
-                                               'delall']:
-                raise DomainError(_(u'Invalid argument: ā€œ%sā€') %
-                                         force, ERR.INVALID_OPTION)
+        if force and force not in ('deluser', 'delalias', 'delall'):
+            raise DomainError(_(u"Invalid argument: '%s'") % force,
+                              ERR.INVALID_OPTION)
         dom = self.__getDomain(domainname)
         gid = dom.gid
         domdir = dom.directory
@@ -497,8 +451,18 @@
         acc = self.__getAccount(emailaddress)
         acc.set_password(password)
         acc.save()
-        #  depends on modules mailbox and maillocation
-        #  self.__mailDirMake(acc.domain_directory, acc.uid, acc.gid)
+        oldpwd = os.getcwd()
+        self.__make_home(acc)
+        mailbox = new_mailbox(acc)
+        mailbox.create()
+        folders = self._Cfg.dget('mailbox.folders').split(':')
+        if any(folders):
+            bad = mailbox.add_boxes(folders,
+                                    self._Cfg.dget('mailbox.subscribe'))
+            if bad:
+                self.__warnings.append(_(u"Skipped mailbox folders:") +
+                                       '\n\t- ' + '\n\t- '.join(bad))
+        os.chdir(oldpwd)
 
     def aliasAdd(self, aliasaddress, *targetaddresses):
         """Creates a new `Alias` entry for the given *aliasaddress* with
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/mailbox.py	Fri Jul 23 02:45:31 2010 +0000
@@ -0,0 +1,293 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2010, Pascal Volk
+# See COPYING for distribution information.
+
+"""
+    VirtualMailManager.mailbox
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    VirtualMailManager's mailbox classes for the Maildir, single dbox
+    (sdbox) and multi dbox (mdbox) mailbox formats.
+"""
+
+import os
+import re
+from binascii import a2b_base64, b2a_base64
+from subprocess import Popen, PIPE
+
+from VirtualMailManager.Account import Account
+from VirtualMailManager.common import is_dir
+from VirtualMailManager.errors import VMMError
+from VirtualMailManager.constants.ERROR import VMM_ERROR
+
+
+__all__ = ('new', 'Maildir', 'SingleDbox', 'MultiDbox',
+           'utf8_to_mutf7', 'mutf7_to_utf8')
+
+cfg_dget = lambda option: None
+
+
+def _mbase64_encode(inp, dest):
+    if inp:
+        mb64 = b2a_base64(''.join(inp).encode('utf-16be'))
+        dest.append('&%s-' % mb64.rstrip('\n=').replace('/', ','))
+        del inp[:]
+
+
+def _mbase64_to_unicode(mb64):
+    return unicode(a2b_base64(mb64.replace(',', '/') + '==='), 'utf-16be')
+
+
+def utf8_to_mutf7(src):
+    """
+    Converts the international mailbox name `src` into a modified
+    version version of the UTF-7 encoding.
+    """
+    ret = []
+    tmp = []
+    for c in src:
+        ordc = ord(c)
+        if 0x20 <= ordc <= 0x25 or 0x27 <= ordc <= 0x7E:
+            _mbase64_encode(tmp, ret)
+            ret.append(c)
+        elif ordc == 0x26:
+            _mbase64_encode(tmp, ret)
+            ret.append('&-')
+        else:
+            tmp.append(c)
+    _mbase64_encode(tmp, ret)
+    return ''.join(ret)
+
+
+def mutf7_to_utf8(src):
+    """
+    Converts the mailbox name `src` from modified UTF-7 encoding to UTF-8.
+    """
+    ret = []
+    tmp = []
+    for c in src:
+        if c == '&' and not tmp:
+            tmp.append(c)
+        elif c == '-' and tmp:
+            if len(tmp) is 1:
+                ret.append('&')
+            else:
+                ret.append(_mbase64_to_unicode(''.join(tmp[1:])))
+            tmp = []
+        elif tmp:
+            tmp.append(c)
+        else:
+            ret.append(c)
+    if tmp:
+        ret.append(_mbase64_to_unicode(''.join(tmp[1:])))
+    return ''.join(ret)
+
+
+class Mailbox(object):
+    """Base class of all mailbox classes."""
+    __slots__ = ('_boxes', '_root', '_sep', '_user')
+    FILE_MODE = 0600
+    _ctrl_chr_re = re.compile('[\x00-\x1F\x7F-\x9F]')
+    _box_name_re = re.compile('^[\x20-\x25\x27-\x7E]+$')
+
+    def __init__(self, account):
+        """
+        Creates a new mailbox instance.
+        Use one of the `Maildir`, `SingleDbox` or `MultiDbox` classes.
+        """
+        assert isinstance(account, Account)
+        is_dir(account.home)
+        self._user = account
+        self._boxes = []
+        self._root = self._user.mail_location.directory
+        self._sep = '/'
+        os.chdir(self._user.home)
+
+    def _add_boxes(self, mailboxes, subscribe):
+        raise NotImplementedError
+
+    def _validate_box_name(self, name, good, bad):
+        """
+        Validates the mailboxes name `name`.  When the name is valid, it
+        will be added to the `good` set.  Invalid mailbox names will be
+        appended to the `bad` list.
+        """
+        name = name.strip()
+        if not name:
+            return
+        if self.__class__._ctrl_chr_re.search(name):  # no control chars
+            bad.append(name)
+            return
+        if name[0] in (self._sep, '~'):
+            bad.append(name)
+            return
+        if self._sep == '/':
+            if '//' in name or '/./' in name or '/../' in name or \
+               name.startswith('../'):
+                bad.append(name)
+                return
+        if '/' in name or '..' in name:
+            bad.append(name)
+            return
+        if not self.__class__._box_name_re.match(name):
+            tmp = utf8_to_mutf7(name)
+            if name == mutf7_to_utf8(tmp):
+                if self._user.mail_location.mbformat == 'maildir':
+                    good.add(tmp)
+                else:
+                    good.add(name)
+                return
+            else:
+                bad.append(name)
+                return
+        good.add(name)
+
+    def add_boxes(self, mailboxes, subscribe):
+        """
+        Create all mailboxes from the `mailboxes` list in the user's
+        mail directory.  When `subscribe` is ``True`` all created mailboxes
+        will be listed in the subscriptions file.
+        Returns a list of invalid mailbox names, if any.
+        """
+        assert isinstance(mailboxes, list) and isinstance(subscribe, bool)
+        good = set()
+        bad = []
+        for box in mailboxes:
+            self._validate_box_name(box, good, bad)
+        self._add_boxes(good, subscribe)
+        return bad
+
+    def create(self):
+        """Create the INBOX in the user's mail directory."""
+        raise NotImplementedError
+
+
+class Maildir(Mailbox):
+    """Class for Maildir++ mailboxes."""
+
+    __slots__ = ('_subdirs')
+
+    def __init__(self, account):
+        """
+        Create a new Maildir++ instance.
+        Call the instance's create() method, in order to create the INBOX.
+        For additional mailboxes use the add_boxes() method.
+        """
+        super(self.__class__, self).__init__(account)
+        self._sep = '.'
+        self._subdirs = ('cur', 'new', 'tmp')
+
+    def _create_maildirfolder_file(self, path):
+        """Mark the Maildir++ folder as Maildir folder."""
+        maildirfolder_file = os.path.join(self._sep + path, 'maildirfolder')
+        os.close(os.open(maildirfolder_file, os.O_CREAT | os.O_WRONLY,
+                         self.__class__.FILE_MODE))
+        os.chown(maildirfolder_file, self._user.uid, self._user.gid)
+
+    def _make_maildir(self, path):
+        """
+        Create Maildir++ folders with the cur, new and tmp subdirectories.
+        """
+        mode = cfg_dget('account.directory_mode')
+        uid = self._user.uid
+        gid = self._user.gid
+        os.mkdir(path, mode)
+        os.chown(path, uid, gid)
+        for subdir in self._subdirs:
+            dir_ = os.path.join(path, subdir)
+            os.mkdir(dir_, mode)
+            os.chown(dir_, uid, gid)
+
+    def _subscribe_boxes(self):
+        """Writes all created mailboxes to the subscriptions file."""
+        if not self._boxes:
+            return
+        subscriptions = open('subscriptions', 'w')
+        subscriptions.write('\n'.join(self._boxes))
+        subscriptions.write('\n')
+        subscriptions.flush()
+        subscriptions.close()
+        os.chown('subscriptions', self._user.uid, self._user.gid)
+        os.chmod('subscriptions', self.__class__.FILE_MODE)
+        del self._boxes[:]
+
+    def _add_boxes(self, mailboxes, subscribe):
+        for mailbox in mailboxes:
+            self._make_maildir(self._sep + mailbox)
+            self._create_maildirfolder_file(mailbox)
+            self._boxes.append(mailbox)
+        if subscribe:
+            self._subscribe_boxes()
+
+    def create(self):
+        """Creates a Maildir++ INBOX."""
+        self._make_maildir(self._root)
+        os.chdir(self._root)
+
+
+class SingleDbox(Mailbox):
+    """
+    Class for (single) dbox mailboxes.
+    See http://wiki.dovecot.org/MailboxFormat/dbox for details.
+    """
+
+    __slots__ = ()
+
+    def __init__(self, account):
+        """
+        Create a new dbox instance.
+        Call the instance's create() method, in order to create the INBOX.
+        For additional mailboxes use the add_boxes() method.
+        """
+        assert cfg_dget('misc.dovecot_version') >= \
+                account.mail_location.dovecot_version
+        super(SingleDbox, self).__init__(account)
+
+    def _doveadm_create(self, mailboxes, subscribe):
+        """Wrap around Dovecot's doveadm"""
+        cmd_args = [cfg_dget('bin.dovecotpw'), 'mailbox', 'create', '-u',
+                    str(self._user.address)]
+        if subscribe:
+            cmd_args.append('-s')
+        cmd_args.extend(mailboxes)
+        print '\n -> %r\n' % cmd_args
+        process = Popen(cmd_args, stdout=PIPE, stderr=PIPE)
+        stdout, stderr = process.communicate()
+        if process.returncode:
+            raise VMMError(stderr.strip(), VMM_ERROR)
+
+    def create(self):
+        """Create a dbox INBOX"""
+        os.mkdir(self._root, cfg_dget('account.directory_mode'))
+        os.chown(self._root, self._user.uid, self._user.gid)
+        self._doveadm_create(('INBOX',), False)
+        os.chdir(self._root)
+
+    def _add_boxes(self, mailboxes, subscribe):
+        self._doveadm_create(mailboxes, subscribe)
+
+
+class MultiDbox(SingleDbox):
+    """
+    Class for multi dbox mailboxes.
+    See http://wiki.dovecot.org/MailboxFormat/dbox#Multi-dbox for details.
+    """
+
+    __slots__ = ()
+
+
+def __get_mailbox_class(mbfmt):
+    if mbfmt == 'maildir':
+        return Maildir
+    elif mbfmt == 'mdbox':
+        return MultiDbox
+    elif mbfmt == 'sdbox':
+        return SingleDbox
+    raise ValueError('unsupported mailbox format: %r' % mbfmt)
+
+
+def new(account):
+    """Create a new Mailbox instance for the given Account."""
+    return __get_mailbox_class(account.mail_location.mbformat)(account)
+
+del cfg_dget