|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2010 - 2012, 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 |