mailbox.py 9.15 KB
Newer Older
1
#! /usr/bin/env python
2 3 4 5

"""Classes to handle Unix style, MMDF style, and MH style mailboxes."""


6
import rfc822
Jack Jansen's avatar
Jack Jansen committed
7
import os
8

9 10
__all__ = ["UnixMailbox","MmdfMailbox","MHMailbox","Maildir","BabylMailbox",
           "PortableUnixMailbox"]
11

12
class _Mailbox:
13

Barry Warsaw's avatar
Barry Warsaw committed
14
    def __init__(self, fp, factory=rfc822.Message):
Fred Drake's avatar
Fred Drake committed
15 16
        self.fp = fp
        self.seekp = 0
Barry Warsaw's avatar
Barry Warsaw committed
17
        self.factory = factory
Fred Drake's avatar
Fred Drake committed
18

19
    def __iter__(self):
20
        return iter(self.next, None)
21

Fred Drake's avatar
Fred Drake committed
22 23 24 25 26 27 28 29 30 31 32
    def next(self):
        while 1:
            self.fp.seek(self.seekp)
            try:
                self._search_start()
            except EOFError:
                self.seekp = self.fp.tell()
                return None
            start = self.fp.tell()
            self._search_end()
            self.seekp = stop = self.fp.tell()
33
            if start != stop:
Fred Drake's avatar
Fred Drake committed
34
                break
Barry Warsaw's avatar
Barry Warsaw committed
35
        return self.factory(_Subfile(self.fp, start, stop))
36

37 38

class _Subfile:
39

Fred Drake's avatar
Fred Drake committed
40 41 42 43 44 45
    def __init__(self, fp, start, stop):
        self.fp = fp
        self.start = start
        self.stop = stop
        self.pos = self.start

46 47

    def _read(self, length, read_function):
Fred Drake's avatar
Fred Drake committed
48 49 50
        if self.pos >= self.stop:
            return ''
        remaining = self.stop - self.pos
51
        if length is None or length < 0 or length > remaining:
Fred Drake's avatar
Fred Drake committed
52 53
            length = remaining
        self.fp.seek(self.pos)
54
        data = read_function(length)
Fred Drake's avatar
Fred Drake committed
55 56 57
        self.pos = self.fp.tell()
        return data

58
    def read(self, length = None):
59
        return self._read(length, self.fp.read)
60

Fred Drake's avatar
Fred Drake committed
61
    def readline(self, length = None):
62
        return self._read(length, self.fp.readline)
Fred Drake's avatar
Fred Drake committed
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89

    def readlines(self, sizehint = -1):
        lines = []
        while 1:
            line = self.readline()
            if not line:
                break
            lines.append(line)
            if sizehint >= 0:
                sizehint = sizehint - len(line)
                if sizehint <= 0:
                    break
        return lines

    def tell(self):
        return self.pos - self.start

    def seek(self, pos, whence=0):
        if whence == 0:
            self.pos = self.start + pos
        elif whence == 1:
            self.pos = self.pos + pos
        elif whence == 2:
            self.pos = self.stop + pos

    def close(self):
        del self.fp
90

91

92
# Recommended to use PortableUnixMailbox instead!
93
class UnixMailbox(_Mailbox):
94

Fred Drake's avatar
Fred Drake committed
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    def _search_start(self):
        while 1:
            pos = self.fp.tell()
            line = self.fp.readline()
            if not line:
                raise EOFError
            if line[:5] == 'From ' and self._isrealfromline(line):
                self.fp.seek(pos)
                return

    def _search_end(self):
        self.fp.readline()      # Throw away header line
        while 1:
            pos = self.fp.tell()
            line = self.fp.readline()
            if not line:
                return
            if line[:5] == 'From ' and self._isrealfromline(line):
                self.fp.seek(pos)
                return

Barry Warsaw's avatar
Barry Warsaw committed
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    # An overridable mechanism to test for From-line-ness.  You can either
    # specify a different regular expression or define a whole new
    # _isrealfromline() method.  Note that this only gets called for lines
    # starting with the 5 characters "From ".
    #
    # BAW: According to
    #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
    # the only portable, reliable way to find message delimiters in a BSD (i.e
    # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
    # beginning of the file, "^From .*\n".  While _fromlinepattern below seems
    # like a good idea, in practice, there are too many variations for more
    # strict parsing of the line to be completely accurate.
    #
    # _strict_isrealfromline() is the old version which tries to do stricter
    # parsing of the From_ line.  _portable_isrealfromline() simply returns
    # true, since it's never called if the line doesn't already start with
    # "From ".
    #
    # This algorithm, and the way it interacts with _search_start() and
    # _search_end() may not be completely correct, because it doesn't check
    # that the two characters preceding "From " are \n\n or the beginning of
    # the file.  Fixing this would require a more extensive rewrite than is
138 139
    # necessary.  For convenience, we've added a PortableUnixMailbox class
    # which uses the more lenient _fromlinepattern regular expression.
Fred Drake's avatar
Fred Drake committed
140 141 142 143 144

    _fromlinepattern = r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+" \
                       r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*$"
    _regexp = None

Barry Warsaw's avatar
Barry Warsaw committed
145
    def _strict_isrealfromline(self, line):
Fred Drake's avatar
Fred Drake committed
146 147 148 149
        if not self._regexp:
            import re
            self._regexp = re.compile(self._fromlinepattern)
        return self._regexp.match(line)
150

Barry Warsaw's avatar
Barry Warsaw committed
151
    def _portable_isrealfromline(self, line):
152
        return True
Barry Warsaw's avatar
Barry Warsaw committed
153 154 155 156 157 158 159

    _isrealfromline = _strict_isrealfromline


class PortableUnixMailbox(UnixMailbox):
    _isrealfromline = UnixMailbox._portable_isrealfromline

160

161
class MmdfMailbox(_Mailbox):
162

Fred Drake's avatar
Fred Drake committed
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    def _search_start(self):
        while 1:
            line = self.fp.readline()
            if not line:
                raise EOFError
            if line[:5] == '\001\001\001\001\n':
                return

    def _search_end(self):
        while 1:
            pos = self.fp.tell()
            line = self.fp.readline()
            if not line:
                return
            if line == '\001\001\001\001\n':
                self.fp.seek(pos)
                return
180

181

Jack Jansen's avatar
Jack Jansen committed
182
class MHMailbox:
183

Barry Warsaw's avatar
Barry Warsaw committed
184
    def __init__(self, dirname, factory=rfc822.Message):
Fred Drake's avatar
Fred Drake committed
185
        import re
186
        pat = re.compile('^[1-9][0-9]*$')
Fred Drake's avatar
Fred Drake committed
187
        self.dirname = dirname
188 189 190 191
        # the three following lines could be combined into:
        # list = map(long, filter(pat.match, os.listdir(self.dirname)))
        list = os.listdir(self.dirname)
        list = filter(pat.match, list)
192 193 194 195 196
        list = map(long, list)
        list.sort()
        # This only works in Python 1.6 or later;
        # before that str() added 'L':
        self.boxes = map(str, list)
197
        self.boxes.reverse()
Barry Warsaw's avatar
Barry Warsaw committed
198
        self.factory = factory
Fred Drake's avatar
Fred Drake committed
199

200
    def __iter__(self):
201
        return iter(self.next, None)
202

Fred Drake's avatar
Fred Drake committed
203 204 205
    def next(self):
        if not self.boxes:
            return None
206
        fn = self.boxes.pop()
Fred Drake's avatar
Fred Drake committed
207
        fp = open(os.path.join(self.dirname, fn))
208 209 210 211 212 213
        msg = self.factory(fp)
        try:
            msg._mh_msgno = fn
        except (AttributeError, TypeError):
            pass
        return msg
214

Guido van Rossum's avatar
Guido van Rossum committed
215 216

class Maildir:
Fred Drake's avatar
Fred Drake committed
217 218
    # Qmail directory mailbox

Barry Warsaw's avatar
Barry Warsaw committed
219
    def __init__(self, dirname, factory=rfc822.Message):
Fred Drake's avatar
Fred Drake committed
220
        self.dirname = dirname
Barry Warsaw's avatar
Barry Warsaw committed
221
        self.factory = factory
Fred Drake's avatar
Fred Drake committed
222 223 224

        # check for new mail
        newdir = os.path.join(self.dirname, 'new')
225 226
        boxes = [os.path.join(newdir, f)
                 for f in os.listdir(newdir) if f[0] != '.']
Fred Drake's avatar
Fred Drake committed
227 228 229

        # Now check for current mail in this maildir
        curdir = os.path.join(self.dirname, 'cur')
230 231
        boxes += [os.path.join(curdir, f)
                  for f in os.listdir(curdir) if f[0] != '.']
232
        boxes.reverse()
233 234
        self.boxes = boxes

235
    def __iter__(self):
236
        return iter(self.next, None)
237

Fred Drake's avatar
Fred Drake committed
238 239 240
    def next(self):
        if not self.boxes:
            return None
241
        fn = self.boxes.pop()
242
        fp = open(fn)
Barry Warsaw's avatar
Barry Warsaw committed
243
        return self.factory(fp)
Guido van Rossum's avatar
Guido van Rossum committed
244 245


246
class BabylMailbox(_Mailbox):
247

Fred Drake's avatar
Fred Drake committed
248 249 250 251 252 253 254
    def _search_start(self):
        while 1:
            line = self.fp.readline()
            if not line:
                raise EOFError
            if line == '*** EOOH ***\n':
                return
255

Fred Drake's avatar
Fred Drake committed
256 257 258 259 260 261
    def _search_end(self):
        while 1:
            pos = self.fp.tell()
            line = self.fp.readline()
            if not line:
                return
262
            if line == '\037\014\n' or line == '\037':
Fred Drake's avatar
Fred Drake committed
263 264
                self.fp.seek(pos)
                return
265 266


267
def _test():
Fred Drake's avatar
Fred Drake committed
268 269 270 271 272
    import sys

    args = sys.argv[1:]
    if not args:
        for key in 'MAILDIR', 'MAIL', 'LOGNAME', 'USER':
273
            if key in os.environ:
Fred Drake's avatar
Fred Drake committed
274 275
                mbox = os.environ[key]
                break
276
        else:
Fred Drake's avatar
Fred Drake committed
277 278 279 280 281 282 283
            print "$MAIL, $LOGNAME nor $USER set -- who are you?"
            return
    else:
        mbox = args[0]
    if mbox[:1] == '+':
        mbox = os.environ['HOME'] + '/Mail/' + mbox[1:]
    elif not '/' in mbox:
284 285 286 287
        if os.path.isfile('/var/mail/' + mbox):
            mbox = '/var/mail/' + mbox
        else:
            mbox = '/usr/mail/' + mbox
Fred Drake's avatar
Fred Drake committed
288 289 290
    if os.path.isdir(mbox):
        if os.path.isdir(os.path.join(mbox, 'cur')):
            mb = Maildir(mbox)
291
        else:
Fred Drake's avatar
Fred Drake committed
292 293 294
            mb = MHMailbox(mbox)
    else:
        fp = open(mbox, 'r')
295
        mb = PortableUnixMailbox(fp)
Fred Drake's avatar
Fred Drake committed
296 297 298 299 300 301 302 303 304 305

    msgs = []
    while 1:
        msg = mb.next()
        if msg is None:
            break
        msgs.append(msg)
        if len(args) <= 1:
            msg.fp = None
    if len(args) > 1:
306
        num = int(args[1])
Fred Drake's avatar
Fred Drake committed
307 308 309 310 311 312 313 314 315 316
        print 'Message %d body:'%num
        msg = msgs[num-1]
        msg.rewindbody()
        sys.stdout.write(msg.fp.read())
    else:
        print 'Mailbox',mbox,'has',len(msgs),'messages:'
        for msg in msgs:
            f = msg.getheader('from') or ""
            s = msg.getheader('subject') or ""
            d = msg.getheader('date') or ""
317
            print '-%20.20s   %20.20s   %-30.30s'%(f, d[5:], s)
318 319 320


if __name__ == '__main__':
Fred Drake's avatar
Fred Drake committed
321
    _test()