|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 |
|
5 """ |
|
6 VirtualMailManager.mailbox |
|
7 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
8 |
|
9 VirtualMailManager's mailbox classes for the Maildir, single dbox |
|
10 (sdbox) and multi dbox (mdbox) mailbox formats. |
|
11 """ |
|
12 |
|
13 import os |
|
14 import re |
|
15 from binascii import a2b_base64, b2a_base64 |
|
16 from subprocess import Popen, PIPE |
|
17 |
|
18 from VirtualMailManager.Account import Account |
|
19 from VirtualMailManager.common import is_dir |
|
20 from VirtualMailManager.errors import VMMError |
|
21 from VirtualMailManager.constants.ERROR import VMM_ERROR |
|
22 |
|
23 |
|
24 __all__ = ('new', 'Maildir', 'SingleDbox', 'MultiDbox', |
|
25 'utf8_to_mutf7', 'mutf7_to_utf8') |
|
26 |
|
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) |
|
99 is_dir(account.home) |
|
100 self._user = account |
|
101 self._boxes = [] |
|
102 self._root = self._user.mail_location.directory |
|
103 self._sep = '/' |
|
104 os.chdir(self._user.home) |
|
105 |
|
106 def _add_boxes(self, mailboxes, subscribe): |
|
107 raise NotImplementedError |
|
108 |
|
109 def _validate_box_name(self, name, good, bad): |
|
110 """ |
|
111 Validates the mailboxes name `name`. When the name is valid, it |
|
112 will be added to the `good` set. Invalid mailbox names will be |
|
113 appended to the `bad` list. |
|
114 """ |
|
115 name = name.strip() |
|
116 if not name: |
|
117 return |
|
118 if self.__class__._ctrl_chr_re.search(name): # no control chars |
|
119 bad.append(name) |
|
120 return |
|
121 if name[0] in (self._sep, '~'): |
|
122 bad.append(name) |
|
123 return |
|
124 if self._sep == '/': |
|
125 if '//' in name or '/./' in name or '/../' in name or \ |
|
126 name.startswith('../'): |
|
127 bad.append(name) |
|
128 return |
|
129 if '/' in name or '..' in name: |
|
130 bad.append(name) |
|
131 return |
|
132 if not self.__class__._box_name_re.match(name): |
|
133 tmp = utf8_to_mutf7(name) |
|
134 if name == mutf7_to_utf8(tmp): |
|
135 if self._user.mail_location.mbformat == 'maildir': |
|
136 good.add(tmp) |
|
137 else: |
|
138 good.add(name) |
|
139 return |
|
140 else: |
|
141 bad.append(name) |
|
142 return |
|
143 good.add(name) |
|
144 |
|
145 def add_boxes(self, mailboxes, subscribe): |
|
146 """ |
|
147 Create all mailboxes from the `mailboxes` list in the user's |
|
148 mail directory. When `subscribe` is ``True`` all created mailboxes |
|
149 will be listed in the subscriptions file. |
|
150 Returns a list of invalid mailbox names, if any. |
|
151 """ |
|
152 assert isinstance(mailboxes, list) and isinstance(subscribe, bool) |
|
153 good = set() |
|
154 bad = [] |
|
155 for box in mailboxes: |
|
156 self._validate_box_name(box, good, bad) |
|
157 self._add_boxes(good, subscribe) |
|
158 return bad |
|
159 |
|
160 def create(self): |
|
161 """Create the INBOX in the user's mail directory.""" |
|
162 raise NotImplementedError |
|
163 |
|
164 |
|
165 class Maildir(Mailbox): |
|
166 """Class for Maildir++ mailboxes.""" |
|
167 |
|
168 __slots__ = ('_subdirs') |
|
169 |
|
170 def __init__(self, account): |
|
171 """ |
|
172 Create a new Maildir++ instance. |
|
173 Call the instance's create() method, in order to create the INBOX. |
|
174 For additional mailboxes use the add_boxes() method. |
|
175 """ |
|
176 super(self.__class__, self).__init__(account) |
|
177 self._sep = '.' |
|
178 self._subdirs = ('cur', 'new', 'tmp') |
|
179 |
|
180 def _create_maildirfolder_file(self, path): |
|
181 """Mark the Maildir++ folder as Maildir folder.""" |
|
182 maildirfolder_file = os.path.join(self._sep + path, 'maildirfolder') |
|
183 os.close(os.open(maildirfolder_file, os.O_CREAT | os.O_WRONLY, |
|
184 self.__class__.FILE_MODE)) |
|
185 os.chown(maildirfolder_file, self._user.uid, self._user.gid) |
|
186 |
|
187 def _make_maildir(self, path): |
|
188 """ |
|
189 Create Maildir++ folders with the cur, new and tmp subdirectories. |
|
190 """ |
|
191 mode = cfg_dget('account.directory_mode') |
|
192 uid = self._user.uid |
|
193 gid = self._user.gid |
|
194 os.mkdir(path, mode) |
|
195 os.chown(path, uid, gid) |
|
196 for subdir in self._subdirs: |
|
197 dir_ = os.path.join(path, subdir) |
|
198 os.mkdir(dir_, mode) |
|
199 os.chown(dir_, uid, gid) |
|
200 |
|
201 def _subscribe_boxes(self): |
|
202 """Writes all created mailboxes to the subscriptions file.""" |
|
203 if not self._boxes: |
|
204 return |
|
205 subscriptions = open('subscriptions', 'w') |
|
206 subscriptions.write('\n'.join(self._boxes)) |
|
207 subscriptions.write('\n') |
|
208 subscriptions.flush() |
|
209 subscriptions.close() |
|
210 os.chown('subscriptions', self._user.uid, self._user.gid) |
|
211 os.chmod('subscriptions', self.__class__.FILE_MODE) |
|
212 del self._boxes[:] |
|
213 |
|
214 def _add_boxes(self, mailboxes, subscribe): |
|
215 for mailbox in mailboxes: |
|
216 self._make_maildir(self._sep + mailbox) |
|
217 self._create_maildirfolder_file(mailbox) |
|
218 self._boxes.append(mailbox) |
|
219 if subscribe: |
|
220 self._subscribe_boxes() |
|
221 |
|
222 def create(self): |
|
223 """Creates a Maildir++ INBOX.""" |
|
224 self._make_maildir(self._root) |
|
225 os.chdir(self._root) |
|
226 |
|
227 |
|
228 class SingleDbox(Mailbox): |
|
229 """ |
|
230 Class for (single) dbox mailboxes. |
|
231 See http://wiki.dovecot.org/MailboxFormat/dbox for details. |
|
232 """ |
|
233 |
|
234 __slots__ = () |
|
235 |
|
236 def __init__(self, account): |
|
237 """ |
|
238 Create a new dbox instance. |
|
239 Call the instance's create() method, in order to create the INBOX. |
|
240 For additional mailboxes use the add_boxes() method. |
|
241 """ |
|
242 assert cfg_dget('misc.dovecot_version') >= \ |
|
243 account.mail_location.dovecot_version |
|
244 super(SingleDbox, self).__init__(account) |
|
245 |
|
246 def _doveadm_create(self, mailboxes, subscribe): |
|
247 """Wrap around Dovecot's doveadm""" |
|
248 cmd_args = [cfg_dget('bin.dovecotpw'), 'mailbox', 'create', '-u', |
|
249 str(self._user.address)] |
|
250 if subscribe: |
|
251 cmd_args.append('-s') |
|
252 cmd_args.extend(mailboxes) |
|
253 print '\n -> %r\n' % cmd_args |
|
254 process = Popen(cmd_args, stdout=PIPE, stderr=PIPE) |
|
255 stdout, stderr = process.communicate() |
|
256 if process.returncode: |
|
257 raise VMMError(stderr.strip(), VMM_ERROR) |
|
258 |
|
259 def create(self): |
|
260 """Create a dbox INBOX""" |
|
261 os.mkdir(self._root, cfg_dget('account.directory_mode')) |
|
262 os.chown(self._root, self._user.uid, self._user.gid) |
|
263 self._doveadm_create(('INBOX',), False) |
|
264 os.chdir(self._root) |
|
265 |
|
266 def _add_boxes(self, mailboxes, subscribe): |
|
267 self._doveadm_create(mailboxes, subscribe) |
|
268 |
|
269 |
|
270 class MultiDbox(SingleDbox): |
|
271 """ |
|
272 Class for multi dbox mailboxes. |
|
273 See http://wiki.dovecot.org/MailboxFormat/dbox#Multi-dbox for details. |
|
274 """ |
|
275 |
|
276 __slots__ = () |
|
277 |
|
278 |
|
279 def __get_mailbox_class(mbfmt): |
|
280 if mbfmt == 'maildir': |
|
281 return Maildir |
|
282 elif mbfmt == 'mdbox': |
|
283 return MultiDbox |
|
284 elif mbfmt == 'sdbox': |
|
285 return SingleDbox |
|
286 raise ValueError('unsupported mailbox format: %r' % mbfmt) |
|
287 |
|
288 |
|
289 def new(account): |
|
290 """Create a new Mailbox instance for the given Account.""" |
|
291 return __get_mailbox_class(account.mail_location.mbformat)(account) |
|
292 |
|
293 del cfg_dget |