VirtualMailManager/mailbox.py
changeset 760 b678a1c43027
parent 748 659c4476c57c
child 761 e4e656f19771
equal deleted inserted replaced
748:659c4476c57c 760:b678a1c43027
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2010 - 2014, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5     VirtualMailManager.mailbox
       
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8     VirtualMailManager's mailbox classes for the Maildir, single dbox
       
     9     (sdbox) and multi dbox (mdbox) mailbox formats.
       
    10 """
       
    11 
       
    12 import os
       
    13 import re
       
    14 from binascii import a2b_base64, b2a_base64
       
    15 from subprocess import Popen, PIPE
       
    16 
       
    17 from VirtualMailManager.account import Account
       
    18 from VirtualMailManager.common import lisdir
       
    19 from VirtualMailManager.errors import VMMError
       
    20 from VirtualMailManager.constants import VMM_ERROR
       
    21 
       
    22 
       
    23 __all__ = ('new', 'Maildir', 'SingleDbox', 'MultiDbox',
       
    24            'utf8_to_mutf7', 'mutf7_to_utf8')
       
    25 
       
    26 _ = lambda msg: msg
       
    27 cfg_dget = lambda option: None
       
    28 
       
    29 
       
    30 def _mbase64_encode(inp, dest):
       
    31     if inp:
       
    32         mb64 = b2a_base64(''.join(inp).encode('utf-16be'))
       
    33         dest.append('&%s-' % mb64.rstrip('\n=').replace('/', ','))
       
    34         del inp[:]
       
    35 
       
    36 
       
    37 def _mbase64_to_unicode(mb64):
       
    38     return unicode(a2b_base64(mb64.replace(',', '/') + '==='), 'utf-16be')
       
    39 
       
    40 
       
    41 def utf8_to_mutf7(src):
       
    42     """
       
    43     Converts the international mailbox name `src` into a modified
       
    44     version version of the UTF-7 encoding.
       
    45     """
       
    46     ret = []
       
    47     tmp = []
       
    48     for c in src:
       
    49         ordc = ord(c)
       
    50         if 0x20 <= ordc <= 0x25 or 0x27 <= ordc <= 0x7E:
       
    51             _mbase64_encode(tmp, ret)
       
    52             ret.append(c)
       
    53         elif ordc == 0x26:
       
    54             _mbase64_encode(tmp, ret)
       
    55             ret.append('&-')
       
    56         else:
       
    57             tmp.append(c)
       
    58     _mbase64_encode(tmp, ret)
       
    59     return ''.join(ret)
       
    60 
       
    61 
       
    62 def mutf7_to_utf8(src):
       
    63     """
       
    64     Converts the mailbox name `src` from modified UTF-7 encoding to UTF-8.
       
    65     """
       
    66     ret = []
       
    67     tmp = []
       
    68     for c in src:
       
    69         if c == '&' and not tmp:
       
    70             tmp.append(c)
       
    71         elif c == '-' and tmp:
       
    72             if len(tmp) is 1:
       
    73                 ret.append('&')
       
    74             else:
       
    75                 ret.append(_mbase64_to_unicode(''.join(tmp[1:])))
       
    76             tmp = []
       
    77         elif tmp:
       
    78             tmp.append(c)
       
    79         else:
       
    80             ret.append(c)
       
    81     if tmp:
       
    82         ret.append(_mbase64_to_unicode(''.join(tmp[1:])))
       
    83     return ''.join(ret)
       
    84 
       
    85 
       
    86 class Mailbox(object):
       
    87     """Base class of all mailbox classes."""
       
    88     __slots__ = ('_boxes', '_root', '_sep', '_user')
       
    89     FILE_MODE = 0600
       
    90     _ctrl_chr_re = re.compile('[\x00-\x1F\x7F-\x9F]')
       
    91     _box_name_re = re.compile('^[\x20-\x25\x27-\x7E]+$')
       
    92 
       
    93     def __init__(self, account):
       
    94         """
       
    95         Creates a new mailbox instance.
       
    96         Use one of the `Maildir`, `SingleDbox` or `MultiDbox` classes.
       
    97         """
       
    98         assert isinstance(account, Account) and lisdir(account.home)
       
    99         self._user = account
       
   100         self._boxes = []
       
   101         self._root = self._user.mail_location.directory
       
   102         self._sep = '/'
       
   103         os.chdir(self._user.home)
       
   104 
       
   105     def _add_boxes(self, mailboxes, subscribe):
       
   106         """Create all mailboxes from the `mailboxes` list.
       
   107         If `subscribe` is *True*, the mailboxes will be listed in the
       
   108         subscriptions file."""
       
   109         raise NotImplementedError
       
   110 
       
   111     def _validate_box_name(self, name, good, bad):
       
   112         """
       
   113         Validates the mailboxes name `name`.  When the name is valid, it
       
   114         will be added to the `good` set.  Invalid mailbox names will be
       
   115         appended to the `bad` list.
       
   116         """
       
   117         name = name.strip()
       
   118         if not name:
       
   119             return
       
   120         if self.__class__._ctrl_chr_re.search(name):  # no control chars
       
   121             bad.append(name)
       
   122             return
       
   123         if name[0] in (self._sep, '~'):
       
   124             bad.append(name)
       
   125             return
       
   126         if self._sep == '/':
       
   127             if '//' in name or '/./' in name or '/../' in name or \
       
   128                name.startswith('../'):
       
   129                 bad.append(name)
       
   130                 return
       
   131         elif '/' in name or '..' in name:
       
   132             bad.append(name)
       
   133             return
       
   134         if not self.__class__._box_name_re.match(name):
       
   135             tmp = utf8_to_mutf7(name)
       
   136             if name == mutf7_to_utf8(tmp):
       
   137                 if self._user.mail_location.mbformat == 'maildir':
       
   138                     good.add(tmp)
       
   139                 else:
       
   140                     good.add(name)
       
   141                 return
       
   142             else:
       
   143                 bad.append(name)
       
   144                 return
       
   145         good.add(name)
       
   146 
       
   147     def add_boxes(self, mailboxes, subscribe):
       
   148         """
       
   149         Create all mailboxes from the `mailboxes` list in the user's
       
   150         mail directory.  When `subscribe` is ``True`` all created mailboxes
       
   151         will be listed in the subscriptions file.
       
   152         Returns a list of invalid mailbox names, if any.
       
   153         """
       
   154         assert isinstance(mailboxes, list) and isinstance(subscribe, bool)
       
   155         good = set()
       
   156         bad = []
       
   157         for box in mailboxes:
       
   158             if self._sep == '/':
       
   159                 box = box.replace('.', self._sep)
       
   160             self._validate_box_name(box, good, bad)
       
   161         self._add_boxes(good, subscribe)
       
   162         return bad
       
   163 
       
   164     def create(self):
       
   165         """Create the INBOX in the user's mail directory."""
       
   166         raise NotImplementedError
       
   167 
       
   168 
       
   169 class Maildir(Mailbox):
       
   170     """Class for Maildir++ mailboxes."""
       
   171 
       
   172     __slots__ = ('_subdirs')
       
   173 
       
   174     def __init__(self, account):
       
   175         """
       
   176         Create a new Maildir++ instance.
       
   177         Call the instance's create() method, in order to create the INBOX.
       
   178         For additional mailboxes use the add_boxes() method.
       
   179         """
       
   180         super(self.__class__, self).__init__(account)
       
   181         self._sep = '.'
       
   182         self._subdirs = ('cur', 'new', 'tmp')
       
   183 
       
   184     def _create_maildirfolder_file(self, path):
       
   185         """Mark the Maildir++ folder as Maildir folder."""
       
   186         maildirfolder_file = os.path.join(self._sep + path, 'maildirfolder')
       
   187         os.close(os.open(maildirfolder_file, os.O_CREAT | os.O_WRONLY,
       
   188                          self.__class__.FILE_MODE))
       
   189         os.chown(maildirfolder_file, self._user.uid, self._user.gid)
       
   190 
       
   191     def _make_maildir(self, path):
       
   192         """
       
   193         Create Maildir++ folders with the cur, new and tmp subdirectories.
       
   194         """
       
   195         mode = cfg_dget('account.directory_mode')
       
   196         uid = self._user.uid
       
   197         gid = self._user.gid
       
   198         os.mkdir(path, mode)
       
   199         os.chown(path, uid, gid)
       
   200         for subdir in self._subdirs:
       
   201             dir_ = os.path.join(path, subdir)
       
   202             os.mkdir(dir_, mode)
       
   203             os.chown(dir_, uid, gid)
       
   204 
       
   205     def _subscribe_boxes(self):
       
   206         """Writes all created mailboxes to the subscriptions file."""
       
   207         if not self._boxes:
       
   208             return
       
   209         subscriptions = open('subscriptions', 'w')
       
   210         subscriptions.write('\n'.join(self._boxes))
       
   211         subscriptions.write('\n')
       
   212         subscriptions.flush()
       
   213         subscriptions.close()
       
   214         os.chown('subscriptions', self._user.uid, self._user.gid)
       
   215         os.chmod('subscriptions', self.__class__.FILE_MODE)
       
   216         del self._boxes[:]
       
   217 
       
   218     def _add_boxes(self, mailboxes, subscribe):
       
   219         for mailbox in mailboxes:
       
   220             self._make_maildir(self._sep + mailbox)
       
   221             self._create_maildirfolder_file(mailbox)
       
   222             self._boxes.append(mailbox)
       
   223         if subscribe:
       
   224             self._subscribe_boxes()
       
   225 
       
   226     def create(self):
       
   227         """Creates a Maildir++ INBOX."""
       
   228         self._make_maildir(self._root)
       
   229         os.chdir(self._root)
       
   230 
       
   231 
       
   232 class SingleDbox(Mailbox):
       
   233     """
       
   234     Class for (single) dbox mailboxes.
       
   235     See http://wiki.dovecot.org/MailboxFormat/dbox for details.
       
   236     """
       
   237 
       
   238     __slots__ = ()
       
   239 
       
   240     def __init__(self, account):
       
   241         """
       
   242         Create a new dbox instance.
       
   243         Call the instance's create() method, in order to create the INBOX.
       
   244         For additional mailboxes use the add_boxes() method.
       
   245         """
       
   246         assert cfg_dget('misc.dovecot_version') >= \
       
   247                 account.mail_location.dovecot_version
       
   248         super(SingleDbox, self).__init__(account)
       
   249 
       
   250     def _doveadm_create(self, mailboxes, subscribe):
       
   251         """Wrap around Dovecot's doveadm"""
       
   252         cmd_args = [cfg_dget('bin.dovecotpw'), 'mailbox', 'create', '-u',
       
   253                     str(self._user.address)]
       
   254         if subscribe:
       
   255             cmd_args.append('-s')
       
   256         cmd_args.extend(mailboxes)
       
   257         process = Popen(cmd_args, stderr=PIPE)
       
   258         stderr = process.communicate()[1]
       
   259         if process.returncode:
       
   260             e_msg = _(u'Failed to create mailboxes: %r\n') % mailboxes
       
   261             raise VMMError(e_msg + stderr.strip(), VMM_ERROR)
       
   262 
       
   263     def create(self):
       
   264         """Create a dbox INBOX"""
       
   265         os.mkdir(self._root, cfg_dget('account.directory_mode'))
       
   266         os.chown(self._root, self._user.uid, self._user.gid)
       
   267         self._doveadm_create(('INBOX',), False)
       
   268         os.chdir(self._root)
       
   269 
       
   270     def _add_boxes(self, mailboxes, subscribe):
       
   271         self._doveadm_create(mailboxes, subscribe)
       
   272 
       
   273 
       
   274 class MultiDbox(SingleDbox):
       
   275     """
       
   276     Class for multi dbox mailboxes.
       
   277     See http://wiki.dovecot.org/MailboxFormat/dbox#Multi-dbox for details.
       
   278     """
       
   279 
       
   280     __slots__ = ()
       
   281 
       
   282 
       
   283 def new(account):
       
   284     """Create a new Mailbox instance for the given Account."""
       
   285     mbfmt = account.mail_location.mbformat
       
   286     if mbfmt == 'maildir':
       
   287         return Maildir(account)
       
   288     elif mbfmt == 'mdbox':
       
   289         return MultiDbox(account)
       
   290     elif mbfmt == 'sdbox':
       
   291         return SingleDbox(account)
       
   292     raise ValueError('unsupported mailbox format: %r' % mbfmt)
       
   293 
       
   294 del _, cfg_dget