]>
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
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 # reset kerberos state
338 self
.gss_step
= self
.GSS_STATE_STEP
341 self
.connectionlock
.release()
343 def keepalive(self
, timeout
, event
):
344 """Sends a NOOP to each connection recorded. It will wait a maximum
345 of timeout seconds between doing this, and will continue to do so
346 until the Event object as passed is true. This method is expected
347 to be invoked in a separate thread, which should be join()'d after
349 ui
= UIBase
.getglobalui()
350 ui
.debug('imap', 'keepalive thread started')
352 ui
.debug('imap', 'keepalive: top of loop')
354 ui
.debug('imap', 'keepalive: event is set; exiting')
356 ui
.debug('imap', 'keepalive: acquiring connectionlock')
357 self
.connectionlock
.acquire()
358 numconnections
= len(self
.assignedconnections
) + \
359 len(self
.availableconnections
)
360 self
.connectionlock
.release()
361 ui
.debug('imap', 'keepalive: connectionlock released')
364 for i
in range(numconnections
):
365 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
366 if len(self
.idlefolders
) > i
:
367 idler
= IdleThread(self
, self
.idlefolders
[i
])
369 idler
= IdleThread(self
)
371 threads
.append(idler
)
372 ui
.debug('imap', 'keepalive: thread started')
374 ui
.debug('imap', 'keepalive: waiting for timeout')
377 ui
.debug('imap', 'keepalive: joining threads')
379 for idler
in threads
:
380 # Make sure all the commands have completed.
384 ui
.debug('imap', 'keepalive: bottom of loop')
386 class IdleThread(object):
387 def __init__(self
, parent
, folder
=None):
392 self
.thread
= Thread(target
=self
.noop
)
394 self
.thread
= Thread(target
=self
.idle
)
395 self
.thread
.setDaemon(1)
407 imapobj
= self
.parent
.acquireconnection()
410 self
.parent
.releaseconnection(imapobj
)
413 remoterepos
= self
.parent
.repos
414 account
= remoterepos
.account
415 localrepos
= account
.localrepos
416 remoterepos
= account
.remoterepos
417 statusrepos
= account
.statusrepos
418 remotefolder
= remoterepos
.getfolder(self
.folder
)
419 syncfolder(account
.name
, remoterepos
, remotefolder
, localrepos
, statusrepos
, quick
=False)
420 ui
= UIBase
.getglobalui()
421 ui
.unregisterthread(currentThread())
424 imapobj
= self
.parent
.acquireconnection()
425 imapobj
.select(self
.folder
)
426 self
.parent
.releaseconnection(imapobj
)
428 if self
.event
.isSet():
430 self
.needsync
= False
432 if not self
.event
.isSet():
435 imapobj
= self
.parent
.acquireconnection()
436 if "IDLE" in imapobj
.capabilities
:
437 imapobj
.idle(callback
=callback
)
441 if self
.event
.isSet():
443 self
.parent
.releaseconnection(imapobj
)
448 class ConfigedIMAPServer(IMAPServer
):
449 """This class is designed for easier initialization given a ConfigParser
450 object and an account name. The passwordhash is used if
451 passwords for certain accounts are known. If the password for this
452 account is listed, it will be obtained from there."""
453 def __init__(self
, repository
, passwordhash
= {}):
454 """Initialize the object. If the account is not a tunnel,
455 the password is required."""
456 self
.repos
= repository
457 self
.config
= self
.repos
.getconfig()
458 usetunnel
= self
.repos
.getpreauthtunnel()
460 host
= self
.repos
.gethost()
461 user
= self
.repos
.getuser()
462 port
= self
.repos
.getport()
463 ssl
= self
.repos
.getssl()
464 sslclientcert
= self
.repos
.getsslclientcert()
465 sslclientkey
= self
.repos
.getsslclientkey()
466 reference
= self
.repos
.getreference()
467 idlefolders
= self
.repos
.getidlefolders()
471 if repository
.getname() in passwordhash
:
472 password
= passwordhash
[repository
.getname()]
474 # Connect to the remote server.
476 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
478 reference
= reference
,
479 idlefolders
= idlefolders
,
480 maxconnections
= self
.repos
.getmaxconnections())
483 password
= self
.repos
.getpassword()
484 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
485 user
, password
, host
, port
, ssl
,
486 self
.repos
.getmaxconnections(),
487 reference
= reference
,
488 idlefolders
= idlefolders
,
489 sslclientcert
= sslclientcert
,
490 sslclientkey
= sslclientkey
)