Merge pull request #42 from simple-login/several-alias-domain

Several alias domain
This commit is contained in:
Son Nguyen Kim 2020-01-22 22:25:26 +01:00 committed by GitHub
commit 57723ddd9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 114 additions and 69 deletions

View file

@ -14,6 +14,9 @@ URL=http://localhost:7777
# domain used to create alias
EMAIL_DOMAIN=sl.local
# other domains that can be used to create aliases, in addition to EMAIL_DOMAIN
OTHER_ALIAS_DOMAINS=["domain1.com", "domain2.com"]
# transactional email is sent from this email address
SUPPORT_EMAIL=support@sl.local

View file

@ -3,7 +3,7 @@ from flask_cors import cross_origin
from sqlalchemy import desc
from app.api.base import api_bp, verify_api_key
from app.config import EMAIL_DOMAIN
from app.config import ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
from app.extensions import db
from app.log import LOG
from app.models import AliasUsedOn, GenEmail, User
@ -67,9 +67,14 @@ def options():
else:
ret["custom"]["suggestion"] = ""
ret["custom"]["suffixes"] = []
# maybe better to make sure the suffix is never used before
# but this is ok as there's a check when creating a new custom alias
ret["custom"]["suffixes"] = [f".{random_word()}@{EMAIL_DOMAIN}"]
for domain in ALIAS_DOMAINS:
if DISABLE_ALIAS_SUFFIX:
ret["custom"]["suffixes"].append(f"@{domain}")
else:
ret["custom"]["suffixes"].append(f".{random_word()}@{domain}")
for custom_domain in user.verified_custom_domains():
ret["custom"]["suffixes"].append("@" + custom_domain.domain)
@ -144,7 +149,11 @@ def options_v2():
# maybe better to make sure the suffix is never used before
# but this is ok as there's a check when creating a new custom alias
ret["suffixes"] = [f".{random_word()}@{EMAIL_DOMAIN}"]
for domain in ALIAS_DOMAINS:
if DISABLE_ALIAS_SUFFIX:
ret["suffixes"].append(f"@{domain}")
else:
ret["suffixes"].append(f".{random_word()}@{domain}")
for custom_domain in user.verified_custom_domains():
ret["suffixes"].append("@" + custom_domain.domain)

View file

@ -3,7 +3,8 @@ from flask import jsonify, request
from flask_cors import cross_origin
from app.api.base import api_bp, verify_api_key
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
from app.config import MAX_NB_EMAIL_FREE_PLAN
from app.dashboard.views.custom_alias import verify_prefix_suffix
from app.extensions import db
from app.log import LOG
from app.models import GenEmail, AliasUsedOn
@ -43,35 +44,12 @@ def new_custom_alias():
if not data:
return jsonify(error="request body cannot be empty"), 400
alias_prefix = data.get("alias_prefix", "")
alias_suffix = data.get("alias_suffix", "")
# make sure alias_prefix is not empty
alias_prefix = alias_prefix.strip()
alias_prefix = data.get("alias_prefix", "").strip()
alias_suffix = data.get("alias_suffix", "").strip()
alias_prefix = convert_to_id(alias_prefix)
if not alias_prefix: # should be checked on frontend
LOG.d("user %s submits an empty alias with the prefix %s", user, alias_prefix)
return jsonify(error="alias prefix cannot be empty"), 400
# make sure alias_suffix is either .random_letters@simplelogin.co or @my-domain.com
alias_suffix = alias_suffix.strip()
if alias_suffix.startswith("@"):
custom_domain = alias_suffix[1:]
if custom_domain not in user_custom_domains:
LOG.d("user %s submits a wrong custom domain %s ", user, custom_domain)
return jsonify(error="error"), 400
else:
if not alias_suffix.startswith("."):
LOG.d("user %s submits a wrong alias suffix %s", user, alias_suffix)
return jsonify(error="error"), 400
if not alias_suffix.endswith(EMAIL_DOMAIN):
LOG.d("user %s submits a wrong alias suffix %s", user, alias_suffix)
return jsonify(error="error"), 400
random_letters = alias_suffix[1 : alias_suffix.find("@")]
if len(random_letters) < 5:
LOG.d("user %s submits a wrong alias suffix %s", user, alias_suffix)
return jsonify(error="error"), 400
if not verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains):
return jsonify(error="wrong alias prefix or suffix"), 400
full_alias = alias_prefix + alias_suffix
if GenEmail.get_by(email=full_alias):

View file

@ -5,7 +5,8 @@ from wtforms import StringField, validators
from app import email_utils
from app.auth.base import auth_bp
from app.config import URL, EMAIL_DOMAIN
from app.config import URL
from app.email_utils import email_belongs_to_alias_domains
from app.extensions import db
from app.log import LOG
from app.models import User, ActivationCode
@ -31,8 +32,7 @@ def register():
if form.validate_on_submit():
email = form.email.data
if email.endswith(EMAIL_DOMAIN):
if email_belongs_to_alias_domains(email):
flash(
"You cannot use alias as your personal inbox. Nice try though 😉",
"error",

View file

@ -47,6 +47,16 @@ MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"])
# allow to override postfix server locally
POSTFIX_SERVER = os.environ.get("POSTFIX_SERVER", "240.0.0.1")
if "OTHER_ALIAS_DOMAINS" in os.environ:
OTHER_ALIAS_DOMAINS = eval(
os.environ["OTHER_ALIAS_DOMAINS"]
) # ["domain1.com", "domain2.com"]
else:
OTHER_ALIAS_DOMAINS = []
# List of domains user can use to create alias
ALIAS_DOMAINS = OTHER_ALIAS_DOMAINS + [EMAIL_DOMAIN]
# list of (priority, email server)
EMAIL_SERVERS_WITH_PRIORITY = eval(
os.environ["EMAIL_SERVERS_WITH_PRIORITY"]

View file

@ -23,7 +23,8 @@
<form method="post">
<div class="row mb-2">
<div class="col-sm-6 pr-1 mb-1" style="min-width: 5em">
<input name="prefix" class="form-control"#}
<input name="prefix" class="form-control"
type="text"
pattern="[0-9a-z-_]{1,}"
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
placeholder="email alias"
@ -36,13 +37,17 @@
{% if suffixes|length > 1 %}
<select class="form-control" name="suffix">
{% for suffix in suffixes %}
<option value="{{ suffix }}">
{{ suffix }}
<option value="{{ suffix[1] }}">
{% if suffix[0] %}
{{ suffix[1] }} (your domain)
{% else %}
{{ suffix[1] }}
{% endif %}
</option>
{% endfor %}
</select>
{% else %}
<span>{{ suffixes[0] }}</span>
<span>{{ suffixes[0][1] }}</span>
{% endif %}
</div>
</div>

View file

@ -1,13 +1,16 @@
from flask import render_template, redirect, url_for, flash, request, session
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators, SelectField
from app.config import EMAIL_DOMAIN, HIGHLIGHT_GEN_EMAIL_ID, DISABLE_ALIAS_SUFFIX
from app.config import (
HIGHLIGHT_GEN_EMAIL_ID,
DISABLE_ALIAS_SUFFIX,
ALIAS_DOMAINS,
)
from app.dashboard.base import dashboard_bp
from app.email_utils import email_belongs_to_alias_domains
from app.extensions import db
from app.log import LOG
from app.models import GenEmail, DeletedAlias, CustomDomain
from app.models import GenEmail
from app.utils import convert_to_id, random_word, word_exist
@ -22,16 +25,21 @@ def custom_alias():
return redirect(url_for("dashboard.index"))
user_custom_domains = [cd.domain for cd in current_user.verified_custom_domains()]
# List of (is_custom_domain, alias-suffix)
suffixes = []
# put custom domain first
for alias_domain in user_custom_domains:
suffixes.append("@" + alias_domain)
suffixes.append((True, "@" + alias_domain))
# then default domain
suffixes.append(
("" if DISABLE_ALIAS_SUFFIX else ".") + random_word() + "@" + EMAIL_DOMAIN
)
for domain in ALIAS_DOMAINS:
suffixes.append(
(
False,
("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + "@" + domain,
)
)
if request.method == "POST":
alias_prefix = request.form.get("prefix")
@ -73,9 +81,12 @@ def verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains)
alias_suffix = alias_suffix.strip()
if alias_suffix.startswith("@"):
alias_domain = alias_suffix[1:]
# alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, EMAIL_DOMAIN
# alias_domain can be either custom_domain or if DISABLE_ALIAS_SUFFIX, one of the default ALIAS_DOMAINS
if DISABLE_ALIAS_SUFFIX:
if alias_domain not in user_custom_domains and alias_domain != EMAIL_DOMAIN:
if (
alias_domain not in user_custom_domains
and alias_domain not in ALIAS_DOMAINS
):
LOG.error("wrong alias suffix %s, user %s", alias_suffix, user)
return False
else:
@ -86,9 +97,11 @@ def verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains)
if not alias_suffix.startswith("."):
LOG.error("User %s submits a wrong alias suffix %s", user, alias_suffix)
return False
if not alias_suffix.endswith(EMAIL_DOMAIN):
full_alias = alias_prefix + alias_suffix
if not email_belongs_to_alias_domains(full_alias):
LOG.error(
"Alias suffix should end with default alias domain %s",
"Alias suffix should end with one of the alias domains %s",
user,
alias_suffix,
)

View file

@ -9,8 +9,9 @@ from flask_wtf.file import FileField
from wtforms import StringField, validators
from app import s3, email_utils
from app.config import URL, EMAIL_DOMAIN
from app.config import URL
from app.dashboard.base import dashboard_bp
from app.email_utils import email_belongs_to_alias_domains
from app.extensions import db
from app.log import LOG
from app.models import (
@ -92,7 +93,7 @@ def setting():
or DeletedAlias.get_by(email=new_email)
):
flash(f"Email {new_email} already used", "error")
elif new_email.endswith(EMAIL_DOMAIN):
elif email_belongs_to_alias_domains(new_email):
flash(
"You cannot use alias as your personal inbox. Nice try though 😉",
"error",

View file

@ -14,6 +14,7 @@ from app.config import (
DKIM_SELECTOR,
DKIM_PRIVATE_KEY,
DKIM_HEADERS,
ALIAS_DOMAINS,
)
from app.log import LOG
@ -253,3 +254,12 @@ def delete_header(msg: Message, header: str):
for h in msg._headers:
if h[0].lower() == header.lower():
msg._headers.remove(h)
def email_belongs_to_alias_domains(email: str) -> bool:
"""return True if an emails ends with one of the alias domains provided by SimpleLogin"""
for domain in ALIAS_DOMAINS:
if email.endswith("@" + domain):
return True
return False

View file

@ -38,7 +38,7 @@ from smtplib import SMTP
from aiosmtpd.controller import Controller
from app.config import EMAIL_DOMAIN, POSTFIX_SERVER, URL
from app.config import EMAIL_DOMAIN, POSTFIX_SERVER, URL, ALIAS_DOMAINS
from app.email_utils import (
get_email_name,
get_email_part,
@ -49,6 +49,7 @@ from app.email_utils import (
delete_header,
send_cannot_create_directory_alias,
send_cannot_create_domain_alias,
email_belongs_to_alias_domains,
)
from app.extensions import db
from app.log import LOG
@ -120,7 +121,7 @@ class MailHandler:
on_the_fly = False
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
if alias.endswith(EMAIL_DOMAIN):
if email_belongs_to_alias_domains(alias):
if "/" in alias or "+" in alias or "#" in alias:
if "/" in alias:
sep = "/"
@ -284,10 +285,10 @@ class MailHandler:
forward_email = ForwardEmail.get_by(reply_email=reply_email)
alias: str = forward_email.gen_email.email
# alias must end with EMAIL_DOMAIN or custom-domain
alias_domain = alias[alias.find("@") + 1 :]
if alias_domain != EMAIL_DOMAIN:
# alias must end with one of the ALIAS_DOMAINS or custom-domain
if not email_belongs_to_alias_domains(alias):
if not CustomDomain.get_by(domain=alias_domain):
return "550 alias unknown by SimpleLogin"
@ -338,9 +339,9 @@ class MailHandler:
envelope.rcpt_options,
)
if alias_domain == EMAIL_DOMAIN:
add_dkim_signature(msg, EMAIL_DOMAIN)
# add DKIM-Signature for non-custom-domain alias
if alias_domain in ALIAS_DOMAINS:
add_dkim_signature(msg, alias_domain)
# add DKIM-Signature for custom-domain alias
else:
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
if custom_domain.dkim_verified:

View file

@ -27,7 +27,8 @@ def test_different_scenarios(flask_client):
assert r.status_code == 200
assert r.json["can_create_custom"]
assert len(r.json["existing"]) == 1
assert r.json["custom"]["suffixes"]
assert len(r.json["custom"]["suffixes"]) == 3
assert r.json["custom"]["suggestion"] == "" # no hostname => no suggestion
# <<< with hostname >>>

View file

@ -1,8 +1,9 @@
from flask import url_for
from app.config import EMAIL_DOMAIN
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
from app.extensions import db
from app.models import User, ApiKey, GenEmail
from app.utils import random_word
def test_success(flask_client):
@ -15,14 +16,16 @@ def test_success(flask_client):
api_key = ApiKey.create(user.id, "for test")
db.session.commit()
word = random_word()
r = flask_client.post(
url_for("api.new_custom_alias", hostname="www.test.com"),
headers={"Authentication": api_key.code},
json={"alias_prefix": "prefix", "alias_suffix": f".abcdef@{EMAIL_DOMAIN}"},
json={"alias_prefix": "prefix", "alias_suffix": f".{word}@{EMAIL_DOMAIN}"},
)
assert r.status_code == 201
assert r.json["alias"] == f"prefix.abcdef@{EMAIL_DOMAIN}"
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
def test_out_of_quota(flask_client):
@ -35,15 +38,15 @@ def test_out_of_quota(flask_client):
api_key = ApiKey.create(user.id, "for test")
db.session.commit()
# create 3 custom alias to run out of quota
GenEmail.create_new(user.id, prefix="test")
GenEmail.create_new(user.id, prefix="test")
GenEmail.create_new(user.id, prefix="test")
# create MAX_NB_EMAIL_FREE_PLAN custom alias to run out of quota
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
GenEmail.create_new(user.id, prefix="test")
word = random_word()
r = flask_client.post(
url_for("api.new_custom_alias", hostname="www.test.com"),
headers={"Authentication": api_key.code},
json={"alias_prefix": "prefix", "alias_suffix": f".abcdef@{EMAIL_DOMAIN}"},
json={"alias_prefix": "prefix", "alias_suffix": f".{word}@{EMAIL_DOMAIN}"},
)
assert r.status_code == 400

View file

@ -5,6 +5,7 @@ URL=http://localhost
# Only print email content, not sending it
NOT_SEND_EMAIL=true
EMAIL_DOMAIN=sl.local
OTHER_ALIAS_DOMAINS=["d1.test", "d2.test"]
SUPPORT_EMAIL=support@sl.local
ADMIN_EMAIL=to_fill
# Max number emails user can generate for free plan

View file

@ -3,6 +3,7 @@ from app.email_utils import (
get_email_part,
get_email_local_part,
get_email_domain_part,
email_belongs_to_alias_domains,
)
@ -26,3 +27,12 @@ def test_get_email_local_part():
def test_get_email_domain_part():
assert get_email_domain_part("ab@cd.com") == "cd.com"
def test_email_belongs_to_alias_domains():
# default alias domain
assert email_belongs_to_alias_domains("ab@sl.local")
assert not email_belongs_to_alias_domains("ab@not-exist.local")
assert email_belongs_to_alias_domains("hey@d1.test")
assert not email_belongs_to_alias_domains("hey@d3.test")