Pretty Good Privacy (PGP)
-
+
By importing your PGP Public Key into SimpleLogin, all emails sent to {{ mailbox.email }} are
encrypted with your key.
@@ -106,6 +117,45 @@
+
+
Advanced Options
diff --git a/app/dashboard/views/mailbox_detail.py b/app/dashboard/views/mailbox_detail.py
index 2df0e4fb..34fa95d6 100644
--- a/app/dashboard/views/mailbox_detail.py
+++ b/app/dashboard/views/mailbox_detail.py
@@ -151,6 +151,29 @@ def mailbox_detail_route(mailbox_id):
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
+ elif request.form.get("form-name") == "generic-subject":
+ if request.form.get("action") == "save":
+ if not mailbox.pgp_finger_print:
+ flash(
+ "Generic subject can only be used on PGP-enabled mailbox", "error"
+ )
+ return redirect(
+ url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
+ )
+
+ mailbox.generic_subject = request.form.get("generic-subject")
+ db.session.commit()
+ flash("Generic subject for PGP-encrypted email is enabled", "success")
+ return redirect(
+ url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
+ )
+ elif request.form.get("action") == "remove":
+ mailbox.generic_subject = None
+ db.session.commit()
+ flash("Generic subject for PGP-encrypted email is disabled", "success")
+ return redirect(
+ url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
+ )
spf_available = ENFORCE_SPF
return render_template("dashboard/mailbox_detail.html", **locals())
From e659680875376fbd1f97aa220a61138a1a8b55f0 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 7 Nov 2020 13:00:12 +0100
Subject: [PATCH 3/6] add_header()
---
app/email_utils.py | 37 +++++++++++++++++++
tests/test_email_utils.py | 76 +++++++++++++++++++++++++++++++++++++++
2 files changed, 113 insertions(+)
diff --git a/app/email_utils.py b/app/email_utils.py
index 0b98cb73..9010dfba 100644
--- a/app/email_utils.py
+++ b/app/email_utils.py
@@ -653,3 +653,40 @@ def is_valid_email(email_address: str) -> bool:
return validate_email(
email_address=email_address, check_mx=False, use_blacklist=False
)
+
+
+def add_header(msg: Message, text_header, html_header) -> Message:
+ if msg.get_content_type() == "text/plain":
+ payload = msg.get_payload()
+ if type(payload) is str:
+ clone_msg = copy(msg)
+ payload = f"{text_header}\n---\n{payload}"
+ clone_msg.set_payload(payload)
+ return clone_msg
+ elif msg.get_content_type() == "text/html":
+ payload = msg.get_payload()
+ if type(payload) is str:
+
+ new_payload = f"""
+
+
+ {html_header} |
+
+
+ {payload} |
+
+
+ """
+ clone_msg = copy(msg)
+ clone_msg.set_payload(new_payload)
+ return clone_msg
+ elif msg.get_content_type() in ("multipart/alternative", "multipart/related"):
+ new_parts = []
+ for part in msg.get_payload():
+ new_parts.append(add_header(part, text_header, html_header))
+ clone_msg = copy(msg)
+ clone_msg.set_payload(new_parts)
+ return clone_msg
+
+ LOG.d("No header added for %s", msg.get_content_type())
+ return msg
diff --git a/tests/test_email_utils.py b/tests/test_email_utils.py
index fea50210..15478c02 100644
--- a/tests/test_email_utils.py
+++ b/tests/test_email_utils.py
@@ -14,6 +14,7 @@ from app.email_utils import (
get_spam_from_header,
get_header_from_bounce,
is_valid_email,
+ add_header,
)
from app.extensions import db
from app.models import User, CustomDomain
@@ -293,3 +294,78 @@ def test_is_valid_email():
assert not is_valid_email("with space@gmail.com")
assert not is_valid_email("strange char !ç@gmail.com")
assert not is_valid_email("emoji👌@gmail.com")
+
+
+def test_add_header_plain_text():
+ msg = email.message_from_string(
+ """Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Test-Header: Test-Value
+
+coucou
+"""
+ )
+ new_msg = add_header(msg, "text header", "html header")
+ assert "text header" in new_msg.as_string()
+ assert "html header" not in new_msg.as_string()
+
+
+def test_add_header_html():
+ msg = email.message_from_string(
+ """Content-Type: text/html; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Test-Header: Test-Value
+
+
+
+
+
+
+
bold
+
+
+"""
+ )
+ new_msg = add_header(msg, "text header", "html header")
+ assert "Test-Header: Test-Value" in new_msg.as_string()
+ assert "
" in new_msg.as_string()
+ assert "html header" in new_msg.as_string()
+ assert "text header" not in new_msg.as_string()
+
+
+def test_add_header_multipart_alternative():
+ msg = email.message_from_string(
+ """Content-Type: multipart/alternative;
+ boundary="foo"
+Content-Transfer-Encoding: 7bit
+Test-Header: Test-Value
+
+--foo
+Content-Transfer-Encoding: 7bit
+Content-Type: text/plain;
+ charset=us-ascii
+
+bold
+
+--foo
+Content-Transfer-Encoding: 7bit
+Content-Type: text/html;
+ charset=us-ascii
+
+
+
+
+
+
+bold
+
+
+"""
+ )
+ new_msg = add_header(msg, "text header", "html header")
+ assert "Test-Header: Test-Value" in new_msg.as_string()
+ assert "" in new_msg.as_string()
+ assert "html header" in new_msg.as_string()
+ assert "text header" in new_msg.as_string()
From 606f9dfbaee0f336c1b24d3fd55f1c151ab1c76e Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 7 Nov 2020 13:00:26 +0100
Subject: [PATCH 4/6] use valid PGP key for fake data
---
server.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/server.py b/server.py
index b4c2e9ee..aa7c700c 100644
--- a/server.py
+++ b/server.py
@@ -49,6 +49,7 @@ from app.config import (
LANDING_PAGE_URL,
STATUS_PAGE_URL,
SUPPORT_EMAIL,
+ get_abs_path,
)
from app.dashboard.base import dashboard_bp
from app.developer.base import developer_bp
@@ -77,6 +78,7 @@ from app.models import (
)
from app.monitor.base import monitor_bp
from app.oauth.base import oauth_bp
+from app.pgp_utils import load_public_key
if SENTRY_DSN:
LOG.d("enable sentry")
@@ -213,12 +215,14 @@ def fake_data():
api_key = ApiKey.create(user_id=user.id, name="Firefox")
api_key.code = "codeFF"
+ pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read()
m1 = Mailbox.create(
user_id=user.id,
- email="m1@cd.ef",
+ email="pgp@example.org",
verified=True,
- pgp_finger_print="fake fingerprint",
+ pgp_public_key=pgp_public_key,
)
+ m1.pgp_finger_print = load_public_key(pgp_public_key)
db.session.commit()
for i in range(3):
From 6a68141d8de25db8d753a2249bf8beac6de27b1e Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 7 Nov 2020 13:00:45 +0100
Subject: [PATCH 5/6] Use mailbox generic subject for forwarded emails
---
email_handler.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/email_handler.py b/email_handler.py
index aa6b4f40..4ac21f41 100644
--- a/email_handler.py
+++ b/email_handler.py
@@ -99,6 +99,7 @@ from app.email_utils import (
send_email_at_most_times,
is_valid_alias_address_domain,
should_add_dkim_signature,
+ add_header,
)
from app.extensions import db
from app.greylisting import greylisting_needed
@@ -397,7 +398,7 @@ def should_append_alias(msg: Message, address: str):
def prepare_pgp_message(
orig_msg: Message, pgp_fingerprint: str, public_key: str, can_sign: bool = False
-):
+) -> Message:
msg = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
# clone orig message to avoid modifying it
@@ -687,6 +688,15 @@ def forward_email_to_mailbox(
# create PGP email if needed
if mailbox.pgp_finger_print and user.is_premium() and not alias.disable_pgp:
LOG.d("Encrypt message using mailbox %s", mailbox)
+ if mailbox.generic_subject:
+ LOG.d("Use a generic subject for %s", mailbox)
+ add_or_replace_header(msg, "Subject", mailbox.generic_subject)
+ msg = add_header(
+ msg,
+ f"""Forwarded by SimpleLogin to {alias.email} with "{msg["Subject"]}" as subject""",
+ f"""Forwarded by SimpleLogin to {alias.email} with {msg["Subject"]} as subject""",
+ )
+
try:
msg = prepare_pgp_message(
msg, mailbox.pgp_finger_print, mailbox.pgp_public_key, can_sign=True
From 4be182320eb570ac9ad58cdf05991c82fb55f7e5 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 7 Nov 2020 13:00:58 +0100
Subject: [PATCH 6/6] black
---
app/dashboard/views/mailbox_detail.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/dashboard/views/mailbox_detail.py b/app/dashboard/views/mailbox_detail.py
index 34fa95d6..4eaa527e 100644
--- a/app/dashboard/views/mailbox_detail.py
+++ b/app/dashboard/views/mailbox_detail.py
@@ -155,7 +155,8 @@ def mailbox_detail_route(mailbox_id):
if request.form.get("action") == "save":
if not mailbox.pgp_finger_print:
flash(
- "Generic subject can only be used on PGP-enabled mailbox", "error"
+ "Generic subject can only be used on PGP-enabled mailbox",
+ "error",
)
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)