Merge pull request 'Fix cron script and more' (#134) from fix/cron-script into main

Reviewed-on: #134
This commit is contained in:
pfm 2023-12-09 20:26:37 +00:00
commit 628de8a28d
8 changed files with 58 additions and 33 deletions

View file

@ -45,17 +45,15 @@ RX_CONFIRM = re.compile(br'key "([^"]+)" imported')
class EncryptionException(Exception):
"""Represents a failure to encrypt a payload."""
"""Represents a failure to encrypt a payload.
def __init__(self, issue: str, recipient: str, cause: str):
"""Initialise an exception."""
self._issue = issue
self._recipient = recipient
self._cause = cause
def __str__(self):
"""Return human-readable string representation."""
return f"issue: {self._issue}; to: {self._recipient}; cause: {self._cause}"
Arguments passed to exception constructor:
- issue: human-readable explanation of the issue;
- recipient: owner of the key;
- cause: any additional information, if present;
- key: fingerprint of the key.
"""
pass
def _build_command(key_home, *args, **kwargs):
@ -232,7 +230,7 @@ class GPGEncryptor:
if p.returncode != 0:
LOG.debug('Errors: %s', err)
details = parse_status(err)
raise EncryptionException(details['issue'], details['recipient'], details['cause'])
raise EncryptionException(details['issue'], details['recipient'], details['cause'], details['key'])
return (encdata, p.returncode)
def _popen(self):
@ -337,17 +335,19 @@ def parse_status_lines(lines: list) -> dict:
continue
if line.startswith(KEY_EXPIRED, STATUS_FD_PREFIX_LEN):
result['issue'] = KEY_EXPIRED
result['issue'] = 'key expired'
elif line.startswith(KEY_REVOKED, STATUS_FD_PREFIX_LEN):
result['issue'] = KEY_REVOKED
result['issue'] = 'key revoked'
elif line.startswith(NO_RECIPIENTS, STATUS_FD_PREFIX_LEN):
result['issue'] = NO_RECIPIENTS
result['issue'] = 'no recipients'
elif line.startswith(KEY_CONSIDERED, STATUS_FD_PREFIX_LEN):
result['key'] = line.split(b' ')[2]
elif line.startswith(INVALID_RECIPIENT, STATUS_FD_PREFIX_LEN):
words = line.split(b' ')
reason_code = int(words[2])
result['recipient'] = words[3]
result['cause'] = INVALID_RECIPIENT_CAUSES[reason_code]
if reason_code:
result['cause'] = INVALID_RECIPIENT_CAUSES[reason_code]
return result

View file

@ -32,3 +32,16 @@ To preview a particular identity, run:
```sh
python -m lacre.admin identities -e alice@example.com
```
## Importing identities from existing GnuPG keyring
If you already have a GnuPG keyring with your users' public keys or for some
reason Lacre's identity database needs to be re-populated with identities,
there's a command to do that:
```sh
python -m lacre.admin import -d /path/to/gnupg/directory
```
If you want to just re-populate the database, Lacre can remove all identities
prior to importing keys -- just add `-r` flag.

View file

@ -70,6 +70,9 @@ def sub_import(args):
conn = repo.connect(conf.get_item('database', 'url'))
identities = repo.IdentityRepository(conn)
if args.reload:
identities.delete_all()
total = 0
for (fingerprint, email) in public.items():
LOG.debug('Importing %s - %s', email, fingerprint)
@ -97,7 +100,10 @@ def main():
cmd_import = sub_commands.add_parser('import',
help='Load identities from GnuPG directory to Lacre database'
)
cmd_import.add_argument('-d', '--homedir', help='specify GnuPG directory (default: use configured dir.)')
cmd_import.add_argument('-d', '--homedir', default=False,
help='specify GnuPG directory (default: use configured dir.)')
cmd_import.add_argument('-r', '--reload', action='store_true',
help='delete all keys from database before importing')
cmd_import.set_defaults(operation=sub_import)
cmd_queue = sub_commands.add_parser('queue',

View file

@ -42,10 +42,6 @@ class MailEncryptionProxy:
LOG.debug('Parsed into %s: %s', type(message), repr(message))
if message.defects:
# Sometimes a weird message cannot be encoded back and
# delivered, so before bouncing such messages we at least
# record information about the issues. Defects are identified
# by email.* package.
LOG.warning("Issues found: %d; %s", len(message.defects), repr(message.defects))
if conf.flag_enabled('daemon', 'log_headers'):
@ -57,13 +53,14 @@ class MailEncryptionProxy:
try:
new_message = operation.perform(message)
send(new_message, operation.recipients())
except EncryptionException:
except EncryptionException as e:
# If the message can't be encrypted, deliver cleartext.
LOG.exception('Unable to encrypt message, delivering in cleartext')
LOG.error('Unable to encrypt message, delivering in cleartext: %s', e)
if not isinstance(operation, KeepIntact):
self._send_unencrypted(operation, message, envelope, send)
else:
LOG.error(f'Cannot perform {operation}')
LOG.exception('Cannot perform: %s', operation)
raise
except:
LOG.exception('Unexpected exception caught, bouncing message')

View file

@ -3,6 +3,10 @@
import logging
import lacre
import lacre.config as conf
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import markdown
# Read configuration from /etc/gpg-mailgate.conf
conf.load_config()

View file

@ -69,6 +69,14 @@ class IdentityRepository(KeyRing):
delq = delete(self._identities).where(self._identities.c.email == email)
LOG.debug('Deleting keys assigned to %s', email)
self._conn.execute(delq)
def delete_all(self):
LOG.warn('Deleting all identities from the database')
delq = delete(self._identities)
self._conn.execute(delq)
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
self._ensure_connected()

View file

@ -65,6 +65,10 @@ class GnuPGUtilitiesTest(unittest.TestCase):
uid = GnuPG._parse_uid_line(sample_in)
self.assertEqual(uid, 'alice@disposlab')
def test_exception_formatting(self):
e = GnuPG.EncryptionException('alice@disposlab', 'key expired', None, b'DEADBEEF')
self.assertEqual(str(e), 'To: alice@disposlab; Issue: key expired; Key: DEADBEEF')
def test_parse_statusfd_key_expired(self):
key_expired = b"""
[GNUPG:] KEYEXPIRED 1668272263
@ -74,9 +78,9 @@ class GnuPGUtilitiesTest(unittest.TestCase):
"""
result = GnuPG.parse_status(key_expired)
self.assertEqual(result['issue'], b'KEYEXPIRED')
self.assertEqual(result['issue'], 'key expired')
self.assertEqual(result['recipient'], b'name@domain')
self.assertEqual(result['cause'], 'No specific reason given')
self.assertEqual(result['cause'], 'Unknown')
self.assertEqual(result['key'], b'XXXXXXXXXXXXX')
def test_parse_statusfd_key_absent(self):
@ -88,7 +92,7 @@ class GnuPGUtilitiesTest(unittest.TestCase):
result = GnuPG.parse_status(non_specific_errors)
self.assertEqual(result['issue'], b'n/a')
self.assertEqual(result['recipient'], b'name@domain')
self.assertEqual(result['cause'], 'No specific reason given')
self.assertEqual(result['cause'], 'Unknown')
self.assertEqual(result['key'], b'n/a')

View file

@ -19,16 +19,9 @@
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
import sqlalchemy
import smtplib
import markdown
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
import lacre
import lacre.config as conf
import lacre.dbschema as db
from lacre.notify import notify
# Read configuration from /etc/gpg-mailgate.conf