Skip to content

Commit 39b1b6f

Browse files
author
Richard Jones
committed
added "imapServer.py" script (patch [SF#934567])
1 parent a673b6f commit 39b1b6f

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Fixed:
2424
- fixed class "help" listing paging (sf bug 1106329)
2525
- nicer error looking up values of None (response to sf bug 1108697)
2626
- fallback for (list) popups if javascript disabled (sf patch 1101626)
27+
- added "imapServer.py" script (sf patch 934567)
2728

2829

2930
2005-01-13 0.8.0b2

scripts/README.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ roundup.rc-debian
2323
Offers start, stop and restart commands and integrates with the Debian
2424
init process.
2525

26+
imapServer.py
27+
This IMAP server script that runs in the background and checks for new
28+
email from a variety of mailboxes.
29+

scripts/imapServer.py

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
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://[email protected]/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+

0 commit comments

Comments
 (0)