]>
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
.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 """Releases a connection, returning it to the pool."""
113 self
.connectionlock
.acquire()
114 self
.assignedconnections
.remove(connection
)
115 self
.availableconnections
.append(connection
)
116 self
.connectionlock
.release()
117 self
.semaphore
.release()
119 def md5handler(self
, response
):
120 ui
= UIBase
.getglobalui()
121 challenge
= response
.strip()
122 ui
.debug('imap', 'md5handler: got challenge %s' % challenge
)
124 passwd
= self
.getpassword()
125 retval
= self
.username
+ ' ' + hmac
.new(passwd
, challenge
).hexdigest()
126 ui
.debug('imap', 'md5handler: returning %s' % retval
)
129 def plainauth(self
, imapobj
):
130 UIBase
.getglobalui().debug('imap',
131 'Attempting plain authentication')
132 imapobj
.login(self
.username
, self
.getpassword())
135 def acquireconnection(self
):
136 """Fetches a connection from the pool, making sure to create a new one
137 if needed, to obey the maximum connection limits, etc.
138 Opens a connection to the server and returns an appropriate
141 self
.semaphore
.acquire()
142 self
.connectionlock
.acquire()
145 if len(self
.availableconnections
): # One is available.
146 # Try to find one that previously belonged to this thread
147 # as an optimization. Start from the back since that's where
149 threadid
= thread
.get_ident()
151 for i
in range(len(self
.availableconnections
) - 1, -1, -1):
152 tryobj
= self
.availableconnections
[i
]
153 if self
.lastowner
[tryobj
] == threadid
:
155 del(self
.availableconnections
[i
])
158 imapobj
= self
.availableconnections
[0]
159 del(self
.availableconnections
[0])
160 self
.assignedconnections
.append(imapobj
)
161 self
.lastowner
[imapobj
] = thread
.get_ident()
162 self
.connectionlock
.release()
165 self
.connectionlock
.release() # Release until need to modify data
169 # Generate a new connection.
171 UIBase
.getglobalui().connecting('tunnel', self
.tunnel
)
172 imapobj
= UsefulIMAP4_Tunnel(self
.tunnel
)
175 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
176 imapobj
= UsefulIMAP4_SSL(self
.hostname
, self
.port
)
178 UIBase
.getglobalui().connecting(self
.hostname
, self
.port
)
179 imapobj
= UsefulIMAP4(self
.hostname
, self
.port
)
181 imapobj
.mustquote
= imaplibutil
.mustquote
185 if 'AUTH=CRAM-MD5' in imapobj
.capabilities
:
186 UIBase
.getglobalui().debug('imap',
187 'Attempting CRAM-MD5 authentication')
189 imapobj
.authenticate('CRAM-MD5', self
.md5handler
)
190 except imapobj
.error
, val
:
191 self
.plainauth(imapobj
)
193 self
.plainauth(imapobj
)
194 # Would bail by here if there was a failure.
196 except imapobj
.error
, val
:
197 self
.passworderror
= str(val
)
200 if self
.delim
== None:
201 listres
= imapobj
.list(self
.reference
, '""')[1]
202 if listres
== [None] or listres
== None:
203 # Some buggy IMAP servers do not respond well to LIST "" ""
205 listres
= imapobj
.list(self
.reference
, '"*"')[1]
206 self
.delim
, self
.root
= \
207 imaputil
.imapsplit(listres
[0])[1:]
208 self
.delim
= imaputil
.dequote(self
.delim
)
209 self
.root
= imaputil
.dequote(self
.root
)
211 self
.connectionlock
.acquire()
212 self
.assignedconnections
.append(imapobj
)
213 self
.lastowner
[imapobj
] = thread
.get_ident()
214 self
.connectionlock
.release()
217 def connectionwait(self
):
218 """Waits until there is a connection available. Note that between
219 the time that a connection becomes available and the time it is
220 requested, another thread may have grabbed it. This function is
221 mainly present as a way to avoid spawning thousands of threads
222 to copy messages, then have them all wait for 3 available connections.
223 It's OK if we have maxconnections + 1 or 2 threads, which is what
224 this will help us do."""
225 threadutil
.semaphorewait(self
.semaphore
)
228 # Make sure I own all the semaphores. Let the threads finish
229 # their stuff. This is a blocking method.
230 self
.connectionlock
.acquire()
231 threadutil
.semaphorereset(self
.semaphore
, self
.maxconnections
)
232 for imapobj
in self
.assignedconnections
+ self
.availableconnections
:
234 self
.assignedconnections
= []
235 self
.availableconnections
= []
237 self
.connectionlock
.release()
239 def keepalive(self
, timeout
, event
):
240 """Sends a NOOP to each connection recorded. It will wait a maximum
241 of timeout seconds between doing this, and will continue to do so
242 until the Event object as passed is true. This method is expected
243 to be invoked in a separate thread, which should be join()'d after
245 ui
= UIBase
.getglobalui()
246 ui
.debug('imap', 'keepalive thread started')
248 ui
.debug('imap', 'keepalive: top of loop')
250 ui
.debug('imap', 'keepalive: after wait')
252 ui
.debug('imap', 'keepalive: event is set; exiting')
254 ui
.debug('imap', 'keepalive: acquiring connectionlock')
255 self
.connectionlock
.acquire()
256 numconnections
= len(self
.assignedconnections
) + \
257 len(self
.availableconnections
)
258 self
.connectionlock
.release()
259 ui
.debug('imap', 'keepalive: connectionlock released')
263 for i
in range(numconnections
):
264 ui
.debug('imap', 'keepalive: processing connection %d of %d' % (i
, numconnections
))
265 imapobj
= self
.acquireconnection()
266 ui
.debug('imap', 'keepalive: connection %d acquired' % i
)
267 imapobjs
.append(imapobj
)
268 thr
= threadutil
.ExitNotifyThread(target
= imapobj
.noop
)
272 ui
.debug('imap', 'keepalive: thread started')
274 ui
.debug('imap', 'keepalive: joining threads')
277 # Make sure all the commands have completed.
280 ui
.debug('imap', 'keepalive: releasing connections')
282 for imapobj
in imapobjs
:
283 self
.releaseconnection(imapobj
)
285 ui
.debug('imap', 'keepalive: bottom of loop')
287 class ConfigedIMAPServer(IMAPServer
):
288 """This class is designed for easier initialization given a ConfigParser
289 object and an account name. The passwordhash is used if
290 passwords for certain accounts are known. If the password for this
291 account is listed, it will be obtained from there."""
292 def __init__(self
, repository
, passwordhash
= {}):
293 """Initialize the object. If the account is not a tunnel,
294 the password is required."""
295 self
.repos
= repository
296 self
.config
= self
.repos
.getconfig()
297 usetunnel
= self
.repos
.getpreauthtunnel()
299 host
= self
.repos
.gethost()
300 user
= self
.repos
.getuser()
301 port
= self
.repos
.getport()
302 ssl
= self
.repos
.getssl()
303 reference
= self
.repos
.getreference()
307 if repository
.getname() in passwordhash
:
308 password
= passwordhash
[repository
.getname()]
310 # Connect to the remote server.
312 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
314 reference
= reference
,
315 maxconnections
= self
.repos
.getmaxconnections())
318 password
= self
.repos
.getpassword()
319 IMAPServer
.__init
__(self
, self
.config
, self
.repos
.getname(),
320 user
, password
, host
, port
, ssl
,
321 self
.repos
.getmaxconnections(),
322 reference
= reference
)