Merge pull request #224 from simple-login/contact-pgp

Contact pgp
This commit is contained in:
Son Nguyen Kim 2020-06-07 13:48:40 +02:00 committed by GitHub
commit 7a3a6784cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 207 additions and 16 deletions

View File

@ -22,4 +22,5 @@ from .views import (
refused_email,
referral,
recovery_code,
contact_detail,
)

View File

@ -9,7 +9,7 @@
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3"> {{ alias.email }}
<h1 class="h3"> {{ alias.email }} contacts
<a class="ml-3 text-info" style="font-size: 12px" data-toggle="collapse" href="#howtouse" role="button"
aria-expanded="false" aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
@ -18,20 +18,19 @@
<div class="alert alert-primary collapse" id="howtouse" role="alert">
<p>
To send an email from your alias to someone, says <b>friend@example.com</b>, you need to: <br>
To send an email from your alias to a contact, says <b>friend@example.com</b>, you need to: <br>
1. Create a special email address called <em>reverse-alias</em> for friend@example.com using the form below
<br>
2. Send the email to the reverse-alias <em>instead of</em> friend@example.com
<br>
3. SimpleLogin will send this email from the alias to friend@example.com for you
3. SimpleLogin will send this email <em>from the alias</em> to friend@example.com for you
</p>
<p>
This might sound complicated but trust us, only the first time is a bit awkward.
</p>
<p>
{% if alias.mailbox_id %}
{% if alias.mailboxes | length == 1 %}
Make sure you send the email from the mailbox <b>{{ alias.mailbox.email }}</b>.
{% else %}
@ -81,7 +80,10 @@
</div>
<div>
<i class="fe fe-mail"></i> ➡ {{ contact.website_email }}
Contact <b>{{ contact.website_email }}</b>
{% if contact.pgp_finger_print %}
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
{% endif %}
</div>
<div class="mb-2 text-muted small-text">
@ -93,15 +95,15 @@
{% endif %}
</div>
<div>
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="contact-id" value="{{ contact.id }}">
<span class="card-link btn btn-link float-right delete-forward-email text-danger">
Delete
</span>
</form>
</div>
<a href="{{ url_for('dashboard.contact_detail_route', contact_id=contact.id) }}">Edit ➡</a>
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="contact-id" value="{{ contact.id }}">
<span class="card-link btn btn-link float-right delete-forward-email text-danger">
Delete
</span>
</form>
</div>
</div>

View File

@ -0,0 +1,73 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Contact {{ contact.email }} - Alias {{ alias.email }}
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}">{{ alias.email }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ contact.email }}
{% if contact.pgp_finger_print %}
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
{% endif %}
</li>
</ol>
</nav>
</h1>
<div class="card">
<form method="post">
<input type="hidden" name="form-name" value="pgp">
<div class="card-body">
<div class="card-title">
Pretty Good Privacy (PGP)
<div class="small-text">
By importing your contact PGP Public Key into SimpleLogin, all emails sent to
<b>{{ contact.email }}</b> from your alias <b>{{ alias.email }}</b>
are <b>encrypted</b>.
</div>
</div>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available in premium plan.
</div>
{% endif %}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp"
{% if not current_user.is_premium() %} disabled {% endif %}
class="form-control" rows=10
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
</div>
<button class="btn btn-primary" name="action"
{% if not current_user.is_premium() %} disabled {% endif %}
value="save">Save
</button>
{% if contact.pgp_finger_print %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.models import Contact
from app.pgp_utils import PGPException, load_public_key
@dashboard_bp.route("/contact/<int:contact_id>/", methods=["GET", "POST"])
@login_required
def contact_detail_route(contact_id):
contact = Contact.get(contact_id)
if not contact or contact.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
alias = contact.alias
if request.method == "POST":
if request.form.get("form-name") == "pgp":
if request.form.get("action") == "save":
if not current_user.is_premium():
flash("Only premium plan can add PGP Key", "warning")
return redirect(
url_for("dashboard.contact_detail_route", contact_id=contact_id)
)
contact.pgp_public_key = request.form.get("pgp")
try:
contact.pgp_finger_print = load_public_key(contact.pgp_public_key)
except PGPException:
flash("Cannot add the public key, please verify it", "error")
else:
db.session.commit()
flash(
f"PGP public key for {contact.email} is saved successfully",
"success",
)
return redirect(
url_for("dashboard.contact_detail_route", contact_id=contact_id)
)
elif request.form.get("action") == "remove":
# Free user can decide to remove contact PGP key
contact.pgp_public_key = None
contact.pgp_finger_print = None
db.session.commit()
flash(f"PGP public key for {contact.email} is removed", "success")
return redirect(
url_for("dashboard.contact_detail_route", contact_id=contact_id)
)
return render_template(
"dashboard/contact_detail.html", contact=contact, alias=alias
)

View File

@ -805,7 +805,7 @@ class Alias(db.Model, ModelMixin):
def get_contacts(self, page=0):
contacts = (
Contact.filter_by(alias_id=self.id)
.order_by(Contact.created_at)
.order_by(Contact.created_at.desc())
.limit(PAGE_LIMIT)
.offset(page * PAGE_LIMIT)
.all()
@ -933,9 +933,16 @@ class Contact(db.Model, ModelMixin):
# whether a contact is created via CC
is_cc = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
pgp_public_key = db.Column(db.Text, nullable=True)
pgp_finger_print = db.Column(db.String(512), nullable=True)
alias = db.relationship(Alias)
user = db.relationship(User)
@property
def email(self):
return self.website_email
def website_send_to(self):
"""return the email address with name.
to use when user wants to send an email from the alias

View File

@ -605,6 +605,11 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, str
if custom_domain.dkim_verified:
add_dkim_signature(msg, alias_domain)
# create PGP email if needed
if contact.pgp_finger_print and user.is_premium():
LOG.d("Encrypt message for contact %s", contact)
msg = prepare_pgp_message(msg, contact.pgp_finger_print)
smtp.sendmail(
alias.email,
contact.website_email,

View File

@ -1,5 +1,5 @@
"""Initial loading script"""
from app.models import Mailbox
from app.models import Mailbox, Contact
from app.log import LOG
from app.extensions import db
from app.pgp_utils import load_public_key
@ -16,6 +16,16 @@ def load_pgp_public_keys():
if fingerprint != mailbox.pgp_finger_print:
LOG.error("fingerprint %s different for mailbox %s", fingerprint, mailbox)
mailbox.pgp_finger_print = fingerprint
db.session.commit()
for contact in Contact.query.filter(Contact.pgp_public_key != None).all():
LOG.d("Load PGP key for %s", contact)
fingerprint = load_public_key(contact.pgp_public_key)
# sanity check
if fingerprint != contact.pgp_finger_print:
LOG.error("fingerprint %s different for contact %s", fingerprint, contact)
contact.pgp_finger_print = fingerprint
db.session.commit()

View File

@ -0,0 +1,31 @@
"""empty message
Revision ID: a5b4dc311a89
Revises: 749c2b85d20f
Create Date: 2020-06-07 00:08:08.588009
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a5b4dc311a89'
down_revision = '749c2b85d20f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('contact', sa.Column('pgp_finger_print', sa.String(length=512), nullable=True))
op.add_column('contact', sa.Column('pgp_public_key', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('contact', 'pgp_public_key')
op.drop_column('contact', 'pgp_finger_print')
# ### end Alembic commands ###

7
static/darkmode.css vendored
View File

@ -10,6 +10,7 @@
--heading-background: #FFF;
--border: 1px solid rgba(0, 40, 100, 0.12);
--input-bg-color: var(--white);
--light-bg-color: #e9ecef;
}
[data-theme="dark"] {
@ -21,6 +22,7 @@
--heading-background: #1a1a1a;
--input-bg-color: #4c4c4c;
--border: 1px solid rgba(228, 236, 238, 0.35);
--light-bg-color: #5c5c5c;
}
/** Override the bootstrap color configurations */
@ -46,6 +48,11 @@ hr {
background-color: var(--input-bg-color);
}
.breadcrumb {
color: var(--font-color);
background-color: var(--light-bg-color);
}
.form-control:focus, .dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus, .modal-content {
border-color: #1991eb;
outline: 0;