]>
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 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_mseg(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
, imaplib
.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
.hostname
= hostname
81 self
.maxconnections
= maxconnections
82 self
.availableconnections
= []
83 self
.assignedconnections
= []
85 self
.semaphore
= BoundedSemaphore(self
.maxconnections
)
86 self
.connectionlock
= Lock()
87 self
.reference
= reference
89 def getpassword(self
):
90 if self
.password
!= None and self
.passworderror
== None:
93 self
.password
= UIBase
.getglobalui().getpass(self
.reposname
,
96 self
.passworderror
= None
101 """Returns this server's folder delimiter. Can only be called
102 after one or more calls to acquireconnection."""
106 """Returns this server's folder root. Can only be called after one
107 or more calls to acquireconnection."""
111 def releaseconnection(self
, connection
):
112 self
.connectionlock
.acquire()
113 self
.assignedconnections
.remove(connection
)
114 self
.availableconnections
.append(connection
)
115 self
.connectionlock
.release()
116 self
.semaphore
.release()
118 def md5handler(self
, response
):
119 ui
= UIBase
.getglobalui()
120 challenge
= response
.strip()
121 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
123 passwd
= self
.getpassword()
124 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
125 ui
.debug('imap', 'md5handler: returning %s' % retval
)
128 def plainauth(self
, imapobj
):
129 UIBase
.getglobalui().debug('imap',
130 'Attempting plain authentication')
131 imapobj
.login(self
.username
, self
.getpassword())
134 def acquireconnection(self
):
135 """Fetches a connection from the pool, making sure to create a new one
136 if needed, to obey the maximum connection limits, etc.
137 Opens a connection to the server and returns an appropriate
140 self
.semaphore
.acquire()
141 self
.connectionlock
.acquire()
144 if len(self
.availableconnections
): # One is available.
145 # Try to find one that previously belonged to this thread
146 # as an optimization. Start from the back since that's where
148 threadid
= thread
.get_ident()
150 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
151 tryobj
= self
.availableconnections
[i
]
152 if self
.lastowner
[tryobj
] == threadid
:
154 del(self
.availableconnections
[i
])
157 imapobj
= self
.availableconnections
[0]
158 del(self
.availableconnections
[0])
159 self
.assignedconnections
.append(imapobj
)
160 self
.lastowner
[imapobj
] = thread
.get_ident()
161 self
.connectionlock
.release()
164 self
.connectionlock
.release() # Release until need to modify data
168 # Generate a new connection.
170 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
171 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
174 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
175 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
)
177 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
178 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
180 imapobj
.mustquote
= imaplibutil
.mustquote
184 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
185 UIBase
.getglobalui().debug('imap',
186 'Attempting CRAM-MD5 authentication')
188 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
189 except imapobj
.error
, val
:
190 self
.plainauth(imapobj
)
192 self
.plainauth(imapobj
)
193 # Would bail by here if there was a failure.
195 except imapobj
.error
, val
:
196 self
.passworderror
= str(val
)
199 if self
.delim
== None:
200 listres
= imapobj
.list(self
.reference
, '""')[1]
201 if listres
== [None] or listres
== None:
202 # Some buggy IMAP servers do not respond well to LIST "" ""
204 listres
= imapobj
.list(self
.reference
, '"*"')[1]
205 self
.delim
, self
.root
= \
206 imaputil
.imapsplit(listres
[0])[1:]
207 self
.delim
= imaputil
.dequote(self
.delim
)
208 self
.root
= imaputil
.dequote(self
.root
)
210 self
.connectionlock
.acquire()
211 self
.assignedconnections
.append(imapobj
)
212 self
.lastowner
[imapobj
] = thread
.get_ident()
213 self
.connectionlock
.release()
216 def connectionwait(self
):
217 """Waits until there is a connection available. Note that between
218 the time that a connection becomes available and the time it is
219 requested, another thread may have grabbed it. This function is
220 mainly present as a way to avoid spawning thousands of threads
221 to copy messages, then have them all wait for 3 available connections.
222 It's OK if we have maxconnections + 1 or 2 threads, which is what
223 this will help us do."""
224 threadutil
.semaphorewait(self
.semaphore
)
227 # Make sure I own all the semaphores. Let the threads finish
228 # their stuff. This is a blocking method.
229 self
.connectionlock
.acquire()
230 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
231 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
233 self
.assignedconnections
= []
234 self
.availableconnections
= []
236 self
.connectionlock
.release()
238 def keepalive(self
, timeout
, event
):
239 """Sends a NOOP to each connection recorded. It will wait a maximum
240 of timeout seconds between doing this, and will continue to do so
241 until the Event object as passed is true. This method is expected
242 to be invoked in a separate thread, which should be join()'d after
244 ui
= UIBase
.getglobalui()
245 ui
.debug('imap', 'keepalive thread started')
247 ui
.debug('imap', 'keepalive: top of loop')
249 ui
.debug('imap', 'keepalive: after wait')
251 ui
.debug('imap', 'keepalive: event is set; exiting')
253 ui
.debug('imap', 'keepalive: acquiring connectionlock')
254 self
.connectionlock
.acquire()
255 numconnections
= len(self
.assignedconnections
) + \
256 len(self
.availableconnections
)
257 self
.connectionlock
.release()
258 ui
.debug('imap', 'keepalive: connectionlock released')
262 for i
in range(numconnections
):
263 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
264 imapobj
= self
.acquireconnection()
265 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
266 imapobjs
.append(imapobj
)
267 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
271 ui
.debug('imap', 'keepalive: thread started')
273 ui
.debug('imap', 'keepalive: joining threads')
276 # Make sure all the commands have completed.
279 ui
.debug('imap', 'keepalive: releasing connections')
281 for imapobj
in imapobjs
:
282 self
.releaseconnection(imapobj
)
284 ui
.debug('imap', 'keepalive: bottom of loop')
286 class ConfigedIMAPServer(IMAPServer
):
287 """This class is designed for easier initialization given a ConfigParser
288 object and an account name. The passwordhash is used if
289 passwords for certain accounts are known. If the password for this
290 account is listed, it will be obtained from there."""
291 def __init__(self
, repository
, passwordhash
= {}):
292 """Initialize the object. If the account is not a tunnel,
293 the password is required."""
294 self
.repos
= repository
295 self
.config
= self
.repos
.getconfig()
296 usetunnel
= self
.repos
.getpreauthtunnel()
298 host
= self
.repos
.gethost()
299 user
= self
.repos
.getuser()
300 port
= self
.repos
.getport()
301 ssl
= self
.repos
.getssl()
302 reference
= self
.repos
.getreference()
306 if repository
.getname() in passwordhash
:
307 password
= passwordhash
[repository
.getname()]
309 # Connect to the remote server.
311 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
313 reference
= reference
,
314 maxconnections
= self
.repos
.getmaxconnections())
317 password
= self
.repos
.getpassword()
318 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
319 user
, password
, host
, port
, ssl
,
320 self
.repos
.getmaxconnections(),
321 reference
= reference
)