def __init__(self, path, spawn=None, thread=None, **kwargs): """ :param path: path to the file to be edited :type path: str :param spawn: force running edtor in a new terminal :type spawn: bool :param thread: run asynchronously, don't block alot :type thread: bool """ self.spawn = spawn if spawn is None: self.spawn = settings.get('editor_spawn') self.thread = thread if thread is None: self.thread = settings.get('editor_in_thread') editor_cmdstring = None if os.path.isfile('/usr/bin/editor'): editor_cmdstring = '/usr/bin/editor' editor_cmdstring = os.environ.get('EDITOR', editor_cmdstring) editor_cmdstring = settings.get('editor_cmd') or editor_cmdstring logging.debug('using editor_cmd: %s' % editor_cmdstring) self.cmdlist = None if '%s' in editor_cmdstring: cmdstring = editor_cmdstring.replace('%s', helper.shell_quote(path)) self.cmdlist = split_commandstring(cmdstring) else: self.cmdlist = split_commandstring(editor_cmdstring) + [path] logging.debug({'spawn: ': self.spawn, 'in_thread': self.thread}) ExternalCommand.__init__(self, self.cmdlist, spawn=self.spawn, thread=self.thread, **kwargs)
def get_authors_string(self, own_addrs=None, replace_own=None): """ returns a string of comma-separated authors Depending on settings, it will substitute "me" for author name if address is user's own. :param own_addrs: list of own email addresses to replace :type own_addrs: list of str :param replace_own: whether or not to actually do replacement :type replace_own: bool :rtype: str """ if replace_own is None: replace_own = settings.get('thread_authors_replace_me') if replace_own: if own_addrs is None: own_addrs = settings.get_addresses() authorslist = [] for aname, aaddress in self.get_authors(): if aaddress in own_addrs: aname = settings.get('thread_authors_me') if not aname: aname = aaddress if aname not in authorslist: authorslist.append(aname) return ', '.join(authorslist) else: return self._notmuch_authors_string
def __init__(self, path, spawn=None, thread=None, **kwargs): """ :param path: path to the file to be edited :type path: str :param spawn: run command in a new terminal :type spawn: bool :param thread: run asynchronously, don't block alot :type thread: bool """ self.path = path if spawn != None: self.spawn = spawn else: self.spawn = settings.get('editor_spawn') if thread != None: self.thread = thread else: self.thread = settings.get('editor_in_thread') self.editor_cmd = None if os.path.isfile('/usr/bin/editor'): self.editor_cmd = '/usr/bin/editor' self.editor_cmd = os.environ.get('EDITOR', self.editor_cmd) self.editor_cmd = settings.get('editor_cmd') or self.editor_cmd logging.debug('using editor_cmd: %s' % self.editor_cmd) ExternalCommand.__init__(self, self.editor_cmd, path=self.path, spawn=self.spawn, thread=self.thread, **kwargs)
def get_authors_string(self, own_addrs=None, replace_own=None): """ returns a string of comma-separated authors Depending on settings, it will substitute "me" for author name if address is user's own. :param own_addrs: list of own email addresses to replace :type own_addrs: list of str :param replace_own: whether or not to actually do replacement :type replace_own: bool :rtype: str """ if replace_own == None: replace_own = settings.get("thread_authors_replace_me") if replace_own: if own_addrs == None: own_addrs = settings.get_addresses() authorslist = [] for aname, aaddress in self.get_authors(): if aaddress in own_addrs: aname = settings.get("thread_authors_me") if not aname: aname = aaddress if not aname in authorslist: authorslist.append(aname) return ", ".join(authorslist) else: return self._notmuch_authors_string
def apply(self, ui): # look if this makes sense: do we have any accounts set up? my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return # get message to forward if not given in constructor if not self.message: self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() envelope = Envelope() if self.inline: # inline mode # set body text name, address = self.message.get_author() timestamp = self.message.get_date() qf = settings.get_hook('forward_prefix') if qf: quote = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quote = 'Forwarded message from %s (%s):\n' % (name or address, timestamp) mailcontent = quote quotehook = settings.get_hook('text_quote') if quotehook: mailcontent += quotehook(self.message.accumulate_body()) else: quote_prefix = settings.get('quote_prefix') for line in self.message.accumulate_body().splitlines(): mailcontent += quote_prefix + line + '\n' envelope.body = mailcontent else: # attach original mode # attach original msg mail.set_type('message/rfc822') mail['Content-Disposition'] = 'attachment' envelope.attach(Attachment(mail)) # copy subject subject = decode_header(mail.get('Subject', '')) subject = 'Fwd: ' + subject forward_subject_hook = settings.get_hook('forward_subject') if forward_subject_hook: subject = forward_subject_hook(subject) else: fsp = settings.get('forward_subject_prefix') if not subject.startswith(('Fwd:', fsp)): subject = fsp + subject envelope.add('Subject', subject) # set From realname, address = recipient_to_from(mail, my_accounts) envelope.add('From', '%s <%s>' % (realname, address)) # continue to compose ui.apply_command( ComposeCommand(envelope=envelope, spawn=self.force_spawn))
def apply(self, ui): # look if this makes sense: do we have any accounts set up? my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return # get message to forward if not given in constructor if not self.message: self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() envelope = Envelope() if self.inline: # inline mode # set body text name, address = self.message.get_author() timestamp = self.message.get_date() qf = settings.get_hook('forward_prefix') if qf: quote = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quote = 'Forwarded message from %s (%s):\n' % ( name or address, timestamp) mailcontent = quote quotehook = settings.get_hook('text_quote') if quotehook: mailcontent += quotehook(self.message.accumulate_body()) else: quote_prefix = settings.get('quote_prefix') for line in self.message.accumulate_body().splitlines(): mailcontent += quote_prefix + line + '\n' envelope.body = mailcontent else: # attach original mode # attach original msg mail.set_type('message/rfc822') mail['Content-Disposition'] = 'attachment' envelope.attach(Attachment(mail)) # copy subject subject = decode_header(mail.get('Subject', '')) subject = 'Fwd: ' + subject forward_subject_hook = settings.get_hook('forward_subject') if forward_subject_hook: subject = forward_subject_hook(subject) else: fsp = settings.get('forward_subject_prefix') if not subject.startswith(('Fwd:', fsp)): subject = fsp + subject envelope.add('Subject', subject) # set From realname, address = recipient_to_from(mail, my_accounts) envelope.add('From', '%s <%s>' % (realname, address)) # continue to compose ui.apply_command(ComposeCommand(envelope=envelope, spawn=self.force_spawn))
def determine_sender(mail, action='reply'): """ Inspect a given mail to reply/forward/bounce and find the most appropriate account to act from and construct a suitable From-Header to use. :param mail: the email to inspect :type mail: `email.message.Message` :param action: intended use case: one of "reply", "forward" or "bounce" :type action: str """ assert action in ['reply', 'forward', 'bounce'] realname = None address = None # get accounts my_accounts = settings.get_accounts() assert my_accounts, 'no accounts set!' # extract list of addresses to check for my address # X-Envelope-To and Envelope-To are used to store the recipient address # if not included in other fields candidate_addresses = getaddresses(mail.get_all('To', []) + mail.get_all('Cc', []) + mail.get_all('Delivered-To', []) + mail.get_all('X-Envelope-To', []) + mail.get_all('Envelope-To', []) + mail.get_all('From', [])) logging.debug('candidate addresses: %s' % candidate_addresses) # pick the most important account that has an address in candidates # and use that accounts realname and the address found here for account in my_accounts: acc_addresses = account.get_addresses() for alias in acc_addresses: if realname is not None: break regex = re.compile(re.escape(alias), flags=re.IGNORECASE) for seen_name, seen_address in candidate_addresses: if regex.match(seen_address): logging.debug("match!: '%s' '%s'" % (seen_address, alias)) if settings.get(action + '_force_realname'): realname = account.realname else: realname = seen_name if settings.get(action + '_force_address'): address = account.address else: address = seen_address # revert to default account if nothing found if realname is None: account = my_accounts[0] realname = account.realname address = account.address logging.debug('using realname: "%s"' % realname) logging.debug('using address: %s' % address) from_value = address if realname == '' else '%s <%s>' % (realname, address) return from_value, account
def determine_sender(mail, action='reply'): """ Inspect a given mail to reply/forward/bounce and find the most appropriate account to act from and construct a suitable From-Header to use. :param mail: the email to inspect :type mail: `email.message.Message` :param action: intended use case: one of "reply", "forward" or "bounce" :type action: str """ assert action in ['reply', 'forward', 'bounce'] realname = None address = None # get accounts my_accounts = settings.get_accounts() assert my_accounts, 'no accounts set!' # extract list of recipients to check for my address rec_to = filter(lambda x: x, mail.get('To', '').split(',')) rec_cc = filter(lambda x: x, mail.get('Cc', '').split(',')) delivered_to = mail.get('Delivered-To', None) recipients = rec_to + rec_cc if delivered_to is not None: recipients.append(delivered_to) logging.debug('recipients: %s' % recipients) # pick the most important account that has an address in recipients # and use that accounts realname and the found recipient address for account in my_accounts: acc_addresses = account.get_addresses() for alias in acc_addresses: if realname is not None: break regex = re.compile(alias) for rec in recipients: seen_name, seen_address = parseaddr(rec) if regex.match(seen_address): logging.debug("match!: '%s' '%s'" % (seen_address, alias)) if settings.get(action + '_force_realname'): realname = account.realname else: realname = seen_name if settings.get(action + '_force_address'): address = account.address else: address = seen_address # revert to default account if nothing found if realname is None: account = my_accounts[0] realname = account.realname address = account.address logging.debug('using realname: "%s"' % realname) logging.debug('using address: %s' % address) from_value = address if realname == '' else '%s <%s>' % (realname, address) return from_value, account
def determine_sender(mail, action='reply'): """ Inspect a given mail to reply/forward/bounce and find the most appropriate account to act from and construct a suitable From-Header to use. :param mail: the email to inspect :type mail: `email.message.Message` :param action: intended use case: one of "reply", "forward" or "bounce" :type action: str """ assert action in ['reply', 'forward', 'bounce'] realname = None address = None # get accounts my_accounts = settings.get_accounts() assert my_accounts, 'no accounts set!' # extract list of addresses to check for my address # X-Envelope-To and Envelope-To are used to store the recipient address # if not included in other fields candidate_addresses = getaddresses( mail.get_all('To', []) + mail.get_all('Cc', []) + mail.get_all('Delivered-To', []) + mail.get_all('X-Envelope-To', []) + mail.get_all('Envelope-To', []) + mail.get_all('From', [])) logging.debug('candidate addresses: %s' % candidate_addresses) # pick the most important account that has an address in candidates # and use that accounts realname and the address found here for account in my_accounts: acc_addresses = account.get_addresses() for alias in acc_addresses: if realname is not None: break regex = re.compile(re.escape(alias), flags=re.IGNORECASE) for seen_name, seen_address in candidate_addresses: if regex.match(seen_address): logging.debug("match!: '%s' '%s'" % (seen_address, alias)) if settings.get(action + '_force_realname'): realname = account.realname else: realname = seen_name if settings.get(action + '_force_address'): address = account.address else: address = seen_address # revert to default account if nothing found if realname is None: account = my_accounts[0] realname = account.realname address = account.address logging.debug('using realname: "%s"' % realname) logging.debug('using address: %s' % address) from_value = address if realname == '' else '%s <%s>' % (realname, address) return from_value, account
def recipient_to_from(mail, my_accounts): """ construct a suitable From-Header for forwards/replies to a given mail. :param mail: the email to inspect :type mail: `email.message.Message` :param my_accounts: list of accounts from which to chose from :type my_accounts: list of `alot.account.Account` """ realname = None address = None # extract list of recipients to check for my address rec_to = filter(lambda x: x, mail.get('To', '').split(',')) rec_cc = filter(lambda x: x, mail.get('Cc', '').split(',')) delivered_to = mail.get('Delivered-To', None) recipients = rec_to + rec_cc if delivered_to is not None: recipients.append(delivered_to) logging.debug('recipients: %s' % recipients) # pick the most important account that has an address in recipients # and use that accounts realname and the found recipient address for acc in my_accounts: acc_addresses = acc.get_addresses() for alias_re in acc_addresses: if realname is not None: break regex = re.compile(alias_re) for rec in recipients: seen_name, seen_address = parseaddr(rec) if regex.match(seen_address): logging.debug("match!: '%s' '%s'" % (seen_address, alias_re)) if settings.get('reply_force_realname'): realname = acc.realname else: realname = seen_name if settings.get('reply_force_address'): address = acc.address else: address = seen_address # revert to default account if nothing found if realname is None: realname = my_accounts[0].realname address = my_accounts[0].address logging.debug('using realname: "%s"' % realname) logging.debug('using address: %s' % address) return address if realname == '' else '%s <%s>' % (realname, address)
def text_quote(message): # avoid importing a big module by using a simple heuristic to guess the # right encoding def decode(s, encodings=('ascii', 'utf8', 'latin1')): for encoding in encodings: try: return s.decode(encoding) except UnicodeDecodeError: pass return s.decode('ascii', 'ignore') lines = message.splitlines() # delete empty lines at beginning and end (some email client insert these # outside of the pgp signed message...) if lines[0] == '' or lines[-1] == '': from itertools import dropwhile lines = list(dropwhile(lambda l: l == '', lines)) lines = list(reversed(list(dropwhile( lambda l: l == '', reversed(lines))))) if len(lines) > 0 and lines[0] == '-----BEGIN PGP MESSAGE-----' \ and lines[-1] == '-----END PGP MESSAGE-----': try: sigs, d = crypto.decrypt_verify(message.encode('utf-8')) message = decode(d) except errors.GPGProblem: pass elif len(lines) > 0 and lines[0] == '-----BEGIN PGP SIGNED MESSAGE-----' \ and lines[-1] == '-----END PGP SIGNATURE-----': # gpgme does not seem to be able to extract the plain text part of # a signed message import gnupg gpg = gnupg.GPG() d = gpg.decrypt(message.encode('utf8')) message = d.data.decode('utf8') quote_prefix = settings.get('quote_prefix') return "\n".join([quote_prefix + line for line in message.splitlines()])
def apply(self, ui): if self.envelope is None: self.envelope = Envelope() if self.template is not None: #get location of tempsdir, containing msg templates tempdir = settings.get('template_dir') tempdir = os.path.expanduser(tempdir) if not tempdir: xdgdir = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) tempdir = os.path.join(xdgdir, 'alot', 'templates') path = os.path.expanduser(self.template) if not os.path.dirname(path): # use tempsdir if not os.path.isdir(tempdir): ui.notify('no templates directory: %s' % tempdir, priority='error') return path = os.path.join(tempdir, path) if not os.path.isfile(path): ui.notify('could not find template: %s' % path, priority='error') return try: self.envelope.parse_template(open(path).read()) except Exception, e: ui.notify(str(e), priority='error') return
def __init__(self, all=False, separately=False, raw=False, add_tags=False, **kwargs): """ :param all: print all, not only selected messages :type all: bool :param separately: call print command once per message :type separately: bool :param raw: pipe raw message string to print command :type raw: bool :param add_tags: add 'Tags' header to the message :type add_tags: bool """ # get print command cmd = settings.get('print_cmd') or '' # set up notification strings if all: confirm_msg = 'print all messages in thread?' ok_msg = 'printed thread using %s' % cmd else: confirm_msg = 'print selected message?' ok_msg = 'printed message using %s' % cmd # no print cmd set noop_msg = 'no print command specified. Set "print_cmd" in the '\ 'global section.' PipeCommand.__init__(self, [cmd], all=all, separately=separately, background=True, shell=False, format='raw' if raw else 'decoded', add_tags=add_tags, noop_msg=noop_msg, confirm_msg=confirm_msg, done_msg=ok_msg, **kwargs)
def apply(self, ui): try: ui.dbman.flush() if callable(self.callback): self.callback() logging.debug('flush complete') if ui.db_was_locked: if not self.silent: ui.notify('changes flushed') ui.db_was_locked = False ui.update() except DatabaseLockedError: timeout = settings.get('flush_retry_timeout') def f(*args): self.apply(ui) ui.mainloop.set_alarm_in(timeout, f) if not ui.db_was_locked: if not self.silent: ui.notify( 'index locked, will try again in %d secs' % timeout) ui.db_was_locked = True ui.update() return
def apply(self, ui): if settings.get("bug_on_exit"): if (yield ui.choice("realy quit?", select="yes", cancel="no", msg_position="left")) == "no": return for b in ui.buffers: b.cleanup() ui.exit()
def apply(self, ui): if self.envelope == None: self.envelope = Envelope() if self.template is not None: #get location of tempsdir, containing msg templates tempdir = settings.get('template_dir') tempdir = os.path.expanduser(tempdir) if not tempdir: xdgdir = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) tempdir = os.path.join(xdgdir, 'alot', 'templates') path = os.path.expanduser(self.template) if not os.path.dirname(path): # use tempsdir if not os.path.isdir(tempdir): ui.notify('no templates directory: %s' % tempdir, priority='error') return path = os.path.join(tempdir, path) if not os.path.isfile(path): ui.notify('could not find template: %s' % path, priority='error') return try: self.envelope.parse_template(open(path).read()) except Exception, e: ui.notify(str(e), priority='error') return
def apply(self, ui): if self.envelope is None: self.envelope = Envelope() if self.template is not None: # get location of tempsdir, containing msg templates tempdir = settings.get("template_dir") tempdir = os.path.expanduser(tempdir) if not tempdir: xdgdir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) tempdir = os.path.join(xdgdir, "alot", "templates") path = os.path.expanduser(self.template) if not os.path.dirname(path): # use tempsdir if not os.path.isdir(tempdir): ui.notify("no templates directory: %s" % tempdir, priority="error") return path = os.path.join(tempdir, path) if not os.path.isfile(path): ui.notify("could not find template: %s" % path, priority="error") return try: self.envelope.parse_template(open(path).read()) except Exception, e: ui.notify(str(e), priority="error") return
def refresh(self, thread=None): """refresh thread metadata from the index""" if not thread: thread = self._dbman._get_notmuch_thread(self._id) self._total_messages = thread.get_total_messages() self._notmuch_authors_string = thread.get_authors() subject_type = settings.get('thread_subject') if subject_type == 'notmuch': subject = thread.get_subject() elif subject_type == 'oldest': try: first_msg = list(thread.get_toplevel_messages())[0] subject = first_msg.get_header('subject') except IndexError: subject = '' self._subject = subject self._authors = None ts = thread.get_oldest_date() try: self._oldest_date = datetime.fromtimestamp(ts) except ValueError: # year is out of range self._oldest_date = None try: timestamp = thread.get_newest_date() self._newest_date = datetime.fromtimestamp(timestamp) except ValueError: # year is out of range self._newest_date = None self._tags = set([t for t in thread.get_tags()]) self._messages = {} # this maps messages to its children self._toplevel_messages = []
def apply(self, ui): try: ui.dbman.flush() if callable(self.callback): self.callback() logging.debug('flush complete') if ui.db_was_locked: if not self.silent: ui.notify('changes flushed') ui.db_was_locked = False ui.update() except DatabaseLockedError: timeout = settings.get('flush_retry_timeout') def f(*args): self.apply(ui) ui.mainloop.set_alarm_in(timeout, f) if not ui.db_was_locked: if not self.silent: ui.notify('index locked, will try again in %d secs' % timeout) ui.db_was_locked = True ui.update() return
def apply(self, ui): if settings.get('bug_on_exit'): if (yield ui.choice('realy quit?', select='yes', cancel='no', msg_position='left')) == 'no': return for b in ui.buffers: b.cleanup() ui.exit()
def main(): """The main entry point to alot. It parses the command line and prepares for the user interface main loop to run.""" options, command = parser() # logging root_logger = logging.getLogger() for log_handler in root_logger.handlers: root_logger.removeHandler(log_handler) root_logger = None numeric_loglevel = getattr(logging, options.debug_level.upper(), None) logformat = '%(levelname)s:%(module)s:%(message)s' logging.basicConfig(level=numeric_loglevel, filename=options.logfile, filemode='w', format=logformat) # locate alot config files if options.config is None: alotconfig = os.path.join( os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), 'alot', 'config') if os.path.exists(alotconfig): settings.alot_rc_path = alotconfig else: settings.alot_rc_path = options.config settings.notmuch_rc_path = options.notmuch_config try: settings.read_config() settings.read_notmuch_config() except (ConfigError, OSError, IOError) as e: sys.exit(e) # store options given by config swiches to the settingsManager: if options.colour_mode: settings.set('colourmode', options.colour_mode) # get ourselves a database manager indexpath = settings.get_notmuch_setting('database', 'path') indexpath = options.mailindex_path or indexpath dbman = DBManager(path=indexpath, ro=options.read_only) # determine what to do if command is None: try: cmdstring = settings.get('initial_command') except CommandParseError as err: sys.exit(err) elif command.subcommand in _SUBCOMMANDS: cmdstring = ' '.join(options.command) # set up and start interface UI(dbman, cmdstring) # run the exit hook exit_hook = settings.get_hook('exit') if exit_hook is not None: exit_hook()
def main(): """The main entry point to alot. It parses the command line and prepares for the user interface main loop to run.""" options, command = parser() # logging root_logger = logging.getLogger() for log_handler in root_logger.handlers: root_logger.removeHandler(log_handler) root_logger = None numeric_loglevel = getattr(logging, options.debug_level.upper(), None) logformat = '%(levelname)s:%(module)s:%(message)s' logging.basicConfig(level=numeric_loglevel, filename=options.logfile, filemode='w', format=logformat) # locate alot config files if options.config is None: alotconfig = os.path.join( os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), 'alot', 'config') if not os.path.exists(alotconfig): alotconfig = None else: alotconfig = options.config try: settings.read_config(alotconfig) settings.read_notmuch_config(options.notmuch_config) except (ConfigError, OSError, IOError) as e: sys.exit(e) # store options given by config swiches to the settingsManager: if options.colour_mode: settings.set('colourmode', options.colour_mode) # get ourselves a database manager indexpath = settings.get_notmuch_setting('database', 'path') indexpath = options.mailindex_path or indexpath dbman = DBManager(path=indexpath, ro=options.read_only) # determine what to do if command is None: try: cmdstring = settings.get('initial_command') except CommandParseError as err: sys.exit(err) elif command.subcommand in _SUBCOMMANDS: cmdstring = ' '.join(options.command) # set up and start interface UI(dbman, cmdstring) # run the exit hook exit_hook = settings.get_hook('exit') if exit_hook is not None: exit_hook()
def __init__(self, message, even=False, folded=True, raw=False, all_headers=False, depth=0, bars_at=[]): """ :param message: the message to display :type message: alot.db.Message :param even: use messagesummary_even theme for summary :type even: bool :param folded: fold message initially :type folded: bool :param raw: show message source initially :type raw: bool :param all_headers: show all headers initially :type all_headers: bool :param depth: number of characters to shift content to the right :type depth: int :param bars_at: defines for each column of the indentation whether to use a vertical bar instead of a space. :type bars_at: list(bool) """ self.message = message self.mail = self.message.get_email() self.depth = depth self.bars_at = bars_at self.even = even self.folded = folded self.show_raw = raw self.show_all_headers = all_headers # define subwidgets that will be created on demand self.sumline = None self.headerw = None self.attachmentw = None self.bodyw = None self.sourcew = None # set available and to be displayed headers self._all_headers = list(set(self.mail.keys())) displayed = settings.get('displayed_headers') self._filtered_headers = [ k for k in displayed if k.lower() == 'tags' or k in self.mail ] self._displayed_headers = None bars = settings.get_theming_attribute('thread', 'arrow_bars') self.arrow_bars_att = bars heads = settings.get_theming_attribute('thread', 'arrow_heads') self.arrow_heads_att = heads logging.debug(self.arrow_heads_att) self.rebuild() # this will build self.pile urwid.WidgetWrap.__init__(self, self.pile)
def __init__(self, cmd, stdin=None, shell=False, spawn=False, refocus=True, thread=False, on_success=None, **kwargs): """ :param cmd: the command to call :type cmd: list or str :param stdin: input to pipe to the process :type stdin: file or str :param spawn: run command in a new terminal :type spawn: bool :param shell: let shell interpret command string :type shell: bool :param thread: run asynchronously, don't block alot :type thread: bool :param refocus: refocus calling buffer after cmd termination :type refocus: bool :param on_success: code to execute after command successfully exited :type on_success: callable """ logging.debug({'spawn': spawn}) # make sure cmd is a list of str if isinstance(cmd, unicode): # convert cmdstring to list: in case shell==True, # Popen passes only the first item in the list to $SHELL cmd = [cmd] if shell else split_commandstring(cmd) # determine complete command list to pass touchhook = settings.get_hook('touch_external_cmdlist') # filter cmd, shell and thread through hook if defined if touchhook is not None: logging.debug('calling hook: touch_external_cmdlist') res = touchhook(cmd, shell=shell, spawn=spawn, thread=thread) logging.debug('got: %s' % res) cmd, shell, self.in_thread = res # otherwise if spawn requested and X11 is running elif spawn: if 'DISPLAY' in os.environ: term_cmd = settings.get('terminal_cmd', '') logging.info('spawn in terminal: %s' % term_cmd) termcmdlist = split_commandstring(term_cmd) cmd = termcmdlist + cmd else: thread = False self.cmdlist = cmd self.stdin = stdin self.shell = shell self.refocus = refocus self.in_thread = thread self.on_success = on_success Command.__init__(self, **kwargs)
def apply(self, ui): msg = "index not fully synced. " if ui.db_was_locked else "" if settings.get("bug_on_exit") or ui.db_was_locked: msg += "really quit?" if (yield ui.choice(msg, select="yes", cancel="no", msg_position="left")) == "no": return for b in ui.buffers: b.cleanup() ui.exit()
def __init__(self, tid, dbman): self.dbman = dbman self.thread = dbman.get_thread(tid) self.tag_widgets = [] self.display_content = settings.get('display_content_in_threadline') self.structure = None self.rebuild() normal = self.structure['normal'] focussed = self.structure['focus'] urwid.AttrMap.__init__(self, self.columns, normal, focussed)
def apply(self, ui): msg = 'index not fully synced. ' if ui.db_was_locked else '' if settings.get('bug_on_exit') or ui.db_was_locked: msg += 'really quit?' if (yield ui.choice(msg, select='yes', cancel='no', msg_position='left')) == 'no': return for b in ui.buffers: b.cleanup() ui.exit()
def apply(self, ui): if self.buffer == None: self.buffer = ui.current_buffer if len(ui.buffers) == 1: if settings.get('quit_on_last_bclose'): logging.info('closing the last buffer, exiting') ui.apply_command(ExitCommand()) else: logging.info('not closing last remaining buffer as ' 'global.quit_on_last_bclose is set to False') else: ui.buffer_close(self.buffer)
def apply(self, ui): try: ui.dbman.flush() except DatabaseLockedError: timeout = settings.get('flush_retry_timeout') def f(*args): self.apply(ui) ui.mainloop.set_alarm_in(timeout, f) ui.notify('index locked, will try again in %d secs' % timeout) ui.update() return
def _get_headers(self): if self.display_all_headers is True: if self._all_headers_tree is None: self._all_headers_tree = self.construct_header_pile() ret = self._all_headers_tree else: if self._default_headers_tree is None: headers = settings.get('displayed_headers') self._default_headers_tree = self.construct_header_pile( headers) ret = self._default_headers_tree return ret
def apply(self, ui): tags = settings.get('my_tag_list') if len(tags) == 0: tags = ui.dbman.get_all_tags() blists = ui.get_buffers_of_type(buffers.TagListBuffer) if blists: buf = blists[0] buf.tags = tags buf.rebuild() ui.buffer_focus(buf) else: ui.buffer_open(buffers.TagListBuffer(ui, tags, self.filtfun))
def apply(self, ui): tbuffer = ui.current_buffer logging.debug('matching lines %s...' % (self.query)) if self.query is None: messagetrees = [tbuffer.get_selected_messagetree()] else: messagetrees = tbuffer.messagetrees() if self.query != '*': def matches(msgt): msg = msgt.get_message() return msg.matches(self.query) messagetrees = filter(matches, messagetrees) for mt in messagetrees: # determine new display values for this message if self.visible == 'toggle': visible = mt.is_collapsed(mt.root) else: visible = self.visible if self.raw == 'toggle': tbuffer.focus_selected_message() raw = not mt.display_source if self.raw == 'toggle' else self.raw all_headers = not mt.display_all_headers \ if self.all_headers == 'toggle' else self.all_headers # collapse/expand depending on new 'visible' value if visible is False: mt.collapse(mt.root) # mark as read if needed if settings.get('auto_remove_unread'): msg = mt.get_message() if 'unread' in msg.get_tags(): msg.remove_tags(['unread']) fcmd = FlushCommand(silent=True) ui.apply_command(fcmd) elif visible is True: # could be None mt.expand(mt.root) tbuffer.focus_selected_message() # set new values in messagetree obj if raw is not None: mt.display_source = raw if all_headers is not None: mt.display_all_headers = all_headers mt.debug() # let the messagetree reassemble itself mt.reassemble() # refresh the buffer (clears Tree caches etc) tbuffer.refresh()
def __init__(self, message, even=False, folded=True, raw=False, all_headers=False, depth=0, bars_at=[]): """ :param message: the message to display :type message: alot.db.Message :param even: use messagesummary_even theme for summary :type even: bool :param folded: fold message initially :type folded: bool :param raw: show message source initially :type raw: bool :param all_headers: show all headers initially :type all_headers: bool :param depth: number of characters to shift content to the right :type depth: int :param bars_at: defines for each column of the indentation whether to use a vertical bar instead of a space. :type bars_at: list(bool) """ self.message = message self.mail = self.message.get_email() self.depth = depth self.bars_at = bars_at self.even = even self.folded = folded self.show_raw = raw self.show_all_headers = all_headers # define subwidgets that will be created on demand self.sumline = None self.headerw = None self.attachmentw = None self.bodyw = None self.sourcew = None # set available and to be displayed headers self._all_headers = list(set(self.mail.keys())) displayed = settings.get('displayed_headers') self._filtered_headers = [k for k in displayed if k.lower() == 'tags' or k in self.mail] self._displayed_headers = None bars = settings.get_theming_attribute('thread', 'arrow_bars') self.arrow_bars_att = bars heads = settings.get_theming_attribute('thread', 'arrow_heads') self.arrow_heads_att = heads logging.debug(self.arrow_heads_att) self.rebuild() # this will build self.pile urwid.WidgetWrap.__init__(self, self.pile)
def __init__( self, cmd, stdin=None, shell=False, spawn=False, refocus=True, thread=False, on_success=None, **kwargs ): """ :param cmd: the command to call :type cmd: list or str :param stdin: input to pipe to the process :type stdin: file or str :param spawn: run command in a new terminal :type spawn: bool :param shell: let shell interpret command string :type shell: bool :param thread: run asynchronously, don't block alot :type thread: bool :param refocus: refocus calling buffer after cmd termination :type refocus: bool :param on_success: code to execute after command successfully exited :type on_success: callable """ logging.debug({"spawn": spawn}) # make sure cmd is a list of str if isinstance(cmd, unicode): # convert cmdstring to list: in case shell==True, # Popen passes only the first item in the list to $SHELL cmd = [cmd] if shell else split_commandstring(cmd) # determine complete command list to pass touchhook = settings.get_hook("touch_external_cmdlist") # filter cmd, shell and thread through hook if defined if touchhook is not None: logging.debug("calling hook: touch_external_cmdlist") res = touchhook(cmd, shell=shell, spawn=spawn, thread=thread) logging.debug("got: %s" % res) cmd, shell, self.in_thread = res # otherwise if spawn requested and X11 is running elif spawn: if "DISPLAY" in os.environ: term_cmd = settings.get("terminal_cmd", "") logging.info("spawn in terminal: %s" % term_cmd) termcmdlist = split_commandstring(term_cmd) cmd = termcmdlist + cmd else: thread = False self.cmdlist = cmd self.stdin = stdin self.shell = shell self.refocus = refocus self.in_thread = thread self.on_success = on_success Command.__init__(self, **kwargs)
def apply(self, ui): try: ui.dbman.flush() except DatabaseLockedError: timeout = settings.get('flush_retry_timeout') def f(*args): self.apply(ui) ui.mainloop.set_alarm_in(timeout, f) ui.notify('index locked, will try again in %d secs' % timeout) ui.update() return logging.debug('flush complete')
def apply(self, ui): try: ui.dbman.flush() except DatabaseLockedError: timeout = settings.get("flush_retry_timeout") def f(*args): self.apply(ui) ui.mainloop.set_alarm_in(timeout, f) ui.notify("index locked, will try again in %d secs" % timeout) ui.update() return logging.debug("flush complete")
def get_datestring(self): """ returns reformated datestring for this messages. It uses the format spacified by `timestamp_format` in the general section of the config. """ if self._datetime == None: return None formatstring = settings.get('timestamp_format') if formatstring == None: res = helper.pretty_datetime(self._datetime) else: res = self._datetime.strftime(formatstring) return res
def apply(self, ui): # get mail to bounce if not self.message: self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() # look if this makes sense: do we have any accounts set up? my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return # remove "Resent-*" headers if already present del mail['Resent-From'] del mail['Resent-To'] del mail['Resent-Cc'] del mail['Resent-Date'] del mail['Resent-Message-ID'] # set Resent-From-header and sending account try: resent_from_header, account = determine_sender(mail, 'bounce') except AssertionError as e: ui.notify(e.message, priority='error') return mail['Resent-From'] = resent_from_header # set Reset-To allbooks = not settings.get('complete_matching_abook_only') logging.debug('allbooks: %s', allbooks) if account is not None: abooks = settings.get_addressbooks(order=[account], append_remaining=allbooks) logging.debug(abooks) completer = ContactsCompleter(abooks) else: completer = None to = yield ui.prompt('To', completer=completer) if to is None: raise CommandCanceled() mail['Resent-To'] = to.strip(' \t\n,') logging.debug("bouncing mail") logging.debug(mail.__class__) ui.apply_command(SendCommand(mail=mail))
def apply(self, ui): if settings.get('bug_on_exit'): msg = 'really quit?' if (yield ui.choice(msg, select='yes', cancel='no', msg_position='left')) == 'no': return for b in ui.buffers: b.cleanup() ui.apply_command(FlushCommand(callback=ui.exit)) if ui.db_was_locked: msg = 'Database locked. Exit without saving?' if (yield ui.choice(msg, select='yes', cancel='no', msg_position='left')) == 'no': return ui.exit()
def apply(self, ui): if self.buffer is None: self.buffer = ui.current_buffer if isinstance(self.buffer, buffers.EnvelopeBuffer) and not self.buffer.envelope.sent_time: if ( not self.force and (yield ui.choice("close without sending?", select="yes", cancel="no", msg_position="left")) == "no" ): raise CommandCanceled() if len(ui.buffers) == 1: if settings.get("quit_on_last_bclose"): logging.info("closing the last buffer, exiting") ui.apply_command(ExitCommand()) else: logging.info("not closing last remaining buffer as " "global.quit_on_last_bclose is set to False") else: ui.buffer_close(self.buffer, self.redraw)
def apply(self, ui): pcomplete = completion.PathCompleter() savedir = settings.get('attachment_prefix', '~') if self.all: msg = ui.current_buffer.get_selected_message() if not self.path: self.path = yield ui.prompt('save attachments to', text=os.path.join(savedir, ''), completer=pcomplete) if self.path: if os.path.isdir(os.path.expanduser(self.path)): for a in msg.get_attachments(): dest = a.save(self.path) name = a.get_filename() if name: ui.notify('saved %s as: %s' % (name, dest)) else: ui.notify('saved attachment as: %s' % dest) else: ui.notify('not a directory: %s' % self.path, priority='error') else: ui.notify('canceled') else: # save focussed attachment focus = ui.get_deep_focus() if isinstance(focus, widgets.AttachmentWidget): attachment = focus.get_attachment() filename = attachment.get_filename() if not self.path: msg = 'save attachment (%s) to ' % filename initialtext = os.path.join(savedir, filename) self.path = yield ui.prompt(msg, completer=pcomplete, text=initialtext) if self.path: try: dest = attachment.save(self.path) ui.notify('saved attachment as: %s' % dest) except (IOError, OSError), e: ui.notify(str(e), priority='error') else: ui.notify('canceled')
def apply(self, ui): if self.buffer is None: self.buffer = ui.current_buffer if (isinstance(self.buffer, buffers.EnvelopeBuffer) and not self.buffer.envelope.sent_time): if (not self.force and (yield ui.choice('close without sending?', select='yes', cancel='no', msg_position='left')) == 'no'): raise CommandCanceled() if len(ui.buffers) == 1: if settings.get('quit_on_last_bclose'): logging.info('closing the last buffer, exiting') ui.apply_command(ExitCommand()) else: logging.info('not closing last remaining buffer as ' 'global.quit_on_last_bclose is set to False') else: ui.buffer_close(self.buffer, self.redraw)
def __init__(self, message, even=True): """ :param message: a message :type message: alot.db.Message :param even: even entry in a pile of messages? Used for theming. :type even: bool """ self.message = message self.even = even if even: attr = settings.get_theming_attribute('thread', 'summary', 'even') else: attr = settings.get_theming_attribute('thread', 'summary', 'odd') focus_att = settings.get_theming_attribute('thread', 'summary', 'focus') cols = [] sumstr = self.__str__() txt = urwid.Text(sumstr) cols.append(txt) if settings.get('msg_summary_hides_threadwide_tags'): thread_tags = message.get_thread().get_tags(intersection=True) outstanding_tags = set(message.get_tags()).difference(thread_tags) tag_widgets = [ TagWidget(t, attr, focus_att) for t in outstanding_tags ] else: tag_widgets = [ TagWidget(t, attr, focus_att) for t in message.get_tags() ] tag_widgets.sort(tag_cmp, lambda tag_widget: tag_widget.translated) for tag_widget in tag_widgets: if not tag_widget.hidden: cols.append(('fixed', tag_widget.width(), tag_widget)) line = urwid.AttrMap(urwid.Columns(cols, dividechars=1), attr, focus_att) urwid.WidgetWrap.__init__(self, line)
def openEnvelopeFromTmpfile(): # This parses the input from the tempfile. # we do this ourselves here because we want to be able to # just type utf-8 encoded stuff into the tempfile and let alot # worry about encodings. # get input # tempfile will be removed on buffer cleanup f = open(self.envelope.tmpfile.name) enc = settings.get('editor_writes_encoding') template = string_decode(f.read(), enc) f.close() # call post-edit translate hook translate = settings.get_hook('post_edit_translate') if translate: template = translate(template, ui=ui, dbm=ui.dbman) self.envelope.parse_template(template, only_body=self.edit_only_body) if self.openNew: ui.buffer_open(buffers.EnvelopeBuffer(ui, self.envelope)) else: ebuffer.envelope = self.envelope ebuffer.rebuild()
def construct_mail(self): """ compiles the information contained in this envelope into a :class:`email.Message`. """ # Build body text part. To properly sign/encrypt messages later on, we # convert the text to its canonical format (as per RFC 2015). canonical_format = self.body.encode('utf-8') canonical_format = canonical_format.replace('\\t', ' ' * 4) textpart = MIMEText(canonical_format, 'plain', 'utf-8') # wrap it in a multipart container if necessary if self.attachments: inner_msg = MIMEMultipart() inner_msg.attach(textpart) # add attachments for a in self.attachments: inner_msg.attach(a.get_mime_representation()) else: inner_msg = textpart if self.sign: plaintext = helper.email_as_string(inner_msg) logging.debug('signing plaintext: ' + plaintext) try: signatures, signature_str = crypto.detached_signature_for( plaintext, self.sign_key) if len(signatures) != 1: raise GPGProblem("Could not sign message (GPGME " "did not return a signature)", code=GPGCode.KEY_CANNOT_SIGN) except gpgme.GpgmeError as e: if e.code == gpgme.ERR_BAD_PASSPHRASE: # If GPG_AGENT_INFO is unset or empty, the user just does # not have gpg-agent running (properly). if os.environ.get('GPG_AGENT_INFO', '').strip() == '': msg = "Got invalid passphrase and GPG_AGENT_INFO\ not set. Please set up gpg-agent." raise GPGProblem(msg, code=GPGCode.BAD_PASSPHRASE) else: raise GPGProblem("Bad passphrase. Is gpg-agent " "running?", code=GPGCode.BAD_PASSPHRASE) raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_SIGN) micalg = crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo) unencrypted_msg = MIMEMultipart('signed', micalg=micalg, protocol='application/pgp-signature') # wrap signature in MIMEcontainter stype = 'pgp-signature; name="signature.asc"' signature_mime = MIMEApplication(_data=signature_str, _subtype=stype, _encoder=encode_7or8bit) signature_mime['Content-Description'] = 'signature' signature_mime.set_charset('us-ascii') # add signed message and signature to outer message unencrypted_msg.attach(inner_msg) unencrypted_msg.attach(signature_mime) unencrypted_msg['Content-Disposition'] = 'inline' else: unencrypted_msg = inner_msg if self.encrypt: plaintext = helper.email_as_string(unencrypted_msg) logging.debug('encrypting plaintext: ' + plaintext) try: encrypted_str = crypto.encrypt(plaintext, self.encrypt_keys.values()) except gpgme.GpgmeError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) outer_msg = MIMEMultipart('encrypted', protocol='application/pgp-encrypted') version_str = 'Version: 1' encryption_mime = MIMEApplication(_data=version_str, _subtype='pgp-encrypted', _encoder=encode_7or8bit) encryption_mime.set_charset('us-ascii') encrypted_mime = MIMEApplication(_data=encrypted_str, _subtype='octet-stream', _encoder=encode_7or8bit) encrypted_mime.set_charset('us-ascii') outer_msg.attach(encryption_mime) outer_msg.attach(encrypted_mime) else: outer_msg = unencrypted_msg headers = self.headers.copy() # add Message-ID if 'Message-ID' not in headers: headers['Message-ID'] = [email.Utils.make_msgid()] if 'User-Agent' in headers: uastring_format = headers['User-Agent'][0] else: uastring_format = settings.get('user_agent').strip() uastring = uastring_format.format(version=__version__) if uastring: headers['User-Agent'] = [uastring] # copy headers from envelope to mail for k, vlist in headers.items(): for v in vlist: outer_msg[k] = encode_header(k, v) return outer_msg
class ComposeCommand(Command): """compose a new email""" def __init__(self, envelope=None, headers={}, template=None, sender=u'', subject=u'', to=[], cc=[], bcc=[], attach=None, omit_signature=False, spawn=None, **kwargs): """ :param envelope: use existing envelope :type envelope: :class:`~alot.db.envelope.Envelope` :param headers: forced header values :type header: doct (str->str) :param template: name of template to parse into the envelope after creation. This should be the name of a file in your template_dir :type template: str :param sender: From-header value :type sender: str :param subject: Subject-header value :type subject: str :param to: To-header value :type to: str :param cc: Cc-header value :type cc: str :param bcc: Bcc-header value :type bcc: str :param attach: Path to files to be attached (globable) :type attach: str :param omit_signature: do not attach/append signature :type omit_signature: bool :param spawn: force spawning of editor in a new terminal :type spawn: bool """ Command.__init__(self, **kwargs) self.envelope = envelope self.template = template self.headers = headers self.sender = sender self.subject = subject self.to = to self.cc = cc self.bcc = bcc self.attach = attach self.omit_signature = omit_signature self.force_spawn = spawn @inlineCallbacks def apply(self, ui): if self.envelope is None: self.envelope = Envelope() if self.template is not None: #get location of tempsdir, containing msg templates tempdir = settings.get('template_dir') tempdir = os.path.expanduser(tempdir) if not tempdir: xdgdir = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) tempdir = os.path.join(xdgdir, 'alot', 'templates') path = os.path.expanduser(self.template) if not os.path.dirname(path): # use tempsdir if not os.path.isdir(tempdir): ui.notify('no templates directory: %s' % tempdir, priority='error') return path = os.path.join(tempdir, path) if not os.path.isfile(path): ui.notify('could not find template: %s' % path, priority='error') return try: self.envelope.parse_template(open(path).read()) except Exception, e: ui.notify(str(e), priority='error') return # set forced headers for key, value in self.headers.items(): self.envelope.add(key, value) # set forced headers for separate parameters if self.sender: self.envelope.add('From', self.sender) if self.subject: self.envelope.add('Subject', self.subject) if self.to: self.envelope.add('To', ','.join(self.to)) if self.cc: self.envelope.add('Cc', ','.join(self.cc)) if self.bcc: self.envelope.add('Bcc', ','.join(self.bcc)) # get missing From header if not 'From' in self.envelope.headers: accounts = settings.get_accounts() if len(accounts) == 1: a = accounts[0] fromstring = "%s <%s>" % (a.realname, a.address) self.envelope.add('From', fromstring) else: cmpl = AccountCompleter() fromaddress = yield ui.prompt('From', completer=cmpl, tab=1) if fromaddress is None: ui.notify('canceled') return self.envelope.add('From', fromaddress) # add signature if not self.omit_signature: name, addr = email.Utils.parseaddr(self.envelope['From']) account = settings.get_account_by_address(addr) if account is not None: if account.signature: logging.debug('has signature') sig = os.path.expanduser(account.signature) if os.path.isfile(sig): logging.debug('is file') if account.signature_as_attachment: name = account.signature_filename or None self.envelope.attach(sig, filename=name) logging.debug('attached') else: sigcontent = open(sig).read() enc = helper.guess_encoding(sigcontent) mimetype = helper.guess_mimetype(sigcontent) if mimetype.startswith('text'): sigcontent = helper.string_decode( sigcontent, enc) self.envelope.body += '\n' + sigcontent else: ui.notify('could not locate signature: %s' % sig, priority='error') if (yield ui.choice('send without signature?', 'yes', 'no')) == 'no': return # Figure out whether we should GPG sign messages by default # and look up key if so sender = self.envelope.get('From') name, addr = email.Utils.parseaddr(sender) account = settings.get_account_by_address(addr) if account: self.envelope.sign = account.sign_by_default self.envelope.sign_key = account.gpg_key # get missing To header if 'To' not in self.envelope.headers: allbooks = not settings.get('complete_matching_abook_only') logging.debug(allbooks) if account is not None: abooks = settings.get_addressbooks(order=[account], append_remaining=allbooks) logging.debug(abooks) completer = ContactsCompleter(abooks) else: completer = None to = yield ui.prompt('To', completer=completer) if to is None: ui.notify('canceled') return self.envelope.add('To', to.strip(' \t\n,')) if settings.get('ask_subject') and \ not 'Subject' in self.envelope.headers: subject = yield ui.prompt('Subject') logging.debug('SUBJECT: "%s"' % subject) if subject is None: ui.notify('canceled') return self.envelope.add('Subject', subject) if settings.get('compose_ask_tags'): comp = TagsCompleter(ui.dbman) tagsstring = yield ui.prompt('Tags', completer=comp) tags = filter(lambda x: x, tagsstring.split(',')) if tags is None: ui.notify('canceled') return self.envelope.tags = tags if self.attach: for gpath in self.attach: for a in glob.glob(gpath): self.envelope.attach(a) logging.debug('attaching: ' + a) cmd = commands.envelope.EditCommand(envelope=self.envelope, spawn=self.force_spawn, refocus=False) ui.apply_command(cmd)
def apply(self, ui): # look if this makes sense: do we have any accounts set up? my_accounts = settings.get_accounts() if not my_accounts: ui.notify('no accounts set', priority='error') return # get message to forward if not given in constructor if not self.message: self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() # set body text name, address = self.message.get_author() timestamp = self.message.get_date() qf = settings.get_hook('reply_prefix') if qf: quotestring = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quotestring = 'Quoting %s (%s)\n' % (name or address, timestamp) mailcontent = quotestring quotehook = settings.get_hook('text_quote') if quotehook: mailcontent += quotehook(self.message.accumulate_body()) else: quote_prefix = settings.get('quote_prefix') for line in self.message.accumulate_body().splitlines(): mailcontent += quote_prefix + line + '\n' envelope = Envelope(bodytext=mailcontent) # copy subject subject = decode_header(mail.get('Subject', '')) reply_subject_hook = settings.get_hook('reply_subject') if reply_subject_hook: subject = reply_subject_hook(subject) else: rsp = settings.get('reply_subject_prefix') if not subject.startswith(('Re:', rsp)): subject = rsp + subject envelope.add('Subject', subject) # set From realname, address = recipient_to_from(mail, my_accounts) envelope.add('From', '%s <%s>' % (realname, address)) # set To sender = mail['Reply-To'] or mail['From'] recipients = [sender] my_addresses = settings.get_addresses() if self.groupreply: if sender != mail['From']: recipients.append(mail['From']) cleared = self.clear_my_address(my_addresses, mail.get('To', '')) recipients.append(cleared) # copy cc for group-replies if 'Cc' in mail: cc = self.clear_my_address(my_addresses, mail['Cc']) envelope.add('Cc', decode_header(cc)) to = ', '.join(recipients) logging.debug('reply to: %s' % to) envelope.add('To', decode_header(to)) # set In-Reply-To header envelope.add('In-Reply-To', '<%s>' % self.message.get_message_id()) # set References header old_references = mail.get('References', '') if old_references: old_references = old_references.split() references = old_references[-8:] if len(old_references) > 8: references = old_references[:1] + references references.append('<%s>' % self.message.get_message_id()) envelope.add('References', ' '.join(references)) else: envelope.add('References', '<%s>' % self.message.get_message_id()) # continue to compose ui.apply_command( ComposeCommand(envelope=envelope, spawn=self.force_spawn))
def apply(self, ui): ebuffer = ui.current_buffer if not self.envelope: self.envelope = ui.current_buffer.envelope #determine editable headers edit_headers = set(settings.get('edit_headers_whitelist')) if '*' in edit_headers: edit_headers = set(self.envelope.headers.keys()) blacklist = set(settings.get('edit_headers_blacklist')) if '*' in blacklist: blacklist = set(self.envelope.headers.keys()) edit_headers = edit_headers - blacklist logging.info('editable headers: %s' % edit_headers) def openEnvelopeFromTmpfile(): # This parses the input from the tempfile. # we do this ourselves here because we want to be able to # just type utf-8 encoded stuff into the tempfile and let alot # worry about encodings. # get input # tempfile will be removed on buffer cleanup f = open(self.envelope.tmpfile.name) enc = settings.get('editor_writes_encoding') template = string_decode(f.read(), enc) f.close() # call post-edit translate hook translate = settings.get_hook('post_edit_translate') if translate: template = translate(template, ui=ui, dbm=ui.dbman) self.envelope.parse_template(template, only_body=self.edit_only_body) if self.openNew: ui.buffer_open(buffers.EnvelopeBuffer(ui, self.envelope)) else: ebuffer.envelope = self.envelope ebuffer.rebuild() # decode header headertext = u'' for key in edit_headers: vlist = self.envelope.get_all(key) if not vlist: # ensure editable headers are present in template vlist = [''] else: # remove to be edited lines from envelope del self.envelope[key] for value in vlist: # newlines (with surrounding spaces) by spaces in values value = value.strip() value = re.sub('[ \t\r\f\v]*\n[ \t\r\f\v]*', ' ', value) headertext += '%s: %s\n' % (key, value) # determine editable content bodytext = self.envelope.body if headertext: content = '%s\n%s' % (headertext, bodytext) self.edit_only_body = False else: content = bodytext self.edit_only_body = True # call pre-edit translate hook translate = settings.get_hook('pre_edit_translate') if translate: content = translate(content, ui=ui, dbm=ui.dbman) #write stuff to tempfile old_tmpfile = None if self.envelope.tmpfile: old_tmpfile = self.envelope.tmpfile self.envelope.tmpfile = tempfile.NamedTemporaryFile(delete=False, prefix='alot.') self.envelope.tmpfile.write(content.encode('utf-8')) self.envelope.tmpfile.flush() self.envelope.tmpfile.close() if old_tmpfile: os.unlink(old_tmpfile.name) cmd = globals.EditCommand(self.envelope.tmpfile.name, on_success=openEnvelopeFromTmpfile, spawn=self.force_spawn, thread=self.force_spawn, refocus=self.refocus) ui.apply_command(cmd)
def extract_body(mail, types=None): """ returns a body text string for given mail. If types is `None`, `text/*` is used: The exact preferred type is specified by the prefer_plaintext config option which defaults to text/html. :param mail: the mail to use :type mail: :class:`email.Message` :param types: mime content types to use for body string :type types: list of str """ preferred = 'text/plain' if settings.get( 'prefer_plaintext') else 'text/html' has_preferred = False # see if the mail has our preferred type if types is None: has_preferred = list( typed_subpart_iterator(mail, *preferred.split('/'))) body_parts = [] for part in mail.walk(): ctype = part.get_content_type() if types is not None: if ctype not in types: continue cd = part.get('Content-Disposition', '') if cd.startswith('attachment'): continue # if the mail has our preferred type, we only keep this type # note that if types != None, has_preferred always stays False if has_preferred and ctype != preferred: continue enc = part.get_content_charset() or 'ascii' raw_payload = part.get_payload(decode=True) if ctype == 'text/plain': raw_payload = string_decode(raw_payload, enc) body_parts.append(string_sanitize(raw_payload)) else: # get mime handler key = 'copiousoutput' handler, entry = settings.mailcap_find_match(ctype, key=key) tempfile_name = None stdin = None if entry: handler_raw_commandstring = entry['view'] # in case the mailcap defined command contains no '%s', # we pipe the files content to the handling command via stdin if '%s' in handler_raw_commandstring: # open tempfile, respect mailcaps nametemplate nametemplate = entry.get('nametemplate', '%s') prefix, suffix = parse_mailcap_nametemplate(nametemplate) tmpfile = tempfile.NamedTemporaryFile(delete=False, prefix=prefix, suffix=suffix) # write payload to tmpfile tmpfile.write(raw_payload) tmpfile.close() tempfile_name = tmpfile.name else: stdin = raw_payload # read parameter, create handler command parms = tuple(map('='.join, part.get_params())) # create and call external command cmd = mailcap.subst(entry['view'], ctype, filename=tempfile_name, plist=parms) logging.debug('command: %s' % cmd) logging.debug('parms: %s' % str(parms)) cmdlist = split_commandstring(cmd) # call handler rendered_payload, errmsg, retval = helper.call_cmd(cmdlist, stdin=stdin) # remove tempfile if tempfile_name: os.unlink(tempfile_name) if rendered_payload: # handler had output body_parts.append(string_sanitize(rendered_payload)) return u'\n\n'.join(body_parts)