瀏覽代碼

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

Contact pgp
Son Nguyen Kim 5 年之前
父節點
當前提交
7a3a6784cc

+ 1 - 0
app/dashboard/__init__.py

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

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

@@ -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>
-            <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>
       </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):
     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

+ 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:
         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,

+ 11 - 1
init_app.py

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

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