OLD | NEW |
| (Empty) |
1 | |
2 # This is a class which watches a maildir for new messages. It uses the | |
3 # linux dirwatcher API (if available) to look for new files. The | |
4 # .messageReceived method is invoked with the filename of the new message, | |
5 # relative to the top of the maildir (so it will look like "new/blahblah"). | |
6 | |
7 import os | |
8 from twisted.python import log | |
9 from twisted.application import service, internet | |
10 from twisted.internet import reactor | |
11 dnotify = None | |
12 try: | |
13 import dnotify | |
14 except: | |
15 # I'm not actually sure this log message gets recorded | |
16 log.msg("unable to import dnotify, so Maildir will use polling instead") | |
17 | |
18 class NoSuchMaildir(Exception): | |
19 pass | |
20 | |
21 class MaildirService(service.MultiService): | |
22 """I watch a maildir for new messages. I should be placed as the service | |
23 child of some MultiService instance. When running, I use the linux | |
24 dirwatcher API (if available) or poll for new files in the 'new' | |
25 subdirectory of my maildir path. When I discover a new message, I invoke | |
26 my .messageReceived() method with the short filename of the new message, | |
27 so the full name of the new file can be obtained with | |
28 os.path.join(maildir, 'new', filename). messageReceived() should be | |
29 overridden by a subclass to do something useful. I will not move or | |
30 delete the file on my own: the subclass's messageReceived() should | |
31 probably do that. | |
32 """ | |
33 pollinterval = 10 # only used if we don't have DNotify | |
34 | |
35 def __init__(self, basedir=None): | |
36 """Create the Maildir watcher. BASEDIR is the maildir directory (the | |
37 one which contains new/ and tmp/) | |
38 """ | |
39 service.MultiService.__init__(self) | |
40 self.basedir = basedir | |
41 self.files = [] | |
42 self.dnotify = None | |
43 | |
44 def setBasedir(self, basedir): | |
45 # some users of MaildirService (scheduler.Try_Jobdir, in particular) | |
46 # don't know their basedir until setServiceParent, since it is | |
47 # relative to the buildmaster's basedir. So let them set it late. We | |
48 # don't actually need it until our own startService. | |
49 self.basedir = basedir | |
50 | |
51 def startService(self): | |
52 service.MultiService.startService(self) | |
53 self.newdir = os.path.join(self.basedir, "new") | |
54 if not os.path.isdir(self.basedir) or not os.path.isdir(self.newdir): | |
55 raise NoSuchMaildir("invalid maildir '%s'" % self.basedir) | |
56 try: | |
57 if dnotify: | |
58 # we must hold an fd open on the directory, so we can get | |
59 # notified when it changes. | |
60 self.dnotify = dnotify.DNotify(self.newdir, | |
61 self.dnotify_callback, | |
62 [dnotify.DNotify.DN_CREATE]) | |
63 except (IOError, OverflowError): | |
64 # IOError is probably linux<2.4.19, which doesn't support | |
65 # dnotify. OverflowError will occur on some 64-bit machines | |
66 # because of a python bug | |
67 log.msg("DNotify failed, falling back to polling") | |
68 if not self.dnotify: | |
69 t = internet.TimerService(self.pollinterval, self.poll) | |
70 t.setServiceParent(self) | |
71 self.poll() | |
72 | |
73 def dnotify_callback(self): | |
74 log.msg("dnotify noticed something, now polling") | |
75 | |
76 # give it a moment. I found that qmail had problems when the message | |
77 # was removed from the maildir instantly. It shouldn't, that's what | |
78 # maildirs are made for. I wasn't able to eyeball any reason for the | |
79 # problem, and safecat didn't behave the same way, but qmail reports | |
80 # "Temporary_error_on_maildir_delivery" (qmail-local.c:165, | |
81 # maildir_child() process exited with rc not in 0,2,3,4). Not sure | |
82 # why, and I'd have to hack qmail to investigate further, so it's | |
83 # easier to just wait a second before yanking the message out of new/ | |
84 | |
85 reactor.callLater(0.1, self.poll) | |
86 | |
87 | |
88 def stopService(self): | |
89 if self.dnotify: | |
90 self.dnotify.remove() | |
91 self.dnotify = None | |
92 return service.MultiService.stopService(self) | |
93 | |
94 def poll(self): | |
95 assert self.basedir | |
96 # see what's new | |
97 for f in self.files: | |
98 if not os.path.isfile(os.path.join(self.newdir, f)): | |
99 self.files.remove(f) | |
100 newfiles = [] | |
101 for f in os.listdir(self.newdir): | |
102 if not f in self.files: | |
103 newfiles.append(f) | |
104 self.files.extend(newfiles) | |
105 # TODO: sort by ctime, then filename, since safecat uses a rather | |
106 # fine-grained timestamp in the filename | |
107 for n in newfiles: | |
108 # TODO: consider catching exceptions in messageReceived | |
109 self.messageReceived(n) | |
110 | |
111 def messageReceived(self, filename): | |
112 """Called when a new file is noticed. Will call | |
113 self.parent.messageReceived() with a path relative to maildir/new. | |
114 Should probably be overridden in subclasses.""" | |
115 self.parent.messageReceived(filename) | |
116 | |
OLD | NEW |