Skip to content

Commit

Permalink
Merge branch 'master' into command_composition
Browse files Browse the repository at this point in the history
Conflicts:
	alot/ui.py
  • Loading branch information
a3nm committed Sep 29, 2012
2 parents 8a7e50c + 27c9105 commit 7eacc33
Show file tree
Hide file tree
Showing 45 changed files with 810 additions and 431 deletions.
22 changes: 22 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
0.3.3:
* interpret (semicolon separated) sequences of commands
* new input handling: allow for binding sequences of keypresses
* add ability to overwrite default bindings
* remove tempfiles (email drafts) as late as possible for better error recovery
* confirmation prompt when closing unsent envelopes
* prevent accidental double sendout of envelopes
* fix focus placement after tagcommand on last entry in search buffer
* new command 'buffer' that can directly jump to buffer with given number
* extra: sup theme
* fix tagstring sorting in taglist buffer
* update docs
* lots of internal cleanups
* search buffer theming fixes (alignment of threadline parts)
* fix help box theming
* comma-separate virtual "Tags" header added before printing mails
* fix pipeto command for interactive (foreground) shell commands
* handle possible errors occurring while saving mails
* indicate (yet uninterpreted) input queue in the status bar
* handle python exceptions that occur during 'call' command


0.3.2:
* fix bad GPG signatures for mails with attachments
* new theme-files + tags section syntax
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,25 @@ Have a look at the [user manual][docs] for installation notes, advanced usage,
customization and hacking guides.

Do comment on the code or file issues! I'm curious what you think of it.
You can talk to me in `#notmuch@Freenode`.
You can talk to me in `#notmuch@freenode`.

Current features include:
-------------------------
* modular and command prompt driven interface
* multiple accounts for sending mails via sendmail
* spawn terminals for asynchronous editing of mails
* tab completion and usage help for all commands
* contacts completion using customizable lookups commands
* user configurable keyboard maps
* spawn terminals for asynchronous editing of mails
* theming, optionally in 2, 16 or 256 colours
* tag specific theming and tag string translation
* (python) hooks to react on events and do custom formatting
* python shell for introspection
* forward/reply/group-reply of emails
* printing/piping of mails and threads
* multiple accounts for sending mails via sendmail
* notification popups with priorities
* database manager that manages a write queue to the notmuch index
* configurable status bar

Soonish to be addressed non-features:
-------------------------------------
Expand All @@ -31,7 +32,6 @@ See [here][features], most notably:
* async. calls to mimeparts renderer, parsing of VT colour escape sequences.
see #272. Milestone `0.4`
* encryption/decryption for messages via gnupg CLI (see branch `feature-gnupg`). Milestone `0.4`
* bind sequences of key presses to commands (POC in `postponed-multiinput`). Milestone `0.5`
* live search results while you're typing (POC in `postponed-livesearch`). Milestone `0.6`
* search for message (POC in `postponed-messagesmode`). Milestone `0.6`
* search for strings in displayed buffer. MS `0.7`
Expand All @@ -44,7 +44,7 @@ The arrow keys, `page-up/down`, `j`, `k` and `Space` can be used to move the foc
`Escape` cancels prompts and `Enter` selects. Hit `:` at any time and type in commands
to the prompt.

The interface shows one buffer at a time, you can use `tab` and `Shift-Tab` to switch
The interface shows one buffer at a time, you can use `Tab` and `Shift-Tab` to switch
between them, close the current buffer with `d` and list them all with `;`.

The buffer type or *mode* (displayed at the bottom left) determines which prompt commands
Expand All @@ -55,4 +55,3 @@ to the prompt; The key bindings for the current mode are listed upon pressing `?
[urwid]: http://excess.org/urwid/
[docs]: http://alot.rtfd.org
[features]: https://github.com/pazz/alot/issues?labels=feature
[wiki]: https://github.com/pazz/alot/wiki
2 changes: 1 addition & 1 deletion alot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__productname__ = 'alot'
__version__ = '0.3.2+'
__version__ = '0.3.3+'
__copyright__ = "Copyright (C) 2012 Patrick Totzke"
__author__ = "Patrick Totzke"
__author_email__ = "patricktotzke@gmail.com"
Expand Down
20 changes: 14 additions & 6 deletions alot/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ class SendingMailFailed(RuntimeError):
pass


class StoreMailError(Exception):
pass


class Account(object):
"""
Datastructure that represents an email account. It manages this account's
settings, can send and store mails to maildirs (drafts/send).
.. note::
This is an abstract class that leaves :meth:`send_mail` unspecified.
See :class:`SendmailAccount` for a subclass that uses a sendmail
command to send out mails.
Expand Down Expand Up @@ -70,7 +73,8 @@ def get_addresses(self):

def store_mail(self, mbx, mail):
"""
stores given mail in mailbox. If mailbox is maildir, set the S-flag.
stores given mail in mailbox. If mailbox is maildir, set the S-flag and
return path to newly added mail. Oherwise this will return `None`.
:param mbx: mailbox to use
:type mbx: :class:`mailbox.Mailbox`
Expand All @@ -79,6 +83,7 @@ def store_mail(self, mbx, mail):
:returns: absolute path of mail-file for Maildir or None if mail was
successfully stored
:rtype: str or None
:raises: StoreMailError
"""
if not isinstance(mbx, mailbox.Mailbox):
logging.debug('Not a mailbox')
Expand All @@ -93,10 +98,13 @@ def store_mail(self, mbx, mail):
logging.debug('no Maildir')
msg = mailbox.Message(mail)

message_id = mbx.add(msg)
mbx.flush()
mbx.unlock()
logging.debug('got id : %s' % id)
try:
message_id = mbx.add(msg)
mbx.flush()
mbx.unlock()
logging.debug('got mailbox msg id : %s' % message_id)
except Exception as e:
raise StoreMailError(e)

path = None
# add new Maildir message to index and add tags
Expand Down
23 changes: 16 additions & 7 deletions alot/addressbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,34 @@ class AddressBook(object):
unspecified. See :class:`AbookAddressBook` and
:class:`MatchSdtoutAddressbook` for implementations.
"""
def __init__(self, ignorecase=True):
self.reflags = re.IGNORECASE if ignorecase else 0

def get_contacts(self):
"""list all contacts tuples in this abook as (name, email) tuples"""
return []

def lookup(self, prefix=''):
"""looks up all contacts with given prefix (in name or address)"""
def lookup(self, query=''):
"""looks up all contacts where name or address match query"""
res = []
query = '.*%s.*' % query
for name, email in self.get_contacts():
if name.startswith(prefix) or email.startswith(prefix):
res.append((name, email))
try:
if re.match(query, name, self.reflags) or re.match(query, email, self.reflags):
res.append((name, email))
except:
pass
return res


class AbookAddressBook(AddressBook):
""":class:`AddressBook` that parses abook's config/database files"""
def __init__(self, path='~/.abook/addressbook'):
def __init__(self, path='~/.abook/addressbook', **kwargs):
"""
:param path: path to theme file
:type path: str
"""
AddressBook.__init__(self, **kwargs)
DEFAULTSPATH = os.path.join(os.path.dirname(__file__), 'defaults')
self._spec = os.path.join(DEFAULTSPATH, 'abook_contacts.spec')
path = os.path.expanduser(path)
Expand All @@ -57,7 +64,8 @@ def get_contacts(self):

class MatchSdtoutAddressbook(AddressBook):
""":class:`AddressBook` that parses a shell command's output for lookups"""
def __init__(self, command, match=None):

def __init__(self, command, match=None, **kwargs):
"""
:param command: lookup command
:type command: str
Expand All @@ -67,6 +75,7 @@ def __init__(self, command, match=None):
:regexp:`^(?P<email>[^@]+@[^\t]+)\t+(?P<name>[^\t]+)`.
:type match: str
"""
AddressBook.__init__(self, **kwargs)
self.command = command
if not match:
self.match = '^(?P<email>[^@]+@[^\t]+)\t+(?P<name>[^\t]+)'
Expand All @@ -84,7 +93,7 @@ def lookup(self, prefix):
lines = resultstring.splitlines()
res = []
for l in lines:
m = re.match(self.match, l)
m = re.match(self.match, l, self.reflags)
if m:
info = m.groupdict()
email = info['email'].strip()
Expand Down
8 changes: 6 additions & 2 deletions alot/buffers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ def cleanup(self):
pass

def get_info(self):
"""return dict of meta infos about this buffer"""
"""
return dict of meta infos about this buffer.
This can be requested to be displayed in the statusbar.
"""
return {}


Expand Down Expand Up @@ -154,7 +157,8 @@ def rebuild(self):
key_att = settings.get_theming_attribute('envelope', 'header_key')
value_att = settings.get_theming_attribute('envelope',
'header_value')
self.header_wgt = HeadersList(lines, key_att, value_att)
gaps_att = settings.get_theming_attribute('envelope', 'header')
self.header_wgt = HeadersList(lines, key_att, value_att, gaps_att)
displayed_widgets.append(self.header_wgt)

#display attachments
Expand Down
27 changes: 11 additions & 16 deletions alot/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,9 @@

class Command(object):
"""base class for commands"""
def __init__(self, prehook=None, posthook=None):
"""
:param prehook: name of the hook to call directly before
applying this command
:type prehook: str
:param posthook: name of the hook to call directly after
applying this command
:type posthook: str
"""
self.prehook = prehook
self.posthook = posthook
def __init__(self):
self.prehook = None
self.posthook = None
self.undoable = False
self.help = self.__doc__

Expand Down Expand Up @@ -191,17 +183,20 @@ def commandfactory(cmdline, mode='global'):

parms = vars(parser.parse_args(args))
parms.update(forcedparms)
logging.debug('PARMS: %s' % parms)

logging.debug('cmd parms %s' % parms)

# create Command
cmd = cmdclass(**parms)

# set pre and post command hooks
get_hook = settings.get_hook
parms['prehook'] = get_hook('pre_%s_%s' % (mode, cmdname)) or \
cmd.prehook = get_hook('pre_%s_%s' % (mode, cmdname)) or \
get_hook('pre_global_%s' % cmdname)
parms['posthook'] = get_hook('post_%s_%s' % (mode, cmdname)) or \
cmd.posthook = get_hook('post_%s_%s' % (mode, cmdname)) or \
get_hook('post_global_%s' % cmdname)

logging.debug('cmd parms %s' % parms)
return cmdclass(**parms)
return cmd


pyfiles = glob.glob1(os.path.dirname(__file__), '*.py')
Expand Down
2 changes: 1 addition & 1 deletion alot/commands/bufferlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
MODE = 'bufferlist'


@registerCommand(MODE, 'select')
@registerCommand(MODE, 'open')
class BufferFocusCommand(Command):
"""focus selected buffer"""
def apply(self, ui):
Expand Down
44 changes: 35 additions & 9 deletions alot/commands/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from twisted.internet.defer import inlineCallbacks
import datetime

from alot.account import SendingMailFailed
from alot.account import SendingMailFailed, StoreMailError
from alot.errors import GPGProblem
from alot import buffers
from alot import commands
Expand Down Expand Up @@ -118,13 +118,25 @@ class SendCommand(Command):
def apply(self, ui):
currentbuffer = ui.current_buffer # needed to close later
envelope = currentbuffer.envelope

# This is to warn the user before re-sending
# an already sent message in case the envelope buffer
# was not closed because it was the last remaining buffer.
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

# don't do anything if another SendCommand is in the middle of sending
# the message and we were triggered accidentally
if envelope.sending:
msg = 'sending this message already!'
logging.debug(msg)
return

frm = envelope.get('From')
sname, saddr = email.Utils.parseaddr(frm)

Expand Down Expand Up @@ -155,38 +167,50 @@ def apply(self, ui):
clearme = ui.notify('sending..', timeout=-1)

def afterwards(returnvalue):
envelope.sending = False
logging.debug('mail sent successfully')
ui.clear_notify([clearme])
envelope.sent_time = datetime.datetime.now()
ui.apply_command(commands.globals.BufferCloseCommand())
ui.notify('mail sent successfully')

# store mail locally
# add Date header
# This can raise StoreMailError
path = account.store_sent_mail(mail)

# add mail to index if maildir path available
if path is not None:
logging.debug('adding new mail to index')
ui.dbman.add_message(path, account.sent_tags)
ui.dbman.add_message(path, account.sent_tags + envelope.tags)
ui.apply_command(globals.FlushCommand())

def errb(failure):
def send_errb(failure):
envelope.sending = False
ui.clear_notify([clearme])
failure.trap(SendingMailFailed)
logging.error(failure.getTraceback())
errmsg = 'failed to send: %s' % failure.value
ui.notify(errmsg, priority='error')

def store_errb(failure):
failure.trap(StoreMailError)
logging.error(failure.getTraceback())
errmsg = 'could not store mail: %s' % failure.value
ui.notify(errmsg, priority='error')

envelope.sending = True
d = account.send_mail(mail)
d.addCallback(afterwards)
d.addErrback(errb)
d.addErrback(send_errb)
d.addErrback(store_errb)
logging.debug('added errbacks,callbacks')


@registerCommand(MODE, 'edit', arguments=[
(['--spawn'], {'action': BooleanAction, 'default':None,
'help':'spawn editor in new terminal'}),
(['--refocus'], {'action': BooleanAction, 'default':True,
'help':'refocus envelope after editing'}),
])
'help':'refocus envelope after editing'})])
class EditCommand(Command):
"""edit mail"""
def __init__(self, envelope=None, spawn=None, refocus=True, **kwargs):
Expand Down Expand Up @@ -287,8 +311,10 @@ def openEnvelopeFromTmpfile():
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)
on_success=openEnvelopeFromTmpfile,
spawn=self.force_spawn,
thread=self.force_spawn,
refocus=self.refocus)
ui.apply_command(cmd)


Expand Down
Loading

0 comments on commit 7eacc33

Please sign in to comment.