# HG changeset patch # User Pascal Volk # Date 1279853131 0 # Node ID d214234788035dd126ffaaecf48ef05d5640c67d # Parent aa4a9fc31e1bab118311dda23b5e2437fed98954 VMM/mailbox: Added to the repository. VMM/Handler: Integrated mailbox module. Code cleanups. diff -r aa4a9fc31e1b -r d21423478803 VirtualMailManager/Handler.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 diff -r aa4a9fc31e1b -r d21423478803 VirtualMailManager/mailbox.py --- /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