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