summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL60
-rw-r--r--NEWS24
-rw-r--r--README.md (renamed from README)20
-rw-r--r--USAGE224
-rw-r--r--alot/account.py19
-rw-r--r--alot/buffer.py5
-rw-r--r--alot/command.py194
-rw-r--r--alot/db.py30
-rw-r--r--alot/helper.py44
-rwxr-xr-xalot/init.py7
-rw-r--r--alot/message.py78
-rw-r--r--alot/settings.py31
-rw-r--r--alot/ui.py55
-rw-r--r--alot/widgets.py48
-rw-r--r--data/example.full.rc28
-rw-r--r--data/example.rc8
-rw-r--r--docs/interaction.rst4
17 files changed, 604 insertions, 275 deletions
diff --git a/INSTALL b/INSTALL
index 8ed2eaeb..a995a3f6 100644
--- a/INSTALL
+++ b/INSTALL
@@ -3,39 +3,45 @@ INSTALL
Alot depends on development versions of notmuch and urwid.
Note that due to restrictions on argparse and subprocess,
-you need to run python v>=2.7.
+you need to run *python v>=2.7*.
-1. make sure you have urwid V>= 1.0
--------------------------------------
+urwid
+-----
+make sure you have
+
+ git clone http://github.com/wardi/urwid
+ cd urwid
+ sudo python setup.py install
-> git clone http://github.com/wardi/urwid
-> cd urwid
-> sudo python setup.py install
It seems you need the python headers for this.
On debian/ubuntu:
->aptitude install python2.7-dev
+ aptitude install python2.7-dev
+
+
+notmuch
+-------
+install notmuch *and* python bindings from git:
-2. install notmuch and python bindings from git:
-------------------------------------------------
+ git clone git://notmuchmail.org/git/notmuch
-> git clone git://notmuchmail.org/git/notmuch
-> cd notmuch
-> ./configure
-> make
-> sudo make install
-> cd bindings/python
-> sudo python setup.py install
+ cd notmuch
+ ./configure
+ make
+ sudo make install
+ cd bindings/python
+ sudo python setup.py install
-3. get alot and install it from git:
-------------------------------------
+alot
+----
+get alot and install it from git:
-> git clone git://github.com/pazz/alot alot
-> cd alot
-> sudo python setup.py install
+ git clone git://github.com/pazz/alot alot
+ cd alot
+ sudo python setup.py install
-That's it, now "alot" should be in your path.
+That's it, now `alot` should be in your path.
Alot tries to be as unobtrusive as possible, with one exception:
It forces you to use UTF-8 encoding whereever it can:
@@ -43,12 +49,10 @@ All text parts and headers of outgoing emails are converted to utf-8,
notmuch tagstrings, edited emails and config files are interpreted as utf-8.
All configs are optional, but if you want to send mails you need to
-specify at least one account section:
+specify at least one account section in you config:
- [account uoe]
- realname = Your Name
- address = your@address
- sendmail_command = msmtp
- sent_mailbox = maildir:///home/you/mail/Sent
+ [account uoe]
+ realname = Your Name
+ address = your@address
See USAGE for default keymaps and how to do fancy customization.
diff --git a/NEWS b/NEWS
new file mode 100644
index 00000000..5b56780e
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,24 @@
+0.11
+
+This minor release is mostly bug fixes and some small features.
+I wanted to release a more stable and usable version before I start
+hacking on a thread view rewrite.
+
+* renamed config section [tag translate] to [tag-translate]
+* docs: more elaborate API docs, INSTALL and USAGE as markdown in github wiki
+* more compact header displays in thread view
+* command-line history (for one session)
+* editor file encoding is now user configurable
+* signatures for outgoing mails per account
+* optional display of message content in search results
+* config option for strftime formating of timestamps
+* printing
+
+* fix parse multiline headers from edited tempfile
+* fix reply to unusually formated mails (e.g. no recipient)
+* fix lots of encoding issues
+* handle extra wide characters in tag widgets
+* fixes in ui.prompt
+* fix storing outgoing mails to sent_box
+* more liberal header encoding for outgoing mails
+* use mimetype lib to guess right content-type of attachments
diff --git a/README b/README.md
index facd5334..6e8ca745 100644
--- a/README
+++ b/README.md
@@ -1,5 +1,14 @@
-This is a proposal for a terminal gui for notmuch mail, written in python.
-You can find some old screenshots in data/alot*png
+This is a proposal for a terminal gui for [notmuch mail][notmuch]
+written in python using the [urwid][urwid] toolkit.
+
+You can find some old screenshots in `data/alot*png`,
+the files `INSTALL` and `USAGE` contain instructions on how to set it up,
+use and customize. These files are nicely rendered in the [github wiki][wiki].
+The API docs for the current master branch are [here][api].
+the `docs` directory contains their sources.
+
+Do comment on the code or file issues! I'm curious what you think of it.
+You can talk to me in #notmuch@Freenode.
Current features include:
-------------------------
@@ -14,12 +23,17 @@ Current features include:
* priorizable notification popups
* database manager that manages a write queue to the notmuch index
* user configurable keyboard maps
+ * printing
Soonish to be addressed non-features:
-------------------------------------
* encryption/decryption for messages
* search for strings in displayed buffer
- * print command
* folding for message parts
* undo for commands
* addressbook integration
+
+[notmuch]: http://notmuchmail.org/
+[urwid]: http://excess.org/urwid/
+[api]: http://pazz.github.com/alot/
+[wiki]: https://github.com/pazz/alot/wiki
diff --git a/USAGE b/USAGE
index 6b92a438..e537956b 100644
--- a/USAGE
+++ b/USAGE
@@ -1,164 +1,180 @@
-USAGE
+Usage
=====
In all views, arrows, page-up/down, j,k and space can be used to move the focus.
Escape cancels prompts. You can hit ":" at any time and type in commands
to the prompt. Any commandline can be mapped by using the "MODE-maps" sections
in the config file. These are the default keymaps:
-[global-maps]
-$ = flush
-: = prompt
-; = bufferlist
-@ = refresh
-I = search tag:inbox AND NOT tag:killed
-L = taglist
-U = search tag:unread
-\ = prompt search
-d = bclose
-m = compose
-o = prompt search
-q = exit
-shift tab = bprevious
-tab = bnext
-
-[bufferlist-maps]
-enter = openfocussed
-x = closefocussed
-
-[search-maps]
-& = toggletag killed
-O = refineprompt
-a = toggletag inbox
-enter = openthread
-l = retagprompt
-| = refineprompt
-
-[envelope-maps]
-a = attach
-enter = reedit
-s = prompt subject
-t = prompt to
-y = send
-
-[taglist-maps]
-enter = select
-
-[thread-maps]
-C = fold --all
-E = unfold --all
-H = toggleheaders
-a = toggletag inbox
-enter = select
-f = forward
-g = groupreply
-r = reply
-
-
-CONFIG
-=====
+ [global-maps]
+ $ = flush
+ : = prompt
+ ; = bufferlist
+ @ = refresh
+ I = search tag:inbox AND NOT tag:killed
+ L = taglist
+ U = search tag:unread
+ \ = prompt search
+ d = bclose
+ m = compose
+ o = prompt search
+ q = exit
+ shift tab = bprevious
+ tab = bnext
+
+ [bufferlist-maps]
+ enter = openfocussed
+ x = closefocussed
+
+ [search-maps]
+ & = toggletag killed
+ O = refineprompt
+ a = toggletag inbox
+ enter = openthread
+ l = retagprompt
+ | = refineprompt
+
+ [envelope-maps]
+ a = attach
+ enter = reedit
+ s = prompt subject
+ t = prompt to
+ y = send
+
+ [taglist-maps]
+ enter = select
+
+ [thread-maps]
+ C = fold --all
+ E = unfold --all
+ H = toggleheaders
+ P = print --all
+ a = toggletag inbox
+ enter = select
+ f = forward
+ g = groupreply
+ p = print
+ r = reply
+
+Config
+------
Just like offlineimap or notmuch itself, alot reads a config file in the "INI" syntax:
It consists of some sections whose names are given in square brackets, followed by
key-value pairs that use "=" or ":" as separator, ';' and '#' are comment-prefixes.
-The default location for the config file is ~/.alot.rc.
-You can find a complete example config in data/example.full.rc.
+The default location for the config file is `~/.alot.rc`.
+You can find a complete example config in `data/example.full.rc`.
Here is a key for the interpreted sections:
-[general] : global settings: set your editor etc
-[account X] : defines the account X: realname, email address, sendmail
-[X-maps] : defines keymaps for mode X. possible modes are:
- envelope, search, thread, taglist, bufferlist and global.
- global-maps are valid in all modes.
-[tag translate] : defines a map from tagnames to strings that is used when
- displaying tags. utf-8 symbols welcome.
-[Xc-theme] : define colour palette for colour mode. X is in {1, 16, 256}.
+ [general]
+ global settings: set your editor etc
+
+ [account X]
+ defines the account X: realname, email address, sendmail
+
+ [X-maps]
+ defines keymaps for mode X. possible modes are:
+ envelope, search, thread, taglist, bufferlist and global.
+ global-maps are valid in all modes.
+
+ [tag-translate]
+ defines a map from tagnames to strings that is used when
+ displaying tags. utf-8 symbols welcome.
+
+ [Xc-theme]
+ define colour palette for colour mode. X is in {1, 16, 256}.
All configs are optional, but if you want to send mails you need to
specify at least one account section.
-A sample gmail section:
+A sample gmail section looks like this:
- [account gmail]
- realname = Patrick Totzke
- address = patricktotzke@gmail.com
- aliases = patricktotzke@googlemail.com
- gpg_key = D7D6C5AA
- sender_type = sendmail
- sendmail_command = msmtp --account=gmail -t
+ [account gmail]
+ realname = Patrick Totzke
+ address = patricktotzke@gmail.com
+ aliases = patricktotzke@googlemail.com
+ gpg_key = D7D6C5AA
+ sender_type = sendmail
+ sendmail_command = msmtp --account=gmail -t
I use this for my uni-account:
- [account uoe]
- realname = Patrick Totzke
- address = ...
- aliases = foobar@myuni.uk;f.bar@myuni.uk;f.b100@students.myuni.uk
- sender_type = sendmail
- sendmail_command = msmtp --account=uoe -t
- sent_mailbox = maildir:///home/pazz/mail/uoe/Sent
+ [account uoe]
+ realname = Patrick Totzke
+ address = ...
+ aliases = foobar@myuni.uk;f.bar@myuni.uk;f.b100@students.myuni.uk
+ sender_type = sendmail
+ sendmail_command = msmtp --account=uoe -t
+ sent_box = maildir:///home/pazz/mail/uoe/Sent
+ draft_box = maildir:///home/pazz/mail/uoe/Drafts
+ signature = ~/my_uni_vcard.vcs
+ signature_filename = p.totzke.vcs
Caution: Sending mails is only supported via sendmail for now. If you want
-to use a sendmail command different from 'sendmail', specify it as sendmail_command.
+to use a sendmail command different from `sendmail`, specify it as `sendmail_command`.
-send_mailbox specifies the mailbox where you want outgoing mails to be stored
+`send_mailbox` specifies the mailbox where you want outgoing mails to be stored
after successfully sending them. You can use mbox, maildir, mh, babyl and mmdf
in the protocol part of the url.
-
+The file specified by `signature` is attached to all outgoing mails from this account, optionally renamed to
+`signature_filename`.
Hooks
-=====
-
+-----
Before and after every command execution, alot calls this commands pre/post hook:
Hooks are python callables with arity 4 that live in a module specified by
-hooksfile in the global section of your config. Per default this points to '~/.alot.py'
+`hooksfile` in the `[global]` section of your config. Per default this points to `~/.alot.py`
For every command X, the callable 'pre_X' will be called before X and 'post_X' afterwards.
When a hook gets called, it receives instances of
-1) alot.ui.UI, the main user interface object that can prompt etc.
-2) alot.db.DBManager, the applications database manager
-3) alot.account.AccountManager, can be used to look up account info
-4) alot.settings.config, a configparser to access the user's config
+1. `alot.ui.UI`, the main user interface object that can prompt etc.
+2. `alot.db.DBManager`, the applications database manager
+3. `alot.account.AccountManager`, can be used to look up account info
+4. `alot.settings.config`, a configparser to access the users config
+
+An autogenerated API doc for these can be found at http://pazz.github.com/alot/ ,
+the sphinx sources live in the `docs` folder.
As an example, consider this pre-hook for the exit command,
that logs a personalized goodby message:
+```python
def pre_exit(ui, dbman, accountman, config):
accounts = accountman.get_accounts()
if accounts:
ui.logger.info('goodbye, %s!' % accounts[0].realname)
else:
ui.logger.info('goodbye!')
+```
Theming
-=======
-
-You can change the colour settings in the section [Xc-theme], where X is the
+-------
+You can change the colour settings in the section `[Xc-theme]`, where X is the
colour mode you use. This defaults to 256, but 16 and 1 are also possible.
The colourmode can be changed in the globals section or given as a commandline
parameter (-C).
The keys in this section should be self explanatory. In 16c and 256c modes you can define Y_fg and
Y_bg for the foreground and background of each keyword Y. These can define colorcodes and flags
-like 'underline' or 'bold', comma separated if you want more than one. See urwids doc on Attributes:
- http://excess.org/urwid/reference.html#AttrSpec
+like `underline` or `bold`, comma separated if you want more than one. See urwids doc on Attributes:
+http://excess.org/urwid/reference.html#AttrSpec
Urwid privides a neat script that makes choosing colours easy, which can be found here:
- http://excess.org/urwid/browser/palette_test.py
+http://excess.org/urwid/browser/palette_test.py
-See data/example.full.rc for a complete list of widgets that can be themed.
+See `data/example.full.rc` for a complete list of widgets that can be themed.
Moreover, keywords that start with "tag_" will be used to display specific tags. For instance, you
can use the following to always display the "todo" tag in white on red, when in 256c-mode.
-[256c-theme]
-tag_todo_bg = #d66
-tag_todo_fg = white
+ [256c-theme]
+ tag_todo_bg = #d66
+ tag_todo_fg = white
-You can translate tag strings before displaying them using the [tag translate] section.
+You can translate tag strings before displaying them using the [tag-translate] section.
A key=value statement in this section is interpreted as:
-Always display the tag "key" as string "value". Utf-8 symbols are welcome here.
+Always display the tag `key` as string `value`. Utf-8 symbols are welcome here.
See e.g. http://panmental.de/symbols/info.htm
I personally display my maildir flags like this:
-[tag translate]
-flagged = ⚑
-unread = ✉
-replied = ⇄
-
+ [tag-translate]
+ flagged = ⚑
+ unread = ✉
+ replied = ⇄
diff --git a/alot/account.py b/alot/account.py
index 720d3878..13bed02b 100644
--- a/alot/account.py
+++ b/alot/account.py
@@ -41,10 +41,13 @@ class Account:
gpg_key = None
"""gpg fingerprint. CURRENTLY IGNORED"""
signature = None
- """signature to append to outgoing mails. CURRENTLY IGNORED"""
+ """path to a signature file to append to outgoing mails."""
+ signature_filename = None
+ """filename of signature file in attachment"""
def __init__(self, address=None, aliases=None, realname=None, gpg_key=None,
- signature=None, sent_box=None, draft_box=None):
+ signature=None, signature_filename=None, sent_box=None,
+ draft_box=None):
self.address = address
self.aliases = []
if aliases:
@@ -52,10 +55,11 @@ class Account:
self.realname = realname
self.gpg_key = gpg_key
self.signature = signature
+ self.signature_filename = signature_filename
self.sent_box = None
if sent_box:
- mburl = urlparse(sent_mailbox)
+ mburl = urlparse(sent_box)
if mburl.scheme == 'mbox':
self.sent_box = mailbox.mbox(mburl.path)
elif mburl.scheme == 'maildir':
@@ -69,7 +73,7 @@ class Account:
self.draft_box = None
if draft_box:
- mburl = urlparse(sent_mailbox)
+ mburl = urlparse(draft_box)
if mburl.scheme == 'mbox':
self.draft_box = mailbox.mbox(mburl.path)
elif mburl.scheme == 'maildir':
@@ -91,11 +95,11 @@ class Account:
"""
mbx.lock()
if isinstance(mbx, mailbox.Maildir):
- msg = mailbox.MaildirMessage(email)
+ msg = mailbox.MaildirMessage(mail)
msg.set_flags('S')
else:
- msg = mailbox.Message(email)
- key = mbx.add(email)
+ msg = mailbox.Message(mail)
+ key = mbx.add(mail)
mbx.flush()
mbx.unlock()
@@ -160,6 +164,7 @@ class AccountManager:
'aliases',
'gpg_key',
'signature',
+ 'signature_filename',
'type',
'sendmail_command',
'sent_box',
diff --git a/alot/buffer.py b/alot/buffer.py
index 4d6eca6d..eb476530 100644
--- a/alot/buffer.py
+++ b/alot/buffer.py
@@ -124,7 +124,8 @@ class EnvelopeBuffer(Buffer):
for part in self.mail.walk():
if not part.is_multipart():
if part.get_content_maintype() != 'text':
- lines.append(widgets.AttachmentWidget(part, selectable=False))
+ lines.append(widgets.AttachmentWidget(part,
+ selectable=False))
self.attachment_wgt = urwid.Pile(lines)
displayed_widgets.append(self.attachment_wgt)
@@ -251,10 +252,10 @@ class ThreadBuffer(Buffer):
for mw in self.get_message_widgets():
msg = mw.get_message()
if msg.matches(querystring):
+ mw.fold(visible=True)
if 'unread' in msg.get_tags():
msg.remove_tags(['unread'])
self.ui.apply_command(command.FlushCommand())
- mw.fold(visible=True)
def get_message_widgets(self):
return self.body.body.contents
diff --git a/alot/command.py b/alot/command.py
index 3f7578a6..942d7797 100644
--- a/alot/command.py
+++ b/alot/command.py
@@ -17,22 +17,20 @@ along with notmuch. If not, see <http://www.gnu.org/licenses/>.
Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com>
"""
import os
-import glob
+import re
import code
+import glob
import logging
import threading
import subprocess
+import shlex
import email
import tempfile
import mimetypes
from email.parser import Parser
from email import Charset
from email.header import Header
-from email import encoders
from email.message import Message
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.image import MIMEImage
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import urwid
@@ -41,6 +39,7 @@ import buffer
import settings
import widgets
import completion
+import helper
from db import DatabaseROError
from db import DatabaseLockedError
from completion import ContactsCompleter
@@ -369,7 +368,7 @@ class ComposeCommand(Command):
cmpl = AccountCompleter(ui.accountman)
fromaddress = ui.prompt(prefix='From>', completer=cmpl, tab=1)
validaddresses = [a.address for a in accounts] + [None]
- while fromaddress not in validaddresses:
+ while fromaddress not in validaddresses: # TODO: not cool
ui.notify('no account for this address. (<esc> cancels)')
fromaddress = ui.prompt(prefix='From>', completer=cmpl)
if not fromaddress:
@@ -381,10 +380,16 @@ class ComposeCommand(Command):
#get To header
if 'To' not in self.mail:
to = ui.prompt(prefix='To>', completer=ContactsCompleter())
+ if to == None:
+ ui.notify('canceled')
+ return
self.mail['To'] = encode_header('to', to)
if settings.config.getboolean('general', 'ask_subject') and \
not 'Subject' in self.mail:
subject = ui.prompt(prefix='Subject>')
+ if subject == None:
+ ui.notify('canceled')
+ return
self.mail['Subject'] = encode_header('subject', subject)
ui.apply_command(EnvelopeEditCommand(mail=self.mail))
@@ -486,10 +491,7 @@ class ReplyCommand(Command):
reply.attach(bodypart)
# copy subject
- if 'Subject' not in mail or mail['Subject'] == None:
- subject = ''
- else:
- subject = mail['Subject']
+ subject = mail.get('Subject', '')
if not subject.startswith('Re:'):
subject = 'Re: ' + subject
reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
@@ -497,7 +499,7 @@ class ReplyCommand(Command):
# set From
my_addresses = ui.accountman.get_addresses()
matched_address = ''
- in_to = [a for a in my_addresses if a in mail['To']]
+ in_to = [a for a in my_addresses if a in mail.get('To', '')]
if in_to:
matched_address = in_to[0]
else:
@@ -511,10 +513,9 @@ class ReplyCommand(Command):
reply['From'] = encode_header('From', fromstring)
# set To
- #reply['To'] = Header(mail['From'].encode('utf-8'), 'UTF-8').encode()
del(reply['To'])
if self.groupreply:
- cleared = self.clear_my_address(my_addresses, mail['To'])
+ cleared = self.clear_my_address(my_addresses, mail.get('To', ''))
if cleared:
logging.info(mail['From'] + ', ' + cleared)
to = mail['From'] + ', ' + cleared
@@ -537,7 +538,7 @@ class ReplyCommand(Command):
reply['In-Reply-To'] = '<%s>' % self.message.get_message_id()
# set References header
- old_references = mail['References']
+ old_references = mail.get('References', '')
if old_references:
old_references = old_references.split()
references = old_references[-8:]
@@ -597,14 +598,14 @@ class ForwardCommand(Command):
reply.attach(mail)
# copy subject
- subject = mail['Subject']
+ subject = mail.get('Subject', '')
subject = 'Fwd: ' + subject
reply['Subject'] = Header(subject.encode('utf-8'), 'UTF-8').encode()
# set From
my_addresses = ui.accountman.get_addresses()
matched_address = ''
- in_to = [a for a in my_addresses if a in mail['To']]
+ in_to = [a for a in my_addresses if a in mail.get('To', '')]
if in_to:
matched_address = in_to[0]
else:
@@ -651,6 +652,57 @@ class ToggleHeaderCommand(Command):
msgw.toggle_full_header()
+class PrintCommand(Command):
+ def __init__(self, all=False, separately=False, confirm=True, **kwargs):
+ Command.__init__(self, **kwargs)
+ self.all = all
+ self.separately = separately
+ self.confirm = confirm
+
+ def apply(self, ui):
+ # get messages to print
+ if self.all:
+ thread = ui.current_buffer.get_selected_thread()
+ to_print = thread.get_messages().keys()
+ confirm_msg = 'print all messages in thread?'
+ ok_msg = 'printed thread: %s' % str(thread)
+ else:
+ to_print = [ui.current_buffer.get_selected_message()]
+ confirm_msg = 'print this message?'
+ ok_msg = 'printed message: %s' % str(to_print[0])
+
+ # ask for confirmation if needed
+ if self.confirm:
+ if not ui.choice(confirm_msg) == 'yes':
+ return
+
+ # prepare message sources
+ mailstrings = [m.get_email().as_string() for m in to_print]
+ if not self.separately:
+ mailstrings = ['\n\n'.join(mailstrings)]
+
+ # get print command
+ cmd = settings.config.get('general', 'print_cmd')
+ args = shlex.split(cmd.encode('ascii'))
+
+ # print
+ try:
+ for mail in mailstrings:
+ proc = subprocess.Popen(args, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = proc.communicate(mail)
+ if proc.poll(): # returncode is not 0
+ raise OSError(err)
+ except OSError, e: # handle errors
+ ui.notify(str(e), priority='error')
+ return
+
+ # display 'done' message
+ ui.notify(ok_msg)
+
+
+
class SaveAttachmentCommand(Command):
def __init__(self, all=False, path=None, **kwargs):
Command.__init__(self, **kwargs)
@@ -752,16 +804,29 @@ class EnvelopeEditCommand(Command):
self.mail = ui.current_buffer.get_email()
def openEnvelopeFromTmpfile():
- f = open(tf.name)
- editor_input = f.read().decode('utf-8')
+ # 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.
- #split editor out
+ # get input
+ f = open(tf.name)
+ enc = settings.config.get('general', 'editor_writes_encoding')
+ editor_input = f.read().decode(enc)
headertext, bodytext = editor_input.split('\n\n', 1)
+ # go through multiline, utf-8 encoded headers
+ key = value = None
for line in headertext.splitlines():
- key, value = line.strip().split(':', 1)
- value = value.strip()
- del self.mail[key] # ensure there is only one
+ if re.match('\w+:', line): #new k/v pair
+ if key and value: # save old one from stack
+ del self.mail[key] # ensure unique values in mails
+ self.mail[key] = encode_header(key, value) # save
+ key, value = line.strip().split(':', 1) # parse new pair
+ elif key and value: # append new line without key prefix to value
+ value += line
+ if key and value: # save last one if present
+ del self.mail[key]
self.mail[key] = encode_header(key, value)
if self.mail.is_multipart():
@@ -785,7 +850,7 @@ class EnvelopeEditCommand(Command):
for key in edit_headers:
value = u''
if key in self.mail:
- value = decode_header(self.mail[key])
+ value = decode_header(self.mail.get(key, ''))
headertext += '%s: %s\n' % (key, value)
if self.mail.is_multipart():
@@ -834,6 +899,21 @@ class EnvelopeSendCommand(Command):
sname, saddr = email.Utils.parseaddr(frm)
account = ui.accountman.get_account_by_address(saddr)
if account:
+ # attach signature file if present
+ if account.signature:
+ sig = os.path.expanduser(account.signature)
+ if os.path.isfile(sig):
+ if account.signature_filename:
+ name = account.signature_filename
+ else:
+ name = None
+ helper.attach(sig, mail, filename=name)
+ else:
+ ui.notify('could not locate signature: %s' % sig,
+ priority='error')
+ if not ui.choice('send without signature') == 'yes':
+ return
+
clearme = ui.notify('sending..', timeout=-1, block=False)
reason = account.send_mail(mail)
ui.clear_notify([clearme])
@@ -849,56 +929,31 @@ class EnvelopeSendCommand(Command):
class EnvelopeAttachCommand(Command):
- def __init__(self, path=None, **kwargs):
+ def __init__(self, path=None, mail=None, **kwargs):
Command.__init__(self, **kwargs)
- self.files = []
- if path:
- self.files = glob.glob(os.path.expanduser(path))
+ self.mail = mail
+ self.path = path
def apply(self, ui):
- if not self.files:
- path = ui.prompt(prefix='attach files matching:', text='~/',
- completer=completion.PathCompleter())
- if path:
- self.files = glob.glob(os.path.expanduser(path))
- if not self.files:
+ msg = self.mail
+ if not msg:
+ msg = ui.current_buffer.get_email()
+
+ if self.path:
+ files = filter(os.path.isfile,
+ glob.glob(os.path.expanduser(self.path)))
+ if not files:
ui.notify('no matches, abort')
return
- logging.info(self.files)
- msg = ui.current_buffer.get_email()
- for path in self.files:
- ctype, encoding = mimetypes.guess_type(path)
- if ctype is None or encoding is not None:
- # No guess could be made, or the file is encoded (compressed),
- # so use a generic bag-of-bits type.
- ctype = 'application/octet-stream'
- maintype, subtype = ctype.split('/', 1)
- if maintype == 'text':
- fp = open(path)
- # Note: we should handle calculating the charset
- part = MIMEText(fp.read(), _subtype=subtype)
- fp.close()
- elif maintype == 'image':
- fp = open(path, 'rb')
- part = MIMEImage(fp.read(), _subtype=subtype)
- fp.close()
- elif maintype == 'audio':
- fp = open(path, 'rb')
- part = MIMEAudio(fp.read(), _subtype=subtype)
- fp.close()
- else:
- fp = open(path, 'rb')
- part = MIMEBase(maintype, subtype)
- part.set_payload(fp.read())
- fp.close()
- # Encode the payload using Base64
- encoders.encode_base64(part)
- # Set the filename parameter
- part.add_header('Content-Disposition', 'attachment',
- filename=os.path.basename(path))
- msg.attach(part)
+ else:
+ ui.notify('no files specified, abort')
+
+ logging.info("attaching: %s" % files)
+ for path in files:
+ helper.attach(path, msg)
- ui.current_buffer.set_email(msg)
+ if not self.mail: # set the envelope msg iff we got it from there
+ ui.current_buffer.set_email(msg)
# TAGLIST
@@ -937,6 +992,7 @@ COMMANDS = {
'groupreply': (ReplyCommand, {'groupreply': True}),
'forward': (ForwardCommand, {}),
'fold': (FoldMessagesCommand, {'visible': False}),
+ 'print': (PrintCommand, {}),
'unfold': (FoldMessagesCommand, {'visible': True}),
'select': (ThreadSelectCommand, {}),
'save': (SaveAttachmentCommand, {}),
@@ -1052,6 +1108,10 @@ def interpret_commandline(cmdline, mode):
filepath = os.path.expanduser(params)
if os.path.isfile(filepath):
return commandfactory(cmd, mode=mode, path=filepath)
+ elif cmd == 'print':
+ args = [a.strip() for a in params.split()]
+ return commandfactory(cmd, mode=mode, all=('--all' in args),
+ separately=('--separately' in args))
elif not params and cmd in ['exit', 'flush', 'pyshell', 'taglist',
'bclose', 'compose', 'openfocussed',
diff --git a/alot/db.py b/alot/db.py
index 3af0c3eb..5dca9b88 100644
--- a/alot/db.py
+++ b/alot/db.py
@@ -21,6 +21,7 @@ from datetime import datetime
from collections import deque
from message import Message
+from settings import notmuchconfig as config
DB_ENC = 'utf-8'
@@ -89,8 +90,7 @@ class DBManager:
sync_maildir_flags=sync)
msg.thaw()
- def tag(self, querystring, tags, remove_rest=False,
- sync_maildir_flags=False):
+ def tag(self, querystring, tags, remove_rest=False):
"""
add tags to all matching messages. Raises
:exc:`DatabaseROError` if in read only mode.
@@ -105,6 +105,7 @@ class DBManager:
"""
if self.ro:
raise DatabaseROError()
+ sync_maildir_flags = config.getboolean('maildir', 'synchronize_flags')
if remove_rest:
self.writequeue.append(('set', querystring, tags,
sync_maildir_flags))
@@ -112,7 +113,7 @@ class DBManager:
self.writequeue.append(('tag', querystring, tags,
sync_maildir_flags))
- def untag(self, querystring, tags, sync_maildir_flags=False):
+ def untag(self, querystring, tags):
"""
add tags to all matching messages. Raises
:exc:`DatabaseROError` if in read only mode.
@@ -125,6 +126,7 @@ class DBManager:
"""
if self.ro:
raise DatabaseROError()
+ sync_maildir_flags = config.getboolean('maildir', 'synchronize_flags')
self.writequeue.append(('untag', querystring, tags,
sync_maildir_flags))
@@ -147,6 +149,13 @@ class DBManager:
except:
return None
+ def get_message(self, mid):
+ """returns the message with given id as alot.message.Message object"""
+ mode = Database.MODE.READ_ONLY
+ db = Database(path=self.path, mode=mode)
+ msg = db.find_message(mid)
+ return Message(self, msg)
+
def get_all_tags(self):
"""returns all tags as list of strings"""
db = Database(path=self.path)
@@ -204,7 +213,7 @@ class Thread:
l.sort()
return l
- def add_tags(self, tags, sync_maildir_flags=False):
+ def add_tags(self, tags):
"""adds tags to all messages in this thread
:param tags: tags to add
@@ -212,11 +221,10 @@ class Thread:
"""
newtags = set(tags).difference(self._tags)
if newtags:
- self._dbman.tag('thread:' + self._id, newtags,
- sync_maildir_flags=sync_maildir_flags)
+ self._dbman.tag('thread:' + self._id, newtags)
self._tags = self._tags.union(newtags)
- def remove_tags(self, tags, sync_maildir_flags=False):
+ def remove_tags(self, tags):
"""remove tags from all messages in this thread
:param tags: tags to remove
@@ -224,11 +232,10 @@ class Thread:
"""
rmtags = set(tags).intersection(self._tags)
if rmtags:
- self._dbman.untag('thread:' + self._id, tags,
- sync_maildir_flags=sync_maildir_flags)
+ self._dbman.untag('thread:' + self._id, tags)
self._tags = self._tags.difference(rmtags)
- def set_tags(self, tags, sync_maildir_flags=False):
+ def set_tags(self, tags):
"""set tags of all messages in this thread. This removes all tags and
attaches the given ones in one step.
@@ -236,8 +243,7 @@ class Thread:
:type tags: list of str
"""
if tags != self._tags:
- self._dbman.tag('thread:' + self._id, tags, remove_rest=True,
- sync_maildir_flags=sync_maildir_flags)
+ self._dbman.tag('thread:' + self._id, tags, remove_rest=True)
self._tags = set(tags)
def get_authors(self): # TODO: make this return a list of strings
diff --git a/alot/helper.py b/alot/helper.py
index 92ed76e9..eb0d4978 100644
--- a/alot/helper.py
+++ b/alot/helper.py
@@ -22,11 +22,18 @@ from datetime import timedelta
import shlex
import subprocess
import email
+import mimetypes
+import os
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
def shorten(string, maxlen):
if len(string) > maxlen - 3:
- string = string[:maxlen - 3] + '...'
+ string = string[:maxlen - 3] + u'\u2026'
return string
@@ -52,3 +59,38 @@ def cmd_output(command_line):
except OSError:
return None
return output
+
+
+def attach(path, mail, filename=None):
+ ctype, encoding = mimetypes.guess_type(path)
+ if ctype is None or encoding is not None:
+ # No guess could be made, or the file is encoded (compressed),
+ # so use a generic bag-of-bits type.
+ ctype = 'application/octet-stream'
+ maintype, subtype = ctype.split('/', 1)
+ if maintype == 'text':
+ fp = open(path)
+ # Note: we should handle calculating the charset
+ part = MIMEText(fp.read(), _subtype=subtype)
+ fp.close()
+ elif maintype == 'image':
+ fp = open(path, 'rb')
+ part = MIMEImage(fp.read(), _subtype=subtype)
+ fp.close()
+ elif maintype == 'audio':
+ fp = open(path, 'rb')
+ part = MIMEAudio(fp.read(), _subtype=subtype)
+ fp.close()
+ else:
+ fp = open(path, 'rb')
+ part = MIMEBase(maintype, subtype)
+ part.set_payload(fp.read())
+ fp.close()
+ # Encode the payload using Base64
+ email.encoders.encode_base64(part)
+ # Set the filename parameter
+ if not filename:
+ filename = os.path.basename(path)
+ part.add_header('Content-Disposition', 'attachment',
+ filename=filename)
+ mail.attach(part)
diff --git a/alot/init.py b/alot/init.py
index 689e677d..e5a13a5b 100755
--- a/alot/init.py
+++ b/alot/init.py
@@ -31,7 +31,10 @@ def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-c', dest='configfile',
default='~/.alot.rc',
- help='config file')
+ 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],
@@ -61,6 +64,8 @@ def main():
#read config file
configfilename = os.path.expanduser(args.configfile)
settings.config.read(configfilename)
+ notmuchfile = os.path.expanduser(args.notmuchconfigfile)
+ settings.notmuchconfig.read(notmuchfile)
settings.hooks.setup(settings.config.get('general', 'hooksfile'))
# setup logging
diff --git a/alot/message.py b/alot/message.py
index 5fc3444c..f4cc2f39 100644
--- a/alot/message.py
+++ b/alot/message.py
@@ -20,11 +20,13 @@ import os
import email
import tempfile
import re
+import mimetypes
from datetime import datetime
from email.header import Header
import helper
from settings import get_mime_handler
+from settings import config
class Message:
@@ -43,7 +45,6 @@ class Message:
self._thread = thread
self._datetime = datetime.fromtimestamp(msg.get_date())
self._filename = msg.get_filename()
- # TODO: change api to return unicode
self._from = msg.get_header('From')
self._email = None # will be read upon first use
self._attachments = None # will be read upon first use
@@ -54,7 +55,6 @@ class Message:
aname, aaddress = self.get_author()
if not aname:
aname = aaddress
- #tags = ','.join(self.get_tags())
return "%s (%s)" % (aname, self.get_datestring())
def __hash__(self):
@@ -115,9 +115,14 @@ class Message:
t = self.get_thread()
return t.get_replies_to(self)
- def get_datestring(self, pretty=True):
- """returns formated datestring in sup-style, eg: 'Yest.3pm'"""
- return helper.pretty_datetime(self._datetime)
+ def get_datestring(self):
+ """returns formated datestring"""
+ formatstring = config.get('general', 'timestamp_format')
+ if formatstring:
+ res = self._datetime.strftime(formatstring)
+ else:
+ res = helper.pretty_datetime(self._datetime)
+ return res
def get_author(self):
"""returns realname and address pair of this messages author"""
@@ -156,6 +161,20 @@ class Message:
searchfor = querystring + ' AND id:' + self._id
return self._dbman.count_messages(searchfor) > 0
+ def get_text_content(self):
+ res = ''
+ for part in self.get_email().walk():
+ ctype = part.get_content_type()
+ enc = part.get_content_charset()
+ if part.get_content_maintype() == 'text':
+ raw_payload = part.get_payload(decode=True)
+ if enc:
+ raw_payload = raw_payload.decode(enc, errors='replace')
+ else:
+ raw_payload = unicode(raw_payload, errors='replace')
+ res += raw_payload
+ return res
+
def extract_body(mail):
bodytxt = ''
@@ -209,36 +228,54 @@ def decode_to_unicode(part):
def decode_header(header):
+ """decode a header value to a unicode string
+
+ values are usually a mixture of different substrings
+ encoded in quoted printable using diffetrent encodings.
+ This turns it into a single unicode string
+
+ :param header: the header value
+ :type header: str in us-ascii
+ :rtype: unicode
+ """
+
valuelist = email.header.decode_header(header)
- value = u''
+ decoded_list = []
for v, enc in valuelist:
if enc:
- value = value + v.decode(enc)
+ decoded_list.append(v.decode(enc))
else:
- value = value + v
- value = value.replace('\r', '')
- value = value.replace('\n', ' ')
- return value
+ decoded_list.append(v)
+ return u' '.join(decoded_list)
def encode_header(key, value):
+ """encodes a unicode string as a valid header value
+
+ :param key: the header field this value will be stored in
+ :type key: str
+ :param value: the value to be encoded
+ :type value: unicode
+ """
+ # handle list of "realname <email>" entries separately
if key.lower() in ['from', 'to', 'cc', 'bcc']:
rawentries = value.split(',')
encodedentries = []
for entry in rawentries:
m = re.search('\s*(.*)\s+<(.*\@.*\.\w*)>$', entry)
- if m:
+ if m: # If a realname part is contained
name, address = m.groups()
- header = Header(name + ' ', 'utf-8')
+ # try to encode as ascii, if that fails, revert to utf-8
+ # name must be a unicode string here
+ header = Header(name)
+ # append address part encoded as ascii
header.append('<%s>' % address, charset='ascii')
encodedentries.append(header.encode())
- else:
- encodedentries.append(entry.encode('ascii', errors='replace'))
+ else: # pure email address
+ encodedentries.append(entry)
value = Header(','.join(encodedentries))
- elif key.lower() == 'subject':
- value = Header(value, 'UTF-8')
else:
- value = Header(value.encode('ascii', errors='replace'))
+ value = Header(value)
return value
@@ -263,7 +300,10 @@ class Attachment:
def get_content_type(self):
"""mime type of the attachment"""
- return self.part.get_content_type()
+ ctype = self.part.get_content_type()
+ if ctype == 'octet/stream' and self.get_filename():
+ ctype, enc = mimetypes.guess_type(self.get_filename())
+ return ctype
def get_size(self):
"""returns attachments size as human-readable string"""
diff --git a/alot/settings.py b/alot/settings.py
index ad4bf4bb..ef3babff 100644
--- a/alot/settings.py
+++ b/alot/settings.py
@@ -28,9 +28,11 @@ DEFAULTS = {
'general': {
'colourmode': '256',
'editor_cmd': "/usr/bin/vim -f -c 'set filetype=mail' +",
+ 'editor_writes_encoding': 'UTF-8',
'terminal_cmd': 'x-terminal-emulator -e',
'spawn_editor': 'False',
'displayed_headers': 'From,To,Cc,Bcc,Subject',
+ 'display_content_in_threadline': 'False',
'authors_maxlength': '30',
'ask_subject': 'True',
'notify_timeout': '2',
@@ -38,6 +40,8 @@ DEFAULTS = {
'flush_retry_timeout': '5',
'hooksfile': '~/.alot.py',
'bug_on_exit': 'False',
+ 'timestamp_format': '',
+ 'print_cmd': 'muttprint',
},
'16c-theme': {
'bufferlist_focus_bg': 'dark gray',
@@ -85,6 +89,8 @@ DEFAULTS = {
'threadline_bg': 'default',
'threadline_content_bg': 'default',
'threadline_content_fg': 'dark gray',
+ 'threadline_content_focus_bg': 'dark cyan',
+ 'threadline_content_focus_fg': 'dark gray',
'threadline_date_bg': 'default',
'threadline_date_fg': 'light gray',
'threadline_date_focus_bg': 'dark cyan',
@@ -129,6 +135,7 @@ DEFAULTS = {
'threadline_authors': 'default,underline',
'threadline_authors_focus': 'standout',
'threadline_content': 'default',
+ 'threadline_content_focus': 'standout',
'threadline_date': 'default',
'threadline_date_focus': 'standout',
'threadline_focus': 'standout',
@@ -185,6 +192,8 @@ DEFAULTS = {
'threadline_bg': 'default',
'threadline_content_bg': 'default',
'threadline_content_fg': '#866',
+ 'threadline_content_focus_bg': 'g58',
+ 'threadline_content_focus_fg': '#866',
'threadline_date_bg': 'default',
'threadline_date_fg': 'g58',
'threadline_date_focus_bg': 'g58',
@@ -234,17 +243,19 @@ DEFAULTS = {
'C': 'fold --all',
'E': 'unfold --all',
'H': 'toggleheaders',
+ 'P': 'print --all',
'a': 'toggletag inbox',
'enter': 'select',
'f': 'forward',
'g': 'groupreply',
+ 'p': 'print',
'r': 'reply',
},
'taglist-maps': {
'enter': 'select',
},
'envelope-maps': {
- 'a': 'attach',
+ 'a': 'prompt attach ~/',
'y': 'send',
'enter': 'reedit',
't': 'prompt to ',
@@ -263,11 +274,16 @@ DEFAULTS = {
}
}
+NOTMUCH_DEFAULTS = {
+ 'maildir': {
+ 'synchronize_flags': 'False',
+ },
+}
-class CustomConfigParser(SafeConfigParser):
+
+class DefaultsConfigParser(SafeConfigParser):
def __init__(self, defaults):
self.defaults = defaults
- self.hooks = None
SafeConfigParser.__init__(self)
self.optionxform = lambda x: x
for sec in defaults.keys():
@@ -293,6 +309,12 @@ class CustomConfigParser(SafeConfigParser):
value = self.get(section, option, **kwargs)
return [s.strip() for s in value.split(',')]
+
+class AlotConfigParser(DefaultsConfigParser):
+ def __init__(self, defaults):
+ DefaultsConfigParser.__init__(self, defaults)
+ self.hooks = None
+
def read(self, file):
if not os.path.isfile(file):
return
@@ -382,7 +404,8 @@ class HookManager:
return f
-config = CustomConfigParser(DEFAULTS)
+config = AlotConfigParser(DEFAULTS)
+notmuchconfig = DefaultsConfigParser(NOTMUCH_DEFAULTS)
hooks = HookManager()
mailcaps = mailcap.getcaps()
diff --git a/alot/ui.py b/alot/ui.py
index 15e6dd7a..74d5aa51 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -67,6 +67,7 @@ class UI:
self.show_statusbar = config.getboolean('general', 'show_statusbar')
self.notificationbar = None
self.mode = ''
+ self.commandprompthistory = []
self.logger.debug('setup bindings')
cmd = commandfactory('search', query=initialquery)
@@ -76,20 +77,24 @@ class UI:
def keypress(self, key):
self.logger.debug('unhandeled input: %s' % key)
- def prompt(self, prefix='>', text=u'', tab=0, completer=None):
+ def prompt(self, prefix='>', text=u'', completer=None, tab=0, history=[]):
"""prompt for text input
:param prefix: text to print before the input field
:type prefix: str
:param text: initial content of the input field
:type text: str
+ :param completer: completion object to use
+ :type completer: `alot.completion.Completer`
:param tab: number of tabs to press initially
(to select completion results)
:type tab: int
- :param completer: completion object to use
- :type completer: `alot.completion.Completer`
+ :param history: history to be used for up/down keys
+ :type history: list of str
"""
self.logger.info('open prompt')
+ history = list(history) # make a local copy
+ historypos = None
leftpart = urwid.Text(prefix, align='left')
if completer:
editpart = CompleteEdit(completer, edit_text=text)
@@ -113,33 +118,63 @@ class UI:
while not keys:
keys = self.mainloop.screen.get_input()
for key in keys:
+ self.logger.debug('prompt got key: %s' % key)
if command_map[key] == 'select':
self.mainframe.set_footer(footer)
self.mainframe.set_focus('body')
return editpart.get_edit_text()
- if command_map[key] == 'cancel':
+ elif command_map[key] == 'cancel':
self.mainframe.set_footer(footer)
self.mainframe.set_focus('body')
return None
+ elif key in ['up', 'down']:
+ if history:
+ if historypos == None:
+ history.append(editpart.get_edit_text())
+ historypos = len(history) - 1
+ if key == 'cursor up':
+ historypos = (historypos - 1) % len(history)
+ else:
+ historypos = (historypos + 1) % len(history)
+ editpart.set_edit_text(history[historypos])
+ self.mainloop.draw_screen()
+
else:
size = (20,) # don't know why they want a size here
editpart.keypress(size, key)
self.mainloop.draw_screen()
def commandprompt(self, startstring):
+ """prompt for a commandline and interpret/apply it upon enter
+
+ :param startstring: initial text in edit part
+ :type startstring: str
+ """
self.logger.info('open command shell')
mode = self.current_buffer.typename
cmdline = self.prompt(prefix=':',
text=startstring,
completer=CommandLineCompleter(self.dbman,
self.accountman,
- mode))
+ mode),
+ history=self.commandprompthistory,
+ )
if cmdline:
- cmd = interpret_commandline(cmdline, mode)
- if cmd:
- self.apply_command(cmd)
- else:
- self.notify('invalid command')
+ self.interpret_commandline(cmdline)
+
+ def interpret_commandline(self, cmdline):
+ """interpret and apply a commandstring
+
+ :param cmdline: command string to apply
+ :type cmdline: str
+ """
+ mode = self.current_buffer.typename
+ self.commandprompthistory.append(cmdline)
+ cmd = interpret_commandline(cmdline, mode)
+ if cmd:
+ self.apply_command(cmd)
+ else:
+ self.notify('invalid command')
def buffer_open(self, b):
"""
diff --git a/alot/widgets.py b/alot/widgets.py
index 2b3a6f19..2b811146 100644
--- a/alot/widgets.py
+++ b/alot/widgets.py
@@ -32,13 +32,20 @@ class ThreadlineWidget(urwid.AttrMap):
self.dbman = dbman
self.thread = dbman.get_thread(tid)
self.tag_widgets = []
+ self.display_content = config.getboolean('general',
+ 'display_content_in_threadline')
self.rebuild()
urwid.AttrMap.__init__(self, self.columns,
'threadline', 'threadline_focus')
def rebuild(self):
cols = []
- datestring = pretty_datetime(self.thread.get_newest_date()).rjust(10)
+ formatstring = config.get('general', 'timestamp_format')
+ newest = self.thread.get_newest_date()
+ if formatstring:
+ datestring = newest.strftime(formatstring)
+ else:
+ datestring = pretty_datetime(newest).rjust(10)
self.date_w = urwid.AttrMap(urwid.Text(datestring), 'threadline_date')
cols.append(('fixed', len(datestring), self.date_w))
@@ -52,20 +59,30 @@ class ThreadlineWidget(urwid.AttrMap):
for tag in tags:
tw = TagWidget(tag)
self.tag_widgets.append(tw)
- cols.append(('fixed', tw.len(), tw))
+ cols.append(('fixed', tw.width(), tw))
authors = self.thread.get_authors() or '(None)'
maxlength = config.getint('general', 'authors_maxlength')
- authorsstring = shorten(authors, maxlength)
+ authorsstring = shorten(authors, maxlength).strip()
self.authors_w = urwid.AttrMap(urwid.Text(authorsstring),
'threadline_authors')
cols.append(('fixed', len(authorsstring), self.authors_w))
- subjectstring = self.thread.get_subject()
+ subjectstring = self.thread.get_subject().strip()
self.subject_w = urwid.AttrMap(urwid.Text(subjectstring, wrap='clip'),
'threadline_subject')
if subjectstring:
- cols.append(self.subject_w)
+ cols.append(('fixed', len(subjectstring), self.subject_w))
+
+ if self.display_content:
+ msgs = self.thread.get_messages().keys()
+ msgs.sort()
+ lastcontent = ' '.join([m.get_text_content() for m in msgs])
+ contentstring = lastcontent.replace('\n', ' ').strip()
+ self.content_w = urwid.AttrMap(urwid.Text(contentstring,
+ wrap='clip'),
+ 'threadline_content')
+ cols.append(self.content_w)
self.columns = urwid.Columns(cols, dividechars=1)
self.original_widget = self.columns
@@ -79,6 +96,8 @@ class ThreadlineWidget(urwid.AttrMap):
tw.set_focussed()
self.authors_w.set_attr_map({None: 'threadline_authors_focus'})
self.subject_w.set_attr_map({None: 'threadline_subject_focus'})
+ if self.display_content:
+ self.content_w.set_attr_map({None: 'threadline_content_focus'})
else:
self.date_w.set_attr_map({None: 'threadline_date'})
self.mailcount_w.set_attr_map({None: 'threadline_mailcount'})
@@ -86,6 +105,8 @@ class ThreadlineWidget(urwid.AttrMap):
tw.set_unfocussed()
self.authors_w.set_attr_map({None: 'threadline_authors'})
self.subject_w.set_attr_map({None: 'threadline_subject'})
+ if self.display_content:
+ self.content_w.set_attr_map({None: 'threadline_content'})
return urwid.AttrMap.render(self, size, focus)
def selectable(self):
@@ -117,16 +138,17 @@ class BufferlineWidget(urwid.Text):
class TagWidget(urwid.AttrMap):
def __init__(self, tag):
self.tag = tag
- self.translated = config.get('tag translate', tag, fallback=tag)
- # encode to utf-8 before passing to urwid (issue #4)
+ self.translated = config.get('tag-translate', tag, fallback=tag)
self.translated = self.translated.encode('utf-8')
- txt = urwid.Text(self.translated, wrap='clip')
+ self.txt = urwid.Text(self.translated, wrap='clip')
normal = config.get_tagattr(tag)
focus = config.get_tagattr(tag, focus=True)
- urwid.AttrMap.__init__(self, txt, normal, focus)
+ urwid.AttrMap.__init__(self, self.txt, normal, focus)
- def len(self):
- return len(self.translated)
+ def width(self):
+ # evil voodoo hotfix for double width chars that may
+ # lead e.g. to strings with length 1 that need width 2
+ return self.txt.pack()[0]
def selectable(self):
return True
@@ -421,8 +443,8 @@ class MessageHeaderWidget(urwid.AttrMap):
else:
value = value + v
#sanitize it a bit:
- value = value.replace('\t', '')
- value = value.replace('\r', '')
+ value = value.replace('\t', ' ')
+ value = ' '.join([line.strip() for line in value.splitlines()])
keyw = ('fixed', max_key_len + 1,
urwid.Text(('message_header_key', key)))
valuew = urwid.Text(('message_header_value', value))
diff --git a/data/example.full.rc b/data/example.full.rc
index f6ebb1ef..71ae8301 100644
--- a/data/example.full.rc
+++ b/data/example.full.rc
@@ -1,27 +1,53 @@
[general]
+
# ask for subject when compose
ask_subject = True
+
# max length of authors line in thread widgets
authors_maxlength = 30
+
+# confirm exit
bug_on_exit = False
+
# number of colours your terminal supports
colourmode = 256
+
+# fill threadline with message content
+display_content_in_threadline = False
+
# headers that get displayed by default
displayed_headers = From,To,Cc,Bcc,Subject
+
+
# editor command
editor_cmd = /usr/bin/vim -f -c 'set filetype=mail' +
+editor_writes_encoding' = UTF-8
+
# timeout in secs after a failed attempt to flush is repeated
flush_retry_timeout = 5
+
# where to look up hooks
hooksfile = ~/.alot.py
+
# time in secs to display status messages
notify_timeout = 2
+
# display statusline?
show_statusbar = True
+
spawn_editor = False
# set terminal for asynchronous editing
terminal_cmd = x-terminal-emulator -e
+# strftime format for timestamps. Note: you must escape % here:
+# use '%%' instead of '%'. otherwise see
+# http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+timestamp_format = ''
+
+#how to print messages:
+print_cmd = muttprint
+
+
[global-maps]
$ = flush
: = prompt
@@ -65,10 +91,12 @@ enter = select
C = fold --all
E = unfold --all
H = toggleheaders
+P = print --all
a = toggletag inbox
enter = select
f = forward
g = groupreply
+p = print
r = reply
[command-aliases]
diff --git a/data/example.rc b/data/example.rc
index 04b383db..699bfd53 100644
--- a/data/example.rc
+++ b/data/example.rc
@@ -9,8 +9,6 @@ authors_maxlength = 30
displayed_headers = From,To,Cc,Bcc,Subject
ask_subject = True
notify_timeout = 1 # how long (in seconds) notifications are shown
-show_notificationbar = False
-show_statusbar = True
flush_retry_timeout = 5 # timeout in secs after a failed attempt to flush is repeated
[account gmail]
@@ -18,9 +16,11 @@ realname = john doe
address = john.doe@gmail.com
aliases = john.doe@googlemail.com;john.d@g.com # ';' separated list of alias addresses
gpg_key = 123ABC
-signature = /home/john/.signature
+signature = /home/john/.signature.gmail # path to signature file
+signature_filename = sig.txt # filename in attachment part
sendmail_command = msmtp --account=gmail -t
-sent_mailbox = maildir:///home/john/mail/gmail/Sent # we accept mbox,maildir,mh,babyl,mmdf here
+sent_box = maildir:///home/john/mail/gmail/Sent # we accept mbox,maildir,mh,babyl,mmdf here
+draft_box = maildir:///home/john/mail/gmail/Drafts # we accept mbox,maildir,mh,babyl,mmdf here
[search-maps]
t = toggletag todo
diff --git a/docs/interaction.rst b/docs/interaction.rst
index a73a348c..95765ad2 100644
--- a/docs/interaction.rst
+++ b/docs/interaction.rst
@@ -10,6 +10,10 @@ User Interaction
.. automethod:: choice
+ .. automethod:: commandprompt
+
+ .. automethod:: interpret_commandline
+
.. automethod:: notify
.. automethod:: prompt