Permalink
Browse files

Merge branch '0.3-feature-pyme' into staging

Conflicts:
	alot/settings/__init__.py
	alot/settings/checks.py
	docs/source/generate_configs.py
  • Loading branch information...
2 parents 138f13c + 56b0b8a commit 08c2ee1d88e69aa5df27b74d1cebd3c0c8be68f6 @pazz committed May 17, 2012
View
@@ -7,6 +7,7 @@
import shlex
from alot.helper import call_cmd_async
+import alot.crypto as crypto
class SendingMailFailed(RuntimeError):
@@ -47,14 +48,15 @@ def __init__(self, address=None, aliases=None, realname=None,
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
self.gpg_key = gpg_key
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 @@ def errb(failure):
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
View
@@ -118,6 +118,14 @@ def rebuild(self):
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')
View
@@ -1,3 +1,4 @@
+import argparse
import os
import re
import glob
@@ -8,8 +9,10 @@
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 @@ def apply(self, ui):
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()
View
@@ -17,12 +17,14 @@
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 @@ def apply(self, ui):
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:
View
@@ -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
Oops, something went wrong.

0 comments on commit 08c2ee1

Please sign in to comment.