]>
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
, time
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,
75 reference
= '""', sslclientcert
= None, sslclientkey
= 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
86 self
.sslclientcert
= sslclientcert
87 self
.sslclientkey
= sslclientkey
95 self
.maxconnections
= maxconnections
96 self
.availableconnections
= []
97 self
.assignedconnections
= []
99 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
100 self
.connectionlock
= Lock()
101 self
.reference
= reference
102 self
.gss_step
= self
.GSS_STATE_STEP
106 def getpassword(self
):
107 if self
.goodpassword
!= None:
108 return self
.goodpassword
110 if self
.password
!= None and self
.passworderror
== None:
113 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
116 self
.passworderror
= None
121 """Returns this server's folder delimiter. Can only be called
122 after one or more calls to acquireconnection."""
126 """Returns this server's folder root. Can only be called after one
127 or more calls to acquireconnection."""
131 def releaseconnection(self
, connection
):
132 """Releases a connection, returning it to the pool."""
133 self
.connectionlock
.acquire()
134 self
.assignedconnections
.remove(connection
)
135 self
.availableconnections
.append(connection
)
136 self
.connectionlock
.release()
137 self
.semaphore
.release()
139 def md5handler(self
, response
):
140 ui
= UIBase
.getglobalui()
141 challenge
= response
.strip()
142 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
144 passwd
= self
.getpassword()
145 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
146 ui
.debug('imap', 'md5handler: returning %s' % retval
)
149 def plainauth(self
, imapobj
):
150 UIBase
.getglobalui().debug('imap',
151 'Attempting plain authentication')
152 imapobj
.login(self
.username
, self
.getpassword())
154 def gssauth(self
, response
):
155 data
= base64
.b64encode(response
)
157 if self
.gss_step
== self
.GSS_STATE_STEP
:
159 rc
, self
.gss_vc
= kerberos
.authGSSClientInit('imap@' +
161 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
162 rc
= kerberos
.authGSSClientStep(self
.gss_vc
, data
)
163 if rc
!= kerberos
.AUTH_GSS_CONTINUE
:
164 self
.gss_step
= self
.GSS_STATE_WRAP
165 elif self
.gss_step
== self
.GSS_STATE_WRAP
:
166 rc
= kerberos
.authGSSClientUnwrap(self
.gss_vc
, data
)
167 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
168 rc
= kerberos
.authGSSClientWrap(self
.gss_vc
, response
,
170 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
171 except kerberos
.GSSError
, err
:
172 # Kerberos errored out on us, respond with None to cancel the
174 UIBase
.getglobalui().debug('imap',
175 '%s: %s' % (err
[0][0], err
[1][0]))
180 return base64
.b64decode(response
)
182 def acquireconnection(self
):
183 """Fetches a connection from the pool, making sure to create a new one
184 if needed, to obey the maximum connection limits, etc.
185 Opens a connection to the server and returns an appropriate
188 self
.semaphore
.acquire()
189 self
.connectionlock
.acquire()
192 if len(self
.availableconnections
): # One is available.
193 # Try to find one that previously belonged to this thread
194 # as an optimization. Start from the back since that's where
196 threadid
= thread
.get_ident()
198 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
199 tryobj
= self
.availableconnections
[i
]
200 if self
.lastowner
[tryobj
] == threadid
:
202 del(self
.availableconnections
[i
])
205 imapobj
= self
.availableconnections
[0]
206 del(self
.availableconnections
[0])
207 self
.assignedconnections
.append(imapobj
)
208 self
.lastowner
[imapobj
] = thread
.get_ident()
209 self
.connectionlock
.release()
212 self
.connectionlock
.release() # Release until need to modify data
216 # Generate a new connection.
218 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
219 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
222 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
223 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
,
224 self
.sslclientkey
, self
.sslclientcert
)
226 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
227 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
229 imapobj
.mustquote
= imaplibutil
.mustquote
233 # Try GSSAPI and continue if it fails
234 if 'AUTH=GSSAPI' in imapobj
.capabilities
and have_gss
:
235 UIBase
.getglobalui().debug('imap',
236 'Attempting GSSAPI authentication')
238 imapobj
.authenticate('GSSAPI', self
.gssauth
)
239 except imapobj
.error
, val
:
240 UIBase
.getglobalui().debug('imap',
241 'GSSAPI Authentication failed')
247 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
248 UIBase
.getglobalui().debug('imap',
249 'Attempting CRAM-MD5 authentication')
251 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
252 except imapobj
.error
, val
:
253 self
.plainauth(imapobj
)
255 self
.plainauth(imapobj
)
256 # Would bail by here if there was a failure.
258 self
.goodpassword
= self
.password
259 except imapobj
.error
, val
:
260 self
.passworderror
= str(val
)
263 if self
.delim
== None:
264 listres
= imapobj
.list(self
.reference
, '""')[1]
265 if listres
== [None] or listres
== None:
266 # Some buggy IMAP servers do not respond well to LIST "" ""
268 listres
= imapobj
.list(self
.reference
, '"*"')[1]
269 self
.delim
, self
.root
= \
270 imaputil
.imapsplit(listres
[0])[1:]
271 self
.delim
= imaputil
.dequote(self
.delim
)
272 self
.root
= imaputil
.dequote(self
.root
)
274 self
.connectionlock
.acquire()
275 self
.assignedconnections
.append(imapobj
)
276 self
.lastowner
[imapobj
] = thread
.get_ident()
277 self
.connectionlock
.release()
280 def connectionwait(self
):
281 """Waits until there is a connection available. Note that between
282 the time that a connection becomes available and the time it is
283 requested, another thread may have grabbed it. This function is
284 mainly present as a way to avoid spawning thousands of threads
285 to copy messages, then have them all wait for 3 available connections.
286 It's OK if we have maxconnections + 1 or 2 threads, which is what
287 this will help us do."""
288 threadutil
.semaphorewait(self
.semaphore
)
291 # Make sure I own all the semaphores. Let the threads finish
292 # their stuff. This is a blocking method.
293 self
.connectionlock
.acquire()
294 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
295 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
297 self
.assignedconnections
= []
298 self
.availableconnections
= []
300 self
.connectionlock
.release()
302 def keepalive(self
, timeout
, event
):
303 """Sends a NOOP to each connection recorded. It will wait a maximum
304 of timeout seconds between doing this, and will continue to do so
305 until the Event object as passed is true. This method is expected
306 to be invoked in a separate thread, which should be join()'d after
308 ui
= UIBase
.getglobalui()
309 ui
.debug('imap', 'keepalive thread started')
311 ui
.debug('imap', 'keepalive: top of loop')
313 ui
.debug('imap', 'keepalive: after wait')
315 ui
.debug('imap', 'keepalive: event is set; exiting')
317 ui
.debug('imap', 'keepalive: acquiring connectionlock')
318 self
.connectionlock
.acquire()
319 numconnections
= len(self
.assignedconnections
) + \
320 len(self
.availableconnections
)
321 self
.connectionlock
.release()
322 ui
.debug('imap', 'keepalive: connectionlock released')
326 for i
in range(numconnections
):
327 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
328 imapobj
= self
.acquireconnection()
329 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
330 imapobjs
.append(imapobj
)
331 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
335 ui
.debug('imap', 'keepalive: thread started')
337 ui
.debug('imap', 'keepalive: joining threads')
340 # Make sure all the commands have completed.
343 ui
.debug('imap', 'keepalive: releasing connections')
345 for imapobj
in imapobjs
:
346 self
.releaseconnection(imapobj
)
348 ui
.debug('imap', 'keepalive: bottom of loop')
350 class ConfigedIMAPServer(IMAPServer
):
351 """This class is designed for easier initialization given a ConfigParser
352 object and an account name. The passwordhash is used if
353 passwords for certain accounts are known. If the password for this
354 account is listed, it will be obtained from there."""
355 def __init__(self
, repository
, passwordhash
= {}):
356 """Initialize the object. If the account is not a tunnel,
357 the password is required."""
358 self
.repos
= repository
359 self
.config
= self
.repos
.getconfig()
360 usetunnel
= self
.repos
.getpreauthtunnel()
362 host
= self
.repos
.gethost()
363 user
= self
.repos
.getuser()
364 port
= self
.repos
.getport()
365 ssl
= self
.repos
.getssl()
366 sslclientcert
= self
.repos
.getsslclientcert()
367 sslclientkey
= self
.repos
.getsslclientkey()
368 reference
= self
.repos
.getreference()
372 if repository
.getname() in passwordhash
:
373 password
= passwordhash
[repository
.getname()]
375 # Connect to the remote server.
377 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
379 reference
= reference
,
380 maxconnections
= self
.repos
.getmaxconnections())
383 password
= self
.repos
.getpassword()
384 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
385 user
, password
, host
, port
, ssl
,
386 self
.repos
.getmaxconnections(),
387 reference
= reference
,
388 sslclientcert
= sslclientcert
,
389 sslclientkey
= sslclientkey
)