mirror of
https://github.com/simple-login/app.git
synced 2024-09-30 05:31:30 +02:00
Merge pull request #327 from simple-login/generic-subject
Generic subject for PGP-encrypted forwarded emails
This commit is contained in:
commit
44fd80b2e1
@ -6,6 +6,17 @@
|
|||||||
Mailbox {{ mailbox.email }}
|
Mailbox {{ mailbox.email }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
div[disabled]
|
||||||
|
{
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -72,7 +83,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
Pretty Good Privacy (PGP)
|
Pretty Good Privacy (PGP)
|
||||||
<div class="small-text">
|
<div class="small-text mt-1">
|
||||||
By importing your PGP Public Key into SimpleLogin, all emails sent to {{ mailbox.email }} are
|
By importing your PGP Public Key into SimpleLogin, all emails sent to {{ mailbox.email }} are
|
||||||
<b>encrypted</b> with your key.
|
<b>encrypted</b> with your key.
|
||||||
</div>
|
</div>
|
||||||
@ -106,6 +117,45 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" {% if not mailbox.pgp_finger_print %} disabled {% endif %}>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="form-name" value="generic-subject">
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title">
|
||||||
|
Hide email subject when PGP is enabled
|
||||||
|
<div class="small-text mt-1">
|
||||||
|
When PGP is enabled, you can choose to use a <b>generic</b> subject for the forwarded emails.
|
||||||
|
The original subject is then added into the email body. <br>
|
||||||
|
As PGP does not encrypt the email subject and the email subject might contain sensitive information,
|
||||||
|
this option will allow a further protection of your email content.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Generic Subject</label>
|
||||||
|
|
||||||
|
<input name="generic-subject"
|
||||||
|
{% if not mailbox.pgp_finger_print %} disabled {% endif %}
|
||||||
|
class="form-control" maxlength="78"
|
||||||
|
placeholder="Generic Subject"
|
||||||
|
value="{{ mailbox.generic_subject or "" }}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" name="action"
|
||||||
|
{% if not mailbox.pgp_finger_print %} disabled {% endif %}
|
||||||
|
value="save">Save
|
||||||
|
</button>
|
||||||
|
{% if mailbox.generic_subject %}
|
||||||
|
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h2 class="h4">Advanced Options</h2>
|
<h2 class="h4">Advanced Options</h2>
|
||||||
|
|
||||||
|
@ -151,6 +151,30 @@ def mailbox_detail_route(mailbox_id):
|
|||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
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
|
spf_available = ENFORCE_SPF
|
||||||
return render_template("dashboard/mailbox_detail.html", **locals())
|
return render_template("dashboard/mailbox_detail.html", **locals())
|
||||||
|
@ -653,3 +653,40 @@ def is_valid_email(email_address: str) -> bool:
|
|||||||
return validate_email(
|
return validate_email(
|
||||||
email_address=email_address, check_mx=False, use_blacklist=False
|
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"""
|
||||||
|
<table width="100%" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-bottom:1px dashed #5675E2; padding: 10px 0px">{html_header}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{payload}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
@ -1642,6 +1642,8 @@ class Mailbox(db.Model, ModelMixin):
|
|||||||
# a mailbox can be disabled if it can't be reached
|
# a mailbox can be disabled if it can't be reached
|
||||||
disabled = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
|
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"),)
|
__table_args__ = (db.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),)
|
||||||
|
|
||||||
user = db.relationship(User, foreign_keys=[user_id])
|
user = db.relationship(User, foreign_keys=[user_id])
|
||||||
|
@ -99,6 +99,7 @@ from app.email_utils import (
|
|||||||
send_email_at_most_times,
|
send_email_at_most_times,
|
||||||
is_valid_alias_address_domain,
|
is_valid_alias_address_domain,
|
||||||
should_add_dkim_signature,
|
should_add_dkim_signature,
|
||||||
|
add_header,
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.greylisting import greylisting_needed
|
from app.greylisting import greylisting_needed
|
||||||
@ -397,7 +398,7 @@ def should_append_alias(msg: Message, address: str):
|
|||||||
|
|
||||||
def prepare_pgp_message(
|
def prepare_pgp_message(
|
||||||
orig_msg: Message, pgp_fingerprint: str, public_key: str, can_sign: bool = False
|
orig_msg: Message, pgp_fingerprint: str, public_key: str, can_sign: bool = False
|
||||||
):
|
) -> Message:
|
||||||
msg = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
|
msg = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
|
||||||
|
|
||||||
# clone orig message to avoid modifying it
|
# clone orig message to avoid modifying it
|
||||||
@ -687,6 +688,15 @@ def forward_email_to_mailbox(
|
|||||||
# create PGP email if needed
|
# create PGP email if needed
|
||||||
if mailbox.pgp_finger_print and user.is_premium() and not alias.disable_pgp:
|
if mailbox.pgp_finger_print and user.is_premium() and not alias.disable_pgp:
|
||||||
LOG.d("Encrypt message using mailbox %s", mailbox)
|
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 <b>{msg["Subject"]}</b> as subject""",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = prepare_pgp_message(
|
msg = prepare_pgp_message(
|
||||||
msg, mailbox.pgp_finger_print, mailbox.pgp_public_key, can_sign=True
|
msg, mailbox.pgp_finger_print, mailbox.pgp_public_key, can_sign=True
|
||||||
|
29
migrations/versions/2020_110712_d0f197979bd9_.py
Normal file
29
migrations/versions/2020_110712_d0f197979bd9_.py
Normal file
@ -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 ###
|
@ -49,6 +49,7 @@ from app.config import (
|
|||||||
LANDING_PAGE_URL,
|
LANDING_PAGE_URL,
|
||||||
STATUS_PAGE_URL,
|
STATUS_PAGE_URL,
|
||||||
SUPPORT_EMAIL,
|
SUPPORT_EMAIL,
|
||||||
|
get_abs_path,
|
||||||
)
|
)
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.developer.base import developer_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.monitor.base import monitor_bp
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
|
from app.pgp_utils import load_public_key
|
||||||
|
|
||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
LOG.d("enable sentry")
|
LOG.d("enable sentry")
|
||||||
@ -213,12 +215,14 @@ def fake_data():
|
|||||||
api_key = ApiKey.create(user_id=user.id, name="Firefox")
|
api_key = ApiKey.create(user_id=user.id, name="Firefox")
|
||||||
api_key.code = "codeFF"
|
api_key.code = "codeFF"
|
||||||
|
|
||||||
|
pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read()
|
||||||
m1 = Mailbox.create(
|
m1 = Mailbox.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
email="m1@cd.ef",
|
email="pgp@example.org",
|
||||||
verified=True,
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
@ -14,6 +14,7 @@ from app.email_utils import (
|
|||||||
get_spam_from_header,
|
get_spam_from_header,
|
||||||
get_header_from_bounce,
|
get_header_from_bounce,
|
||||||
is_valid_email,
|
is_valid_email,
|
||||||
|
add_header,
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, CustomDomain
|
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("with space@gmail.com")
|
||||||
assert not is_valid_email("strange char !ç@gmail.com")
|
assert not is_valid_email("strange char !ç@gmail.com")
|
||||||
assert not is_valid_email("emoji👌@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
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||||
|
</head>
|
||||||
|
<body style="word-wrap: break-word;" class="">
|
||||||
|
<b class="">bold</b>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
new_msg = add_header(msg, "text header", "html header")
|
||||||
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||||
|
assert "<table" in new_msg.as_string()
|
||||||
|
assert "</table>" 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
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||||
|
</head>
|
||||||
|
<body style="word-wrap: break-word;" class="">
|
||||||
|
<b class="">bold</b>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
new_msg = add_header(msg, "text header", "html header")
|
||||||
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||||
|
assert "<table" in new_msg.as_string()
|
||||||
|
assert "</table>" in new_msg.as_string()
|
||||||
|
assert "html header" in new_msg.as_string()
|
||||||
|
assert "text header" in new_msg.as_string()
|
||||||
|
Loading…
Reference in New Issue
Block a user