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..4eaa527e 100644
--- a/app/dashboard/views/mailbox_detail.py
+++ b/app/dashboard/views/mailbox_detail.py
@@ -151,6 +151,30 @@ 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())
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/app/models.py b/app/models.py
index fe2a397b..ae358644 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1642,6 +1642,8 @@ class Mailbox(db.Model, ModelMixin):
# a mailbox can be disabled if it can't be reached
disabled = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
+ generic_subject = db.Column(db.String(78), nullable=True)
+
__table_args__ = (db.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),)
user = db.relationship(User, foreign_keys=[user_id])
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
diff --git a/migrations/versions/2020_110712_d0f197979bd9_.py b/migrations/versions/2020_110712_d0f197979bd9_.py
new file mode 100644
index 00000000..319b8346
--- /dev/null
+++ b/migrations/versions/2020_110712_d0f197979bd9_.py
@@ -0,0 +1,29 @@
+"""empty message
+
+Revision ID: d0f197979bd9
+Revises: 84dec6c29c48
+Create Date: 2020-11-07 12:47:44.131900
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'd0f197979bd9'
+down_revision = '84dec6c29c48'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('mailbox', sa.Column('generic_subject', sa.String(length=78), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('mailbox', 'generic_subject')
+ # ### end Alembic commands ###
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):
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()