summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2012-05-17 15:47:23 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2012-05-17 15:47:23 +0100
commit08c2ee1d88e69aa5df27b74d1cebd3c0c8be68f6 (patch)
tree2dde7d7db9146918242f2c289c64e587706b3f09
parent138f13c682f6a63da67cced1f0c267b5f6f349b8 (diff)
parent56b0b8a0f310403d4664649ab9b69d3cd765dbe5 (diff)
Merge branch '0.3-feature-pyme' into staging
Conflicts: alot/settings/__init__.py alot/settings/checks.py docs/source/generate_configs.py
-rw-r--r--alot/account.py6
-rw-r--r--alot/buffers.py8
-rw-r--r--alot/commands/envelope.py66
-rw-r--r--alot/commands/globals.py14
-rw-r--r--alot/crypto.py140
-rw-r--r--alot/db/envelope.py81
-rw-r--r--alot/db/errors.py1
-rw-r--r--alot/defaults/alot.rc.spec6
-rw-r--r--alot/defaults/config.stub1
-rw-r--r--alot/errors.py3
-rw-r--r--alot/settings/__init__.py7
-rw-r--r--alot/settings/checks.py15
-rw-r--r--docs/source/api/crypto.rst7
-rw-r--r--docs/source/api/index.rst1
-rw-r--r--docs/source/configuration/accounts_table.rst65
-rw-r--r--docs/source/configuration/alotrc_table.rst112
-rw-r--r--docs/source/crypto/index.rst42
-rwxr-xr-xdocs/source/generate_configs.py5
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/installation.rst3
-rwxr-xr-xsetup.py3
21 files changed, 487 insertions, 100 deletions
diff --git a/alot/account.py b/alot/account.py
index d7273707..b65bf3cb 100644
--- a/alot/account.py
+++ b/alot/account.py
@@ -7,6 +7,7 @@ import glob
import shlex
from alot.helper import call_cmd_async
+import alot.crypto as crypto
class SendingMailFailed(RuntimeError):
@@ -47,7 +48,7 @@ class Account(object):
gpg_key=None, signature=None, signature_filename=None,
signature_as_attachment=False, sent_box=None,
sent_tags=['sent'], draft_box=None, draft_tags=['draft'],
- abook=None, **rest):
+ abook=None, sign_by_default=False, **rest):
self.address = address
self.aliases = aliases
self.realname = realname
@@ -55,6 +56,7 @@ class Account(object):
self.signature = signature
self.signature_filename = signature_filename
self.signature_as_attachment = signature_as_attachment
+ self.sign_by_default = sign_by_default
self.sent_box = sent_box
self.sent_tags = sent_tags
self.draft_box = draft_box
@@ -163,7 +165,7 @@ class SendmailAccount(Account):
logging.error(failure.value.stderr)
raise SendingMailFailed(errmsg)
- d = call_cmd_async(cmdlist, stdin=mail.as_string())
+ d = call_cmd_async(cmdlist, stdin=crypto.email_as_string(mail))
d.addCallback(cb)
d.addErrback(errb)
return d
diff --git a/alot/buffers.py b/alot/buffers.py
index 1e20d85a..bb1cd156 100644
--- a/alot/buffers.py
+++ b/alot/buffers.py
@@ -118,6 +118,14 @@ class EnvelopeBuffer(Buffer):
for value in vlist:
lines.append((k, value))
+ # sign/encrypt lines
+ if self.envelope.sign:
+ description = 'Yes'
+ sign_key = self.envelope.sign_key
+ if sign_key is not None and len(sign_key.subkeys) > 0:
+ description += ', with key ' + sign_key.subkeys[0].keyid
+ lines.append(('GPG sign', description))
+
# add header list widget iff header values exists
if lines:
key_att = settings.get_theming_attribute('envelope', 'header_key')
diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py
index c906bef4..70547075 100644
--- a/alot/commands/envelope.py
+++ b/alot/commands/envelope.py
@@ -1,3 +1,4 @@
+import argparse
import os
import re
import glob
@@ -8,8 +9,10 @@ from twisted.internet.defer import inlineCallbacks
import datetime
from alot.account import SendingMailFailed
+from alot.errors import GPGProblem
from alot import buffers
from alot import commands
+from alot import crypto
from alot.commands import Command, registerCommand
from alot.commands import globals
from alot.helper import string_decode
@@ -130,9 +133,20 @@ class SendCommand(Command):
else:
account = settings.get_accounts()[0]
+ clearme = ui.notify(u'constructing mail (GPG, attachments)\u2026',
+ timeout=-1)
+
+ try:
+ mail = envelope.construct_mail()
+ except GPGProblem, e:
+ ui.clear_notify([clearme])
+ ui.notify(e.message, priority='error')
+ return
+
+ ui.clear_notify([clearme])
+
# send
clearme = ui.notify('sending..', timeout=-1)
- mail = envelope.construct_mail()
def afterwards(returnvalue):
logging.debug('mail sent successfully')
@@ -317,3 +331,53 @@ class ToggleHeaderCommand(Command):
"""toggle display of all headers"""
def apply(self, ui):
ui.current_buffer.toggle_all_headers()
+
+
+@registerCommand(MODE, 'sign', forced={'action': 'sign'}, arguments=[
+ (['keyid'], {'nargs':argparse.REMAINDER, 'help':'which key id to use'})],
+ help='mark mail to be signed before sending')
+@registerCommand(MODE, 'unsign', forced={'action': 'unsign'},
+ help='mark mail not to be signed before sending')
+@registerCommand(MODE, 'togglesign', forced={'action': 'toggle'}, arguments=[
+ (['keyid'], {'nargs':argparse.REMAINDER, 'help':'which key id to use'})],
+ help='toggle sign status')
+class SignCommand(Command):
+ """toggle signing this email"""
+ def __init__(self, action=None, keyid=None, **kwargs):
+ """
+ :param action: whether to sign/unsign/toggle
+ :type action: str
+ :param keyid: which key id to use
+ :type keyid: str
+ """
+ self.action = action
+ self.keyid = keyid
+ Command.__init__(self, **kwargs)
+
+ def apply(self, ui):
+ sign = None
+ key = None
+ envelope = ui.current_buffer.envelope
+ # sign status
+ if self.action == 'sign':
+ sign = True
+ elif self.action == 'unsign':
+ sign = False
+ elif self.action == 'toggle':
+ sign = not envelope.sign
+ envelope.sign = sign
+
+ # try to find key if hint given as parameter
+ if sign:
+ if len(self.keyid) > 0:
+ keyid = str(' '.join(self.keyid))
+ try:
+ key = crypto.CryptoContext().get_key(keyid)
+ except GPGProblem, e:
+ envelope.sign = False
+ ui.notify(e.message, priority='error')
+ return
+ envelope.sign_key = key
+
+ # reload buffer
+ ui.current_buffer.rebuild()
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
index b0956ddb..65609996 100644
--- a/alot/commands/globals.py
+++ b/alot/commands/globals.py
@@ -17,12 +17,14 @@ from alot.commands import commandfactory
from alot import buffers
from alot import widgets
from alot import helper
+from alot import crypto
from alot.db.errors import DatabaseLockedError
from alot.completion import ContactsCompleter
from alot.completion import AccountCompleter
from alot.db.envelope import Envelope
from alot import commands
from alot.settings import settings
+from alot.errors import GPGProblem
MODE = 'global'
@@ -596,12 +598,16 @@ class ComposeCommand(Command):
select='yes', cancel='no')) == 'no':
return
+ # Figure out whether we should GPG sign messages by default
+ # and look up key if so
+ sender = self.envelope.get('From')
+ name, addr = email.Utils.parseaddr(sender)
+ account = settings.get_account_by_address(addr)
+ self.envelope.sign = account.sign_by_default
+ self.envelope.sign_key = account.gpg_key
+
# get missing To header
if 'To' not in self.envelope.headers:
- sender = self.envelope.get('From')
- name, addr = email.Utils.parseaddr(sender)
- account = settings.get_account_by_address(addr)
-
allbooks = not settings.get('complete_matching_abook_only')
logging.debug(allbooks)
if account is not None:
diff --git a/alot/crypto.py b/alot/crypto.py
new file mode 100644
index 00000000..4e38a472
--- /dev/null
+++ b/alot/crypto.py
@@ -0,0 +1,140 @@
+# vim:ts=4:sw=4:expandtab
+import re
+
+from email.generator import Generator
+from cStringIO import StringIO
+import pyme.core
+import pyme.constants
+from alot.errors import GPGProblem
+
+
+def email_as_string(mail):
+ """
+ Converts the given message to a string, without mangling "From" lines
+ (like as_string() does).
+
+ :param mail: email to convert to string
+ :rtype: str
+ """
+ fp = StringIO()
+ g = Generator(fp, mangle_from_=False)
+ g.flatten(mail)
+ return RFC3156_canonicalize(fp.getvalue())
+
+
+def _engine_file_name_by_protocol(engines, protocol):
+ for engine in engines:
+ if engine.protocol == protocol:
+ return engine.file_name
+ return None
+
+
+def RFC3156_micalg_from_result(result):
+ """
+ Converts a GPGME hash algorithm name to one conforming to RFC3156.
+
+ GPGME returns hash algorithm names such as "SHA256", but RFC3156 says that
+ programs need to use names such as "pgp-sha256" instead.
+
+ :param result: GPGME op_sign_result() return value
+ :rtype: str
+ """
+ # hash_algo will be something like SHA256, but we need pgp-sha256.
+ hash_algo = pyme.core.hash_algo_name(result.signatures[0].hash_algo)
+ return 'pgp-' + hash_algo.lower()
+
+
+def RFC3156_canonicalize(text):
+ """
+ Canonicalizes plain text (MIME-encoded usually) according to RFC3156.
+
+ This function works as follows (in that order):
+
+ 1. Convert all line endings to \\\\r\\\\n (DOS line endings).
+ 2. Ensure the text ends with a newline (\\\\r\\\\n).
+ 3. Encode all occurences of "From " at the beginning of a line
+ to "From=20" in order to prevent other mail programs to replace
+ this with "> From" (to avoid MBox conflicts) and thus invalidate
+ the signature.
+
+ :param text: text to canonicalize (already encoded as quoted-printable)
+ :rtype: str
+ """
+ text = re.sub("\r?\n", "\r\n", text)
+ if not text.endswith("\r\n"):
+ text += "\r\n"
+ text = re.sub("^From ", "From=20", text, flags=re.MULTILINE)
+ return text
+
+
+class CryptoContext(pyme.core.Context):
+ """
+ This is a wrapper around pyme.core.Context which simplifies the pyme API.
+ """
+ def __init__(self):
+ pyme.core.Context.__init__(self)
+ gpg_path = _engine_file_name_by_protocol(pyme.core.get_engine_info(),
+ pyme.constants.PROTOCOL_OpenPGP)
+ if not gpg_path:
+ # TODO: proper exception
+ raise "no GPG engine found"
+
+ self.set_engine_info(pyme.constants.PROTOCOL_OpenPGP, gpg_path)
+ self.set_armor(1)
+
+ def get_key(self, keyid):
+ """
+ Gets a key from the keyring by filtering for the specified keyid, but
+ only if the given keyid is specific enough (if it matches multiple
+ keys, an exception will be thrown).
+ The same happens if no key is found for the given hint.
+
+ :param keyid: filter term for the keyring (usually a key ID)
+ :type keyid: bytestring
+ :rtype: pyme.pygpgme._gpgme_key
+ :raises: GPGProblem
+ """
+ result = self.op_keylist_start(str(keyid), 0)
+ key = self.op_keylist_next()
+ if self.op_keylist_next() is not None:
+ raise GPGProblem(("More than one key found matching this filter."
+ " Please be more specific (use a key ID like 4AC8EE1D)."))
+ self.op_keylist_end()
+ if key == None:
+ raise GPGProblem('No key could be found for hint "%s"' % keyid)
+ return key
+
+ def detached_signature_for(self, plaintext_str, key=None):
+ """
+ Signs the given plaintext string and returns the detached signature.
+
+ A detached signature in GPG speak is a separate blob of data containing
+ a signature for the specified plaintext.
+
+ .. note:: You should use #set_passphrase_cb before calling this method
+ if gpg-agent is not running.
+ ::
+
+ context = crypto.CryptoContext()
+ def gpg_passphrase_cb(hint, desc, prev_bad):
+ return raw_input("Passphrase for key " + hint + ":")
+ context.set_passphrase_cb(gpg_passphrase_cb)
+ result, signature = context.detached_signature_for('Hello World')
+ if result is None:
+ return
+
+ :param plaintext_str: text to sign
+ :param key: gpgme_key_t object representing the key to use
+ :rtype: tuple of pyme.pygpgme._gpgme_op_sign_result and str
+ """
+ if key is not None:
+ self.signers_clear()
+ self.signers_add(key)
+ plaintext_data = pyme.core.Data(plaintext_str)
+ signature_data = pyme.core.Data()
+ self.op_sign(plaintext_data, signature_data,
+ pyme.pygpgme.GPGME_SIG_MODE_DETACH)
+ result = self.op_sign_result()
+ signature_data.seek(0, 0)
+ signature = signature_data.read()
+ return result, signature
diff --git a/alot/db/envelope.py b/alot/db/envelope.py
index f84d305a..14859d9f 100644
--- a/alot/db/envelope.py
+++ b/alot/db/envelope.py
@@ -1,15 +1,23 @@
+# vim:ts=4:sw=4:expandtab
import os
import email
import re
import email.charset as charset
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
+from email.encoders import encode_7or8bit
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
+from email.mime.application import MIMEApplication
+import pyme.core
+import pyme.constants
+import pyme.errors
from alot import __version__
import logging
import alot.helper as helper
+import alot.crypto as crypto
from alot.settings import settings
+from alot.errors import GPGProblem
from attachment import Attachment
from utils import encode_header
@@ -18,7 +26,7 @@ from utils import encode_header
class Envelope(object):
"""a message that is not yet sent and still editable"""
def __init__(self, template=None, bodytext=u'', headers={}, attachments=[],
- sign=False, encrypt=False):
+ sign=False, sign_key=None, encrypt=False):
"""
:param template: if not None, the envelope will be initialised by
:meth:`parsing <parse_template>` this string before
@@ -44,6 +52,7 @@ class Envelope(object):
self.headers.update(headers)
self.attachments = list(attachments)
self.sign = sign
+ self.sign_key = sign_key
self.encrypt = encrypt
self.sent_time = None
self.modified_since_sent = False
@@ -133,15 +142,65 @@ class Envelope(object):
compiles the information contained in this envelope into a
:class:`email.Message`.
"""
- # build body text part
- textpart = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
+ # Build body text part. To properly sign/encrypt messages later on, we
+ # convert the text to its canonical format (as per RFC 2015).
+ canonical_format = self.body.encode('utf-8')
+ canonical_format = canonical_format.replace('\\t', ' '*4)
+ textpart = MIMEText(canonical_format, 'plain', 'utf-8')
# wrap it in a multipart container if necessary
- if self.attachments or self.sign or self.encrypt:
- msg = MIMEMultipart()
- msg.attach(textpart)
+ if self.attachments:
+ inner_msg = MIMEMultipart()
+ inner_msg.attach(textpart)
+ # add attachments
+ for a in self.attachments:
+ inner_msg.attach(a.get_mime_representation())
else:
- msg = textpart
+ inner_msg = textpart
+
+ if self.sign:
+ context = crypto.CryptoContext()
+
+ plaintext = crypto.email_as_string(inner_msg)
+ logging.info('signing plaintext: ' + plaintext)
+
+ try:
+ result, signature_str = context.detached_signature_for(
+ plaintext, self.sign_key)
+ if len(result.signatures) != 1:
+ raise GPGProblem(("Could not sign message "
+ "(GPGME did not return a signature)"))
+ except pyme.errors.GPGMEError as e:
+ # 11 == GPG_ERR_BAD_PASSPHRASE
+ if e.getcode() == 11:
+ # If GPG_AGENT_INFO is unset or empty, the user just does
+ # not have gpg-agent running (properly).
+ if os.environ.get('GPG_AGENT_INFO', '').strip() == '':
+ raise GPGProblem(("Bad passphrase and "
+ "GPG_AGENT_INFO not set. Please setup "
+ "gpg-agent."))
+ else:
+ raise GPGProblem(("Bad passphrase. Is "
+ "gpg-agent running?"))
+ raise GPGProblem(str(e))
+
+ micalg = crypto.RFC3156_micalg_from_result(result)
+ outer_msg = MIMEMultipart('signed', micalg=micalg,
+ protocol='application/pgp-signature')
+
+ # wrap signature in MIMEcontainter
+ signature_mime = MIMEApplication(_data=signature_str,
+ _subtype='pgp-signature; name="signature.asc"',
+ _encoder=encode_7or8bit)
+ signature_mime['Content-Description'] = 'signature'
+ signature_mime.set_charset('us-ascii')
+
+ # add signed message and signature to outer message
+ outer_msg.attach(inner_msg)
+ outer_msg.attach(signature_mime)
+ outer_msg['Content-Disposition'] = 'inline'
+ else:
+ outer_msg = inner_msg
headers = self.headers.copy()
# add Message-ID
@@ -159,13 +218,9 @@ class Envelope(object):
# copy headers from envelope to mail
for k, vlist in headers.items():
for v in vlist:
- msg[k] = encode_header(k, v)
-
- # add attachments
- for a in self.attachments:
- msg.attach(a.get_mime_representation())
+ outer_msg[k] = encode_header(k, v)
- return msg
+ return outer_msg
def parse_template(self, tmp, reset=False, only_body=False):
"""parses a template or user edited string to fills this envelope.
diff --git a/alot/db/errors.py b/alot/db/errors.py
index 240e769b..eb1a56d7 100644
--- a/alot/db/errors.py
+++ b/alot/db/errors.py
@@ -15,3 +15,4 @@ class DatabaseLockedError(DatabaseError):
class NonexistantObjectError(DatabaseError):
"""requested thread or message does not exist in the index"""
pass
+
diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec
index 7d6715d9..dc1c3c1f 100644
--- a/alot/defaults/alot.rc.spec
+++ b/alot/defaults/alot.rc.spec
@@ -181,6 +181,12 @@ prompt_suffix = string(default=':')
# :ref:`signature_as_attachment <signature-as-attachment>` is set to True
signature_filename = string(default=None)
+ # Outgoing messages will be GPG signed by default if this is set to True.
+ sign_by_default = boolean(default=False)
+
+ # The GPG key ID you want to use with this account. If unset, alot will
+ # use your default key.
+ gpg_key = gpg_key_hint(default=None)
# address book for this account
[[[abook]]]
diff --git a/alot/defaults/config.stub b/alot/defaults/config.stub
index 9d6f3661..94f78c4d 100644
--- a/alot/defaults/config.stub
+++ b/alot/defaults/config.stub
@@ -67,6 +67,7 @@
t = 'refine To'
b = 'refine Bcc'
c = 'refine Cc'
+ S = togglesign
select = edit
H = toggleheaders
diff --git a/alot/errors.py b/alot/errors.py
new file mode 100644
index 00000000..29283a45
--- /dev/null
+++ b/alot/errors.py
@@ -0,0 +1,3 @@
+class GPGProblem(Exception):
+ """GPG Error"""
+ pass
diff --git a/alot/settings/__init__.py b/alot/settings/__init__.py
index e0cd24f5..b33dca62 100644
--- a/alot/settings/__init__.py
+++ b/alot/settings/__init__.py
@@ -15,7 +15,9 @@ from alot.helper import pretty_datetime, string_decode
from errors import ConfigError
from utils import read_config
-from checks import mail_container, force_list
+from checks import force_list
+from checks import mail_container
+from checks import gpg_key
from theme import Theme
@@ -56,7 +58,8 @@ class SettingsManager(object):
spec = os.path.join(DEFAULTSPATH, 'alot.rc.spec')
newconfig = read_config(path, spec,
checks={'mail_container': mail_container,
- 'force_list': force_list})
+ 'force_list': force_list,
+ 'gpg_key_hint': gpg_key})
self._config.merge(newconfig)
hooks_path = os.path.expanduser(self._config.get('hooksfile'))
diff --git a/alot/settings/checks.py b/alot/settings/checks.py
index 4b56a284..353842de 100644
--- a/alot/settings/checks.py
+++ b/alot/settings/checks.py
@@ -3,6 +3,10 @@ import re
from urlparse import urlparse
from validate import VdtTypeError
from validate import is_list
+from validate import ValidateError
+
+from alot import crypto
+from alot.errors import GPGProblem
def mail_container(value):
@@ -53,3 +57,14 @@ def force_list(value, min=None, max=None):
if rlist == ['']:
rlist = []
return rlist
+
+
+def gpg_key(value):
+ """
+ test if value points to a known gpg key
+ and return that key as :class:`pyme.pygpgme._gpgme_key`.
+ """
+ try:
+ return crypto.CryptoContext().get_key(value)
+ except GPGProblem, e:
+ raise ValidateError(e.message)
diff --git a/docs/source/api/crypto.rst b/docs/source/api/crypto.rst
new file mode 100644
index 00000000..f162455e
--- /dev/null
+++ b/docs/source/api/crypto.rst
@@ -0,0 +1,7 @@
+Crypto
+======
+
+.. module:: alot.crypto
+
+.. automodule:: alot.crypto
+ :members:
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
index 69be4f8a..9c69e021 100644
--- a/docs/source/api/index.rst
+++ b/docs/source/api/index.rst
@@ -12,3 +12,4 @@ API and Development
settings
utils
commands
+ crypto
diff --git a/docs/source/configuration/accounts_table.rst b/docs/source/configuration/accounts_table.rst
index 3b92099a..7fea81b6 100644
--- a/docs/source/configuration/accounts_table.rst
+++ b/docs/source/configuration/accounts_table.rst
@@ -9,7 +9,7 @@
.. describe:: address
- your main email address
+ your main email address
:type: string
@@ -17,7 +17,7 @@
.. describe:: realname
- used to format the (proposed) From-header in outgoing mails
+ used to format the (proposed) From-header in outgoing mails
:type: string
@@ -25,17 +25,17 @@
.. describe:: aliases
- used to clear your addresses/ match account when formatting replies
+ used to clear your addresses/ match account when formatting replies
- :type: string_list
- :default: `,`
+ :type: string list
+ :default: ,
.. _sendmail-command:
.. describe:: sendmail_command
- sendmail command. This is the shell command used to send out mails via the sendmail protocol
+ sendmail command. This is the shell command used to send out mails via the sendmail protocol
:type: string
:default: `sendmail -t`
@@ -45,8 +45,11 @@
.. describe:: sent_box
- where to store outgoing mails, e.g. `maildir:///home/you/mail//Sent`
- You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL.
+ where to store outgoing mails, e.g. `maildir:///home/you/mail/Sent`.
+ You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL.
+
+ .. note:: If you want to add outgoing mails automatically to the notmuch index
+ you must use maildir in a path within your notmuch database path.
:type: mail_container
:default: None
@@ -56,7 +59,12 @@
.. describe:: draft_box
- where to store draft mails, see :ref:`sent_box <sent-box>` for the format
+ where to store draft mails, e.g. `maildir:///home/you/mail/Drafts`.
+ You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL.
+
+ .. note:: You will most likely want drafts indexed by notmuch to be able to
+ later access them within alot. This currently only works for
+ maildir containers in a path below your notmuch database path.
:type: mail_container
:default: None
@@ -66,18 +74,18 @@
.. describe:: sent_tags
- list of tags to automatically add to outgoing messages
+ list of tags to automatically add to outgoing messages
- :type: string_list
- :default: `sent,`
+ :type: string list
+ :default: sent,
.. _signature:
.. describe:: signature
- path to signature file that gets attached to all outgoing mails from this account, optionally
- renamed to ref:`signature_filename <signature-filename>`.
+ path to signature file that gets attached to all outgoing mails from this account, optionally
+ renamed to ref:`signature_filename <signature-filename>`.
:type: string
:default: None
@@ -87,8 +95,8 @@
.. describe:: signature_as_attachment
- attach signature file if set to True, append its content (mimetype text)
- to the body text if set to False.
+ attach signature file if set to True, append its content (mimetype text)
+ to the body text if set to False.
:type: boolean
:default: False
@@ -98,8 +106,29 @@
.. describe:: signature_filename
- signature file's name as it appears in outgoing mails if
- :ref:`signature_as_attachment <signature-as-attachment>` is set to True
+ signature file's name as it appears in outgoing mails if
+ :ref:`signature_as_attachment <signature-as-attachment>` is set to True
+
+ :type: string
+ :default: None
+
+
+.. _sign-by-default:
+
+.. describe:: sign_by_default
+
+ Outgoing messages will be GPG signed by default if this is set to True.
+
+ :type: boolean
+ :default: False
+
+
+.. _gpg-key:
+
+.. describe:: gpg_key
+
+ The GPG key ID you want to use with this account. If unset, alot will
+ use your default key.
:type: string
:default: None
diff --git a/docs/source/configuration/alotrc_table.rst b/docs/source/configuration/alotrc_table.rst
index 85fecef9..5940e97e 100644
--- a/docs/source/configuration/alotrc_table.rst
+++ b/docs/source/configuration/alotrc_table.rst
@@ -18,7 +18,7 @@
.. describe:: authors_maxlength
- maximal length of authors string in search mode before it gets truncated
+ maximal length of authors string in search mode before it gets truncated
:type: integer
:default: 30
@@ -28,7 +28,7 @@
.. describe:: bufferclose_focus_offset
- offset of next focused buffer if the current one gets closed
+ offset of next focused buffer if the current one gets closed
:type: integer
:default: -1
@@ -38,7 +38,7 @@
.. describe:: bug_on_exit
- confirm exit
+ confirm exit
:type: boolean
:default: False
@@ -48,7 +48,7 @@
.. describe:: colourmode
- number of colours to use
+ number of colours to use
:type: option, one of ['1', '16', '256']
:default: 256
@@ -58,9 +58,9 @@
.. describe:: complete_matching_abook_only
- in case more than one account has an address book:
- Set this to True to make tab completion for recipients during compose only
- look in the abook of the account matching the sender address
+ in case more than one account has an address book:
+ Set this to True to make tab completion for recipients during compose only
+ look in the abook of the account matching the sender address
:type: boolean
:default: False
@@ -70,7 +70,7 @@
.. describe:: display_content_in_threadline
- fill threadline with message content
+ fill threadline with message content
:type: boolean
:default: False
@@ -80,40 +80,40 @@
.. describe:: displayed_headers
- headers that get displayed by default
+ headers that get displayed by default
- :type: string_list
- :default: `From, To, Cc, Bcc, Subject`
+ :type: string list
+ :default: From, To, Cc, Bcc, Subject
.. _edit-headers-blacklist:
.. describe:: edit_headers_blacklist
- see :ref:`edit_headers_whitelist <edit-headers-whitelist>`
+ see :ref:`edit_headers_whitelist <edit-headers-whitelist>`
- :type: string_list
- :default: `Content-Type, MIME-Version, References, In-Reply-To`
+ :type: string list
+ :default: Content-Type, MIME-Version, References, In-Reply-To
.. _edit-headers-whitelist:
.. describe:: edit_headers_whitelist
- Which header fields should be editable in your editor
- used are those that match the whitelist and don't match the blacklist.
- in both cases '*' may be used to indicate all fields.
+ Which header fields should be editable in your editor
+ used are those that match the whitelist and don't match the blacklist.
+ in both cases '*' may be used to indicate all fields.
- :type: string_list
- :default: `*,`
+ :type: string list
+ :default: *,
.. _editor-cmd:
.. describe:: editor_cmd
- editor command
- if unset, alot will first try the :envvar:`EDITOR` env variable, then :file:`/usr/bin/editor`
+ editor command
+ if unset, alot will first try the :envvar:`EDITOR` env variable, then :file:`/usr/bin/editor`
:type: string
:default: None
@@ -123,9 +123,9 @@
.. describe:: editor_in_thread
- call editor in separate thread.
- In case your editor doesn't run in the same window as alot, setting true here
- will make alot non-blocking during edits
+ call editor in separate thread.
+ In case your editor doesn't run in the same window as alot, setting true here
+ will make alot non-blocking during edits
:type: boolean
:default: False
@@ -135,8 +135,8 @@
.. describe:: editor_spawn
- use terminal_command to spawn a new terminal for the editor?
- equivalent to always providing the `--spawn` parameter to compose/edit commands
+ use terminal_command to spawn a new terminal for the editor?
+ equivalent to always providing the `--spawn` parameter to compose/edit commands
:type: boolean
:default: False
@@ -146,7 +146,7 @@
.. describe:: editor_writes_encoding
- file encoding used by your editor
+ file encoding used by your editor
:type: string
:default: `UTF-8`
@@ -156,17 +156,17 @@
.. describe:: envelope_headers_blacklist
- headers that are hidden in envelope buffers by default
+ headers that are hidden in envelope buffers by default
- :type: string_list
- :default: `In-Reply-To, References`
+ :type: string list
+ :default: In-Reply-To, References
.. _flush-retry-timeout:
.. describe:: flush_retry_timeout
- timeout in seconds after a failed attempt to writeout the database is repeated
+ timeout in seconds after a failed attempt to writeout the database is repeated
:type: integer
:default: 5
@@ -176,7 +176,7 @@
.. describe:: hooksfile
- where to look up hooks
+ where to look up hooks
:type: string
:default: `~/.config/alot/hooks.py`
@@ -186,7 +186,7 @@
.. describe:: initial_command
- initial command when none is given as argument:
+ initial command when none is given as argument:
:type: string
:default: `search tag:inbox AND NOT tag:killed`
@@ -196,7 +196,7 @@
.. describe:: notify_timeout
- time in secs to display status messages
+ time in secs to display status messages
:type: integer
:default: 2
@@ -206,10 +206,10 @@
.. describe:: print_cmd
- how to print messages:
- this specifies a shell command used for printing.
- threads/messages are piped to this command as plain text.
- muttprint/a2ps works nicely
+ how to print messages:
+ this specifies a shell command used for printing.
+ threads/messages are piped to this command as plain text.
+ muttprint/a2ps works nicely
:type: string
:default: None
@@ -219,7 +219,7 @@
.. describe:: prompt_suffix
- Suffix of the prompt used when waiting for user input
+ Suffix of the prompt used when waiting for user input
:type: string
:default: `:`
@@ -229,7 +229,7 @@
.. describe:: quit_on_last_bclose
- shut down when the last buffer gets closed
+ shut down when the last buffer gets closed
:type: boolean
:default: False
@@ -239,7 +239,7 @@
.. describe:: search_threads_sort_order
- default sort order of results in a search
+ default sort order of results in a search
:type: option, one of ['oldest_first', 'newest_first', 'message_id', 'unsorted']
:default: newest_first
@@ -249,7 +249,7 @@
.. describe:: show_statusbar
- display status-bar at the bottom of the screen?
+ display status-bar at the bottom of the screen?
:type: boolean
:default: True
@@ -259,7 +259,7 @@
.. describe:: tabwidth
- number of spaces used to replace tab characters
+ number of spaces used to replace tab characters
:type: integer
:default: 8
@@ -269,8 +269,8 @@
.. describe:: template_dir
- templates directory that contains your message templates.
- It will be used if you give `compose --template` a filename without a path prefix.
+ templates directory that contains your message templates.
+ It will be used if you give `compose --template` a filename without a path prefix.
:type: string
:default: `$XDG_CONFIG_HOME/alot/templates`
@@ -280,7 +280,7 @@
.. describe:: terminal_cmd
- set terminal command used for spawning shell commands
+ set terminal command used for spawning shell commands
:type: string
:default: `x-terminal-emulator -e`
@@ -290,7 +290,7 @@
.. describe:: theme
- name of the theme to use
+ name of the theme to use
:type: string
:default: None
@@ -300,7 +300,7 @@
.. describe:: themes_dir
- directory containing theme files
+ directory containing theme files
:type: string
:default: None
@@ -310,8 +310,8 @@
.. describe:: thread_authors_me
- Word to replace own addresses with. Works in combination with
- :ref:`thread_authors_replace_me <thread-authors-replace-me>`
+ Word to replace own addresses with. Works in combination with
+ :ref:`thread_authors_replace_me <thread-authors-replace-me>`
:type: string
:default: `Me`
@@ -321,8 +321,8 @@
.. describe:: thread_authors_replace_me
- Replace own email addresses with "me" in author lists
- Uses own addresses and aliases in all configured accounts.
+ Replace own email addresses with "me" in author lists
+ Uses own addresses and aliases in all configured accounts.
:type: boolean
:default: True
@@ -332,7 +332,7 @@
.. describe:: timestamp_format
- timestamp format in `strftime format syntax <http://docs.python.org/library/datetime.html#strftime-strptime-behavior>`_
+ timestamp format in `strftime format syntax <http://docs.python.org/library/datetime.html#strftime-strptime-behavior>`_
:type: string
:default: None
@@ -342,9 +342,9 @@
.. describe:: user_agent
- value of the User-Agent header used for outgoing mails.
- setting this to the empty string will cause alot to omit the header all together.
- The string '{version}' will be replaced by the version string of the running instance.
+ value of the User-Agent header used for outgoing mails.
+ setting this to the empty string will cause alot to omit the header all together.
+ The string '{version}' will be replaced by the version string of the running instance.
:type: string
:default: `alot/{version}`
diff --git a/docs/source/crypto/index.rst b/docs/source/crypto/index.rst
new file mode 100644
index 00000000..c17d440d
--- /dev/null
+++ b/docs/source/crypto/index.rst
@@ -0,0 +1,42 @@
+.. _cryptography
+
+************
+Cryptography
+************
+
+At the moment alot only supports signing of outgoing mails via PGP/MIME (:rfc:`3156`).
+
+.. note:: To use GPG with alot, you need to have `gpg-agent` running.
+
+ `gpg-agent` will handle passphrase entry in a secure and configurable way, and it will cache your passphrase for some
+ amount of time so you don’t have to enter it over and over again. For details on how to set this up we refer to
+ `gnupg's manual <http://www.gnupg.org/documentation/manuals/gnupg/>`_.
+
+.. rubric:: Signing outgoing emails
+
+You can use the commands `sign`, `unsign` and `togglesign` in envelope mode
+to determine if you want this mail signed and if so, which key to use.
+To specify the key to use you can pass a hint string as argument to
+the `sign` or `togglesign` command. This hint would typically
+be a fingerprint or an email address associated (by gnupg) with a key.
+
+Signing (and hence passwd entry) will be done at most once shortly before
+a mail is sent.
+
+In case no key is specified, alot will leave the selection of a suitable key to gnupg
+so you can influence that by setting the `default-key` option in :file:`~/.gnupg/gpg.conf`
+accordingly.
+
+You can set the default to-sign bit and the key to use for each :ref:`account <account>`
+individually using the options :ref:`sign_by_default <sign-by-default>` and :ref:`gpg_key <gpg-key>`.
+
+
+.. rubric:: Tips
+
+In case you are using alot via SSH, we recommend to use `pinentry-curses`
+instead of the default graphical pinentry. You can do that by setting up your
+:file:`~/.gnupg/gpg-agent.conf` like this::
+
+ pinentry-program /usr/bin/pinentry-curses
+
+
diff --git a/docs/source/generate_configs.py b/docs/source/generate_configs.py
index eea7946b..77403998 100755
--- a/docs/source/generate_configs.py
+++ b/docs/source/generate_configs.py
@@ -31,7 +31,8 @@ def rewrite_entries(config, path, specpath, sec=None, sort=False):
if default is not None:
default = config._quote(default)
- #print etype
+ if etype == 'gpg_key_hint':
+ etype = 'string'
description = '\n.. _%s:\n' % entry.replace('_', '-')
description += '\n.. describe:: %s\n\n' % entry
comments = [sec.inline_comments[entry]] + sec.comments[entry]
@@ -46,7 +47,7 @@ def rewrite_entries(config, path, specpath, sec=None, sort=False):
description += '\n :type: %s\n' % etype
if default != None:
- if etype in ['string', 'string list'] and default != 'None':
+ if etype in ['string', 'string_list', 'gpg_key_hint'] and default != 'None':
description += ' :default: `%s`\n\n' % (default)
else:
description += ' :default: %s\n\n' % (default)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 7b9203e3..cc227b77 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -14,5 +14,6 @@ User Manual
installation
usage/index
configuration/index
+ crypto/index
api/index
faq
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 7f616099..1e6ec533 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -12,10 +12,11 @@ A full list of dependencies is below:
* `twisted <http://twistedmatrix.com/trac/>`_, ≥ `10.2.0`:
* `libnotmuch <http://notmuchmail.org/>`_ and it's python bindings, ≥ `0.12`.
* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.0`
+* `pyme <http://pyme.sourceforge.net/>`_
On debian/ubuntu these are packaged as::
- python-magic python-configobj python-twisted python-notmuch python-urwid
+ python-magic python-configobj python-twisted python-notmuch python-urwid python-pyme
Alot uses `mailcap <http://en.wikipedia.org/wiki/Mailcap>`_ to look up mime-handler for inline
rendering and opening of attachments. For a full description of the maicap protocol consider the
diff --git a/setup.py b/setup.py
index 81207a0d..e5dde751 100755
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,7 @@ setup(name='alot',
'twisted (>=10.2.0)',
'magic',
'configobj (>=4.6.0)',
- 'subprocess (>=2.7)'],
+ 'subprocess (>=2.7)',
+ 'pyme (>=0.8.1)'],
provides='alot',
)