commit
fea60c7386
|
@ -13,4 +13,5 @@ from .views import (
|
||||||
mfa_cancel,
|
mfa_cancel,
|
||||||
domain_detail,
|
domain_detail,
|
||||||
lifetime_licence,
|
lifetime_licence,
|
||||||
|
directory,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
{% extends 'default.html' %}
|
||||||
|
{% set active_page = "directory" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Directory
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<h1 class="h3"> Directories </h1>
|
||||||
|
|
||||||
|
<div class="alert alert-primary" role="alert">
|
||||||
|
Directory allows you to create aliases <b>on the fly</b>. <br>
|
||||||
|
Simply use <em>directory/<b>anything</b>@{{ EMAIL_DOMAIN }}</em>
|
||||||
|
next time you need an email address. <br>
|
||||||
|
The alias will be created the first time it receives an email.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for dir in dirs %}
|
||||||
|
<div class="card" style="max-width: 50rem">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{{ dir.name }}
|
||||||
|
</h5>
|
||||||
|
<h6 class="card-subtitle mb-2 text-muted">
|
||||||
|
Created {{ dir.created_at | dt }} <br>
|
||||||
|
<span class="font-weight-bold">{{ dir.nb_alias() }}</span> aliases.
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer p-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="form-name" value="delete">
|
||||||
|
<input type="hidden" class="dir-name" value="{{ dir.name }}">
|
||||||
|
<input type="hidden" name="dir-id" value="{{ dir.id }}">
|
||||||
|
<span class="card-link btn btn-link float-right delete-dir">
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if dirs|length > 0 %}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" class="mt-6">
|
||||||
|
{{ new_dir_form.csrf_token }}
|
||||||
|
<input type="hidden" name="form-name" value="create">
|
||||||
|
|
||||||
|
<div class="font-weight-bold">Directory Name</div>
|
||||||
|
<div class="small-text">
|
||||||
|
Directory name must be at least 3 characters.
|
||||||
|
Only letter, number, dash (-), underscore (_) can be used.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ new_dir_form.name(class="form-control", placeholder="my-directory", pattern="[0-9A-Za-z-_]{3,}",
|
||||||
|
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.",
|
||||||
|
autofocus="1") }}
|
||||||
|
{{ render_field_errors(new_dir_form.name) }}
|
||||||
|
<button class="btn btn-lg btn-success mt-2">Create</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
$(".delete-dir").on("click", function (e) {
|
||||||
|
let directory = $(this).parent().find(".dir-name").val();
|
||||||
|
notie.confirm({
|
||||||
|
text: `All aliases associated with <b>${directory}</b> directory will be also deleted, ` +
|
||||||
|
" please confirm.",
|
||||||
|
cancelCallback: () => {
|
||||||
|
// nothing to do
|
||||||
|
},
|
||||||
|
submitCallback: () => {
|
||||||
|
$(this).closest("form").submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,70 @@
|
||||||
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, validators
|
||||||
|
|
||||||
|
from app.config import EMAIL_DOMAIN
|
||||||
|
from app.dashboard.base import dashboard_bp
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import Directory
|
||||||
|
|
||||||
|
|
||||||
|
class NewDirForm(FlaskForm):
|
||||||
|
name = StringField(
|
||||||
|
"name", validators=[validators.DataRequired(), validators.Length(min=3)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/directory", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def directory():
|
||||||
|
# only premium user can add directory
|
||||||
|
if not current_user.is_premium():
|
||||||
|
flash("Only premium user can add directories", "warning")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
dirs = Directory.query.filter_by(user_id=current_user.id).all()
|
||||||
|
|
||||||
|
new_dir_form = NewDirForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if request.form.get("form-name") == "delete":
|
||||||
|
dir_id = request.form.get("dir-id")
|
||||||
|
dir = Directory.get(dir_id)
|
||||||
|
|
||||||
|
if not dir:
|
||||||
|
flash("Unknown error. Refresh the page", "warning")
|
||||||
|
return redirect(url_for("dashboard.directory"))
|
||||||
|
elif dir.user_id != current_user.id:
|
||||||
|
flash("You cannot delete this directory", "warning")
|
||||||
|
return redirect(url_for("dashboard.directory"))
|
||||||
|
|
||||||
|
name = dir.name
|
||||||
|
Directory.delete(dir_id)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Directory {name} has been deleted", "success")
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard.directory"))
|
||||||
|
|
||||||
|
elif request.form.get("form-name") == "create":
|
||||||
|
if new_dir_form.validate():
|
||||||
|
new_dir_name = new_dir_form.name.data
|
||||||
|
|
||||||
|
if Directory.get_by(name=new_dir_name):
|
||||||
|
flash(f"{new_dir_name} already added", "warning")
|
||||||
|
else:
|
||||||
|
new_dir = Directory.create(
|
||||||
|
name=new_dir_name, user_id=current_user.id
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f"Directory {new_dir.name} is created", "success")
|
||||||
|
|
||||||
|
return redirect(url_for("dashboard.directory",))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"dashboard/directory.html",
|
||||||
|
dirs=dirs,
|
||||||
|
new_dir_form=new_dir_form,
|
||||||
|
EMAIL_DOMAIN=EMAIL_DOMAIN,
|
||||||
|
)
|
|
@ -439,6 +439,11 @@ class GenEmail(db.Model, ModelMixin):
|
||||||
db.Boolean, nullable=False, default=False, server_default="0"
|
db.Boolean, nullable=False, default=False, server_default="0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# to know whether an alias belongs to a directory
|
||||||
|
directory_id = db.Column(
|
||||||
|
db.ForeignKey("directory.id", ondelete="cascade"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
user = db.relationship(User)
|
user = db.relationship(User)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -725,3 +730,16 @@ class CustomDomain(db.Model, ModelMixin):
|
||||||
class LifetimeCoupon(db.Model, ModelMixin):
|
class LifetimeCoupon(db.Model, ModelMixin):
|
||||||
code = db.Column(db.String(128), nullable=False, unique=True)
|
code = db.Column(db.String(128), nullable=False, unique=True)
|
||||||
nb_used = db.Column(db.Integer, nullable=False)
|
nb_used = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Directory(db.Model, ModelMixin):
|
||||||
|
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
|
||||||
|
name = db.Column(db.String(128), unique=True, nullable=False)
|
||||||
|
|
||||||
|
user = db.relationship(User)
|
||||||
|
|
||||||
|
def nb_alias(self):
|
||||||
|
return GenEmail.filter_by(directory_id=self.id).count()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Directory {self.name}>"
|
||||||
|
|
|
@ -50,7 +50,7 @@ from app.email_utils import (
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, ForwardEmail, ForwardEmailLog, CustomDomain
|
from app.models import GenEmail, ForwardEmail, ForwardEmailLog, CustomDomain, Directory
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
from server import create_app
|
from server import create_app
|
||||||
|
|
||||||
|
@ -110,22 +110,46 @@ class MailHandler:
|
||||||
|
|
||||||
gen_email = GenEmail.get_by(email=alias)
|
gen_email = GenEmail.get_by(email=alias)
|
||||||
if not gen_email:
|
if not gen_email:
|
||||||
LOG.d("alias %s not exist", alias)
|
LOG.d("alias %s not exist. Try to see if it can created on the fly", alias)
|
||||||
|
|
||||||
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
|
# try to see if alias could be created on-the-fly
|
||||||
alias_domain = get_email_domain_part(alias)
|
on_the_fly = False
|
||||||
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
|
||||||
if custom_domain and custom_domain.catch_all:
|
|
||||||
LOG.d("create alias %s for domain %s", alias, custom_domain)
|
|
||||||
|
|
||||||
gen_email = GenEmail.create(
|
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
||||||
email=alias,
|
if alias.endswith(EMAIL_DOMAIN):
|
||||||
user_id=custom_domain.user_id,
|
if "/" in alias:
|
||||||
custom_domain_id=custom_domain.id,
|
directory_name = alias[: alias.find("/")]
|
||||||
automatic_creation=True,
|
LOG.d("directory_name %s", directory_name)
|
||||||
)
|
|
||||||
db.session.commit()
|
directory = Directory.get_by(name=directory_name)
|
||||||
|
if directory:
|
||||||
|
LOG.d("create alias %s for directory %s", alias, directory)
|
||||||
|
on_the_fly = True
|
||||||
|
|
||||||
|
gen_email = GenEmail.create(
|
||||||
|
email=alias,
|
||||||
|
user_id=directory.user_id,
|
||||||
|
directory_id=directory.id,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
|
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
|
||||||
|
alias_domain = get_email_domain_part(alias)
|
||||||
|
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
|
if custom_domain and custom_domain.catch_all:
|
||||||
|
LOG.d("create alias %s for domain %s", alias, custom_domain)
|
||||||
|
on_the_fly = True
|
||||||
|
|
||||||
|
gen_email = GenEmail.create(
|
||||||
|
email=alias,
|
||||||
|
user_id=custom_domain.user_id,
|
||||||
|
custom_domain_id=custom_domain.id,
|
||||||
|
automatic_creation=True,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if not on_the_fly:
|
||||||
|
LOG.d("alias %s not exist, return 510", alias)
|
||||||
return "510 Email not exist"
|
return "510 Email not exist"
|
||||||
|
|
||||||
user_email = gen_email.user.email
|
user_email = gen_email.user.email
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: ba6f13ccbabb
|
||||||
|
Revises: d29cca963221
|
||||||
|
Create Date: 2020-01-08 21:23:06.288453
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ba6f13ccbabb'
|
||||||
|
down_revision = 'd29cca963221'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('directory',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||||
|
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.add_column('gen_email', sa.Column('directory_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'gen_email', 'directory', ['directory_id'], ['id'], ondelete='cascade')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'gen_email', type_='foreignkey')
|
||||||
|
op.drop_column('gen_email', 'directory_id')
|
||||||
|
op.drop_table('directory')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -30,6 +30,14 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('dashboard.directory') }}"
|
||||||
|
class="nav-link {{ 'active' if active_page == 'directory' }}">
|
||||||
|
<i class="fe fe-folder"></i> Alias Directory
|
||||||
|
<span class="badge badge-info" style="font-size: .5rem; top: 5px">Beta</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ url_for('discover.index') }}"
|
<a href="{{ url_for('discover.index') }}"
|
||||||
|
|
Loading…
Reference in New Issue