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, refused_email,
referral, referral,
recovery_code, recovery_code,
contact_detail,
) )

View File

@ -9,7 +9,7 @@
{% block default_content %} {% block default_content %}
<div class="row"> <div class="row">
<div class="col"> <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" <a class="ml-3 text-info" style="font-size: 12px" data-toggle="collapse" href="#howtouse" role="button"
aria-expanded="false" aria-controls="collapseExample"> aria-expanded="false" aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i> How to use <i class="fe fe-chevrons-down"></i>
@ -18,20 +18,19 @@
<div class="alert alert-primary collapse" id="howtouse" role="alert"> <div class="alert alert-primary collapse" id="howtouse" role="alert">
<p> <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 1. Create a special email address called <em>reverse-alias</em> for friend@example.com using the form below
<br> <br>
2. Send the email to the reverse-alias <em>instead of</em> friend@example.com 2. Send the email to the reverse-alias <em>instead of</em> friend@example.com
<br> <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>
<p> <p>
This might sound complicated but trust us, only the first time is a bit awkward. This might sound complicated but trust us, only the first time is a bit awkward.
</p> </p>
<p> <p>
{% if alias.mailbox_id %} {% if alias.mailbox_id %}
{% if alias.mailboxes | length == 1 %} {% if alias.mailboxes | length == 1 %}
Make sure you send the email from the mailbox <b>{{ alias.mailbox.email }}</b>. Make sure you send the email from the mailbox <b>{{ alias.mailbox.email }}</b>.
{% else %} {% else %}
@ -81,7 +80,10 @@
</div> </div>
<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>
<div class="mb-2 text-muted small-text"> <div class="mb-2 text-muted small-text">
@ -93,15 +95,15 @@
{% endif %} {% endif %}
</div> </div>
<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"> <form method="post">
<input type="hidden" name="contact-id" value="{{ contact.id }}"> <input type="hidden" name="form-name" value="delete">
<span class="card-link btn btn-link float-right delete-forward-email text-danger"> <input type="hidden" name="contact-id" value="{{ contact.id }}">
Delete <span class="card-link btn btn-link float-right delete-forward-email text-danger">
</span> Delete
</form> </span>
</div> </form>
</div> </div>
</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): def get_contacts(self, page=0):
contacts = ( contacts = (
Contact.filter_by(alias_id=self.id) Contact.filter_by(alias_id=self.id)
.order_by(Contact.created_at) .order_by(Contact.created_at.desc())
.limit(PAGE_LIMIT) .limit(PAGE_LIMIT)
.offset(page * PAGE_LIMIT) .offset(page * PAGE_LIMIT)
.all() .all()
@ -933,9 +933,16 @@ class Contact(db.Model, ModelMixin):
# whether a contact is created via CC # whether a contact is created via CC
is_cc = db.Column(db.Boolean, nullable=False, default=False, server_default="0") 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) alias = db.relationship(Alias)
user = db.relationship(User) user = db.relationship(User)
@property
def email(self):
return self.website_email
def website_send_to(self): def website_send_to(self):
"""return the email address with name. """return the email address with name.
to use when user wants to send an email from the alias 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: if custom_domain.dkim_verified:
add_dkim_signature(msg, alias_domain) 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( smtp.sendmail(
alias.email, alias.email,
contact.website_email, contact.website_email,

View File

@ -1,5 +1,5 @@
"""Initial loading script""" """Initial loading script"""
from app.models import Mailbox from app.models import Mailbox, Contact
from app.log import LOG from app.log import LOG
from app.extensions import db from app.extensions import db
from app.pgp_utils import load_public_key from app.pgp_utils import load_public_key
@ -16,6 +16,16 @@ def load_pgp_public_keys():
if fingerprint != mailbox.pgp_finger_print: if fingerprint != mailbox.pgp_finger_print:
LOG.error("fingerprint %s different for mailbox %s", fingerprint, mailbox) LOG.error("fingerprint %s different for mailbox %s", fingerprint, mailbox)
mailbox.pgp_finger_print = fingerprint 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() 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; --heading-background: #FFF;
--border: 1px solid rgba(0, 40, 100, 0.12); --border: 1px solid rgba(0, 40, 100, 0.12);
--input-bg-color: var(--white); --input-bg-color: var(--white);
--light-bg-color: #e9ecef;
} }
[data-theme="dark"] { [data-theme="dark"] {
@ -21,6 +22,7 @@
--heading-background: #1a1a1a; --heading-background: #1a1a1a;
--input-bg-color: #4c4c4c; --input-bg-color: #4c4c4c;
--border: 1px solid rgba(228, 236, 238, 0.35); --border: 1px solid rgba(228, 236, 238, 0.35);
--light-bg-color: #5c5c5c;
} }
/** Override the bootstrap color configurations */ /** Override the bootstrap color configurations */
@ -46,6 +48,11 @@ hr {
background-color: var(--input-bg-color); 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 { .form-control:focus, .dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus, .modal-content {
border-color: #1991eb; border-color: #1991eb;
outline: 0; outline: 0;