diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2011-12-22 10:45:14 +0000 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2011-12-22 10:45:14 +0000 |
commit | f21c55c075ce3477cb09ccc0929116fab895c435 (patch) | |
tree | 225a479697f33b4bcf6fee3c746ae2248eb31a18 | |
parent | 2d0b7ba4be907896be3a0a9132b8b775b8b7a6cd (diff) | |
parent | e2016895fb7d2850cdbdad1b628159d6136bd964 (diff) |
Merge branch 'testing'
-rw-r--r-- | alot/account.py | 8 | ||||
-rw-r--r-- | alot/buffers.py | 8 | ||||
-rw-r--r-- | alot/commands/envelope.py | 37 | ||||
-rw-r--r-- | alot/commands/globals.py | 49 | ||||
-rw-r--r-- | alot/commands/thread.py | 44 | ||||
-rw-r--r-- | alot/defaults/alot.rc | 3 | ||||
-rw-r--r-- | alot/helper.py | 55 | ||||
-rwxr-xr-x | alot/init.py | 119 | ||||
-rw-r--r-- | alot/message.py | 57 | ||||
-rw-r--r-- | alot/ui.py | 30 | ||||
-rwxr-xr-x | bin/alot | 30 |
11 files changed, 276 insertions, 164 deletions
diff --git a/alot/account.py b/alot/account.py index 1b7d35e2..6eb9f099 100644 --- a/alot/account.py +++ b/alot/account.py @@ -4,10 +4,10 @@ import time import re import email import os +import shlex from ConfigParser import SafeConfigParser from urlparse import urlparse -from helper import cmd_output import helper @@ -140,7 +140,8 @@ class SendmailAccount(Account): def send_mail(self, mail): mail['Date'] = email.utils.formatdate(time.time(), True) - out, err = helper.pipe_to_command(self.cmd, mail.as_string()) + cmdlist = shlex.split(self.cmd.encode('utf-8', errors='ignore')) + out, err, retval = helper.call_cmd(cmdlist, stdin=mail.as_string()) if err: return err + '. sendmail_cmd set to: %s' % self.cmd self.store_sent_mail(mail) @@ -320,7 +321,8 @@ class MatchSdtoutAddressbook(AddressBook): return self.lookup('\'\'') def lookup(self, prefix): - resultstring = cmd_output('%s %s' % (self.command, prefix)) + cmdlist = shlex.split(self.command.encode('utf-8', errors='ignore')) + resultstring, errmsg, retval = helper.call_cmd(cmdlist + [prefix]) if not resultstring: return [] lines = resultstring.replace('\t', ' ' * 4).splitlines() diff --git a/alot/buffers.py b/alot/buffers.py index 09db8246..4fa66d62 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -86,7 +86,6 @@ class EnvelopeBuffer(Buffer): def __init__(self, ui, envelope): self.ui = ui self.envelope = envelope - self.mail = envelope.construct_mail() self.all_headers = False self.rebuild() Buffer.__init__(self, ui, self.body, 'envelope') @@ -96,15 +95,15 @@ class EnvelopeBuffer(Buffer): return '[%s] to: %s' % (self.typename, shorten_author_string(to, 400)) def rebuild(self): - self.mail = self.envelope.construct_mail() displayed_widgets = [] hidden = settings.config.getstringlist('general', 'envelope_headers_blacklist') #build lines lines = [] - for (k, v) in self.envelope.headers.items(): + for (k, vlist) in self.envelope.headers.items(): if (k not in hidden) or self.all_headers: - lines.append((k, v)) + for value in vlist: + lines.append((k, value)) self.header_wgt = widgets.HeadersList(lines) displayed_widgets.append(self.header_wgt) @@ -117,7 +116,6 @@ class EnvelopeBuffer(Buffer): self.attachment_wgt = urwid.Pile(lines) displayed_widgets.append(self.attachment_wgt) - #self.body_wgt = widgets.MessageBodyWidget(self.mail) self.body_wgt = urwid.Text(self.envelope.body) displayed_widgets.append(self.body_wgt) self.body = urwid.ListBox(displayed_widgets) diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 2597e9de..21b6aa26 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -1,12 +1,15 @@ import os +import re import glob import logging import email import tempfile from twisted.internet.defer import inlineCallbacks import threading +import datetime from alot import buffers +from alot import commands from alot.commands import Command, registerCommand from alot import settings from alot import helper @@ -32,7 +35,7 @@ class AttachCommand(Command): def apply(self, ui): envelope = ui.current_buffer.envelope - if self.path: + if self.path: # TODO: not possible, otherwise argparse error before files = filter(os.path.isfile, glob.glob(os.path.expanduser(self.path))) if not files: @@ -40,6 +43,7 @@ class AttachCommand(Command): return else: ui.notify('no files specified, abort') + return logging.info("attaching: %s" % files) for path in files: @@ -72,6 +76,13 @@ class SendCommand(Command): def apply(self, ui): currentbuffer = ui.current_buffer # needed to close later envelope = currentbuffer.envelope + if envelope.sent_time: + warning = 'A modified version of ' * envelope.modified_since_sent + warning += 'this message has been sent at %s.' % envelope.sent_time + warning += ' Do you want to resend?' + if (yield ui.choice(warning, cancel='no', + msg_position='left')) == 'no': + return frm = envelope.get('From') sname, saddr = email.Utils.parseaddr(frm) omit_signature = False @@ -107,7 +118,8 @@ class SendCommand(Command): def afterwards(returnvalue): ui.clear_notify([clearme]) if returnvalue == 'success': # sucessfully send mail - ui.buffer_close(currentbuffer) + envelope.sent_time = datetime.datetime.now() + ui.apply_command(commands.globals.BufferCloseCommand()) ui.notify('mail send successful') else: ui.notify('failed to send: %s' % returnvalue, @@ -181,8 +193,16 @@ class EditCommand(Command): # decode header headertext = u'' for key in edit_headers: - value = self.envelope.headers.get(key, '') - headertext += '%s: %s\n' % (key, value) + vlist = self.envelope.get_all(key) + + # 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) bodytext = self.envelope.body @@ -207,8 +227,7 @@ class EditCommand(Command): @registerCommand(MODE, 'set', arguments=[ - # TODO - #(['--append'], {'action': 'store_true', 'help':'keep previous value'}), + (['--append'], {'action': 'store_true', 'help':'keep previous values'}), (['key'], {'help':'header to refine'}), (['value'], {'nargs':'+', 'help':'value'})]) class SetCommand(Command): @@ -222,11 +241,13 @@ class SetCommand(Command): """ self.key = key self.value = ' '.join(value) - self.append = append + self.reset = not append Command.__init__(self, **kwargs) def apply(self, ui): - ui.current_buffer.envelope[self.key] = self.value + if self.reset: + del(ui.current_buffer.envelope[self.key]) + ui.current_buffer.envelope.add(self.key, self.value) ui.current_buffer.rebuild() diff --git a/alot/commands/globals.py b/alot/commands/globals.py index b7e55598..4ead9c58 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -250,10 +250,18 @@ class PythonShellCommand(Command): @registerCommand(MODE, 'bclose') class BufferCloseCommand(Command): - """close current buffer or exit if it is the last""" + """close current buffer""" def apply(self, ui): selected = ui.current_buffer - ui.buffer_close(selected) + if len(ui.buffers) == 1: + if settings.config.getboolean('general', 'quit_on_last_bclose'): + ui.logger.info('closing the last buffer, exiting') + ui.apply_command(ExitCommand()) + else: + ui.logger.info('not closing last remaining buffer as ' + 'global.quit_on_last_bclose is set to False') + else: + ui.buffer_close(selected) @registerCommand(MODE, 'bprevious', forced={'offset': -1}, @@ -418,11 +426,12 @@ class HelpCommand(Command): (['--to'], {'nargs':'+', 'help':'recipients'}), (['--cc'], {'nargs':'+', 'help':'copy to'}), (['--bcc'], {'nargs':'+', 'help':'blind copy to'}), + (['--attach'], {'nargs':'+', 'help':'attach files'}), ]) class ComposeCommand(Command): """compose a new email""" def __init__(self, envelope=None, headers={}, template=None, - sender=u'', subject=u'', to=[], cc=[], bcc=[], + sender=u'', subject=u'', to=[], cc=[], bcc=[], attach=None, **kwargs): """ :param envelope: use existing envelope @@ -443,8 +452,10 @@ class ComposeCommand(Command): :type cc: str :param bcc: Bcc-header value :type bcc: str + :param attach: Path to files to be attached (globable) + :type attach: str """ -#TODO + Command.__init__(self, **kwargs) self.envelope = envelope @@ -455,6 +466,7 @@ class ComposeCommand(Command): self.to = to self.cc = cc self.bcc = bcc + self.attach = attach @inlineCallbacks def apply(self, ui): @@ -489,19 +501,19 @@ class ComposeCommand(Command): # set forced headers for key, value in self.headers.items(): - self.envelope.headers[key] = value + self.envelope.add(key, value) # set forced headers for separate parameters if self.sender: - self.envelope['From'] = self.sender + self.envelope.add('From', self.sender) if self.subject: - self.envelope['Subject'] = self.subject + self.envelope.add('Subject', self.subject) if self.to: - self.envelope['To'] = ','.join(self.to) + self.envelope.add('To', ','.join(self.to)) if self.cc: - self.envelope['Cc'] = ','.join(self.cc) + self.envelope.add('Cc', ','.join(self.cc)) if self.bcc: - self.envelope['Bcc'] = ','.join(self.bcc) + self.envelope.add('Bcc', ','.join(self.bcc)) # get missing From header if not 'From' in self.envelope.headers: @@ -509,7 +521,7 @@ class ComposeCommand(Command): if len(accounts) == 1: a = accounts[0] fromstring = "%s <%s>" % (a.realname, a.address) - self.envelope['From'] = fromstring + self.envelope.add('From', fromstring) else: cmpl = AccountCompleter(ui.accountman) fromaddress = yield ui.prompt(prefix='From>', completer=cmpl, @@ -520,13 +532,13 @@ class ComposeCommand(Command): a = ui.accountman.get_account_by_address(fromaddress) if a is not None: fromstring = "%s <%s>" % (a.realname, a.address) - self.envelope['From'] = fromstring + self.envelope.add('From', fromstring) else: - self.envelope.headers['From'] = fromaddress + self.envelope.add('From', fromaddress) # get missing To header if 'To' not in self.envelope.headers: - sender = self.envelope.headers.get('From') + sender = self.envelope.get('From') name, addr = email.Utils.parseaddr(sender) a = ui.accountman.get_account_by_address(addr) @@ -541,7 +553,7 @@ class ComposeCommand(Command): if to == None: ui.notify('canceled') return - self.envelope.headers['To'] = to + self.envelope.add('To', to) if settings.config.getboolean('general', 'ask_subject') and \ not 'Subject' in self.envelope.headers: @@ -550,7 +562,12 @@ class ComposeCommand(Command): if subject == None: ui.notify('canceled') return - self.envelope['Subject'] = subject + self.envelope.add('Subject', subject) + + if self.attach: + for a in self.attach: + self.envelope.attach(a) + cmd = commands.envelope.EditCommand(envelope=self.envelope) ui.apply_command(cmd) diff --git a/alot/commands/thread.py b/alot/commands/thread.py index 464fb91a..b7159cc1 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -3,6 +3,7 @@ import logging import tempfile from twisted.internet.defer import inlineCallbacks import mimetypes +import shlex from alot.commands import Command, registerCommand from alot.commands.globals import ExternalCommand @@ -59,7 +60,7 @@ class ReplyCommand(Command): subject = decode_header(mail.get('Subject', '')) if not subject.startswith('Re:'): subject = 'Re: ' + subject - envelope['Subject'] = subject + envelope.add('Subject', subject) # set From my_addresses = ui.accountman.get_addresses() @@ -75,7 +76,7 @@ class ReplyCommand(Command): if matched_address: account = ui.accountman.get_account_by_address(matched_address) fromstring = '%s <%s>' % (account.realname, account.address) - envelope['From'] = fromstring + envelope.add('From', fromstring) # set To if self.groupreply: @@ -83,21 +84,22 @@ class ReplyCommand(Command): if cleared: logging.info(mail['From'] + ', ' + cleared) to = mail['From'] + ', ' + cleared - envelope['To'] = to + envelope.add('To', decode_header(to)) + else: - envelope['To'] = mail['From'] + envelope.add('To', decode_header(mail['From'])) # copy cc and bcc for group-replies if 'Cc' in mail: cc = self.clear_my_address(my_addresses, mail['Cc']) - envelope['Cc'] = cc + envelope.add('Cc', decode_header(cc)) if 'Bcc' in mail: bcc = self.clear_my_address(my_addresses, mail['Bcc']) - envelope['Bcc'] = bcc + envelope.add('Bcc', decode_header(bcc)) else: - envelope['To'] = mail['From'] + envelope.add('To', decode_header(mail['From'])) # set In-Reply-To header - envelope['In-Reply-To'] = '<%s>' % self.message.get_message_id() + envelope.add('In-Reply-To', '<%s>' % self.message.get_message_id()) # set References header old_references = mail.get('References', '') @@ -107,9 +109,9 @@ class ReplyCommand(Command): if len(old_references) > 8: references = old_references[:1] + references references.append('<%s>' % self.message.get_message_id()) - envelope['References'] = ' '.join(references) + envelope.add('References', ' '.join(references)) else: - envelope['References'] = '<%s>' % self.message.get_message_id() + envelope.add('References', '<%s>' % self.message.get_message_id()) ui.apply_command(ComposeCommand(envelope=envelope)) @@ -168,9 +170,11 @@ class ForwardCommand(Command): # copy subject subject = decode_header(mail.get('Subject', '')) subject = 'Fwd: ' + subject - envelope['Subject'] = subject + envelope.add('Subject', subject) # set From + # we look for own addresses in the To,Cc,Ccc headers in that order + # and use the first match as new From header if there is one. my_addresses = ui.accountman.get_addresses() matched_address = '' in_to = [a for a in my_addresses if a in mail.get('To', '')] @@ -184,7 +188,7 @@ class ForwardCommand(Command): if matched_address: account = ui.accountman.get_account_by_address(matched_address) fromstring = '%s <%s>' % (account.realname, account.address) - envelope['From'] = fromstring + envelope.add('From', fromstring) ui.apply_command(ComposeCommand(envelope=envelope)) @@ -239,7 +243,7 @@ class ToggleHeaderCommand(Command): @registerCommand(MODE, 'pipeto', arguments=[ - (['cmd'], {'help':'shellcommand to pipe to'}), + (['cmd'], {'nargs':'?', 'help':'shellcommand to pipe to'}), (['--all'], {'action': 'store_true', 'help':'pass all messages'}), (['--decode'], {'action': 'store_true', 'help':'use only decoded body lines'}), @@ -250,14 +254,13 @@ class ToggleHeaderCommand(Command): ) class PipeCommand(Command): """pipe message(s) to stdin of a shellcommand""" - #TODO: make cmd a list #TODO: use raw arg from print command here def __init__(self, cmd, all=False, ids=False, separately=False, decode=True, noop_msg='no command specified', confirm_msg='', done_msg='done', **kwargs): """ :param cmd: shellcommand to open - :type cmd: str + :type cmd: list of str :param all: pipe all, not only selected message :type all: bool :param ids: only write message ids, not the message source @@ -273,7 +276,7 @@ class PipeCommand(Command): :type done_msg: str """ Command.__init__(self, **kwargs) - self.cmd = cmd + self.cmdlist = cmd self.whole_thread = all self.separately = separately self.ids = ids @@ -285,7 +288,7 @@ class PipeCommand(Command): @inlineCallbacks def apply(self, ui): # abort if command unset - if not self.cmd: + if not self.cmdlist: ui.notify(self.noop_msg, priority='error') return @@ -324,7 +327,7 @@ class PipeCommand(Command): # do teh monkey for mail in mailstrings: ui.logger.debug("%s" % mail) - out, err = helper.pipe_to_command(self.cmd, mail) + out, err, retval = helper.call_cmd(self.cmdlist, stdin=mail) if err: ui.notify(err, priority='error') return @@ -353,6 +356,7 @@ class PrintCommand(PipeCommand): """ # get print command cmd = settings.config.get('general', 'print_cmd', fallback='') + cmdlist = shlex.split(cmd.encode('utf-8', errors='ignore')) # set up notification strings if all: @@ -365,7 +369,7 @@ class PrintCommand(PipeCommand): # no print cmd set noop_msg = 'no print command specified. Set "print_cmd" in the '\ 'global section.' - PipeCommand.__init__(self, cmd, all=all, + PipeCommand.__init__(self, cmdlist, all=all, separately=separately, decode=not raw, noop_msg=noop_msg, confirm_msg=confirm_msg, @@ -374,7 +378,7 @@ class PrintCommand(PipeCommand): @registerCommand(MODE, 'save', arguments=[ (['--all'], {'action': 'store_true', 'help':'save all attachments'}), - (['path'], {'nargs':'?', 'help':'path to save to'})]) + (['path'], {'help':'path to save to'})]) class SaveAttachmentCommand(Command): """save attachment(s)""" def __init__(self, all=False, path=None, **kwargs): diff --git a/alot/defaults/alot.rc b/alot/defaults/alot.rc index f92d1454..087d1a12 100644 --- a/alot/defaults/alot.rc +++ b/alot/defaults/alot.rc @@ -93,6 +93,9 @@ initial_command = search tag:inbox AND NOT tag:killed # look in the abook of the account matching the sender address complete_matching_abook_only = False +# shut down when the last buffer gets closed +quit_on_last_bclose = False + [global-maps] j = move down k = move up diff --git a/alot/helper.py b/alot/helper.py index c4e6d7fa..4d0c01f4 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -2,7 +2,6 @@ from datetime import date from datetime import timedelta from collections import deque from string import strip -import shlex import subprocess import email import mimetypes @@ -213,39 +212,37 @@ def pretty_datetime(d): return string -def cmd_output(command_line): - args = shlex.split(command_line.encode('utf-8', errors='ignore')) +def call_cmd(cmdlist, stdin=None): + """ + get a shell commands output, error message and return value + + :param cmdlist: shellcommand to call, already splitted into a list accepted + by :meth:`subprocess.Popen` + :type cmdlist: list of str + :param stdin: string to pipe to the process + :type stdin: str + :return: triple of stdout, error msg, return value of the shell command + :rtype: str, str, int + """ + + out, err, ret = '', '', 0 try: - output = subprocess.check_output(args) - output = string_decode(output, urwid.util.detected_encoding) - except subprocess.CalledProcessError: - return None - except OSError: - return None - return output - - -def pipe_to_command(cmd, stdin): - # remove quotes which have been put around the whole command - cmd = cmd.strip() - stdin = stdin + '\n' - if cmd[0] == '"' and cmd[-1] == '"': - cmd = cmd[1:-1] - args = shlex.split(cmd.encode('utf-8', errors='ignore')) - try: - proc = subprocess.Popen(args, stdin=subprocess.PIPE, + if stdin: + proc = subprocess.Popen(cmdlist, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate(stdin) - except OSError, e: - return '', str(e) - if proc.poll(): # returncode is not 0 - e = 'return value != 0' - if err.strip(): - e = e + ': %s' % err - return '', e + ret = proc.poll() else: - return out, err + out = subprocess.check_output(cmdlist) + # todo: get error msg. rval + except (subprocess.CalledProcessError, OSError), e: + err = str(e) + ret = -1 + + out = string_decode(out, urwid.util.detected_encoding) + err = string_decode(err, urwid.util.detected_encoding) + return out, err, ret def mimewrap(path, filename=None, ctype=None): diff --git a/alot/init.py b/alot/init.py index b0a84777..d88e7346 100755 --- a/alot/init.py +++ b/alot/init.py @@ -1,6 +1,5 @@ #!/usr/bin/env python import sys -import argparse import logging import os @@ -14,42 +13,74 @@ from commands import * from alot.commands import CommandParseError import alot - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument('-c', dest='configfile', - default=None, - help='alot\'s config file') - parser.add_argument('-n', dest='notmuchconfigfile', - default='~/.notmuch-config', - help='notmuch\'s config file') - parser.add_argument('-C', dest='colours', - type=int, - choices=[1, 16, 256], - help='colour mode') - parser.add_argument('-r', dest='read_only', - action='store_true', - help='open db in read only mode') - parser.add_argument('-p', dest='db_path', - help='path to notmuch index') - parser.add_argument('-d', dest='debug_level', - default='info', - choices=['debug', 'info', 'warning', 'error'], - help='debug level') - parser.add_argument('-l', dest='logfile', - default='/dev/null', - help='logfile') - parser.add_argument('command', nargs='?', - default='', - help='initial command') - parser.add_argument('-v', '--version', action='version', - version='%(prog)s ' + alot.__version__) - return parser.parse_args() +from twisted.python import usage + + +class SubcommandOptions(usage.Options): + def parseArgs(self, *args): + self.args = args + + def as_argparse_opts(self): + optstr = '' + for k, v in self.items(): + if v is not None: + optstr += '--%s \'%s\' ' % (k, v) + return optstr + + def opt_version(self): + print alot.__version__ + sys.exit(0) + + +class ComposeOptions(SubcommandOptions): + optParameters = [ + ['sender', '', None, 'From line'], + ['subject', '', None, 'subject line'], + ['cc', '', None, 'copy to'], + ['bcc', '', None, 'blind copy to'], + ['template', '', None, 'path to template file'], + ['attach', '', None, 'files to attach'], + ] + + def parseArgs(self, *args): + SubcommandOptions.parseArgs(self, *args) + self['to'] = ' '.join(args) + + +class Options(usage.Options): + optFlags = [["read-only", "r", 'open db in read only mode'], ] + + def colourint(val): + val = int(val) + if val not in [1, 16, 256]: + raise ValueError("Not in range") + return val + colourint.coerceDoc = "Must be 1, 16 or 256" + optParameters = [ + ['config', 'c', '~/.config/alot/config', 'config file'], + ['notmuch-config', 'n', '~/.notmuch-config', 'notmuch config'], + ['colour-mode', 'C', 256, 'terminal colour mode', colourint], + ['mailindex-path', 'p', None, 'path to notmuch index'], + ['debug-level', 'd', 'info', 'debug level used with -l'], + ['logfile', 'l', '/dev/null', 'logfile'], + ] + subCommands = [['search', None, SubcommandOptions, "search for threads"], + ['compose', None, ComposeOptions, "compose a message"]] + + def opt_version(self): + print alot.__version__ + sys.exit(0) def main(): # interpret cml arguments - args = parse_args() + args = Options() + try: + args.parseOptions() # When given no argument, parses sys.argv[1:] + except usage.UsageError, errortext: + print '%s: %s' % (sys.argv[0], errortext) + print '%s: Try --help for usage details.' % (sys.argv[0]) + sys.exit(1) # locate alot config files configfiles = [ @@ -58,14 +89,14 @@ def main(): 'alot', 'config'), os.path.expanduser('~/.alot.rc'), ] - if args.configfile: - expanded_path = os.path.expanduser(args.configfile) + if args['config']: + expanded_path = os.path.expanduser(args['config']) if not os.path.exists(expanded_path): sys.exit('File %s does not exist' % expanded_path) configfiles.insert(0, expanded_path) # locate notmuch config - notmuchfile = os.path.expanduser(args.notmuchconfigfile) + notmuchfile = os.path.expanduser(args['notmuch-config']) try: # read the first alot config file we find @@ -81,8 +112,8 @@ def main(): sys.exit(e) # setup logging - numeric_loglevel = getattr(logging, args.debug_level.upper(), None) - logfilename = os.path.expanduser(args.logfile) + numeric_loglevel = getattr(logging, args['debug-level'].upper(), None) + logfilename = os.path.expanduser(args['logfile']) logging.basicConfig(level=numeric_loglevel, filename=logfilename) logger = logging.getLogger() @@ -91,12 +122,16 @@ def main(): aman = AccountManager(settings.config) # get ourselves a database manager - dbman = DBManager(path=args.db_path, ro=args.read_only) + dbman = DBManager(path=args['mailindex-path'], ro=args['read-only']) # get initial searchstring try: - if args.command != '': - cmd = commands.commandfactory(args.command, 'global') + if args.subCommand == 'search': + query = ' '.join(args.subOptions.args) + cmd = commands.commandfactory('search ' + query, 'global') + elif args.subCommand == 'compose': + cmdstring = 'compose %s' % args.subOptions.as_argparse_opts() + cmd = commands.commandfactory(cmdstring, 'global') else: default_commandline = settings.config.get('general', 'initial_command') @@ -109,7 +144,7 @@ def main(): logger, aman, cmd, - args.colours, + args['colour-mode'], ) if __name__ == "__main__": diff --git a/alot/message.py b/alot/message.py index d674b730..0b8131c1 100644 --- a/alot/message.py +++ b/alot/message.py @@ -3,6 +3,7 @@ import email import tempfile import re import mimetypes +import shlex from datetime import datetime from email.header import Header import email.charset as charset @@ -276,7 +277,8 @@ def extract_body(mail, types=None): tmpfile.close() #create and call external command cmd = handler % tmpfile.name - rendered_payload = helper.cmd_output(cmd) + cmdlist = shlex.split(cmd.encode('utf-8', errors='ignore')) + rendered_payload, errmsg, retval = helper.call_cmd(cmdlist) #remove tempfile os.unlink(tmpfile.name) if rendered_payload: # handler had output @@ -434,6 +436,8 @@ class Envelope(object): self.attachments = list(attachments) self.sign = sign self.encrypt = encrypt + self.sent_time = None + self.modified_since_sent = False def __str__(self): return "Envelope (%s)\n%s" % (self.headers, self.body) @@ -445,6 +449,9 @@ class Envelope(object): """ self.headers[name] = val + if self.sent_time: + self.modified_since_sent = True + def __getitem__(self, name): """getter for header values. :raises: KeyError if undefined @@ -454,15 +461,36 @@ class Envelope(object): def __delitem__(self, name): del(self.headers[name]) + if self.sent_time: + self.modified_since_sent = True + def get(self, key, fallback=None): """secure getter for header values that allows specifying a `fallback` - return string (defaults to None). This doesn't raise KeyErrors""" + return string (defaults to None). This returns the first matching value + and doesn't raise KeyErrors""" + if key in self.headers: + value = self.headers[key][0] + else: + value = fallback + return value + + def get_all(self, key, fallback=[]): + """returns all header values for given key""" if key in self.headers: value = self.headers[key] else: value = fallback return value + def add(self, key, value): + """add header value""" + if key not in self.headers: + self.headers[key] = [] + self.headers[key].append(value) + + if self.sent_time: + self.modified_since_sent = True + def attach(self, path, filename=None, ctype=None): """ attach a file @@ -475,9 +503,13 @@ class Envelope(object): :type ctype: str """ + path = os.path.expanduser(path) part = helper.mimewrap(path, filename, ctype) self.attachments.append(part) + if self.sent_time: + self.modified_since_sent = True + def construct_mail(self): """ compiles the information contained in this envelope into a @@ -489,18 +521,20 @@ class Envelope(object): msg.attach(textpart) else: msg = textpart - for k, v in self.headers.items(): - msg[k] = encode_header(k, v) + for k, vlist in self.headers.items(): + for v in vlist: + msg[k] = encode_header(k, v) for a in self.attachments: msg.attach(a) - logging.debug(msg) return msg - def parse_template(self, tmp): + def parse_template(self, tmp, reset=False): """parses a template or user edited string to fills this envelope. :param tmp: the string to parse. :type tmp: str + :param reset: remove previous envelope content + :type reset: bool """ logging.debug('GoT: """\n%s\n"""' % tmp) m = re.match('(?P<h>([a-zA-Z0-9_-]+:.+\n)*)(?P<b>(\s*.*)*)', tmp) @@ -510,6 +544,10 @@ class Envelope(object): headertext = d['h'] self.body = d['b'] + # remove existing content + if reset: + self.headers = {} + # go through multiline, utf-8 encoded headers # we decode the edited text ourselves here as # email.message_from_file can't deal with raw utf8 header values @@ -517,9 +555,12 @@ class Envelope(object): for line in headertext.splitlines(): if re.match('[a-zA-Z0-9_-]+:', line): # new k/v pair if key and value: # save old one from stack - self.headers[key] = value # save + self.add(key, value) # save key, value = line.strip().split(':', 1) # parse new pair elif key and value: # append new line without key prefix value += line if key and value: # save last one if present - self.headers[key] = value + self.add(key, value) + + if self.sent_time: + self.modified_since_sent = True @@ -201,31 +201,25 @@ class UI(object): """ closes given :class:`~alot.buffers.Buffer`. - This shuts down alot in case its the last - active buffer. Otherwise it removes it from the bufferlist - and calls its cleanup() method. + This it removes it from the bufferlist and calls its cleanup() method. """ buffers = self.buffers if buf not in buffers: string = 'tried to close unknown buffer: %s. \n\ni have:%s' self.logger.error(string % (buf, self.buffers)) - elif len(buffers) == 1: - self.logger.info('closing the last buffer, exiting') - cmd = commandfactory('exit') - self.apply_command(cmd) + elif self.current_buffer == buf: + self.logger.debug('UI: closing current buffer %s' % buf) + index = buffers.index(buf) + buffers.remove(buf) + offset = config.getint('general', 'bufferclose_focus_offset') + nextbuffer = buffers[(index + offset) % len(buffers)] + self.buffer_focus(nextbuffer) + buf.cleanup() else: - if self.current_buffer == buf: - self.logger.debug('UI: closing current buffer %s' % buf) - index = buffers.index(buf) - buffers.remove(buf) - offset = config.getint('general', 'bufferclose_focus_offset') - nextbuffer = buffers[(index + offset) % len(buffers)] - self.buffer_focus(nextbuffer) - else: - string = 'closing buffer %d:%s' - self.logger.debug(string % (buffers.index(buf), buf)) - buffers.remove(buf) + string = 'closing buffer %d:%s' + self.logger.debug(string % (buffers.index(buf), buf)) + buffers.remove(buf) buf.cleanup() def buffer_focus(self, buf): @@ -1,20 +1,20 @@ #!/usr/bin/env python -""" -This file is part of alot, a terminal UI to notmuch mail (notmuchmail.org). -Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com> -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. +# This file is part of alot, a terminal UI to notmuch mail (notmuchmail.org). +# Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see <http://www.gnu.org/licenses/>. -""" from alot.init import main main() |