comparison scripts/imapServer.py @ 3176:18ad9d702a5b

added "imapServer.py" script (patch [SF#934567])
author Richard Jones <richard@users.sourceforge.net>
date Mon, 14 Feb 2005 06:09:14 +0000
parents
children eddcfee2cc19
comparison
equal deleted inserted replaced
3175:e2a590ecc5e4 3176:18ad9d702a5b
1 #!/usr/bin/env python2.3
2 # arch-tag: f2d1fd6e-df72-4188-a3b4-a9dbbb0807b9
3 # vim: filetype=python ts=4 sw=4 noexpandtab si
4 """\
5 This script is a wrapper around the mailgw.py script that exists in roundup.
6 It runs as service instead of running as a one-time shot.
7 It also connects to a secure IMAP server. The main reasons for this script are:
8
9 1) The roundup-mailgw script isn't designed to run as a server. It expects that you
10 either run it by hand, and enter the password each time, or you supply the
11 password on the command line. I prefer to run a server that I initialize with
12 the password, and then it just runs. I don't want to have to pass it on the
13 command line, so running through crontab isn't a possibility. (This wouldn't
14 be a problem on a local machine running through a mailspool.)
15 2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So hopefully
16 running that work outside of the mailgw will allow it to work.
17 3) I wanted to be able to check multiple projects at the same time. roundup-mailgw is
18 only for 1 mailbox and 1 project.
19
20
21 *TODO*:
22 For the first round, the program spawns a new roundup-mailgw for each imap message
23 that it finds and pipes the result in. In the future it might be more practical to
24 actually include the roundup files and run the appropriate commands using python.
25
26 *TODO*:
27 Look into supporting a logfile instead of using 2>/logfile
28
29 *TODO*:
30 Add an option for changing the uid/gid of the running process.
31 """
32
33 import logging
34 logging.basicConfig()
35 log = logging.getLogger('IMAPServer')
36
37 version = '0.1.2'
38
39 class RoundupMailbox:
40 """This contains all the info about each mailbox.
41 Username, Password, server, security, roundup database
42 """
43 def __init__(self, dbhome='', username=None, password=None, mailbox=None
44 , server=None, protocol='imaps'):
45 self.username = username
46 self.password = password
47 self.mailbox = mailbox
48 self.server = server
49 self.protocol = protocol
50 self.dbhome = dbhome
51
52 try:
53 if not self.dbhome:
54 import os
55 self.dbhome = raw_input('Tracker home: ')
56 if not os.path.exists(self.dbhome):
57 raise ValueError, 'Invalid home address directory does not exist. %s' % self.dbhome
58
59 if not self.server:
60 self.server = raw_input('Server: ')
61 if not self.server:
62 raise ValueError, 'No Servername supplied'
63 protocol = raw_input('protocol [imaps]? ')
64 self.protocol = protocol
65
66 if not self.username:
67 self.username = raw_input('Username: ')
68 if not self.username:
69 raise ValueError, 'Invalid Username'
70
71 if not self.password:
72 import getpass
73 print 'For server %s, user %s' % (self.server, self.username)
74 self.password = getpass.getpass()
75 # password can be empty because it could be superceeded by a later
76 # entry
77
78 #if self.mailbox is None:
79 # self.mailbox = raw_input('Mailbox [INBOX]: ')
80 # # We allow an empty mailbox because that will
81 # # select the INBOX, whatever it is called
82
83 except (KeyboardInterrupt, EOFError):
84 raise ValueError, 'Canceled by User'
85
86 def __str__(self):
87 return """Mailbox{ server:%(server)s, protocol:%(protocol)s, username:%(username)s, mailbox:%(mailbox)s, dbhome:%(dbhome)s }""" % self.__dict__
88
89
90
91 class IMAPServer:
92 """This class runs as a server process. It is configured with a list of
93 mailboxes to connect to, along with the roundup database directories that correspond
94 with each email address.
95 It then connects to each mailbox at a specified interval, and if there are new messages
96 it reads them, and sends the result to the roundup.mailgw.
97
98 *TODO*:
99 Try to be smart about how you access the mailboxes so that you can connect once, and
100 access multiple mailboxes and possibly multiple usernames.
101
102 *NOTE*:
103 This assumes that if you are using the same user on the same server, you are using
104 the same password. (the last one supplied is used.) Empty passwords are ignored.
105 Only the last protocol supplied is used.
106 """
107
108 def __init__(self, pidfile=None, delay=5, daemon=False):
109 #This is sorted by servername, then username, then mailboxes
110 self.mailboxes = {}
111 self.delay = float(delay)
112 self.pidfile = pidfile
113 self.daemon = daemon
114
115 def setDelay(self, delay):
116 self.delay = delay
117
118 def addMailbox(self, mailbox):
119 """ The linkage is as follows:
120 servers -- users - mailbox:dbhome
121 So there can be multiple servers, each with multiple users.
122 Each username can be associated with multiple mailboxes.
123 each mailbox is associated with 1 database home
124 """
125 log.info('Adding mailbox %s', mailbox)
126 if not self.mailboxes.has_key(mailbox.server):
127 self.mailboxes[mailbox.server] = {'protocol':'imaps', 'users':{}}
128 server = self.mailboxes[mailbox.server]
129 if mailbox.protocol:
130 server['protocol'] = mailbox.protocol
131
132 if not server['users'].has_key(mailbox.username):
133 server['users'][mailbox.username] = {'password':'', 'mailboxes':{}}
134 user = server['users'][mailbox.username]
135 if mailbox.password:
136 user['password'] = mailbox.password
137
138 if user['mailboxes'].has_key(mailbox.mailbox):
139 raise ValueError, 'Mailbox is already defined'
140
141 user['mailboxes'][mailbox.mailbox] = mailbox.dbhome
142
143 def _process(self, message, dbhome):
144 """Actually process one of the email messages"""
145 import os, sys
146 child = os.popen('roundup-mailgw %s' % dbhome, 'wb')
147 child.write(message)
148 child.close()
149 #print message
150
151 def _getMessages(self, serv, count, dbhome):
152 """This assumes that you currently have a mailbox open, and want to
153 process all messages that are inside.
154 """
155 for n in range(1, count+1):
156 (t, data) = serv.fetch(n, '(RFC822)')
157 if t == 'OK':
158 self._process(data[0][1], dbhome)
159 serv.store(n, '+FLAGS', r'(\Deleted)')
160
161 def checkBoxes(self):
162 """This actually goes out and does all the checking.
163 Returns False if there were any errors, otherwise returns true.
164 """
165 import imaplib
166 noErrors = True
167 for server in self.mailboxes:
168 log.info('Connecting to server: %s', server)
169 s_vals = self.mailboxes[server]
170
171 try:
172 for user in s_vals['users']:
173 u_vals = s_vals['users'][user]
174 # TODO: As near as I can tell, you can only
175 # login with 1 username for each connection to a server.
176 protocol = s_vals['protocol'].lower()
177 if protocol == 'imaps':
178 serv = imaplib.IMAP4_SSL(server)
179 elif protocol == 'imap':
180 serv = imaplib.IMAP4(server)
181 else:
182 raise ValueError, 'Unknown protocol %s' % protocol
183
184 password = u_vals['password']
185
186 try:
187 log.info('Connecting as user: %s', user)
188 serv.login(user, password)
189
190 for mbox in u_vals['mailboxes']:
191 dbhome = u_vals['mailboxes'][mbox]
192 log.info('Using mailbox: %s, home: %s', mbox, dbhome)
193 #access a specific mailbox
194 if mbox:
195 (t, data) = serv.select(mbox)
196 else:
197 # Select the default mailbox (INBOX)
198 (t, data) = serv.select()
199 try:
200 nMessages = int(data[0])
201 except ValueError:
202 nMessages = 0
203
204 log.info('Found %s messages', nMessages)
205
206 if nMessages:
207 self._getMessages(serv, nMessages, dbhome)
208 serv.expunge()
209
210 # We are done with this mailbox
211 serv.close()
212 except:
213 log.exception('Exception with server %s user %s', server, user)
214 noErrors = False
215
216 serv.logout()
217 serv.shutdown()
218 del serv
219 except:
220 log.exception('Exception while connecting to %s', server)
221 noErrors = False
222 return noErrors
223
224
225 def makeDaemon(self):
226 """This forks a couple of times, and otherwise makes this run as a daemon."""
227 ''' Turn this process into a daemon.
228 - make our parent PID 1
229
230 Write our new PID to the pidfile.
231
232 From A.M. Kuuchling (possibly originally Greg Ward) with
233 modification from Oren Tirosh, and finally a small mod from me.
234 Originally taken from roundup.scripts.roundup_server.py
235 '''
236 log.info('Running as Daemon')
237 import os
238 # Fork once
239 if os.fork() != 0:
240 os._exit(0)
241
242 # Create new session
243 os.setsid()
244
245 # Second fork to force PPID=1
246 pid = os.fork()
247 if pid:
248 if self.pidfile:
249 pidfile = open(self.pidfile, 'w')
250 pidfile.write(str(pid))
251 pidfile.close()
252 os._exit(0)
253
254 #os.chdir("/")
255 #os.umask(0)
256
257 def run(self):
258 """This spawns itself as a daemon, and then runs continually, just sleeping inbetween checks.
259 It is recommended that you run checkBoxes once first before you select run. That way you can
260 know if there were any failures.
261 """
262 import time
263 if self.daemon:
264 self.makeDaemon()
265 while True:
266
267 time.sleep(self.delay * 60.0)
268 log.info('Time: %s', time.strftime('%Y-%m-%d %H:%M:%S'))
269 self.checkBoxes()
270
271 def getItems(s):
272 """Parse a string looking for userame@server"""
273 import re
274 myRE = re.compile(
275 r'((?P<proto>[^:]+)://)?'#You can supply a protocol if you like
276 r'(' #The username part is optional
277 r'(?P<user>[^:]+)' #You can supply the password as
278 r'(:(?P<pass>.+))?' #username:password@server
279 r'@)?'
280 r'(?P<server>[^/]+)'
281 r'(/(?P<mailbox>.+))?$'
282 )
283 m = myRE.match(s)
284 if m:
285 return {'username':m.group('user'), 'password':m.group('pass')
286 , 'server':m.group('server'), 'protocol':m.group('proto')
287 , 'mailbox':m.group('mailbox')
288 }
289
290 def main():
291 """This is what is called if run at the prompt"""
292 import optparse, os
293 parser = optparse.OptionParser(
294 version=('%prog ' + version)
295 , usage="""usage: %prog [options] (home server)...
296 So each entry has a home, and then the server configuration. home is just a path to the
297 roundup issue tracker. The server is something of the form:
298 imaps://user:password@server/mailbox
299 If you don't supply the protocol, imaps is assumed. Without user or password, you will be
300 prompted for them. The server must be supplied. Without mailbox the INBOX is used.
301
302 Examples:
303 %prog /home/roundup/trackers/test imaps://test@imap.example.com/test
304 %prog /home/roundup/trackers/test imap.example.com /home/roundup/trackers/test2 imap.example.com/test2
305 """
306 )
307 parser.add_option('-d', '--delay', dest='delay', type='float', metavar='<sec>'
308 , default=5
309 , help="Set the delay between checks in minutes. (default 5)"
310 )
311 parser.add_option('-p', '--pid-file', dest='pidfile', metavar='<file>'
312 , default=None
313 , help="The pid of the server process will be written to <file>"
314 )
315 parser.add_option('-n', '--no-daemon', dest='daemon', action='store_false'
316 , default=True
317 , help="Do not fork into the background after running the first check."
318 )
319 parser.add_option('-v', '--verbose', dest='verbose', action='store_const'
320 , const=logging.INFO
321 , help="Be more verbose in letting you know what is going on."
322 " Enables informational messages."
323 )
324 parser.add_option('-V', '--very-verbose', dest='verbose', action='store_const'
325 , const=logging.DEBUG
326 , help="Be very verbose in letting you know what is going on."
327 " Enables debugging messages."
328 )
329 parser.add_option('-q', '--quiet', dest='verbose', action='store_const'
330 , const=logging.ERROR
331 , help="Be less verbose. Ignores warnings, only prints errors."
332 )
333 parser.add_option('-Q', '--very-quiet', dest='verbose', action='store_const'
334 , const=logging.CRITICAL
335 , help="Be much less verbose. Ignores warnings and errors."
336 " Only print CRITICAL messages."
337 )
338
339 (opts, args) = parser.parse_args()
340 if (len(args) == 0) or (len(args) % 2 == 1):
341 parser.error('Invalid number of arguments. Each site needs a home and a server.')
342
343 log.setLevel(opts.verbose)
344 myServer = IMAPServer(delay=opts.delay, pidfile=opts.pidfile, daemon=opts.daemon)
345 for i in range(0,len(args),2):
346 home = args[i]
347 server = args[i+1]
348 if not os.path.exists(home):
349 parser.error('Home: "%s" does not exist' % home)
350
351 info = getItems(server)
352 if not info:
353 parser.error('Invalid server string: "%s"' % server)
354
355 myServer.addMailbox(
356 RoundupMailbox(dbhome=home, mailbox=info['mailbox']
357 , username=info['username'], password=info['password']
358 , server=info['server'], protocol=info['protocol']
359 )
360 )
361
362 if myServer.checkBoxes():
363 myServer.run()
364
365 if __name__ == '__main__':
366 main()
367

Roundup Issue Tracker: http://roundup-tracker.org/