]>
code.delx.au - offlineimap/blob - offlineimap/imapserver.py
2 # Copyright (C) 2002, 2003 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 imaplib
, imaputil
, threadutil
20 from offlineimap
.ui
import UIBase
21 from threading
import *
22 import thread
, hmac
, os
25 class UsefulIMAPMixIn
:
28 def getselectedfolder(self
):
29 if self
.getstate() == 'SELECTED':
30 return self
.selectedfolder
33 def select(self
, mailbox
='INBOX', readonly
=None, force
= 0):
34 if (not force
) and self
.getselectedfolder() == mailbox
:
35 self
.is_readonly
= readonly
38 result
= self
.__class
__.__bases
__[1].select(self
, mailbox
, readonly
)
40 raise ValueError, "Error from select: %s" % str(result
)
41 if self
.getstate() == 'SELECTED':
42 self
.selectedfolder
= mailbox
44 self
.selectedfolder
= None
46 class UsefulIMAP4(UsefulIMAPMixIn
, imaplib
.IMAP4
): pass
47 class UsefulIMAP4_SSL(UsefulIMAPMixIn
, imaplib
.IMAP4_SSL
): pass
48 class UsefulIMAP4_Tunnel(UsefulIMAPMixIn
, imaplib
.IMAP4_Tunnel
): pass
51 def __init__(self
, config
, reposname
,
52 username
= None, password
= None, hostname
= None,
53 port
= None, ssl
= 1, maxconnections
= 1, tunnel
= None,
55 self
.reposname
= reposname
57 self
.username
= username
58 self
.password
= password
59 self
.passworderror
= None
60 self
.hostname
= hostname
71 self
.maxconnections
= maxconnections
72 self
.availableconnections
= []
73 self
.assignedconnections
= []
75 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
76 self
.connectionlock
= Lock()
77 self
.reference
= reference
79 def getpassword(self
):
80 if self
.password
!= None and self
.passworderror
== None:
83 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
86 self
.passworderror
= None
91 """Returns this server's folder delimiter. Can only be called
92 after one or more calls to acquireconnection."""
96 """Returns this server's folder root. Can only be called after one
97 or more calls to acquireconnection."""
101 def releaseconnection(self
, connection
):
102 self
.connectionlock
.acquire()
103 self
.assignedconnections
.remove(connection
)
104 self
.availableconnections
.append(connection
)
105 self
.connectionlock
.release()
106 self
.semaphore
.release()
108 def md5handler(self
, response
):
109 ui
= UIBase
.getglobalui()
110 challenge
= response
.strip()
111 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
113 passwd
= self
.getpassword()
114 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
115 ui
.debug('imap', 'md5handler: returning %s' % retval
)
118 def plainauth(self
, imapobj
):
119 UIBase
.getglobalui().debug('imap',
120 'Attempting plain authentication')
121 imapobj
.login(self
.username
, self
.getpassword())
124 def acquireconnection(self
):
125 """Fetches a connection from the pool, making sure to create a new one
126 if needed, to obey the maximum connection limits, etc.
127 Opens a connection to the server and returns an appropriate
130 self
.semaphore
.acquire()
131 self
.connectionlock
.acquire()
134 if len(self
.availableconnections
): # One is available.
135 # Try to find one that previously belonged to this thread
136 # as an optimization. Start from the back since that's where
138 threadid
= thread
.get_ident()
140 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
141 tryobj
= self
.availableconnections
[i
]
142 if self
.lastowner
[tryobj
] == threadid
:
144 del(self
.availableconnections
[i
])
147 imapobj
= self
.availableconnections
[0]
148 del(self
.availableconnections
[0])
149 self
.assignedconnections
.append(imapobj
)
150 self
.lastowner
[imapobj
] = thread
.get_ident()
151 self
.connectionlock
.release()
154 self
.connectionlock
.release() # Release until need to modify data
158 # Generate a new connection.
160 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
161 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
164 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
165 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
)
167 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
168 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
172 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
173 UIBase
.getglobalui().debug('imap',
174 'Attempting CRAM-MD5 authentication')
176 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
177 except imapobj
.error
, val
:
178 self
.plainauth(imapobj
)
180 self
.plainauth(imapobj
)
181 # Would bail by here if there was a failure.
183 except imapobj
.error
, val
:
184 self
.passworderror
= str(val
)
187 if self
.delim
== None:
188 listres
= imapobj
.list(self
.reference
, '""')[1]
189 if listres
== [None] or listres
== None:
190 # Some buggy IMAP servers do not respond well to LIST "" ""
192 listres
= imapobj
.list(self
.reference
, '"*"')[1]
193 self
.delim
, self
.root
= \
194 imaputil
.imapsplit(listres
[0])[1:]
195 self
.delim
= imaputil
.dequote(self
.delim
)
196 self
.root
= imaputil
.dequote(self
.root
)
198 self
.connectionlock
.acquire()
199 self
.assignedconnections
.append(imapobj
)
200 self
.lastowner
[imapobj
] = thread
.get_ident()
201 self
.connectionlock
.release()
204 def connectionwait(self
):
205 """Waits until there is a connection available. Note that between
206 the time that a connection becomes available and the time it is
207 requested, another thread may have grabbed it. This function is
208 mainly present as a way to avoid spawning thousands of threads
209 to copy messages, then have them all wait for 3 available connections.
210 It's OK if we have maxconnections + 1 or 2 threads, which is what
211 this will help us do."""
212 threadutil
.semaphorewait(self
.semaphore
)
215 # Make sure I own all the semaphores. Let the threads finish
216 # their stuff. This is a blocking method.
217 self
.connectionlock
.acquire()
218 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
219 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
221 self
.assignedconnections
= []
222 self
.availableconnections
= []
224 self
.connectionlock
.release()
226 def keepalive(self
, timeout
, event
):
227 """Sends a NOOP to each connection recorded. It will wait a maximum
228 of timeout seconds between doing this, and will continue to do so
229 until the Event object as passed is true. This method is expected
230 to be invoked in a separate thread, which should be join()'d after
232 ui
= UIBase
.getglobalui()
233 ui
.debug('imap', 'keepalive thread started')
235 ui
.debug('imap', 'keepalive: top of loop')
237 ui
.debug('imap', 'keepalive: after wait')
239 ui
.debug('imap', 'keepalive: event is set; exiting')
241 ui
.debug('imap', 'keepalive: acquiring connectionlock')
242 self
.connectionlock
.acquire()
243 numconnections
= len(self
.assignedconnections
) + \
244 len(self
.availableconnections
)
245 self
.connectionlock
.release()
246 ui
.debug('imap', 'keepalive: connectionlock released')
250 for i
in range(numconnections
):
251 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
252 imapobj
= self
.acquireconnection()
253 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
254 imapobjs
.append(imapobj
)
255 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
259 ui
.debug('imap', 'keepalive: thread started')
261 ui
.debug('imap', 'keepalive: joining threads')
264 # Make sure all the commands have completed.
267 ui
.debug('imap', 'keepalive: releasing connections')
269 for imapobj
in imapobjs
:
270 self
.releaseconnection(imapobj
)
272 ui
.debug('imap', 'keepalive: bottom of loop')
274 class ConfigedIMAPServer(IMAPServer
):
275 """This class is designed for easier initialization given a ConfigParser
276 object and an account name. The passwordhash is used if
277 passwords for certain accounts are known. If the password for this
278 account is listed, it will be obtained from there."""
279 def __init__(self
, repository
, passwordhash
= {}):
280 """Initialize the object. If the account is not a tunnel,
281 the password is required."""
282 self
.repos
= repository
283 self
.config
= self
.repos
.getconfig()
284 usetunnel
= self
.repos
.getpreauthtunnel()
286 host
= self
.repos
.gethost()
287 user
= self
.repos
.getuser()
288 port
= self
.repos
.getport()
289 ssl
= self
.repos
.getssl()
290 reference
= self
.repos
.getreference()
294 if repository
.getname() in passwordhash
:
295 password
= passwordhash
[repository
.getname()]
297 # Connect to the remote server.
299 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
301 reference
= reference
,
302 maxconnections
= self
.repos
.getmaxconnections())
305 password
= self
.repos
.getpassword()
306 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
307 user
, password
, host
, port
, ssl
,
308 self
.repos
.getmaxconnections(),
309 reference
= reference
)