diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2012-05-17 15:47:23 +0100 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2012-05-17 15:47:23 +0100 |
commit | 08c2ee1d88e69aa5df27b74d1cebd3c0c8be68f6 (patch) | |
tree | 2dde7d7db9146918242f2c289c64e587706b3f09 | |
parent | 138f13c682f6a63da67cced1f0c267b5f6f349b8 (diff) | |
parent | 56b0b8a0f310403d4664649ab9b69d3cd765dbe5 (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.py | 6 | ||||
-rw-r--r-- | alot/buffers.py | 8 | ||||
-rw-r--r-- | alot/commands/envelope.py | 66 | ||||
-rw-r--r-- | alot/commands/globals.py | 14 | ||||
-rw-r--r-- | alot/crypto.py | 140 | ||||
-rw-r--r-- | alot/db/envelope.py | 81 | ||||
-rw-r--r-- | alot/db/errors.py | 1 | ||||
-rw-r--r-- | alot/defaults/alot.rc.spec | 6 | ||||
-rw-r--r-- | alot/defaults/config.stub | 1 | ||||
-rw-r--r-- | alot/errors.py | 3 | ||||
-rw-r--r-- | alot/settings/__init__.py | 7 | ||||
-rw-r--r-- | alot/settings/checks.py | 15 | ||||
-rw-r--r-- | docs/source/api/crypto.rst | 7 | ||||
-rw-r--r-- | docs/source/api/index.rst | 1 | ||||
-rw-r--r-- | docs/source/configuration/accounts_table.rst | 65 | ||||
-rw-r--r-- | docs/source/configuration/alotrc_table.rst | 112 | ||||
-rw-r--r-- | docs/source/crypto/index.rst | 42 | ||||
-rwxr-xr-x | docs/source/generate_configs.py | 5 | ||||
-rw-r--r-- | docs/source/index.rst | 1 | ||||
-rw-r--r-- | docs/source/installation.rst | 3 | ||||
-rwxr-xr-x | setup.py | 3 |
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 @@ -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', ) |