| 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 |