blob: 69a33ec6797f24d15d8968e497ba92526cf42e67 [file] [log] [blame]
Guido van Rossum56013131994-06-23 12:06:02 +00001# MH interface -- purely object-oriented (well, almost)
2#
3# Executive summary:
4#
5# import mhlib
6#
7# mh = mhlib.MH() # use default mailbox directory and profile
8# mh = mhlib.MH(mailbox) # override mailbox location (default from profile)
9# mh = mhlib.MH(mailbox, profile) # override mailbox and profile
10#
11# mh.error(format, ...) # print error message -- can be overridden
12# s = mh.getprofile(key) # profile entry (None if not set)
13# path = mh.getpath() # mailbox pathname
14# name = mh.getcontext() # name of current folder
Guido van Rossum508a0921996-05-28 22:59:37 +000015# mh.setcontext(name) # set name of current folder
Guido van Rossum56013131994-06-23 12:06:02 +000016#
17# list = mh.listfolders() # names of top-level folders
18# list = mh.listallfolders() # names of all folders, including subfolders
19# list = mh.listsubfolders(name) # direct subfolders of given folder
20# list = mh.listallsubfolders(name) # all subfolders of given folder
21#
22# mh.makefolder(name) # create new folder
23# mh.deletefolder(name) # delete folder -- must have no subfolders
24#
25# f = mh.openfolder(name) # new open folder object
26#
27# f.error(format, ...) # same as mh.error(format, ...)
28# path = f.getfullname() # folder's full pathname
29# path = f.getsequencesfilename() # full pathname of folder's sequences file
30# path = f.getmessagefilename(n) # full pathname of message n in folder
31#
32# list = f.listmessages() # list of messages in folder (as numbers)
33# n = f.getcurrent() # get current message
34# f.setcurrent(n) # set current message
Guido van Rossum508a0921996-05-28 22:59:37 +000035# list = f.parsesequence(seq) # parse msgs syntax into list of messages
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000036# n = f.getlast() # get last message (0 if no messagse)
37# f.setlast(n) # set last message (internal use only)
Guido van Rossum56013131994-06-23 12:06:02 +000038#
39# dict = f.getsequences() # dictionary of sequences in folder {name: list}
40# f.putsequences(dict) # write sequences back to folder
41#
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000042# f.removemessages(list) # remove messages in list from folder
43# f.refilemessages(list, tofolder) # move messages in list to other folder
44# f.movemessage(n, tofolder, ton) # move one message to a given destination
45# f.copymessage(n, tofolder, ton) # copy one message to a given destination
46#
Guido van Rossum56013131994-06-23 12:06:02 +000047# m = f.openmessage(n) # new open message object (costs a file descriptor)
Guido van Rossum85347411994-09-09 11:10:15 +000048# m is a derived class of mimetools.Message(rfc822.Message), with:
Guido van Rossum56013131994-06-23 12:06:02 +000049# s = m.getheadertext() # text of message's headers
50# s = m.getheadertext(pred) # text of message's headers, filtered by pred
51# s = m.getbodytext() # text of message's body, decoded
52# s = m.getbodytext(0) # text of message's body, not decoded
53#
54# XXX To do, functionality:
Guido van Rossum56013131994-06-23 12:06:02 +000055# - annotate messages
56# - create, send messages
57#
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000058# XXX To do, organization:
Guido van Rossum56013131994-06-23 12:06:02 +000059# - move IntSet to separate file
60# - move most Message functionality to module mimetools
61
62
63# Customizable defaults
64
65MH_PROFILE = '~/.mh_profile'
66PATH = '~/Mail'
67MH_SEQUENCES = '.mh_sequences'
68FOLDER_PROTECT = 0700
69
70
71# Imported modules
72
73import os
Guido van Rossum508a0921996-05-28 22:59:37 +000074import sys
Guido van Rossum56013131994-06-23 12:06:02 +000075from stat import ST_NLINK
Guido van Rossum9694fca1997-10-22 21:00:49 +000076import re
Guido van Rossum56013131994-06-23 12:06:02 +000077import string
78import mimetools
79import multifile
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000080import shutil
Guido van Rossum7cfd31e1997-04-16 02:45:08 +000081from bisect import bisect
Guido van Rossum56013131994-06-23 12:06:02 +000082
83
84# Exported constants
85
86Error = 'mhlib.Error'
87
88
89# Class representing a particular collection of folders.
90# Optional constructor arguments are the pathname for the directory
91# containing the collection, and the MH profile to use.
92# If either is omitted or empty a default is used; the default
93# directory is taken from the MH profile if it is specified there.
94
95class MH:
96
Guido van Rossum0c5e0491997-04-16 02:47:12 +000097 # Constructor
98 def __init__(self, path = None, profile = None):
99 if not profile: profile = MH_PROFILE
100 self.profile = os.path.expanduser(profile)
101 if not path: path = self.getprofile('Path')
102 if not path: path = PATH
103 if not os.path.isabs(path) and path[0] != '~':
104 path = os.path.join('~', path)
105 path = os.path.expanduser(path)
106 if not os.path.isdir(path): raise Error, 'MH() path not found'
107 self.path = path
Guido van Rossum56013131994-06-23 12:06:02 +0000108
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000109 # String representation
110 def __repr__(self):
111 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
Guido van Rossum56013131994-06-23 12:06:02 +0000112
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000113 # Routine to print an error. May be overridden by a derived class
114 def error(self, msg, *args):
115 sys.stderr.write('MH error: %s\n' % (msg % args))
Guido van Rossum56013131994-06-23 12:06:02 +0000116
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000117 # Return a profile entry, None if not found
118 def getprofile(self, key):
119 return pickline(self.profile, key)
Guido van Rossum56013131994-06-23 12:06:02 +0000120
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000121 # Return the path (the name of the collection's directory)
122 def getpath(self):
123 return self.path
Guido van Rossum56013131994-06-23 12:06:02 +0000124
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000125 # Return the name of the current folder
126 def getcontext(self):
127 context = pickline(os.path.join(self.getpath(), 'context'),
128 'Current-Folder')
129 if not context: context = 'inbox'
130 return context
Guido van Rossum56013131994-06-23 12:06:02 +0000131
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000132 # Set the name of the current folder
133 def setcontext(self, context):
134 fn = os.path.join(self.getpath(), 'context')
135 f = open(fn, "w")
136 f.write("Current-Folder: %s\n" % context)
137 f.close()
Guido van Rossum508a0921996-05-28 22:59:37 +0000138
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000139 # Return the names of the top-level folders
140 def listfolders(self):
141 folders = []
142 path = self.getpath()
143 for name in os.listdir(path):
144 fullname = os.path.join(path, name)
145 if os.path.isdir(fullname):
146 folders.append(name)
147 folders.sort()
148 return folders
Guido van Rossum56013131994-06-23 12:06:02 +0000149
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000150 # Return the names of the subfolders in a given folder
151 # (prefixed with the given folder name)
152 def listsubfolders(self, name):
153 fullname = os.path.join(self.path, name)
154 # Get the link count so we can avoid listing folders
155 # that have no subfolders.
156 st = os.stat(fullname)
157 nlinks = st[ST_NLINK]
158 if nlinks <= 2:
159 return []
160 subfolders = []
161 subnames = os.listdir(fullname)
162 for subname in subnames:
163 fullsubname = os.path.join(fullname, subname)
164 if os.path.isdir(fullsubname):
165 name_subname = os.path.join(name, subname)
166 subfolders.append(name_subname)
167 # Stop looking for subfolders when
168 # we've seen them all
169 nlinks = nlinks - 1
Guido van Rossum56013131994-06-23 12:06:02 +0000170 if nlinks <= 2:
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000171 break
172 subfolders.sort()
173 return subfolders
Guido van Rossum56013131994-06-23 12:06:02 +0000174
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000175 # Return the names of all folders, including subfolders, recursively
176 def listallfolders(self):
177 return self.listallsubfolders('')
Guido van Rossum56013131994-06-23 12:06:02 +0000178
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000179 # Return the names of subfolders in a given folder, recursively
180 def listallsubfolders(self, name):
181 fullname = os.path.join(self.path, name)
182 # Get the link count so we can avoid listing folders
183 # that have no subfolders.
184 st = os.stat(fullname)
185 nlinks = st[ST_NLINK]
186 if nlinks <= 2:
187 return []
188 subfolders = []
189 subnames = os.listdir(fullname)
190 for subname in subnames:
191 if subname[0] == ',' or isnumeric(subname): continue
192 fullsubname = os.path.join(fullname, subname)
193 if os.path.isdir(fullsubname):
194 name_subname = os.path.join(name, subname)
195 subfolders.append(name_subname)
196 if not os.path.islink(fullsubname):
197 subsubfolders = self.listallsubfolders(
198 name_subname)
199 subfolders = subfolders + subsubfolders
200 # Stop looking for subfolders when
201 # we've seen them all
202 nlinks = nlinks - 1
Guido van Rossum56013131994-06-23 12:06:02 +0000203 if nlinks <= 2:
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000204 break
205 subfolders.sort()
206 return subfolders
Guido van Rossum56013131994-06-23 12:06:02 +0000207
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000208 # Return a new Folder object for the named folder
209 def openfolder(self, name):
210 return Folder(self, name)
Guido van Rossum56013131994-06-23 12:06:02 +0000211
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000212 # Create a new folder. This raises os.error if the folder
213 # cannot be created
214 def makefolder(self, name):
215 protect = pickline(self.profile, 'Folder-Protect')
216 if protect and isnumeric(protect):
217 mode = string.atoi(protect, 8)
218 else:
219 mode = FOLDER_PROTECT
220 os.mkdir(os.path.join(self.getpath(), name), mode)
Guido van Rossum56013131994-06-23 12:06:02 +0000221
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000222 # Delete a folder. This removes files in the folder but not
223 # subdirectories. If deleting the folder itself fails it
224 # raises os.error
225 def deletefolder(self, name):
226 fullname = os.path.join(self.getpath(), name)
227 for subname in os.listdir(fullname):
228 fullsubname = os.path.join(fullname, subname)
229 try:
230 os.unlink(fullsubname)
231 except os.error:
232 self.error('%s not deleted, continuing...' %
233 fullsubname)
234 os.rmdir(fullname)
Guido van Rossum56013131994-06-23 12:06:02 +0000235
236
237# Class representing a particular folder
238
Guido van Rossum9694fca1997-10-22 21:00:49 +0000239numericprog = re.compile('^[1-9][0-9]*$')
Guido van Rossum56013131994-06-23 12:06:02 +0000240def isnumeric(str):
Guido van Rossum9694fca1997-10-22 21:00:49 +0000241 return numericprog.match(str) is not None
Guido van Rossum56013131994-06-23 12:06:02 +0000242
243class Folder:
244
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000245 # Constructor
246 def __init__(self, mh, name):
247 self.mh = mh
248 self.name = name
249 if not os.path.isdir(self.getfullname()):
250 raise Error, 'no folder %s' % name
Guido van Rossum56013131994-06-23 12:06:02 +0000251
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000252 # String representation
253 def __repr__(self):
254 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
Guido van Rossum56013131994-06-23 12:06:02 +0000255
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000256 # Error message handler
257 def error(self, *args):
258 apply(self.mh.error, args)
Guido van Rossum56013131994-06-23 12:06:02 +0000259
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000260 # Return the full pathname of the folder
261 def getfullname(self):
262 return os.path.join(self.mh.path, self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000263
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000264 # Return the full pathname of the folder's sequences file
265 def getsequencesfilename(self):
266 return os.path.join(self.getfullname(), MH_SEQUENCES)
Guido van Rossum56013131994-06-23 12:06:02 +0000267
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000268 # Return the full pathname of a message in the folder
269 def getmessagefilename(self, n):
270 return os.path.join(self.getfullname(), str(n))
Guido van Rossum56013131994-06-23 12:06:02 +0000271
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000272 # Return list of direct subfolders
273 def listsubfolders(self):
274 return self.mh.listsubfolders(self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000275
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000276 # Return list of all subfolders
277 def listallsubfolders(self):
278 return self.mh.listallsubfolders(self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000279
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000280 # Return the list of messages currently present in the folder.
281 # As a side effect, set self.last to the last message (or 0)
282 def listmessages(self):
283 messages = []
284 match = numericprog.match
285 append = messages.append
286 for name in os.listdir(self.getfullname()):
287 if match(name) >= 0:
288 append(name)
289 messages = map(string.atoi, messages)
290 messages.sort()
291 if messages:
292 self.last = messages[-1]
293 else:
294 self.last = 0
295 return messages
Guido van Rossum56013131994-06-23 12:06:02 +0000296
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000297 # Return the set of sequences for the folder
298 def getsequences(self):
299 sequences = {}
300 fullname = self.getsequencesfilename()
301 try:
302 f = open(fullname, 'r')
303 except IOError:
304 return sequences
305 while 1:
306 line = f.readline()
307 if not line: break
308 fields = string.splitfields(line, ':')
309 if len(fields) <> 2:
310 self.error('bad sequence in %s: %s' %
311 (fullname, string.strip(line)))
312 key = string.strip(fields[0])
313 value = IntSet(string.strip(fields[1]), ' ').tolist()
314 sequences[key] = value
315 return sequences
Guido van Rossum56013131994-06-23 12:06:02 +0000316
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000317 # Write the set of sequences back to the folder
318 def putsequences(self, sequences):
319 fullname = self.getsequencesfilename()
320 f = None
321 for key in sequences.keys():
322 s = IntSet('', ' ')
323 s.fromlist(sequences[key])
324 if not f: f = open(fullname, 'w')
325 f.write('%s: %s\n' % (key, s.tostring()))
326 if not f:
327 try:
328 os.unlink(fullname)
329 except os.error:
330 pass
331 else:
332 f.close()
Guido van Rossum56013131994-06-23 12:06:02 +0000333
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000334 # Return the current message. Raise KeyError when there is none
335 def getcurrent(self):
336 seqs = self.getsequences()
337 try:
338 return max(seqs['cur'])
339 except (ValueError, KeyError):
340 raise Error, "no cur message"
341
342 # Set the current message
343 def setcurrent(self, n):
344 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
345
346 # Parse an MH sequence specification into a message list.
347 # Attempt to mimic mh-sequence(5) as close as possible.
348 # Also attempt to mimic observed behavior regarding which
349 # conditions cause which error messages
350 def parsesequence(self, seq):
351 # XXX Still not complete (see mh-format(5)).
352 # Missing are:
353 # - 'prev', 'next' as count
354 # - Sequence-Negation option
355 all = self.listmessages()
356 # Observed behavior: test for empty folder is done first
357 if not all:
358 raise Error, "no messages in %s" % self.name
359 # Common case first: all is frequently the default
360 if seq == 'all':
361 return all
362 # Test for X:Y before X-Y because 'seq:-n' matches both
363 i = string.find(seq, ':')
364 if i >= 0:
365 head, dir, tail = seq[:i], '', seq[i+1:]
366 if tail[:1] in '-+':
367 dir, tail = tail[:1], tail[1:]
368 if not isnumeric(tail):
369 raise Error, "bad message list %s" % seq
370 try:
371 count = string.atoi(tail)
372 except (ValueError, OverflowError):
373 # Can't use sys.maxint because of i+count below
374 count = len(all)
375 try:
376 anchor = self._parseindex(head, all)
377 except Error, msg:
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000378 seqs = self.getsequences()
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000379 if not seqs.has_key(head):
380 if not msg:
381 msg = "bad message list %s" % seq
Guido van Rossumf15d1591997-09-29 23:22:12 +0000382 raise Error, msg, sys.exc_info()[2]
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000383 msgs = seqs[head]
384 if not msgs:
385 raise Error, "sequence %s empty" % head
386 if dir == '-':
387 return msgs[-count:]
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000388 else:
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000389 return msgs[:count]
390 else:
391 if not dir:
392 if head in ('prev', 'last'):
393 dir = '-'
394 if dir == '-':
395 i = bisect(all, anchor)
396 return all[max(0, i-count):i]
397 else:
398 i = bisect(all, anchor-1)
399 return all[i:i+count]
400 # Test for X-Y next
401 i = string.find(seq, '-')
402 if i >= 0:
403 begin = self._parseindex(seq[:i], all)
404 end = self._parseindex(seq[i+1:], all)
405 i = bisect(all, begin-1)
406 j = bisect(all, end)
407 r = all[i:j]
408 if not r:
409 raise Error, "bad message list %s" % seq
410 return r
411 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
412 try:
413 n = self._parseindex(seq, all)
414 except Error, msg:
415 seqs = self.getsequences()
416 if not seqs.has_key(seq):
417 if not msg:
418 msg = "bad message list %s" % seq
419 raise Error, msg
420 return seqs[seq]
421 else:
422 if n not in all:
Guido van Rossum7cfd31e1997-04-16 02:45:08 +0000423 if isnumeric(seq):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000424 raise Error, "message %d doesn't exist" % n
Guido van Rossum56013131994-06-23 12:06:02 +0000425 else:
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000426 raise Error, "no %s message" % seq
427 else:
428 return [n]
429
430 # Internal: parse a message number (or cur, first, etc.)
431 def _parseindex(self, seq, all):
432 if isnumeric(seq):
433 try:
434 return string.atoi(seq)
435 except (OverflowError, ValueError):
436 return sys.maxint
437 if seq in ('cur', '.'):
438 return self.getcurrent()
439 if seq == 'first':
440 return all[0]
441 if seq == 'last':
442 return all[-1]
443 if seq == 'next':
444 n = self.getcurrent()
445 i = bisect(all, n)
446 try:
447 return all[i]
448 except IndexError:
449 raise Error, "no next message"
450 if seq == 'prev':
451 n = self.getcurrent()
452 i = bisect(all, n-1)
453 if i == 0:
454 raise Error, "no prev message"
455 try:
456 return all[i-1]
457 except IndexError:
458 raise Error, "no prev message"
459 raise Error, None
460
461 # Open a message -- returns a Message object
462 def openmessage(self, n):
463 return Message(self, n)
464
465 # Remove one or more messages -- may raise os.error
466 def removemessages(self, list):
467 errors = []
468 deleted = []
469 for n in list:
470 path = self.getmessagefilename(n)
471 commapath = self.getmessagefilename(',' + str(n))
472 try:
473 os.unlink(commapath)
474 except os.error:
475 pass
476 try:
477 os.rename(path, commapath)
478 except os.error, msg:
479 errors.append(msg)
480 else:
481 deleted.append(n)
482 if deleted:
483 self.removefromallsequences(deleted)
484 if errors:
485 if len(errors) == 1:
486 raise os.error, errors[0]
487 else:
488 raise os.error, ('multiple errors:', errors)
489
490 # Refile one or more messages -- may raise os.error.
491 # 'tofolder' is an open folder object
492 def refilemessages(self, list, tofolder, keepsequences=0):
493 errors = []
494 refiled = {}
495 for n in list:
496 ton = tofolder.getlast() + 1
497 path = self.getmessagefilename(n)
498 topath = tofolder.getmessagefilename(ton)
499 try:
500 os.rename(path, topath)
501 except os.error:
502 # Try copying
503 try:
504 shutil.copy2(path, topath)
505 os.unlink(path)
506 except (IOError, os.error), msg:
507 errors.append(msg)
508 try:
509 os.unlink(topath)
510 except os.error:
511 pass
512 continue
513 tofolder.setlast(ton)
514 refiled[n] = ton
515 if refiled:
516 if keepsequences:
517 tofolder._copysequences(self, refiled.items())
518 self.removefromallsequences(refiled.keys())
519 if errors:
520 if len(errors) == 1:
521 raise os.error, errors[0]
522 else:
523 raise os.error, ('multiple errors:', errors)
524
525 # Helper for refilemessages() to copy sequences
526 def _copysequences(self, fromfolder, refileditems):
527 fromsequences = fromfolder.getsequences()
528 tosequences = self.getsequences()
529 changed = 0
530 for name, seq in fromsequences.items():
531 try:
532 toseq = tosequences[name]
533 new = 0
534 except:
535 toseq = []
536 new = 1
537 for fromn, ton in refileditems:
538 if fromn in seq:
539 toseq.append(ton)
540 changed = 1
541 if new and toseq:
542 tosequences[name] = toseq
543 if changed:
544 self.putsequences(tosequences)
545
546 # Move one message over a specific destination message,
547 # which may or may not already exist.
548 def movemessage(self, n, tofolder, ton):
549 path = self.getmessagefilename(n)
550 # Open it to check that it exists
551 f = open(path)
552 f.close()
553 del f
554 topath = tofolder.getmessagefilename(ton)
555 backuptopath = tofolder.getmessagefilename(',%d' % ton)
556 try:
557 os.rename(topath, backuptopath)
558 except os.error:
559 pass
560 try:
561 os.rename(path, topath)
562 except os.error:
563 # Try copying
564 ok = 0
565 try:
566 tofolder.setlast(None)
567 shutil.copy2(path, topath)
568 ok = 1
569 finally:
570 if not ok:
571 try:
572 os.unlink(topath)
573 except os.error:
574 pass
575 os.unlink(path)
576 self.removefromallsequences([n])
577
578 # Copy one message over a specific destination message,
579 # which may or may not already exist.
580 def copymessage(self, n, tofolder, ton):
581 path = self.getmessagefilename(n)
582 # Open it to check that it exists
583 f = open(path)
584 f.close()
585 del f
586 topath = tofolder.getmessagefilename(ton)
587 backuptopath = tofolder.getmessagefilename(',%d' % ton)
588 try:
589 os.rename(topath, backuptopath)
590 except os.error:
591 pass
592 ok = 0
593 try:
594 tofolder.setlast(None)
595 shutil.copy2(path, topath)
596 ok = 1
597 finally:
598 if not ok:
599 try:
600 os.unlink(topath)
601 except os.error:
602 pass
603
Guido van Rossum4e5cbcf1997-07-25 14:59:10 +0000604 # Create a message, with text from the open file txt.
605 def createmessage(self, n, txt):
606 path = self.getmessagefilename(n)
607 backuppath = self.getmessagefilename(',%d' % n)
608 try:
609 os.rename(path, backuppath)
610 except os.error:
611 pass
612 ok = 0
613 BUFSIZE = 16*1024
614 try:
615 f = open(path, "w")
616 while 1:
617 buf = txt.read(BUFSIZE)
618 if not buf:
619 break
620 f.write(buf)
621 f.close()
622 ok = 1
623 finally:
624 if not ok:
625 try:
626 os.unlink(path)
627 except os.error:
628 pass
629
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000630 # Remove one or more messages from all sequeuces (including last)
631 # -- but not from 'cur'!!!
632 def removefromallsequences(self, list):
633 if hasattr(self, 'last') and self.last in list:
634 del self.last
635 sequences = self.getsequences()
636 changed = 0
637 for name, seq in sequences.items():
638 if name == 'cur':
639 continue
640 for n in list:
641 if n in seq:
642 seq.remove(n)
643 changed = 1
644 if not seq:
645 del sequences[name]
646 if changed:
647 self.putsequences(sequences)
648
649 # Return the last message number
650 def getlast(self):
651 if not hasattr(self, 'last'):
652 messages = self.listmessages()
653 return self.last
654
655 # Set the last message number
656 def setlast(self, last):
657 if last is None:
658 if hasattr(self, 'last'):
659 del self.last
660 else:
661 self.last = last
Guido van Rossum56013131994-06-23 12:06:02 +0000662
663class Message(mimetools.Message):
664
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000665 # Constructor
666 def __init__(self, f, n, fp = None):
667 self.folder = f
668 self.number = n
669 if not fp:
670 path = f.getmessagefilename(n)
671 fp = open(path, 'r')
672 mimetools.Message.__init__(self, fp)
Guido van Rossum56013131994-06-23 12:06:02 +0000673
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000674 # String representation
675 def __repr__(self):
676 return 'Message(%s, %s)' % (repr(self.folder), self.number)
Guido van Rossum56013131994-06-23 12:06:02 +0000677
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000678 # Return the message's header text as a string. If an
679 # argument is specified, it is used as a filter predicate to
680 # decide which headers to return (its argument is the header
681 # name converted to lower case).
682 def getheadertext(self, pred = None):
683 if not pred:
684 return string.joinfields(self.headers, '')
685 headers = []
686 hit = 0
687 for line in self.headers:
688 if line[0] not in string.whitespace:
689 i = string.find(line, ':')
690 if i > 0:
691 hit = pred(string.lower(line[:i]))
692 if hit: headers.append(line)
693 return string.joinfields(headers, '')
Guido van Rossum56013131994-06-23 12:06:02 +0000694
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000695 # Return the message's body text as string. This undoes a
696 # Content-Transfer-Encoding, but does not interpret other MIME
697 # features (e.g. multipart messages). To suppress to
698 # decoding, pass a 0 as argument
699 def getbodytext(self, decode = 1):
700 self.fp.seek(self.startofbody)
701 encoding = self.getencoding()
702 if not decode or encoding in ('7bit', '8bit', 'binary'):
703 return self.fp.read()
704 from StringIO import StringIO
705 output = StringIO()
706 mimetools.decode(self.fp, output, encoding)
707 return output.getvalue()
Guido van Rossum56013131994-06-23 12:06:02 +0000708
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000709 # Only for multipart messages: return the message's body as a
710 # list of SubMessage objects. Each submessage object behaves
711 # (almost) as a Message object.
712 def getbodyparts(self):
713 if self.getmaintype() != 'multipart':
714 raise Error, 'Content-Type is not multipart/*'
715 bdry = self.getparam('boundary')
716 if not bdry:
717 raise Error, 'multipart/* without boundary param'
718 self.fp.seek(self.startofbody)
719 mf = multifile.MultiFile(self.fp)
720 mf.push(bdry)
721 parts = []
722 while mf.next():
723 n = str(self.number) + '.' + `1 + len(parts)`
724 part = SubMessage(self.folder, n, mf)
725 parts.append(part)
726 mf.pop()
727 return parts
Guido van Rossum56013131994-06-23 12:06:02 +0000728
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000729 # Return body, either a string or a list of messages
730 def getbody(self):
731 if self.getmaintype() == 'multipart':
732 return self.getbodyparts()
733 else:
734 return self.getbodytext()
Guido van Rossum56013131994-06-23 12:06:02 +0000735
736
737class SubMessage(Message):
738
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000739 # Constructor
740 def __init__(self, f, n, fp):
741 Message.__init__(self, f, n, fp)
742 if self.getmaintype() == 'multipart':
743 self.body = Message.getbodyparts(self)
744 else:
745 self.body = Message.getbodytext(self)
746 # XXX If this is big, should remember file pointers
Guido van Rossum56013131994-06-23 12:06:02 +0000747
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000748 # String representation
749 def __repr__(self):
750 f, n, fp = self.folder, self.number, self.fp
751 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
Guido van Rossum56013131994-06-23 12:06:02 +0000752
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000753 def getbodytext(self):
754 if type(self.body) == type(''):
755 return self.body
Guido van Rossum56013131994-06-23 12:06:02 +0000756
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000757 def getbodyparts(self):
758 if type(self.body) == type([]):
759 return self.body
Guido van Rossum56013131994-06-23 12:06:02 +0000760
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000761 def getbody(self):
762 return self.body
Guido van Rossum56013131994-06-23 12:06:02 +0000763
764
765# Class implementing sets of integers.
766#
767# This is an efficient representation for sets consisting of several
768# continuous ranges, e.g. 1-100,200-400,402-1000 is represented
769# internally as a list of three pairs: [(1,100), (200,400),
770# (402,1000)]. The internal representation is always kept normalized.
771#
772# The constructor has up to three arguments:
773# - the string used to initialize the set (default ''),
774# - the separator between ranges (default ',')
775# - the separator between begin and end of a range (default '-')
776# The separators may be regular expressions and should be different.
777#
778# The tostring() function yields a string that can be passed to another
779# IntSet constructor; __repr__() is a valid IntSet constructor itself.
780#
781# XXX The default begin/end separator means that negative numbers are
782# not supported very well.
783#
784# XXX There are currently no operations to remove set elements.
785
786class IntSet:
787
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000788 def __init__(self, data = None, sep = ',', rng = '-'):
789 self.pairs = []
790 self.sep = sep
791 self.rng = rng
792 if data: self.fromstring(data)
Guido van Rossum56013131994-06-23 12:06:02 +0000793
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000794 def reset(self):
795 self.pairs = []
Guido van Rossum56013131994-06-23 12:06:02 +0000796
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000797 def __cmp__(self, other):
798 return cmp(self.pairs, other.pairs)
Guido van Rossum56013131994-06-23 12:06:02 +0000799
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000800 def __hash__(self):
801 return hash(self.pairs)
Guido van Rossum56013131994-06-23 12:06:02 +0000802
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000803 def __repr__(self):
804 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
805 `self.sep`, `self.rng`)
Guido van Rossum56013131994-06-23 12:06:02 +0000806
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000807 def normalize(self):
808 self.pairs.sort()
809 i = 1
810 while i < len(self.pairs):
811 alo, ahi = self.pairs[i-1]
812 blo, bhi = self.pairs[i]
813 if ahi >= blo-1:
814 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
815 else:
816 i = i+1
Guido van Rossum56013131994-06-23 12:06:02 +0000817
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000818 def tostring(self):
819 s = ''
820 for lo, hi in self.pairs:
821 if lo == hi: t = `lo`
822 else: t = `lo` + self.rng + `hi`
823 if s: s = s + (self.sep + t)
824 else: s = t
825 return s
Guido van Rossum56013131994-06-23 12:06:02 +0000826
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000827 def tolist(self):
828 l = []
829 for lo, hi in self.pairs:
830 m = range(lo, hi+1)
831 l = l + m
832 return l
Guido van Rossum56013131994-06-23 12:06:02 +0000833
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000834 def fromlist(self, list):
835 for i in list:
836 self.append(i)
Guido van Rossum56013131994-06-23 12:06:02 +0000837
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000838 def clone(self):
839 new = IntSet()
840 new.pairs = self.pairs[:]
841 return new
Guido van Rossum56013131994-06-23 12:06:02 +0000842
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000843 def min(self):
844 return self.pairs[0][0]
Guido van Rossum56013131994-06-23 12:06:02 +0000845
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000846 def max(self):
847 return self.pairs[-1][-1]
Guido van Rossum56013131994-06-23 12:06:02 +0000848
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000849 def contains(self, x):
850 for lo, hi in self.pairs:
851 if lo <= x <= hi: return 1
852 return 0
Guido van Rossum56013131994-06-23 12:06:02 +0000853
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000854 def append(self, x):
855 for i in range(len(self.pairs)):
856 lo, hi = self.pairs[i]
857 if x < lo: # Need to insert before
858 if x+1 == lo:
859 self.pairs[i] = (x, hi)
860 else:
861 self.pairs.insert(i, (x, x))
862 if i > 0 and x-1 == self.pairs[i-1][1]:
863 # Merge with previous
864 self.pairs[i-1:i+1] = [
865 (self.pairs[i-1][0],
866 self.pairs[i][1])
867 ]
868 return
869 if x <= hi: # Already in set
870 return
871 i = len(self.pairs) - 1
872 if i >= 0:
873 lo, hi = self.pairs[i]
874 if x-1 == hi:
875 self.pairs[i] = lo, x
876 return
877 self.pairs.append((x, x))
Guido van Rossum56013131994-06-23 12:06:02 +0000878
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000879 def addpair(self, xlo, xhi):
880 if xlo > xhi: return
881 self.pairs.append((xlo, xhi))
882 self.normalize()
Guido van Rossum56013131994-06-23 12:06:02 +0000883
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000884 def fromstring(self, data):
885 import string, regsub
886 new = []
887 for part in regsub.split(data, self.sep):
888 list = []
889 for subp in regsub.split(part, self.rng):
890 s = string.strip(subp)
891 list.append(string.atoi(s))
892 if len(list) == 1:
893 new.append((list[0], list[0]))
894 elif len(list) == 2 and list[0] <= list[1]:
895 new.append((list[0], list[1]))
896 else:
897 raise ValueError, 'bad data passed to IntSet'
898 self.pairs = self.pairs + new
899 self.normalize()
Guido van Rossum56013131994-06-23 12:06:02 +0000900
901
902# Subroutines to read/write entries in .mh_profile and .mh_sequences
903
904def pickline(file, key, casefold = 1):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000905 try:
906 f = open(file, 'r')
907 except IOError:
Guido van Rossum56013131994-06-23 12:06:02 +0000908 return None
Guido van Rossum9694fca1997-10-22 21:00:49 +0000909 pat = re.escape(key) + ':'
910 prog = re.compile(pat, casefold and re.IGNORECASE)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000911 while 1:
912 line = f.readline()
913 if not line: break
Guido van Rossum9694fca1997-10-22 21:00:49 +0000914 if prog.match(line):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000915 text = line[len(key)+1:]
916 while 1:
917 line = f.readline()
918 if not line or line[0] not in string.whitespace:
919 break
920 text = text + line
921 return string.strip(text)
922 return None
Guido van Rossum56013131994-06-23 12:06:02 +0000923
924def updateline(file, key, value, casefold = 1):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000925 try:
926 f = open(file, 'r')
927 lines = f.readlines()
Guido van Rossum56013131994-06-23 12:06:02 +0000928 f.close()
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000929 except IOError:
930 lines = []
Guido van Rossum9694fca1997-10-22 21:00:49 +0000931 pat = re.escape(key) + ':(.*)\n'
932 prog = re.compile(pat, casefold and re.IGNORECASE)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000933 if value is None:
934 newline = None
935 else:
936 newline = '%s: %s\n' % (key, value)
937 for i in range(len(lines)):
938 line = lines[i]
Guido van Rossum9694fca1997-10-22 21:00:49 +0000939 if prog.match(line):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000940 if newline is None:
941 del lines[i]
942 else:
943 lines[i] = newline
944 break
945 else:
946 if newline is not None:
947 lines.append(newline)
948 tempfile = file + "~"
949 f = open(tempfile, 'w')
950 for line in lines:
951 f.write(line)
952 f.close()
953 os.rename(tempfile, file)
Guido van Rossum56013131994-06-23 12:06:02 +0000954
955
956# Test program
957
958def test():
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000959 global mh, f
960 os.system('rm -rf $HOME/Mail/@test')
961 mh = MH()
962 def do(s): print s; print eval(s)
963 do('mh.listfolders()')
964 do('mh.listallfolders()')
965 testfolders = ['@test', '@test/test1', '@test/test2',
966 '@test/test1/test11', '@test/test1/test12',
967 '@test/test1/test11/test111']
968 for t in testfolders: do('mh.makefolder(%s)' % `t`)
969 do('mh.listsubfolders(\'@test\')')
970 do('mh.listallsubfolders(\'@test\')')
971 f = mh.openfolder('@test')
972 do('f.listsubfolders()')
973 do('f.listallsubfolders()')
974 do('f.getsequences()')
975 seqs = f.getsequences()
976 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
977 print seqs
978 f.putsequences(seqs)
979 do('f.getsequences()')
980 testfolders.reverse()
981 for t in testfolders: do('mh.deletefolder(%s)' % `t`)
982 do('mh.getcontext()')
983 context = mh.getcontext()
984 f = mh.openfolder(context)
985 do('f.getcurrent()')
986 for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
987 'first:3', 'last:3', 'cur:3', 'cur:-3',
988 'prev:3', 'next:3',
989 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
990 'all']:
991 try:
992 do('f.parsesequence(%s)' % `seq`)
993 except Error, msg:
994 print "Error:", msg
995 stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
996 list = map(string.atoi, string.split(stuff))
997 print list, "<-- pick"
998 do('f.listmessages()')
Guido van Rossum56013131994-06-23 12:06:02 +0000999
1000
1001if __name__ == '__main__':
Guido van Rossum0c5e0491997-04-16 02:47:12 +00001002 test()