]>
code.delx.au - offlineimap/blob - offlineimap/imapserver.py
f295743646cfb8f6ef2fe93f7fd4c77b403f2b03
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
19 from offlineimap
import imaplib2
, imaplibutil
, imaputil
, threadutil
20 from offlineimap
.ui
import UIBase
21 from offlineimap
.accounts
import syncfolder
22 from threading
import *
23 import thread
, hmac
, os
, time
26 from StringIO
import StringIO
27 from platform
import system
30 # do we have a recent pykerberos?
33 if 'authGSSClientWrap' in dir(kerberos
):
38 class UsefulIMAPMixIn
:
41 def getselectedfolder(self
):
42 if self
.getstate() == 'SELECTED':
43 return self
.selectedfolder
46 def select(self
, mailbox
='INBOX', readonly
=None, force
= 0):
47 if (not force
) and self
.getselectedfolder() == mailbox \
48 and self
.is_readonly
== readonly
:
51 result
= self
.__class
__.__bases
__[1].select(self
, mailbox
, readonly
)
53 raise ValueError, "Error from select: %s" % str(result
)
54 if self
.getstate() == 'SELECTED':
55 self
.selectedfolder
= mailbox
57 self
.selectedfolder
= None
59 def _mesg(self
, s
, tn
=None, secs
=None):
60 imaplibutil
.new_mesg(self
, s
, tn
, secs
)
62 class UsefulIMAP4(UsefulIMAPMixIn
, imaplib2
.IMAP4
):
63 # This is a hack around Darwin's implementation of realloc() (which
64 # Python uses inside the socket code). On Darwin, we split the
65 # message into 100k chunks, which should be small enough - smaller
66 # might start seriously hurting performance ...
69 if (system() == 'Darwin') and (size
>0) :
73 sz
= min(size
-read
, 8192)
74 data
= imaplib2
.IMAP4
.read (self
, sz
)
81 return imaplib2
.IMAP4
.read (self
, size
)
83 class UsefulIMAP4_SSL(UsefulIMAPMixIn
, imaplibutil
.WrappedIMAP4_SSL
):
84 # This is the same hack as above, to be used in the case of an SSL
88 if (system() == 'Darwin') and (size
>0) :
92 sz
= min(size
-read
,8192)
93 data
= imaplibutil
.WrappedIMAP4_SSL
.read (self
, sz
)
100 return imaplibutil
.WrappedIMAP4_SSL
.read (self
,size
)
102 class UsefulIMAP4_Tunnel(UsefulIMAPMixIn
, imaplibutil
.IMAP4_Tunnel
): pass
107 def __init__(self
, config
, reposname
,
108 username
= None, password
= None, hostname
= None,
109 port
= None, ssl
= 1, maxconnections
= 1, tunnel
= None,
110 reference
= '""', sslclientcert
= None, sslclientkey
= None,
112 self
.reposname
= reposname
114 self
.username
= username
115 self
.password
= password
116 self
.passworderror
= None
117 self
.goodpassword
= None
118 self
.hostname
= hostname
122 self
.sslclientcert
= sslclientcert
123 self
.sslclientkey
= sslclientkey
131 self
.maxconnections
= maxconnections
132 self
.availableconnections
= []
133 self
.assignedconnections
= []
135 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
136 self
.connectionlock
= Lock()
137 self
.reference
= reference
138 self
.idlefolders
= idlefolders
139 self
.gss_step
= self
.GSS_STATE_STEP
143 def getpassword(self
):
144 if self
.goodpassword
!= None:
145 return self
.goodpassword
147 if self
.password
!= None and self
.passworderror
== None:
150 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
153 self
.passworderror
= None
158 """Returns this server's folder delimiter. Can only be called
159 after one or more calls to acquireconnection."""
163 """Returns this server's folder root. Can only be called after one
164 or more calls to acquireconnection."""
168 def releaseconnection(self
, connection
):
169 """Releases a connection, returning it to the pool."""
170 self
.connectionlock
.acquire()
171 self
.assignedconnections
.remove(connection
)
172 self
.availableconnections
.append(connection
)
173 self
.connectionlock
.release()
174 self
.semaphore
.release()
176 def md5handler(self
, response
):
177 ui
= UIBase
.getglobalui()
178 challenge
= response
.strip()
179 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
181 passwd
= self
.getpassword()
182 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
183 ui
.debug('imap', 'md5handler: returning %s' % retval
)
186 def plainauth(self
, imapobj
):
187 UIBase
.getglobalui().debug('imap',
188 'Attempting plain authentication')
189 imapobj
.login(self
.username
, self
.getpassword())
191 def gssauth(self
, response
):
192 data
= base64
.b64encode(response
)
194 if self
.gss_step
== self
.GSS_STATE_STEP
:
196 rc
, self
.gss_vc
= kerberos
.authGSSClientInit('imap@' +
198 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
199 rc
= kerberos
.authGSSClientStep(self
.gss_vc
, data
)
200 if rc
!= kerberos
.AUTH_GSS_CONTINUE
:
201 self
.gss_step
= self
.GSS_STATE_WRAP
202 elif self
.gss_step
== self
.GSS_STATE_WRAP
:
203 rc
= kerberos
.authGSSClientUnwrap(self
.gss_vc
, data
)
204 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
205 rc
= kerberos
.authGSSClientWrap(self
.gss_vc
, response
,
207 response
= kerberos
.authGSSClientResponse(self
.gss_vc
)
208 except kerberos
.GSSError
, err
:
209 # Kerberos errored out on us, respond with None to cancel the
211 UIBase
.getglobalui().debug('imap',
212 '%s: %s' % (err
[0][0], err
[1][0]))
217 return base64
.b64decode(response
)
219 def acquireconnection(self
):
220 """Fetches a connection from the pool, making sure to create a new one
221 if needed, to obey the maximum connection limits, etc.
222 Opens a connection to the server and returns an appropriate
225 self
.semaphore
.acquire()
226 self
.connectionlock
.acquire()
229 if len(self
.availableconnections
): # One is available.
230 # Try to find one that previously belonged to this thread
231 # as an optimization. Start from the back since that's where
233 threadid
= thread
.get_ident()
235 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
236 tryobj
= self
.availableconnections
[i
]
237 if self
.lastowner
[tryobj
] == threadid
:
239 del(self
.availableconnections
[i
])
242 imapobj
= self
.availableconnections
[0]
243 del(self
.availableconnections
[0])
244 self
.assignedconnections
.append(imapobj
)
245 self
.lastowner
[imapobj
] = thread
.get_ident()
246 self
.connectionlock
.release()
249 self
.connectionlock
.release() # Release until need to modify data
253 # Generate a new connection.
255 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
256 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
259 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
260 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
,
261 self
.sslclientkey
, self
.sslclientcert
)
263 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
264 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
266 imapobj
.mustquote
= imaplibutil
.mustquote
270 # Try GSSAPI and continue if it fails
271 if 'AUTH=GSSAPI' in imapobj
.capabilities
and have_gss
:
272 UIBase
.getglobalui().debug('imap',
273 'Attempting GSSAPI authentication')
275 imapobj
.authenticate('GSSAPI', self
.gssauth
)
276 except imapobj
.error
, val
:
277 UIBase
.getglobalui().debug('imap',
278 'GSSAPI Authentication failed')
284 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
285 UIBase
.getglobalui().debug('imap',
286 'Attempting CRAM-MD5 authentication')
288 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
289 except imapobj
.error
, val
:
290 self
.plainauth(imapobj
)
292 self
.plainauth(imapobj
)
293 # Would bail by here if there was a failure.
295 self
.goodpassword
= self
.password
296 except imapobj
.error
, val
:
297 self
.passworderror
= str(val
)
300 if self
.delim
== None:
301 listres
= imapobj
.list(self
.reference
, '""')[1]
302 if listres
== [None] or listres
== None:
303 # Some buggy IMAP servers do not respond well to LIST "" ""
305 listres
= imapobj
.list(self
.reference
, '"*"')[1]
306 self
.delim
, self
.root
= \
307 imaputil
.imapsplit(listres
[0])[1:]
308 self
.delim
= imaputil
.dequote(self
.delim
)
309 self
.root
= imaputil
.dequote(self
.root
)
311 self
.connectionlock
.acquire()
312 self
.assignedconnections
.append(imapobj
)
313 self
.lastowner
[imapobj
] = thread
.get_ident()
314 self
.connectionlock
.release()
317 def connectionwait(self
):
318 """Waits until there is a connection available. Note that between
319 the time that a connection becomes available and the time it is
320 requested, another thread may have grabbed it. This function is
321 mainly present as a way to avoid spawning thousands of threads
322 to copy messages, then have them all wait for 3 available connections.
323 It's OK if we have maxconnections + 1 or 2 threads, which is what
324 this will help us do."""
325 threadutil
.semaphorewait(self
.semaphore
)
328 # Make sure I own all the semaphores. Let the threads finish
329 # their stuff. This is a blocking method.
330 self
.connectionlock
.acquire()
331 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
332 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
334 self
.assignedconnections
= []
335 self
.availableconnections
= []
337 self
.connectionlock
.release()
339 def keepalive(self
, timeout
, event
):
340 """Sends a NOOP to each connection recorded. It will wait a maximum
341 of timeout seconds between doing this, and will continue to do so
342 until the Event object as passed is true. This method is expected
343 to be invoked in a separate thread, which should be join()'d after
345 ui
= UIBase
.getglobalui()
346 ui
.debug('imap', 'keepalive thread started')
348 ui
.debug('imap', 'keepalive: top of loop')
350 ui
.debug('imap', 'keepalive: event is set; exiting')
352 ui
.debug('imap', 'keepalive: acquiring connectionlock')
353 self
.connectionlock
.acquire()
354 numconnections
= len(self
.assignedconnections
) + \
355 len(self
.availableconnections
)
356 self
.connectionlock
.release()
357 ui
.debug('imap', 'keepalive: connectionlock released')
360 for i
in range(numconnections
):
361 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
362 if len(self
.idlefolders
) > i
:
363 idler
= IdleThread(self
, self
.idlefolders
[i
])
365 idler
= IdleThread(self
)
367 threads
.append(idler
)
368 ui
.debug('imap', 'keepalive: thread started')
370 ui
.debug('imap', 'keepalive: waiting for timeout')
373 ui
.debug('imap', 'keepalive: joining threads')
375 for idler
in threads
:
376 # Make sure all the commands have completed.
380 ui
.debug('imap', 'keepalive: bottom of loop')
382 class IdleThread(object):
383 def __init__(self
, parent
, folder
=None):
388 self
.thread
= Thread(target
=self
.noop
)
390 self
.thread
= Thread(target
=self
.idle
)
391 self
.thread
.setDaemon(1)
403 imapobj
= self
.parent
.acquireconnection()
406 self
.parent
.releaseconnection(imapobj
)
409 remoterepos
= self
.parent
.repos
410 account
= remoterepos
.account
411 localrepos
= account
.localrepos
412 remoterepos
= account
.remoterepos
413 statusrepos
= account
.statusrepos
414 remotefolder
= remoterepos
.getfolder(self
.folder
)
415 syncfolder(account
.name
, remoterepos
, remotefolder
, localrepos
, statusrepos
, quick
=False)
416 ui
= UIBase
.getglobalui()
417 ui
.unregisterthread(currentThread())
420 imapobj
= self
.parent
.acquireconnection()
421 imapobj
.select(self
.folder
)
422 self
.parent
.releaseconnection(imapobj
)
424 if self
.event
.isSet():
426 self
.needsync
= False
428 if not self
.event
.isSet():
431 imapobj
= self
.parent
.acquireconnection()
432 imapobj
.idle(callback
=callback
)
434 if self
.event
.isSet():
436 self
.parent
.releaseconnection(imapobj
)
441 class ConfigedIMAPServer(IMAPServer
):
442 """This class is designed for easier initialization given a ConfigParser
443 object and an account name. The passwordhash is used if
444 passwords for certain accounts are known. If the password for this
445 account is listed, it will be obtained from there."""
446 def __init__(self
, repository
, passwordhash
= {}):
447 """Initialize the object. If the account is not a tunnel,
448 the password is required."""
449 self
.repos
= repository
450 self
.config
= self
.repos
.getconfig()
451 usetunnel
= self
.repos
.getpreauthtunnel()
453 host
= self
.repos
.gethost()
454 user
= self
.repos
.getuser()
455 port
= self
.repos
.getport()
456 ssl
= self
.repos
.getssl()
457 sslclientcert
= self
.repos
.getsslclientcert()
458 sslclientkey
= self
.repos
.getsslclientkey()
459 reference
= self
.repos
.getreference()
460 idlefolders
= self
.repos
.getidlefolders()
464 if repository
.getname() in passwordhash
:
465 password
= passwordhash
[repository
.getname()]
467 # Connect to the remote server.
469 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
471 reference
= reference
,
472 idlefolders
= idlefolders
,
473 maxconnections
= self
.repos
.getmaxconnections())
476 password
= self
.repos
.getpassword()
477 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
478 user
, password
, host
, port
, ssl
,
479 self
.repos
.getmaxconnections(),
480 reference
= reference
,
481 idlefolders
= idlefolders
,
482 sslclientcert
= sslclientcert
,
483 sslclientkey
= sslclientkey
)