blob: 23e936df026fbb2f3d839d1ab7425748a48c4003 [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 Rossum4fe6caa1999-02-24 16:25:17 +000042# f.createmessage(n, fp) # add message from file f as number n
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000043# f.removemessages(list) # remove messages in list from folder
44# f.refilemessages(list, tofolder) # move messages in list to other folder
45# f.movemessage(n, tofolder, ton) # move one message to a given destination
46# f.copymessage(n, tofolder, ton) # copy one message to a given destination
47#
Guido van Rossum56013131994-06-23 12:06:02 +000048# m = f.openmessage(n) # new open message object (costs a file descriptor)
Guido van Rossum85347411994-09-09 11:10:15 +000049# m is a derived class of mimetools.Message(rfc822.Message), with:
Guido van Rossum56013131994-06-23 12:06:02 +000050# s = m.getheadertext() # text of message's headers
51# s = m.getheadertext(pred) # text of message's headers, filtered by pred
52# s = m.getbodytext() # text of message's body, decoded
53# s = m.getbodytext(0) # text of message's body, not decoded
54#
55# XXX To do, functionality:
Guido van Rossum56013131994-06-23 12:06:02 +000056# - annotate messages
Guido van Rossum4fe6caa1999-02-24 16:25:17 +000057# - send messages
Guido van Rossum56013131994-06-23 12:06:02 +000058#
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000059# XXX To do, organization:
Guido van Rossum56013131994-06-23 12:06:02 +000060# - move IntSet to separate file
61# - move most Message functionality to module mimetools
62
63
64# Customizable defaults
65
66MH_PROFILE = '~/.mh_profile'
67PATH = '~/Mail'
68MH_SEQUENCES = '.mh_sequences'
69FOLDER_PROTECT = 0700
70
71
72# Imported modules
73
74import os
Guido van Rossum508a0921996-05-28 22:59:37 +000075import sys
Guido van Rossum56013131994-06-23 12:06:02 +000076from stat import ST_NLINK
Guido van Rossum9694fca1997-10-22 21:00:49 +000077import re
Guido van Rossum56013131994-06-23 12:06:02 +000078import string
79import mimetools
80import multifile
Guido van Rossum40b2cfb1995-01-02 18:38:23 +000081import shutil
Guido van Rossum7cfd31e1997-04-16 02:45:08 +000082from bisect import bisect
Guido van Rossum56013131994-06-23 12:06:02 +000083
84
85# Exported constants
86
87Error = 'mhlib.Error'
88
89
90# Class representing a particular collection of folders.
91# Optional constructor arguments are the pathname for the directory
92# containing the collection, and the MH profile to use.
93# If either is omitted or empty a default is used; the default
94# directory is taken from the MH profile if it is specified there.
95
96class MH:
97
Guido van Rossum0c5e0491997-04-16 02:47:12 +000098 # Constructor
99 def __init__(self, path = None, profile = None):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000100 if not profile: profile = MH_PROFILE
101 self.profile = os.path.expanduser(profile)
102 if not path: path = self.getprofile('Path')
103 if not path: path = PATH
104 if not os.path.isabs(path) and path[0] != '~':
105 path = os.path.join('~', path)
106 path = os.path.expanduser(path)
107 if not os.path.isdir(path): raise Error, 'MH() path not found'
108 self.path = path
Guido van Rossum56013131994-06-23 12:06:02 +0000109
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000110 # String representation
111 def __repr__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000112 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
Guido van Rossum56013131994-06-23 12:06:02 +0000113
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000114 # Routine to print an error. May be overridden by a derived class
115 def error(self, msg, *args):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000116 sys.stderr.write('MH error: %s\n' % (msg % args))
Guido van Rossum56013131994-06-23 12:06:02 +0000117
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000118 # Return a profile entry, None if not found
119 def getprofile(self, key):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000120 return pickline(self.profile, key)
Guido van Rossum56013131994-06-23 12:06:02 +0000121
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000122 # Return the path (the name of the collection's directory)
123 def getpath(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000124 return self.path
Guido van Rossum56013131994-06-23 12:06:02 +0000125
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000126 # Return the name of the current folder
127 def getcontext(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000128 context = pickline(os.path.join(self.getpath(), 'context'),
129 'Current-Folder')
130 if not context: context = 'inbox'
131 return context
Guido van Rossum56013131994-06-23 12:06:02 +0000132
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000133 # Set the name of the current folder
134 def setcontext(self, context):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000135 fn = os.path.join(self.getpath(), 'context')
136 f = open(fn, "w")
137 f.write("Current-Folder: %s\n" % context)
138 f.close()
Guido van Rossum508a0921996-05-28 22:59:37 +0000139
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000140 # Return the names of the top-level folders
141 def listfolders(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000142 folders = []
143 path = self.getpath()
144 for name in os.listdir(path):
145 fullname = os.path.join(path, name)
146 if os.path.isdir(fullname):
147 folders.append(name)
148 folders.sort()
149 return folders
Guido van Rossum56013131994-06-23 12:06:02 +0000150
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000151 # Return the names of the subfolders in a given folder
152 # (prefixed with the given folder name)
153 def listsubfolders(self, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000154 fullname = os.path.join(self.path, name)
155 # Get the link count so we can avoid listing folders
156 # that have no subfolders.
157 st = os.stat(fullname)
158 nlinks = st[ST_NLINK]
159 if nlinks <= 2:
160 return []
161 subfolders = []
162 subnames = os.listdir(fullname)
163 for subname in subnames:
164 fullsubname = os.path.join(fullname, subname)
165 if os.path.isdir(fullsubname):
166 name_subname = os.path.join(name, subname)
167 subfolders.append(name_subname)
168 # Stop looking for subfolders when
169 # we've seen them all
170 nlinks = nlinks - 1
171 if nlinks <= 2:
172 break
173 subfolders.sort()
174 return subfolders
Guido van Rossum56013131994-06-23 12:06:02 +0000175
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000176 # Return the names of all folders, including subfolders, recursively
177 def listallfolders(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000178 return self.listallsubfolders('')
Guido van Rossum56013131994-06-23 12:06:02 +0000179
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000180 # Return the names of subfolders in a given folder, recursively
181 def listallsubfolders(self, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000182 fullname = os.path.join(self.path, name)
183 # Get the link count so we can avoid listing folders
184 # that have no subfolders.
185 st = os.stat(fullname)
186 nlinks = st[ST_NLINK]
187 if nlinks <= 2:
188 return []
189 subfolders = []
190 subnames = os.listdir(fullname)
191 for subname in subnames:
192 if subname[0] == ',' or isnumeric(subname): continue
193 fullsubname = os.path.join(fullname, subname)
194 if os.path.isdir(fullsubname):
195 name_subname = os.path.join(name, subname)
196 subfolders.append(name_subname)
197 if not os.path.islink(fullsubname):
198 subsubfolders = self.listallsubfolders(
199 name_subname)
200 subfolders = subfolders + subsubfolders
201 # Stop looking for subfolders when
202 # we've seen them all
203 nlinks = nlinks - 1
204 if nlinks <= 2:
205 break
206 subfolders.sort()
207 return subfolders
Guido van Rossum56013131994-06-23 12:06:02 +0000208
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000209 # Return a new Folder object for the named folder
210 def openfolder(self, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000211 return Folder(self, name)
Guido van Rossum56013131994-06-23 12:06:02 +0000212
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000213 # Create a new folder. This raises os.error if the folder
214 # cannot be created
215 def makefolder(self, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000216 protect = pickline(self.profile, 'Folder-Protect')
217 if protect and isnumeric(protect):
218 mode = string.atoi(protect, 8)
219 else:
220 mode = FOLDER_PROTECT
221 os.mkdir(os.path.join(self.getpath(), name), mode)
Guido van Rossum56013131994-06-23 12:06:02 +0000222
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000223 # Delete a folder. This removes files in the folder but not
224 # subdirectories. If deleting the folder itself fails it
225 # raises os.error
226 def deletefolder(self, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000227 fullname = os.path.join(self.getpath(), name)
228 for subname in os.listdir(fullname):
229 fullsubname = os.path.join(fullname, subname)
230 try:
231 os.unlink(fullsubname)
232 except os.error:
233 self.error('%s not deleted, continuing...' %
234 fullsubname)
235 os.rmdir(fullname)
Guido van Rossum56013131994-06-23 12:06:02 +0000236
237
238# Class representing a particular folder
239
Guido van Rossum9694fca1997-10-22 21:00:49 +0000240numericprog = re.compile('^[1-9][0-9]*$')
Guido van Rossum56013131994-06-23 12:06:02 +0000241def isnumeric(str):
Guido van Rossum9694fca1997-10-22 21:00:49 +0000242 return numericprog.match(str) is not None
Guido van Rossum56013131994-06-23 12:06:02 +0000243
244class Folder:
245
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000246 # Constructor
247 def __init__(self, mh, name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000248 self.mh = mh
249 self.name = name
250 if not os.path.isdir(self.getfullname()):
251 raise Error, 'no folder %s' % name
Guido van Rossum56013131994-06-23 12:06:02 +0000252
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000253 # String representation
254 def __repr__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000255 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
Guido van Rossum56013131994-06-23 12:06:02 +0000256
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000257 # Error message handler
258 def error(self, *args):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000259 apply(self.mh.error, args)
Guido van Rossum56013131994-06-23 12:06:02 +0000260
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000261 # Return the full pathname of the folder
262 def getfullname(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000263 return os.path.join(self.mh.path, self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000264
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000265 # Return the full pathname of the folder's sequences file
266 def getsequencesfilename(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000267 return os.path.join(self.getfullname(), MH_SEQUENCES)
Guido van Rossum56013131994-06-23 12:06:02 +0000268
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000269 # Return the full pathname of a message in the folder
270 def getmessagefilename(self, n):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000271 return os.path.join(self.getfullname(), str(n))
Guido van Rossum56013131994-06-23 12:06:02 +0000272
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000273 # Return list of direct subfolders
274 def listsubfolders(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000275 return self.mh.listsubfolders(self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000276
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000277 # Return list of all subfolders
278 def listallsubfolders(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000279 return self.mh.listallsubfolders(self.name)
Guido van Rossum56013131994-06-23 12:06:02 +0000280
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000281 # Return the list of messages currently present in the folder.
282 # As a side effect, set self.last to the last message (or 0)
283 def listmessages(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000284 messages = []
285 match = numericprog.match
286 append = messages.append
287 for name in os.listdir(self.getfullname()):
Guido van Rossumd9d26251998-06-23 14:43:06 +0000288 if match(name):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000289 append(name)
290 messages = map(string.atoi, messages)
291 messages.sort()
292 if messages:
293 self.last = messages[-1]
294 else:
295 self.last = 0
296 return messages
Guido van Rossum56013131994-06-23 12:06:02 +0000297
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000298 # Return the set of sequences for the folder
299 def getsequences(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000300 sequences = {}
301 fullname = self.getsequencesfilename()
302 try:
303 f = open(fullname, 'r')
304 except IOError:
305 return sequences
306 while 1:
307 line = f.readline()
308 if not line: break
309 fields = string.splitfields(line, ':')
310 if len(fields) <> 2:
311 self.error('bad sequence in %s: %s' %
312 (fullname, string.strip(line)))
313 key = string.strip(fields[0])
314 value = IntSet(string.strip(fields[1]), ' ').tolist()
315 sequences[key] = value
316 return sequences
Guido van Rossum56013131994-06-23 12:06:02 +0000317
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000318 # Write the set of sequences back to the folder
319 def putsequences(self, sequences):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000320 fullname = self.getsequencesfilename()
321 f = None
322 for key in sequences.keys():
323 s = IntSet('', ' ')
324 s.fromlist(sequences[key])
325 if not f: f = open(fullname, 'w')
326 f.write('%s: %s\n' % (key, s.tostring()))
327 if not f:
328 try:
329 os.unlink(fullname)
330 except os.error:
331 pass
332 else:
333 f.close()
Guido van Rossum56013131994-06-23 12:06:02 +0000334
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000335 # Return the current message. Raise KeyError when there is none
336 def getcurrent(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000337 seqs = self.getsequences()
338 try:
339 return max(seqs['cur'])
340 except (ValueError, KeyError):
341 raise Error, "no cur message"
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000342
343 # Set the current message
344 def setcurrent(self, n):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000345 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000346
347 # Parse an MH sequence specification into a message list.
348 # Attempt to mimic mh-sequence(5) as close as possible.
349 # Also attempt to mimic observed behavior regarding which
350 # conditions cause which error messages
351 def parsesequence(self, seq):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000352 # XXX Still not complete (see mh-format(5)).
353 # Missing are:
354 # - 'prev', 'next' as count
355 # - Sequence-Negation option
356 all = self.listmessages()
357 # Observed behavior: test for empty folder is done first
358 if not all:
359 raise Error, "no messages in %s" % self.name
360 # Common case first: all is frequently the default
361 if seq == 'all':
362 return all
363 # Test for X:Y before X-Y because 'seq:-n' matches both
364 i = string.find(seq, ':')
365 if i >= 0:
366 head, dir, tail = seq[:i], '', seq[i+1:]
367 if tail[:1] in '-+':
368 dir, tail = tail[:1], tail[1:]
369 if not isnumeric(tail):
370 raise Error, "bad message list %s" % seq
371 try:
372 count = string.atoi(tail)
373 except (ValueError, OverflowError):
374 # Can't use sys.maxint because of i+count below
375 count = len(all)
376 try:
377 anchor = self._parseindex(head, all)
378 except Error, msg:
379 seqs = self.getsequences()
380 if not seqs.has_key(head):
381 if not msg:
382 msg = "bad message list %s" % seq
383 raise Error, msg, sys.exc_info()[2]
384 msgs = seqs[head]
385 if not msgs:
386 raise Error, "sequence %s empty" % head
387 if dir == '-':
388 return msgs[-count:]
389 else:
390 return msgs[:count]
391 else:
392 if not dir:
393 if head in ('prev', 'last'):
394 dir = '-'
395 if dir == '-':
396 i = bisect(all, anchor)
397 return all[max(0, i-count):i]
398 else:
399 i = bisect(all, anchor-1)
400 return all[i:i+count]
401 # Test for X-Y next
402 i = string.find(seq, '-')
403 if i >= 0:
404 begin = self._parseindex(seq[:i], all)
405 end = self._parseindex(seq[i+1:], all)
406 i = bisect(all, begin-1)
407 j = bisect(all, end)
408 r = all[i:j]
409 if not r:
410 raise Error, "bad message list %s" % seq
411 return r
412 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
413 try:
414 n = self._parseindex(seq, all)
415 except Error, msg:
416 seqs = self.getsequences()
417 if not seqs.has_key(seq):
418 if not msg:
419 msg = "bad message list %s" % seq
420 raise Error, msg
421 return seqs[seq]
422 else:
423 if n not in all:
424 if isnumeric(seq):
425 raise Error, "message %d doesn't exist" % n
426 else:
427 raise Error, "no %s message" % seq
428 else:
429 return [n]
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000430
431 # Internal: parse a message number (or cur, first, etc.)
432 def _parseindex(self, seq, all):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000433 if isnumeric(seq):
434 try:
435 return string.atoi(seq)
436 except (OverflowError, ValueError):
437 return sys.maxint
438 if seq in ('cur', '.'):
439 return self.getcurrent()
440 if seq == 'first':
441 return all[0]
442 if seq == 'last':
443 return all[-1]
444 if seq == 'next':
445 n = self.getcurrent()
446 i = bisect(all, n)
447 try:
448 return all[i]
449 except IndexError:
450 raise Error, "no next message"
451 if seq == 'prev':
452 n = self.getcurrent()
453 i = bisect(all, n-1)
454 if i == 0:
455 raise Error, "no prev message"
456 try:
457 return all[i-1]
458 except IndexError:
459 raise Error, "no prev message"
460 raise Error, None
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000461
462 # Open a message -- returns a Message object
463 def openmessage(self, n):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000464 return Message(self, n)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000465
466 # Remove one or more messages -- may raise os.error
467 def removemessages(self, list):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000468 errors = []
469 deleted = []
470 for n in list:
471 path = self.getmessagefilename(n)
472 commapath = self.getmessagefilename(',' + str(n))
473 try:
474 os.unlink(commapath)
475 except os.error:
476 pass
477 try:
478 os.rename(path, commapath)
479 except os.error, msg:
480 errors.append(msg)
481 else:
482 deleted.append(n)
483 if deleted:
484 self.removefromallsequences(deleted)
485 if errors:
486 if len(errors) == 1:
487 raise os.error, errors[0]
488 else:
489 raise os.error, ('multiple errors:', errors)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000490
491 # Refile one or more messages -- may raise os.error.
492 # 'tofolder' is an open folder object
493 def refilemessages(self, list, tofolder, keepsequences=0):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000494 errors = []
495 refiled = {}
496 for n in list:
497 ton = tofolder.getlast() + 1
498 path = self.getmessagefilename(n)
499 topath = tofolder.getmessagefilename(ton)
500 try:
501 os.rename(path, topath)
502 except os.error:
503 # Try copying
504 try:
505 shutil.copy2(path, topath)
506 os.unlink(path)
507 except (IOError, os.error), msg:
508 errors.append(msg)
509 try:
510 os.unlink(topath)
511 except os.error:
512 pass
513 continue
514 tofolder.setlast(ton)
515 refiled[n] = ton
516 if refiled:
517 if keepsequences:
518 tofolder._copysequences(self, refiled.items())
519 self.removefromallsequences(refiled.keys())
520 if errors:
521 if len(errors) == 1:
522 raise os.error, errors[0]
523 else:
524 raise os.error, ('multiple errors:', errors)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000525
526 # Helper for refilemessages() to copy sequences
527 def _copysequences(self, fromfolder, refileditems):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000528 fromsequences = fromfolder.getsequences()
529 tosequences = self.getsequences()
530 changed = 0
531 for name, seq in fromsequences.items():
532 try:
533 toseq = tosequences[name]
534 new = 0
535 except:
536 toseq = []
537 new = 1
538 for fromn, ton in refileditems:
539 if fromn in seq:
540 toseq.append(ton)
541 changed = 1
542 if new and toseq:
543 tosequences[name] = toseq
544 if changed:
545 self.putsequences(tosequences)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000546
547 # Move one message over a specific destination message,
548 # which may or may not already exist.
549 def movemessage(self, n, tofolder, ton):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000550 path = self.getmessagefilename(n)
551 # Open it to check that it exists
552 f = open(path)
553 f.close()
554 del f
555 topath = tofolder.getmessagefilename(ton)
556 backuptopath = tofolder.getmessagefilename(',%d' % ton)
557 try:
558 os.rename(topath, backuptopath)
559 except os.error:
560 pass
561 try:
562 os.rename(path, topath)
563 except os.error:
564 # Try copying
565 ok = 0
566 try:
567 tofolder.setlast(None)
568 shutil.copy2(path, topath)
569 ok = 1
570 finally:
571 if not ok:
572 try:
573 os.unlink(topath)
574 except os.error:
575 pass
576 os.unlink(path)
577 self.removefromallsequences([n])
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000578
579 # Copy one message over a specific destination message,
580 # which may or may not already exist.
581 def copymessage(self, n, tofolder, ton):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000582 path = self.getmessagefilename(n)
583 # Open it to check that it exists
584 f = open(path)
585 f.close()
586 del f
587 topath = tofolder.getmessagefilename(ton)
588 backuptopath = tofolder.getmessagefilename(',%d' % ton)
589 try:
590 os.rename(topath, backuptopath)
591 except os.error:
592 pass
593 ok = 0
594 try:
595 tofolder.setlast(None)
596 shutil.copy2(path, topath)
597 ok = 1
598 finally:
599 if not ok:
600 try:
601 os.unlink(topath)
602 except os.error:
603 pass
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000604
Guido van Rossum4e5cbcf1997-07-25 14:59:10 +0000605 # Create a message, with text from the open file txt.
606 def createmessage(self, n, txt):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000607 path = self.getmessagefilename(n)
608 backuppath = self.getmessagefilename(',%d' % n)
609 try:
610 os.rename(path, backuppath)
611 except os.error:
612 pass
613 ok = 0
614 BUFSIZE = 16*1024
615 try:
616 f = open(path, "w")
617 while 1:
618 buf = txt.read(BUFSIZE)
619 if not buf:
620 break
621 f.write(buf)
622 f.close()
623 ok = 1
624 finally:
625 if not ok:
626 try:
627 os.unlink(path)
628 except os.error:
629 pass
Guido van Rossum4e5cbcf1997-07-25 14:59:10 +0000630
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000631 # Remove one or more messages from all sequeuces (including last)
632 # -- but not from 'cur'!!!
633 def removefromallsequences(self, list):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000634 if hasattr(self, 'last') and self.last in list:
635 del self.last
636 sequences = self.getsequences()
637 changed = 0
638 for name, seq in sequences.items():
639 if name == 'cur':
640 continue
641 for n in list:
642 if n in seq:
643 seq.remove(n)
644 changed = 1
645 if not seq:
646 del sequences[name]
647 if changed:
648 self.putsequences(sequences)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000649
650 # Return the last message number
651 def getlast(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000652 if not hasattr(self, 'last'):
653 messages = self.listmessages()
654 return self.last
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000655
656 # Set the last message number
657 def setlast(self, last):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000658 if last is None:
659 if hasattr(self, 'last'):
660 del self.last
661 else:
662 self.last = last
Guido van Rossum56013131994-06-23 12:06:02 +0000663
664class Message(mimetools.Message):
665
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000666 # Constructor
667 def __init__(self, f, n, fp = None):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000668 self.folder = f
669 self.number = n
670 if not fp:
671 path = f.getmessagefilename(n)
672 fp = open(path, 'r')
673 mimetools.Message.__init__(self, fp)
Guido van Rossum56013131994-06-23 12:06:02 +0000674
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000675 # String representation
676 def __repr__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000677 return 'Message(%s, %s)' % (repr(self.folder), self.number)
Guido van Rossum56013131994-06-23 12:06:02 +0000678
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000679 # Return the message's header text as a string. If an
680 # argument is specified, it is used as a filter predicate to
681 # decide which headers to return (its argument is the header
682 # name converted to lower case).
683 def getheadertext(self, pred = None):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000684 if not pred:
685 return string.joinfields(self.headers, '')
686 headers = []
687 hit = 0
688 for line in self.headers:
689 if line[0] not in string.whitespace:
690 i = string.find(line, ':')
691 if i > 0:
692 hit = pred(string.lower(line[:i]))
693 if hit: headers.append(line)
694 return string.joinfields(headers, '')
Guido van Rossum56013131994-06-23 12:06:02 +0000695
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000696 # Return the message's body text as string. This undoes a
697 # Content-Transfer-Encoding, but does not interpret other MIME
698 # features (e.g. multipart messages). To suppress to
699 # decoding, pass a 0 as argument
700 def getbodytext(self, decode = 1):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000701 self.fp.seek(self.startofbody)
702 encoding = self.getencoding()
Guido van Rossum4fe6caa1999-02-24 16:25:17 +0000703 if not decode or encoding in ('', '7bit', '8bit', 'binary'):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000704 return self.fp.read()
705 from StringIO import StringIO
706 output = StringIO()
707 mimetools.decode(self.fp, output, encoding)
708 return output.getvalue()
Guido van Rossum56013131994-06-23 12:06:02 +0000709
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000710 # Only for multipart messages: return the message's body as a
711 # list of SubMessage objects. Each submessage object behaves
712 # (almost) as a Message object.
713 def getbodyparts(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000714 if self.getmaintype() != 'multipart':
715 raise Error, 'Content-Type is not multipart/*'
716 bdry = self.getparam('boundary')
717 if not bdry:
718 raise Error, 'multipart/* without boundary param'
719 self.fp.seek(self.startofbody)
720 mf = multifile.MultiFile(self.fp)
721 mf.push(bdry)
722 parts = []
723 while mf.next():
724 n = str(self.number) + '.' + `1 + len(parts)`
725 part = SubMessage(self.folder, n, mf)
726 parts.append(part)
727 mf.pop()
728 return parts
Guido van Rossum56013131994-06-23 12:06:02 +0000729
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000730 # Return body, either a string or a list of messages
731 def getbody(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000732 if self.getmaintype() == 'multipart':
733 return self.getbodyparts()
734 else:
735 return self.getbodytext()
Guido van Rossum56013131994-06-23 12:06:02 +0000736
737
738class SubMessage(Message):
739
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000740 # Constructor
741 def __init__(self, f, n, fp):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000742 Message.__init__(self, f, n, fp)
743 if self.getmaintype() == 'multipart':
744 self.body = Message.getbodyparts(self)
745 else:
746 self.body = Message.getbodytext(self)
Guido van Rossum4fe6caa1999-02-24 16:25:17 +0000747 self.bodyencoded = Message.getbodytext(self, decode=0)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000748 # XXX If this is big, should remember file pointers
Guido van Rossum56013131994-06-23 12:06:02 +0000749
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000750 # String representation
751 def __repr__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000752 f, n, fp = self.folder, self.number, self.fp
753 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
Guido van Rossum56013131994-06-23 12:06:02 +0000754
Guido van Rossum4fe6caa1999-02-24 16:25:17 +0000755 def getbodytext(self, decode = 1):
756 if not decode:
757 return self.bodyencoded
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000758 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 getbodyparts(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000762 if type(self.body) == type([]):
763 return self.body
Guido van Rossum56013131994-06-23 12:06:02 +0000764
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000765 def getbody(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000766 return self.body
Guido van Rossum56013131994-06-23 12:06:02 +0000767
768
769# Class implementing sets of integers.
770#
771# This is an efficient representation for sets consisting of several
772# continuous ranges, e.g. 1-100,200-400,402-1000 is represented
773# internally as a list of three pairs: [(1,100), (200,400),
774# (402,1000)]. The internal representation is always kept normalized.
775#
776# The constructor has up to three arguments:
777# - the string used to initialize the set (default ''),
778# - the separator between ranges (default ',')
779# - the separator between begin and end of a range (default '-')
Guido van Rossum00f9fea1997-12-24 21:18:41 +0000780# The separators must be strings (not regexprs) and should be different.
Guido van Rossum56013131994-06-23 12:06:02 +0000781#
782# The tostring() function yields a string that can be passed to another
783# IntSet constructor; __repr__() is a valid IntSet constructor itself.
784#
785# XXX The default begin/end separator means that negative numbers are
786# not supported very well.
787#
788# XXX There are currently no operations to remove set elements.
789
790class IntSet:
791
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000792 def __init__(self, data = None, sep = ',', rng = '-'):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000793 self.pairs = []
794 self.sep = sep
795 self.rng = rng
796 if data: self.fromstring(data)
Guido van Rossum56013131994-06-23 12:06:02 +0000797
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000798 def reset(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000799 self.pairs = []
Guido van Rossum56013131994-06-23 12:06:02 +0000800
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000801 def __cmp__(self, other):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000802 return cmp(self.pairs, other.pairs)
Guido van Rossum56013131994-06-23 12:06:02 +0000803
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000804 def __hash__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000805 return hash(self.pairs)
Guido van Rossum56013131994-06-23 12:06:02 +0000806
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000807 def __repr__(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000808 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
809 `self.sep`, `self.rng`)
Guido van Rossum56013131994-06-23 12:06:02 +0000810
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000811 def normalize(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000812 self.pairs.sort()
813 i = 1
814 while i < len(self.pairs):
815 alo, ahi = self.pairs[i-1]
816 blo, bhi = self.pairs[i]
817 if ahi >= blo-1:
818 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
819 else:
820 i = i+1
Guido van Rossum56013131994-06-23 12:06:02 +0000821
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000822 def tostring(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000823 s = ''
824 for lo, hi in self.pairs:
825 if lo == hi: t = `lo`
826 else: t = `lo` + self.rng + `hi`
827 if s: s = s + (self.sep + t)
828 else: s = t
829 return s
Guido van Rossum56013131994-06-23 12:06:02 +0000830
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000831 def tolist(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000832 l = []
833 for lo, hi in self.pairs:
834 m = range(lo, hi+1)
835 l = l + m
836 return l
Guido van Rossum56013131994-06-23 12:06:02 +0000837
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000838 def fromlist(self, list):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000839 for i in list:
840 self.append(i)
Guido van Rossum56013131994-06-23 12:06:02 +0000841
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000842 def clone(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000843 new = IntSet()
844 new.pairs = self.pairs[:]
845 return new
Guido van Rossum56013131994-06-23 12:06:02 +0000846
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000847 def min(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000848 return self.pairs[0][0]
Guido van Rossum56013131994-06-23 12:06:02 +0000849
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000850 def max(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000851 return self.pairs[-1][-1]
Guido van Rossum56013131994-06-23 12:06:02 +0000852
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000853 def contains(self, x):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000854 for lo, hi in self.pairs:
855 if lo <= x <= hi: return 1
856 return 0
Guido van Rossum56013131994-06-23 12:06:02 +0000857
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000858 def append(self, x):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000859 for i in range(len(self.pairs)):
860 lo, hi = self.pairs[i]
861 if x < lo: # Need to insert before
862 if x+1 == lo:
863 self.pairs[i] = (x, hi)
864 else:
865 self.pairs.insert(i, (x, x))
866 if i > 0 and x-1 == self.pairs[i-1][1]:
867 # Merge with previous
868 self.pairs[i-1:i+1] = [
869 (self.pairs[i-1][0],
870 self.pairs[i][1])
871 ]
872 return
873 if x <= hi: # Already in set
874 return
875 i = len(self.pairs) - 1
876 if i >= 0:
877 lo, hi = self.pairs[i]
878 if x-1 == hi:
879 self.pairs[i] = lo, x
880 return
881 self.pairs.append((x, x))
Guido van Rossum56013131994-06-23 12:06:02 +0000882
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000883 def addpair(self, xlo, xhi):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000884 if xlo > xhi: return
885 self.pairs.append((xlo, xhi))
886 self.normalize()
Guido van Rossum56013131994-06-23 12:06:02 +0000887
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000888 def fromstring(self, data):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000889 import string
890 new = []
891 for part in string.splitfields(data, self.sep):
892 list = []
893 for subp in string.splitfields(part, self.rng):
894 s = string.strip(subp)
895 list.append(string.atoi(s))
896 if len(list) == 1:
897 new.append((list[0], list[0]))
898 elif len(list) == 2 and list[0] <= list[1]:
899 new.append((list[0], list[1]))
900 else:
901 raise ValueError, 'bad data passed to IntSet'
902 self.pairs = self.pairs + new
903 self.normalize()
Guido van Rossum56013131994-06-23 12:06:02 +0000904
905
906# Subroutines to read/write entries in .mh_profile and .mh_sequences
907
908def pickline(file, key, casefold = 1):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000909 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000910 f = open(file, 'r')
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000911 except IOError:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000912 return None
Guido van Rossum9694fca1997-10-22 21:00:49 +0000913 pat = re.escape(key) + ':'
914 prog = re.compile(pat, casefold and re.IGNORECASE)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000915 while 1:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000916 line = f.readline()
917 if not line: break
918 if prog.match(line):
919 text = line[len(key)+1:]
920 while 1:
921 line = f.readline()
922 if not line or line[0] not in string.whitespace:
923 break
924 text = text + line
925 return string.strip(text)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000926 return None
Guido van Rossum56013131994-06-23 12:06:02 +0000927
928def updateline(file, key, value, casefold = 1):
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000929 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000930 f = open(file, 'r')
931 lines = f.readlines()
932 f.close()
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000933 except IOError:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000934 lines = []
Guido van Rossum9694fca1997-10-22 21:00:49 +0000935 pat = re.escape(key) + ':(.*)\n'
936 prog = re.compile(pat, casefold and re.IGNORECASE)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000937 if value is None:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000938 newline = None
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000939 else:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000940 newline = '%s: %s\n' % (key, value)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000941 for i in range(len(lines)):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000942 line = lines[i]
943 if prog.match(line):
944 if newline is None:
945 del lines[i]
946 else:
947 lines[i] = newline
948 break
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000949 else:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000950 if newline is not None:
951 lines.append(newline)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000952 tempfile = file + "~"
953 f = open(tempfile, 'w')
954 for line in lines:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000955 f.write(line)
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000956 f.close()
957 os.rename(tempfile, file)
Guido van Rossum56013131994-06-23 12:06:02 +0000958
959
960# Test program
961
962def test():
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000963 global mh, f
964 os.system('rm -rf $HOME/Mail/@test')
965 mh = MH()
966 def do(s): print s; print eval(s)
967 do('mh.listfolders()')
968 do('mh.listallfolders()')
969 testfolders = ['@test', '@test/test1', '@test/test2',
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000970 '@test/test1/test11', '@test/test1/test12',
971 '@test/test1/test11/test111']
Guido van Rossum0c5e0491997-04-16 02:47:12 +0000972 for t in testfolders: do('mh.makefolder(%s)' % `t`)
973 do('mh.listsubfolders(\'@test\')')
974 do('mh.listallsubfolders(\'@test\')')
975 f = mh.openfolder('@test')
976 do('f.listsubfolders()')
977 do('f.listallsubfolders()')
978 do('f.getsequences()')
979 seqs = f.getsequences()
980 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
981 print seqs
982 f.putsequences(seqs)
983 do('f.getsequences()')
984 testfolders.reverse()
985 for t in testfolders: do('mh.deletefolder(%s)' % `t`)
986 do('mh.getcontext()')
987 context = mh.getcontext()
988 f = mh.openfolder(context)
989 do('f.getcurrent()')
990 for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000991 'first:3', 'last:3', 'cur:3', 'cur:-3',
992 'prev:3', 'next:3',
993 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
994 'all']:
995 try:
996 do('f.parsesequence(%s)' % `seq`)
997 except Error, msg:
998 print "Error:", msg
999 stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
1000 list = map(string.atoi, string.split(stuff))
1001 print list, "<-- pick"
Guido van Rossum0c5e0491997-04-16 02:47:12 +00001002 do('f.listmessages()')
Guido van Rossum56013131994-06-23 12:06:02 +00001003
1004
1005if __name__ == '__main__':
Guido van Rossum0c5e0491997-04-16 02:47:12 +00001006 test()