## $Id: msg.py,v 1.91 2002/05/22 14:33:01 kjetilja Exp $ ## System modules from gtk import * from gnome.ui import * import gnome.mime import rfc822, string, mimify import os, cStringIO ## Local modules import folderops, fileops, edit, headers, mime, addresslist, privacy, fonts from prefs import MSG_SIZE ## Try to load the GtkHTML rendering widget try: from html import HtmlWindow gtkhtml_present = 1 except: gtkhtml_present = 0 ## Error messages from the GUI err1 = """Error opening destination file""" err2 = """No viewer specified for type '%s' Define a viewer, or save the attachment and view manually""" err3 = """Unable to store attachment file""" err4 = """Unable to start viewer: %s""" err5 = """Unable to save attachment as %s""" ## ## ## Message (view) window class ## ## class MsgWindow: ## ## Method __init__ (self, folder window instance, msg instance) ## ## Message widget constructor. ## ## def __init__(self, fld, msg, inline): self.fld = fld self.prefs = fld.prefs self.msg = msg self.hdr = msg.headers self.multipart = 0 self.mime = 0 self.inline = inline self.tmpfiles = [] self.privacy = self.fld.privacy self.gtkhtml = gtkhtml_present and self.prefs.gtkhtml # Font styles self.bold_font = load_font(fonts.BOLD_FONT) self.normal_font = load_font(fonts.NORMAL_FONT) # Make a separate window to play in self.vbox = GtkVBox() self.vbox.show() # View inline if not inline: self.win = GnomeApp('Pygmy Viewer', ':Pygmy - View Mail Message') self.win.set_wmclass('pygmy viewer', ':Pygmy - View Mail Message') self.win.set_border_width(5) self.win.connect_after('size_allocate', self.resize) apply(self.win.set_default_size, self.prefs.size[MSG_SIZE]) self.win.set_contents(self.vbox) self.win.show() self.win.create_menus(self.create_menu()) self.win.create_toolbar(self.create_toolbar()) else: self.win = None self.fld.vpaned.pack2(self.vbox) # Initialize window self.init_msg_window() # Setup the headers self.init_contents() # Set viewpoint to start of text widget if not self.gtkhtml: self.msg_text.get_vadjustment().set_value(0) # Make the text widget grab focus if not inline viewer if not inline: self.msg_text.grab_focus() # View more headers if configured self.prefs.more = not self.prefs.more self.more_toggle() ## Catch resize events which are saved later def resize(self, w=None, a=None, c=None): size = w.get_allocation() if w == self.win: self.prefs.size[MSG_SIZE] = size[2:4] ## ## Method create_menu (self) ## ## Create the menu elements. ## ## def create_menu(self): from prefix import PYGMY_ICONDIR file_menu = [ UIINFO_ITEM_STOCK('Save As...', None, self.saveas, STOCK_MENU_SAVE_AS), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Close', None, self.destroy, STOCK_MENU_QUIT) ] edit_menu = [ UIINFO_ITEM_STOCK('Cut', None, self.edit_cut, STOCK_MENU_CUT), UIINFO_ITEM_STOCK('Copy', None, self.edit_copy, STOCK_MENU_COPY), UIINFO_ITEM_STOCK('Paste', None, self.edit_paste, STOCK_MENU_PASTE), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Select All', None, self.sel_all, STOCK_MENU_BLANK), ] msg_menu = [ UIINFO_ITEM_STOCK('Reply', None, self.reply_callback, STOCK_MENU_MAIL_RPL), UIINFO_ITEM_STOCK('Reply All', None, self.replyall_callback, PYGMY_ICONDIR+"/reply_to_all_menu.xpm"), UIINFO_ITEM_STOCK('Forward', None, self.fwd_callback, STOCK_MENU_MAIL_FWD), UIINFO_ITEM_STOCK('View All Headers', None, self.all_headers, STOCK_MENU_BLANK), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Previous', None, self.prev_msg_callback, STOCK_MENU_BACK), UIINFO_ITEM_STOCK('Next', None, self.next_msg_callback, STOCK_MENU_FORWARD), ] menu_info = [ UIINFO_SUBTREE('File', file_menu), UIINFO_SUBTREE('Edit', edit_menu), UIINFO_SUBTREE('Message', msg_menu), ] return menu_info ## ## Method create_toolbar (self) ## ## Create the toolbar elements. ## ## def create_toolbar(self): from prefix import PYGMY_ICONDIR toolbar_info = [ UIINFO_ITEM_STOCK('Reply', None, self.reply_callback, STOCK_PIXMAP_MAIL_RPL), UIINFO_ITEM_STOCK('Reply All', None, self.replyall_callback, PYGMY_ICONDIR+"/reply_to_all.xpm"), UIINFO_ITEM_STOCK('Forward', None, self.fwd_callback, STOCK_PIXMAP_MAIL_FWD), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Previous', None, self.prev_msg_callback, STOCK_PIXMAP_BACK), UIINFO_ITEM_STOCK('Next', None, self.next_msg_callback, STOCK_PIXMAP_FORWARD), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Headers', None, self.all_headers, STOCK_PIXMAP_MAIL), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Close', None, self.destroy, STOCK_PIXMAP_CLOSE), ] return toolbar_info ## ## Method next_msg_callback () ## def next_msg_callback(self, button): # This is to be able to browse from folder view window if self.fld.fdisp.selection == []: if self.fld.numrows >= 1: self.fld.row = -1 else: return else: self.fld.row = self.fld.fdisp.selection[0] if self.fld.row+1 < self.fld.numrows: self.fld.row = self.fld.row + 1 self.fld.fdisp.unselect_row(self.fld.row-1, 0) self.fld.fdisp.select_row(self.fld.row, 0) self.fld.fdisp.moveto(row=self.fld.row) self.fld.view_mail(self.fld.row, inline=0, msgwin=self) self.msg_text.grab_focus() ## ## Method prev_msg_callback () ## def prev_msg_callback(self, button): # This is to be able to browse from folder view window if self.fld.fdisp.selection == []: return self.fld.row = self.fld.fdisp.selection[0] if self.fld.row-1 >= 0: self.fld.row = self.fld.row - 1 self.fld.fdisp.unselect_row(self.fld.row+1, 0) self.fld.fdisp.select_row(self.fld.row, 0) self.fld.fdisp.moveto(row=self.fld.row) self.fld.view_mail(self.fld.row, inline=0, msgwin=self) self.msg_text.grab_focus() ## ## Method saveas (self, b) ## ## Save the currently viewed message to disk. ## ## def saveas(self, button=None): import fileops fname = fileops.getfilename( self.prefs.filepaths, '', 'Save message to file...', 'save-message') if fname == None: return try: f = open(fname, 'w') except: # Unable to open file, show error w = GnomeErrorDialog(err1) w.set_parent(self.win) w.show() else: # Blast the message into the file import types if type(self.msg.fp) != types.FileType: f.write(self.msg.fp.getvalue()) else: pathname = folderops.get_folder_pathname(self.prefs.folders, self.msg.mbox) m = open(pathname) m.seek(self.msg.start) f.write(m.read(self.msg.stop-self.msg.start)) m.close() f.close() ## ## Method {sel|usel}_all (self, b) ## ## Select/Unselect all functions. ## ## def sel_all(self, b): if not self.gtkhtml: self.msg_text.select_region(0, self.msg_text.get_length()) else: print 'not implemented for gtkhtml yet' ## ## Method edit_{cut|copy|paste} (self, b) ## ## Clipboard functions. ## ## def edit_cut(self, b): if not self.gtkhtml: self.msg_text.cut_clipboard() else: print 'not implemented for gtkhtml yet' def edit_copy(self, b): if not self.gtkhtml: self.msg_text.copy_clipboard() else: print 'not implemented for gtkhtml yet' def edit_paste(self, b): if not self.gtkhtml: self.msg_text.paste_clipboard() else: print 'not implemented for gtkhtml yet' ## Reply callback def reply_callback(self, button=None): edtwin = edit.EditWindow(self.fld, edit.REPLY, self.msg) edtwin.setup_widgets() self.destroy() ## Forward callback def fwd_callback(self, button=None): edtwin = edit.EditWindow(self.fld, edit.FORW, self.msg) edtwin.setup_widgets() self.destroy() ## Reply all callback def replyall_callback(self, button=None): edtwin = edit.EditWindow(self.fld, edit.REPLYALL, self.msg) edtwin.setup_widgets() self.destroy() ## View all headers callback def all_headers(self, button=None): headers.HeaderWindow(self.hdr, self.win) ## Construct the buttons used for attachments def open_att_button(self, next, t): # Open attachment button w = GnomeStock(STOCK_MENU_OPEN) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_border_width(1) b.set_relief(RELIEF_NONE) b.connect('clicked', self.view_gnome) t.attach(b, 1, 2, next, next+1, xoptions=0, yoptions=0, xpadding=3) def save_att_button(self, next, t): w = GnomeStock(STOCK_MENU_SAVE) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_border_width(1) b.set_relief(RELIEF_NONE) b.connect('clicked', self.save) t.attach(b, 2, 3, next, next+1, xoptions=0, yoptions=0, xpadding=3) ## ## Method init_contents () ## def init_contents(self): msg = self.msg fld = self.fld self.current_attachment = None self.attachments = [] # Update the folder index status field status = folderops.STATUS_READ # Set message status to 'read' if this message is unread if msg.msg_unread: pathname = folderops.get_folder_pathname(fld.prefs.folders, msg.mbox) folderops.update_folder_index_status(pathname, str(msg.start), status) pos, rest = fld.fdisp.get_row_data(fld.row) rest[0] = status fld.fdisp.set_row_data(fld.row, (pos,rest)) fld.fdisp.set_text(fld.row, 3, status) style = fld.fdisp.get_style().copy() style.font = fld.sub_nfont fld.fdisp.set_row_style(fld.row, style) # Shortcut some references m = self.msg f = self.fld # Construct To: string to = rfc822.AddressList(m.getheader('to')) tostr = [] for i in to.addresslist: tostr.append(i[1]) if len(tostr) == 0 or (len(tostr) != 0 and tostr[0] == None): # To: is missing, insert the email address in the default account instead if self.prefs.defacc: tostr = [self.prefs.accounts[self.prefs.defacc][1]] else: if len(self.prefs.accounts) > 0: tostr = [self.prefs.accounts.keys()[0][1]] else: tostr = "" self.e4.set_text(mimify.mime_decode_header(string.join(tostr, ', '))[:70]) # Construct Cc: string cc = rfc822.AddressList(m.getheader('cc')) ccstr = [] for i in cc.addresslist: ccstr.append(i[1]) if len(ccstr) != 0 and ccstr[0] != None: ccstr = string.join(ccstr, ', ') self.e5.set_text(mimify.mime_decode_header(ccstr)[:70]) if self.prefs.more: self.l2.show() self.e5.show() self.has_cc = 1 else: self.e5.set_text('') self.e5.hide() self.l2.hide() self.has_cc = 0 # Get from, subject and date self.frm = folderops.get_from(m) self.mail_field = self.frm sub = folderops.get_subject(m) date = m.getheader('date') or '' # Insert default header entries self.e1.set_text(mimify.mime_decode_header(sub)[:60]) self.e2.set_text(mimify.mime_decode_header('%s <%s>' %\ (self.frm[0], self.frm[1]))) self.e3.set_text(date) # Clear the text area if not self.gtkhtml: self.msg_text.freeze() self.msg_text.delete_text(0, -1) # Clear else: self.msg_text.load_empty() # Container for attachments that are not viewed inline if not hasattr(self, 't'): t = GtkTable(1,3,0) c = GtkOptionMenu() t.attach(c, 0,1,0,1, xpadding=4, ypadding=3) self.t = t self.c = c self.vbox.pack_start(t, expand=0) # Open attachment button self.open_att_button(0, t) # Save attachment button self.save_att_button(0, t) else: t = self.t c = self.c t.show() c.show() menu = GtkMenu() menu.show() # Check for messages types, single or multipart if m.getmaintype() == 'multipart': self.multipart = 1 self.mime = 0 # Multipart message try: p = mime.find_all_parts(m.getbodyparts(), new_parts=[]) except SystemError: # Unable to parse the mail self.insert_body_text("This mail has malformed content", 'text/plain') self.msg_text.thaw() t.hide() return total_num = len(p) if total_num == 0: # Mail is multipart but we cannot find the parts, show error self.insert_body_text("This mail has malformed content", 'text/plain') self.msg_text.thaw() t.hide() return current_num = 1 first_not_inline = None for sm in p: type = sm.gettype() shown = 0 # Display some types inline if type == 'text/plain' or type == 'text/html': # Plain text, print in the message window shown = 1 try: txt = sm.getbodytext() except: txt = 'Message contains null-bytes and will not be displayed' # Check if message has been encrypted or signed txt = self.privacy.handle_read(txt, self.init_contents) self.insert_body_text(txt, type) del txt elif type == 'application/pgp-signature': # Do not search for boundaries here since they may be invalid # when the mime-type is specified try: txt = sm.getbodytext() except: txt = 'Message contains null-bytes and will not be displayed' self.privacy.decrypt(txt) del txt # Register the first attachment which is not inline if first_not_inline == None and shown == 0: first_not_inline = current_num - 1 name = mimify.mime_decode_header((sm.getparam('name') or sm.getparam('filename') or\ edit.NONAME))[:40] if len(name) > 36: namestr = ".." + name[-36:] else: namestr = name menustr = "%s (%s, part %d of %d)" % (namestr, type, current_num, total_num) if shown: menustr = menustr + ", shown" i = GtkMenuItem(menustr) i.show() i.connect('activate', self.set_current_attachment, (name, type, sm)) menu.append(i) self.attachments.append((name, type, sm)) current_num = current_num + 1 # Ensure the attachment menu index is actually set if first_not_inline == None: first_not_inline = 0 # Set the default entry in the attachment menu menu.set_active(first_not_inline) # Set current attachment self.current_attachment = self.attachments[first_not_inline] c.set_menu(menu) t.queue_draw() self.vbox.queue_resize() self.vbox.queue_draw() else: # We have a single part, possibly mime-encoded message valid_types = ['text/plain', 'text/html'] type = m.gettype() or 'text/plain' shown = type in valid_types self.mime = 1 self.multipart = 0 # Need to add a backref here m.fp.seek(m.startofbody) self.body = m.fp.read(m.stop-m.startofbody) type = m.gettype() name = mimify.mime_decode_header((m.getparam('name') or m.getparam('filename') or\ edit.NONAME))[:40] if len(name) > 36: namestr = ".." + name[-36:] else: namestr = name menustr = "%s (%s, part 1 of 1)" % (namestr, type) if shown: menustr = menustr + ", shown" i = GtkMenuItem(menustr) i.show() i.connect('activate', self.set_current_attachment, (name, type, m)) menu.append(i) self.attachments.append((name, type, m)) # Set current attachment self.current_attachment = self.attachments[0] c.set_menu(menu) t.queue_draw() self.vbox.queue_resize() self.vbox.queue_draw() # Decode encoded messages encoding = m.getencoding() if encoding == 'quoted-printable': self.body = mime.decode_quoted_printable(self.body) elif encoding == 'base64': self.body = mime.decode_base64(self.body) # Insert message if it can be viewed inside if shown: self.body = self.privacy.handle_read(self.body, self.init_contents) self.insert_body_text(self.body, type) if self.gtkhtml: self.msg_text.end_insert() self.msg_text.thaw() def set_current_attachment(self, button, name): self.current_attachment = name def insert_body_text(self, text, type): if not self.gtkhtml: self.msg_text.insert(None, None, None, text) else: self.msg_text.insert(text, type) ## ## Method save (self, button) ## ## Save an attachment to disk. ## ## def save(self, button): # Get a filename to save as name, type, message = self.current_attachment attname = mimify.mime_decode_header(message.getparam('name') or message.getparam('filename') or '') savename = fileops.getfilename( self.prefs.filepaths, attname or 'unnamed', 'Save attachment as...', 'save-attachment' ) if savename == None: # No filename given return try: f = open(savename, 'w') except: w = GnomeErrorDialog(err5 % savename) w.set_parent(self.fld.win) w.show() return # Save the thing to disk if self.mime: body = self.body else: body = message.getbodytext() f.write(body) f.close() ## ## Method view_gnome (self, button) ## ## View an attachment, creates temp file and starts viewer app. ## ## def view_gnome(self, button): name, type, message = self.current_attachment # Insert name if no name is specified if name == edit.NONAME: name = 'unnamed' # Make a temporary file with the attachment contents import tempfile fn = tempfile.mktemp()+os.path.basename(name) try: f = open(fn, 'w') except: w = GnomeErrorDialog(err3) w.set_parent(self.fld.win) w.show() return if self.mime: body = self.body else: body = message.getbodytext() f.write(body) f.close() # Log the file to be deleted when the widget exits self.tmpfiles.append(fn) # Determine mime-type and which application to invoke if type == 'message/rfc822': # Instantiate another message viewer window and throw in # the message for display m = mime.Message(cStringIO.StringIO(body), 0, len(body)) m.msg_unread = 0 MsgWindow(self.fld, m, 0) return else: # Use the regular method of finding the name/application gtype = gnome.mime.type_or_default_of_file(os.path.basename(name), type) gtypekeys = gnome.mime.get_keys(gtype) if 'open' in gtypekeys: prog = gnome.mime.program(gtype) if not prog: w = GnomeErrorDialog(err2 % type) w.set_parent(self.fld.win) w.show() return else: w = GnomeErrorDialog(err2 % type) w.set_parent(self.fld.win) w.show() return # Strip off %f at the end of the programs name prog = prog[:-2] cmd = prog + "'" + fn + "' 2>/dev/null &" # Lauch program os.system(cmd) ## ## Method destroy (self) ## ## Destructor, clean up window stuff. ## ## def destroy(self, button=None): # Take out the main stuff and mime self.msg_text.destroy() self.ht.destroy() self.swin.destroy() if self.multipart or self.mime: try: self.t.destroy() except: pass self.t = None self.vbox.destroy() del self.msg # Remove the body text cache try: del self.body except: pass # Remove attachments try: self.attachments = [] except: pass self.current_attachment = None # Wipe out temporary files for file in self.tmpfiles: try: os.unlink(file) except: pass self.tmpfiles = [] # Crunch the message window and update the pane of inline if self.inline: self.fld.msgwin = None self.fld.vpaned.set_position(-1) else: self.win.destroy() ## ## Method display_headers (self) ## ## Create widgets for viewing header contents. ## ## def display_headers(self): t = GtkTable(4,3,0) t.set_border_width(3) t.show() self.ht = t self.vbox.pack_start(self.ht, expand=FALSE) l = GtkLabel("Subject") l.set_alignment(0.0, 0.5) style = l.get_style().copy() style.font = self.bold_font l.set_style(style) l.show() t.attach(l, 0,1,0,1, xoptions=FILL, xpadding=3) self.e1 = GtkLabel() self.e1.set_alignment(0.0, 0.5) self.e1.show() t.attach(self.e1, 1,2,0,1, xoptions=EXPAND|FILL, xpadding=10) if self.inline: w = GnomeStock(STOCK_MENU_CLOSE) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_relief(RELIEF_NONE) b.set_border_width(0) b.connect('clicked', self.destroy) t.attach(b, 2,3,0,1, xoptions=0) l = GtkLabel("From") l.set_alignment(0.0, 0.5) style = l.get_style().copy() style.font = self.bold_font l.set_style(style) l.show() t.attach(l, 0,1,1,2, xoptions=FILL, xpadding=3) self.e2 = GtkLabel() self.e2.set_alignment(0.0, 0.5) self.e2.show() b = GtkButton() b.show() b.add(self.e2) b.set_relief(RELIEF_NONE) b.set_border_width(0) b.connect('clicked', self.add_to_address) t.attach(b, 1,2,1,2, xoptions=EXPAND|FILL, xpadding=7) w = GnomeStock(STOCK_MENU_DOWN) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_relief(RELIEF_NONE) b.connect('clicked', self.more_toggle) t.attach(b, 2,3,1,2, xoptions=0) # Optional headers self.l0 = GtkLabel("Date") self.l0.set_alignment(0.0, 0.5) style = self.l0.get_style().copy() style.font = self.bold_font self.l0.set_style(style) self.e3 = GtkLabel() self.e3.set_alignment(0.0, 0.5) self.ht.attach(self.l0, 0,1,2,3, xoptions=FILL, xpadding=3) self.ht.attach(self.e3, 1,2,2,3, xoptions=EXPAND|FILL, xpadding=10) self.l1 = GtkLabel("To") self.l1.set_alignment(0.0, 0.5) style = self.l1.get_style().copy() style.font = self.bold_font self.l1.set_style(style) self.e4 = GtkLabel() self.e4.set_alignment(0.0, 0.5) self.ht.attach(self.l1, 0,1,3,4, xoptions=FILL, xpadding=3) self.ht.attach(self.e4, 1,2,3,4, xoptions=EXPAND|FILL, xpadding=10) self.l2 = GtkLabel("Cc") self.l2.set_alignment(0.0, 0.5) style = self.l2.get_style().copy() style.font = self.bold_font self.l2.set_style(style) self.e5 = GtkLabel() self.e5.set_alignment(0.0, 0.5) self.ht.attach(self.l2, 0,1,4,5, xoptions=FILL, xpadding=3) self.ht.attach(self.e5, 1,2,4,5, xoptions=EXPAND|FILL, xpadding=10) ## ## Method more_toggle (self, button=None) ## ## Toggle whether to give extended header list. ## ## def more_toggle(self, button=None): if not self.prefs.more: if self.has_cc: self.e5.show() self.l2.show() self.e4.show() self.l1.show() self.e3.show() self.l0.show() self.prefs.more = 1 w = GnomeStock(STOCK_MENU_UP) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_relief(RELIEF_NONE) b.connect('clicked', self.more_toggle) self.ht.attach(b, 2,3,1,2, xoptions=0) else: if self.has_cc: self.e5.hide() self.l2.hide() self.e4.hide() self.l1.hide() self.e3.hide() self.l0.hide() self.prefs.more = 0 w = GnomeStock(STOCK_MENU_DOWN) w.show() h = GtkVBox() h.show() h.add(w) b = GtkButton() b.show() b.add(h) b.set_relief(RELIEF_NONE) b.connect('clicked', self.more_toggle) self.ht.attach(b, 2,3,1,2, xoptions=0) self.ht.queue_resize() ## ## Method init_msg_window (self) ## ## Initialize message display window. ## ## def init_msg_window(self): # Build the header widget self.display_headers() # Message body widget if self.gtkhtml: self.msg_text = HtmlWindow(self) else: self.msg_text = GtkText() self.msg_text.set_editable(FALSE) self.msg_text.set_word_wrap(TRUE) self.msg_text.show() style = self.msg_text.get_style().copy() style.font = load_font(self.prefs.msg_font) self.msg_text.set_style(style) # Crank text widget into a scrolledwindow self.swin = GtkScrolledWindow() self.swin.set_policy(POLICY_NEVER, POLICY_AUTOMATIC) self.vbox.pack_start(self.swin) self.swin.add(self.msg_text) self.swin.show() ## ## ## Method add_to_address () ## def add_to_address(self, button=None): # Launch address list window adr = addresslist.AddressListWindow(self.fld, None, self.win) # Update GUI to ensure we get the list window up before the popup while events_pending(): mainiteration(FALSE) # Force a popup with the name and address filled in adr.add_name_and_edit(self.frm[0], self.frm[1])