]>
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
26 class UsefulIMAPMixIn
:
29 def getselectedfolder(self
):
30 if self
.getstate() == 'SELECTED':
31 return self
.selectedfolder
34 def select(self
, mailbox
='INBOX', readonly
=None, force
= 0):
35 if (not force
) and self
.getselectedfolder() == mailbox \
36 and self
.is_readonly
== readonly
:
39 result
= self
.__class
__.__bases
__[1].select(self
, mailbox
, readonly
)
41 raise ValueError, "Error from select: %s" % str(result
)
42 if self
.getstate() == 'SELECTED':
43 self
.selectedfolder
= mailbox
45 self
.selectedfolder
= None
47 def _mesg(self
, s
, secs
=None):
48 imaplibutil
.new_mesg(self
, s
, secs
)
50 class UsefulIMAP4(UsefulIMAPMixIn
, imaplib
.IMAP4
):
51 def open(self
, host
= '', port
= imaplib
.IMAP4_PORT
):
52 imaplibutil
.new_open(self
, host
, port
)
54 class UsefulIMAP4_SSL(UsefulIMAPMixIn
, imaplib
.IMAP4_SSL
):
55 def open(self
, host
= '', port
= imaplib
.IMAP4_SSL_PORT
):
56 imaplibutil
.new_open_ssl(self
, host
, port
)
58 class UsefulIMAP4_Tunnel(UsefulIMAPMixIn
, imaplibutil
.IMAP4_Tunnel
): pass
61 def __init__(self
, config
, reposname
,
62 username
= None, password
= None, hostname
= None,
63 port
= None, ssl
= 1, maxconnections
= 1, tunnel
= None,
65 self
.reposname
= reposname
67 self
.username
= username
68 self
.password
= password
69 self
.passworderror
= None
70 self
.goodpassword
= None
71 self
.hostname
= hostname
82 self
.maxconnections
= maxconnections
83 self
.availableconnections
= []
84 self
.assignedconnections
= []
86 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
87 self
.connectionlock
= Lock()
88 self
.reference
= reference
90 def getpassword(self
):
91 if self
.goodpassword
!= None:
92 return self
.goodpassword
94 if self
.password
!= None and self
.passworderror
== None:
97 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
100 self
.passworderror
= None
105 """Returns this server's folder delimiter. Can only be called
106 after one or more calls to acquireconnection."""
110 """Returns this server's folder root. Can only be called after one
111 or more calls to acquireconnection."""
115 def releaseconnection(self
, connection
):
116 """Releases a connection, returning it to the pool."""
117 self
.connectionlock
.acquire()
118 self
.assignedconnections
.remove(connection
)
119 self
.availableconnections
.append(connection
)
120 self
.connectionlock
.release()
121 self
.semaphore
.release()
123 def md5handler(self
, response
):
124 ui
= UIBase
.getglobalui()
125 challenge
= response
.strip()
126 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
128 passwd
= self
.getpassword()
129 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
130 ui
.debug('imap', 'md5handler: returning %s' % retval
)
133 def plainauth(self
, imapobj
):
134 UIBase
.getglobalui().debug('imap',
135 'Attempting plain authentication')
136 imapobj
.login(self
.username
, self
.getpassword())
139 def acquireconnection(self
):
140 """Fetches a connection from the pool, making sure to create a new one
141 if needed, to obey the maximum connection limits, etc.
142 Opens a connection to the server and returns an appropriate
145 self
.semaphore
.acquire()
146 self
.connectionlock
.acquire()
149 if len(self
.availableconnections
): # One is available.
150 # Try to find one that previously belonged to this thread
151 # as an optimization. Start from the back since that's where
153 threadid
= thread
.get_ident()
155 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
156 tryobj
= self
.availableconnections
[i
]
157 if self
.lastowner
[tryobj
] == threadid
:
159 del(self
.availableconnections
[i
])
162 imapobj
= self
.availableconnections
[0]
163 del(self
.availableconnections
[0])
164 self
.assignedconnections
.append(imapobj
)
165 self
.lastowner
[imapobj
] = thread
.get_ident()
166 self
.connectionlock
.release()
169 self
.connectionlock
.release() # Release until need to modify data
173 # Generate a new connection.
175 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
176 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
179 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
180 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
)
182 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
183 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
185 imapobj
.mustquote
= imaplibutil
.mustquote
189 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
190 UIBase
.getglobalui().debug('imap',
191 'Attempting CRAM-MD5 authentication')
193 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
194 except imapobj
.error
, val
:
195 self
.plainauth(imapobj
)
197 self
.plainauth(imapobj
)
198 # Would bail by here if there was a failure.
200 self
.goodpassword
= self
.password
201 except imapobj
.error
, val
:
202 self
.passworderror
= str(val
)
205 if self
.delim
== None:
206 listres
= imapobj
.list(self
.reference
, '""')[1]
207 if listres
== [None] or listres
== None:
208 # Some buggy IMAP servers do not respond well to LIST "" ""
210 listres
= imapobj
.list(self
.reference
, '"*"')[1]
211 self
.delim
, self
.root
= \
212 imaputil
.imapsplit(listres
[0])[1:]
213 self
.delim
= imaputil
.dequote(self
.delim
)
214 self
.root
= imaputil
.dequote(self
.root
)
216 self
.connectionlock
.acquire()
217 self
.assignedconnections
.append(imapobj
)
218 self
.lastowner
[imapobj
] = thread
.get_ident()
219 self
.connectionlock
.release()
222 def connectionwait(self
):
223 """Waits until there is a connection available. Note that between
224 the time that a connection becomes available and the time it is
225 requested, another thread may have grabbed it. This function is
226 mainly present as a way to avoid spawning thousands of threads
227 to copy messages, then have them all wait for 3 available connections.
228 It's OK if we have maxconnections + 1 or 2 threads, which is what
229 this will help us do."""
230 threadutil
.semaphorewait(self
.semaphore
)
233 # Make sure I own all the semaphores. Let the threads finish
234 # their stuff. This is a blocking method.
235 self
.connectionlock
.acquire()
236 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
237 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
239 self
.assignedconnections
= []
240 self
.availableconnections
= []
242 self
.connectionlock
.release()
244 def keepalive(self
, timeout
, event
):
245 """Sends a NOOP to each connection recorded. It will wait a maximum
246 of timeout seconds between doing this, and will continue to do so
247 until the Event object as passed is true. This method is expected
248 to be invoked in a separate thread, which should be join()'d after
250 ui
= UIBase
.getglobalui()
251 ui
.debug('imap', 'keepalive thread started')
253 ui
.debug('imap', 'keepalive: top of loop')
255 ui
.debug('imap', 'keepalive: after wait')
257 ui
.debug('imap', 'keepalive: event is set; exiting')
259 ui
.debug('imap', 'keepalive: acquiring connectionlock')
260 self
.connectionlock
.acquire()
261 numconnections
= len(self
.assignedconnections
) + \
262 len(self
.availableconnections
)
263 self
.connectionlock
.release()
264 ui
.debug('imap', 'keepalive: connectionlock released')
268 for i
in range(numconnections
):
269 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
270 imapobj
= self
.acquireconnection()
271 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
272 imapobjs
.append(imapobj
)
273 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
277 ui
.debug('imap', 'keepalive: thread started')
279 ui
.debug('imap', 'keepalive: joining threads')
282 # Make sure all the commands have completed.
285 ui
.debug('imap', 'keepalive: releasing connections')
287 for imapobj
in imapobjs
:
288 self
.releaseconnection(imapobj
)
290 ui
.debug('imap', 'keepalive: bottom of loop')
292 class ConfigedIMAPServer(IMAPServer
):
293 """This class is designed for easier initialization given a ConfigParser
294 object and an account name. The passwordhash is used if
295 passwords for certain accounts are known. If the password for this
296 account is listed, it will be obtained from there."""
297 def __init__(self
, repository
, passwordhash
= {}):
298 """Initialize the object. If the account is not a tunnel,
299 the password is required."""
300 self
.repos
= repository
301 self
.config
= self
.repos
.getconfig()
302 usetunnel
= self
.repos
.getpreauthtunnel()
304 host
= self
.repos
.gethost()
305 user
= self
.repos
.getuser()
306 port
= self
.repos
.getport()
307 ssl
= self
.repos
.getssl()
308 reference
= self
.repos
.getreference()
312 if repository
.getname() in passwordhash
:
313 password
= passwordhash
[repository
.getname()]
315 # Connect to the remote server.
317 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
319 reference
= reference
,
320 maxconnections
= self
.repos
.getmaxconnections())
323 password
= self
.repos
.getpassword()
324 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
325 user
, password
, host
, port
, ssl
,
326 self
.repos
.getmaxconnections(),
327 reference
= reference
)