uncouple FE from BE repos;initial improvements to frontend(FE);
This commit is contained in:
parent
4d346f723c
commit
e2207b7487
69 changed files with 32 additions and 2439 deletions
48
.gitignore
vendored
48
.gitignore
vendored
|
@ -1,48 +0,0 @@
|
|||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# GPG-Mailgate test files
|
||||
test/logs
|
||||
test/tmp
|
||||
test/gpg-mailgate.conf
|
||||
test/keyhome/random_seed
|
||||
|
||||
# Emacs files
|
||||
*~
|
||||
TAGS
|
||||
TAGS-Python
|
||||
TAGS-PHP
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import subprocess
|
||||
import shutil
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
|
||||
|
||||
LINE_FINGERPRINT = 'fpr'
|
||||
LINE_USER_ID = 'uid'
|
||||
|
||||
POS_FINGERPRINT = 9
|
||||
|
||||
def build_command(key_home, *args, **kwargs):
|
||||
cmd = ["gpg", '--homedir', key_home] + list(args)
|
||||
return cmd
|
||||
|
||||
def public_keys( keyhome ):
|
||||
cmd = build_command(keyhome, '--list-keys', '--with-colons')
|
||||
p = subprocess.Popen( cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
||||
p.wait()
|
||||
|
||||
keys = dict()
|
||||
fingerprint = None
|
||||
email = None
|
||||
for line in p.stdout.readlines():
|
||||
line = line.decode(sys.getdefaultencoding())
|
||||
if line[0:3] == LINE_FINGERPRINT:
|
||||
fingerprint = line.split(':')[POS_FINGERPRINT]
|
||||
if line[0:3] == LINE_USER_ID:
|
||||
if ('<' not in line or '>' not in line):
|
||||
continue
|
||||
email = line.split('<')[1].split('>')[0]
|
||||
if not (fingerprint is None or email is None):
|
||||
keys[fingerprint] = email
|
||||
fingerprint = None
|
||||
email = None
|
||||
return keys
|
||||
|
||||
def to_bytes(s) -> bytes:
|
||||
if isinstance(s, str):
|
||||
return bytes(s, sys.getdefaultencoding())
|
||||
else:
|
||||
return s
|
||||
|
||||
# Confirms a key has a given email address by importing it into a temporary
|
||||
# keyring. If this operation succeeds and produces a message mentioning the
|
||||
# expected email, a key is confirmed.
|
||||
def confirm_key( content, email ):
|
||||
tmpkeyhome = ''
|
||||
content = to_bytes(content)
|
||||
expected_email = to_bytes(email.lower())
|
||||
|
||||
while True:
|
||||
tmpkeyhome = '/tmp/' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(12))
|
||||
if not os.path.exists(tmpkeyhome):
|
||||
break
|
||||
|
||||
# let only the owner access the directory, otherwise gpg would complain
|
||||
os.mkdir(tmpkeyhome, mode=0o700)
|
||||
localized_env = os.environ.copy()
|
||||
localized_env["LANG"] = "C"
|
||||
p = subprocess.Popen( build_command(tmpkeyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=localized_env )
|
||||
result = p.communicate(input=content)[1]
|
||||
confirmed = False
|
||||
|
||||
for line in result.split(b"\n"):
|
||||
if b'imported' in line and b'<' in line and b'>' in line:
|
||||
if line.split(b'<')[1].split(b'>')[0].lower() == expected_email:
|
||||
confirmed = True
|
||||
break
|
||||
else:
|
||||
break # confirmation failed
|
||||
|
||||
# cleanup
|
||||
shutil.rmtree(tmpkeyhome)
|
||||
|
||||
return confirmed
|
||||
|
||||
# adds a key and ensures it has the given email address
|
||||
def add_key( keyhome, content ):
|
||||
if isinstance(content, str):
|
||||
content = bytes(content, sys.getdefaultencoding())
|
||||
p = subprocess.Popen( build_command(keyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
||||
p.communicate(input=content)
|
||||
p.wait()
|
||||
|
||||
def delete_key( keyhome, email ):
|
||||
from email.utils import parseaddr
|
||||
result = parseaddr(email)
|
||||
|
||||
if result[1]:
|
||||
# delete all keys matching this email address
|
||||
p = subprocess.Popen( build_command(keyhome, '--delete-key', '--batch', '--yes', result[1]), stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
||||
p.wait()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class GPGEncryptor:
|
||||
def __init__(self, keyhome, recipients = None, charset = None):
|
||||
self._keyhome = keyhome
|
||||
self._message = b''
|
||||
self._recipients = list()
|
||||
self._charset = charset
|
||||
if recipients != None:
|
||||
self._recipients.extend(recipients)
|
||||
|
||||
def update(self, message):
|
||||
self._message += message
|
||||
|
||||
def encrypt(self):
|
||||
p = subprocess.Popen( self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
||||
encdata = p.communicate(input=self._message)[0]
|
||||
return (encdata, p.returncode)
|
||||
|
||||
def _command(self):
|
||||
cmd = build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--pgp7", "--no-secmem-warning", "-a", "-e")
|
||||
|
||||
# add recipients
|
||||
for recipient in self._recipients:
|
||||
cmd.append("-r")
|
||||
cmd.append(recipient)
|
||||
|
||||
# add on the charset, if set
|
||||
if self._charset:
|
||||
cmd.append("--comment")
|
||||
cmd.append('Charset: ' + self._charset)
|
||||
|
||||
return cmd
|
||||
|
||||
class GPGDecryptor:
|
||||
def __init__(self, keyhome):
|
||||
self._keyhome = keyhome
|
||||
self._message = ''
|
||||
|
||||
def update(self, message):
|
||||
self._message += message
|
||||
|
||||
def decrypt(self):
|
||||
p = subprocess.Popen( self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
||||
decdata = p.communicate(input=self._message)[0]
|
||||
return (decdata, p.returncode)
|
||||
|
||||
def _command(self):
|
||||
return build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--no-secmem-warning", "-a", "-d")
|
190
INSTALL.md
190
INSTALL.md
|
@ -1,190 +0,0 @@
|
|||
# Installation instructions
|
||||
## Content
|
||||
- General information
|
||||
- Install GPG-Mailgate
|
||||
- Install GPG-Mailgate-Web
|
||||
- Install Register-handler
|
||||
|
||||
## General information
|
||||
GPG-Mailgate is divided in 3 main parts: GPG-Mailgate itself, GPG-Mailgate-Web and Register-handler. Some parts of the GPG-Mailgate project depend on other parts of the project. You will find information about these dependencies at the beginning of every installation part.
|
||||
|
||||
These instructions show you how to set up GPG-Mailgate in an easy way. If you are a more advanced user, feel free to experiment with the settings. For these instructions a home directory for the user `nobody` is set. Sadly this is an odd workaround but no better solution was found.
|
||||
|
||||
These instructions are based on an installation on an Ubuntu 14.04 LTS virtual machine. For other Linux distributions and other versions these instructions might need to be adapted to your distribution (e.g. installation of packages and used directories).
|
||||
|
||||
## Install GPG-Mailgate
|
||||
### Requirements
|
||||
- Python 3.X is already installed
|
||||
- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems
|
||||
- GnuPG is already installed and configured
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install the Python-M2Crypto module:
|
||||
|
||||
apt-get install python-m2crypto
|
||||
|
||||
2. Set the home directory for the user `nobody` (sadly this workaround is needed as there is no better solution at this point). If you get an error that the user is currently used by a process, you might need to kill the process manually.
|
||||
|
||||
usermod -d /var/gpgmailgate nobody
|
||||
|
||||
3. Create dedicated directories for storing PGP keys and S/MIME certificates and make the user `nobody` owner of these:
|
||||
|
||||
mkdir -p /var/gpgmailgate/.gnupg
|
||||
mkdir -p /var/gpgmailgate/smime
|
||||
chown -R nobody:nogroup /var/gpgmailgate/
|
||||
|
||||
4. Place the `gpg-mailgate.py` in `/usr/local/bin/`, make the user `nobody` owner of the file and make it executable:
|
||||
|
||||
chown nobody:nogroup /usr/local/bin/gpg-mailgate.py
|
||||
chmod u+x /usr/local/bin/gpg-mailgate.py
|
||||
|
||||
5. Place the `GnuPG` directory in `/usr/local/lib/python3.x/dist-packages` (replace 3.x with your Python version)
|
||||
|
||||
6. Configure `/etc/gpg-mailgate.conf` based on the provided `gpg-mailgate.conf.sample`. Change the settings according to your configuration. If you follow this guide and have a standard configuration for postfix, you don't need to change much.
|
||||
|
||||
7. Configure logging by copying `gpg-lacre-logging.conf.sample` to `/etc/gpg-lacre-logging.conf` and editing it according to your needs. The path to this file is included in `[logging]` section of `gpg-mailgate.conf` file, so if you place it somewhere else, make sure to update the path too. See also: [Configuration file format](https://docs.python.org/3/library/logging.config.html#configuration-file-format).
|
||||
|
||||
8. Add the following to the end of `/etc/postfix/master.cf`
|
||||
|
||||
gpg-mailgate unix - n n - - pipe
|
||||
flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient}
|
||||
|
||||
127.0.0.1:10028 inet n - n - 10 smtpd
|
||||
-o content_filter=
|
||||
-o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
|
||||
-o smtpd_helo_restrictions=
|
||||
-o smtpd_client_restrictions=
|
||||
-o smtpd_sender_restrictions=
|
||||
-o smtpd_recipient_restrictions=permit_mynetworks,reject
|
||||
-o mynetworks=127.0.0.0/8
|
||||
-o smtpd_authorized_xforward_hosts=127.0.0.0/8
|
||||
|
||||
If you use Postfix versions from 2.5 onwards, it is recommended to change `${recipient}` to `${original_recipient}` in line two of the lines above.
|
||||
|
||||
9. Add the following line to `/etc/postfix/main.cf`
|
||||
|
||||
content_filter = gpg-mailgate
|
||||
|
||||
10. Optional: GPG can automatically download new public keys for automatic signature verification. To enable automatic create the file `/var/gpgmailgate/.gnupg/gpg.conf`. Add the following line to the file:
|
||||
|
||||
keyserver-options auto-key-retrieve
|
||||
|
||||
11. Restart Postfix
|
||||
|
||||
You are now ready to go. To add a public key for encryption just use the following command:
|
||||
|
||||
sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/public.key
|
||||
|
||||
- Replace `/some/public.key` with the location of a public key
|
||||
- `/some/public.key` can be deleted after importation
|
||||
- Confirm that it's working:
|
||||
`sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpgmailgate/.gnupg`
|
||||
|
||||
Please also test your installation before using it.
|
||||
|
||||
GPG-Mailgate is also able to handle S/MIME certificates for encrypting mails. However, it is best to use it in combination with Register-Handler described later to add new certificates. If you try to add them manually it might fail. The certificates are stored in `/var/gpgmailgate/smime` in PKCS7 format and are named like `User@example.com` (the user part is case sensitive, the domain part should be in lower case).
|
||||
|
||||
####Additional settings
|
||||
Most mail servers do not handle mail addresses case sensitive. If you know that all your recipient mail servers do not care about case sensitivity then you can set `mail_case_insensitive` in the settings to `yes` so looking up PGP keys or S/MIME certificates does also happen case insensitive.
|
||||
If your recipients have problems to decrypt mails encrypted by GPG-Mailgate they might use a piece of software that does not support PGP/MIME encrypted mails. You can tell GPG-Mailgate to use the legacy PGP/INLINE format by adding the recipient to the `pgp_style` map in the following format:
|
||||
`User@example.com=inline`
|
||||
|
||||
|
||||
### Mail decryption
|
||||
GPG-Mailgate does not only feature encryption of mails but also decryption of PGP encrypted mails.
|
||||
#### Important notice
|
||||
**Read carefully before setting up and using this functionality!**
|
||||
|
||||
With this functionality you could use GPG-Mailgate to decrypt incoming PGP encrypted mails (it is also capable of decrypting outgoing mails if the necessary key is present). To use this, you need to store your private keys on the server. This means that anyone who is able to obtain admin rights on the server is able to get the private keys stored on the server and is able to decrypt any mail encrypted with the corresponding public key. **If the server gets compromised in any kind and the attacker may have gained access to the server's file system, the keys have to be regarded as compromised as well!** If this happens you have to revoke your keys, notify everyone who has your public key (key servers as well) not to use this key any longer. You also need to create a new key pair for encrypted communication.
|
||||
|
||||
#### Limitations
|
||||
There are two main types of PGP encryption: PGP/MIME and PGP/INLINE. PGP/MIME is standardized while PGP/INLINE isn't completely clear standardized (even though some people claim so). Decrypting PGP/MIME encrypted mails works in most cases while decrypting PGP/INLINE encrypted mails may fail more often. The reason is that most clients are implementing PGP/INLINE in their own way. GPG-Mailgate is able to decrypt mails which are encrypted PGP/INLINE by GPG-Mailgate on the sender's side. Furthermore it should be able to decrypt PGP/INLINE encrypted mails encrypted by Enigmail. For PGP/INLINE the mail's structure may not be preserved due to how PGP/INLINE is implemented on most clients. If you receive a PGP/INLINE encrypted mail that could not be decrypted by GPG-Mailgate you may ask the sender to use PGP/MIME instead. Furthermore file types might get lost when using PGP/INLINE. Due to this limitations decrypting PGP/INLINE encrypted mails is disabled by default. If you want to take the risk you can set `no_inline_dec` to `no` in the `[default]` section. You have been warned.
|
||||
|
||||
#### Setting up decryption
|
||||
You need the recipient's private key for whom you want to decrypt mails. Only unprotected keys are supported. Keys protected by a passphrase could not be used. To add the private key, use the following command:
|
||||
`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/private.key`
|
||||
From now on PGP encrypted mails will be decrypted for the recipients for whom the keys are imported.
|
||||
|
||||
You also can remove a private key by using the following command. Replace `user@example.com` with the user's address for whom you want to remove the key:
|
||||
`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --delete-secret-keys user@example.com`
|
||||
|
||||
## Install GPG-Mailgate-Web
|
||||
### Requirements
|
||||
- A webserver is installed and reachable
|
||||
- The webserver is able to handle PHP scripts
|
||||
- MySQL is installed
|
||||
- Python 3.X is already installed
|
||||
|
||||
### Installation
|
||||
All files you need can be found in the [gpg-mailgate-web](gpg-mailgate-web/) directory.
|
||||
|
||||
1. Install the Python-mysqldb and Python-markdown modules:
|
||||
|
||||
apt-get install python-mysqldb python-markdown
|
||||
|
||||
2. Create a new database for GPG-Mailgate-Web.
|
||||
|
||||
3. Import the schema file `schema.sql` into the newly created database.
|
||||
|
||||
4. Edit the config file located at `/etc/gpg-mailgate.conf`. Set `enabled = yes` in `[database]` and fill in the necessary settings for the database connection.
|
||||
|
||||
5. Copy the files located in the [public_html](gpg-mailgate-web/public_html) directory onto your webserver. They can also be placed in a subdirectory on your webserver.
|
||||
|
||||
6. On your webserver move the `config.sample.php` file to `config.php` and edit the configuration file.
|
||||
|
||||
7. Create directories for storing email templates:
|
||||
|
||||
mkdir -p /var/gpgmailgate/cron_templates
|
||||
|
||||
8. Copy the templates found in the [cron_templates](cron_templates/) directory into the newly created directory and transfer ownership:
|
||||
|
||||
chown -R nobody:nogroup /var/gpgmailgate/cron_templates
|
||||
|
||||
9. Copy `cron.py` to `/usr/local/bin/gpgmw-cron.py`. Make it executable and and transfer ownership to `nobody`:
|
||||
|
||||
chown nobody:nogroup /usr/local/bin/gpgmw-cron.py
|
||||
chmod u+x /usr/local/bin/gpgmw-cron.py
|
||||
|
||||
10. Create `/etc/cron.d/gpgmw` with contents:
|
||||
`*/3 * * * * nobody /usr/bin/python /usr/local/bin/gpgmw-cron.py > /dev/null`
|
||||
for executing the cron job automatically.
|
||||
|
||||
11. Test your installation.
|
||||
|
||||
### GPG-Mailgate-Web as keyserver
|
||||
GPG-Mailgate-Web can also be used as a keyserver. For more information have a look at GPG-Mailgate-Web's [readme](gpg-mailgate-web/README).
|
||||
|
||||
## Install Register-handler
|
||||
### Requirements
|
||||
- Already set up and working GPG-Mailgate-Web. It should be reachable from the machine that will run register-handler
|
||||
- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems. Your Postfix configuration should also support aliases
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install the Python-requests module:
|
||||
|
||||
apt-get install python-requests
|
||||
|
||||
2. Create directories for storing email templates:
|
||||
|
||||
mkdir -p /var/gpgmailgate/register_templates
|
||||
|
||||
3. Copy the templates found in the [register_templates](register_templates/) directory into the newly created directory and transfer ownership:
|
||||
|
||||
chown -R nobody:nogroup /var/gpgmailgate/register_templates
|
||||
|
||||
4. Copy `register-handler.py` to `/usr/local/bin/register-handler.py`. Make it executable and own it to `nobody`:
|
||||
|
||||
chown nobody:nogroup /usr/local/bin/register-handler.py
|
||||
chmod a+x /usr/local/bin/register-handler.py
|
||||
|
||||
5. Edit the config file located at `/etc/gpg-mailgate.conf`. Set the parameter `webpanel_url` in `[mailregister]` to the url of your GPG-Mailgate-Web panel (the URL should be the same as the one you use to access the panel with your web browser). Also set the parameter `register_email` to the email address you want the user to see when receiving mails from the register-handler (it does not have to be an existing address but it is recommended). Register-handler will send users mails when they are registering S/MIME certificates or when neither a S/MIME certificate nor a PGP key was found in a mail sent to the register-handler.
|
||||
|
||||
6. Add `register: |/usr/local/bin/register-handler.py` to `/etc/aliases`
|
||||
|
||||
7. Update postfix's alias database with `postalias /etc/aliases`
|
||||
|
||||
8. Restart postfix.
|
||||
|
||||
9. Test your installation.
|
67
Makefile
67
Makefile
|
@ -1,67 +0,0 @@
|
|||
.POSIX:
|
||||
.PHONY: test e2etest unittest crontest pre-clean clean
|
||||
|
||||
#
|
||||
# On systems where Python 3.x binary has a different name, just
|
||||
# overwrite the name/path on the command line, like:
|
||||
#
|
||||
# make test PYTHON=/usr/local/bin/python3.8
|
||||
#
|
||||
# This marco is passed via environment to test/e2e_test.py, where it's
|
||||
# used to compute further commands.
|
||||
#
|
||||
PYTHON = python3
|
||||
|
||||
TEST_DB = test/lacre.db
|
||||
|
||||
#
|
||||
# Main goal to run tests.
|
||||
#
|
||||
test: e2etest unittest crontest
|
||||
|
||||
#
|
||||
# Run a set of end-to-end tests.
|
||||
#
|
||||
# Test scenarios are described and configured by the test/e2e.ini
|
||||
# file. Basically this is just a script that feeds GPG Mailgate with
|
||||
# known input and checks whether output meets expectations.
|
||||
#
|
||||
e2etest: test/tmp test/logs pre-clean
|
||||
$(PYTHON) test/e2e_test.py
|
||||
|
||||
#
|
||||
# Run a basic cron-job test.
|
||||
#
|
||||
# We use PYTHONPATH to make sure that cron.py can import GnuPG
|
||||
# package. We also set GPG_MAILGATE_CONFIG env. variable to make sure
|
||||
# it slurps the right config.
|
||||
#
|
||||
crontest: clean-db $(TEST_DB)
|
||||
GPG_MAILGATE_CONFIG=test/gpg-mailgate-cron-test.conf PYTHONPATH=`pwd` $(PYTHON) gpg-mailgate-web/cron.py
|
||||
|
||||
$(TEST_DB):
|
||||
$(PYTHON) test/schema.py $(TEST_DB)
|
||||
|
||||
# Before running the crontest goal we need to make sure that the
|
||||
# database gets regenerated.
|
||||
clean-db:
|
||||
rm -f $(TEST_DB)
|
||||
|
||||
#
|
||||
# Run unit tests
|
||||
#
|
||||
unittest:
|
||||
$(PYTHON) -m unittest discover -s test
|
||||
|
||||
pre-clean:
|
||||
rm -fv test/gpg-mailgate.conf
|
||||
rm -f test/logs/*.log
|
||||
|
||||
test/tmp:
|
||||
mkdir test/tmp
|
||||
|
||||
test/logs:
|
||||
mkdir test/logs
|
||||
|
||||
clean: pre-clean
|
||||
rm -rfv test/tmp test/logs
|
28
README
28
README
|
@ -0,0 +1,28 @@
|
|||
Lacre Web
|
||||
--------------
|
||||
This is Lacre's simple php based frontend. It has been forked from [gpg-mailgate](https://github.com/fkrone/gpg-mailgate) project and it is a continuation of the original work. Special thanks to all those who have contributed to amazing work of gpg-mailgate-web and those who are willing to contribute to Lacre to improve security of email storage.
|
||||
|
||||
Lacre-Web is a simple web interface designed to allow any web user to upload their PGP public key and then have all mail sent via your mail server be encrypted. (Note: this is not meant for email authentication, only encryption.)
|
||||
|
||||
After submitting their key to a web form, the user will be required to confirm their email address. A cron script will register the public key with gpg-lacre (keyhome_only must be set to no currently, which is the default) after email confirmation. From then on, email to the specified address will be encrypted with the public key.
|
||||
|
||||
Lacre-Web frontend is useful for two purposes:
|
||||
- for a transparent PGP encryption layer in front of any web application
|
||||
- as a web interface for gpg-lacre so that users on your mail server can easily upload and change their PGP keys.
|
||||
|
||||
Note that all processing relating to the mail server is done via the cron script. This means that gpg-Lacre and the web frontend cron can be installed on a different server from the web server. The MySQL database must be shared between the two applications though.
|
||||
|
||||
1. Installation instructions:
|
||||
Below instructions assume working gpg-Lacre setup (backend) as well as working web-server (eg. nginx+php8) and sql database (eg. mariadb).
|
||||
|
||||
1) Create a MySQL database for Lacre-Web.
|
||||
a) Schema file is located in schema.sql
|
||||
b) Database name and account goes in /etc/gpg-lacre.conf (and set enabled = yes)
|
||||
3) Copy the contents of public_html to your web directory.
|
||||
4) Move config.sample.php to config.php and edit the configuration file.
|
||||
5) Copy cron.py to /usr/local/bin/Lacre-web-cron.py and set up a cron job
|
||||
```
|
||||
*/3 * * * * lacre /usr/bin/python /usr/local/bin/Lacre-web-cron.py > /dev/null
|
||||
```
|
||||
6) Ensure that cron is working and test your new Lacre-web installation!
|
||||
|
60
README.md
60
README.md
|
@ -1,60 +0,0 @@
|
|||
# GPG Lacre Project
|
||||
|
||||
Fork and continuation of original work of gpg-mailgate project: https://github.com/fkrone/gpg-mailgate
|
||||
|
||||
|
||||
**GPG Lacre** (wax seal in Portuguese) is a content filter for Postfix that automatically encrypts unencrypted incoming email using PGP or S/MIME for select recipients.
|
||||
This project is the continuation of the work of "gpg-mailgate" on providing open source, GnuPG based email encryption for emails at rest. All incoming emails are automatically encrypted with user's public key before they are saved on the server. It is a server side encryption solution while the control of the encryption keys are fully at the hands of the end-user and private keys are never stored on the server.
|
||||
|
||||
The scope of the project is to improve on the already existing code, provide easy to use key upload system (standalone as well as Roundcube plugin) and key discoverability. Beside providing a solution that is easy to use we will also provide easy to digest material about encryption, how it works and how to make use of it in situations other the just mailbox encryption. Understanding how encryption works is the key to self-determination and is therefore an important part of the project.
|
||||
|
||||
GPG Lacre will be battle tested on the email infrastructure of https://disroot.org (an ethical non-profit service provider).
|
||||
|
||||
---
|
||||
|
||||
The work on this project in 2021 is funded by https://nlnet.nl/thema/NGIZeroPET.html for which we are very thankful.
|
||||
|
||||
The scope of the work for 2021 is:
|
||||
- Rewrite code to python3
|
||||
- Improve standalone key upload website
|
||||
- Provide Roundcube plugin for key management
|
||||
- Improve key server features
|
||||
- Provide webiste with information and tutorials on how to use GPG in general and also **Lacre**
|
||||
- (Optional) provide Autocrypt support
|
||||
|
||||
Made possible thanks to:<br>
|
||||
![](https://nlnet.nl/logo/banner.png)
|
||||
|
||||
---
|
||||
For installation instructions, please refer to the included **INSTALL** file.
|
||||
|
||||
---
|
||||
|
||||
# Features
|
||||
- Correctly displays attachments and general email content; currently will only display first part of multipart messages
|
||||
- Public keys are stored in a dedicated gpg-home-directory
|
||||
- Encrypts both matching incoming and outgoing mail (this means gpg-mailgate can be used to encrypt outgoing mail for software that doesn't support PGP or S/MIME)
|
||||
- Decrypts PGP encrypted mails for present private keys (but no signature check and it does not always work with PGP/INLINE encrypted mails)
|
||||
- Easy installation
|
||||
- gpg-mailgate-web extension is a web interface allowing any user to upload PGP keys so that emails sent to them from your mail server will be encrypted (see gpg-mailgate-web directory for details)
|
||||
- people can submit their public key like to any keyserver to gpg-mailgate with the gpg-mailgate-web extension
|
||||
- people can send an S/MIME signed email to register@yourdomain.tld to register their public key
|
||||
- people can send their public OpenPGP key as attachment or inline to register@yourdomain.tld to register it
|
||||
|
||||
This is forked from the original project at http://code.google.com/p/gpg-mailgate/
|
||||
|
||||
# Authors
|
||||
|
||||
This is a combined work of many developers and contributors. We would like to pay honours to original gpg mailbox developers for making this project happen, and providing solid solution for encryption emails at rest:
|
||||
|
||||
* mcmaster <mcmaster@aphrodite.hurricanelabs.rsoc>
|
||||
* Igor Rzegocki <ajgon@irgon.com> - [GitHub](https://github.com/ajgon/gpg-mailgate)
|
||||
* Favyen Bastani <fbastani@perennate.com> - [GitHub](https://github.com/uakfdotb/gpg-mailgate)
|
||||
* Colin Moller <colin@unixarmy.com> - [GitHub](https://github.com/LeftyBC/gpg-mailgate)
|
||||
* Taylor Hornby <havoc@defuse.ca> - [GitHub](https://github.com/defuse/gpg-mailgate)
|
||||
* Martin (uragit) <uragit@telemage.com> - [GitHub](https://github.com/uragit/gpg-mailgate)
|
||||
* Braden Thomas - [BitBucket](https://bitbucket.org/drspringfield/emailencrypt.net/)
|
||||
* Bruce Markey - [GitHub](https://github.com/TheEd1tor)
|
||||
* Remko Tronçon - [GitHub](https://github.com/remko/phkp/)
|
||||
* Kiritan Flux [GitHub](https://github.com/kflux)
|
||||
* Fabian Krone [GitHub] (https://github.com/fkrone/gpg-mailgate)
|
|
@ -1 +0,0 @@
|
|||
Your PGP key has been deleted from our gateway. From now on you will receive mails from us unencrypted.
|
|
@ -1,3 +0,0 @@
|
|||
Your PGP key could not be validated. Please try again with a valid key.
|
||||
|
||||
Any valid keys from you submitted to the gateway before are now deleted.
|
|
@ -1 +0,0 @@
|
|||
Your PGP key has been imported successfully.
|
|
@ -1,47 +0,0 @@
|
|||
# Testing
|
||||
|
||||
First tests have been set up to cover GPG Mailgate with at least basic test
|
||||
that would be easy to run. The tests are called "end-to-end", meaning that we
|
||||
feed some input to GPG Mailgate and inspect the output.
|
||||
|
||||
## Running tests
|
||||
|
||||
To run tests, use command `make test`.
|
||||
|
||||
There are 3 types of tests:
|
||||
|
||||
* `make e2etest` -- they cover a complete Lacre flow, from feeding it with
|
||||
an email to accepting its encrypted form;
|
||||
* `make unittest` -- just small tests of small units of code;
|
||||
* `make crontest` -- execute cron job with a SQLite database.
|
||||
|
||||
E2E tests (`make e2etest`) should produce some helpful logs, so inspect
|
||||
contents of `test/logs` directory if something goes wrong.
|
||||
|
||||
If your system's Python binary isn't found in your `$PATH` or you want to use
|
||||
a specific binary, use make's macro overriding: `make test
|
||||
PYTHON=/path/to/python`.
|
||||
|
||||
## Key building blocks
|
||||
|
||||
- *Test Script* (`test/e2e_test.py`) that orchestrates the other components.
|
||||
It performs test cases described in the *Test Configuration*. It spawns
|
||||
*Test Mail Relay* and *GPG Mailgate* in appropriate order.
|
||||
- *Test Mail Relay* (`test/relay.py`), a simplistic mail daemon that only
|
||||
supports the happy path. It accepts a mail message and prints it to
|
||||
stdandard output.
|
||||
- *Test Configuration* (`test/e2e.ini`) specifies test cases: their input,
|
||||
expected results and helpful documentation. It also specifies the port that
|
||||
the *Test Mail Relay* should listen on.
|
||||
|
||||
## Limitations
|
||||
|
||||
Currently tests only check if the message has been encrypted, without
|
||||
verifying that the correct key has been used. That's because we don't know
|
||||
(yet) how to have a reproducible encrypted message. Option
|
||||
`--faked-system-time` wasn't enough to produce identical output.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
When things go wrong, be sure to study `test/logs/e2e.log` and
|
||||
`test/logs/gpg-mailgate.log` files -- they contain some useful information.
|
|
@ -1,57 +0,0 @@
|
|||
# Example configuration for Lacre logging. If you don't intend to change the
|
||||
# log format, you can just keep this file unchanged.
|
||||
|
||||
# HANDLERS:
|
||||
#
|
||||
# Two main targets for log entries are defined here: syslog and a plain text
|
||||
# log file. They are available as "handlers" named "syslog" and "lacrelog"
|
||||
# respectively.
|
||||
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[logger_root]
|
||||
level=NOTSET
|
||||
# Comma-separated handler names, see HANDLERS note at the top.
|
||||
handlers=syslog
|
||||
|
||||
[handlers]
|
||||
# Comma-separated handler names, see HANDLERS note at the top.
|
||||
keys=syslog
|
||||
|
||||
[formatters]
|
||||
keys=postfixfmt
|
||||
|
||||
#
|
||||
# By default, include messages from all log levels up to DEBUG.
|
||||
# However, productive systems may use something less verbose, like
|
||||
# WARN or even ERROR.
|
||||
#
|
||||
[handler_lacrelog]
|
||||
class=FileHandler
|
||||
level=DEBUG
|
||||
formatter=postfixfmt
|
||||
args=('test/logs/lacre.log', 'a+')
|
||||
|
||||
# You may want to change the second argument (handlers.SysLogHandler.LOG_MAIL)
|
||||
# to change the syslog facility used to record messages from Lacre.
|
||||
#
|
||||
# Options you can consider are "localX" facilities, available under names from
|
||||
# handlers.SysLogHandler.LOG_LOCAL0 to handlers.SysLogHandler.LOG_LOCAL7.
|
||||
#
|
||||
# Please refer to your syslog configuration for details on how to separate
|
||||
# records from different facilities.
|
||||
[handler_syslog]
|
||||
class=handlers.SysLogHandler
|
||||
level=INFO
|
||||
formatter=postfixfmt
|
||||
args=('/dev/log', handlers.SysLogHandler.LOG_MAIL)
|
||||
|
||||
#
|
||||
# Default Postfix log format.
|
||||
#
|
||||
[formatter_postfixfmt]
|
||||
format=%(asctime)s %(module)s[%(process)d]: %(message)s
|
||||
datefmt=%b %e %H:%M:%S
|
||||
style=%
|
||||
validate=True
|
|
@ -1,70 +0,0 @@
|
|||
gpg-mailgate-web
|
||||
----------------
|
||||
|
||||
gpg-mailgate-web is a web interface designed to allow any web user
|
||||
to upload their PGP public key and then have all mail sent via
|
||||
your mail server be encrypted. (Note: this is not meant for email
|
||||
authentication, only encryption.)
|
||||
|
||||
After submitting their key to a web form, the user will be required
|
||||
to confirm their email address. A cron script will register the
|
||||
public key with gpg-mailgate (keyhome_only must be set to no
|
||||
currently, which is the default) after email confirmation. From
|
||||
then on, email to the specified address will be encrypted with
|
||||
the public key.
|
||||
|
||||
gpg-mailgate-web is useful for two purposes: for a transparent
|
||||
PGP encryption layer in front of any web application, or simple as
|
||||
a web interface for gpg-mailgate so that users on your mail server
|
||||
can easily upload and change their PGP keys.
|
||||
|
||||
Note that all processing relating to the mail server is done via the
|
||||
cron script. This means that gpg-mailgate and the gpgmw cron can
|
||||
be installed on a different server from the web server. The MySQL
|
||||
database must be shared between the two applications though.
|
||||
|
||||
1. Installation instructions:
|
||||
|
||||
1) Install gpg-mailgate.
|
||||
2) Create a MySQL database for gpg-mailgate.
|
||||
a) Schema file is located in schema.sql
|
||||
b) Database name and account goes in /etc/gpg-mailgate.conf (and set enabled = yes)
|
||||
3) Copy the contents of public_html to your web directory.
|
||||
4) Move config.sample.php to config.php and edit the configuration file.
|
||||
5) Copy cron.py to /usr/local/bin/gpgmw-cron.py and set up a cron job
|
||||
a) Create /etc/cron.d/gpgmw with the contents:
|
||||
|
||||
*/3 * * * * nobody /usr/bin/python /usr/local/bin/gpgmw-cron.py > /dev/null
|
||||
|
||||
6) Ensure that cron is working and test your new gpg-mailgate-web installation!
|
||||
|
||||
----------------------------------------
|
||||
|
||||
2. Adding rudimentary HKP Keyserver functionality for submitting public keys from the GPG client
|
||||
|
||||
(so far only implemented and tested with lighttpd - basically you just need to make your http server
|
||||
listen on port 11371, redirect it to your gpg-mailgate-web directory and add a rewrite rule to catch
|
||||
'pks/add' in the URI)
|
||||
|
||||
1) add the following lines to your lighttp.conf file and change the path to your gpg-mailgate-web directory
|
||||
|
||||
server.reject-expect-100-with-417 = "disable"
|
||||
|
||||
$SERVER["socket"] == ":11371" {
|
||||
server.document-root = "/var/www/gpgmw"
|
||||
setenv.add-response-header = ( "Via" => "1.1 yourserver.tld:11371 (lighttpd)" )
|
||||
accesslog.filename = "/var/log/lighttpd/hkp-access.log"
|
||||
url.rewrite-once = ( "^/pks/(.*)" => "/index.php?/pks/$1" )
|
||||
}
|
||||
|
||||
2) reload lighttpd: /etc/init.d/lighttpd restart
|
||||
3) in the index.php add the following line after the other required_once(...) lines:
|
||||
|
||||
require_once("include/phphkp.php");
|
||||
|
||||
4) change the constants in the include/phphkp.php file!
|
||||
|
||||
5) check if it works with a GPG client of your choice pushing a public key to your server's
|
||||
domain or IP
|
||||
|
||||
(HTTP request to http://yourserver.tld:11371/pks/add with the public key in a POST variable 'keytext')
|
|
@ -1,155 +0,0 @@
|
|||
[default]
|
||||
# Whether gpg-mailgate should add a header after it has processed an email
|
||||
# This may be useful for debugging purposes
|
||||
add_header = yes
|
||||
|
||||
# Whether we should only encrypt emails if they are explicitly defined in
|
||||
# the key mappings below ([enc_keymap] section)
|
||||
# This means gpg-mailgate won't automatically detect PGP recipients for encrypting
|
||||
enc_keymap_only = no
|
||||
|
||||
# Whether we should only decrypt emails if they are explicitly defined in
|
||||
# the key mappings below ([dec_keymap] section)
|
||||
# This means gpg-mailgate won't automatically detect PGP recipients for decrypting
|
||||
dec_keymap_only = no
|
||||
|
||||
# If dec_keymap_only is set to yes and recipients have private keys present for decrypting
|
||||
# but are not on in the keymap, this can cause that mails for them will be
|
||||
# encrypted. Set this to no if you want this behaviour.
|
||||
failsave_dec = yes
|
||||
|
||||
# Convert encrypted text/plain email to MIME-attached encrypt style.
|
||||
# (Default is to use older inline-style PGP encoding.)
|
||||
mime_conversion = yes
|
||||
|
||||
# RFC 2821 defines that the user part (User@domain.tld) of a mail address should be treated case sensitive.
|
||||
# However, in the real world this is ignored very often. This option disables the RFC 2821
|
||||
# compatibility so both the user part and the domain part are treated case insensitive.
|
||||
# Disabling the compatibility is more convenient to users. So if you know that your
|
||||
# recipients all ignore the RFC you could this to yes.
|
||||
mail_case_insensitive = no
|
||||
|
||||
# This setting disables PGP/INLINE decryption completely. However,
|
||||
# PGP/MIME encrypted mails will still be decrypted if possible. PGP/INLINE
|
||||
# decryption has to be seen as experimental and could have some negative
|
||||
# side effects. So if you want to take the risk set this to no.
|
||||
no_inline_dec = yes
|
||||
|
||||
# Here you can define a regex for which the gateway should try to decrypt mails.
|
||||
# It could be used to define that decryption should be used for a wider range of
|
||||
# mail addresses e.g. a whole domain. No key is needed here. It is even active if
|
||||
# dec_keymap is set to yes. If this feature should be disabled, don't leave it blank.
|
||||
# Set it to None. For further regex information please have a look at
|
||||
# https://docs.python.org/2/library/re.html
|
||||
dec_regex = None
|
||||
|
||||
[gpg]
|
||||
# the directory where gpg-mailgate public keys are stored
|
||||
# (see INSTALL for details)
|
||||
#
|
||||
# Note that this directory should be accessible only for the Lacre user,
|
||||
# i.e. have mode 700.
|
||||
keyhome = /var/gpgmailgate/.gnupg
|
||||
|
||||
[smime]
|
||||
# the directory for the S/MIME certificate files
|
||||
cert_path = /var/gpgmailgate/smime
|
||||
|
||||
[mailregister]
|
||||
# settings for the register-handler
|
||||
register_email = register@yourdomain.tld
|
||||
mail_templates = /var/gpgmailgate/register_templates
|
||||
|
||||
# URL to webpanel. Upon receiving an email with a key, register-handler
|
||||
# uploads it to the web panel.
|
||||
webpanel_url = http://yourdomain.tld
|
||||
|
||||
[cron]
|
||||
# settings for the gpgmw cron job
|
||||
send_email = yes
|
||||
notification_email = gpg-mailgate@yourdomain.tld
|
||||
mail_templates = /var/gpgmailgate/cron_templates
|
||||
|
||||
[logging]
|
||||
# path to the logging configuration; see documentation for details:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging-config-fileformat
|
||||
config = /etc/gpg-lacre-logging.conf
|
||||
|
||||
[relay]
|
||||
# the relay settings to use for Postfix
|
||||
# gpg-mailgate will submit email to this relay after it is done processing
|
||||
# unless you alter the default Postfix configuration, you won't have to modify this
|
||||
host = 127.0.0.1
|
||||
port = 10028
|
||||
# This is the default port of postfix. It is used to send some
|
||||
# mails through the GPG-Mailgate so they are encrypted
|
||||
enc_port = 25
|
||||
|
||||
# Set this option to yes to use TLS for SMTP Servers which require TLS.
|
||||
starttls = no
|
||||
|
||||
[smtp]
|
||||
# Options when smtp auth is required to send out emails
|
||||
enabled = false
|
||||
username = gpg-mailgate
|
||||
password = changeme
|
||||
host = yourdomain.tld
|
||||
port = 587
|
||||
starttls = true
|
||||
|
||||
[database]
|
||||
# edit the settings below if you want to read keys from a
|
||||
# gpg-mailgate-web database other than SQLite
|
||||
enabled = yes
|
||||
url = sqlite:///test.db
|
||||
|
||||
# For a MySQL database "gpgmw", user "gpgmw" and password "password",
|
||||
# use the following URL:
|
||||
#
|
||||
#url = mysql://gpgmw:password@localhost/gpgmw
|
||||
#
|
||||
# For other RDBMS backends, see:
|
||||
# https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls
|
||||
|
||||
[enc_keymap]
|
||||
# You can find these by running the following command:
|
||||
# gpg --list-keys --keyid-format long user@example.com
|
||||
# Which will return output similar to:
|
||||
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
|
||||
# uid Joe User <user@example.com>
|
||||
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
|
||||
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
|
||||
#you@domain.tld = 12345678
|
||||
|
||||
[enc_domain_keymap]
|
||||
# This seems to be similar to the [enc_keymap] section. However, you
|
||||
# can define default keys for a domain here. Entries in the enc_keymap
|
||||
# and individual keys stored on the system have a higher priority than
|
||||
# the default keys specified here.
|
||||
#
|
||||
#
|
||||
# You can find these by running the following command:
|
||||
# gpg --list-keys --keyid-format long user@example.com
|
||||
# Which will return output similar to:
|
||||
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
|
||||
# uid Joe User <user@example.com>
|
||||
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
|
||||
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
|
||||
#domain.tld = 12345678
|
||||
|
||||
[dec_keymap]
|
||||
# You can find these by running the following command:
|
||||
# gpg --list-secret-keys --keyid-format long user@example.com
|
||||
# Which will return output similar to:
|
||||
# sec 1024D/AAAAAAAAAAAAAAAA 2007-10-22
|
||||
# uid Joe User <user@example.com>
|
||||
# ssb 2048g/BBBBBBBBBBBBBBBB 2007-10-22
|
||||
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
|
||||
#you@domain.tld = 12345678
|
||||
|
||||
[pgp_style]
|
||||
# Here a PGP style (inline or PGP/MIME) could be defined for recipients.
|
||||
# This overwrites the setting mime_conversion for the defined recipients.
|
||||
# Valid entries are inline and mime
|
||||
# If an entry is not valid, the setting mime_conversion is used as fallback.
|
||||
#you@domian.tld = mime
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import email
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
|
||||
import lacre
|
||||
import lacre.config as conf
|
||||
import lacre.mailgate as mailgate
|
||||
|
||||
|
||||
start = time.time()
|
||||
conf.load_config()
|
||||
lacre.init_logging(conf.get_item('logging', 'config'))
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
missing_params = conf.validate_config()
|
||||
if missing_params:
|
||||
LOG.error(f"Aborting delivery! Following mandatory config parameters are missing: {missing_params!r}")
|
||||
sys.exit(lacre.EX_CONFIG)
|
||||
|
||||
# Read e-mail from stdin
|
||||
raw = sys.stdin.read()
|
||||
raw_message = email.message_from_string( raw )
|
||||
from_addr = raw_message['From']
|
||||
to_addrs = sys.argv[1:]
|
||||
|
||||
# Let's start
|
||||
mailgate.deliver_message(raw_message, from_addr, to_addrs)
|
||||
(elapsed_s, process_t) = mailgate.exec_time_info(start)
|
||||
|
||||
LOG.info("Elapsed-time: {elapsed:.2f}s; Process-time: {process:.4f}s".format(elapsed=elapsed_s, process=process_t))
|
|
@ -1,48 +0,0 @@
|
|||
"""Lacre --- the Postfix mail filter encrypting incoming email
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
# Following structure configures logging iff a file-based configuration cannot
|
||||
# be performed. It only sets up a syslog handler, so that the admin has at
|
||||
# least some basic information.
|
||||
FAIL_OVER_LOGGING_CONFIG = {
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'sysfmt': {
|
||||
'format': '%(asctime)s %(module)s %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'syslog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'level': 'INFO',
|
||||
'formatter': 'sysfmt'
|
||||
},
|
||||
'lacrelog': {
|
||||
'class': 'logging.FileHandler',
|
||||
'level': 'INFO',
|
||||
'formatter': 'sysfmt',
|
||||
'filename': 'lacre.log'
|
||||
}
|
||||
},
|
||||
'root': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['syslog', 'lacrelog']
|
||||
}
|
||||
}
|
||||
|
||||
# Exit code taken from <sysexits.h>:
|
||||
EX_UNAVAILABLE = 69
|
||||
EX_TEMPFAIL = 75
|
||||
EX_CONFIG = 78
|
||||
|
||||
|
||||
def init_logging(config_filename):
|
||||
if config_filename is not None:
|
||||
logging.config.fileConfig(config_filename)
|
||||
else:
|
||||
logging.config.dictConfig(FAIL_OVER_LOGGING_CONFIG)
|
||||
logging.warning('Lacre logging configuration missing, using syslog as default')
|
|
@ -1,87 +0,0 @@
|
|||
"""Lacre configuration
|
||||
|
||||
Routines defined here are responsible for processing configuration.
|
||||
"""
|
||||
|
||||
from configparser import RawConfigParser
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Environment variable name we read to retrieve configuration path. This is to
|
||||
# enable non-root users to set up and run GPG Mailgate and to make the software
|
||||
# testable.
|
||||
CONFIG_PATH_ENV = "GPG_MAILGATE_CONFIG"
|
||||
|
||||
# List of mandatory configuration parameters. Each item on this list should be
|
||||
# a pair: a section name and a parameter name.
|
||||
MANDATORY_CONFIG_ITEMS = [("relay", "host"),
|
||||
("relay", "port")]
|
||||
|
||||
# Global dict to keep configuration parameters. It's hidden behind several
|
||||
# utility functions to make it easy to replace it with ConfigParser object in
|
||||
# the future.
|
||||
cfg = dict()
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Parses configuration file.
|
||||
|
||||
If environment variable identified by CONFIG_PATH_ENV
|
||||
variable is set, its value is taken as a configuration file
|
||||
path. Otherwise, the default is taken
|
||||
('/etc/gpg-mailgate.conf').
|
||||
"""
|
||||
configFile = os.getenv(CONFIG_PATH_ENV, '/etc/gpg-mailgate.conf')
|
||||
|
||||
parser = read_config(configFile)
|
||||
|
||||
global cfg
|
||||
cfg = copy_to_dict(parser)
|
||||
return cfg
|
||||
|
||||
def read_config(fileName) -> RawConfigParser:
|
||||
cp = RawConfigParser()
|
||||
cp.read(fileName)
|
||||
|
||||
return cp
|
||||
|
||||
def copy_to_dict(confParser) -> dict:
|
||||
config = dict()
|
||||
|
||||
for sect in confParser.sections():
|
||||
config[sect] = dict()
|
||||
for (name, value) in confParser.items(sect):
|
||||
config[sect][name] = value
|
||||
|
||||
return config
|
||||
|
||||
def get_item(section, key, empty_value = None):
|
||||
global cfg
|
||||
if config_item_set(section, key):
|
||||
return cfg[section][key]
|
||||
else:
|
||||
return empty_value
|
||||
|
||||
def has_section(section) -> bool:
|
||||
global cfg
|
||||
return section in cfg
|
||||
|
||||
def config_item_set(section, key) -> bool:
|
||||
global cfg
|
||||
return section in cfg and (key in cfg[section]) and not (cfg[section][key] is None)
|
||||
|
||||
def config_item_equals(section, key, value) -> bool:
|
||||
global cfg
|
||||
return section in cfg and key in cfg[section] and cfg[section][key] == value
|
||||
|
||||
def validate_config():
|
||||
"""Checks whether the configuration is complete.
|
||||
|
||||
Returns a list of missing parameters, so an empty list means
|
||||
configuration is complete.
|
||||
"""
|
||||
missing = []
|
||||
for (section, param) in MANDATORY_CONFIG_ITEMS:
|
||||
if not config_item_set(section, param):
|
||||
missing.append((section, param))
|
||||
return missing
|
|
@ -1,440 +0,0 @@
|
|||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from configparser import RawConfigParser
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import copy
|
||||
import email
|
||||
import email.message
|
||||
import email.utils
|
||||
import GnuPG
|
||||
import os
|
||||
import re
|
||||
import smtplib
|
||||
import sys
|
||||
import syslog
|
||||
import traceback
|
||||
import time
|
||||
|
||||
|
||||
# imports for S/MIME
|
||||
from M2Crypto import BIO, Rand, SMIME, X509
|
||||
from email.mime.message import MIMEMessage
|
||||
|
||||
import logging
|
||||
import lacre
|
||||
import lacre.text as text
|
||||
import lacre.config as conf
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
def gpg_encrypt( raw_message, recipients ):
|
||||
global LOG
|
||||
|
||||
if not conf.config_item_set('gpg', 'keyhome'):
|
||||
LOG.error("No valid entry for gpg keyhome. Encryption aborted.")
|
||||
return recipients
|
||||
|
||||
keys = GnuPG.public_keys( conf.get_item('gpg', 'keyhome') )
|
||||
for fingerprint in keys:
|
||||
keys[fingerprint] = sanitize_case_sense(keys[fingerprint])
|
||||
|
||||
# This list will be filled with pairs (M, N), where M is the destination
|
||||
# address we're going to deliver the message to and N is the identity we're
|
||||
# going to encrypt it for.
|
||||
gpg_to = list()
|
||||
|
||||
ungpg_to = list()
|
||||
|
||||
enc_keymap_only = conf.config_item_equals('default', 'enc_keymap_only', 'yes')
|
||||
|
||||
for to in recipients:
|
||||
|
||||
# Check if recipient is in keymap
|
||||
if conf.config_item_set('enc_keymap', to):
|
||||
LOG.info("Encrypt keymap has key '%s'" % conf.get_item('enc_keymap', to) )
|
||||
# Check we've got a matching key!
|
||||
if conf.get_item('enc_keymap', to) in keys:
|
||||
gpg_to.append( (to, conf.get_item('enc_keymap', to)) )
|
||||
continue
|
||||
else:
|
||||
LOG.info("Key '%s' in encrypt keymap not found in keyring for email address '%s'." % (conf.get_item('enc_keymap', to), to))
|
||||
|
||||
# Check if key in keychain is present
|
||||
if not enc_keymap_only:
|
||||
if to in keys.values():
|
||||
gpg_to.append( (to, to) )
|
||||
continue
|
||||
|
||||
# If this is an address with a delimiter (i.e. "foo+bar@example.com"),
|
||||
# then strip whatever is found after the delimiter and try this address.
|
||||
(newto, topic) = text.parse_delimiter(to)
|
||||
if newto in keys.values():
|
||||
gpg_to.append((to, newto))
|
||||
|
||||
# Check if there is a default key for the domain
|
||||
splitted_to = to.split('@')
|
||||
if len(splitted_to) > 1:
|
||||
domain = splitted_to[1]
|
||||
if conf.config_item_set('enc_domain_keymap', domain):
|
||||
LOG.info("Encrypt domain keymap has key '%s'" % conf.get_item('enc_domain_keymap', domain) )
|
||||
# Check we've got a matching key!
|
||||
if conf.get_item('enc_domain_keymap', domain) in keys:
|
||||
LOG.info("Using default domain key for recipient '%s'" % to)
|
||||
gpg_to.append( (to, conf.get_item('enc_domain_keymap', domain)) )
|
||||
continue
|
||||
else:
|
||||
LOG.info("Key '%s' in encrypt domain keymap not found in keyring for email address '%s'." % (conf.get_item('enc_domain_keymap', domain), to))
|
||||
|
||||
# At this point no key has been found
|
||||
LOG.debug("Recipient (%s) not in PGP domain list for encrypting." % to)
|
||||
ungpg_to.append(to)
|
||||
|
||||
if gpg_to:
|
||||
LOG.info("Encrypting email to: %s" % ' '.join( x[0] for x in gpg_to ))
|
||||
|
||||
# Getting PGP style for recipient
|
||||
gpg_to_smtp_mime = list()
|
||||
gpg_to_cmdline_mime = list()
|
||||
|
||||
gpg_to_smtp_inline = list()
|
||||
gpg_to_cmdline_inline = list()
|
||||
|
||||
for rcpt in gpg_to:
|
||||
# Checking pre defined styles in settings first
|
||||
if conf.config_item_equals('pgp_style', rcpt[0], 'mime'):
|
||||
gpg_to_smtp_mime.append(rcpt[0])
|
||||
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
|
||||
elif conf.config_item_equals('pgp_style', rcpt[0], 'inline'):
|
||||
gpg_to_smtp_inline.append(rcpt[0])
|
||||
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
|
||||
else:
|
||||
# Log message only if an unknown style is defined
|
||||
if conf.config_item_set('pgp_style', rcpt[0]):
|
||||
LOG.debug("Style %s for recipient %s is not known. Use default as fallback." % (conf.get_item("pgp_style", rcpt[0]), rcpt[0]))
|
||||
|
||||
# If no style is in settings defined for recipient, use default from settings
|
||||
if conf.config_item_equals('default', 'mime_conversion', 'yes'):
|
||||
gpg_to_smtp_mime.append(rcpt[0])
|
||||
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
|
||||
else:
|
||||
gpg_to_smtp_inline.append(rcpt[0])
|
||||
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
|
||||
|
||||
if gpg_to_smtp_mime:
|
||||
# Encrypt mail with PGP/MIME
|
||||
raw_message_mime = copy.deepcopy(raw_message)
|
||||
|
||||
if conf.config_item_equals('default', 'add_header', 'yes'):
|
||||
raw_message_mime['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
|
||||
|
||||
if 'Content-Transfer-Encoding' in raw_message_mime:
|
||||
raw_message_mime.replace_header('Content-Transfer-Encoding', '8BIT')
|
||||
else:
|
||||
raw_message_mime['Content-Transfer-Encoding'] = '8BIT'
|
||||
|
||||
encrypted_payloads = encrypt_all_payloads_mime( raw_message_mime, gpg_to_cmdline_mime )
|
||||
raw_message_mime.set_payload( encrypted_payloads )
|
||||
|
||||
send_msg( raw_message_mime.as_string(), gpg_to_smtp_mime )
|
||||
|
||||
if gpg_to_smtp_inline:
|
||||
# Encrypt mail with PGP/INLINE
|
||||
raw_message_inline = copy.deepcopy(raw_message)
|
||||
|
||||
if conf.config_item_equals('default', 'add_header', 'yes'):
|
||||
raw_message_inline['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
|
||||
|
||||
if 'Content-Transfer-Encoding' in raw_message_inline:
|
||||
raw_message_inline.replace_header('Content-Transfer-Encoding', '8BIT')
|
||||
else:
|
||||
raw_message_inline['Content-Transfer-Encoding'] = '8BIT'
|
||||
|
||||
encrypted_payloads = encrypt_all_payloads_inline( raw_message_inline, gpg_to_cmdline_inline )
|
||||
raw_message_inline.set_payload( encrypted_payloads )
|
||||
|
||||
send_msg( raw_message_inline.as_string(), gpg_to_smtp_inline )
|
||||
|
||||
return ungpg_to
|
||||
|
||||
def encrypt_all_payloads_inline( message, gpg_to_cmdline ):
|
||||
|
||||
# This breaks cascaded MIME messages. Blame PGP/INLINE.
|
||||
encrypted_payloads = list()
|
||||
if isinstance(message.get_payload(), str):
|
||||
return encrypt_payload( message, gpg_to_cmdline ).get_payload()
|
||||
|
||||
for payload in message.get_payload():
|
||||
if( isinstance(payload.get_payload(), list) ):
|
||||
encrypted_payloads.extend( encrypt_all_payloads_inline( payload, gpg_to_cmdline ) )
|
||||
else:
|
||||
encrypted_payloads.append( encrypt_payload( payload, gpg_to_cmdline ) )
|
||||
|
||||
return encrypted_payloads
|
||||
|
||||
def encrypt_all_payloads_mime( message, gpg_to_cmdline ):
|
||||
# Convert a plain text email into PGP/MIME attachment style. Modeled after enigmail.
|
||||
pgp_ver_part = email.message.Message()
|
||||
pgp_ver_part.set_payload("Version: 1"+text.EOL)
|
||||
pgp_ver_part.set_type("application/pgp-encrypted")
|
||||
pgp_ver_part.set_param('PGP/MIME version identification', "", 'Content-Description' )
|
||||
|
||||
encrypted_part = email.message.Message()
|
||||
encrypted_part.set_type("application/octet-stream")
|
||||
encrypted_part.set_param('name', "encrypted.asc")
|
||||
encrypted_part.set_param('OpenPGP encrypted message', "", 'Content-Description' )
|
||||
encrypted_part.set_param('inline', "", 'Content-Disposition' )
|
||||
encrypted_part.set_param('filename', "encrypted.asc", 'Content-Disposition' )
|
||||
|
||||
if isinstance(message.get_payload(), str):
|
||||
# WTF! It seems to swallow the first line. Not sure why. Perhaps
|
||||
# it's skipping an imaginary blank line someplace. (ie skipping a header)
|
||||
# Workaround it here by prepending a blank line.
|
||||
# This happens only on text only messages.
|
||||
additionalSubHeader=""
|
||||
encoding = sys.getdefaultencoding()
|
||||
if 'Content-Type' in message and not message['Content-Type'].startswith('multipart'):
|
||||
additionalSubHeader="Content-Type: "+message['Content-Type']+text.EOL
|
||||
(base, encoding) = text.parse_content_type(message['Content-Type'])
|
||||
LOG.debug(f"Identified encoding as {encoding}")
|
||||
encrypted_part.set_payload(additionalSubHeader+text.EOL +message.get_payload(decode=True).decode(encoding))
|
||||
check_nested = True
|
||||
else:
|
||||
processed_payloads = generate_message_from_payloads(message)
|
||||
encrypted_part.set_payload(processed_payloads.as_string())
|
||||
check_nested = False
|
||||
|
||||
message.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
|
||||
|
||||
# Use this just to generate a MIME boundary string.
|
||||
junk_msg = MIMEMultipart()
|
||||
junk_str = junk_msg.as_string() # WTF! Without this, get_boundary() will return 'None'!
|
||||
boundary = junk_msg.get_boundary()
|
||||
|
||||
# This also modifies the boundary in the body of the message, ie it gets parsed.
|
||||
if 'Content-Type' in message:
|
||||
message.replace_header('Content-Type', f"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"{boundary}\""+text.EOL)
|
||||
else:
|
||||
message['Content-Type'] = f"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"{boundary}\""+text.EOL
|
||||
|
||||
return [ pgp_ver_part, encrypt_payload(encrypted_part, gpg_to_cmdline, check_nested) ]
|
||||
|
||||
def encrypt_payload( payload, gpg_to_cmdline, check_nested = True ):
|
||||
global LOG
|
||||
|
||||
raw_payload = payload.get_payload(decode=True)
|
||||
if check_nested and text.is_pgp_inline(raw_payload):
|
||||
LOG.debug("Message is already pgp encrypted. No nested encryption needed.")
|
||||
return payload
|
||||
|
||||
# No check is needed for conf.get_item('gpg', 'keyhome') as this is already done in method gpg_encrypt
|
||||
gpg = GnuPG.GPGEncryptor( conf.get_item('gpg', 'keyhome'), gpg_to_cmdline, payload.get_content_charset() )
|
||||
gpg.update( raw_payload )
|
||||
encrypted_data, returncode = gpg.encrypt()
|
||||
LOG.debug("Return code from encryption=%d (0 indicates success)." % returncode)
|
||||
if returncode != 0:
|
||||
LOG.info("Encrytion failed with return code %d. Encryption aborted." % returncode)
|
||||
return payload
|
||||
|
||||
payload.set_payload( encrypted_data )
|
||||
isAttachment = payload.get_param( 'attachment', None, 'Content-Disposition' ) is not None
|
||||
|
||||
if isAttachment:
|
||||
filename = payload.get_filename()
|
||||
if filename:
|
||||
pgpFilename = filename + ".pgp"
|
||||
if not (payload.get('Content-Disposition') is None):
|
||||
payload.set_param( 'filename', pgpFilename, 'Content-Disposition' )
|
||||
if not (payload.get('Content-Type') is None) and not (payload.get_param( 'name' ) is None):
|
||||
payload.set_param( 'name', pgpFilename )
|
||||
if not (payload.get('Content-Transfer-Encoding') is None):
|
||||
payload.replace_header( 'Content-Transfer-Encoding', "7bit" )
|
||||
|
||||
return payload
|
||||
|
||||
def smime_encrypt( raw_message, recipients ):
|
||||
global LOG
|
||||
global from_addr
|
||||
|
||||
if not conf.config_item_set('smime', 'cert_path'):
|
||||
LOG.info("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
|
||||
return recipients
|
||||
|
||||
cert_path = conf.get_item('smime', 'cert_path')+"/"
|
||||
s = SMIME.SMIME()
|
||||
sk = X509.X509_Stack()
|
||||
smime_to = list()
|
||||
unsmime_to = list()
|
||||
|
||||
for addr in recipients:
|
||||
cert_and_email = get_cert_for_email(addr, cert_path)
|
||||
|
||||
if not (cert_and_email is None):
|
||||
(to_cert, normal_email) = cert_and_email
|
||||
LOG.debug("Found cert " + to_cert + " for " + addr + ": " + normal_email)
|
||||
smime_to.append(addr)
|
||||
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
|
||||
sk.push(x509)
|
||||
else:
|
||||
unsmime_to.append(addr)
|
||||
|
||||
if smime_to:
|
||||
s.set_x509_stack(sk)
|
||||
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
|
||||
p7 = s.encrypt( BIO.MemoryBuffer( raw_message.as_string() ) )
|
||||
# Output p7 in mail-friendly format.
|
||||
out = BIO.MemoryBuffer()
|
||||
out.write('From: ' + from_addr + text.EOL)
|
||||
out.write('To: ' + raw_message['To'] + text.EOL)
|
||||
if raw_message['Cc']:
|
||||
out.write('Cc: ' + raw_message['Cc'] + text.EOL)
|
||||
if raw_message['Bcc']:
|
||||
out.write('Bcc: ' + raw_message['Bcc'] + text.EOL)
|
||||
if raw_message['Subject']:
|
||||
out.write('Subject: '+ raw_message['Subject'] + text.EOL)
|
||||
|
||||
if conf.config_item_equals('default', 'add_header', 'yes'):
|
||||
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate' + text.EOL)
|
||||
|
||||
s.write(out, p7)
|
||||
|
||||
LOG.debug(f"Sending message from {from_addr} to {smime_to}")
|
||||
|
||||
send_msg(out.read(), smime_to)
|
||||
if unsmime_to:
|
||||
LOG.debug(f"Unable to find valid S/MIME certificates for {unsmime_to}")
|
||||
|
||||
return unsmime_to
|
||||
|
||||
def get_cert_for_email( to_addr, cert_path ):
|
||||
global LOG
|
||||
|
||||
insensitive = conf.config_item_equals('default', 'mail_case_insensitive', 'yes')
|
||||
|
||||
files_in_directory = os.listdir(cert_path)
|
||||
for filename in files_in_directory:
|
||||
file_path = os.path.join(cert_path, filename)
|
||||
if not os.path.isfile(file_path):
|
||||
continue
|
||||
|
||||
if insensitive:
|
||||
if filename.casefold() == to_addr:
|
||||
return (file_path, to_addr)
|
||||
else:
|
||||
if filename == to_addr:
|
||||
return (file_path, to_addr)
|
||||
|
||||
# support foo+ignore@bar.com -> foo@bar.com
|
||||
(fixed_up_email, topic) = text.parse_delimiter(to_addr)
|
||||
if topic is None:
|
||||
# delimiter not used
|
||||
return None
|
||||
else:
|
||||
LOG.debug(f"Looking up certificate for {fixed_up_email} after parsing {to_addr}")
|
||||
return get_cert_for_email(fixed_up_email, cert_path)
|
||||
|
||||
def sanitize_case_sense( address ):
|
||||
if conf.config_item_equals('default', 'mail_case_insensitive', 'yes'):
|
||||
address = address.lower()
|
||||
else:
|
||||
splitted_address = address.split('@')
|
||||
if len(splitted_address) > 1:
|
||||
address = splitted_address[0] + '@' + splitted_address[1].lower()
|
||||
|
||||
return address
|
||||
|
||||
def generate_message_from_payloads( payloads, message = None ):
|
||||
if message == None:
|
||||
message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype())
|
||||
|
||||
for payload in payloads.get_payload():
|
||||
if( isinstance(payload.get_payload(), list) ):
|
||||
message.attach(generate_message_from_payloads(payload))
|
||||
else:
|
||||
message.attach(payload)
|
||||
|
||||
return message
|
||||
|
||||
def get_first_payload( payloads ):
|
||||
if payloads.is_multipart():
|
||||
return get_first_payload(payloads.get_payload(0))
|
||||
else:
|
||||
return payloads
|
||||
|
||||
def send_msg( message, recipients ):
|
||||
global LOG
|
||||
global from_addr
|
||||
|
||||
recipients = [_f for _f in recipients if _f]
|
||||
if recipients:
|
||||
LOG.info(f"Sending email to: {recipients!r}")
|
||||
relay = (conf.get_item('relay', 'host'), int(conf.get_item('relay', 'port')))
|
||||
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||
if conf.config_item_equals('relay', 'starttls', 'yes'):
|
||||
smtp.starttls()
|
||||
smtp.sendmail( from_addr, recipients, message )
|
||||
else:
|
||||
LOG.info("No recipient found")
|
||||
|
||||
def deliver_message( raw_message, from_address, to_addrs ):
|
||||
global LOG
|
||||
global from_addr
|
||||
|
||||
# Ugly workaround to keep the code working without too many changes.
|
||||
from_addr = from_address
|
||||
|
||||
recipients_left = [sanitize_case_sense(recipient) for recipient in to_addrs]
|
||||
|
||||
# There is no need for nested encryption
|
||||
first_payload = get_first_payload(raw_message)
|
||||
if first_payload.get_content_type() == 'application/pkcs7-mime':
|
||||
LOG.debug("Message is already encrypted with S/MIME. Encryption aborted.")
|
||||
send_msg(raw_message.as_string(), recipients_left)
|
||||
return
|
||||
|
||||
first_payload = first_payload.get_payload(decode=True)
|
||||
if text.is_pgp_inline(first_payload):
|
||||
LOG.debug("Message is already encrypted as PGP/INLINE. Encryption aborted.")
|
||||
send_msg(raw_message.as_string(), recipients_left)
|
||||
return
|
||||
|
||||
if raw_message.get_content_type() == 'multipart/encrypted':
|
||||
LOG.debug("Message is already encrypted. Encryption aborted.")
|
||||
send_msg(raw_message.as_string(), recipients_left)
|
||||
return
|
||||
|
||||
# Encrypt mails for recipients with known public PGP keys
|
||||
recipients_left = gpg_encrypt(raw_message, recipients_left)
|
||||
if not recipients_left:
|
||||
return
|
||||
|
||||
# Encrypt mails for recipients with known S/MIME certificate
|
||||
recipients_left = smime_encrypt(raw_message, recipients_left)
|
||||
if not recipients_left:
|
||||
return
|
||||
|
||||
# Send out mail to recipients which are left
|
||||
send_msg(raw_message.as_string(), recipients_left)
|
||||
|
||||
def exec_time_info(start_timestamp):
|
||||
elapsed_s = time.time() - start_timestamp
|
||||
process_t = time.process_time()
|
||||
return (elapsed_s, process_t)
|
|
@ -1,35 +0,0 @@
|
|||
import sys
|
||||
import re
|
||||
|
||||
# The standard way to encode line-ending in email:
|
||||
EOL = "\r\n"
|
||||
|
||||
PGP_INLINE_BEGIN = b"-----BEGIN PGP MESSAGE-----"
|
||||
PGP_INLINE_END = b"-----END PGP MESSAGE-----"
|
||||
|
||||
def parse_content_type(content_type):
|
||||
parts = [p.strip() for p in content_type.split(';')]
|
||||
if len(parts) == 1:
|
||||
# No additional attributes provided. Use default encoding.
|
||||
return (content_type, sys.getdefaultencoding())
|
||||
|
||||
# At least one attribute provided. Find out if any of them is named
|
||||
# 'charset' and if so, use it.
|
||||
ctype = parts[0]
|
||||
encoding = [p for p in parts[1:] if p.startswith('charset=') ]
|
||||
if encoding:
|
||||
eq_idx = encoding[0].index('=')
|
||||
return (ctype, encoding[0][eq_idx+1:])
|
||||
else:
|
||||
return (ctype, sys.getdefaultencoding())
|
||||
|
||||
def parse_delimiter(address):
|
||||
withdelim = re.match('^([^\+]+)\+([^@]+)@(.*)$', address)
|
||||
if withdelim:
|
||||
return (withdelim.group(1) + '@' + withdelim.group(3), withdelim.group(2))
|
||||
else:
|
||||
return (address, None)
|
||||
|
||||
def is_pgp_inline(payload):
|
||||
"""Finds out if the payload (bytes) contains PGP/INLINE markers."""
|
||||
return PGP_INLINE_BEGIN in payload and PGP_INLINE_END in payload
|
|
@ -26,7 +26,6 @@ require_once("include/common.php");
|
|||
require_once("include/dbconnect.php");
|
||||
require_once("include/pgp.php");
|
||||
require_once("include/phphkp.php");
|
||||
echo "<link rel='stylesheet' href='theme/style.css' type='text/css'>";
|
||||
|
||||
if(isset($_POST['email']) && isset($_POST['key'])) {
|
||||
$result = requestPGP($_POST['email'], $_POST['key']);
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
@ -19,6 +19,7 @@
|
|||
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
echo "<link rel='stylesheet' href='theme/style.css' type='text/css'>";
|
||||
?>
|
||||
|
||||
<div id=header>
|
||||
|
@ -30,6 +31,7 @@
|
|||
<? if(!empty($message)) { ?>
|
||||
<div id=infomsg><p><b><i><?= htmlspecialchars($message) ?></i></b></p></div>
|
||||
<? } ?>
|
||||
<? if(!empty($fullform)) { ?>
|
||||
<p><?= $lang['home_text'] ?></p>
|
||||
<form id="keyform" method="POST">
|
||||
<p>
|
||||
|
@ -41,13 +43,13 @@
|
|||
<label for="key"><?= $lang['home_keydesc'] ?></label>
|
||||
<br>
|
||||
<!--input type="text" class="key" name="key" required-->
|
||||
<textarea rows="5" cols="80" name="key" id="key" class="key" required></textarea>
|
||||
<textarea rows="5" cols="80" name="key" id="key" class="key"></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" value="<?= $lang['home_submitkey'] ?>" />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<? } ?>
|
||||
</div>
|
||||
|
||||
<div id=footer>
|
|
@ -1,137 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
from configparser import RawConfigParser
|
||||
import email, os, smtplib, sys, traceback, markdown, syslog, requests
|
||||
from M2Crypto import BIO, Rand, SMIME, X509
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
import logging
|
||||
|
||||
import lacre
|
||||
import lacre.config as conf
|
||||
|
||||
def send_msg( message, from_addr, recipients = None ):
|
||||
if conf.config_item_set('relay', 'host') and conf.config_item_set('relay', 'enc_port'):
|
||||
relay = (conf.get_item('relay', 'host'), int(conf.get_item('relay', 'enc_port')))
|
||||
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||
smtp.sendmail( from_addr, recipients, message.as_string() )
|
||||
else:
|
||||
LOG.info("Could not send mail due to wrong configuration")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# try:
|
||||
conf.load_config()
|
||||
lacre.init_logging(conf.get_item('logging', 'config'))
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CERT_PATH = conf.get_item('smime', 'cert_path') + '/'
|
||||
|
||||
# Read e-mail from stdin
|
||||
raw = sys.stdin.read()
|
||||
register_msg = email.message_from_string( raw )
|
||||
from_addr = email.utils.parseaddr(register_msg['From'])[1]
|
||||
|
||||
sign_part = None
|
||||
for msg_part in register_msg.walk():
|
||||
if msg_part.get_content_type().lower() == "application/pkcs7-signature" or msg_part.get_content_type().lower() == "application/x-pkcs7-signature":
|
||||
sign_type = 'smime'
|
||||
sign_part = msg_part
|
||||
break
|
||||
# This may cause that a non ASCII-armored key will be seen as valid. Other solution is not that efficient though
|
||||
#elif msg_part.get_content_type().lower() == "application/pgp-keys":
|
||||
# sign_type = 'pgp'
|
||||
# sign_part = msg_part.get_payload()
|
||||
# break
|
||||
elif "-----BEGIN PGP PUBLIC KEY BLOCK-----" in msg_part.get_payload() and "-----END PGP PUBLIC KEY BLOCK-----" in msg_part.get_payload():
|
||||
msg_content = msg_part.get_payload()
|
||||
start = msg_content.find("-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||
end = msg_content.find("-----END PGP PUBLIC KEY BLOCK-----")
|
||||
sign_type = 'pgp'
|
||||
sign_part = msg_content[start:end + 34]
|
||||
break
|
||||
|
||||
if sign_part == None:
|
||||
LOG.info("Unable to find PKCS7 signature or public PGP key in registration email")
|
||||
|
||||
failure_msg = file( conf.get_item('mailregister', 'mail_templates') + "/registrationError.md").read()
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = conf.get_item('mailregister', 'register_email')
|
||||
msg["To"] = from_addr
|
||||
msg["Subject"] = "S/MIME / OpenPGP registration failed"
|
||||
|
||||
msg.attach(MIMEText(failure_msg, 'plain'))
|
||||
msg.attach(MIMEText(markdown.markdown(failure_msg), 'html'))
|
||||
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
sys.exit(0)
|
||||
|
||||
if sign_type == 'smime':
|
||||
raw_sig = sign_part.get_payload().replace("\n", "")
|
||||
# re-wrap signature so that it fits base64 standards
|
||||
cooked_sig = '\n'.join(raw_sig[pos:pos+76] for pos in range(0, len(raw_sig), 76))
|
||||
|
||||
# now, wrap the signature in a PKCS7 block
|
||||
sig = """
|
||||
-----BEGIN PKCS7-----
|
||||
%s
|
||||
-----END PKCS7-----
|
||||
""" % cooked_sig
|
||||
|
||||
# and load it into an SMIME p7 object through the BIO I/O buffer:
|
||||
buf = BIO.MemoryBuffer(sig)
|
||||
p7 = SMIME.load_pkcs7_bio(buf)
|
||||
|
||||
sk = X509.X509_Stack()
|
||||
signers = p7.get0_signers(sk)
|
||||
signing_cert = signers[0]
|
||||
|
||||
#Save certificate compatible to RFC 2821
|
||||
splitted_from_addr = from_addr.split('@')
|
||||
processed_from_addr = splitted_from_addr[0] + '@' + splitted_from_addr[1].lower()
|
||||
|
||||
signing_cert.save(os.path.join(CERT_PATH, processed_from_addr))
|
||||
|
||||
# format in user-specific data
|
||||
# sending success mail only for S/MIME as GPGMW handles this on its own
|
||||
success_msg = file(conf.get_item('mailregister', 'mail_templates')+"/registrationSuccess.md").read()
|
||||
success_msg = success_msg.replace("[:FROMADDRESS:]", from_addr)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = conf.get_item('mailregister', 'register_email')
|
||||
msg["To"] = from_addr
|
||||
msg["Subject"] = "S/MIME certificate registration succeeded"
|
||||
|
||||
msg.attach(MIMEText(success_msg, 'plain'))
|
||||
msg.attach(MIMEText(markdown.markdown(success_msg), 'html'))
|
||||
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
|
||||
LOG.info("S/MIME Registration succeeded")
|
||||
elif sign_type == 'pgp':
|
||||
# send POST to gpg-mailgate webpanel
|
||||
sig = sign_part
|
||||
payload = {'email': from_addr, 'key': sig}
|
||||
r = requests.post(conf.get_item('mailregister', 'webpanel_url'), data=payload)
|
||||
|
||||
if r.status_code != 200:
|
||||
LOG.info("Could not hand registration over to GPGMW. Error: %s" % r.status_code)
|
||||
error_msg = open(conf.get_item('mailregister', 'mail_templates')+"/gpgmwFailed.md").read()
|
||||
error_msg = error_msg.replace("[:FROMADDRESS:]", from_addr)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = conf.get_item('mailregister', 'register_email')
|
||||
msg["To"] = from_addr
|
||||
msg["Subject"] = "PGP key registration failed"
|
||||
|
||||
msg.attach(MIMEText(error_msg, 'plain'))
|
||||
msg.attach(MIMEText(markdown.markdown(error_msg), 'html'))
|
||||
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
else:
|
||||
LOG.info("PGP registration is handed over to GPGMW")
|
||||
# except:
|
||||
# LOG.info("Registration exception")
|
||||
# sys.exit(0)
|
|
@ -1 +0,0 @@
|
|||
Could not register your PGP key due to a server error. Please contact the administrator.
|
|
@ -1,4 +0,0 @@
|
|||
Could not register a S/MIME certificate or PGP key.
|
||||
|
||||
For S/MIME make sure your message is signed.
|
||||
For PGP make sure you attach your ASCII-armored public key to the email.
|
|
@ -1 +0,0 @@
|
|||
Registration was a success. A signature was found in your message, and your S/MIME certificate was saved.
|
86
test/e2e.ini
86
test/e2e.ini
|
@ -1,86 +0,0 @@
|
|||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# NOTE: We use <key>:<value> syntax, because some values contain
|
||||
# colons and that is default ConfigParser key-value separator.
|
||||
|
||||
[relay]
|
||||
port: 2500
|
||||
script: test/relay.py
|
||||
|
||||
[dirs]
|
||||
keys: test/keyhome
|
||||
certs: test/certs
|
||||
|
||||
[tests]
|
||||
# Number of "test-*" sections in this file, describing test cases.
|
||||
cases: 8
|
||||
e2e_log: test/logs/e2e.log
|
||||
e2e_log_format: %(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s
|
||||
e2e_log_datefmt: %Y-%m-%d %H:%M:%S
|
||||
lacre_log: test/logs/gpg-mailgate.log
|
||||
log_config: test/gpg-lacre-log.ini
|
||||
|
||||
[case-1]
|
||||
descr: Clear text message to a user without a key
|
||||
to: carlos@disposlab
|
||||
in: test/msgin/clear2clear.msg
|
||||
out: Body of the message.
|
||||
|
||||
[case-2]
|
||||
descr: Clear text message to a user with an RSA key
|
||||
to: alice@disposlab
|
||||
in: test/msgin/clear2rsa.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-3]
|
||||
descr: Clear text message to a user with an Ed25519 key
|
||||
to: bob@disposlab
|
||||
in: test/msgin/clear2ed.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-4]
|
||||
descr: Encrypted message to a user with an Ed25519 key
|
||||
to: bob@disposlab
|
||||
in: test/msgin/ed2ed.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-5]
|
||||
descr: Signed message to a user with an Ed25519 key
|
||||
to: bob@disposlab
|
||||
in: test/msgin/signed.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-6]
|
||||
descr: Multipart encrypted message to a user with an Ed25519 key.
|
||||
to: bob@disposlab
|
||||
in: test/msgin/multipart2rsa.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-7]
|
||||
descr: Clear text message to a user with an RSA key and PGP/MIME enabled in configuration
|
||||
to: evan@disposlab
|
||||
in: test/msgin/clear2rsa2.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
||||
|
||||
[case-8]
|
||||
descr: Clear text message to address with delimiter and a user with an Ed25519 key.
|
||||
to: bob@disposlab
|
||||
in: test/msgin/clear2ed-delim.msg
|
||||
out: -----BEGIN PGP MESSAGE-----
|
163
test/e2e_test.py
163
test/e2e_test.py
|
@ -1,163 +0,0 @@
|
|||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import subprocess
|
||||
|
||||
import difflib
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
|
||||
from time import sleep
|
||||
|
||||
RELAY_SCRIPT = "test/relay.py"
|
||||
CONFIG_FILE = "test/gpg-mailgate.conf"
|
||||
|
||||
def build_config(config):
|
||||
cp = configparser.RawConfigParser()
|
||||
|
||||
cp.add_section("logging")
|
||||
cp.set("logging", "config", config["log_config"])
|
||||
|
||||
cp.add_section("gpg")
|
||||
cp.set("gpg", "keyhome", config["gpg_keyhome"])
|
||||
|
||||
cp.add_section("smime")
|
||||
cp.set("smime", "cert_path", config["smime_certpath"])
|
||||
|
||||
cp.add_section("relay")
|
||||
cp.set("relay", "host", "localhost")
|
||||
cp.set("relay", "port", config["port"])
|
||||
|
||||
cp.add_section("enc_keymap")
|
||||
cp.set("enc_keymap", "alice@disposlab", "1CD245308F0963D038E88357973CF4D9387C44D7")
|
||||
cp.set("enc_keymap", "bob@disposlab", "19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67")
|
||||
cp.set("enc_keymap", "evan@disposlab", "530B1BB2D0CC7971648198BBA4774E507D3AF5BC")
|
||||
|
||||
cp.add_section("pgp_style")
|
||||
# Default style is PGP/Inline, so to cover more branches, one test identity
|
||||
# uses PGP/MIME.
|
||||
cp.set("pgp_style", "evan@disposlab", "mime")
|
||||
|
||||
logging.debug(f"Created config with keyhome={config['gpg_keyhome']}, cert_path={config['smime_certpath']} and relay at port {config['port']}")
|
||||
return cp
|
||||
|
||||
def write_test_config(outfile, **config):
|
||||
logging.debug(f"Generating configuration with {config!r}")
|
||||
|
||||
out = open(outfile, "w+")
|
||||
cp = build_config(config)
|
||||
cp.write(out)
|
||||
out.close()
|
||||
|
||||
logging.debug(f"Wrote configuration to {outfile}")
|
||||
|
||||
def load_file(name):
|
||||
f = open(name, 'r')
|
||||
contents = f.read()
|
||||
f.close()
|
||||
|
||||
return bytes(contents, 'utf-8')
|
||||
|
||||
def report_result(message_file, expected, test_output):
|
||||
status = None
|
||||
if expected in test_output:
|
||||
status = "Success"
|
||||
else:
|
||||
status = "Failure"
|
||||
|
||||
print(message_file.ljust(30), status)
|
||||
|
||||
def execute_e2e_test(case_name, config, config_path):
|
||||
"""Read test case configuration from config and run that test case.
|
||||
|
||||
Parameter case_name should refer to a section in test
|
||||
config file. Each of these sections should contain
|
||||
following properties: 'descr', 'to', 'in' and 'out'.
|
||||
"""
|
||||
# This environment variable is set in Makefile.
|
||||
python_path = os.getenv('PYTHON', 'python3')
|
||||
|
||||
gpglacre_cmd = [python_path,
|
||||
"gpg-mailgate.py",
|
||||
config.get(case_name, "to")]
|
||||
|
||||
relay_cmd = [python_path,
|
||||
config.get("relay", "script"),
|
||||
config.get("relay", "port")]
|
||||
|
||||
logging.debug(f"Spawning relay: {relay_cmd}")
|
||||
relay_proc = subprocess.Popen(relay_cmd,
|
||||
stdin = None,
|
||||
stdout = subprocess.PIPE)
|
||||
|
||||
logging.debug(f"Spawning GPG-Lacre: {gpglacre_cmd}, stdin = {config.get(case_name, 'in')}")
|
||||
|
||||
# pass PATH because otherwise it would be dropped
|
||||
gpglacre_proc = subprocess.run(gpglacre_cmd,
|
||||
input = load_file(config.get(case_name, "in")),
|
||||
capture_output = True,
|
||||
env = {"GPG_MAILGATE_CONFIG": config_path,
|
||||
"PATH": os.getenv("PATH")})
|
||||
|
||||
# Let the relay process the data.
|
||||
relay_proc.wait()
|
||||
|
||||
(testout, _) = relay_proc.communicate()
|
||||
testout = testout.decode('utf-8')
|
||||
|
||||
logging.debug(f"Read {len(testout)} characters of test output: '{testout}'")
|
||||
|
||||
report_result(config.get(case_name, "in"), config.get(case_name, "out"), testout)
|
||||
|
||||
def load_test_config():
|
||||
cp = configparser.ConfigParser()
|
||||
cp.read("test/e2e.ini")
|
||||
|
||||
return cp
|
||||
|
||||
|
||||
config = load_test_config()
|
||||
|
||||
logging.basicConfig(filename = config.get("tests", "e2e_log"),
|
||||
# Get raw values of log and date formats because they
|
||||
# contain %-sequences and we don't want them to be expanded
|
||||
# by the ConfigParser.
|
||||
format = config.get("tests", "e2e_log_format", raw=True),
|
||||
datefmt = config.get("tests", "e2e_log_datefmt", raw=True),
|
||||
level = logging.DEBUG)
|
||||
|
||||
config_path = os.getcwd() + "/" + CONFIG_FILE
|
||||
|
||||
write_test_config(config_path,
|
||||
port = config.get("relay", "port"),
|
||||
gpg_keyhome = config.get("dirs", "keys"),
|
||||
smime_certpath = config.get("dirs", "certs"),
|
||||
log_config = config.get("tests", "log_config"))
|
||||
|
||||
for case_no in range(1, config.getint("tests", "cases")+1):
|
||||
case_name = f"case-{case_no}"
|
||||
logging.info(f"Executing {case_name}: {config.get(case_name, 'descr')}")
|
||||
|
||||
execute_e2e_test(case_name, config, config_path)
|
||||
|
||||
print("See diagnostic output for details. Tests: '%s', Lacre: '%s'" % (config.get("tests", "e2e_log"), config.get("tests", "lacre_log")))
|
|
@ -1,24 +0,0 @@
|
|||
[loggers]
|
||||
keys=root
|
||||
|
||||
[logger_root]
|
||||
level=NOTSET
|
||||
handlers=lacrelog
|
||||
|
||||
[handlers]
|
||||
keys=lacrelog
|
||||
|
||||
[formatters]
|
||||
keys=postfixfmt
|
||||
|
||||
[handler_lacrelog]
|
||||
class=FileHandler
|
||||
level=DEBUG
|
||||
formatter=postfixfmt
|
||||
args=('test/logs/lacre.log', 'a+')
|
||||
|
||||
[formatter_postfixfmt]
|
||||
format=%(asctime)s %(module)s[%(process)d]: %(message)s
|
||||
datefmt=%b %e %H:%M:%S
|
||||
style=%
|
||||
validate=True
|
|
@ -1,27 +0,0 @@
|
|||
[logging]
|
||||
config = test/gpg-lacre-log.ini
|
||||
file = test/logs/gpg-mailgate.log
|
||||
format = %(asctime)s %(module)s[%(process)d]: %(message)s
|
||||
date_format = ISO
|
||||
|
||||
[gpg]
|
||||
keyhome = test/keyhome
|
||||
|
||||
[smime]
|
||||
cert_path = test/certs
|
||||
|
||||
[database]
|
||||
enabled = yes
|
||||
url = sqlite:///test/lacre.db
|
||||
|
||||
[relay]
|
||||
host = localhost
|
||||
port = 2500
|
||||
|
||||
[cron]
|
||||
send_email = no
|
||||
|
||||
[enc_keymap]
|
||||
alice@disposlab = 1CD245308F0963D038E88357973CF4D9387C44D7
|
||||
bob@disposlab = 19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67
|
||||
|
|
@ -1 +0,0 @@
|
|||
v:1:
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Carlos <carlos@localhost>
|
||||
Subject: Test
|
||||
|
||||
Body of the message.
|
|
@ -1,5 +0,0 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Bob <bob+foobar@localhost>
|
||||
Subject: Test
|
||||
|
||||
Body of the message.
|
|
@ -1,5 +0,0 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Bob <bob@localhost>
|
||||
Subject: Test
|
||||
|
||||
Body of the message.
|
|
@ -1,5 +0,0 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Alice <alice@localhost>
|
||||
Subject: Test
|
||||
|
||||
Body of the message.
|
|
@ -1,6 +0,0 @@
|
|||
From: Dave <dave@disposlab
|
||||
To: Evan <evan@disposlab>
|
||||
Subject: Test
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
|
||||
Body of the message.
|
|
@ -1,13 +0,0 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Bob <bob@localhost>
|
||||
Subject: Test
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hF4DujWCoRS24dYSAQdAyGDF9Us11JDr8+XPmvlJHsMS7A4UBIcCiresJyZpSxYw
|
||||
Cqcugy5AX5fgSAiL1Cd2b1zpQ/rYdTWkFYMVbH4jBEoPC3z/aSd+hTnneJFDUdXl
|
||||
0koBDIw7NQylu6SrW+Y/DmXgalIHtwACuKivJTq/z9jdwFScV7adRR/VO53Inah3
|
||||
L1+Ho7Zta95AYW3UPu71Gw3rrkfjY4uGDiFAFg==
|
||||
=yTzD
|
||||
-----END PGP MESSAGE-----
|
|
@ -1,43 +0,0 @@
|
|||
Date: Sun, 18 Jul 2021 16:53:45 +0200
|
||||
From: User Alice <alice@disposlab>
|
||||
To: User Bob <bob@disposlab>
|
||||
Subject: encrypted
|
||||
Message-ID: <YPRAeEEc3z2M9BCy@disposlab>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="95hZs/zeBetwhuEy"
|
||||
Content-Disposition: inline
|
||||
Status: RO
|
||||
Content-Length: 1140
|
||||
Lines: 30
|
||||
|
||||
|
||||
--95hZs/zeBetwhuEy
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Disposition: attachment
|
||||
|
||||
Version: 1
|
||||
|
||||
--95hZs/zeBetwhuEy
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename="msg.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQGMA/vsqjpkmZurAQwAnb+2kDPgVFWVLkafuzVJGqFWKNtdVsvk7I1zhzFw5Hsr
|
||||
h4irSHcH0X0QjaHprNiMBDfIZaCx5VVsvGYLiu/iQkdVPXItugTpln8aAvDt8/Bp
|
||||
Hse69tgG5S9o4fPK4K2bMjNdomclDdz51cu9NXYjk/6OtzVwcSypyEmxgw24Oo1+
|
||||
Q8KfZN9n6VTXGNlrV9KnAZYs/5aaSABTeC+cDvOcjDbPAmwDHYS3qsbITYoGHnEz
|
||||
QfPIakYWPtPWkajhm4Z/iyEUSTeqew1/gAJ8sZnJpV0eg1Cr/44XgklZKFr8aJgk
|
||||
SG8PkQxsyzAZklpwMSWdbb+t9a5nEKvky3zMpdmS1GE7ubTO7nQ1geUdBiv1UUNh
|
||||
BY9d4nlGirqxX1MZUTGZidJgCy0365xbJSKkU0yFFW2uWtCKzJTEQBk3YZkNmnGH
|
||||
h8BiVvMhQ8SxKBRPeH6Zb6HHlbcgkPvJAAI4VLqkZPCBvp9irmcdFGmrgCWLxzgk
|
||||
sIjYGLA+ZuSXOKuAssXE0sAbASPAkUJRTIjzXFrCnr/MB3ZonESH01fsbsX+E/Qi
|
||||
+2oLrgjjPHcPq76fvdO6fJP6c1pM8TlOoZKn/RkPm1llULtOn4n5JZJjeUA0F2ID
|
||||
Te/U9i4YtcFZbuvw2bjeu8sAf77U6O3iTTBWkPWQT3H4YMskQc7lS1Mug6A9HL/n
|
||||
TQvAwh2MIveYyEy/y/dKeFUbpSKxyOInhTg1XtYFiT8bzEF7OEJLU9GyF5oMs67d
|
||||
o12uYlEnPhWz9oZp11aSdnyeADpVu6BQsPbwfTifcpajQSarH5sG8+rDSPju
|
||||
=7CnH
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--95hZs/zeBetwhuEy--
|
|
@ -1,39 +0,0 @@
|
|||
Date: Sun, 18 Jul 2021 12:08:41 +0200
|
||||
From: User Alice <alice@disposlab>
|
||||
To: User Bob <bob@disposlab>
|
||||
Subject: signed
|
||||
Message-ID: <YPP9qer9j2u4qXsq@disposlab>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/signed; micalg=pgp-sha256;
|
||||
protocol="application/pgp-signature"; boundary="U/XjR71RAixRcb28"
|
||||
Content-Disposition: inline
|
||||
Status: RO
|
||||
Content-Length: 870
|
||||
Lines: 26
|
||||
|
||||
|
||||
--U/XjR71RAixRcb28
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Disposition: inline
|
||||
|
||||
A signed msg.
|
||||
|
||||
--U/XjR71RAixRcb28
|
||||
Content-Type: application/pgp-signature; name="signature.asc"
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAEBCAAdFiEEHNJFMI8JY9A46INXlzz02Th8RNcFAmDz/aUACgkQlzz02Th8
|
||||
RNdtOQv/ca8c51KoVq7CyPJUr54n4DEk/LlYniR0W51tL2a4rQxyF2AxqjdI8T4u
|
||||
bT1+bqPNYgegesyCLokeZKqhLVtCH+UVOTdtUq5bB1J7ALuuVTOIdR5woMBBsazV
|
||||
ETYEMzL6y2sGPW92ynriEw6B9pPnFKFPhOOZLrnMzM8CpkTfNmGoej+EdV74s0z4
|
||||
RayKu/WaZ1Dtx2Vy2YDtG36p/Y3n62bnzQJCRyPYfrmCxH5X5i5oibQwxLROCFNE
|
||||
4X3iVZLPHFg/DS9m4L7mBe0MJewGa1oPFr7t3ZfJ+24aJ/AvUv5uQIO+s6a7AcjD
|
||||
Pgw/IjeM/uZdPrzniZI2zsWEgsjRCL1fj49XWVNkTHrWCqLvkBg+suucNO2SR0/d
|
||||
ps+RP5mkJJHaSZyPpxwo9/PHKX67Mkpn/uEXlE8nV6IqKoXRzr1N0qwyhvbZQZLD
|
||||
FMumxx/eOSiOpaiRhGhoZiUpf+VdnV/1ClpAcdbthy/psx/CMYVblAM8xg74NR9+
|
||||
Q/WlFbRl
|
||||
=uMdE
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--U/XjR71RAixRcb28--
|
104
test/relay.py
104
test/relay.py
|
@ -1,104 +0,0 @@
|
|||
#!/usr/local/bin/python2
|
||||
#
|
||||
# This quick-and-dirty script supports only the happy case of SMTP session,
|
||||
# i.e. what gpg-mailgate/gpg-lacre needs to deliver encrypted email.
|
||||
#
|
||||
# It listens on the port given as the only command-line argument and consumes a
|
||||
# message, then prints it to standard output. The goal is to be able to
|
||||
# compare that output with expected clear-text or encrypted message body.
|
||||
#
|
||||
|
||||
import sys
|
||||
import socket
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
EXIT_UNAVAILABLE = 1
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
|
||||
BUFFER_SIZE = 4096
|
||||
EOM = "\r\n.\r\n"
|
||||
LAST_LINE = -3
|
||||
|
||||
|
||||
def welcome(msg):
|
||||
return b"220 %b\r\n" % (msg)
|
||||
|
||||
def ok(msg = b"OK"):
|
||||
return b"250 %b\r\n" % (msg)
|
||||
|
||||
def bye():
|
||||
return b"251 Bye"
|
||||
|
||||
def provide_message():
|
||||
return b"354 Enter a message, ending it with a '.' on a line by itself\r\n"
|
||||
|
||||
def receive_and_confirm(session):
|
||||
session.recv(BUFFER_SIZE)
|
||||
session.sendall(ok())
|
||||
|
||||
def localhost_at(port):
|
||||
return ('127.0.0.1', port)
|
||||
|
||||
def serve(port):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.bind(localhost_at(port))
|
||||
logging.info(f"Listening on localhost, port {port}")
|
||||
s.listen(1)
|
||||
logging.info("Listening...")
|
||||
except socket.error as e:
|
||||
print("Cannot connect", e)
|
||||
logging.error(f"Cannot connect {e}")
|
||||
sys.exit(EXIT_UNAVAILABLE)
|
||||
|
||||
logging.debug("About to accept a connection...")
|
||||
(conn, addr) = s.accept()
|
||||
logging.debug(f"Accepting connection from {conn}")
|
||||
conn.sendall(welcome(b"TEST SERVER"))
|
||||
|
||||
receive_and_confirm(conn) # Ignore HELO/EHLO
|
||||
receive_and_confirm(conn) # Ignore sender address
|
||||
receive_and_confirm(conn) # Ignore recipient address
|
||||
|
||||
data = conn.recv(BUFFER_SIZE)
|
||||
conn.sendall(provide_message())
|
||||
|
||||
# Consume until we get <CR><LF>.<CR><LF>, the end-of-message marker.
|
||||
message = ''
|
||||
while not message.endswith(EOM):
|
||||
message += conn.recv(BUFFER_SIZE).decode(ENCODING)
|
||||
conn.sendall(ok(b"OK, id=test"))
|
||||
|
||||
conn.recv(BUFFER_SIZE)
|
||||
conn.sendall(bye())
|
||||
|
||||
conn.close()
|
||||
|
||||
logging.debug(f"Received {len(message)} characters of data")
|
||||
|
||||
# Trim EOM marker as we're only interested in the message body.
|
||||
return message[:-len(EOM)]
|
||||
|
||||
def error(msg, exit_code):
|
||||
logging.error(msg)
|
||||
print("ERROR: %s" % (msg))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
# filename is relative to where we run the tests from, i.e. the project root
|
||||
# directory
|
||||
logging.basicConfig(filename = 'test/logs/relay.log',
|
||||
format = '%(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s',
|
||||
datefmt = '%Y-%m-%d %H:%M:%S',
|
||||
level = logging.DEBUG)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
error("Usage: relay.py PORT_NUMBER", EXIT_UNAVAILABLE)
|
||||
|
||||
port = int(sys.argv[1])
|
||||
body = serve(port)
|
||||
|
||||
print(body)
|
|
@ -1,3 +0,0 @@
|
|||
[foo]
|
||||
bar: quux
|
||||
baz: 14
|
|
@ -1,91 +0,0 @@
|
|||
import sys
|
||||
import sqlalchemy
|
||||
from sqlalchemy.sql import insert
|
||||
|
||||
def define_db_schema():
|
||||
meta = sqlalchemy.MetaData()
|
||||
|
||||
gpgmw_keys = sqlalchemy.Table('gpgmw_keys', meta,
|
||||
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True),
|
||||
sqlalchemy.Column('email', sqlalchemy.String(256)),
|
||||
sqlalchemy.Column('publickey', sqlalchemy.Text),
|
||||
sqlalchemy.Column('confirm', sqlalchemy.String(32)),
|
||||
sqlalchemy.Column('status', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('time', sqlalchemy.DateTime))
|
||||
|
||||
return (meta, gpgmw_keys)
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("ERROR: output database missing")
|
||||
sys.exit(1)
|
||||
|
||||
(meta, gpgmw_keys) = define_db_schema()
|
||||
|
||||
dbname = sys.argv[1]
|
||||
test_db = sqlalchemy.create_engine(f"sqlite:///{dbname}")
|
||||
|
||||
# Initialise the schema
|
||||
meta.create_all(test_db)
|
||||
|
||||
conn = test_db.connect()
|
||||
|
||||
# Populate the database with dummy data
|
||||
conn.execute(gpgmw_keys.insert(), [
|
||||
{"id": 1, "email": "alice@disposlab", "publickey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\
|
||||
\n\
|
||||
mQGNBGDYY5oBDAC+HAVjA05jsIpHfQ2KQ9m2olo1Qnlk+dkjD+Gagxj1ACezyiGL\n\
|
||||
cfZfoE/MJYLCH9yPcX1fUIAPwdAyfJKlvkVcz+MhEpgl3aP3NM2L2unSx3v9ZFwT\n\
|
||||
/qyMo9Zst5VSD04TVx2ySQB1vucd2ppgp66X7hlCxs+P8d0FV7VcdrNYol2oOtYP\n\
|
||||
yEFXkdyXLI/INI6jrqNkBF87ej+dlTQZAm3zoj61Xwq4gW0YesAZoJyXs8X+a4Am\n\
|
||||
8KF7YYcTcIy89yXflotmExpE+i77datSBLM/FpIPiUfkfK6q/TNyno8Z3PBC0QD5\n\
|
||||
21leqfp/QHRkwmqFbIVuoeonCvrAccjM0ITLjW+P0xXJa3q0lQQCgcGOgqTuNWPT\n\
|
||||
6FhlmvkXt6fBZ11C2I1b033HTePvjIwxOrEY8pSqYwerVX9EU7FXT+S98HNW/1nF\n\
|
||||
cNk3SoofzUOcKZOwc5n0NEESrW7sWpmD6Qmf52+GURuO+15DSUt13xqmnte19Xqd\n\
|
||||
n98y0wrYAUgyUY8AEQEAAbQPYWxpY2VAZGlzcG9zbGFiiQHUBBMBCAA+FiEEHNJF\n\
|
||||
MI8JY9A46INXlzz02Th8RNcFAmDYY5oCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYC\n\
|
||||
AwECHgECF4AACgkQlzz02Th8RNdZeAv+IVVK49f0tY5QOSERu5RqdyFNpsVlUws9\n\
|
||||
swvSvXXK/ZQxZ3YD3o0WEJG5G8jRO+Zjrljx6zzH39ofEKn8QMQUuw+SVPrzbqQb\n\
|
||||
Yp/idn1E9RZCyyhtwcYnIwUObq2NNsCk8UmnjYvpwoh/QcHic13/RSUj7vejujtB\n\
|
||||
SRTjNUE/RK5ROY8r+xZW9ZV/Q0NEzKl2wQtmbt8vTRX9yNEB171XZHG7dg4bTzm+\n\
|
||||
zs0jPGNT0ygcx+uE7DZ3RkyPLRk3fB+GPiYrL2lfPF1KkrHGY4PGhClKdR1kjfBA\n\
|
||||
Kweb6ExZg0fBYlB8ia8z3RZQF29pztoVfk8KIimg9RoYNOKw3Jp5SnHsbz9JygmZ\n\
|
||||
mp3M3Lrs7357oSn9x25/nrFGeUBWbbKoXSdoXZr0Ix4xxkOJPAK966w0pQq+sP+o\n\
|
||||
Ozg3F2rFRc6SoQw1pNLQ57hhWTblQlz8ETY7GnVJ+0xiqkAq2hrLt0jhQ5taWjV6\n\
|
||||
Fgy8fKUPd5OAMvB9bfmAErclWcqKarMcuQGNBGDYY5oBDAC6yOtgUwtKUsI3jTu2\n\
|
||||
VdjNDEnt/VLdRseT4JosSMglZ963nlA4mltCjxj59DeM0Ft8eyF7Bu4EFw5Kid+O\n\
|
||||
vKGA5rGZBE0IVROOvSJQNbcELkY9XYtZjOJ7elfG37rDQKfDk82xqod9iTd48nm7\n\
|
||||
vrllvylQhKfXa+m99KxWabtKqCyXVjaZP9vfD3nVauu16oHW6rQavlLXo5MetFan\n\
|
||||
Iwv1sTqnpzCt+cuG/7vUt89rOiJRalRP3/e1K5MSM6aWC/SHZs6HcrT+WT5nuPA+\n\
|
||||
5VQ4gFCSb8UlscF4sI++hhB/k821vyl9hIjnR3aRiFWdrkykQOfZNhovvsnmJmk9\n\
|
||||
+Zcq0M3pZBnBuLgxVwJNVa4gi63cYwtExpcAZcG28wSVmcXcPN2wxEpYg5n/nvvG\n\
|
||||
8Dsk0AA5WU5WW8aLLLQNBmVg2y4Oa1Fy0M7yfSylLWBAdj7y8+UzspN6JCbYhOpP\n\
|
||||
lLRCJv5+JOgR0MrA+lxfFZwfcSO12x+gkfQ9oyUBdXNuydMAEQEAAYkBtgQYAQgA\n\
|
||||
IBYhBBzSRTCPCWPQOOiDV5c89Nk4fETXBQJg2GOaAhsMAAoJEJc89Nk4fETXpooL\n\
|
||||
/iJKgNF80neUamewma1aZJjwKWoHysSWWSlPeU6pGctuJv15fbAfI/NM1iXnSEGt\n\
|
||||
odsn0oHtuAASlVB0ckSFdE0a2DwLgO6s6oEJof/yrE5hIAAlwzjHsi1G/dtHcfIo\n\
|
||||
SjHzE22qUZwwm5ketuvKvEDKKp3b1ccu37AZC1caRFh3q8xB5ByLh1gPiDJ+ehwU\n\
|
||||
puXkXPdFQhQTZib4LYuMxzh6A+S9U0AM7WMKjX7PhJ68maOeQ+yOIBSWtBKyWwZu\n\
|
||||
Sx01w+Y/USPz02AxUn102se52FCISc/NijlX1JvFQdzf/WaZu28nTmW9OXSW3WeK\n\
|
||||
ql7zNQqj494JD8gJuRGCU9AaiCmOaBokRdLiGbin/wxiG1CkXGRDN5/r0m/1IoNz\n\
|
||||
I4m2SLsB/a89WACQ//CKJyNn4xPOEQoix35tXjdjTLAVyTrX502vHGieZ3HJU2tb\n\
|
||||
nmmMf/H0kReMtNYFwHxoTpBJ8vk+xcZ+6ETzH8nk6av+zZ/5T5Y0aD5zO89PcQk6\n\
|
||||
pw==\n\
|
||||
=Tbwz\n\
|
||||
-----END PGP PUBLIC KEY BLOCK-----\
|
||||
", "status": 0, "confirm": "", "time": None},
|
||||
{"id": 2, "email": "bob@disposlab", "publickey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\
|
||||
\n\
|
||||
mDMEYdTFkRYJKwYBBAHaRw8BAQdA2tgdP1pMt3cv3XAW7ov5AFn74mMZvyTksp9Q\n\
|
||||
eO1PkpK0GkJvYiBGb29iYXIgPGJvYkBkaXNwb3NsYWI+iJYEExYIAD4WIQQZz0tH\n\
|
||||
7MnEevqE1L2W85/aDjG7ZwUCYdTFkQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgID\n\
|
||||
AQIeAQIXgAAKCRCW85/aDjG7ZxVnAP49t7BU2H+/WCpa3fCAlMEcik82sU4p+U9D\n\
|
||||
pMsbjawwYgEA1SbA5CF835cMjoEufy1h+2M4T9gI/0X2lk8OAtwwggm4OARh1MXg\n\
|
||||
EgorBgEEAZdVAQUBAQdAUVNKx2OsGtNdRsnl3J/uv6obkUC0KcO4ikdRs+iejlMD\n\
|
||||
AQgHiHgEGBYIACAWIQQZz0tH7MnEevqE1L2W85/aDjG7ZwUCYdTF4AIbDAAKCRCW\n\
|
||||
85/aDjG7Z039APwLGP5ibqCC9yIr4YVbdWff1Ch+2C91MR2ObF93Up9+ogD8D2zd\n\
|
||||
OjjB6xRD0Q2FN+alsNGCtdutAs18AZ5l33RMzws=\n\
|
||||
=wWoq\n\
|
||||
-----END PGP PUBLIC KEY BLOCK-----\
|
||||
", "status": 0, "confirm": "", "time": None},
|
||||
{"id": 3, "email": "cecil@lacre.io", "publickey": "RUBBISH", "status": 0, "confirm": "", "time": None}
|
||||
])
|
|
@ -1,92 +0,0 @@
|
|||
#
|
||||
# gpg-mailgate
|
||||
#
|
||||
# This file is part of the gpg-mailgate source code.
|
||||
#
|
||||
# gpg-mailgate is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gpg-mailgate source code is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""Unit-tests as contracts for external dependencies.
|
||||
|
||||
Unit tests defined here are our contracts for the dependencies used by Lacre.
|
||||
Since not all software is documented thoroughly, they are also a form of
|
||||
documentation.
|
||||
"""
|
||||
|
||||
import email
|
||||
import unittest
|
||||
from configparser import RawConfigParser
|
||||
|
||||
class EmailParsingTest(unittest.TestCase):
|
||||
"""This test serves as a package contract and documentation of its behaviour."""
|
||||
|
||||
def test_message_from_bytes_produces_message_with_str_headers(self):
|
||||
rawmsg = b"From: alice@lacre.io\r\n" \
|
||||
+ b"To: bob@lacre.io\r\n" \
|
||||
+ b"Subject: Test message\r\n" \
|
||||
+ b"\r\n" \
|
||||
+ b"Test message from Alice to Bob.\r\n"
|
||||
|
||||
parsed = email.message_from_bytes(rawmsg)
|
||||
|
||||
self.assertEqual(parsed["From"], "alice@lacre.io")
|
||||
self.assertEqual(parsed["To"], "bob@lacre.io")
|
||||
self.assertEqual(parsed["Subject"], "Test message")
|
||||
|
||||
def test_bytes_message_payload_decoded_produces_bytes(self):
|
||||
rawmsg = b"From: alice@lacre.io\r\n" \
|
||||
+ b"To: bob@lacre.io\r\n" \
|
||||
+ b"Subject: Test message\r\n" \
|
||||
+ b"\r\n" \
|
||||
+ b"Test message from Alice to Bob.\r\n"
|
||||
|
||||
parsed = email.message_from_bytes(rawmsg)
|
||||
|
||||
self.assertEqual(parsed.get_payload(), "Test message from Alice to Bob.\r\n")
|
||||
self.assertEqual(parsed.get_payload(decode=True), b"Test message from Alice to Bob.\r\n")
|
||||
|
||||
def test_message_from_string_produces_message_with_str_headers(self):
|
||||
rawmsg = "From: alice@lacre.io\r\n" \
|
||||
+ "To: bob@lacre.io\r\n" \
|
||||
+ "Subject: Test message\r\n" \
|
||||
+ "\r\n" \
|
||||
+ "Test message from Alice to Bob.\r\n"
|
||||
|
||||
parsed = email.message_from_string(rawmsg)
|
||||
|
||||
self.assertEqual(parsed["From"], "alice@lacre.io")
|
||||
self.assertEqual(parsed["To"], "bob@lacre.io")
|
||||
self.assertEqual(parsed["Subject"], "Test message")
|
||||
|
||||
def test_str_message_payload_decoded_produces_bytes(self):
|
||||
rawmsg = "From: alice@lacre.io\r\n" \
|
||||
+ "To: bob@lacre.io\r\n" \
|
||||
+ "Subject: Test message\r\n" \
|
||||
+ "\r\n" \
|
||||
+ "Test message from Alice to Bob.\r\n"
|
||||
|
||||
parsed = email.message_from_string(rawmsg)
|
||||
|
||||
self.assertEqual(parsed.get_payload(), "Test message from Alice to Bob.\r\n")
|
||||
self.assertEqual(parsed.get_payload(decode=True), b"Test message from Alice to Bob.\r\n")
|
||||
|
||||
class RawConfigParserTest(unittest.TestCase):
|
||||
def test_config_parser_returns_str(self):
|
||||
cp = RawConfigParser()
|
||||
cp.read("test/sample.ini")
|
||||
self.assertEqual(cp.get("foo", "bar"), "quux")
|
||||
self.assertEqual(cp.get("foo", "baz"), "14")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,16 +0,0 @@
|
|||
import GnuPG
|
||||
|
||||
import unittest
|
||||
|
||||
class GnuPGUtilitiesTest(unittest.TestCase):
|
||||
def test_build_default_command(self):
|
||||
cmd = GnuPG.build_command("test/keyhome")
|
||||
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome"])
|
||||
|
||||
def test_build_command_extended_with_args(self):
|
||||
cmd = GnuPG.build_command("test/keyhome", "--foo", "--bar")
|
||||
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome", "--foo", "--bar"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,37 +0,0 @@
|
|||
import lacre.text
|
||||
import sys
|
||||
|
||||
import unittest
|
||||
|
||||
class LacreTextTest(unittest.TestCase):
|
||||
def test_parse_content_type_without_charset(self):
|
||||
(mtype, mcharset) = lacre.text.parse_content_type('text/plain')
|
||||
self.assertEqual(mtype, 'text/plain')
|
||||
self.assertEqual(mcharset, sys.getdefaultencoding())
|
||||
|
||||
def test_parse_content_type_with_charset(self):
|
||||
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; charset="UTF-8"')
|
||||
self.assertEqual(mtype, 'text/plain')
|
||||
self.assertEqual(mcharset, '"UTF-8"')
|
||||
|
||||
def test_parse_content_type_with_other_attributes(self):
|
||||
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; some-param="Some Value"')
|
||||
self.assertEqual(mtype, 'text/plain')
|
||||
self.assertEqual(mcharset, sys.getdefaultencoding())
|
||||
|
||||
def test_parse_content_type_with_several_attributes(self):
|
||||
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; charset="UTF-8"; some-param="Some Value"')
|
||||
self.assertEqual(mtype, 'text/plain')
|
||||
self.assertEqual(mcharset, '"UTF-8"')
|
||||
|
||||
def test_parse_email_without_delimiter(self):
|
||||
addr = "Some.Name@example.com"
|
||||
(addr2, topic) = lacre.text.parse_delimiter(addr)
|
||||
self.assertEqual(addr2, "Some.Name@example.com")
|
||||
self.assertEqual(topic, None)
|
||||
|
||||
def test_parse_email_with_delimiter(self):
|
||||
addr = "Some.Name+some-topic@example.com"
|
||||
(addr2, topic) = lacre.text.parse_delimiter(addr)
|
||||
self.assertEqual(addr2, "Some.Name@example.com")
|
||||
self.assertEqual(topic, "some-topic")
|
Loading…
Reference in a new issue