~ubuntu-branches/ubuntu/karmic/spambayes/karmic

« back to all changes in this revision

Viewing changes to scripts/sb_mboxtrain.py

  • Committer: Bazaar Package Importer
  • Author(s): Jorge Bernal
  • Date: 2005-04-07 14:02:02 UTC
  • Revision ID: james.westby@ubuntu.com-20050407140202-mgyh6t7gn2dlrrw5
Tags: upstream-1.0.1
ImportĀ upstreamĀ versionĀ 1.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
 
 
3
### Train spambayes on all previously-untrained messages in a mailbox.
 
4
###
 
5
### This keeps track of messages it's already trained by adding an
 
6
### X-Spambayes-Trained: header to each one.  Then, if you move one to
 
7
### another folder, it will retrain that message.  You would want to run
 
8
### this from a cron job on your server.
 
9
 
 
10
"""Usage: %(program)s [OPTIONS] ...
 
11
 
 
12
Where OPTIONS is one or more of:
 
13
    -h
 
14
        show usage and exit
 
15
    -d DBNAME
 
16
        use the DBM store.  A DBM file is larger than the pickle and
 
17
        creating it is slower, but loading it is much faster,
 
18
        especially for large word databases.  Recommended for use with
 
19
        sb_filter or any procmail-based filter.
 
20
    -p DBNAME
 
21
        use the pickle store.  A pickle is smaller and faster to create,
 
22
        but much slower to load.  Recommended for use with sb_server and
 
23
        sb_xmlrpcserver.
 
24
    -g PATH
 
25
        mbox or directory of known good messages (non-spam) to train on.
 
26
        Can be specified more than once.
 
27
    -s PATH
 
28
        mbox or directory of known spam messages to train on.
 
29
        Can be specified more than once.
 
30
    -f
 
31
        force training, ignoring the trained header.  Use this if you
 
32
        need to rebuild your database from scratch.
 
33
    -q
 
34
        quiet mode; no output
 
35
 
 
36
    -n  train mail residing in "new" directory, in addition to "cur"
 
37
        directory, which is always trained (Maildir only)
 
38
 
 
39
    -r  remove mail which was trained on (Maildir only)
 
40
 
 
41
    -o section:option:value
 
42
        set [section, option] in the options database to value
 
43
"""
 
44
 
 
45
try:
 
46
    True, False
 
47
except NameError:
 
48
    # Maintain compatibility with Python 2.2
 
49
    True, False = 1, 0
 
50
 
 
51
import sys, os, getopt, email
 
52
import shutil
 
53
from spambayes import hammie, storage, mboxutils
 
54
from spambayes.Options import options, get_pathname_option
 
55
 
 
56
program = sys.argv[0]
 
57
loud = True
 
58
 
 
59
def get_message(obj):
 
60
    """Return an email Message object.
 
61
 
 
62
    This works like mboxutils.get_message, except it doesn't junk the
 
63
    headers if there's an error.  Doing so would cause a headerless
 
64
    message to be written back out!
 
65
 
 
66
    """
 
67
 
 
68
    if isinstance(obj, email.Message.Message):
 
69
        return obj
 
70
    # Create an email Message object.
 
71
    if hasattr(obj, "read"):
 
72
        obj = obj.read()
 
73
    try:
 
74
        msg = email.message_from_string(obj)
 
75
    except email.Errors.MessageParseError:
 
76
        msg = None
 
77
    return msg
 
78
 
 
79
def msg_train(h, msg, is_spam, force):
 
80
    """Train bayes with a single message."""
 
81
 
 
82
    # XXX: big hack -- why is email.Message unable to represent
 
83
    # multipart/alternative?
 
84
    try:
 
85
        mboxutils.as_string(msg)
 
86
    except TypeError:
 
87
        # We'll be unable to represent this as text :(
 
88
        return False
 
89
 
 
90
    if is_spam:
 
91
        spamtxt = options["Headers", "header_spam_string"]
 
92
    else:
 
93
        spamtxt = options["Headers", "header_ham_string"]
 
94
    oldtxt = msg.get(options["Headers", "trained_header_name"])
 
95
    if force:
 
96
        # Train no matter what.
 
97
        if oldtxt != None:
 
98
            del msg[options["Headers", "trained_header_name"]]
 
99
    elif oldtxt == spamtxt:
 
100
        # Skip this one, we've already trained with it.
 
101
        return False
 
102
    elif oldtxt != None:
 
103
        # It's been trained, but as something else.  Untrain.
 
104
        del msg[options["Headers", "trained_header_name"]]
 
105
        h.untrain(msg, not is_spam)
 
106
    h.train(msg, is_spam)
 
107
    msg.add_header(options["Headers", "trained_header_name"], spamtxt)
 
108
 
 
109
    return True
 
110
 
 
111
def maildir_train(h, path, is_spam, force, removetrained):
 
112
    """Train bayes with all messages from a maildir."""
 
113
 
 
114
    if loud: print "  Reading %s as Maildir" % (path,)
 
115
 
 
116
    import time
 
117
    import socket
 
118
 
 
119
    pid = os.getpid()
 
120
    host = socket.gethostname()
 
121
    counter = 0
 
122
    trained = 0
 
123
 
 
124
    for fn in os.listdir(path):
 
125
        cfn = os.path.join(path, fn)
 
126
        tfn = os.path.normpath(os.path.join(path, "..", "tmp",
 
127
                           "%d.%d_%d.%s" % (time.time(), pid,
 
128
                                            counter, host)))
 
129
        if (os.path.isdir(cfn)):
 
130
            continue
 
131
        counter += 1
 
132
        if loud and counter % 10 == 0:
 
133
            sys.stdout.write("\r%6d" % counter)
 
134
            sys.stdout.flush()
 
135
        f = file(cfn, "rb")
 
136
        msg = get_message(f)
 
137
        f.close()
 
138
        if not msg:
 
139
            print "Malformed message: %s.  Skipping..." % cfn
 
140
            continue
 
141
        if not msg_train(h, msg, is_spam, force):
 
142
            continue
 
143
        trained += 1
 
144
        if not options["Headers", "include_trained"]:
 
145
            continue
 
146
        f = file(tfn, "wb")
 
147
        f.write(mboxutils.as_string(msg))
 
148
        f.close()
 
149
        shutil.copystat(cfn, tfn)
 
150
 
 
151
        # XXX: This will raise an exception on Windows.  Do any Windows
 
152
        # people actually use Maildirs?
 
153
        os.rename(tfn, cfn)
 
154
        if (removetrained):
 
155
            os.unlink(cfn)
 
156
 
 
157
    if loud:
 
158
        sys.stdout.write("\r%6d" % counter)
 
159
        sys.stdout.write("\r  Trained %d out of %d messages\n" %
 
160
                         (trained, counter))
 
161
 
 
162
def mbox_train(h, path, is_spam, force):
 
163
    """Train bayes with a Unix mbox"""
 
164
 
 
165
    if loud: print "  Reading as Unix mbox"
 
166
 
 
167
    import mailbox
 
168
    import fcntl
 
169
 
 
170
    # Open and lock the mailbox.  Some systems require it be opened for
 
171
    # writes in order to assert an exclusive lock.
 
172
    f = file(path, "r+b")
 
173
    fcntl.flock(f, fcntl.LOCK_EX)
 
174
    mbox = mailbox.PortableUnixMailbox(f, get_message)
 
175
 
 
176
    outf = os.tmpfile()
 
177
    counter = 0
 
178
    trained = 0
 
179
 
 
180
    for msg in mbox:
 
181
        if not msg:
 
182
            print "Malformed message number %d.  I can't train on this mbox, sorry." % counter
 
183
            return
 
184
        counter += 1
 
185
        if loud and counter % 10 == 0:
 
186
            sys.stdout.write("\r%6d" % counter)
 
187
            sys.stdout.flush()
 
188
        if msg_train(h, msg, is_spam, force):
 
189
            trained += 1
 
190
        if options["Headers", "include_trained"]:
 
191
            # Write it out with the Unix "From " line
 
192
            outf.write(mboxutils.as_string(msg, True))
 
193
 
 
194
    if options["Headers", "include_trained"]:
 
195
        outf.seek(0)
 
196
        try:
 
197
            os.ftruncate(f.fileno(), 0)
 
198
            f.seek(0)
 
199
        except:
 
200
            # If anything goes wrong, don't try to write
 
201
            print "Problem truncating mbox--nothing written"
 
202
            raise
 
203
        try:
 
204
            for line in outf.xreadlines():
 
205
                f.write(line)
 
206
        except:
 
207
            print >> sys.stderr ("Problem writing mbox!  Sorry, "
 
208
                                 "I tried my best, but your mail "
 
209
                                 "may be corrupted.")
 
210
            raise
 
211
 
 
212
    fcntl.flock(f, fcntl.LOCK_UN)
 
213
    f.close()
 
214
    if loud:
 
215
        sys.stdout.write("\r%6d" % counter)
 
216
        sys.stdout.write("\r  Trained %d out of %d messages\n" %
 
217
                         (trained, counter))
 
218
 
 
219
def mhdir_train(h, path, is_spam, force):
 
220
    """Train bayes with an mh directory"""
 
221
 
 
222
    if loud: print "  Reading as MH mailbox"
 
223
 
 
224
    import glob
 
225
 
 
226
    counter = 0
 
227
    trained = 0
 
228
 
 
229
    for fn in glob.glob(os.path.join(path, "[0-9]*")):
 
230
        counter += 1
 
231
 
 
232
        cfn = fn
 
233
        tfn = os.path.join(path, "spambayes.tmp")
 
234
        if loud and counter % 10 == 0:
 
235
            sys.stdout.write("\r%6d" % counter)
 
236
            sys.stdout.flush()
 
237
        f = file(fn, "rb")
 
238
        msg = get_message(f)
 
239
        f.close()
 
240
        if not msg:
 
241
            print "Malformed message: %s.  Skipping..." % cfn
 
242
            continue
 
243
        msg_train(h, msg, is_spam, force)
 
244
        trained += 1
 
245
        if not options["Headers", "include_trained"]:
 
246
            continue
 
247
        f = file(tfn, "wb")
 
248
        f.write(mboxutils.as_string(msg))
 
249
        f.close()
 
250
        shutil.copystat(cfn, tfn)
 
251
 
 
252
        # XXX: This will raise an exception on Windows.  Do any Windows
 
253
        # people actually use MH directories?
 
254
        os.rename(tfn, cfn)
 
255
 
 
256
    if loud:
 
257
        sys.stdout.write("\r%6d" % counter)
 
258
        sys.stdout.write("\r  Trained %d out of %d messages\n" %
 
259
                         (trained, counter))
 
260
 
 
261
def train(h, path, is_spam, force, trainnew, removetrained):
 
262
    if not os.path.exists(path):
 
263
        raise ValueError("Nonexistent path: %s" % path)
 
264
    elif os.path.isfile(path):
 
265
        mbox_train(h, path, is_spam, force)
 
266
    elif os.path.isdir(os.path.join(path, "cur")):
 
267
        maildir_train(h, os.path.join(path, "cur"), is_spam, force,
 
268
                      removetrained)
 
269
        if trainnew:
 
270
            maildir_train(h, os.path.join(path, "new"), is_spam, force,
 
271
                          removetrained)
 
272
    elif os.path.isdir(path):
 
273
        mhdir_train(h, path, is_spam, force)
 
274
    else:
 
275
        raise ValueError("Unable to determine mailbox type: " + path)
 
276
 
 
277
 
 
278
def usage(code, msg=''):
 
279
    """Print usage message and sys.exit(code)."""
 
280
    if msg:
 
281
        print >> sys.stderr, msg
 
282
        print >> sys.stderr
 
283
    print >> sys.stderr, __doc__ % globals()
 
284
    sys.exit(code)
 
285
 
 
286
def main():
 
287
    """Main program; parse options and go."""
 
288
 
 
289
    global loud
 
290
 
 
291
    try:
 
292
        opts, args = getopt.getopt(sys.argv[1:], 'hfqnrd:p:g:s:o:')
 
293
    except getopt.error, msg:
 
294
        usage(2, msg)
 
295
 
 
296
    if not opts:
 
297
        usage(2, "No options given")
 
298
 
 
299
    force = False
 
300
    trainnew = False
 
301
    removetrained = False
 
302
    good = []
 
303
    spam = []
 
304
    for opt, arg in opts:
 
305
        if opt == '-h':
 
306
            usage(0)
 
307
        elif opt == "-f":
 
308
            force = True
 
309
        elif opt == "-n":
 
310
            trainnew = True
 
311
        elif opt == "-q":
 
312
            loud = False
 
313
        elif opt == '-g':
 
314
            good.append(arg)
 
315
        elif opt == '-s':
 
316
            spam.append(arg)
 
317
        elif opt == "-r":
 
318
            removetrained = True
 
319
        elif opt == '-o':
 
320
            options.set_from_cmdline(arg, sys.stderr)
 
321
    pck, usedb = storage.database_type(opts)
 
322
    if args:
 
323
        usage(2, "Positional arguments not allowed")
 
324
 
 
325
    if usedb == None:
 
326
        # Use settings in configuration file.
 
327
        usedb = options["Storage", "persistent_use_database"]
 
328
        pck = get_pathname_option("Storage",
 
329
                                          "persistent_storage_file")
 
330
 
 
331
    h = hammie.open(pck, usedb, "c")
 
332
 
 
333
    for g in good:
 
334
        if loud: print "Training ham (%s):" % g
 
335
        train(h, g, False, force, trainnew, removetrained)
 
336
        sys.stdout.flush()
 
337
        save = True
 
338
 
 
339
    for s in spam:
 
340
        if loud: print "Training spam (%s):" % s
 
341
        train(h, s, True, force, trainnew, removetrained)
 
342
        sys.stdout.flush()
 
343
        save = True
 
344
 
 
345
    if save:
 
346
        h.store()
 
347
 
 
348
 
 
349
if __name__ == "__main__":
 
350
    main()