mirror of
https://github.com/simple-login/app.git
synced 2024-09-30 05:31:30 +02:00
commit
7a3a6784cc
@ -22,4 +22,5 @@ from .views import (
|
|||||||
refused_email,
|
refused_email,
|
||||||
referral,
|
referral,
|
||||||
recovery_code,
|
recovery_code,
|
||||||
|
contact_detail,
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
73
app/dashboard/templates/dashboard/contact_detail.html
Normal file
73
app/dashboard/templates/dashboard/contact_detail.html
Normal 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 %}
|
||||||
|
|
||||||
|
|
55
app/dashboard/views/contact_detail.py
Normal file
55
app/dashboard/views/contact_detail.py
Normal 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
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
12
init_app.py
12
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
migrations/versions/2020_060700_a5b4dc311a89_.py
Normal file
31
migrations/versions/2020_060700_a5b4dc311a89_.py
Normal 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
7
static/darkmode.css
vendored
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user