]> code.delx.au - offlineimap/blob - offlineimap/imapserver.py
Attempt to fix a crashing bug in Gmail driver
[offlineimap] / offlineimap / imapserver.py
1 # IMAP server support
2 # Copyright (C) 2002 - 2007 John Goerzen
3 # <jgoerzen@complete.org>
4 #
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.
9 #
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.
14 #
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
18
19 import imaplib
20 from offlineimap import imaplibutil, imaputil, threadutil
21 from offlineimap.ui import UIBase
22 from threading import *
23 import thread, hmac, os
24
25
26 class UsefulIMAPMixIn:
27 def getstate(self):
28 return self.state
29 def getselectedfolder(self):
30 if self.getstate() == 'SELECTED':
31 return self.selectedfolder
32 return None
33
34 def select(self, mailbox='INBOX', readonly=None, force = 0):
35 if (not force) and self.getselectedfolder() == mailbox \
36 and self.is_readonly == readonly:
37 # No change; return.
38 return
39 result = self.__class__.__bases__[1].select(self, mailbox, readonly)
40 if result[0] != 'OK':
41 raise ValueError, "Error from select: %s" % str(result)
42 if self.getstate() == 'SELECTED':
43 self.selectedfolder = mailbox
44 else:
45 self.selectedfolder = None
46
47 def _mesg(self, s, secs=None):
48 imaplibutil.new_mesg(self, s, secs)
49
50 class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4):
51 def open(self, host = '', port = imaplib.IMAP4_PORT):
52 imaplibutil.new_open(self, host, port)
53
54 class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL):
55 def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
56 imaplibutil.new_open_ssl(self, host, port)
57
58 class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplibutil.IMAP4_Tunnel): pass
59
60 class IMAPServer:
61 def __init__(self, config, reposname,
62 username = None, password = None, hostname = None,
63 port = None, ssl = 1, maxconnections = 1, tunnel = None,
64 reference = '""'):
65 self.reposname = reposname
66 self.config = config
67 self.username = username
68 self.password = password
69 self.passworderror = None
70 self.goodpassword = None
71 self.hostname = hostname
72 self.tunnel = tunnel
73 self.port = port
74 self.usessl = ssl
75 self.delim = None
76 self.root = None
77 if port == None:
78 if ssl:
79 self.port = 993
80 else:
81 self.port = 143
82 self.maxconnections = maxconnections
83 self.availableconnections = []
84 self.assignedconnections = []
85 self.lastowner = {}
86 self.semaphore = BoundedSemaphore(self.maxconnections)
87 self.connectionlock = Lock()
88 self.reference = reference
89
90 def getpassword(self):
91 if self.goodpassword != None:
92 return self.goodpassword
93
94 if self.password != None and self.passworderror == None:
95 return self.password
96
97 self.password = UIBase.getglobalui().getpass(self.reposname,
98 self.config,
99 self.passworderror)
100 self.passworderror = None
101
102 return self.password
103
104 def getdelim(self):
105 """Returns this server's folder delimiter. Can only be called
106 after one or more calls to acquireconnection."""
107 return self.delim
108
109 def getroot(self):
110 """Returns this server's folder root. Can only be called after one
111 or more calls to acquireconnection."""
112 return self.root
113
114
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()
122
123 def md5handler(self, response):
124 ui = UIBase.getglobalui()
125 challenge = response.strip()
126 ui.debug('imap', 'md5handler: got challenge %s' % challenge)
127
128 passwd = self.getpassword()
129 retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest()
130 ui.debug('imap', 'md5handler: returning %s' % retval)
131 return retval
132
133 def plainauth(self, imapobj):
134 UIBase.getglobalui().debug('imap',
135 'Attempting plain authentication')
136 imapobj.login(self.username, self.getpassword())
137
138
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
143 object."""
144
145 self.semaphore.acquire()
146 self.connectionlock.acquire()
147 imapobj = None
148
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
152 # they're popped on.
153 threadid = thread.get_ident()
154 imapobj = None
155 for i in range(len(self.availableconnections) - 1, -1, -1):
156 tryobj = self.availableconnections[i]
157 if self.lastowner[tryobj] == threadid:
158 imapobj = tryobj
159 del(self.availableconnections[i])
160 break
161 if not imapobj:
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()
167 return imapobj
168
169 self.connectionlock.release() # Release until need to modify data
170
171 success = 0
172 while not success:
173 # Generate a new connection.
174 if self.tunnel:
175 UIBase.getglobalui().connecting('tunnel', self.tunnel)
176 imapobj = UsefulIMAP4_Tunnel(self.tunnel)
177 success = 1
178 elif self.usessl:
179 UIBase.getglobalui().connecting(self.hostname, self.port)
180 imapobj = UsefulIMAP4_SSL(self.hostname, self.port)
181 else:
182 UIBase.getglobalui().connecting(self.hostname, self.port)
183 imapobj = UsefulIMAP4(self.hostname, self.port)
184
185 imapobj.mustquote = imaplibutil.mustquote
186
187 if not self.tunnel:
188 try:
189 if 'AUTH=CRAM-MD5' in imapobj.capabilities:
190 UIBase.getglobalui().debug('imap',
191 'Attempting CRAM-MD5 authentication')
192 try:
193 imapobj.authenticate('CRAM-MD5', self.md5handler)
194 except imapobj.error, val:
195 self.plainauth(imapobj)
196 else:
197 self.plainauth(imapobj)
198 # Would bail by here if there was a failure.
199 success = 1
200 self.goodpassword = self.password
201 except imapobj.error, val:
202 self.passworderror = str(val)
203 self.password = None
204
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 "" ""
209 # Work around them.
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)
215
216 self.connectionlock.acquire()
217 self.assignedconnections.append(imapobj)
218 self.lastowner[imapobj] = thread.get_ident()
219 self.connectionlock.release()
220 return imapobj
221
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)
231
232 def close(self):
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:
238 imapobj.logout()
239 self.assignedconnections = []
240 self.availableconnections = []
241 self.lastowner = {}
242 self.connectionlock.release()
243
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
249 the event is set."""
250 ui = UIBase.getglobalui()
251 ui.debug('imap', 'keepalive thread started')
252 while 1:
253 ui.debug('imap', 'keepalive: top of loop')
254 event.wait(timeout)
255 ui.debug('imap', 'keepalive: after wait')
256 if event.isSet():
257 ui.debug('imap', 'keepalive: event is set; exiting')
258 return
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')
265 threads = []
266 imapobjs = []
267
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)
274 thr.setDaemon(1)
275 thr.start()
276 threads.append(thr)
277 ui.debug('imap', 'keepalive: thread started')
278
279 ui.debug('imap', 'keepalive: joining threads')
280
281 for thr in threads:
282 # Make sure all the commands have completed.
283 thr.join()
284
285 ui.debug('imap', 'keepalive: releasing connections')
286
287 for imapobj in imapobjs:
288 self.releaseconnection(imapobj)
289
290 ui.debug('imap', 'keepalive: bottom of loop')
291
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()
303 if not usetunnel:
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()
309 server = None
310 password = None
311
312 if repository.getname() in passwordhash:
313 password = passwordhash[repository.getname()]
314
315 # Connect to the remote server.
316 if usetunnel:
317 IMAPServer.__init__(self, self.config, self.repos.getname(),
318 tunnel = usetunnel,
319 reference = reference,
320 maxconnections = self.repos.getmaxconnections())
321 else:
322 if not password:
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)