summaryrefslogtreecommitdiff
path: root/alot
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2011-08-27 22:04:59 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2011-08-27 22:04:59 +0100
commita32214f88f41c4919272780412976c8994838d09 (patch)
tree7105d9af07ed985bf46ae6597f2983f2b3b0e46b /alot
parent744e86bbaa5f521908100f4fd3c8b8fc43a894f4 (diff)
parent4ea9ef9dab9202149e851ef1bfff2b3cb29a34d2 (diff)
Merge branch 'develop' into addressbook
Diffstat (limited to 'alot')
-rw-r--r--alot/__init__.py4
-rw-r--r--alot/buffer.py3
-rw-r--r--alot/command.py125
-rw-r--r--alot/message.py2
-rw-r--r--alot/settings.py5
-rw-r--r--alot/ui.py30
-rw-r--r--alot/widgets.py19
7 files changed, 136 insertions, 52 deletions
diff --git a/alot/__init__.py b/alot/__init__.py
index ba4f02f1..4a84dc9b 100644
--- a/alot/__init__.py
+++ b/alot/__init__.py
@@ -17,10 +17,10 @@ along with notmuch. If not, see <http://www.gnu.org/licenses/>.
Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com>
"""
__productname__ = 'alot'
-__version__ = '0.1'
+__version__ = '0.11'
__copyright__ = "Copyright (C) 2011 Patrick Totzke"
__author__ = "Patrick Totzke"
__author_email__ = "patricktotzke@gmail.com"
-__description__ = "Terminal User Interface for notmuch mail (notmuchmail.org)"
+__description__ = "Command-line MUA using notmuch mail"
__url__ = "https://github.com/pazz/alot"
__license__ = "Licensed under the GNU GPL v3+."
diff --git a/alot/buffer.py b/alot/buffer.py
index 553648b4..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)
diff --git a/alot/command.py b/alot/command.py
index 8deea288..1c58e370 100644
--- a/alot/command.py
+++ b/alot/command.py
@@ -17,11 +17,13 @@ along with notmuch. If not, see <http://www.gnu.org/licenses/>.
Copyright (C) 2011 Patrick Totzke <patricktotzke@gmail.com>
"""
import os
+import re
import code
import glob
import logging
import threading
import subprocess
+import shlex
import email
import tempfile
import mimetypes
@@ -490,10 +492,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()
@@ -501,7 +500,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:
@@ -515,10 +514,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
@@ -541,7 +539,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:]
@@ -601,14 +599,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:
@@ -655,6 +653,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)
@@ -756,17 +805,29 @@ class EnvelopeEditCommand(Command):
self.mail = ui.current_buffer.get_email()
def openEnvelopeFromTmpfile():
+ # This parses the input from the tempfile.
+ # we do this ourselves here because we want to be able to
+ # just type utf-8 encoded stuff into the tempfile and let alot worry
+ # about encodings.
+
+ # get input
f = open(tf.name)
enc = settings.config.get('general', 'editor_writes_encoding')
editor_input = f.read().decode(enc)
-
- #split editor out
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():
@@ -790,7 +851,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():
@@ -871,26 +932,25 @@ class EnvelopeSendCommand(Command):
class EnvelopeAttachCommand(Command):
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:
- ui.notify('no matches, abort')
- return
- logging.info(self.files)
-
msg = self.mail
if not msg:
msg = ui.current_buffer.get_email()
- for path in self.files:
+
+ if self.path:
+ files = filter(os.path.isfile,
+ glob.glob(os.path.expanduser(self.path)))
+ if not files:
+ ui.notify('no matches, abort')
+ return
+ else:
+ ui.notify('no files specified, abort')
+
+ logging.info("attaching: %s" % files)
+ for path in files:
helper.attach(path, msg)
if not self.mail: # set the envelope msg iff we got it from there
@@ -933,6 +993,7 @@ COMMANDS = {
'groupreply': (ReplyCommand, {'groupreply': True}),
'forward': (ForwardCommand, {}),
'fold': (FoldMessagesCommand, {'visible': False}),
+ 'print': (PrintCommand, {}),
'unfold': (FoldMessagesCommand, {'visible': True}),
'select': (ThreadSelectCommand, {}),
'save': (SaveAttachmentCommand, {}),
@@ -1048,6 +1109,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/message.py b/alot/message.py
index f4cc2f39..f3128eba 100644
--- a/alot/message.py
+++ b/alot/message.py
@@ -184,7 +184,7 @@ def extract_body(mail):
raw_payload = part.get_payload(decode=True)
if part.get_content_maintype() == 'text':
if enc:
- raw_payload = unicode(raw_payload, enc)
+ raw_payload = unicode(raw_payload, enc, errors='replace')
else:
raw_payload = unicode(raw_payload, errors='replace')
if ctype == 'text/plain':
diff --git a/alot/settings.py b/alot/settings.py
index 55b543a3..ef3babff 100644
--- a/alot/settings.py
+++ b/alot/settings.py
@@ -41,6 +41,7 @@ DEFAULTS = {
'hooksfile': '~/.alot.py',
'bug_on_exit': 'False',
'timestamp_format': '',
+ 'print_cmd': 'muttprint',
},
'16c-theme': {
'bufferlist_focus_bg': 'dark gray',
@@ -242,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 ',
diff --git a/alot/ui.py b/alot/ui.py
index e8b7c5fd..74d5aa51 100644
--- a/alot/ui.py
+++ b/alot/ui.py
@@ -77,7 +77,7 @@ class UI:
def keypress(self, key):
self.logger.debug('unhandeled input: %s' % key)
- def prompt(self, prefix='>', text=u'', completer=None, tab=0 ,history=[]):
+ def prompt(self, prefix='>', text=u'', completer=None, tab=0, history=[]):
"""prompt for text input
:param prefix: text to print before the input field
@@ -131,7 +131,7 @@ class UI:
if history:
if historypos == None:
history.append(editpart.get_edit_text())
- historypos = len(history)-1
+ historypos = len(history) - 1
if key == 'cursor up':
historypos = (historypos - 1) % len(history)
else:
@@ -145,6 +145,11 @@ class UI:
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=':',
@@ -155,12 +160,21 @@ class UI:
history=self.commandprompthistory,
)
if cmdline:
- self.commandprompthistory.append(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 690c6260..2b811146 100644
--- a/alot/widgets.py
+++ b/alot/widgets.py
@@ -59,7 +59,7 @@ 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')
@@ -138,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
@@ -442,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))