diff --git a/.gitignore b/.gitignore
index 6db145c624ea35345ca1edac8f7dc402a75a14f5..e4f7e20f1ed85ea30415e3cadba3b0ee16acdd83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
 *.pyc
 .coverage
 .tox
+/htmlcov/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be668c18f8578e8595d7559e1f7c8a9359bf3eb6..11712e17fcbd83757c1d6f68020206a6e4292b06 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,4 @@
 image: "ai/test:base"
 
 run_tests:
-  script: "tox -e py27"
-
+  script: "tox"
diff --git a/pgp_mime_lib/__init__.py b/pgp_mime_lib/__init__.py
index b3500250237b49ac368ffbb274f28cd6f724960f..1a7ce329f0061233b181471386b350a6dcd769b2 100644
--- a/pgp_mime_lib/__init__.py
+++ b/pgp_mime_lib/__init__.py
@@ -1,19 +1,20 @@
 # Package to send PGP-encrypted emails to users using ephemeral key
 # storage (so, no keyserver interaction, no keyring management).
+from __future__ import absolute_import
 
 import contextlib
-import gnupg
 import os
 import re
 import shutil
 import tempfile
 
-from cStringIO import StringIO
 from email import encoders
-from email.mime.multipart import MIMEMultipart
 from email.mime.application import MIMEApplication
 
-from pgp_mime_lib import rfc3156
+import gnupg
+
+from ._compat import StringIO
+from . import rfc3156
 
 
 # Digest algorithm to use in signatures.
@@ -33,20 +34,20 @@ class GPG(object):
     public keyring which is deleted afterwards.
 
     """
-    
-    def __init__(self, key_id, public_keyring=None, secret_keyring=None,
+
+    def __init__(self, key_id, public_keyring=None, secret_key_dir=None,
                  passphrase=None):
         if not public_keyring:
             public_keyring = os.path.join(_default_gpghome(), 'pubring.gpg')
-        if not secret_keyring:
-            secret_keyring = os.path.join(_default_gpghome(), 'secring.gpg')
-        
-        for f in (public_keyring, secret_keyring):
+        if not secret_key_dir:
+            secret_key_dir = os.path.join(_default_gpghome(), 'private-keys-v1.d')
+
+        for f in (public_keyring, secret_key_dir):
             if not os.path.exists(f):
                 raise Error('no such file or directory: %s' % f)
 
         self.public_keyring = public_keyring
-        self.secret_keyring = secret_keyring
+        self.secret_key_dir = secret_key_dir
         self.secret_key_id = key_id
         self.passphrase = passphrase
 
@@ -67,7 +68,8 @@ class GPG(object):
             if not public_keyring:
                 public_keyring = os.path.join(tmpdir, 'pubring.gpg')
                 shutil.copy(self.public_keyring, public_keyring)
-                os.chmod(public_keyring, 0600)
+                os.chmod(public_keyring, 0o600)
+
             # Create a gpg.conf with some options. Not all are used
             # and for some of them there are equivalent command-line
             # options available through the gnupg module.
@@ -77,14 +79,18 @@ personal-digest-preferences SHA512 SHA384 SHA256 SHA224
 cert-digest-algo SHA256
 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed
 sig-notation issuer-fpr@notations.openpgp.fifthhorseman.net=%%g
-#throw-keyids
+throw-keyids
 default-key %s\n
-''' % self.secret_key_id)
+''' % self.secret_key_id)  # noqa
+
+            # symlink the private key directory
+            os.symlink(self.secret_key_dir, os.path.join(tmpdir, 'private-keys-v1.d'))
+
             # Create the gnupg.GPG object to talk to gnupg.
-            gpg = gnupg.GPG(binary=_default_gpgbinary(),
-                            homedir=tmpdir,
-                            keyring=public_keyring,
-                            secring=self.secret_keyring)
+            gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
+                            gnupghome=tmpdir,
+                            keyring=public_keyring)
+            # XXX this below might be wrong.
             gpg.encoding = 'utf-8'
             yield gpg
 
@@ -96,15 +102,14 @@ default-key %s\n
         # public key, but we can't do it with the gnupg module.
         fp = _import_one_key(gpg, public_key)
         s = gpg.encrypt(msg_str, fp, passphrase=self.passphrase,
-                        throw_keyids=True, always_trust=True)
+                        always_trust=True, extra_args=["--throw-keyids"])
         if not s:
             raise Error('gpg --encrypt failed (no result)')
         return str(s)
 
     def _sign(self, gpg, msg_str):
         """Sign msg_str and return the detached signature."""
-        s = gpg.sign(msg_str, digest_algo=DIGEST_ALGO,
-                     clearsign=False, detach=True, binary=False,
+        s = gpg.sign(msg_str, clearsign=False, detach=True, binary=False,
                      passphrase=self.passphrase)
         if not s:
             raise Error('gpg --sign failed (no result)')
@@ -133,7 +138,8 @@ default-key %s\n
 
     def _sign_message(self, gpg, msg):
         """Build a multipart/signed MIME message."""
-        container = rfc3156.MultipartSigned('application/pgp-signature', 'pgp-' + DIGEST_ALGO.lower())
+        container = rfc3156.MultipartSigned('application/pgp-signature',
+                                            'pgp-' + DIGEST_ALGO.lower())
 
         # _openpgp_mangle_for_signature modifies msg as a side effect.
         sig_msg = rfc3156.PGPSignature(
@@ -177,7 +183,7 @@ default-key %s\n
         """
         with self._sane_gpg(self.public_keyring) as gpg:
             return self._sign_message(gpg, msg)
-        
+
 
 def _openpgp_mangle_for_signature(msg):
     """Return a message suitable for signing.
@@ -233,10 +239,10 @@ def parse_public_key(public_key):
     with _temp_dir() as tmpdir:
         # Create empty pubrings.
         for p in 'pubring.gpg', 'secring.gpg':
-            with open(os.path.join(tmpdir, p), 'w') as fd:
+            with open(os.path.join(tmpdir, p), 'w'):
                 pass
-        gpg = gnupg.GPG(binary=_default_gpgbinary(),
-                        homedir=tmpdir)
+        gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
+                        gnupghome=tmpdir)
         gpg.encoding = 'utf-8'
         return _import_one_key(gpg, public_key)
 
@@ -251,10 +257,13 @@ def _import_one_key(gpg, public_key):
     # one new key. We also want to report meaningful error
     # messages, so we are just going to bail out on the first bad
     # status returned by gpg.
+    if import_result.count != 1:
+        raise Error('more than one public key found in input')
+
     fp = None
     for res in import_result.results:
-        if res['status'].strip() != 'Entirely new key':
-            raise Error('no valid keys found in input: %s' % res['status'])
+        if "Entirely new key" not in res["text"]:
+            raise Error('no valid keys found in input: %r' % res['text'])
         if not res['fingerprint']:
             continue
         if fp:
diff --git a/pgp_mime_lib/_compat.py b/pgp_mime_lib/_compat.py
new file mode 100644
index 0000000000000000000000000000000000000000..e474cd5a0c19862d7c60ff2c32ed7a5db9b1b85a
--- /dev/null
+++ b/pgp_mime_lib/_compat.py
@@ -0,0 +1,20 @@
+import sys
+import base64
+
+
+PY2 = sys.version_info[0] == 2
+
+
+if not PY2:
+    from io import StringIO
+    string_types = (str,)
+else:
+    from cStringIO import StringIO
+    string_types = (str, unicode)
+
+
+def base64_encodestring(data):
+    if PY2:
+        return base64.encodestring(data)
+
+    return base64.encodebytes(data)
diff --git a/pgp_mime_lib/rfc3156.py b/pgp_mime_lib/rfc3156.py
index 35503c75adf0a18e6eb37c2df42b69ffcf2efe88..a46ab4a94176611cfaddd98b8915c2d20b6524da 100644
--- a/pgp_mime_lib/rfc3156.py
+++ b/pgp_mime_lib/rfc3156.py
@@ -20,9 +20,7 @@
 """
 Implements RFC 3156: MIME Security with OpenPGP.
 """
-
-import base64
-from StringIO import StringIO
+from __future__ import absolute_import
 
 from email.mime.application import MIMEApplication
 from email.mime.multipart import MIMEMultipart
@@ -34,6 +32,8 @@ from email.generator import (
     _make_boundary,
 )
 
+from ._compat import StringIO, string_types, base64_encodestring
+
 
 #
 # A generator that solves http://bugs.python.org/issue14983
@@ -66,7 +66,7 @@ class RFC3156CompliantGenerator(Generator):
         subparts = msg.get_payload()
         if subparts is None:
             subparts = []
-        elif isinstance(subparts, basestring):
+        elif isinstance(subparts, string_types):
             # e.g. a non-strict parse of a message with no starting boundary.
             self._fp.write(subparts)
             return
@@ -120,18 +120,18 @@ class RFC3156CompliantGenerator(Generator):
 # solution, but a bit modified.
 #
 
-def _bencode(s):
+def _bencode(data):
     """
-    Encode C{s} in base64.
+    Encode C{data} in base64.
 
-    :param s: The string to be encoded.
-    :type s: str
+    :param data: The string to be encoded.
+    :type data: bytes
     """
     # We can't quite use base64.encodestring() since it tacks on a "courtesy
     # newline".  Blech!
-    if not s:
-        return s
-    value = base64.encodestring(s)
+    if not data:
+        return data
+    value = base64_encodestring(data)
     return value[:-1]
 
 
diff --git a/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/4516CCD58FA4AAD110C1263FF072A27F0D2F23E6.key b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/4516CCD58FA4AAD110C1263FF072A27F0D2F23E6.key
new file mode 100644
index 0000000000000000000000000000000000000000..ae1a1568fc77edbdffafae9f65cf34410d3c8c1a
Binary files /dev/null and b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/4516CCD58FA4AAD110C1263FF072A27F0D2F23E6.key differ
diff --git a/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/9A858840ABF0ACFBA624D9AB03BF665BF9472335.key b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/9A858840ABF0ACFBA624D9AB03BF665BF9472335.key
new file mode 100644
index 0000000000000000000000000000000000000000..a5e9f14f514eedced646fb4f368a8bc77a845a66
Binary files /dev/null and b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/9A858840ABF0ACFBA624D9AB03BF665BF9472335.key differ
diff --git a/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/BAF156EE72BCD5DC86C631A2D0C879EE103E2B23.key b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/BAF156EE72BCD5DC86C631A2D0C879EE103E2B23.key
new file mode 100644
index 0000000000000000000000000000000000000000..4975c17082c841110905a420c5341acac95eaa02
Binary files /dev/null and b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/BAF156EE72BCD5DC86C631A2D0C879EE103E2B23.key differ
diff --git a/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/F847C7872AF397308E1B62CCA66B103D4BD1DD61.key b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/F847C7872AF397308E1B62CCA66B103D4BD1DD61.key
new file mode 100644
index 0000000000000000000000000000000000000000..053ea087ce94d2b017c56ada0a56cf80d8155f9d
Binary files /dev/null and b/pgp_mime_lib/test/fixtures/gnupghome/private-keys-v1.d/F847C7872AF397308E1B62CCA66B103D4BD1DD61.key differ
diff --git a/pgp_mime_lib/test/fixtures/secret/pubring.gpg b/pgp_mime_lib/test/fixtures/gnupghome/pubring.gpg
similarity index 100%
rename from pgp_mime_lib/test/fixtures/secret/pubring.gpg
rename to pgp_mime_lib/test/fixtures/gnupghome/pubring.gpg
diff --git a/pgp_mime_lib/test/fixtures/secret/pubring.gpg~ b/pgp_mime_lib/test/fixtures/gnupghome/pubring.gpg~
similarity index 100%
rename from pgp_mime_lib/test/fixtures/secret/pubring.gpg~
rename to pgp_mime_lib/test/fixtures/gnupghome/pubring.gpg~
diff --git a/pgp_mime_lib/test/fixtures/secret/random_seed b/pgp_mime_lib/test/fixtures/gnupghome/random_seed
similarity index 100%
rename from pgp_mime_lib/test/fixtures/secret/random_seed
rename to pgp_mime_lib/test/fixtures/gnupghome/random_seed
diff --git a/pgp_mime_lib/test/fixtures/secret/secring.gpg b/pgp_mime_lib/test/fixtures/gnupghome/secring.gpg
similarity index 100%
rename from pgp_mime_lib/test/fixtures/secret/secring.gpg
rename to pgp_mime_lib/test/fixtures/gnupghome/secring.gpg
diff --git a/pgp_mime_lib/test/fixtures/secret/trustdb.gpg b/pgp_mime_lib/test/fixtures/gnupghome/trustdb.gpg
similarity index 100%
rename from pgp_mime_lib/test/fixtures/secret/trustdb.gpg
rename to pgp_mime_lib/test/fixtures/gnupghome/trustdb.gpg
diff --git a/pgp_mime_lib/test/test_pgp_mime_lib.py b/pgp_mime_lib/test/test_pgp_mime_lib.py
index 9da4a3c06d842bb15b25efabeb386e671760d845..1539dfcd308470c4c11843aa37ff0fd6d2b6679e 100644
--- a/pgp_mime_lib/test/test_pgp_mime_lib.py
+++ b/pgp_mime_lib/test/test_pgp_mime_lib.py
@@ -3,7 +3,6 @@ import shutil
 import tempfile
 import unittest
 import smtplib
-import gnupg
 
 from email.mime.image import MIMEImage
 from email.mime.multipart import MIMEMultipart
@@ -11,8 +10,13 @@ from email.mime.text import MIMEText
 
 import pgp_mime_lib
 
+import logging
+logging.getLogger('gnupg').setLevel(logging.DEBUG)
+logging.basicConfig()
+
 
 fixtures_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
+secret_key_dir = os.path.join(fixtures_dir, "gnupghome/private-keys-v1.d")
 
 
 class TestBase(unittest.TestCase):
@@ -22,16 +26,15 @@ class TestBase(unittest.TestCase):
 
         # Load our test secret key.
         self.public_keyring = os.path.join(
-            fixtures_dir, 'secret/pubring.gpg')
-        self.secret_keyring = os.path.join(
-            fixtures_dir, 'secret/secring.gpg')
+            fixtures_dir, 'gnupghome/pubring.gpg')
+        self.secret_key_dir = secret_key_dir
         self.secret_key = '0x56A1E35992BCB5CF'
 
     def tearDown(self):
         shutil.rmtree(self.dir)
 
     def _gpg(self):
-        return pgp_mime_lib.GPG(self.secret_key, self.public_keyring, self.secret_keyring)
+        return pgp_mime_lib.GPG(self.secret_key, self.public_keyring, self.secret_key_dir)
 
 
 class TestSign(TestBase):
@@ -188,7 +191,7 @@ class TestMIME(TestBase):
         m = MIMEText(u'test message')
         m.replace_header('Content-Transfer-Encoding', '8bit')
         self._encrypt(m)
-        
+
     def test_multipart_message_with_image(self):
         msg = MIMEMultipart()
         msg.preamble = 'This is a message with an image attachment'
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000000000000000000000000000000000000..9512d4df52196ee6861151ee7e78247116af5ddb
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,86 @@
+[[package]]
+category = "dev"
+description = "A platform independent file lock."
+name = "filelock"
+optional = false
+python-versions = "*"
+version = "3.0.10"
+
+[[package]]
+category = "dev"
+description = "plugin and hook calling mechanisms for python"
+name = "pluggy"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "0.8.1"
+
+[[package]]
+category = "dev"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+name = "py"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "1.7.0"
+
+[[package]]
+category = "main"
+description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)"
+name = "python-gnupg"
+optional = false
+python-versions = "*"
+version = "0.4.4"
+
+[[package]]
+category = "dev"
+description = "Python 2 and 3 compatibility utilities"
+name = "six"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*"
+version = "1.12.0"
+
+[[package]]
+category = "dev"
+description = "Python Library for Tom's Obvious, Minimal Language"
+name = "toml"
+optional = false
+python-versions = "*"
+version = "0.10.0"
+
+[[package]]
+category = "dev"
+description = "virtualenv-based automation of test activities"
+name = "tox"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "3.7.0"
+
+[package.dependencies]
+filelock = ">=3.0.0,<4"
+pluggy = ">=0.3.0,<1"
+py = ">=1.4.17,<2"
+setuptools = ">=30.0.0"
+six = ">=1.0.0,<2"
+toml = ">=0.9.4"
+virtualenv = ">=1.11.2"
+
+[[package]]
+category = "dev"
+description = "Virtual Python Environment builder"
+name = "virtualenv"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "16.4.0"
+
+[metadata]
+content-hash = "28aa155b6cccb2f93f6e632be14599cf66f63da25a8598a423cab7aae6a6a9eb"
+python-versions = "~2.7 || ^3.5"
+
+[metadata.hashes]
+filelock = ["b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", "d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"]
+pluggy = ["8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"]
+py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"]
+python-gnupg = ["45daf020b370bda13a1429c859fcdff0b766c0576844211446f9266cae97fb0e", "85c231850a0275c9722f06e34b45a22510b83a6a6e88f93b5ae32ba04c95056c"]
+six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
+toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
+tox = ["04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", "25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"]
+virtualenv = ["8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db", "cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..3b4809de2cb6da9a22eb9458521ed6fef325a31b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "pgp_mime_lib"
+version = "0.1.1"
+description = "Create PGP/MIME emails."
+authors = ["AI <info@autistici.org>"]
+
+[tool.poetry.dependencies]
+python = "~2.7 || ^3.5"
+python-gnupg = "^0.4.4"
+
+[tool.poetry.dev-dependencies]
+tox = "^3.7"
+
+[build-system]
+requires = ["poetry>=0.12"]
+build-backend = "poetry.masonry.api"
diff --git a/setup.py b/setup.py
index c5e06fb26217a370884c181a8acff12b651bcba4..f38b4082aed00e903bff86d62fee5b12fb460a57 100755
--- a/setup.py
+++ b/setup.py
@@ -9,11 +9,9 @@ setup(
     author="Autistici/Inventati",
     author_email="info@autistici.org",
     url="https://git.autistici.org/ai/pgp-mime-lib.git",
-    install_requires=["gnupg"],
-    setup_requires=[],
-    zip_safe=False,
+    install_requires=[
+        "python-gnupg",
+    ],
     packages=find_packages(),
-    package_data={},
-    entry_points={},
+    zip_safe=False,
 )
-
diff --git a/tox.ini b/tox.ini
index e71f54e0f5a86089595a26538b7296da2139047f..677b10850cfa7840fa7cb316d4f7c109684305ef 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,13 +1,31 @@
 [tox]
-envlist = py27
+envlist = py27, py3, flake8
 
 [testenv]
 deps=
-  nose
-  coverage
-commands=
-  /usr/bin/env LANG=en_US.UTF-8 \
-  nosetests -v \
-    --with-coverage --cover-package=pgp_mime_lib \
-    --cover-erase --cover-html --cover-html-dir=htmlcov \
-    []
+  pytest
+  pytest-cov
+setenv =
+  LANG=en_US.UTF-8
+  GNUPG=/usr/bin/gpg
+commands =
+  # run pytest with -x --pdb to drop into pdb at the first failure
+  pytest --cov=pgp_mime_lib --cov-report html:htmlcov --cov-report term {posargs}
+
+[testenv:flake8]
+basepython = python3
+deps =
+  flake8
+  flake8-builtins
+  flake8-bugbear
+skip_install = true
+commands = flake8 pgp_mime_lib/ setup.py
+
+[flake8]
+exclude = .tox/*, .git/*, build/*, pgp_mime_lib/_compat.py
+max-line-length = 100
+select =
+    E,F,W,C90,B,B902,C
+
+[pytest]
+norecursedirs = .tox