]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | """MH interface -- purely object-oriented (well, almost)\r |
2 | \r | |
3 | Executive summary:\r | |
4 | \r | |
5 | import mhlib\r | |
6 | \r | |
7 | mh = mhlib.MH() # use default mailbox directory and profile\r | |
8 | mh = mhlib.MH(mailbox) # override mailbox location (default from profile)\r | |
9 | mh = mhlib.MH(mailbox, profile) # override mailbox and profile\r | |
10 | \r | |
11 | mh.error(format, ...) # print error message -- can be overridden\r | |
12 | s = mh.getprofile(key) # profile entry (None if not set)\r | |
13 | path = mh.getpath() # mailbox pathname\r | |
14 | name = mh.getcontext() # name of current folder\r | |
15 | mh.setcontext(name) # set name of current folder\r | |
16 | \r | |
17 | list = mh.listfolders() # names of top-level folders\r | |
18 | list = mh.listallfolders() # names of all folders, including subfolders\r | |
19 | list = mh.listsubfolders(name) # direct subfolders of given folder\r | |
20 | list = mh.listallsubfolders(name) # all subfolders of given folder\r | |
21 | \r | |
22 | mh.makefolder(name) # create new folder\r | |
23 | mh.deletefolder(name) # delete folder -- must have no subfolders\r | |
24 | \r | |
25 | f = mh.openfolder(name) # new open folder object\r | |
26 | \r | |
27 | f.error(format, ...) # same as mh.error(format, ...)\r | |
28 | path = f.getfullname() # folder's full pathname\r | |
29 | path = f.getsequencesfilename() # full pathname of folder's sequences file\r | |
30 | path = f.getmessagefilename(n) # full pathname of message n in folder\r | |
31 | \r | |
32 | list = f.listmessages() # list of messages in folder (as numbers)\r | |
33 | n = f.getcurrent() # get current message\r | |
34 | f.setcurrent(n) # set current message\r | |
35 | list = f.parsesequence(seq) # parse msgs syntax into list of messages\r | |
36 | n = f.getlast() # get last message (0 if no messagse)\r | |
37 | f.setlast(n) # set last message (internal use only)\r | |
38 | \r | |
39 | dict = f.getsequences() # dictionary of sequences in folder {name: list}\r | |
40 | f.putsequences(dict) # write sequences back to folder\r | |
41 | \r | |
42 | f.createmessage(n, fp) # add message from file f as number n\r | |
43 | f.removemessages(list) # remove messages in list from folder\r | |
44 | f.refilemessages(list, tofolder) # move messages in list to other folder\r | |
45 | f.movemessage(n, tofolder, ton) # move one message to a given destination\r | |
46 | f.copymessage(n, tofolder, ton) # copy one message to a given destination\r | |
47 | \r | |
48 | m = f.openmessage(n) # new open message object (costs a file descriptor)\r | |
49 | m is a derived class of mimetools.Message(rfc822.Message), with:\r | |
50 | s = m.getheadertext() # text of message's headers\r | |
51 | s = m.getheadertext(pred) # text of message's headers, filtered by pred\r | |
52 | s = m.getbodytext() # text of message's body, decoded\r | |
53 | s = m.getbodytext(0) # text of message's body, not decoded\r | |
54 | """\r | |
55 | from warnings import warnpy3k\r | |
56 | warnpy3k("the mhlib module has been removed in Python 3.0; use the mailbox "\r | |
57 | "module instead", stacklevel=2)\r | |
58 | del warnpy3k\r | |
59 | \r | |
60 | # XXX To do, functionality:\r | |
61 | # - annotate messages\r | |
62 | # - send messages\r | |
63 | #\r | |
64 | # XXX To do, organization:\r | |
65 | # - move IntSet to separate file\r | |
66 | # - move most Message functionality to module mimetools\r | |
67 | \r | |
68 | \r | |
69 | # Customizable defaults\r | |
70 | \r | |
71 | MH_PROFILE = '~/.mh_profile'\r | |
72 | PATH = '~/Mail'\r | |
73 | MH_SEQUENCES = '.mh_sequences'\r | |
74 | FOLDER_PROTECT = 0700\r | |
75 | \r | |
76 | \r | |
77 | # Imported modules\r | |
78 | \r | |
79 | import os\r | |
80 | import sys\r | |
81 | import re\r | |
82 | import mimetools\r | |
83 | import multifile\r | |
84 | import shutil\r | |
85 | from bisect import bisect\r | |
86 | \r | |
87 | __all__ = ["MH","Error","Folder","Message"]\r | |
88 | \r | |
89 | # Exported constants\r | |
90 | \r | |
91 | class Error(Exception):\r | |
92 | pass\r | |
93 | \r | |
94 | \r | |
95 | class MH:\r | |
96 | """Class representing a particular collection of folders.\r | |
97 | Optional constructor arguments are the pathname for the directory\r | |
98 | containing the collection, and the MH profile to use.\r | |
99 | If either is omitted or empty a default is used; the default\r | |
100 | directory is taken from the MH profile if it is specified there."""\r | |
101 | \r | |
102 | def __init__(self, path = None, profile = None):\r | |
103 | """Constructor."""\r | |
104 | if profile is None: profile = MH_PROFILE\r | |
105 | self.profile = os.path.expanduser(profile)\r | |
106 | if path is None: path = self.getprofile('Path')\r | |
107 | if not path: path = PATH\r | |
108 | if not os.path.isabs(path) and path[0] != '~':\r | |
109 | path = os.path.join('~', path)\r | |
110 | path = os.path.expanduser(path)\r | |
111 | if not os.path.isdir(path): raise Error, 'MH() path not found'\r | |
112 | self.path = path\r | |
113 | \r | |
114 | def __repr__(self):\r | |
115 | """String representation."""\r | |
116 | return 'MH(%r, %r)' % (self.path, self.profile)\r | |
117 | \r | |
118 | def error(self, msg, *args):\r | |
119 | """Routine to print an error. May be overridden by a derived class."""\r | |
120 | sys.stderr.write('MH error: %s\n' % (msg % args))\r | |
121 | \r | |
122 | def getprofile(self, key):\r | |
123 | """Return a profile entry, None if not found."""\r | |
124 | return pickline(self.profile, key)\r | |
125 | \r | |
126 | def getpath(self):\r | |
127 | """Return the path (the name of the collection's directory)."""\r | |
128 | return self.path\r | |
129 | \r | |
130 | def getcontext(self):\r | |
131 | """Return the name of the current folder."""\r | |
132 | context = pickline(os.path.join(self.getpath(), 'context'),\r | |
133 | 'Current-Folder')\r | |
134 | if not context: context = 'inbox'\r | |
135 | return context\r | |
136 | \r | |
137 | def setcontext(self, context):\r | |
138 | """Set the name of the current folder."""\r | |
139 | fn = os.path.join(self.getpath(), 'context')\r | |
140 | f = open(fn, "w")\r | |
141 | f.write("Current-Folder: %s\n" % context)\r | |
142 | f.close()\r | |
143 | \r | |
144 | def listfolders(self):\r | |
145 | """Return the names of the top-level folders."""\r | |
146 | folders = []\r | |
147 | path = self.getpath()\r | |
148 | for name in os.listdir(path):\r | |
149 | fullname = os.path.join(path, name)\r | |
150 | if os.path.isdir(fullname):\r | |
151 | folders.append(name)\r | |
152 | folders.sort()\r | |
153 | return folders\r | |
154 | \r | |
155 | def listsubfolders(self, name):\r | |
156 | """Return the names of the subfolders in a given folder\r | |
157 | (prefixed with the given folder name)."""\r | |
158 | fullname = os.path.join(self.path, name)\r | |
159 | # Get the link count so we can avoid listing folders\r | |
160 | # that have no subfolders.\r | |
161 | nlinks = os.stat(fullname).st_nlink\r | |
162 | if nlinks <= 2:\r | |
163 | return []\r | |
164 | subfolders = []\r | |
165 | subnames = os.listdir(fullname)\r | |
166 | for subname in subnames:\r | |
167 | fullsubname = os.path.join(fullname, subname)\r | |
168 | if os.path.isdir(fullsubname):\r | |
169 | name_subname = os.path.join(name, subname)\r | |
170 | subfolders.append(name_subname)\r | |
171 | # Stop looking for subfolders when\r | |
172 | # we've seen them all\r | |
173 | nlinks = nlinks - 1\r | |
174 | if nlinks <= 2:\r | |
175 | break\r | |
176 | subfolders.sort()\r | |
177 | return subfolders\r | |
178 | \r | |
179 | def listallfolders(self):\r | |
180 | """Return the names of all folders and subfolders, recursively."""\r | |
181 | return self.listallsubfolders('')\r | |
182 | \r | |
183 | def listallsubfolders(self, name):\r | |
184 | """Return the names of subfolders in a given folder, recursively."""\r | |
185 | fullname = os.path.join(self.path, name)\r | |
186 | # Get the link count so we can avoid listing folders\r | |
187 | # that have no subfolders.\r | |
188 | nlinks = os.stat(fullname).st_nlink\r | |
189 | if nlinks <= 2:\r | |
190 | return []\r | |
191 | subfolders = []\r | |
192 | subnames = os.listdir(fullname)\r | |
193 | for subname in subnames:\r | |
194 | if subname[0] == ',' or isnumeric(subname): continue\r | |
195 | fullsubname = os.path.join(fullname, subname)\r | |
196 | if os.path.isdir(fullsubname):\r | |
197 | name_subname = os.path.join(name, subname)\r | |
198 | subfolders.append(name_subname)\r | |
199 | if not os.path.islink(fullsubname):\r | |
200 | subsubfolders = self.listallsubfolders(\r | |
201 | name_subname)\r | |
202 | subfolders = subfolders + subsubfolders\r | |
203 | # Stop looking for subfolders when\r | |
204 | # we've seen them all\r | |
205 | nlinks = nlinks - 1\r | |
206 | if nlinks <= 2:\r | |
207 | break\r | |
208 | subfolders.sort()\r | |
209 | return subfolders\r | |
210 | \r | |
211 | def openfolder(self, name):\r | |
212 | """Return a new Folder object for the named folder."""\r | |
213 | return Folder(self, name)\r | |
214 | \r | |
215 | def makefolder(self, name):\r | |
216 | """Create a new folder (or raise os.error if it cannot be created)."""\r | |
217 | protect = pickline(self.profile, 'Folder-Protect')\r | |
218 | if protect and isnumeric(protect):\r | |
219 | mode = int(protect, 8)\r | |
220 | else:\r | |
221 | mode = FOLDER_PROTECT\r | |
222 | os.mkdir(os.path.join(self.getpath(), name), mode)\r | |
223 | \r | |
224 | def deletefolder(self, name):\r | |
225 | """Delete a folder. This removes files in the folder but not\r | |
226 | subdirectories. Raise os.error if deleting the folder itself fails."""\r | |
227 | fullname = os.path.join(self.getpath(), name)\r | |
228 | for subname in os.listdir(fullname):\r | |
229 | fullsubname = os.path.join(fullname, subname)\r | |
230 | try:\r | |
231 | os.unlink(fullsubname)\r | |
232 | except os.error:\r | |
233 | self.error('%s not deleted, continuing...' %\r | |
234 | fullsubname)\r | |
235 | os.rmdir(fullname)\r | |
236 | \r | |
237 | \r | |
238 | numericprog = re.compile('^[1-9][0-9]*$')\r | |
239 | def isnumeric(str):\r | |
240 | return numericprog.match(str) is not None\r | |
241 | \r | |
242 | class Folder:\r | |
243 | """Class representing a particular folder."""\r | |
244 | \r | |
245 | def __init__(self, mh, name):\r | |
246 | """Constructor."""\r | |
247 | self.mh = mh\r | |
248 | self.name = name\r | |
249 | if not os.path.isdir(self.getfullname()):\r | |
250 | raise Error, 'no folder %s' % name\r | |
251 | \r | |
252 | def __repr__(self):\r | |
253 | """String representation."""\r | |
254 | return 'Folder(%r, %r)' % (self.mh, self.name)\r | |
255 | \r | |
256 | def error(self, *args):\r | |
257 | """Error message handler."""\r | |
258 | self.mh.error(*args)\r | |
259 | \r | |
260 | def getfullname(self):\r | |
261 | """Return the full pathname of the folder."""\r | |
262 | return os.path.join(self.mh.path, self.name)\r | |
263 | \r | |
264 | def getsequencesfilename(self):\r | |
265 | """Return the full pathname of the folder's sequences file."""\r | |
266 | return os.path.join(self.getfullname(), MH_SEQUENCES)\r | |
267 | \r | |
268 | def getmessagefilename(self, n):\r | |
269 | """Return the full pathname of a message in the folder."""\r | |
270 | return os.path.join(self.getfullname(), str(n))\r | |
271 | \r | |
272 | def listsubfolders(self):\r | |
273 | """Return list of direct subfolders."""\r | |
274 | return self.mh.listsubfolders(self.name)\r | |
275 | \r | |
276 | def listallsubfolders(self):\r | |
277 | """Return list of all subfolders."""\r | |
278 | return self.mh.listallsubfolders(self.name)\r | |
279 | \r | |
280 | def listmessages(self):\r | |
281 | """Return the list of messages currently present in the folder.\r | |
282 | As a side effect, set self.last to the last message (or 0)."""\r | |
283 | messages = []\r | |
284 | match = numericprog.match\r | |
285 | append = messages.append\r | |
286 | for name in os.listdir(self.getfullname()):\r | |
287 | if match(name):\r | |
288 | append(name)\r | |
289 | messages = map(int, messages)\r | |
290 | messages.sort()\r | |
291 | if messages:\r | |
292 | self.last = messages[-1]\r | |
293 | else:\r | |
294 | self.last = 0\r | |
295 | return messages\r | |
296 | \r | |
297 | def getsequences(self):\r | |
298 | """Return the set of sequences for the folder."""\r | |
299 | sequences = {}\r | |
300 | fullname = self.getsequencesfilename()\r | |
301 | try:\r | |
302 | f = open(fullname, 'r')\r | |
303 | except IOError:\r | |
304 | return sequences\r | |
305 | while 1:\r | |
306 | line = f.readline()\r | |
307 | if not line: break\r | |
308 | fields = line.split(':')\r | |
309 | if len(fields) != 2:\r | |
310 | self.error('bad sequence in %s: %s' %\r | |
311 | (fullname, line.strip()))\r | |
312 | key = fields[0].strip()\r | |
313 | value = IntSet(fields[1].strip(), ' ').tolist()\r | |
314 | sequences[key] = value\r | |
315 | return sequences\r | |
316 | \r | |
317 | def putsequences(self, sequences):\r | |
318 | """Write the set of sequences back to the folder."""\r | |
319 | fullname = self.getsequencesfilename()\r | |
320 | f = None\r | |
321 | for key, seq in sequences.iteritems():\r | |
322 | s = IntSet('', ' ')\r | |
323 | s.fromlist(seq)\r | |
324 | if not f: f = open(fullname, 'w')\r | |
325 | f.write('%s: %s\n' % (key, s.tostring()))\r | |
326 | if not f:\r | |
327 | try:\r | |
328 | os.unlink(fullname)\r | |
329 | except os.error:\r | |
330 | pass\r | |
331 | else:\r | |
332 | f.close()\r | |
333 | \r | |
334 | def getcurrent(self):\r | |
335 | """Return the current message. Raise Error when there is none."""\r | |
336 | seqs = self.getsequences()\r | |
337 | try:\r | |
338 | return max(seqs['cur'])\r | |
339 | except (ValueError, KeyError):\r | |
340 | raise Error, "no cur message"\r | |
341 | \r | |
342 | def setcurrent(self, n):\r | |
343 | """Set the current message."""\r | |
344 | updateline(self.getsequencesfilename(), 'cur', str(n), 0)\r | |
345 | \r | |
346 | def parsesequence(self, seq):\r | |
347 | """Parse an MH sequence specification into a message list.\r | |
348 | Attempt to mimic mh-sequence(5) as close as possible.\r | |
349 | Also attempt to mimic observed behavior regarding which\r | |
350 | conditions cause which error messages."""\r | |
351 | # XXX Still not complete (see mh-format(5)).\r | |
352 | # Missing are:\r | |
353 | # - 'prev', 'next' as count\r | |
354 | # - Sequence-Negation option\r | |
355 | all = self.listmessages()\r | |
356 | # Observed behavior: test for empty folder is done first\r | |
357 | if not all:\r | |
358 | raise Error, "no messages in %s" % self.name\r | |
359 | # Common case first: all is frequently the default\r | |
360 | if seq == 'all':\r | |
361 | return all\r | |
362 | # Test for X:Y before X-Y because 'seq:-n' matches both\r | |
363 | i = seq.find(':')\r | |
364 | if i >= 0:\r | |
365 | head, dir, tail = seq[:i], '', seq[i+1:]\r | |
366 | if tail[:1] in '-+':\r | |
367 | dir, tail = tail[:1], tail[1:]\r | |
368 | if not isnumeric(tail):\r | |
369 | raise Error, "bad message list %s" % seq\r | |
370 | try:\r | |
371 | count = int(tail)\r | |
372 | except (ValueError, OverflowError):\r | |
373 | # Can't use sys.maxint because of i+count below\r | |
374 | count = len(all)\r | |
375 | try:\r | |
376 | anchor = self._parseindex(head, all)\r | |
377 | except Error, msg:\r | |
378 | seqs = self.getsequences()\r | |
379 | if not head in seqs:\r | |
380 | if not msg:\r | |
381 | msg = "bad message list %s" % seq\r | |
382 | raise Error, msg, sys.exc_info()[2]\r | |
383 | msgs = seqs[head]\r | |
384 | if not msgs:\r | |
385 | raise Error, "sequence %s empty" % head\r | |
386 | if dir == '-':\r | |
387 | return msgs[-count:]\r | |
388 | else:\r | |
389 | return msgs[:count]\r | |
390 | else:\r | |
391 | if not dir:\r | |
392 | if head in ('prev', 'last'):\r | |
393 | dir = '-'\r | |
394 | if dir == '-':\r | |
395 | i = bisect(all, anchor)\r | |
396 | return all[max(0, i-count):i]\r | |
397 | else:\r | |
398 | i = bisect(all, anchor-1)\r | |
399 | return all[i:i+count]\r | |
400 | # Test for X-Y next\r | |
401 | i = seq.find('-')\r | |
402 | if i >= 0:\r | |
403 | begin = self._parseindex(seq[:i], all)\r | |
404 | end = self._parseindex(seq[i+1:], all)\r | |
405 | i = bisect(all, begin-1)\r | |
406 | j = bisect(all, end)\r | |
407 | r = all[i:j]\r | |
408 | if not r:\r | |
409 | raise Error, "bad message list %s" % seq\r | |
410 | return r\r | |
411 | # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence\r | |
412 | try:\r | |
413 | n = self._parseindex(seq, all)\r | |
414 | except Error, msg:\r | |
415 | seqs = self.getsequences()\r | |
416 | if not seq in seqs:\r | |
417 | if not msg:\r | |
418 | msg = "bad message list %s" % seq\r | |
419 | raise Error, msg\r | |
420 | return seqs[seq]\r | |
421 | else:\r | |
422 | if n not in all:\r | |
423 | if isnumeric(seq):\r | |
424 | raise Error, "message %d doesn't exist" % n\r | |
425 | else:\r | |
426 | raise Error, "no %s message" % seq\r | |
427 | else:\r | |
428 | return [n]\r | |
429 | \r | |
430 | def _parseindex(self, seq, all):\r | |
431 | """Internal: parse a message number (or cur, first, etc.)."""\r | |
432 | if isnumeric(seq):\r | |
433 | try:\r | |
434 | return int(seq)\r | |
435 | except (OverflowError, ValueError):\r | |
436 | return sys.maxint\r | |
437 | if seq in ('cur', '.'):\r | |
438 | return self.getcurrent()\r | |
439 | if seq == 'first':\r | |
440 | return all[0]\r | |
441 | if seq == 'last':\r | |
442 | return all[-1]\r | |
443 | if seq == 'next':\r | |
444 | n = self.getcurrent()\r | |
445 | i = bisect(all, n)\r | |
446 | try:\r | |
447 | return all[i]\r | |
448 | except IndexError:\r | |
449 | raise Error, "no next message"\r | |
450 | if seq == 'prev':\r | |
451 | n = self.getcurrent()\r | |
452 | i = bisect(all, n-1)\r | |
453 | if i == 0:\r | |
454 | raise Error, "no prev message"\r | |
455 | try:\r | |
456 | return all[i-1]\r | |
457 | except IndexError:\r | |
458 | raise Error, "no prev message"\r | |
459 | raise Error, None\r | |
460 | \r | |
461 | def openmessage(self, n):\r | |
462 | """Open a message -- returns a Message object."""\r | |
463 | return Message(self, n)\r | |
464 | \r | |
465 | def removemessages(self, list):\r | |
466 | """Remove one or more messages -- may raise os.error."""\r | |
467 | errors = []\r | |
468 | deleted = []\r | |
469 | for n in list:\r | |
470 | path = self.getmessagefilename(n)\r | |
471 | commapath = self.getmessagefilename(',' + str(n))\r | |
472 | try:\r | |
473 | os.unlink(commapath)\r | |
474 | except os.error:\r | |
475 | pass\r | |
476 | try:\r | |
477 | os.rename(path, commapath)\r | |
478 | except os.error, msg:\r | |
479 | errors.append(msg)\r | |
480 | else:\r | |
481 | deleted.append(n)\r | |
482 | if deleted:\r | |
483 | self.removefromallsequences(deleted)\r | |
484 | if errors:\r | |
485 | if len(errors) == 1:\r | |
486 | raise os.error, errors[0]\r | |
487 | else:\r | |
488 | raise os.error, ('multiple errors:', errors)\r | |
489 | \r | |
490 | def refilemessages(self, list, tofolder, keepsequences=0):\r | |
491 | """Refile one or more messages -- may raise os.error.\r | |
492 | 'tofolder' is an open folder object."""\r | |
493 | errors = []\r | |
494 | refiled = {}\r | |
495 | for n in list:\r | |
496 | ton = tofolder.getlast() + 1\r | |
497 | path = self.getmessagefilename(n)\r | |
498 | topath = tofolder.getmessagefilename(ton)\r | |
499 | try:\r | |
500 | os.rename(path, topath)\r | |
501 | except os.error:\r | |
502 | # Try copying\r | |
503 | try:\r | |
504 | shutil.copy2(path, topath)\r | |
505 | os.unlink(path)\r | |
506 | except (IOError, os.error), msg:\r | |
507 | errors.append(msg)\r | |
508 | try:\r | |
509 | os.unlink(topath)\r | |
510 | except os.error:\r | |
511 | pass\r | |
512 | continue\r | |
513 | tofolder.setlast(ton)\r | |
514 | refiled[n] = ton\r | |
515 | if refiled:\r | |
516 | if keepsequences:\r | |
517 | tofolder._copysequences(self, refiled.items())\r | |
518 | self.removefromallsequences(refiled.keys())\r | |
519 | if errors:\r | |
520 | if len(errors) == 1:\r | |
521 | raise os.error, errors[0]\r | |
522 | else:\r | |
523 | raise os.error, ('multiple errors:', errors)\r | |
524 | \r | |
525 | def _copysequences(self, fromfolder, refileditems):\r | |
526 | """Helper for refilemessages() to copy sequences."""\r | |
527 | fromsequences = fromfolder.getsequences()\r | |
528 | tosequences = self.getsequences()\r | |
529 | changed = 0\r | |
530 | for name, seq in fromsequences.items():\r | |
531 | try:\r | |
532 | toseq = tosequences[name]\r | |
533 | new = 0\r | |
534 | except KeyError:\r | |
535 | toseq = []\r | |
536 | new = 1\r | |
537 | for fromn, ton in refileditems:\r | |
538 | if fromn in seq:\r | |
539 | toseq.append(ton)\r | |
540 | changed = 1\r | |
541 | if new and toseq:\r | |
542 | tosequences[name] = toseq\r | |
543 | if changed:\r | |
544 | self.putsequences(tosequences)\r | |
545 | \r | |
546 | def movemessage(self, n, tofolder, ton):\r | |
547 | """Move one message over a specific destination message,\r | |
548 | which may or may not already exist."""\r | |
549 | path = self.getmessagefilename(n)\r | |
550 | # Open it to check that it exists\r | |
551 | f = open(path)\r | |
552 | f.close()\r | |
553 | del f\r | |
554 | topath = tofolder.getmessagefilename(ton)\r | |
555 | backuptopath = tofolder.getmessagefilename(',%d' % ton)\r | |
556 | try:\r | |
557 | os.rename(topath, backuptopath)\r | |
558 | except os.error:\r | |
559 | pass\r | |
560 | try:\r | |
561 | os.rename(path, topath)\r | |
562 | except os.error:\r | |
563 | # Try copying\r | |
564 | ok = 0\r | |
565 | try:\r | |
566 | tofolder.setlast(None)\r | |
567 | shutil.copy2(path, topath)\r | |
568 | ok = 1\r | |
569 | finally:\r | |
570 | if not ok:\r | |
571 | try:\r | |
572 | os.unlink(topath)\r | |
573 | except os.error:\r | |
574 | pass\r | |
575 | os.unlink(path)\r | |
576 | self.removefromallsequences([n])\r | |
577 | \r | |
578 | def copymessage(self, n, tofolder, ton):\r | |
579 | """Copy one message over a specific destination message,\r | |
580 | which may or may not already exist."""\r | |
581 | path = self.getmessagefilename(n)\r | |
582 | # Open it to check that it exists\r | |
583 | f = open(path)\r | |
584 | f.close()\r | |
585 | del f\r | |
586 | topath = tofolder.getmessagefilename(ton)\r | |
587 | backuptopath = tofolder.getmessagefilename(',%d' % ton)\r | |
588 | try:\r | |
589 | os.rename(topath, backuptopath)\r | |
590 | except os.error:\r | |
591 | pass\r | |
592 | ok = 0\r | |
593 | try:\r | |
594 | tofolder.setlast(None)\r | |
595 | shutil.copy2(path, topath)\r | |
596 | ok = 1\r | |
597 | finally:\r | |
598 | if not ok:\r | |
599 | try:\r | |
600 | os.unlink(topath)\r | |
601 | except os.error:\r | |
602 | pass\r | |
603 | \r | |
604 | def createmessage(self, n, txt):\r | |
605 | """Create a message, with text from the open file txt."""\r | |
606 | path = self.getmessagefilename(n)\r | |
607 | backuppath = self.getmessagefilename(',%d' % n)\r | |
608 | try:\r | |
609 | os.rename(path, backuppath)\r | |
610 | except os.error:\r | |
611 | pass\r | |
612 | ok = 0\r | |
613 | BUFSIZE = 16*1024\r | |
614 | try:\r | |
615 | f = open(path, "w")\r | |
616 | while 1:\r | |
617 | buf = txt.read(BUFSIZE)\r | |
618 | if not buf:\r | |
619 | break\r | |
620 | f.write(buf)\r | |
621 | f.close()\r | |
622 | ok = 1\r | |
623 | finally:\r | |
624 | if not ok:\r | |
625 | try:\r | |
626 | os.unlink(path)\r | |
627 | except os.error:\r | |
628 | pass\r | |
629 | \r | |
630 | def removefromallsequences(self, list):\r | |
631 | """Remove one or more messages from all sequences (including last)\r | |
632 | -- but not from 'cur'!!!"""\r | |
633 | if hasattr(self, 'last') and self.last in list:\r | |
634 | del self.last\r | |
635 | sequences = self.getsequences()\r | |
636 | changed = 0\r | |
637 | for name, seq in sequences.items():\r | |
638 | if name == 'cur':\r | |
639 | continue\r | |
640 | for n in list:\r | |
641 | if n in seq:\r | |
642 | seq.remove(n)\r | |
643 | changed = 1\r | |
644 | if not seq:\r | |
645 | del sequences[name]\r | |
646 | if changed:\r | |
647 | self.putsequences(sequences)\r | |
648 | \r | |
649 | def getlast(self):\r | |
650 | """Return the last message number."""\r | |
651 | if not hasattr(self, 'last'):\r | |
652 | self.listmessages() # Set self.last\r | |
653 | return self.last\r | |
654 | \r | |
655 | def setlast(self, last):\r | |
656 | """Set the last message number."""\r | |
657 | if last is None:\r | |
658 | if hasattr(self, 'last'):\r | |
659 | del self.last\r | |
660 | else:\r | |
661 | self.last = last\r | |
662 | \r | |
663 | class Message(mimetools.Message):\r | |
664 | \r | |
665 | def __init__(self, f, n, fp = None):\r | |
666 | """Constructor."""\r | |
667 | self.folder = f\r | |
668 | self.number = n\r | |
669 | if fp is None:\r | |
670 | path = f.getmessagefilename(n)\r | |
671 | fp = open(path, 'r')\r | |
672 | mimetools.Message.__init__(self, fp)\r | |
673 | \r | |
674 | def __repr__(self):\r | |
675 | """String representation."""\r | |
676 | return 'Message(%s, %s)' % (repr(self.folder), self.number)\r | |
677 | \r | |
678 | def getheadertext(self, pred = None):\r | |
679 | """Return the message's header text as a string. If an\r | |
680 | argument is specified, it is used as a filter predicate to\r | |
681 | decide which headers to return (its argument is the header\r | |
682 | name converted to lower case)."""\r | |
683 | if pred is None:\r | |
684 | return ''.join(self.headers)\r | |
685 | headers = []\r | |
686 | hit = 0\r | |
687 | for line in self.headers:\r | |
688 | if not line[0].isspace():\r | |
689 | i = line.find(':')\r | |
690 | if i > 0:\r | |
691 | hit = pred(line[:i].lower())\r | |
692 | if hit: headers.append(line)\r | |
693 | return ''.join(headers)\r | |
694 | \r | |
695 | def getbodytext(self, decode = 1):\r | |
696 | """Return the message's body text as string. This undoes a\r | |
697 | Content-Transfer-Encoding, but does not interpret other MIME\r | |
698 | features (e.g. multipart messages). To suppress decoding,\r | |
699 | pass 0 as an argument."""\r | |
700 | self.fp.seek(self.startofbody)\r | |
701 | encoding = self.getencoding()\r | |
702 | if not decode or encoding in ('', '7bit', '8bit', 'binary'):\r | |
703 | return self.fp.read()\r | |
704 | try:\r | |
705 | from cStringIO import StringIO\r | |
706 | except ImportError:\r | |
707 | from StringIO import StringIO\r | |
708 | output = StringIO()\r | |
709 | mimetools.decode(self.fp, output, encoding)\r | |
710 | return output.getvalue()\r | |
711 | \r | |
712 | def getbodyparts(self):\r | |
713 | """Only for multipart messages: return the message's body as a\r | |
714 | list of SubMessage objects. Each submessage object behaves\r | |
715 | (almost) as a Message object."""\r | |
716 | if self.getmaintype() != 'multipart':\r | |
717 | raise Error, 'Content-Type is not multipart/*'\r | |
718 | bdry = self.getparam('boundary')\r | |
719 | if not bdry:\r | |
720 | raise Error, 'multipart/* without boundary param'\r | |
721 | self.fp.seek(self.startofbody)\r | |
722 | mf = multifile.MultiFile(self.fp)\r | |
723 | mf.push(bdry)\r | |
724 | parts = []\r | |
725 | while mf.next():\r | |
726 | n = "%s.%r" % (self.number, 1 + len(parts))\r | |
727 | part = SubMessage(self.folder, n, mf)\r | |
728 | parts.append(part)\r | |
729 | mf.pop()\r | |
730 | return parts\r | |
731 | \r | |
732 | def getbody(self):\r | |
733 | """Return body, either a string or a list of messages."""\r | |
734 | if self.getmaintype() == 'multipart':\r | |
735 | return self.getbodyparts()\r | |
736 | else:\r | |
737 | return self.getbodytext()\r | |
738 | \r | |
739 | \r | |
740 | class SubMessage(Message):\r | |
741 | \r | |
742 | def __init__(self, f, n, fp):\r | |
743 | """Constructor."""\r | |
744 | Message.__init__(self, f, n, fp)\r | |
745 | if self.getmaintype() == 'multipart':\r | |
746 | self.body = Message.getbodyparts(self)\r | |
747 | else:\r | |
748 | self.body = Message.getbodytext(self)\r | |
749 | self.bodyencoded = Message.getbodytext(self, decode=0)\r | |
750 | # XXX If this is big, should remember file pointers\r | |
751 | \r | |
752 | def __repr__(self):\r | |
753 | """String representation."""\r | |
754 | f, n, fp = self.folder, self.number, self.fp\r | |
755 | return 'SubMessage(%s, %s, %s)' % (f, n, fp)\r | |
756 | \r | |
757 | def getbodytext(self, decode = 1):\r | |
758 | if not decode:\r | |
759 | return self.bodyencoded\r | |
760 | if type(self.body) == type(''):\r | |
761 | return self.body\r | |
762 | \r | |
763 | def getbodyparts(self):\r | |
764 | if type(self.body) == type([]):\r | |
765 | return self.body\r | |
766 | \r | |
767 | def getbody(self):\r | |
768 | return self.body\r | |
769 | \r | |
770 | \r | |
771 | class IntSet:\r | |
772 | """Class implementing sets of integers.\r | |
773 | \r | |
774 | This is an efficient representation for sets consisting of several\r | |
775 | continuous ranges, e.g. 1-100,200-400,402-1000 is represented\r | |
776 | internally as a list of three pairs: [(1,100), (200,400),\r | |
777 | (402,1000)]. The internal representation is always kept normalized.\r | |
778 | \r | |
779 | The constructor has up to three arguments:\r | |
780 | - the string used to initialize the set (default ''),\r | |
781 | - the separator between ranges (default ',')\r | |
782 | - the separator between begin and end of a range (default '-')\r | |
783 | The separators must be strings (not regexprs) and should be different.\r | |
784 | \r | |
785 | The tostring() function yields a string that can be passed to another\r | |
786 | IntSet constructor; __repr__() is a valid IntSet constructor itself.\r | |
787 | """\r | |
788 | \r | |
789 | # XXX The default begin/end separator means that negative numbers are\r | |
790 | # not supported very well.\r | |
791 | #\r | |
792 | # XXX There are currently no operations to remove set elements.\r | |
793 | \r | |
794 | def __init__(self, data = None, sep = ',', rng = '-'):\r | |
795 | self.pairs = []\r | |
796 | self.sep = sep\r | |
797 | self.rng = rng\r | |
798 | if data: self.fromstring(data)\r | |
799 | \r | |
800 | def reset(self):\r | |
801 | self.pairs = []\r | |
802 | \r | |
803 | def __cmp__(self, other):\r | |
804 | return cmp(self.pairs, other.pairs)\r | |
805 | \r | |
806 | def __hash__(self):\r | |
807 | return hash(self.pairs)\r | |
808 | \r | |
809 | def __repr__(self):\r | |
810 | return 'IntSet(%r, %r, %r)' % (self.tostring(), self.sep, self.rng)\r | |
811 | \r | |
812 | def normalize(self):\r | |
813 | self.pairs.sort()\r | |
814 | i = 1\r | |
815 | while i < len(self.pairs):\r | |
816 | alo, ahi = self.pairs[i-1]\r | |
817 | blo, bhi = self.pairs[i]\r | |
818 | if ahi >= blo-1:\r | |
819 | self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]\r | |
820 | else:\r | |
821 | i = i+1\r | |
822 | \r | |
823 | def tostring(self):\r | |
824 | s = ''\r | |
825 | for lo, hi in self.pairs:\r | |
826 | if lo == hi: t = repr(lo)\r | |
827 | else: t = repr(lo) + self.rng + repr(hi)\r | |
828 | if s: s = s + (self.sep + t)\r | |
829 | else: s = t\r | |
830 | return s\r | |
831 | \r | |
832 | def tolist(self):\r | |
833 | l = []\r | |
834 | for lo, hi in self.pairs:\r | |
835 | m = range(lo, hi+1)\r | |
836 | l = l + m\r | |
837 | return l\r | |
838 | \r | |
839 | def fromlist(self, list):\r | |
840 | for i in list:\r | |
841 | self.append(i)\r | |
842 | \r | |
843 | def clone(self):\r | |
844 | new = IntSet()\r | |
845 | new.pairs = self.pairs[:]\r | |
846 | return new\r | |
847 | \r | |
848 | def min(self):\r | |
849 | return self.pairs[0][0]\r | |
850 | \r | |
851 | def max(self):\r | |
852 | return self.pairs[-1][-1]\r | |
853 | \r | |
854 | def contains(self, x):\r | |
855 | for lo, hi in self.pairs:\r | |
856 | if lo <= x <= hi: return True\r | |
857 | return False\r | |
858 | \r | |
859 | def append(self, x):\r | |
860 | for i in range(len(self.pairs)):\r | |
861 | lo, hi = self.pairs[i]\r | |
862 | if x < lo: # Need to insert before\r | |
863 | if x+1 == lo:\r | |
864 | self.pairs[i] = (x, hi)\r | |
865 | else:\r | |
866 | self.pairs.insert(i, (x, x))\r | |
867 | if i > 0 and x-1 == self.pairs[i-1][1]:\r | |
868 | # Merge with previous\r | |
869 | self.pairs[i-1:i+1] = [\r | |
870 | (self.pairs[i-1][0],\r | |
871 | self.pairs[i][1])\r | |
872 | ]\r | |
873 | return\r | |
874 | if x <= hi: # Already in set\r | |
875 | return\r | |
876 | i = len(self.pairs) - 1\r | |
877 | if i >= 0:\r | |
878 | lo, hi = self.pairs[i]\r | |
879 | if x-1 == hi:\r | |
880 | self.pairs[i] = lo, x\r | |
881 | return\r | |
882 | self.pairs.append((x, x))\r | |
883 | \r | |
884 | def addpair(self, xlo, xhi):\r | |
885 | if xlo > xhi: return\r | |
886 | self.pairs.append((xlo, xhi))\r | |
887 | self.normalize()\r | |
888 | \r | |
889 | def fromstring(self, data):\r | |
890 | new = []\r | |
891 | for part in data.split(self.sep):\r | |
892 | list = []\r | |
893 | for subp in part.split(self.rng):\r | |
894 | s = subp.strip()\r | |
895 | list.append(int(s))\r | |
896 | if len(list) == 1:\r | |
897 | new.append((list[0], list[0]))\r | |
898 | elif len(list) == 2 and list[0] <= list[1]:\r | |
899 | new.append((list[0], list[1]))\r | |
900 | else:\r | |
901 | raise ValueError, 'bad data passed to IntSet'\r | |
902 | self.pairs = self.pairs + new\r | |
903 | self.normalize()\r | |
904 | \r | |
905 | \r | |
906 | # Subroutines to read/write entries in .mh_profile and .mh_sequences\r | |
907 | \r | |
908 | def pickline(file, key, casefold = 1):\r | |
909 | try:\r | |
910 | f = open(file, 'r')\r | |
911 | except IOError:\r | |
912 | return None\r | |
913 | pat = re.escape(key) + ':'\r | |
914 | prog = re.compile(pat, casefold and re.IGNORECASE)\r | |
915 | while 1:\r | |
916 | line = f.readline()\r | |
917 | if not line: break\r | |
918 | if prog.match(line):\r | |
919 | text = line[len(key)+1:]\r | |
920 | while 1:\r | |
921 | line = f.readline()\r | |
922 | if not line or not line[0].isspace():\r | |
923 | break\r | |
924 | text = text + line\r | |
925 | return text.strip()\r | |
926 | return None\r | |
927 | \r | |
928 | def updateline(file, key, value, casefold = 1):\r | |
929 | try:\r | |
930 | f = open(file, 'r')\r | |
931 | lines = f.readlines()\r | |
932 | f.close()\r | |
933 | except IOError:\r | |
934 | lines = []\r | |
935 | pat = re.escape(key) + ':(.*)\n'\r | |
936 | prog = re.compile(pat, casefold and re.IGNORECASE)\r | |
937 | if value is None:\r | |
938 | newline = None\r | |
939 | else:\r | |
940 | newline = '%s: %s\n' % (key, value)\r | |
941 | for i in range(len(lines)):\r | |
942 | line = lines[i]\r | |
943 | if prog.match(line):\r | |
944 | if newline is None:\r | |
945 | del lines[i]\r | |
946 | else:\r | |
947 | lines[i] = newline\r | |
948 | break\r | |
949 | else:\r | |
950 | if newline is not None:\r | |
951 | lines.append(newline)\r | |
952 | tempfile = file + "~"\r | |
953 | f = open(tempfile, 'w')\r | |
954 | for line in lines:\r | |
955 | f.write(line)\r | |
956 | f.close()\r | |
957 | os.rename(tempfile, file)\r | |
958 | \r | |
959 | \r | |
960 | # Test program\r | |
961 | \r | |
962 | def test():\r | |
963 | global mh, f\r | |
964 | os.system('rm -rf $HOME/Mail/@test')\r | |
965 | mh = MH()\r | |
966 | def do(s): print s; print eval(s)\r | |
967 | do('mh.listfolders()')\r | |
968 | do('mh.listallfolders()')\r | |
969 | testfolders = ['@test', '@test/test1', '@test/test2',\r | |
970 | '@test/test1/test11', '@test/test1/test12',\r | |
971 | '@test/test1/test11/test111']\r | |
972 | for t in testfolders: do('mh.makefolder(%r)' % (t,))\r | |
973 | do('mh.listsubfolders(\'@test\')')\r | |
974 | do('mh.listallsubfolders(\'@test\')')\r | |
975 | f = mh.openfolder('@test')\r | |
976 | do('f.listsubfolders()')\r | |
977 | do('f.listallsubfolders()')\r | |
978 | do('f.getsequences()')\r | |
979 | seqs = f.getsequences()\r | |
980 | seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()\r | |
981 | print seqs\r | |
982 | f.putsequences(seqs)\r | |
983 | do('f.getsequences()')\r | |
984 | for t in reversed(testfolders): do('mh.deletefolder(%r)' % (t,))\r | |
985 | do('mh.getcontext()')\r | |
986 | context = mh.getcontext()\r | |
987 | f = mh.openfolder(context)\r | |
988 | do('f.getcurrent()')\r | |
989 | for seq in ('first', 'last', 'cur', '.', 'prev', 'next',\r | |
990 | 'first:3', 'last:3', 'cur:3', 'cur:-3',\r | |
991 | 'prev:3', 'next:3',\r | |
992 | '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',\r | |
993 | 'all'):\r | |
994 | try:\r | |
995 | do('f.parsesequence(%r)' % (seq,))\r | |
996 | except Error, msg:\r | |
997 | print "Error:", msg\r | |
998 | stuff = os.popen("pick %r 2>/dev/null" % (seq,)).read()\r | |
999 | list = map(int, stuff.split())\r | |
1000 | print list, "<-- pick"\r | |
1001 | do('f.listmessages()')\r | |
1002 | \r | |
1003 | \r | |
1004 | if __name__ == '__main__':\r | |
1005 | test()\r |