3 This script is a wrapper around the mailgw.py script that exists in roundup.
4 It runs as service instead of running as a one-time shot.
5 It also connects to a secure IMAP server. The main reasons for this script are:
7 1) The roundup-mailgw script isn't designed to run as a server. It
8 expects that you either run it by hand, and enter the password each
9 time, or you supply the password on the command line. I prefer to
10 run a server that I initialize with the password, and then it just
11 runs. I don't want to have to pass it on the command line, so
12 running through crontab isn't a possibility. (This wouldn't be a
13 problem on a local machine running through a mailspool.)
14 2) mailgw.py somehow screws up SSL support so IMAP4_SSL doesn't work. So
15 hopefully running that work outside of the mailgw will allow it to work.
16 3) I wanted to be able to check multiple projects at the same time.
17 roundup-mailgw is only for 1 mailbox and 1 project.
21 For the first round, the program spawns a new roundup-mailgw for
22 each imap message that it finds and pipes the result in. In the
23 future it might be more practical to actually include the roundup
24 files and run the appropriate commands using python.
27 Look into supporting a logfile instead of using 2>/logfile
30 Add an option for changing the uid/gid of the running process.
42 log
= logging
.getLogger('IMAPServer')
47 """This contains all the info about each mailbox.
48 Username, Password, server, security, roundup database
50 def __init__(self
, dbhome
='', username
=None, password
=None, mailbox
=None
51 , server
=None, protocol
='imaps'):
52 self
.username
= username
53 self
.password
= password
54 self
.mailbox
= mailbox
56 self
.protocol
= protocol
61 self
.dbhome
= raw_input('Tracker home: ')
62 if not os
.path
.exists(self
.dbhome
):
63 raise ValueError, 'Invalid home address: ' \
64 'directory "%s" does not exist.' % self
.dbhome
67 self
.server
= raw_input('Server: ')
69 raise ValueError, 'No Servername supplied'
70 protocol
= raw_input('protocol [imaps]? ')
71 self
.protocol
= protocol
74 self
.username
= raw_input('Username: ')
76 raise ValueError, 'Invalid Username'
79 print 'For server %s, user %s' % (self
.server
, self
.username
)
80 self
.password
= getpass
.getpass()
81 # password can be empty because it could be superceeded
84 #if self.mailbox is None:
85 # self.mailbox = raw_input('Mailbox [INBOX]: ')
86 # # We allow an empty mailbox because that will
87 # # select the INBOX, whatever it is called
89 except (KeyboardInterrupt, EOFError):
90 raise ValueError, 'Canceled by User'
93 return 'Mailbox{ server:%(server)s, protocol:%(protocol)s, ' \
94 'username:%(username)s, mailbox:%(mailbox)s, ' \
95 'dbhome:%(dbhome)s }' % self
.__dict__
98 # [als] class name is misleading. this is imap client, not imap server
101 """IMAP mail gatherer.
103 This class runs as a server process. It is configured with a list of
104 mailboxes to connect to, along with the roundup database directories
105 that correspond with each email address. It then connects to each
106 mailbox at a specified interval, and if there are new messages it
107 reads them, and sends the result to the roundup.mailgw.
110 Try to be smart about how you access the mailboxes so that you can
111 connect once, and access multiple mailboxes and possibly multiple
115 This assumes that if you are using the same user on the same
116 server, you are using the same password. (the last one supplied is
117 used.) Empty passwords are ignored. Only the last protocol
121 def __init__(self
, pidfile
=None, delay
=5, daemon
=False):
122 #This is sorted by servername, then username, then mailboxes
124 self
.delay
= float(delay
)
125 self
.pidfile
= pidfile
128 def setDelay(self
, delay
):
131 def addMailbox(self
, mailbox
):
132 """ The linkage is as follows:
133 servers -- users - mailbox:dbhome
134 So there can be multiple servers, each with multiple users.
135 Each username can be associated with multiple mailboxes.
136 each mailbox is associated with 1 database home
138 log
.info('Adding mailbox %s', mailbox
)
139 if not self
.mailboxes
.has_key(mailbox
.server
):
140 self
.mailboxes
[mailbox
.server
] = {'protocol':'imaps', 'users':{}}
141 server
= self
.mailboxes
[mailbox
.server
]
143 server
['protocol'] = mailbox
.protocol
145 if not server
['users'].has_key(mailbox
.username
):
146 server
['users'][mailbox
.username
] = {'password':'', 'mailboxes':{}}
147 user
= server
['users'][mailbox
.username
]
149 user
['password'] = mailbox
.password
151 if user
['mailboxes'].has_key(mailbox
.mailbox
):
152 raise ValueError, 'Mailbox is already defined'
154 user
['mailboxes'][mailbox
.mailbox
] = mailbox
.dbhome
156 def _process(self
, message
, dbhome
):
157 """Actually process one of the email messages"""
158 child
= os
.popen('roundup-mailgw %s' % dbhome
, 'wb')
163 def _getMessages(self
, serv
, count
, dbhome
):
164 """This assumes that you currently have a mailbox open, and want to
165 process all messages that are inside.
167 for n
in range(1, count
+1):
168 (t
, data
) = serv
.fetch(n
, '(RFC822)')
170 self
._process(data
[0][1], dbhome
)
171 serv
.store(n
, '+FLAGS', r
'(\Deleted)')
173 def checkBoxes(self
):
174 """This actually goes out and does all the checking.
175 Returns False if there were any errors, otherwise returns true.
178 for server
in self
.mailboxes
:
179 log
.info('Connecting to server: %s', server
)
180 s_vals
= self
.mailboxes
[server
]
183 for user
in s_vals
['users']:
184 u_vals
= s_vals
['users'][user
]
185 # TODO: As near as I can tell, you can only
186 # login with 1 username for each connection to a server.
187 protocol
= s_vals
['protocol'].lower()
188 if protocol
== 'imaps':
189 serv
= imaplib
.IMAP4_SSL(server
)
190 elif protocol
== 'imap':
191 serv
= imaplib
.IMAP4(server
)
193 raise ValueError, 'Unknown protocol %s' % protocol
195 password
= u_vals
['password']
198 log
.info('Connecting as user: %s', user
)
199 serv
.login(user
, password
)
201 for mbox
in u_vals
['mailboxes']:
202 dbhome
= u_vals
['mailboxes'][mbox
]
203 log
.info('Using mailbox: %s, home: %s',
205 #access a specific mailbox
207 (t
, data
) = serv
.select(mbox
)
209 # Select the default mailbox (INBOX)
210 (t
, data
) = serv
.select()
212 nMessages
= int(data
[0])
216 log
.info('Found %s messages', nMessages
)
219 self
._getMessages(serv
, nMessages
, dbhome
)
222 # We are done with this mailbox
225 log
.exception('Exception with server %s user %s',
233 log
.exception('Exception while connecting to %s', server
)
238 def makeDaemon(self
):
239 """Turn this process into a daemon.
241 - make our parent PID 1
243 Write our new PID to the pidfile.
245 From A.M. Kuuchling (possibly originally Greg Ward) with
246 modification from Oren Tirosh, and finally a small mod from me.
247 Originally taken from roundup.scripts.roundup_server.py
249 log
.info('Running as Daemon')
257 # Second fork to force PPID=1
261 pidfile
= open(self
.pidfile
, 'w')
262 pidfile
.write(str(pid
))
267 """Run email gathering daemon.
269 This spawns itself as a daemon, and then runs continually, just
270 sleeping inbetween checks. It is recommended that you run
271 checkBoxes once first before you select run. That way you can
272 know if there were any failures.
278 time
.sleep(self
.delay
* 60.0)
279 log
.info('Time: %s', time
.strftime('%Y-%m-%d %H:%M:%S'))
283 """Parse a string looking for userame@server"""
285 r
'((?P<protocol>[^:]+)://)?'#You can supply a protocol if you like
286 r
'(' #The username part is optional
287 r
'(?P<username>[^:]+)' #You can supply the password as
288 r
'(:(?P<password>.+))?' #username:password@server
291 r
'(/(?P<mailbox>.+))?$'
300 """This is what is called if run at the prompt"""
301 parser
= optparse
.OptionParser(
302 version
=('%prog ' + version
),
303 usage
="""usage: %prog [options] (home server)...
305 So each entry has a home, and then the server configuration. Home is just
306 a path to the roundup issue tracker. The server is something of the form:
308 imaps://user:password@server/mailbox
310 If you don't supply the protocol, imaps is assumed. Without user or
311 password, you will be prompted for them. The server must be supplied.
312 Without mailbox the INBOX is used.
315 %prog /home/roundup/trackers/test imaps://test@imap.example.com/test
316 %prog /home/roundup/trackers/test imap.example.com \
317 /home/roundup/trackers/test2 imap.example.com/test2
320 parser
.add_option('-d', '--delay', dest
='delay', type='float',
321 metavar
='<sec>', default
=5,
322 help="Set the delay between checks in minutes. (default 5)"
324 parser
.add_option('-p', '--pid-file', dest
='pidfile',
325 metavar
='<file>', default
=None,
326 help="The pid of the server process will be written to <file>"
328 parser
.add_option('-n', '--no-daemon', dest
='daemon',
329 action
='store_false', default
=True,
330 help="Do not fork into the background after running the first check."
332 parser
.add_option('-v', '--verbose', dest
='verbose',
333 action
='store_const', const
=logging
.INFO
,
334 help="Be more verbose in letting you know what is going on."
335 " Enables informational messages."
337 parser
.add_option('-V', '--very-verbose', dest
='verbose',
338 action
='store_const', const
=logging
.DEBUG
,
339 help="Be very verbose in letting you know what is going on."
340 " Enables debugging messages."
342 parser
.add_option('-q', '--quiet', dest
='verbose',
343 action
='store_const', const
=logging
.ERROR
,
344 help="Be less verbose. Ignores warnings, only prints errors."
346 parser
.add_option('-Q', '--very-quiet', dest
='verbose',
347 action
='store_const', const
=logging
.CRITICAL
,
348 help="Be much less verbose. Ignores warnings and errors."
349 " Only print CRITICAL messages."
352 (opts
, args
) = parser
.parse_args()
353 if (len(args
) == 0) or (len(args
) % 2 == 1):
354 parser
.error('Invalid number of arguments. '
355 'Each site needs a home and a server.')
357 log
.setLevel(opts
.verbose
)
358 myServer
= IMAPServer(delay
=opts
.delay
, pidfile
=opts
.pidfile
,
360 for i
in range(0,len(args
),2):
363 if not os
.path
.exists(home
):
364 parser
.error('Home: "%s" does not exist' % home
)
366 info
= getItems(server
)
368 parser
.error('Invalid server string: "%s"' % server
)
371 RoundupMailbox(dbhome
=home
, mailbox
=info
['mailbox']
372 , username
=info
['username'], password
=info
['password']
373 , server
=info
['server'], protocol
=info
['protocol']
377 if myServer
.checkBoxes():
380 if __name__
== '__main__':
383 # vim: et ft=python si sts=4 sw=4