Merge pull request #80 from simple-login/real-email

Multiple Mailboxes
This commit is contained in:
Son Nguyen Kim 2020-02-11 23:22:49 +07:00 committed by GitHub
commit df9af7c102
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 541 additions and 72 deletions

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ db.sqlite
.vscode
.DS_Store
config
static/node_modules
static/node_modules
db.sqlite-journal

View File

@ -34,7 +34,7 @@ def get_aliases():
except (ValueError, TypeError):
return jsonify(error="page_id must be provided in request query"), 400
aliases: [AliasInfo] = get_alias_info(user.id, page_id=page_id)
aliases: [AliasInfo] = get_alias_info(user, page_id=page_id)
return (
jsonify(

View File

@ -16,7 +16,7 @@ from app.extensions import db
from app.log import LOG
from app.models import User
from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email
from ...email_utils import can_be_used_as_personal_email, email_already_used
_authorization_base_url = "https://www.facebook.com/dialog/oauth"
_token_url = "https://graph.facebook.com/oauth/access_token"
@ -112,7 +112,7 @@ def facebook_callback():
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
if not can_be_used_as_personal_email(email):
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(
f"You cannot use {email} as your personal inbox.", "error",
)

View File

@ -6,7 +6,7 @@ from app import email_utils
from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login
from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL, DISABLE_REGISTRATION
from app.email_utils import can_be_used_as_personal_email
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db
from app.log import LOG
from app.models import User
@ -89,7 +89,7 @@ def github_callback():
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
if not can_be_used_as_personal_email(email):
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(
f"You cannot use {email} as your personal inbox.", "error",
)

View File

@ -10,7 +10,7 @@ from app.log import LOG
from app.models import User, File
from app.utils import random_string
from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email
from ...email_utils import can_be_used_as_personal_email, email_already_used
_authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth"
_token_url = "https://www.googleapis.com/oauth2/v4/token"
@ -97,7 +97,7 @@ def google_callback():
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
if not can_be_used_as_personal_email(email):
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(
f"You cannot use {email} as your personal inbox.", "error",
)

View File

@ -6,7 +6,7 @@ from wtforms import StringField, validators
from app import email_utils, config
from app.auth.base import auth_bp
from app.config import URL, DISABLE_REGISTRATION
from app.email_utils import can_be_used_as_personal_email
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db
from app.log import LOG
from app.models import User, ActivationCode
@ -41,9 +41,7 @@ def register():
"You cannot use this email address as your personal inbox.", "error",
)
else:
user = User.get_by(email=email)
if user:
if email_already_used(email):
flash(f"Email {email} already used", "error")
else:
LOG.debug("create user %s", form.email.data)

View File

@ -14,4 +14,5 @@ from .views import (
domain_detail,
lifetime_licence,
directory,
mailbox,
)

View File

@ -15,13 +15,23 @@
<div class="alert alert-primary" role="alert">
<p>
To send an email from your alias, just send the email to a special email address that we call
<em>reverse-alias</em>
and SimpleLogin will send it from the alias to the destination.
To send an email from your alias to someone, 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
</p>
<p>
Make sure you send the email from your personal email address ({{ current_user.email }}).
This special email address can <em>only</em> be used by you.
This might sound complicated but trust us, only the first time is a bit awkward.
</p>
<p>
{% if gen_email.mailbox_id %}
Make sure you send the email from the mailbox <b>{{ gen_email.mailbox.email }}</b>.
This is because only the mailbox that owns the alias can send emails from it.
{% else %}
Make sure you send the email from your personal email address ({{ current_user.email }}).
{% endif %}
</p>
</div>

View File

@ -48,6 +48,25 @@
</div>
</div>
{% if mailboxes|length > 1 %}
<div class="row mb-2">
<div class="col">
<select class="form-control" name="mailbox">
{% for mailbox in mailboxes %}
<option value="{{ mailbox }}">
{{ mailbox }}
</option>
{% endfor %}
</select>
<div class="small-text">
The mailbox that owns this alias.
</div>
</div>
</div>
{% else %}
<input type="hidden" name="mailbox" value="{{ mailboxes[0] }}">
{% endif %}
<div class="row mb-2">
<div class="col">
<textarea name="note"

View File

@ -134,6 +134,12 @@
<hr class="my-2">
{% if alias_info.mailbox != None %}
<div class="small-text">
Owned by <b>{{ alias_info.mailbox.email }}</b> mailbox
</div>
{% endif %}
<p class="small-text">
Created {{ gen_email.created_at | dt }}
{% if alias_info.highlight %}

View File

@ -0,0 +1,111 @@
{% extends 'default.html' %}
{% set active_page = "mailbox" %}
{% block title %}
Mailboxes
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> Mailboxes </h1>
{% 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="alert alert-primary" role="alert">
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the
mailbox that <em>owns</em> this alias, i.e: <br>
- all emails sent to this alias will be forwarded to this mailbox <br>
- from this mailbox, you can reply/send emails from the alias. <br>
By default, all aliases are owned by your email <b>{{ current_user.email }}</b>. <br><br>
The mailbox doesn't have to be your email: it can be your friend's email
if you want to create aliases for your buddy. <br>
They just need to validate this mailbox when they receive the activation email.
</div>
{% for mailbox in mailboxes %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">
{{ mailbox.email }}
{% if mailbox.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Not Verified">
🚫
</span>
{% endif %}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ mailbox.created_at | dt }} <br>
<span class="font-weight-bold">{{ mailbox.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="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<span class="card-link btn btn-link float-right delete-mailbox">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% if mailboxs|length > 0 %}
<hr>
{% endif %}
<form method="post" class="mt-6">
{{ new_mailbox_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold">Email</div>
<div class="small-text">
A verification email will be sent to this email to make sure you have access to this email.
</div>
{{ new_mailbox_form.email(class="form-control", placeholder="email@example.com",
autofocus="1") }}
{{ render_field_errors(new_mailbox_form.email) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-mailbox").on("click", function (e) {
let mailbox = $(this).parent().find(".mailbox").val();
notie.confirm({
text: `All aliases owned by this mailbox <b>${mailbox}</b> will be also deleted, ` +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -34,7 +34,10 @@
</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Directory (or Username)
<span class="badge badge-info">Coming Soon</span>
</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Multiple Mailboxes
<span class="badge badge-info">Beta</span>
</li>
</ul>

View File

@ -141,6 +141,7 @@ def alias_contact_manager(alias_id, forward_email_id=None):
"dashboard/alias_contact_manager.html",
forward_emails=forward_emails,
alias=gen_email.email,
gen_email=gen_email,
new_contact_form=new_contact_form,
forward_email_id=forward_email_id,
)

View File

@ -9,7 +9,7 @@ from app.dashboard.base import dashboard_bp
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
from app.extensions import db
from app.log import LOG
from app.models import GenEmail, CustomDomain, DeletedAlias
from app.models import GenEmail, CustomDomain, DeletedAlias, Mailbox
from app.utils import convert_to_id, random_word, word_exist
@ -43,11 +43,23 @@ def custom_alias():
)
)
mailboxes = [current_user.email]
for mailbox in Mailbox.query.filter_by(user_id=current_user.id):
mailboxes.append(mailbox.email)
if request.method == "POST":
alias_prefix = request.form.get("prefix")
alias_suffix = request.form.get("suffix")
mailbox_email = request.form.get("mailbox")
alias_note = request.form.get("note")
# check if mailbox is not tempered with
if mailbox_email != current_user.email:
mailbox = Mailbox.get_by(email=mailbox_email)
if not mailbox or mailbox.user_id != current_user.id:
flash("Something went wrong, please retry", "warning")
return redirect(url_for("dashboard.custom_alias"))
if verify_prefix_suffix(
current_user, alias_prefix, alias_suffix, user_custom_domains
):
@ -70,8 +82,14 @@ def custom_alias():
alias_domain = get_email_domain_part(full_alias)
custom_domain = CustomDomain.get_by(domain=alias_domain)
if custom_domain:
LOG.d("Set alias %s domain to %s", full_alias, custom_domain)
gen_email.custom_domain_id = custom_domain.id
if mailbox_email != current_user.email:
mailbox = Mailbox.get_by(email=mailbox_email)
gen_email.mailbox_id = mailbox.id
LOG.d("Set alias %s mailbox to %s", full_alias, mailbox)
db.session.commit()
flash(f"Alias {full_alias} has been created", "success")

View File

@ -16,12 +16,14 @@ from app.models import (
ForwardEmailLog,
DeletedAlias,
AliasGeneratorEnum,
Mailbox,
)
class AliasInfo:
id: int
gen_email: GenEmail
mailbox: Mailbox
nb_forward: int
nb_blocked: int
nb_reply: int
@ -154,7 +156,7 @@ def index():
return render_template(
"dashboard/index.html",
client_users=client_users,
aliases=get_alias_info(current_user.id, query, highlight_gen_email_id),
aliases=get_alias_info(current_user, query, highlight_gen_email_id),
highlight_gen_email_id=highlight_gen_email_id,
query=query,
AliasGeneratorEnum=AliasGeneratorEnum,
@ -162,7 +164,7 @@ def index():
def get_alias_info(
user_id, query=None, highlight_gen_email_id=None, page_id=None
user, query=None, highlight_gen_email_id=None, page_id=None
) -> [AliasInfo]:
if query:
query = query.strip().lower()
@ -170,12 +172,13 @@ def get_alias_info(
aliases = {} # dict of alias and AliasInfo
q = (
db.session.query(GenEmail, ForwardEmail, ForwardEmailLog)
db.session.query(GenEmail, ForwardEmail, ForwardEmailLog, Mailbox)
.join(ForwardEmail, GenEmail.id == ForwardEmail.gen_email_id, isouter=True)
.join(
ForwardEmailLog, ForwardEmail.id == ForwardEmailLog.forward_id, isouter=True
)
.filter(GenEmail.user_id == user_id)
.join(Mailbox, GenEmail.mailbox_id == Mailbox.id, isouter=True)
.filter(GenEmail.user_id == user.id)
.order_by(GenEmail.created_at.desc())
)
@ -188,7 +191,7 @@ def get_alias_info(
if page_id is not None:
q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
for ge, fe, fel in q:
for ge, fe, fel, mb in q:
if ge.email not in aliases:
aliases[ge.email] = AliasInfo(
id=ge.id,
@ -197,6 +200,7 @@ def get_alias_info(
nb_forward=0,
nb_reply=0,
highlight=ge.id == highlight_gen_email_id,
mailbox=mb,
)
alias_info = aliases[ge.email]

View File

@ -0,0 +1,134 @@
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from itsdangerous import Signer, BadSignature
from wtforms import validators
from wtforms.fields.html5 import EmailField
from app.config import EMAIL_DOMAIN, ALIAS_DOMAINS, FLASK_SECRET, URL
from app.dashboard.base import dashboard_bp
from app.email_utils import (
send_email,
render,
can_be_used_as_personal_email,
email_already_used,
)
from app.extensions import db
from app.log import LOG
from app.models import Mailbox
class NewMailboxForm(FlaskForm):
email = EmailField(
"email", validators=[validators.DataRequired(), validators.Email()]
)
@dashboard_bp.route("/mailbox", methods=["GET", "POST"])
@login_required
def mailbox_route():
if not current_user.can_use_multiple_mailbox:
flash("You don't have access to this page, redirect to home page", "warning")
return redirect(url_for("dashboard.index"))
mailboxes = Mailbox.query.filter_by(user_id=current_user.id).all()
new_mailbox_form = NewMailboxForm()
if request.method == "POST":
if request.form.get("form-name") == "delete":
mailbox_id = request.form.get("mailbox-id")
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.id:
flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.mailbox_route"))
email = mailbox.email
Mailbox.delete(mailbox_id)
db.session.commit()
flash(f"Mailbox {email} has been deleted", "success")
return redirect(url_for("dashboard.mailbox_route"))
elif request.form.get("form-name") == "create":
# todo: only premium user can add additional mailbox?
# if not current_user.is_premium():
# flash("Only premium plan can add additional mailbox", "warning")
# return redirect(url_for("dashboard.mailbox_route"))
if new_mailbox_form.validate():
mailbox_email = new_mailbox_form.email.data.lower()
if email_already_used(mailbox_email):
flash(f"{mailbox_email} already used", "error")
elif not can_be_used_as_personal_email(mailbox_email):
flash(
f"You cannot use {mailbox_email}.", "error",
)
else:
new_mailbox = Mailbox.create(
email=mailbox_email, user_id=current_user.id
)
db.session.commit()
s = Signer(FLASK_SECRET)
mailbox_id_signed = s.sign(str(new_mailbox.id)).decode()
verification_url = (
URL
+ "/dashboard/mailbox_verify"
+ f"?mailbox_id={mailbox_id_signed}"
)
send_email(
mailbox_email,
f"Please confirm your email {mailbox_email}",
render(
"transactional/verify-mailbox.txt",
user=current_user,
link=verification_url,
mailbox_email=mailbox_email,
),
render(
"transactional/verify-mailbox.html",
user=current_user,
link=verification_url,
mailbox_email=mailbox_email,
),
)
flash(
f"You are going to receive an email to confirm {mailbox_email}.",
"success",
)
return redirect(url_for("dashboard.mailbox_route"))
return render_template(
"dashboard/mailbox.html",
mailboxes=mailboxes,
new_mailbox_form=new_mailbox_form,
EMAIL_DOMAIN=EMAIL_DOMAIN,
ALIAS_DOMAINS=ALIAS_DOMAINS,
)
@dashboard_bp.route("/mailbox_verify")
def mailbox_verify():
s = Signer(FLASK_SECRET)
mailbox_id = request.args.get("mailbox_id")
try:
r_id = int(s.unsign(mailbox_id))
except BadSignature:
flash("Invalid link. Please delete and re-add your mailbox", "error")
else:
mailbox = Mailbox.get(r_id)
mailbox.verified = True
db.session.commit()
LOG.d("Mailbox %s is verified", mailbox)
flash(
f"The {mailbox.email} is now verified, you can start creating alias with it",
"success",
)
return redirect(url_for("dashboard.mailbox_route"))

View File

@ -11,7 +11,7 @@ from wtforms import StringField, validators
from app import s3, email_utils
from app.config import URL
from app.dashboard.base import dashboard_bp
from app.email_utils import can_be_used_as_personal_email
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db
from app.log import LOG
from app.models import (
@ -88,7 +88,7 @@ def setting():
# check if this email is not used by other user, or as alias
if (
User.get_by(email=new_email)
email_already_used(new_email)
or GenEmail.get_by(email=new_email)
or DeletedAlias.get_by(email=new_email)
):

View File

@ -18,6 +18,7 @@ from app.config import (
SUPPORT_NAME,
)
from app.log import LOG
from app.models import Mailbox, User
def render(template_name, **kwargs) -> str:
@ -166,31 +167,6 @@ def send_cannot_create_domain_alias(user, alias, domain):
)
def send_reply_alias_must_use_personal_email(user, alias, sender):
"""
The reply_email can be used only by user personal email.
Notify user if it's used by someone else
"""
send_email(
user.email,
f"Reply from your alias {alias} only works with your personal email",
render(
"transactional/reply-must-use-personal-email.txt",
name=user.name,
alias=alias,
sender=sender,
user_email=user.email,
),
render(
"transactional/reply-must-use-personal-email.html",
name=user.name,
alias=alias,
sender=sender,
user_email=user.email,
),
)
def send_email(to_email, subject, plaintext, html):
if NOT_SEND_EMAIL:
LOG.d(
@ -210,7 +186,7 @@ def send_email(to_email, subject, plaintext, html):
msg["To"] = to_email
msg.set_content(plaintext)
if html is not None:
if html:
msg.add_alternative(html, subtype="html")
msg_id_header = make_msgid()
@ -330,3 +306,17 @@ def can_be_used_as_personal_email(email: str) -> bool:
return False
return True
def email_already_used(email: str) -> bool:
"""test if an email can be used when:
- user signs up
- add a new mailbox
"""
if User.get_by(email=email):
return True
if Mailbox.get_by(email=email):
return True
return False

View File

@ -17,7 +17,7 @@ from app.config import (
AVATAR_URL_EXPIRATION,
JOB_ONBOARDING_1,
)
from app.email_utils import get_email_name
from app.extensions import db
from app.log import LOG
from app.oauth_models import Scope
@ -133,6 +133,10 @@ class User(db.Model, ModelMixin, UserMixin):
ArrowType, default=lambda: arrow.now().shift(days=7, hours=1), nullable=True
)
can_use_multiple_mailbox = db.Column(
db.Boolean, default=False, nullable=False, server_default="0"
)
profile_picture = db.relationship(File)
@classmethod
@ -478,7 +482,13 @@ class GenEmail(db.Model, ModelMixin):
note = db.Column(db.Text, default=None, nullable=True)
# an alias can be owned by another mailbox
mailbox_id = db.Column(
db.ForeignKey("mailbox.id", ondelete="cascade"), nullable=True, default=None
)
user = db.relationship(User)
mailbox = db.relationship("Mailbox")
@classmethod
def create_new(cls, user_id, prefix, note=None):
@ -626,6 +636,8 @@ class ForwardEmail(db.Model, ModelMixin):
def website_send_to(self):
"""return the email address with name.
to use when user wants to send an email from the alias"""
from app.email_utils import get_email_name
if self.website_from:
name = get_email_name(self.website_from)
if name:
@ -799,3 +811,17 @@ class Job(db.Model, ModelMixin):
def __repr__(self):
return f"<Job {self.id} {self.name} {self.payload}>"
class Mailbox(db.Model, ModelMixin):
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
email = db.Column(db.String(256), unique=True, nullable=False)
verified = db.Column(db.Boolean, default=False, nullable=False)
user = db.relationship(User)
def nb_alias(self):
return GenEmail.filter_by(mailbox_id=self.id).count()
def __repr__(self):
return f"<Mailbox {self.email}>"

View File

@ -50,7 +50,7 @@ from app.email_utils import (
send_cannot_create_directory_alias,
send_cannot_create_domain_alias,
email_belongs_to_alias_domains,
send_reply_alias_must_use_personal_email,
render,
)
from app.extensions import db
from app.log import LOG
@ -199,7 +199,10 @@ class MailHandler:
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
return "510 Email not exist"
user_email = gen_email.user.email
if gen_email.mailbox_id:
mailbox_email = gen_email.mailbox.email
else:
mailbox_email = gen_email.user.email
website_email = get_email_part(msg["From"])
@ -267,7 +270,7 @@ class MailHandler:
LOG.d(
"Forward mail from %s to %s, mail_options %s, rcpt_options %s ",
website_email,
user_email,
mailbox_email,
envelope.mail_options,
envelope.rcpt_options,
)
@ -277,7 +280,7 @@ class MailHandler:
msg_raw = msg.as_string().encode()
smtp.sendmail(
forward_email.reply_email,
user_email,
mailbox_email,
msg_raw,
envelope.mail_options,
envelope.rcpt_options,
@ -310,26 +313,50 @@ class MailHandler:
if not CustomDomain.get_by(domain=alias_domain):
return "550 alias unknown by SimpleLogin"
user_email = forward_email.gen_email.user.email
if envelope.mail_from.lower() != user_email.lower():
gen_email = forward_email.gen_email
if gen_email.mailbox_id:
mailbox_email = gen_email.mailbox.email
else:
mailbox_email = gen_email.user.email
if envelope.mail_from.lower() != mailbox_email.lower():
LOG.warning(
f"Reply email can only be used by user email. Actual mail_from: %s. msg from header: %s, User email %s. reply_email %s",
envelope.mail_from,
msg["From"],
user_email,
mailbox_email,
reply_email,
)
send_reply_alias_must_use_personal_email(
forward_email.gen_email.user,
forward_email.gen_email.email,
envelope.mail_from,
user = gen_email.user
send_email(
mailbox_email,
f"Reply from your alias {alias} only works from your mailbox",
render(
"transactional/reply-must-use-personal-email.txt",
name=user.name,
alias=alias,
sender=envelope.mail_from,
mailbox_email=mailbox_email,
),
render(
"transactional/reply-must-use-personal-email.html",
name=user.name,
alias=alias,
sender=envelope.mail_from,
mailbox_email=mailbox_email,
),
)
# Notify sender that they cannot send emails to this address
send_email(
envelope.mail_from,
f"Your email ({envelope.mail_from}) is not allowed to send email to {reply_email}",
"",
f"Your email ({envelope.mail_from}) is not allowed to send emails to {reply_email}",
render(
"transactional/send-from-alias-from-unknown-sender.txt",
sender=envelope.mail_from,
reply_email=reply_email,
),
"",
)

View File

@ -0,0 +1,43 @@
"""empty message
Revision ID: 6664d75ce3d4
Revises: b9f849432543
Create Date: 2020-02-10 23:10:09.134369
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6664d75ce3d4'
down_revision = 'b9f849432543'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mailbox',
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('email', sa.String(length=256), nullable=False),
sa.Column('verified', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.add_column('gen_email', sa.Column('mailbox_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'gen_email', 'mailbox', ['mailbox_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', 'mailbox_id')
op.drop_table('mailbox')
# ### end Alembic commands ###

View File

@ -0,0 +1,29 @@
"""empty message
Revision ID: 3c9542fc54e9
Revises: 6664d75ce3d4
Create Date: 2020-02-11 22:28:58.017384
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3c9542fc54e9'
down_revision = '6664d75ce3d4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('can_use_multiple_mailbox', sa.Boolean(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'can_use_multiple_mailbox')
# ### end Alembic commands ###

View File

@ -48,6 +48,7 @@ from app.models import (
CustomDomain,
LifetimeCoupon,
Directory,
Mailbox,
)
from app.monitor.base import monitor_bp
from app.oauth.base import oauth_bp
@ -129,6 +130,7 @@ def fake_data():
activated=True,
is_admin=True,
otp_secret="base32secret3232",
can_use_multiple_mailbox=True,
)
db.session.commit()
@ -179,6 +181,7 @@ def fake_data():
client2.published = True
db.session.commit()
Mailbox.create(user_id=user.id, email="ab@cd.ef", verified=True)
db.session.commit()

View File

@ -2,7 +2,8 @@ Hi {{name}} <br><br>
We have recorded an attempt to send an email from your alias <b>{{ alias }}</b> using <b>{{ sender }}</b>. <br><br>
Please note that sending from alias only works from your personal email ({{user_email}}): no one else could send emails on behalf of your alias. <br><br>
Please note that sending from this alias only works from {{mailbox_email}}. <br>
Indeed, only you (or the mailbox that owns {{ alias }}) can send emails on behalf of this alias. <br><br>
Best, <br>
SimpleLogin team.

View File

@ -2,7 +2,7 @@ Hi {{name}}
We have recorded an attempt to send an email from your alias {{ alias }} using {{ sender }}.
Please note that sending from alias only works from your personal email ({{user_email}}): no one else could send emails on behalf of your alias.
Please note that sending from this alias only works from {{mailbox_email}}: only you (i.e. no one else) can send emails on behalf of your alias.
Best,
SimpleLogin team.

View File

@ -0,0 +1,13 @@
Hi,
This is an automated email from SimpleLogin.
We have recorded an attempt to send an email from your email ({{sender}}) to {{reply_email}}.
{{reply_email}} is a special email address that only receives emails from its authorized user.
This user has been also informed of this incident.
If you have any question, you can contact us by replying to this email or consult our website at https://simplelogin.io.
Regards,
SimpleLogin team.

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
{{ render_text("Hi " + user.name) }}
{{ render_text("You have added "+ mailbox_email +" as an additional mailbox.") }}
{{ render_text("To confirm, please click on the button below or use this link: <br> " + link) }}
{{ render_button("Confirm email", link) }}
{% endblock %}

View File

@ -0,0 +1,10 @@
Hi {{user.name}}
You have added {{mailbox_email}} as an additional mailbox.
To confirm, please click on this link:
{{link}}
Regards,
Son - SimpleLogin founder.

View File

@ -27,7 +27,6 @@
<a href="{{ url_for('dashboard.custom_domain') }}"
class="nav-link {{ 'active' if active_page == 'custom_domain' }}">
<i class="fe fe-server"></i> Custom Domains
<span class="badge badge-success" style="font-size: .5rem; top: 5px">Premium</span>
</a>
</li>
@ -35,10 +34,19 @@
<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>
{% if current_user.can_use_multiple_mailbox %}
<li class="nav-item">
<a href="{{ url_for('dashboard.mailbox_route') }}"
class="nav-link {{ 'active' if active_page == 'mailbox' }}">
<i class="fe fe-inbox"></i> Mailboxes
<span class="badge badge-info" style="font-size: .5rem; top: 5px">Beta</span>
</a>
</li>
{% endif %}
<!--
<li class="nav-item">
<a href="{{ url_for('discover.index') }}"

View File

@ -7,14 +7,18 @@ from tests.utils import login
def test_add_alias_success(flask_client):
login(flask_client)
user = login(flask_client)
db.session.commit()
word = random_word()
r = flask_client.post(
url_for("dashboard.custom_alias"),
data={"prefix": "prefix", "suffix": f".{word}@{EMAIL_DOMAIN}"},
data={
"prefix": "prefix",
"suffix": f".{word}@{EMAIL_DOMAIN}",
"mailbox": user.email,
},
follow_redirects=True,
)