]>
code.delx.au - offlineimap/blob - offlineimap/imapserver.py
2 # Copyright (C) 2002 - 2007 John Goerzen
3 # <jgoerzen@complete.org>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20 from offlineimap
import imaplibutil
, imaputil
, threadutil
21 from offlineimap
.ui
import UIBase
22 from threading
import *
23 import thread
, hmac
, os
27 # do we have a recent pykerberos?
30 if 'authGSSClientWrap' in dir(kerberos
):
35 class UsefulIMAPMixIn
:
38 def getselectedfolder(self
):
39 if self
.getstate() == 'SELECTED':
40 return self
.selectedfolder
43 def select(self
, mailbox
='INBOX', readonly
=None, force
= 0):
44 if (not force
) and self
.getselectedfolder() == mailbox \
45 and self
.is_readonly
== readonly
:
48 result
= self
.__class
__.__bases
__[1].select(self
, mailbox
, readonly
)
50 raise ValueError, "Error from select: %s" % str(result
)
51 if self
.getstate() == 'SELECTED':
52 self
.selectedfolder
= mailbox
54 self
.selectedfolder
= None
56 def _mesg(self
, s
, secs
=None):
57 imaplibutil
.new_mesg(self
, s
, secs
)
59 class UsefulIMAP4(UsefulIMAPMixIn
, imaplib
.IMAP4
):
60 def open(self
, host
= '', port
= imaplib
.IMAP4_PORT
):
61 imaplibutil
.new_open(self
, host
, port
)
63 class UsefulIMAP4_SSL(UsefulIMAPMixIn
, imaplibutil
.WrappedIMAP4_SSL
):
64 def open(self
, host
= '', port
= imaplib
.IMAP4_SSL_PORT
):
65 imaplibutil
.new_open_ssl(self
, host
, port
)
67 class UsefulIMAP4_Tunnel(UsefulIMAPMixIn
, imaplibutil
.IMAP4_Tunnel
): pass
72 def __init__(self
, config
, reposname
,
73 username
= None, password
= None, hostname
= None,
74 port
= None, ssl
= 1, maxconnections
= 1, tunnel
= None,
76 self
.reposname
= reposname
78 self
.username
= username
79 self
.password
= password
80 self
.passworderror
= None
81 self
.goodpassword
= None
82 self
.hostname
= hostname
93 self
.maxconnections
= maxconnections
94 self
.availableconnections
= []
95 self
.assignedconnections
= []
97 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
98 self
.connectionlock
= Lock()
99 self
.reference
= reference
100 self
.gss_step
= self
.GSS_STATE_STEP
104 def getpassword(self
):
105 if self
.goodpassword
!= None:
106 return self
.goodpassword
108 if self
.password
!= None and self
.passworderror
== None:
111 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
114 self
.passworderror
= None
119 """Returns this server's folder delimiter. Can only be called
120 after one or more calls to acquireconnection."""
124 """Returns this server's folder root. Can only be called after one
125 or more calls to acquireconnection."""
129 def releaseconnection(self
, connection
):
130 """Releases a connection, returning it to the pool."""
131 self
.connectionlock
.acquire()
132 self
.assignedconnections
.remove(connection
)
133 self
.availableconnections
.append(connection
)
134 self
.connectionlock
.release()
135 self
.semaphore
.release()
137 def md5handler(self
, response
):
138 ui
= UIBase
.getglobalui()
139 challenge
= response
.strip()
140 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
142 passwd
= self
.getpassword()
143 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
144 ui
.debug('imap', 'md5handler: returning %s' % retval
)
147 def plainauth(self
, imapobj
):
148 UIBase
.getglobalui().debug('imap',
149 'Attempting plain authentication')
150 imapobj
.login(self
.username
, self
.getpassword())
152 def gssauth(self
, response
):
153 data
= base64
.b64encode(response
)
155 if self
.gss_step
== self
.GSS_STATE_STEP
:
157 rc
, self
.gss_vc
= kerberos
.authGSSClientInit('imap@' +
159 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
160 rc
= kerberos
.authGSSClientStep(self
.gss_vc
, data
)
161 if rc
!= kerberos
.AUTH_GSS_CONTINUE
:
162 self
.gss_step
= self
.GSS_STATE_WRAP
163 elif self
.gss_step
== self
.GSS_STATE_WRAP
:
164 rc
= kerberos
.authGSSClientUnwrap(self
.gss_vc
, data
)
165 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
166 rc
= kerberos
.authGSSClientWrap(self
.gss_vc
, response
,
168 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
169 except kerberos
.GSSError
, err
:
170 # Kerberos errored out on us, respond with None to cancel the
172 UIBase
.getglobalui().debug('imap',
173 '%s: %s' % (err
[0][0], err
[1][0]))
178 return base64
.b64decode(response
)
180 def acquireconnection(self
):
181 """Fetches a connection from the pool, making sure to create a new one
182 if needed, to obey the maximum connection limits, etc.
183 Opens a connection to the server and returns an appropriate
186 self
.semaphore
.acquire()
187 self
.connectionlock
.acquire()
190 if len(self
.availableconnections
): # One is available.
191 # Try to find one that previously belonged to this thread
192 # as an optimization. Start from the back since that's where
194 threadid
= thread
.get_ident()
196 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
197 tryobj
= self
.availableconnections
[i
]
198 if self
.lastowner
[tryobj
] == threadid
:
200 del(self
.availableconnections
[i
])
203 imapobj
= self
.availableconnections
[0]
204 del(self
.availableconnections
[0])
205 self
.assignedconnections
.append(imapobj
)
206 self
.lastowner
[imapobj
] = thread
.get_ident()
207 self
.connectionlock
.release()
210 self
.connectionlock
.release() # Release until need to modify data
214 # Generate a new connection.
216 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
217 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
220 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
221 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
)
223 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
224 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
226 imapobj
.mustquote
= imaplibutil
.mustquote
230 # Try GSSAPI and continue if it fails
231 if 'AUTH=GSSAPI' in imapobj
.capabilities
and have_gss
:
232 UIBase
.getglobalui().debug('imap',
233 'Attempting GSSAPI authentication')
235 imapobj
.authenticate('GSSAPI', self
.gssauth
)
236 except imapobj
.error
, val
:
237 UIBase
.getglobalui().debug('imap',
238 'GSSAPI Authentication failed')
244 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
245 UIBase
.getglobalui().debug('imap',
246 'Attempting CRAM-MD5 authentication')
248 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
249 except imapobj
.error
, val
:
250 self
.plainauth(imapobj
)
252 self
.plainauth(imapobj
)
253 # Would bail by here if there was a failure.
255 self
.goodpassword
= self
.password
256 except imapobj
.error
, val
:
257 self
.passworderror
= str(val
)
260 if self
.delim
== None:
261 listres
= imapobj
.list(self
.reference
, '""')[1]
262 if listres
== [None] or listres
== None:
263 # Some buggy IMAP servers do not respond well to LIST "" ""
265 listres
= imapobj
.list(self
.reference
, '"*"')[1]
266 self
.delim
, self
.root
= \
267 imaputil
.imapsplit(listres
[0])[1:]
268 self
.delim
= imaputil
.dequote(self
.delim
)
269 self
.root
= imaputil
.dequote(self
.root
)
271 self
.connectionlock
.acquire()
272 self
.assignedconnections
.append(imapobj
)
273 self
.lastowner
[imapobj
] = thread
.get_ident()
274 self
.connectionlock
.release()
277 def connectionwait(self
):
278 """Waits until there is a connection available. Note that between
279 the time that a connection becomes available and the time it is
280 requested, another thread may have grabbed it. This function is
281 mainly present as a way to avoid spawning thousands of threads
282 to copy messages, then have them all wait for 3 available connections.
283 It's OK if we have maxconnections + 1 or 2 threads, which is what
284 this will help us do."""
285 threadutil
.semaphorewait(self
.semaphore
)
288 # Make sure I own all the semaphores. Let the threads finish
289 # their stuff. This is a blocking method.
290 self
.connectionlock
.acquire()
291 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
292 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
294 self
.assignedconnections
= []
295 self
.availableconnections
= []
297 self
.connectionlock
.release()
299 def keepalive(self
, timeout
, event
):
300 """Sends a NOOP to each connection recorded. It will wait a maximum
301 of timeout seconds between doing this, and will continue to do so
302 until the Event object as passed is true. This method is expected
303 to be invoked in a separate thread, which should be join()'d after
305 ui
= UIBase
.getglobalui()
306 ui
.debug('imap', 'keepalive thread started')
308 ui
.debug('imap', 'keepalive: top of loop')
310 ui
.debug('imap', 'keepalive: after wait')
312 ui
.debug('imap', 'keepalive: event is set; exiting')
314 ui
.debug('imap', 'keepalive: acquiring connectionlock')
315 self
.connectionlock
.acquire()
316 numconnections
= len(self
.assignedconnections
) + \
317 len(self
.availableconnections
)
318 self
.connectionlock
.release()
319 ui
.debug('imap', 'keepalive: connectionlock released')
323 for i
in range(numconnections
):
324 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
325 imapobj
= self
.acquireconnection()
326 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
327 imapobjs
.append(imapobj
)
328 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
332 ui
.debug('imap', 'keepalive: thread started')
334 ui
.debug('imap', 'keepalive: joining threads')
337 # Make sure all the commands have completed.
340 ui
.debug('imap', 'keepalive: releasing connections')
342 for imapobj
in imapobjs
:
343 self
.releaseconnection(imapobj
)
345 ui
.debug('imap', 'keepalive: bottom of loop')
347 class ConfigedIMAPServer(IMAPServer
):
348 """This class is designed for easier initialization given a ConfigParser
349 object and an account name. The passwordhash is used if
350 passwords for certain accounts are known. If the password for this
351 account is listed, it will be obtained from there."""
352 def __init__(self
, repository
, passwordhash
= {}):
353 """Initialize the object. If the account is not a tunnel,
354 the password is required."""
355 self
.repos
= repository
356 self
.config
= self
.repos
.getconfig()
357 usetunnel
= self
.repos
.getpreauthtunnel()
359 host
= self
.repos
.gethost()
360 user
= self
.repos
.getuser()
361 port
= self
.repos
.getport()
362 ssl
= self
.repos
.getssl()
363 reference
= self
.repos
.getreference()
367 if repository
.getname() in passwordhash
:
368 password
= passwordhash
[repository
.getname()]
370 # Connect to the remote server.
372 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
374 reference
= reference
,
375 maxconnections
= self
.repos
.getmaxconnections())
378 password
= self
.repos
.getpassword()
379 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
380 user
, password
, host
, port
, ssl
,
381 self
.repos
.getmaxconnections(),
382 reference
= reference
)