]> code.delx.au - offlineimap/blob - offlineimap/ui/UIBase.py
59ea78eba74036cb09087cf4f1d39e9ea24acec6
[offlineimap] / offlineimap / ui / UIBase.py
1 # UI base class
2 # Copyright (C) 2002 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 offlineimap.version
20 import re, time, sys, traceback, threading, thread
21 from StringIO import StringIO
22
23 debugtypes = {'imap': 'IMAP protocol debugging',
24 'maildir': 'Maildir repository debugging',
25 'thread': 'Threading debugging'}
26
27 globalui = None
28 def setglobalui(newui):
29 global globalui
30 globalui = newui
31 def getglobalui():
32 global globalui
33 return globalui
34
35 class UIBase:
36 def __init__(s, config, verbose = 0):
37 s.verbose = verbose
38 s.config = config
39 s.debuglist = []
40 s.debugmessages = {}
41 s.debugmsglen = 50
42 s.threadaccounts = {}
43 s.logfile = None
44
45 ################################################## UTILS
46 def _msg(s, msg):
47 """Generic tool called when no other works."""
48 s._log(msg)
49 s._display(msg)
50
51 def _log(s, msg):
52 """Log it to disk. Returns true if it wrote something; false
53 otherwise."""
54 if s.logfile:
55 s.logfile.write("%s: %s\n" % (threading.currentThread().getName(),
56 msg))
57 return 1
58 return 0
59
60 def setlogfd(s, logfd):
61 s.logfile = logfd
62 logfd.write("This is %s %s\n" % \
63 (offlineimap.version.productname,
64 offlineimap.version.versionstr))
65 logfd.write("Python: %s\n" % sys.version)
66 logfd.write("Platform: %s\n" % sys.platform)
67 logfd.write("Args: %s\n" % sys.argv)
68
69 def _display(s, msg):
70 """Display a message."""
71 raise NotImplementedError
72
73 def warn(s, msg, minor = 0):
74 if minor:
75 s._msg("warning: " + msg)
76 else:
77 s._msg("WARNING: " + msg)
78
79 def registerthread(s, account):
80 """Provides a hint to UIs about which account this particular
81 thread is processing."""
82 if s.threadaccounts.has_key(threading.currentThread()):
83 raise ValueError, "Thread %s already registered (old %s, new %s)" %\
84 (threading.currentThread().getName(),
85 s.getthreadaccount(s), account)
86 s.threadaccounts[threading.currentThread()] = account
87
88 def unregisterthread(s, thr):
89 """Recognizes a thread has exited."""
90 if s.threadaccounts.has_key(thr):
91 del s.threadaccounts[thr]
92
93 def getthreadaccount(s, thr = None):
94 if not thr:
95 thr = threading.currentThread()
96 if s.threadaccounts.has_key(thr):
97 return s.threadaccounts[thr]
98 return '*Control'
99
100 def debug(s, debugtype, msg):
101 thisthread = threading.currentThread()
102 if s.debugmessages.has_key(thisthread):
103 s.debugmessages[thisthread].append("%s: %s" % (debugtype, msg))
104 else:
105 s.debugmessages[thisthread] = ["%s: %s" % (debugtype, msg)]
106
107 while len(s.debugmessages[thisthread]) > s.debugmsglen:
108 s.debugmessages[thisthread] = s.debugmessages[thisthread][1:]
109
110 if debugtype in s.debuglist:
111 if not s._log("DEBUG[%s]: %s" % (debugtype, msg)):
112 s._display("DEBUG[%s]: %s" % (debugtype, msg))
113
114 def add_debug(s, debugtype):
115 global debugtypes
116 if debugtype in debugtypes:
117 if not debugtype in s.debuglist:
118 s.debuglist.append(debugtype)
119 s.debugging(debugtype)
120 else:
121 s.invaliddebug(debugtype)
122
123 def debugging(s, debugtype):
124 global debugtypes
125 s._msg("Now debugging for %s: %s" % (debugtype, debugtypes[debugtype]))
126
127 def invaliddebug(s, debugtype):
128 s.warn("Invalid debug type: %s" % debugtype)
129
130 def locked(s):
131 raise Exception, "Another OfflineIMAP is running with the same metadatadir; exiting."
132
133 def getnicename(s, object):
134 prelimname = str(object.__class__).split('.')[-1]
135 # Strip off extra stuff.
136 return re.sub('(Folder|Repository)', '', prelimname)
137
138 def isusable(s):
139 """Returns true if this UI object is usable in the current
140 environment. For instance, an X GUI would return true if it's
141 being run in X with a valid DISPLAY setting, and false otherwise."""
142 return 1
143
144 ################################################## INPUT
145
146 def getpass(s, accountname, config, errmsg = None):
147 raise NotImplementedError
148
149 def folderlist(s, list):
150 return ', '.join(["%s[%s]" % (s.getnicename(x), x.getname()) for x in list])
151
152 ################################################## WARNINGS
153 def msgtoreadonly(s, destfolder, uid, content, flags):
154 if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
155 s.warn("Attempted to synchronize message %d to folder %s[%s], but that folder is read-only. The message will not be copied to that folder." % \
156 (uid, s.getnicename(destfolder), destfolder.getname()))
157
158 def flagstoreadonly(s, destfolder, uidlist, flags):
159 if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
160 s.warn("Attempted to modify flags for messages %s in folder %s[%s], but that folder is read-only. No flags have been modified for that message." % \
161 (str(uidlist), s.getnicename(destfolder), destfolder.getname()))
162
163 def deletereadonly(s, destfolder, uidlist):
164 if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
165 s.warn("Attempted to delete messages %s in folder %s[%s], but that folder is read-only. No messages have been deleted in that folder." % \
166 (str(uidlist), s.getnicename(destfolder), destfolder.getname()))
167
168 ################################################## MESSAGES
169
170 def init_banner(s):
171 """Called when the UI starts. Must be called before any other UI
172 call except isusable(). Displays the copyright banner. This is
173 where the UI should do its setup -- TK, for instance, would
174 create the application window here."""
175 if s.verbose >= 0:
176 s._msg(offlineimap.version.banner)
177
178 def connecting(s, hostname, port):
179 if s.verbose < 0:
180 return
181 if hostname == None:
182 hostname = ''
183 if port != None:
184 port = ":%s" % str(port)
185 displaystr = ' to %s%s.' % (hostname, port)
186 if hostname == '' and port == None:
187 displaystr = '.'
188 s._msg("Establishing connection" + displaystr)
189
190 def acct(s, accountname):
191 if s.verbose >= 0:
192 s._msg("***** Processing account %s" % accountname)
193
194 def acctdone(s, accountname):
195 if s.verbose >= 0:
196 s._msg("***** Finished processing account " + accountname)
197
198 def syncfolders(s, srcrepos, destrepos):
199 if s.verbose >= 0:
200 s._msg("Copying folder structure from %s to %s" % \
201 (s.getnicename(srcrepos), s.getnicename(destrepos)))
202
203 ############################## Folder syncing
204 def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
205 """Called when a folder sync operation is started."""
206 if s.verbose >= 0:
207 s._msg("Syncing %s: %s -> %s" % (srcfolder.getname(),
208 s.getnicename(srcrepos),
209 s.getnicename(destrepos)))
210
211 def validityproblem(s, folder, saved, new):
212 s.warn("UID validity problem for folder %s (saved %d; got %d); skipping it" % \
213 (folder.getname(), saved, new))
214
215 def loadmessagelist(s, repos, folder):
216 if s.verbose > 0:
217 s._msg("Loading message list for %s[%s]" % (s.getnicename(repos),
218 folder.getname()))
219
220 def messagelistloaded(s, repos, folder, count):
221 if s.verbose > 0:
222 s._msg("Message list for %s[%s] loaded: %d messages" % \
223 (s.getnicename(repos), folder.getname(), count))
224
225 ############################## Message syncing
226
227 def syncingmessages(s, sr, sf, dr, df):
228 if s.verbose > 0:
229 s._msg("Syncing messages %s[%s] -> %s[%s]" % (s.getnicename(sr),
230 sf.getname(),
231 s.getnicename(dr),
232 df.getname()))
233
234 def copyingmessage(s, uid, src, destlist):
235 if s.verbose >= 0:
236 ds = s.folderlist(destlist)
237 s._msg("Copy message %d %s[%s] -> %s" % (uid, s.getnicename(src),
238 src.getname(), ds))
239
240 def deletingmessage(s, uid, destlist):
241 if s.verbose >= 0:
242 ds = s.folderlist(destlist)
243 s._msg("Deleting message %d in %s" % (uid, ds))
244
245 def deletingmessages(s, uidlist, destlist):
246 if s.verbose >= 0:
247 ds = s.folderlist(destlist)
248 s._msg("Deleting %d messages (%s) in %s" % \
249 (len(uidlist),
250 ", ".join([str(u) for u in uidlist]),
251 ds))
252
253 def addingflags(s, uidlist, flags, destlist):
254 if s.verbose >= 0:
255 ds = s.folderlist(destlist)
256 s._msg("Adding flags %s to %d messages on %s" % \
257 (", ".join(flags), len(uidlist), ds))
258
259 def deletingflags(s, uidlist, flags, destlist):
260 if s.verbose >= 0:
261 ds = s.folderlist(destlist)
262 s._msg("Deleting flags %s to %d messages on %s" % \
263 (", ".join(flags), len(uidlist), ds))
264
265 ################################################## Threads
266
267 def getThreadDebugLog(s, thread):
268 if s.debugmessages.has_key(thread):
269 message = "\nLast %d debug messages logged for %s prior to exception:\n"\
270 % (len(s.debugmessages[thread]), thread.getName())
271 message += "\n".join(s.debugmessages[thread])
272 else:
273 message = "\nNo debug messages were logged for %s." % \
274 thread.getName()
275 return message
276
277 def delThreadDebugLog(s, thread):
278 if s.debugmessages.has_key(thread):
279 del s.debugmessages[thread]
280
281 def getThreadExceptionString(s, thread):
282 message = "Thread '%s' terminated with exception:\n%s" % \
283 (thread.getName(), thread.getExitStackTrace())
284 message += "\n" + s.getThreadDebugLog(thread)
285 return message
286
287 def threadException(s, thread):
288 """Called when a thread has terminated with an exception.
289 The argument is the ExitNotifyThread that has so terminated."""
290 s._msg(s.getThreadExceptionString(thread))
291 s.delThreadDebugLog(thread)
292 s.terminate(100)
293
294 def getMainExceptionString(s):
295 sbuf = StringIO()
296 traceback.print_exc(file = sbuf)
297 return "Main program terminated with exception:\n" + \
298 sbuf.getvalue() + "\n" + \
299 s.getThreadDebugLog(threading.currentThread())
300
301 def mainException(s):
302 s._msg(s.getMainExceptionString())
303
304 def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
305 """Called to terminate the application."""
306 if errormsg <> None:
307 if errortitle <> None:
308 sys.stderr.write('ERROR: %s\n\n%s\n'%(errortitle, errormsg))
309 else:
310 sys.stderr.write('%s\n' % errormsg)
311 sys.exit(exitstatus)
312
313 def threadExited(s, thread):
314 """Called when a thread has exited normally. Many UIs will
315 just ignore this."""
316 s.delThreadDebugLog(thread)
317 s.unregisterthread(thread)
318
319 ################################################## Other
320
321 def sleep(s, sleepsecs):
322 """This function does not actually output anything, but handles
323 the overall sleep, dealing with updates as necessary. It will,
324 however, call sleeping() which DOES output something.
325
326 Returns 0 if timeout expired, 1 if there is a request to cancel
327 the timer, and 2 if there is a request to abort the program."""
328
329 abortsleep = 0
330 while sleepsecs > 0 and not abortsleep:
331 abortsleep = s.sleeping(1, sleepsecs)
332 sleepsecs -= 1
333 s.sleeping(0, 0) # Done sleeping.
334 return abortsleep
335
336 def sleeping(s, sleepsecs, remainingsecs):
337 """Sleep for sleepsecs, remainingsecs to go.
338 If sleepsecs is 0, indicates we're done sleeping.
339
340 Return 0 for normal sleep, or 1 to indicate a request
341 to sync immediately."""
342 s._msg("Next refresh in %d seconds" % remainingsecs)
343 if sleepsecs > 0:
344 time.sleep(sleepsecs)
345 return 0