## $Id: edit.py,v 1.140 2002/07/09 15:12:57 kjetilja Exp $ ## System modules from gtk import * from gnome.ui import * import mimify, string, rfc822, cStringIO, sys, MimeWriter, os.path, time ## Local modules import cite, folderops, fileops, smileys, sendmail, mime, editor, builtineditor import externaleditor, pygmymimetools ## Mail composition modes NEW = 1 REPLY = 2 REPLYALL = 3 FORW = 4 EDIT = 5 MAILTO = 6 ## File reference for mail parts without a name NONAME = "" ## Escaped dot DOT = string.upper(hex(ord('.'))[2:]) ## Error messages from the GUI err1 = "You must specify a recipient for your message!" err2 = "This mail has malformed content which cannot be\nsuccessfully viewed." err3 = "The file cannot be inserted because it contains null bytes." ## Other messages from the GUI pop1 = "The message has been sent." pop2 = "The message has been stored in the 'drafts' folder.\n\nDouble-click "\ "on the message in the 'drafts' folder to edit it." ## Filter callback to determine whether to quote text or not def needsquoting(c): if c == '\n' or c == '\t': return 0 return c == '=' or not(' ' <= c <= '~') ## Function that ensures split email addresses are parsed correctly def verify_split(addr): for name in addr: if name[0] == '"' or name[0] == "'": if string.rfind(name, '"') > 0 or\ string.rfind(name, "'") > 0: continue idx = addr.index(name) if len(addr) > idx+1: addr[idx] = addr[idx] +", "+ addr[idx+1] del addr[idx+1] ## Grabbed from email/Utils.py in later Python versions def formatdate(timeval=None, localtime=0, fromline=0): # Note: we cannot use strftime() because that honors the locale and RFC # 2822 requires that day and month names be the English abbreviations. if timeval is None: timeval = time.time() if localtime: now = time.localtime(timeval) # Calculate timezone offset, based on whether the local zone has # daylight savings time, and whether DST is in effect. if time.daylight and now[-1]: offset = time.altzone else: offset = time.timezone hours, minutes = divmod(abs(offset), 3600) # Remember offset is in seconds west of UTC, but the timezone is in # minutes east of UTC, so the signs differ. if offset > 0: sign = '-' else: sign = '+' zone = '%s%02d%02d' % (sign, hours, minutes / 60) else: now = time.gmtime(timeval) # Timezone offset is always -0000 zone = '-0000' if fromline: # One format for the From line return '%s %s %02d %02d:%02d:%02d %04d' % ( ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1], now[2], now[3], now[4], now[5], now[0]) else: # Another format for regular date headers return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], now[2], ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1], now[0], now[3], now[4], now[5], zone) ## Date formatting for the reply/reply all citation start line def format_date_reply(timeval, localtime=0): if timeval is None: timeval = time.time() now = time.localtime(timeval) return '%s, %02d %s %04d at %02d:%02d' % ( ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], now[2], ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1], now[0], now[3], now[4]) ## ## ## Edit/Compose window class ## ## class EditWindow: ## ## Method __init__ (self, folder window class instance, ## delivery mode, message class instance) ## ## Main widget constructor. ## ## def __init__(self, fld, mode, msg=None, mailto=None): # Add class instance references self.fld = fld self.prefs = fld.prefs self.mode = mode self.msg = msg self.next = 0 self.headers = {} self.smileys_active = 0 self.more = 0 self.has_cc = 0 self.attachments = [] self.attachment_table = None self.current_attachment = None self.privacy = self.fld.privacy self.mbox = fld.mbox self.mail_is_sent = 0 self.mailto = mailto # Make a separate window to play in self.vbox = GtkVBox() self.vbox.set_spacing(5) self.vbox.show() self.win = GnomeApp('Pygmy Viewer', ':Pygmy - Compose Message') self.win.set_wmclass('pygmy viewer', ':Pygmy - Compose Message') self.win.set_border_width(5) self.win.set_default_size(512, 480) self.win.set_contents(self.vbox) self.win.connect('delete_event', self.destroy) self.win.show() ## ## Method setup_widgets (self) ## ## Setup the window ## ## def setup_widgets(self): # Set up widgets for this page self.init_header() self.init_editor() # Create menus and toolbar self.win.create_menus(self.create_menu()) self.win.create_toolbar(self.create_toolbar()) # Fill widgets with content self.init_mode() # Update the status indicator for the message if not new self.update_status_field() # Update the window counter in the folder instance self.fld.num_edits = self.fld.num_edits + 1 ## ## Method edit_mode (self) ## ## Handle edit ## ## def edit_mode(self): msg = self.msg # Fill in the subject, to, date and body parts sub = mimify.mime_decode_header(msg.getheader('subject')) self.e3.set_text(sub) # Get all the target addresses to = rfc822.AddressList(msg.getheader('to')) cc = rfc822.AddressList(msg.getheader('cc')) bcc = rfc822.AddressList(msg.getheader('bcc')) tostr = [] for e in to.addresslist: tostr.append(e[1]) ccstr = [] for e in cc.addresslist: ccstr.append(e[1]) bccstr = [] for e in bcc.addresslist: bccstr.append(e[1]) self.e1.set_text(mimify.mime_decode_header(string.join(tostr, ', '))) self.e2.set_text(mimify.mime_decode_header(string.join(ccstr, ', '))) self.e4.set_text(mimify.mime_decode_header(string.join(bccstr, ', '))) # If (B)CC is set, show all of the header display if ccstr != [] or bccstr != []: self.more_toggle(None) # Draw text and set the scroll position to the top of the text self.ed.set_scroll_position( 'top' ) self.ed.redraw() # Update the status indicator for the message self.status_update = folderops.STATUS_READ irto = msg.getheader('message-id') if irto != None: self.headers['In-Reply-To'] = irto # Check for MIME messages self.ed.freeze() if msg.getmaintype() == 'multipart': text = '' not_viewed = [] try: p = mime.find_all_parts(msg.getbodyparts(), new_parts=[]) except SystemError: text = text + err2 else: for sm in p: type = sm.gettype() # Display some types directly if type == 'text/plain': text = text + sm.getbodytext() else: not_viewed.append(sm) del p self.ed.insert_text( text, pos='bottom' ) if len(not_viewed) > 0: for sm in not_viewed: type = sm.gettype() name = mimify.mime_decode_header((sm.getparam('name') or sm.getparam('filename') or\ NONAME))[:40] # Append attachment to list self.attachments.insert(0, (name, type, sm)) # Update view self.view_attachment_table() del not_viewed else: msg.fp.seek(msg.startofbody) text = msg.fp.read(msg.stop - msg.startofbody) # Decode quoted-printable mails if msg.getencoding() == 'quoted-printable': text = mime.decode_quoted_printable(text) elif msg.getencoding() == 'base64': text = mime.decode_base64(text) self.ed.insert_text(text, pos='bottom') self.ed.thaw() self.ed.set_scroll_position( 'top' ) self.ed.redraw() self.ed.grab_focus() ## ## Method forward_mode (self) ## ## Handle forward ## ## def forward_mode(self): body = '' msg = self.msg # Extract some header information we would like to embed in the message sub = msg.getheader('subject') if sub: sub = mimify.mime_decode_header(sub) frm = msg.getheader('from') if frm: frm = mimify.mime_decode_header(frm) date = msg.getheader('date') if date: date = mimify.mime_decode_header(date) cc = msg.getheader('cc') if cc: cc = mimify.mime_decode_header(cc) to = msg.getheader('to') if to: to = mimify.mime_decode_header(to) # Fill in the subject and body parts (forward) self.e3.set_text(sub) self.ed.set_scroll_position( 'top' ) self.ed.redraw() self.e1.grab_focus() # Update the status indicator for the message self.status_update = folderops.STATUS_FORW # Check for MIME messages if msg.getmaintype() == 'multipart': text = '' not_viewed = [] try: p = mime.find_all_parts(msg.getbodyparts(), new_parts=[]) except SystemError: text = text + err2 else: for sm in p: type = sm.gettype() # Display some types directly if type == 'text/plain': text = text + sm.getbodytext() else: not_viewed.append(sm) del p text = self.privacy.handle_read(text, self.init_mode) body = body + cite.body_forward(text, sub, frm, date, cc, to) if len(not_viewed) > 0: for sm in not_viewed: type = sm.gettype() name = mimify.mime_decode_header((sm.getparam('name') or sm.getparam('filename') or\ NONAME))[:40] # Append attachment to list self.attachments.insert(0, (name, type, sm)) # Update view self.view_attachment_table() del not_viewed else: if msg.gettype() == 'text/plain': msg.fp.seek(msg.startofbody) text = msg.fp.read(msg.stop - msg.startofbody) text = self.privacy.handle_read(text, self.init_mode) else: text = '' self.attachments.insert(0, (NONAME, msg.gettype(), msg)) self.view_attachment_table() # Decode quoted-printable mails if msg.getencoding() == 'quoted-printable': text = mime.decode_quoted_printable(text) elif msg.getencoding() == 'base64': text = mime.decode_base64(text) body = body + cite.body_forward(text, sub, frm, date, cc, to) # add signature if necessary body = self.check_add_sig( FORW, body ) self.ed.freeze() self.ed.insert_text( body, pos='cursor') self.ed.thaw() ## ## Method reply_mode (self, all) ## ## Handle (group) reply ## ## def reply_mode(self, all=0): body = '' msg = self.msg date = format_date_reply(int(folderops.date_to_epoch(msg))) frm = mimify.mime_decode_header(msg.getaddr('from')[0] or 'you') # Fill in the subject, to, date and body parts (reply to all) sub = cite.citesubject(mimify.mime_decode_header(msg.getheader('subject') or '')) self.e3.set_text(sub) # Use the reply-to header as reply address if specified rt = msg.getheader('reply-to') if rt == None or rt == '': rt = msg.getheader('from') if rt == None or rt == '': rt = 'No valid email address found' # Get all the relevant addresses from the accounts ownaddrs = [] for a in self.prefs.accounts.keys(): username, emailaddr, sigfile, replyaddr, smtpserver, localsendmail \ = self.prefs.accounts[a] if emailaddr != '': ownaddrs.append(string.lower(emailaddr)) if replyaddr != '': ownaddrs.append(string.lower(replyaddr)) # Do group reply or just a regular reply if all == 1: # Get all the target addresses to = rfc822.AddressList(rt) + \ rfc822.AddressList(msg.getheader('to')) cc = rfc822.AddressList(msg.getheader('cc')) tostr = [] for i in to.addresslist: if i[1] != None and string.lower(i[1]) not in ownaddrs: tostr.append(i[1]) ccstr = [] for i in cc.addresslist: if i[1] != None and string.lower(i[1]) not in ownaddrs: ccstr.append(i[1]) self.e1.set_text(mimify.mime_decode_header(string.join(tostr, ', '))) self.e2.set_text(mimify.mime_decode_header(string.join(ccstr, ', '))) # If CC is set, show all of the header display if ccstr != []: self.more_toggle(None) else: # Just regular reply here, not group reply self.e1.set_text(mimify.mime_decode_header(rt)) # Set message status self.status_update = folderops.STATUS_REPL # Create In-Reply-To header entry irto = msg.getheader('message-id') if irto != None: self.headers['In-Reply-To'] = irto # Check for MIME messages if msg.getmaintype() == 'multipart': text = '' try: p = mime.find_all_parts(msg.getbodyparts(), new_parts=[]) except SystemError: text = text + err2 else: for sm in p: type = sm.gettype() # Display some types directly if type == 'text/plain': text = text + sm.getbodytext() del p text = self.privacy.handle_read(text, self.init_mode) body = body + cite.citebody(text, date, frm, self.prefs.citechar) else: if msg.gettype() == 'text/plain': msg.fp.seek(msg.startofbody) text = msg.fp.read(msg.stop - msg.startofbody) text = self.privacy.handle_read(text, self.init_mode) else: text = '' # Decode quoted-printable mails if msg.getencoding() == 'quoted-printable': text = mime.decode_quoted_printable(text) elif msg.getencoding() == 'base64': text = mime.decode_base64(text) body = body + cite.citebody(text, date, frm, self.prefs.citechar) # add signature if necessary body = self.check_add_sig( REPLY, body ) self.ed.freeze() self.ed.insert_text( body, pos='cursor' ) self.ed.thaw() self.ed.set_scroll_position( 'top' ) self.ed.redraw() self.ed.grab_focus() ## ## Method init_mode (self) ## ## Fill the widgets according forward, edit, reply or new message ## ## def init_mode(self): mode = self.mode if mode == NEW: self.e1.grab_focus() self.status_update = None body = '' body = self.check_add_sig( NEW, body ) self.ed.insert_text( body ) elif mode == EDIT: self.edit_mode() elif mode == FORW: self.forward_mode() elif mode == REPLY: self.reply_mode(all=0) elif mode == REPLYALL: self.reply_mode(all=1) elif mode == MAILTO: if len(self.mailto) > len('mailto:'): if self.mailto[:len('mailto:')] == 'mailto:': self.mailto = self.mailto[len('mailto:'):] self.e1.set_text(mimify.mime_decode_header(self.mailto)) self.e3.grab_focus() self.status_update = None self.ed.insert_text( '' ) ## ## Method create_menu (self) ## ## Update the status indicator for the message both on screen and in index ## ## def update_status_field(self): if self.status_update != None: fld = self.fld pathname = folderops.get_folder_pathname(fld.prefs.folders, self.mbox) try: folderops.update_folder_index_status(pathname, self.msg.start, self.status_update) except KeyError: print '** Warning: unable to update status field, to be fixed later' return pos, rest = fld.fdisp.get_row_data(fld.row) rest[0] = self.status_update fld.fdisp.set_row_data(fld.row, (pos, rest)) fld.fdisp.set_text(fld.row, 3, self.status_update) ## ## Method create_menu (self) ## ## Create the menu elements. ## ## def create_menu(self): from prefix import PYGMY_ICONDIR import os.path file_menu = [ UIINFO_ITEM_STOCK('Insert File', None, self.insert_file, STOCK_MENU_OPEN), UIINFO_ITEM_STOCK('Attach File', None, self.add_attachment, STOCK_MENU_ATTACH), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Close', None, self.destroy, STOCK_MENU_CLOSE) ] 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), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Fill Paragraph', None, self.fill_paragraph, STOCK_MENU_BLANK), ] msg_menu = [ UIINFO_ITEM_STOCK('Send', None, self.send_mail, STOCK_MENU_MAIL_SND), UIINFO_ITEM_STOCK('Draft', None, self.send_mail_later, STOCK_MENU_TIMER), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Security', None, self.privacy_dialog, PYGMY_ICONDIR+"/encrypt.xpm"), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Add Signature', None, self.add_sig, STOCK_MENU_BLANK), UIINFO_ITEM_STOCK('Add Smileys', None, self.smileys, STOCK_MENU_BLANK), ] 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): toolbar_info = [ UIINFO_ITEM_STOCK('Send', None, self.send_mail, STOCK_PIXMAP_MAIL_SND), UIINFO_ITEM_STOCK('Draft', None, self.send_mail_later, STOCK_PIXMAP_TIMER), UIINFO_SEPARATOR, UIINFO_ITEM_STOCK('Attach', None, self.add_attachment, STOCK_PIXMAP_ATTACH), UIINFO_ITEM_STOCK('Insert', None, self.insert_file, STOCK_PIXMAP_OPEN), ] return toolbar_info ## ## Method {sel|usel}_all (self, b) ## ## Select/Unselect all functions. ## ## def sel_all(self, b): self.ed.sel_all() ## ## Method edit_{cut|copy|paste} (self, b) ## ## Clipboard functions. ## ## def edit_cut(self, b): self.ed.cut_clipboard() def edit_copy(self, b): self.ed.copy_clipboard() def edit_paste(self, b): self.ed.paste_clipboard() ## ## Method insert_file (self) ## ## Insert a file in the current editor window. ## ## def insert_file(self, button=None): import fileops fname = fileops.getfilename( self.prefs.filepaths,'', 'Select file to insert...', 'insert-file' ) if fname and os.path.isfile(fname): self.ed.insert_file( fname, pos='cursor' ) def smileys(self, buttion=None): if self.smileys_active: return self.smileys_active = 1 self.smileyswnd = smileys.SmileysWindow(self) def insert_smiley(self, smiley): self.ed.insert_text(smiley, pos='cursor') ## ## Method add_sig (self) ## ## Add signature to message (this action is selected manually by the user). ## ## def add_sig(self, button=None): import fileops sigfile = self.prefs.accounts[self.account_name][2] buf = fileops.getsignature(sigfile) if buf != None and buf != '': self.ed.insert_text( "\n" + buf, pos='bottom' ) del buf ## ## Method check_add_sig (self) ## ## Check if a signature is to be added automatically, and ## whether it should be placed before or after the main text ## (depending on the mode) and do it. ## ## def check_add_sig( self, mode, body='' ): if not self.prefs.addsig: return body sigfile = self.prefs.accounts[self.account_name][2] sig = fileops.getsignature(sigfile) if mode == REPLY: if self.prefs.sigreply == 'before': body = "\n" + sig + body elif self.prefs.sigreply == 'after': body = body + "\n" + sig elif mode == FORW: if self.prefs.addsig: if self.prefs.sigforward == 'before': body = "\n" + sig + body elif self.prefs.sigforward == 'after': body = body + "\n" + sig elif mode == NEW: body = body + "\n" + sig return body def set_current_attachment(self, button, name): self.current_attachment = name def view_attachment_table(self): # If already set, remove the old version if self.attachment_table != None: self.attachment_table.destroy() # If no attachments, return if len(self.attachments) == 0: return # Create a table to hold the attachment entries t = GtkTable(1,2,0) t.show() c = GtkOptionMenu() c.show() m = GtkMenu() m.show() current_num = 1 total_num = len(self.attachments) for name, type, sm in self.attachments: if len(name) > 36: namestr = ".." + name[-36:] else: namestr = name menustr = "%s (%s, %d of %d)" % (namestr, type, current_num, total_num) i = GtkMenuItem(menustr) i.show() i.connect('activate', self.set_current_attachment, (name, type, sm)) m.append(i) current_num = current_num + 1 c.set_menu(m) t.attach(c, 0,1,0,1, xpadding=4, ypadding=1) # Remove attachment button w = GnomeStock(STOCK_MENU_TRASH) 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.remove_attachment) t.attach(b, 1, 2, 0, 1, xoptions=0, yoptions=0, xpadding=3, ypadding=0) # Set the current attachment if not set if self.current_attachment not in self.attachments: self.current_attachment = self.attachments[0] # Store a reference to the attachment self.vbox.pack_start(t, expand=0) self.attachment_table = t ## ## Method add_attachment (self) ## ## Add an attachment to the document. ## ## def add_attachment(self, button=None, fname=None): import mimetypes, gnome.mime, os.path # Get attachment file name if fname == None: # Request dialog name = fileops.getfilename( self.prefs.filepaths, '', "Select file to attach...", 'load-attachment' ) if name == None: return else: # gmc drag and drop name = fname # Check that attachment is actually a file and not a dir or something if not os.path.isfile(name): return # Try using gnome.mime to determine the mime-type type = gnome.mime.type_of_file(os.path.basename(name)) if type == 'text/plain': # Try using python libs to determine mime-type type, encoding = mimetypes.guess_type(name) if type == None: # Got no type, set to application/octet-stream -- hmmm.... type = 'application/octet-stream' # If we alreay have the attachment, ignore it if (name, type, None) in self.attachments: return # Append attachment to list self.attachments.insert(0, (name, type, None)) # Update view self.view_attachment_table() ## Callback for the remove attachment button def remove_attachment(self, button): # Remove attachment from list self.attachments.remove(self.current_attachment) # Update view self.view_attachment_table() ## ## Method init_editor (self) ## ## Editor widget setup. ## ## def init_editor(self): from gnome.zvt import ZvtTerm targets = [ ('text/plain', 0, -1) ] # create editor widget if not self.prefs.usebuiltined and "writechild" in dir(ZvtTerm): try: self.ed = externaleditor.ExternalEditor( self.prefs, self.fld.comp_nfont ) except "BadEditorPath", path: # fallback to builtin editor self.ed = builtineditor.BuiltinEditor( self.prefs, self.fld.comp_nfont ) else: self.ed = builtineditor.BuiltinEditor( self.prefs, self.fld.comp_nfont ) edwidget = self.ed.widget() edwidget.connect('drag_data_received', self.drag_drop_attachment) edwidget.drag_dest_set(DEST_DEFAULT_ALL, targets, GDK.ACTION_COPY) self.vbox.pack_start(edwidget, expand=TRUE) ## Callback for dnd to: from GnomeCard def drag_drop_to(self, w, context, x, y, data, info, time): self.insert_dragdrop(data, self.e1) ## Callback for dnd cc: from GnomeCard def drag_drop_cc(self, w, context, x, y, data, info, time): self.insert_dragdrop(data, self.e2) ## Callback for dnd bcc: from GnomeCard def drag_drop_bcc(self, w, context, x, y, data, info, time): self.insert_dragdrop(data, self.e4) ## Callback for attachment dnd from gmc and mozilla -- jeez what a hack dnd is def drag_drop_attachment(self, w, context, x, y, data, info, time): filename = data.data if filename[:len('file:')] == 'file:': # Strip away the file: part filename = filename[len('file:'):] elif filename[0] != '/': # We only handle file: protocol for now return tmp = string.split(filename, ' ') if len(tmp) > 1: # We get two refs from mozilla, only interested in the first filename = tmp[0] if filename[:len('///')] == '///': # Hack away the prefix in mozilla filename = filename[len('//'):] if filename[-1] == '\000': # Hack away the string termination in gmc and nautilus filename = filename[:-1] if filename[-2:] == '\015\012': filename = filename[:-2] self.add_attachment(None, filename) ## Do the actual insert in the widget def insert_dragdrop(self, data, e): # This is for parsing GnomeCard stuff import string lines = string.split(data.data, '\012') for i in range(len(lines)): if lines[i][:len('E-mail:')] == 'E-mail:': if e.get_text() == '': pre = '' else: pre = ', ' e.append_text(pre+string.lstrip(lines[i][len('E-mail:'):])) ## Set the account name to be used here def select_from(self, button, name): self.account_name = name ## ## Method init_header (self) ## ## Header widget setup. ## ## def init_header(self): targets = [ ('text/plain', 0, -1) ] t = GtkTable(4,2,0) t.set_border_width(3) t.show() self.ht = t # Determine whether to display the From: field if len(self.prefs.accounts) <= 1: showfrom = 0 else: showfrom = 1 # Set the default account to use -- adapt to # the recipient address if replying to a mail if REPLY/REPLYALL if self.mode in (REPLY, REPLYALL): self.account_name = self.get_account_name_for_reply() else: self.account_name = self.prefs.defacc # Determine which account is the default one e = self.prefs.accounts.keys() e.sort() if self.account_name and len(e) > 1: item = e.index(self.account_name) else: item = 0 self.account_name = e[0] # From: entry l = GtkLabel("From") t.attach(l, 0,1,0,1, xoptions=FILL, xpadding=3) c = GtkOptionMenu() m = GtkMenu() # Generate menu of accounts for menu in e: menustr = "%s: %s <%s>" % \ (menu, self.prefs.accounts[menu][0], self.prefs.accounts[menu][1]) i = GtkMenuItem(menustr) i.show() i.connect('activate', self.select_from, menu) m.append(i) m.set_active(item) c.set_menu(m) t.attach(c, 1,2,0,1, xpadding=4, ypadding=1) # Only display the From: field if there is more than one account if showfrom: l.show() c.show() # To: entry l = GtkButton('To') l.show() l.unset_flags(CAN_FOCUS) l.connect('clicked', self.add_to) t.attach(l, 0,1,1,2, xoptions=FILL, xpadding=3, ypadding=3) # DnD from GnomeCard for To: self.e1 = GtkEntry() self.e1.connect('drag_data_received', self.drag_drop_to) self.e1.drag_dest_set(DEST_DEFAULT_ALL, targets, GDK.ACTION_COPY) self.e1.show() t.attach(self.e1, 1,2,1,2, xpadding=5, ypadding=3) # Subject: entry l = GtkLabel("Subject") l.show() l.set_alignment(0.0, 0.5) t.attach(l, 0,1,2,3, xoptions=FILL, xpadding=3) self.e3 = GtkEntry() t.attach(self.e3, 1,2,2,3, xpadding=5, ypadding=3) self.e3.show() # The 'More' arrow button 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.unset_flags(CAN_FOCUS) b.connect('clicked', self.more_toggle) t.attach(b, 2,3,2,3, xoptions=0) # Cc: entry self.l1 = GtkButton("Cc") self.l1.unset_flags(CAN_FOCUS) self.l1.connect('clicked', self.add_cc) t.attach(self.l1, 0,1,3,4, xoptions=FILL, xpadding=3, ypadding=3) # DnD from GnomeCard for Cc: self.e2 = GtkEntry() self.e2.connect('drag_data_received', self.drag_drop_cc) self.e2.drag_dest_set(DEST_DEFAULT_ALL, targets, GDK.ACTION_COPY) t.attach(self.e2, 1,2,3,4, xpadding=5, ypadding=3) # BCC and from self.l2 = GtkButton("Bcc") self.l2.unset_flags(CAN_FOCUS) self.l2.connect('clicked', self.add_bcc) t.attach(self.l2, 0,1,4,5, xoptions=FILL, xpadding=3, ypadding=3) self.e4 = GtkEntry() self.e4.connect('drag_data_received', self.drag_drop_bcc) self.e4.drag_dest_set(DEST_DEFAULT_ALL, targets, GDK.ACTION_COPY) t.attach(self.e4, 1,2,4,5, xpadding=5, ypadding=3) # Pack the widgets together self.vbox.pack_start(t, expand=FALSE) # Only show if we have a Cc entry if self.has_cc: self.l1.show() self.e2.show() self.l2.show() self.e4.show() ## def get_account_name_for_reply(self): import string # Get all the relevant accounts corresponding to email address ownacc = {} for a in self.prefs.accounts.keys(): username, emailaddr, sigfile, replyaddr, smtpserver, localsendmail \ = self.prefs.accounts[a] if emailaddr != '': ownacc[emailaddr] = a if replyaddr != '': ownacc[replyaddr] = a # Get all to + cc address in the to-be-replied msg to = self.msg.getaddrlist('To') cc = self.msg.getaddrlist('Cc') allAddr = map (lambda x: x[1], to + cc) for addr in allAddr: if addr in ownacc.keys(): return ownacc[addr] # return the first matched emailAddr's # accout name return self.prefs.defacc # else, the default account ## Toggle more or less information for headers def more_toggle(self, button=None): if not self.more: self.l1.show() self.e2.show() self.l2.show() self.e4.show() self.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) b.unset_flags(CAN_FOCUS) self.ht.attach(b, 2,3,2,3, xoptions=0) else: self.l1.hide() self.e2.hide() self.l2.hide() self.e4.hide() self.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) b.unset_flags(CAN_FOCUS) self.ht.attach(b, 2,3,2,3, xoptions=0) self.ht.queue_resize() ## Launch the Address List with callback to To: def add_to(self, button): self.field = 'to' self.fld.addresslist(None, self, self.win) ## Launch the Address List with callback to CC: def add_cc(self, button): self.field = 'cc' self.fld.addresslist(None, self, self.win) ## Launch the Address List with callback to Bcc: def add_bcc(self, button): self.field = 'bcc' self.fld.addresslist(None, self, self.win) ## ## Method destroy (self) ## ## Destructor, clean up window stuff. ## ## def destroy(self, button=None, eventx=None): # If this is in edit mode, save the document again if self.mode == EDIT and self.mail_is_sent == 0: self.mail_is_sent = 1 self.send_mail_later() self.ht.destroy() self.ed.destroy() self.win.destroy() # Decrease window reference counter self.fld.num_edits = self.fld.num_edits - 1 # Destroy the main window if mailto is set if self.mailto != None: self.fld.prefs.mailnotify = self.fld.oldmailnotify self.fld.quit() def fill_paragraph(self, button=None): self.ed.format_paragraph( int(self.prefs.fillwidth ) ) ## ## Method expand (self, to, cc, bcc) ## ## Expand nicknames from the the address list. ## ## def expand(self, to, cc, bcc): import marshal import expand # Load address list try: alist = marshal.load(open(self.prefs.alistfile)) except: # No defined address list, just return return 0 # Expand names to cc: and to: for name in to: if alist.has_key(name): if self.prefs.expand: e = expand.ExpandWindow(self.fld, name, alist[name], 'To:') e.mainloop() if self.fld.status == 0: continue to[to.index(name)] = alist[name] for name in cc: if alist.has_key(name): if self.prefs.expand: e = expand.ExpandWindow(self.fld, name, alist[name], 'Cc:') e.mainloop() if self.fld.status == 0: continue cc[cc.index(name)] = alist[name] # Expand names for blind copy recipients as well for name in bcc: if alist.has_key(name): if self.prefs.expand: e = expand.ExpandWindow(self.fld, name, alist[name], 'Bcc:') e.mainloop() if self.fld.status == 0: continue bcc[bcc.index(name)] = alist[name] del alist ## ## Method send_mail_later (self, None) ## ## Send mail later by appending to drafts folder (callback). ## ## def send_mail_later(self, button=None): self.send_mail(immediate=0) def privacy_dialog(self, button=None): self.privacy.parent = self.win self.privacy.dialog(self.fld, self.e1.get_text()) ## ## Method send_mail (self, button, immediate) ## ## Send mail message (callback). ## ## def send_mail(self, button=None, immediate=1): import string, time # Get the account we should use for sending the mail account = self.prefs.accounts[self.account_name] username, emailaddr, sigfile, replyaddr, smtpserver, localsendmail = account # Extract To and CC widget contents to = self.e1.get_text() cc = self.e2.get_text() bcc = self.e4.get_text() # Make the from address proper frm = mime_encode(username) +" <"+ emailaddr +">" if to == '': w = GnomeErrorDialog(err1) w.set_parent(self.win) w.show() return 0 # Add (B)CC to list of recipients to = string.split(string.replace(to, ', ', ','), ',') if cc != '': cc = string.split(string.replace(cc, ', ', ','), ',') else: cc = [] if bcc != '': bcc = string.split(string.replace(bcc, ', ', ','), ',') else: bcc = [] # Handle the case of "Bar, Foo" foo@bar address entries verify_split(to) verify_split(cc) verify_split(bcc) # Expand nicknames self.expand(to, cc, bcc) # Mimify the to and cc stuff newto = map(mime_encode, to) newcc = map(mime_encode, cc) newbcc = map(mime_encode, bcc) # Need to have separate recipients to handle to/cc/bcc correctly recp_tocc = 'To: %s\n' % string.join(to, ',\n ') if cc != []: recp_tocc = recp_tocc + 'Cc: %s\n' % string.join(cc, ',\n ') if bcc != []: recp_bcc = 'To: %s\n' % string.join(bcc, ',\n ') headers = 'From: %s\n' % frm if replyaddr != '': headers = headers + 'Reply-To: %s\n' % mime_encode(replyaddr) headers = headers + 'Subject: %s\n' % mime_encode(self.e3.get_text()) # No need to mime-encode this one since content is static headers = headers + 'X-Mailer: Pygmy (v%s)\n' % self.prefs.version # Set date header entry headers = headers + 'Date: %s\n' % formatdate(localtime=1) # Add the rest of the headers for h in self.headers.keys(): headers = headers + '%s: %s\n' % (h, mime_encode(self.headers[h])) charset = '' if (string.find(self.prefs.compose_font,'8859-') > 0): charset = 'iso-' + (self.prefs.compose_font[(string.find(self.prefs.compose_font,'8859-')):]) # Message body msg = '' if len(self.attachments) > 0: # Compose multipart message with inlined attachments s = cStringIO.StringIO() t = MimeWriter.MimeWriter(s) f = t.startmultipartbody('mixed') f.write("This is a multi-part message in MIME format.\n") # Write text body if there is any contents textlength = self.ed.get_length() if textlength > 0: p = t.nextpart() txt = self.ed.get_text() # Check if text needs quoting if filter(needsquoting, txt) != '' or string.find(txt, '\n.\n') != -1: # Must quote the text enc = 'quoted-printable' p.addheader('Content-Transfer-Encoding', enc) inbody = cStringIO.StringIO(txt) outbody = cStringIO.StringIO() pygmymimetools.encode(inbody, outbody, enc) txt = '\n' + outbody.getvalue() del inbody, outbody while string.find(txt, '\n.\n') != -1: txt = string.replace(txt, '\n.\n', '\n=%s\n' % DOT) txt = txt[1:] # Strip first newline character txt = self.privacy.handle_write(txt, self.send_mail) if not txt: # Will be None if e.g. passphrase was wrong return if charset: b = p.startbody('text/plain' + '; charset="' + charset + '"') else: b = p.startbody('text/plain') b.write(txt) # Write all the attachments enc = 'base64' for name, type, sm in self.attachments: p = t.nextpart() p.addheader('Content-Transfer-Encoding', enc) filename = os.path.basename(name) b = p.startbody(type, [('name', filename)]) if sm != None: # A submessage may be included (e.g. for forward) pygmymimetools.encode(cStringIO.StringIO(sm.getbodytext()), b, enc) del sm else: # ... or just a file name reference (for new messages) file = open(name) pygmymimetools.encode(file, b, enc) file.close() t.lastpart() # Some clients need this to catch multipart msgs, e.g. pine msg = msg + 'Mime-Version: 1.0\n' + s.getvalue() s.close() del s, t, f else: # Compose singlepart message msg = '\n' + self.ed.get_text() # Check if text needs quoting if filter(needsquoting, msg) != '' or string.find(msg, '\n.\n') != -1: # Must quote the text enc = 'quoted-printable' inbody = cStringIO.StringIO(msg) outbody = cStringIO.StringIO() pygmymimetools.encode(inbody, outbody, enc) msg = outbody.getvalue() del inbody, outbody if charset: headers = headers + "Content-Type: text/plain" + '; charset="' + charset + '"\n' else: headers = headers + "Content-Type: text/plain\n" headers = headers + "Content-Transfer-Encoding: %s\n" % enc # Must encode single dots on lines (quoted-printable) while string.find(msg, '\n.\n') != -1: msg = string.replace(msg, '\n.\n', '\n=%s\n' % DOT) # Need encryption or signature msg = self.privacy.handle_write(msg, self.send_mail) if msg == None: # Will be None if e.g. passphrase was wrong return # Send mail to target host if immediate send was specified if immediate: target = 'sent-mail' if smtpserver != '': # Use direct connection to mail server message = recp_tocc + headers + msg if not sendmail.sendmail_server(smtpserver, frm, to+cc, message): return 0 if bcc != []: message = recp_bcc + headers + msg if not sendmail.sendmail_server(smtpserver, frm, bcc, message): return 0 else: # Use sendmail command at localhost message = recp_tocc + headers + msg if not sendmail.sendmail_cmd(localsendmail, message): return 0 if bcc != []: message = recp_bcc + headers + msg if not sendmail.sendmail_cmd(localsendmail, message): return 0 # Popup that the message was successfully sent/queued if self.prefs.confirmsend: w = GnomeOkDialog(pop1) w.set_parent(self.win) w.show() else: target = 'drafts' # Notify the user that the message is drafted if self.prefs.confirmsend: w = GnomeOkDialog(pop2) w.set_parent(self.win) w.show() # Add a date header and the From line at the top of the message prefix = 'From %s %s\n' % (emailaddr, formatdate(localtime=1, fromline=1)) # Open the target folder and store the message there fo = open(self.prefs.folders+'/'+target, 'a') fo.write(prefix) fo.write(recp_tocc) if not immediate: # Write the bcc entry if the mail is to be sent later # so we can reconstruct the bcc field if the mail is edited fo.write('Bcc: %s\n' % string.join(bcc, ',\n ')) fo.write(headers) fo.write(msg) fo.write('\n\n') fo.close() # Update the folder index for the target folder pathname = folderops.get_folder_pathname(self.prefs.folders, target) folderops.update_folder_index(pathname) self.fld.unread[target] = self.fld.unread[target] + 1 self.fld.total[target] = self.fld.total[target] + 1 # Update folder if currently viewed in list if self.fld.mbox == target: self.fld.update_folderview(self.fld.mbox) # Update folder tree view self.fld.update_foldertree_nodes([target]) # Check if we should call destroy if self.mail_is_sent == 0: self.mail_is_sent = 1 self.destroy() return 1 ## ## Function mime_encode (line) ## ## Encode mime headers ## ## def mime_encode(line): import re, string reg = re.compile('[\t\n=?\177-\377]') # quote these in header newline = '' pos = 0 while 1: res = reg.search(line, pos) if res is None: break newline = newline + line[pos:res.start(0)] + \ string.upper('=%02x' % ord(res.group(0))) pos = res.end(0) # This means we had no mime-chars, just return the original if pos == 0: return line line = newline + line[pos:] # Quote spaces as well (we don't want these to be encoded otherwise) line = string.replace(line, ' ', '_') # Return an quoted-printable armoured version of the line return '=?%s?Q?%s?=' % (mimify.CHARSET, line)