# -*- coding: UTF-8 -*-# Copyright (c) 2010 - 2013, 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."""importosimportrefrombinasciiimporta2b_base64,b2a_base64fromsubprocessimportPopen,PIPEfromVirtualMailManagerimportENCODINGfromVirtualMailManager.accountimportAccountfromVirtualMailManager.commonimportlisdirfromVirtualMailManager.errorsimportVMMErrorfromVirtualMailManager.constantsimportVMM_ERROR__all__=('new','Maildir','SingleDbox','MultiDbox','utf8_to_mutf7','mutf7_to_utf8')_=lambdamsg:msgcfg_dget=lambdaoption:Nonedef_mbase64_encode(inp,dest):ifinp:mb64=b2a_base64(''.join(inp).encode('utf-16be')).decode()dest.append('&%s-'%mb64.rstrip('\n=').replace('/',','))delinp[:]def_mbase64_to_unicode(mb64):returnstr(a2b_base64(mb64.replace(',','/').encode()+b'==='),'utf-16be')defutf8_to_mutf7(src):""" Converts the international mailbox name `src` into a modified version version of the UTF-7 encoding. """ret=[]tmp=[]forcinsrc:ordc=ord(c)if0x20<=ordc<=0x25or0x27<=ordc<=0x7E:_mbase64_encode(tmp,ret)ret.append(c)elifordc==0x26:_mbase64_encode(tmp,ret)ret.append('&-')else:tmp.append(c)_mbase64_encode(tmp,ret)return''.join(ret)defmutf7_to_utf8(src):""" Converts the mailbox name `src` from modified UTF-7 encoding to UTF-8. """ret=[]tmp=[]forcinsrc:ifc=='&'andnottmp:tmp.append(c)elifc=='-'andtmp:iflen(tmp)is1:ret.append('&')else:ret.append(_mbase64_to_unicode(''.join(tmp[1:])))tmp=[]eliftmp:tmp.append(c)else:ret.append(c)iftmp:ret.append(_mbase64_to_unicode(''.join(tmp[1:])))return''.join(ret)classMailbox(object):"""Base class of all mailbox classes."""__slots__=('_boxes','_root','_sep','_user')FILE_MODE=0o600_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. """assertisinstance(account,Account)andlisdir(account.home)self._user=accountself._boxes=[]self._root=self._user.mail_location.directoryself._sep='/'os.chdir(self._user.home)def_add_boxes(self,mailboxes,subscribe):"""Create all mailboxes from the `mailboxes` list. If `subscribe` is *True*, the mailboxes will be listed in the subscriptions file."""raiseNotImplementedErrordef_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()ifnotname:returnifself.__class__._ctrl_chr_re.search(name):# no control charsbad.append(name)returnifname[0]in(self._sep,'~'):bad.append(name)returnifself._sep=='/':if'//'innameor'/./'innameor'/../'innameor \name.startswith('../'):bad.append(name)returnelif'/'innameor'..'inname:bad.append(name)returnifnotself.__class__._box_name_re.match(name):tmp=utf8_to_mutf7(name)ifname==mutf7_to_utf8(tmp):ifself._user.mail_location.mbformat=='maildir':good.add(tmp)else:good.add(name)returnelse:bad.append(name)returngood.add(name)defadd_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. """assertisinstance(mailboxes,list)andisinstance(subscribe,bool)good=set()bad=[]forboxinmailboxes:ifself._sep=='/':box=box.replace('.',self._sep)self._validate_box_name(box,good,bad)self._add_boxes(good,subscribe)returnbaddefcreate(self):"""Create the INBOX in the user's mail directory."""raiseNotImplementedErrorclassMaildir(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.uidgid=self._user.gidos.mkdir(path,mode)os.chown(path,uid,gid)forsubdirinself._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."""ifnotself._boxes:returnwithopen('subscriptions','w')assubscriptions:subscriptions.write('\n'.join(self._boxes))subscriptions.write('\n')os.chown('subscriptions',self._user.uid,self._user.gid)os.chmod('subscriptions',self.__class__.FILE_MODE)delself._boxes[:]def_add_boxes(self,mailboxes,subscribe):formailboxinmailboxes:self._make_maildir(self._sep+mailbox)self._create_maildirfolder_file(mailbox)self._boxes.append(mailbox)ifsubscribe:self._subscribe_boxes()defcreate(self):"""Creates a Maildir++ INBOX."""self._make_maildir(self._root)os.chdir(self._root)classSingleDbox(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. """assertcfg_dget('misc.dovecot_version')>= \account.mail_location.dovecot_versionsuper(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)]ifsubscribe:cmd_args.append('-s')cmd_args.extend(mailboxes)process=Popen(cmd_args,stderr=PIPE)stderr=process.communicate()[1]ifprocess.returncode:e_msg=_('Failed to create mailboxes: %r\n')%mailboxesraiseVMMError(e_msg+stderr.strip().decode(ENCODING),VMM_ERROR)defcreate(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)classMultiDbox(SingleDbox):""" Class for multi dbox mailboxes. See http://wiki.dovecot.org/MailboxFormat/dbox#Multi-dbox for details. """__slots__=()defnew(account):"""Create a new Mailbox instance for the given Account."""mbfmt=account.mail_location.mbformatifmbfmt=='maildir':returnMaildir(account)elifmbfmt=='mdbox':returnMultiDbox(account)elifmbfmt=='sdbox':returnSingleDbox(account)raiseValueError('unsupported mailbox format: %r'%mbfmt)del_,cfg_dget