diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py index fc216406..41ff2bc0 100644 --- a/app/dashboard/__init__.py +++ b/app/dashboard/__init__.py @@ -22,4 +22,5 @@ from .views import ( refused_email, referral, recovery_code, + contact_detail, ) diff --git a/app/dashboard/templates/dashboard/alias_contact_manager.html b/app/dashboard/templates/dashboard/alias_contact_manager.html index 9e201d8b..d3c3e5bc 100644 --- a/app/dashboard/templates/dashboard/alias_contact_manager.html +++ b/app/dashboard/templates/dashboard/alias_contact_manager.html @@ -9,7 +9,7 @@ {% block default_content %}
-

{{ alias.email }} +

{{ alias.email }} contacts Edit ➡ + +
+ + + + Delete + +

diff --git a/app/dashboard/templates/dashboard/contact_detail.html b/app/dashboard/templates/dashboard/contact_detail.html new file mode 100644 index 00000000..25a79776 --- /dev/null +++ b/app/dashboard/templates/dashboard/contact_detail.html @@ -0,0 +1,73 @@ +{% extends 'default.html' %} + +{% set active_page = "dashboard" %} + +{% block title %} + Contact {{ contact.email }} - Alias {{ alias.email }} +{% endblock %} + +{% block default_content %} + +
+
+

+ +

+ +
+
+ + +
+
+ Pretty Good Privacy (PGP) +
+ By importing your contact PGP Public Key into SimpleLogin, all emails sent to + {{ contact.email }} from your alias {{ alias.email }} + are encrypted. +
+
+ + {% if not current_user.is_premium() %} + + {% endif %} + +
+ + + +
+ + + {% if contact.pgp_finger_print %} + + {% endif %} + +
+
+ +
+ +
+
+{% endblock %} + + diff --git a/app/dashboard/views/contact_detail.py b/app/dashboard/views/contact_detail.py new file mode 100644 index 00000000..ead29120 --- /dev/null +++ b/app/dashboard/views/contact_detail.py @@ -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//", 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 + ) diff --git a/app/models.py b/app/models.py index 0bc40075..7cebed23 100644 --- a/app/models.py +++ b/app/models.py @@ -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 diff --git a/email_handler.py b/email_handler.py index f9868635..d2487630 100644 --- a/email_handler.py +++ b/email_handler.py @@ -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, diff --git a/init_app.py b/init_app.py index 22389fc2..b77088ea 100644 --- a/init_app.py +++ b/init_app.py @@ -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() diff --git a/migrations/versions/2020_060700_a5b4dc311a89_.py b/migrations/versions/2020_060700_a5b4dc311a89_.py new file mode 100644 index 00000000..7e9afbcd --- /dev/null +++ b/migrations/versions/2020_060700_a5b4dc311a89_.py @@ -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 ### diff --git a/static/darkmode.css b/static/darkmode.css index 66c696e1..795f6f88 100644 --- a/static/darkmode.css +++ b/static/darkmode.css @@ -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;