]> code.delx.au - offlineimap/blob - offlineimap/imapserver.py
Merging imaplibutil into code
[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 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_mseg(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, imaplib.IMAP4_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, imaplib.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.hostname = hostname
71 self.tunnel = tunnel
72 self.port = port
73 self.usessl = ssl
74 self.delim = None
75 self.root = None
76 if port == None:
77 if ssl:
78 self.port = 993
79 else:
80 self.port = 143
81 self.maxconnections = maxconnections
82 self.availableconnections = []
83 self.assignedconnections = []
84 self.lastowner = {}
85 self.semaphore = BoundedSemaphore(self.maxconnections)
86 self.connectionlock = Lock()
87 self.reference = reference
88
89 def getpassword(self):
90 if self.password != None and self.passworderror == None:
91 return self.password
92
93 self.password = UIBase.getglobalui().getpass(self.reposname,
94 self.config,
95 self.passworderror)
96 self.passworderror = None
97
98 return self.password
99
100 def getdelim(self):
101 """Returns this server's folder delimiter. Can only be called
102 after one or more calls to acquireconnection."""
103 return self.delim
104
105 def getroot(self):
106 """Returns this server's folder root. Can only be called after one
107 or more calls to acquireconnection."""
108 return self.root
109
110
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()
117
118 def md5handler(self, response):
119 ui = UIBase.getglobalui()
120 challenge = response.strip()
121 ui.debug('imap', 'md5handler: got challenge %s' % challenge)
122
123 passwd = self.getpassword()
124 retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest()
125 ui.debug('imap', 'md5handler: returning %s' % retval)
126 return retval
127
128 def plainauth(self, imapobj):
129 UIBase.getglobalui().debug('imap',
130 'Attempting plain authentication')
131 imapobj.login(self.username, self.getpassword())
132
133
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
138 object."""
139
140 self.semaphore.acquire()
141 self.connectionlock.acquire()
142 imapobj = None
143
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
147 # they're popped on.
148 threadid = thread.get_ident()
149 imapobj = None
150 for i in range(len(self.availableconnections) - 1, -1, -1):
151 tryobj = self.availableconnections[i]
152 if self.lastowner[tryobj] == threadid:
153 imapobj = tryobj
154 del(self.availableconnections[i])
155 break
156 if not imapobj:
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()
162 return imapobj
163
164 self.connectionlock.release() # Release until need to modify data
165
166 success = 0
167 while not success:
168 # Generate a new connection.
169 if self.tunnel:
170 UIBase.getglobalui().connecting('tunnel', self.tunnel)
171 imapobj = UsefulIMAP4_Tunnel(self.tunnel)
172 success = 1
173 elif self.usessl:
174 UIBase.getglobalui().connecting(self.hostname, self.port)
175 imapobj = UsefulIMAP4_SSL(self.hostname, self.port)
176 else:
177 UIBase.getglobalui().connecting(self.hostname, self.port)
178 imapobj = UsefulIMAP4(self.hostname, self.port)
179
180 imapobj.mustquote = imaplibutil.mustquote
181
182 if not self.tunnel:
183 try:
184 if 'AUTH=CRAM-MD5' in imapobj.capabilities:
185 UIBase.getglobalui().debug('imap',
186 'Attempting CRAM-MD5 authentication')
187 try:
188 imapobj.authenticate('CRAM-MD5', self.md5handler)
189 except imapobj.error, val:
190 self.plainauth(imapobj)
191 else:
192 self.plainauth(imapobj)
193 # Would bail by here if there was a failure.
194 success = 1
195 except imapobj.error, val:
196 self.passworderror = str(val)
197 self.password = None
198
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 "" ""
203 # Work around them.
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)
209
210 self.connectionlock.acquire()
211 self.assignedconnections.append(imapobj)
212 self.lastowner[imapobj] = thread.get_ident()
213 self.connectionlock.release()
214 return imapobj
215
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)
225
226 def close(self):
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:
232 imapobj.logout()
233 self.assignedconnections = []
234 self.availableconnections = []
235 self.lastowner = {}
236 self.connectionlock.release()
237
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
243 the event is set."""
244 ui = UIBase.getglobalui()
245 ui.debug('imap', 'keepalive thread started')
246 while 1:
247 ui.debug('imap', 'keepalive: top of loop')
248 event.wait(timeout)
249 ui.debug('imap', 'keepalive: after wait')
250 if event.isSet():
251 ui.debug('imap', 'keepalive: event is set; exiting')
252 return
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')
259 threads = []
260 imapobjs = []
261
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)
268 thr.setDaemon(1)
269 thr.start()
270 threads.append(thr)
271 ui.debug('imap', 'keepalive: thread started')
272
273 ui.debug('imap', 'keepalive: joining threads')
274
275 for thr in threads:
276 # Make sure all the commands have completed.
277 thr.join()
278
279 ui.debug('imap', 'keepalive: releasing connections')
280
281 for imapobj in imapobjs:
282 self.releaseconnection(imapobj)
283
284 ui.debug('imap', 'keepalive: bottom of loop')
285
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()
297 if not usetunnel:
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()
303 server = None
304 password = None
305
306 if repository.getname() in passwordhash:
307 password = passwordhash[repository.getname()]
308
309 # Connect to the remote server.
310 if usetunnel:
311 IMAPServer.__init__(self, self.config, self.repos.getname(),
312 tunnel = usetunnel,
313 reference = reference,
314 maxconnections = self.repos.getmaxconnections())
315 else:
316 if not password:
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)