]> code.delx.au - offlineimap/blob - offlineimap/ui/UIBase.py
Applied pre/post sync hooks
[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 (s.config.has_option('general', 'ignore-readonly') and s.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 (s.config.has_option('general', 'ignore-readonly') and s.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 (s.config.has_option('general', 'ignore-readonly') and s.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 skippingfolder(s, folder):
212 """Called when a folder sync operation is started."""
213 if s.verbose >= 0:
214 s._msg("Skipping %s (not changed)" % folder.getname())
215
216 def validityproblem(s, folder):
217 s.warn("UID validity problem for folder %s (repo %s) (saved %d; got %d); skipping it" % \
218 (folder.getname(), folder.getrepository().getname(),
219 folder.getsaveduidvalidity(), folder.getuidvalidity()))
220
221 def loadmessagelist(s, repos, folder):
222 if s.verbose > 0:
223 s._msg("Loading message list for %s[%s]" % (s.getnicename(repos),
224 folder.getname()))
225
226 def messagelistloaded(s, repos, folder, count):
227 if s.verbose > 0:
228 s._msg("Message list for %s[%s] loaded: %d messages" % \
229 (s.getnicename(repos), folder.getname(), count))
230
231 ############################## Message syncing
232
233 def syncingmessages(s, sr, sf, dr, df):
234 if s.verbose > 0:
235 s._msg("Syncing messages %s[%s] -> %s[%s]" % (s.getnicename(sr),
236 sf.getname(),
237 s.getnicename(dr),
238 df.getname()))
239
240 def copyingmessage(s, uid, src, destlist):
241 if s.verbose >= 0:
242 ds = s.folderlist(destlist)
243 s._msg("Copy message %d %s[%s] -> %s" % (uid, s.getnicename(src),
244 src.getname(), ds))
245
246 def deletingmessage(s, uid, destlist):
247 if s.verbose >= 0:
248 ds = s.folderlist(destlist)
249 s._msg("Deleting message %d in %s" % (uid, ds))
250
251 def deletingmessages(s, uidlist, destlist):
252 if s.verbose >= 0:
253 ds = s.folderlist(destlist)
254 s._msg("Deleting %d messages (%s) in %s" % \
255 (len(uidlist),
256 ", ".join([str(u) for u in uidlist]),
257 ds))
258
259 def addingflags(s, uidlist, flags, destlist):
260 if s.verbose >= 0:
261 ds = s.folderlist(destlist)
262 s._msg("Adding flags %s to %d messages on %s" % \
263 (", ".join(flags), len(uidlist), ds))
264
265 def deletingflags(s, uidlist, flags, destlist):
266 if s.verbose >= 0:
267 ds = s.folderlist(destlist)
268 s._msg("Deleting flags %s to %d messages on %s" % \
269 (", ".join(flags), len(uidlist), ds))
270
271 ################################################## Threads
272
273 def getThreadDebugLog(s, thread):
274 if s.debugmessages.has_key(thread):
275 message = "\nLast %d debug messages logged for %s prior to exception:\n"\
276 % (len(s.debugmessages[thread]), thread.getName())
277 message += "\n".join(s.debugmessages[thread])
278 else:
279 message = "\nNo debug messages were logged for %s." % \
280 thread.getName()
281 return message
282
283 def delThreadDebugLog(s, thread):
284 if s.debugmessages.has_key(thread):
285 del s.debugmessages[thread]
286
287 def getThreadExceptionString(s, thread):
288 message = "Thread '%s' terminated with exception:\n%s" % \
289 (thread.getName(), thread.getExitStackTrace())
290 message += "\n" + s.getThreadDebugLog(thread)
291 return message
292
293 def threadException(s, thread):
294 """Called when a thread has terminated with an exception.
295 The argument is the ExitNotifyThread that has so terminated."""
296 s._msg(s.getThreadExceptionString(thread))
297 s.delThreadDebugLog(thread)
298 s.terminate(100)
299
300 def getMainExceptionString(s):
301 sbuf = StringIO()
302 traceback.print_exc(file = sbuf)
303 return "Main program terminated with exception:\n" + \
304 sbuf.getvalue() + "\n" + \
305 s.getThreadDebugLog(threading.currentThread())
306
307 def mainException(s):
308 s._msg(s.getMainExceptionString())
309
310 def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
311 """Called to terminate the application."""
312 if errormsg <> None:
313 if errortitle <> None:
314 sys.stderr.write('ERROR: %s\n\n%s\n'%(errortitle, errormsg))
315 else:
316 sys.stderr.write('%s\n' % errormsg)
317 sys.exit(exitstatus)
318
319 def threadExited(s, thread):
320 """Called when a thread has exited normally. Many UIs will
321 just ignore this."""
322 s.delThreadDebugLog(thread)
323 s.unregisterthread(thread)
324
325 ################################################## Hooks
326
327 def callhook(s, msg):
328 if s.verbose >= 0:
329 s._msg(msg)
330
331 ################################################## Other
332
333 def sleep(s, sleepsecs):
334 """This function does not actually output anything, but handles
335 the overall sleep, dealing with updates as necessary. It will,
336 however, call sleeping() which DOES output something.
337
338 Returns 0 if timeout expired, 1 if there is a request to cancel
339 the timer, and 2 if there is a request to abort the program."""
340
341 abortsleep = 0
342 while sleepsecs > 0 and not abortsleep:
343 abortsleep = s.sleeping(1, sleepsecs)
344 sleepsecs -= 1
345 s.sleeping(0, 0) # Done sleeping.
346 return abortsleep
347
348 def sleeping(s, sleepsecs, remainingsecs):
349 """Sleep for sleepsecs, remainingsecs to go.
350 If sleepsecs is 0, indicates we're done sleeping.
351
352 Return 0 for normal sleep, or 1 to indicate a request
353 to sync immediately."""
354 s._msg("Next refresh in %d seconds" % remainingsecs)
355 if sleepsecs > 0:
356 time.sleep(sleepsecs)
357 return 0