Parcourir la source

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

Contact pgp
Son Nguyen Kim il y a 5 ans
Parent
commit
7a3a6784cc

+ 1 - 0
app/dashboard/__init__.py

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

+ 16 - 14
app/dashboard/templates/dashboard/alias_contact_manager.html

@@ -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>

+ 73 - 0
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 %}
+
+  <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 %}
+
+

+ 55 - 0
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/<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
+    )

+ 8 - 1
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

+ 5 - 0
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,

+ 11 - 1
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()
 

+ 31 - 0
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 ###

+ 7 - 0
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;