]> code.delx.au - offlineimap/blob - offlineimap/imapserver.py
Update FSF address
[offlineimap] / offlineimap / imapserver.py
1 # IMAP server support
2 # Copyright (C) 2002, 2003 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 from offlineimap import imaplib, imaputil, threadutil
20 from offlineimap.ui import UIBase
21 from threading import *
22 import thread, hmac, os
23
24
25 class UsefulIMAPMixIn:
26 def getstate(self):
27 return self.state
28 def getselectedfolder(self):
29 if self.getstate() == 'SELECTED':
30 return self.selectedfolder
31 return None
32
33 def select(self, mailbox='INBOX', readonly=None, force = 0):
34 if (not force) and self.getselectedfolder() == mailbox:
35 self.is_readonly = readonly
36 # No change; return.
37 return
38 result = self.__class__.__bases__[1].select(self, mailbox, readonly)
39 if result[0] != 'OK':
40 raise ValueError, "Error from select: %s" % str(result)
41 if self.getstate() == 'SELECTED':
42 self.selectedfolder = mailbox
43 else:
44 self.selectedfolder = None
45
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
49
50 class IMAPServer:
51 def __init__(self, config, reposname,
52 username = None, password = None, hostname = None,
53 port = None, ssl = 1, maxconnections = 1, tunnel = None,
54 reference = '""'):
55 self.reposname = reposname
56 self.config = config
57 self.username = username
58 self.password = password
59 self.passworderror = None
60 self.hostname = hostname
61 self.tunnel = tunnel
62 self.port = port
63 self.usessl = ssl
64 self.delim = None
65 self.root = None
66 if port == None:
67 if ssl:
68 self.port = 993
69 else:
70 self.port = 143
71 self.maxconnections = maxconnections
72 self.availableconnections = []
73 self.assignedconnections = []
74 self.lastowner = {}
75 self.semaphore = BoundedSemaphore(self.maxconnections)
76 self.connectionlock = Lock()
77 self.reference = reference
78
79 def getpassword(self):
80 if self.password != None and self.passworderror == None:
81 return self.password
82
83 self.password = UIBase.getglobalui().getpass(self.reposname,
84 self.config,
85 self.passworderror)
86 self.passworderror = None
87
88 return self.password
89
90 def getdelim(self):
91 """Returns this server's folder delimiter. Can only be called
92 after one or more calls to acquireconnection."""
93 return self.delim
94
95 def getroot(self):
96 """Returns this server's folder root. Can only be called after one
97 or more calls to acquireconnection."""
98 return self.root
99
100
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()
107
108 def md5handler(self, response):
109 ui = UIBase.getglobalui()
110 challenge = response.strip()
111 ui.debug('imap', 'md5handler: got challenge %s' % challenge)
112
113 passwd = self.getpassword()
114 retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest()
115 ui.debug('imap', 'md5handler: returning %s' % retval)
116 return retval
117
118 def plainauth(self, imapobj):
119 UIBase.getglobalui().debug('imap',
120 'Attempting plain authentication')
121 imapobj.login(self.username, self.getpassword())
122
123
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
128 object."""
129
130 self.semaphore.acquire()
131 self.connectionlock.acquire()
132 imapobj = None
133
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
137 # they're popped on.
138 threadid = thread.get_ident()
139 imapobj = None
140 for i in range(len(self.availableconnections) - 1, -1, -1):
141 tryobj = self.availableconnections[i]
142 if self.lastowner[tryobj] == threadid:
143 imapobj = tryobj
144 del(self.availableconnections[i])
145 break
146 if not imapobj:
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()
152 return imapobj
153
154 self.connectionlock.release() # Release until need to modify data
155
156 success = 0
157 while not success:
158 # Generate a new connection.
159 if self.tunnel:
160 UIBase.getglobalui().connecting('tunnel', self.tunnel)
161 imapobj = UsefulIMAP4_Tunnel(self.tunnel)
162 success = 1
163 elif self.usessl:
164 UIBase.getglobalui().connecting(self.hostname, self.port)
165 imapobj = UsefulIMAP4_SSL(self.hostname, self.port)
166 else:
167 UIBase.getglobalui().connecting(self.hostname, self.port)
168 imapobj = UsefulIMAP4(self.hostname, self.port)
169
170 if not self.tunnel:
171 try:
172 if 'AUTH=CRAM-MD5' in imapobj.capabilities:
173 UIBase.getglobalui().debug('imap',
174 'Attempting CRAM-MD5 authentication')
175 try:
176 imapobj.authenticate('CRAM-MD5', self.md5handler)
177 except imapobj.error, val:
178 self.plainauth(imapobj)
179 else:
180 self.plainauth(imapobj)
181 # Would bail by here if there was a failure.
182 success = 1
183 except imapobj.error, val:
184 self.passworderror = str(val)
185 self.password = None
186
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 "" ""
191 # Work around them.
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)
197
198 self.connectionlock.acquire()
199 self.assignedconnections.append(imapobj)
200 self.lastowner[imapobj] = thread.get_ident()
201 self.connectionlock.release()
202 return imapobj
203
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)
213
214 def close(self):
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:
220 imapobj.logout()
221 self.assignedconnections = []
222 self.availableconnections = []
223 self.lastowner = {}
224 self.connectionlock.release()
225
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
231 the event is set."""
232 ui = UIBase.getglobalui()
233 ui.debug('imap', 'keepalive thread started')
234 while 1:
235 ui.debug('imap', 'keepalive: top of loop')
236 event.wait(timeout)
237 ui.debug('imap', 'keepalive: after wait')
238 if event.isSet():
239 ui.debug('imap', 'keepalive: event is set; exiting')
240 return
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')
247 threads = []
248 imapobjs = []
249
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)
256 thr.setDaemon(1)
257 thr.start()
258 threads.append(thr)
259 ui.debug('imap', 'keepalive: thread started')
260
261 ui.debug('imap', 'keepalive: joining threads')
262
263 for thr in threads:
264 # Make sure all the commands have completed.
265 thr.join()
266
267 ui.debug('imap', 'keepalive: releasing connections')
268
269 for imapobj in imapobjs:
270 self.releaseconnection(imapobj)
271
272 ui.debug('imap', 'keepalive: bottom of loop')
273
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()
285 if not usetunnel:
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()
291 server = None
292 password = None
293
294 if repository.getname() in passwordhash:
295 password = passwordhash[repository.getname()]
296
297 # Connect to the remote server.
298 if usetunnel:
299 IMAPServer.__init__(self, self.config, self.repos.getname(),
300 tunnel = usetunnel,
301 reference = reference,
302 maxconnections = self.repos.getmaxconnections())
303 else:
304 if not password:
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)