Merge pull request #91 from simple-login/mailbox2

Improve Mailbox
This commit is contained in:
Son Nguyen Kim 2020-02-23 16:39:44 +07:00 committed by GitHub
commit e8e0923de7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 510 additions and 46 deletions

View File

@ -5,7 +5,7 @@
{% endblock %} {% endblock %}
{% block single_content %} {% block single_content %}
<div class="text-center"> <div class="text-center bg-white p-5" style="max-width: 50rem">
<h1> <h1>
An email to validate your email is on its way. An email to validate your email is on its way.
</h1> </h1>

View File

@ -16,4 +16,5 @@ from .views import (
directory, directory,
mailbox, mailbox,
deleted_alias, deleted_alias,
mailbox_detail,
) )

View File

@ -40,7 +40,7 @@
<div class="col-sm-6 p-1"> <div class="col-sm-6 p-1">
<select class="form-control" name="suffix"> <select class="form-control custom-select" name="suffix">
{% for suffix in suffixes %} {% for suffix in suffixes %}
<option value="{{ suffix[1] }}"> <option value="{{ suffix[1] }}">
{% if suffix[0] %} {% if suffix[0] %}
@ -54,10 +54,10 @@
</div> </div>
</div> </div>
{% if mailboxes|length > 1 %} {% if mailboxes|length > 1 or current_user.full_mailbox %}
<div class="row mb-2"> <div class="row mb-2">
<div class="col p-1"> <div class="col p-1">
<select class="form-control" name="mailbox"> <select class="form-control custom-select" name="mailbox">
{% for mailbox in mailboxes %} {% for mailbox in mailboxes %}
<option value="{{ mailbox }}"> <option value="{{ mailbox }}">
{{ mailbox }} {{ mailbox }}

View File

@ -57,7 +57,7 @@
<input type="hidden" name="form-name" value="delete"> <input type="hidden" name="form-name" value="delete">
<input type="hidden" class="dir-name" value="{{ dir.name }}"> <input type="hidden" class="dir-name" value="{{ dir.name }}">
<input type="hidden" name="dir-id" value="{{ dir.id }}"> <input type="hidden" name="dir-id" value="{{ dir.id }}">
<span class="card-link btn btn-link float-right delete-dir"> <span class="card-link btn btn-link float-right text-danger delete-dir">
Delete Delete
</span> </span>
</form> </form>
@ -85,8 +85,7 @@
{{ new_dir_form.name(class="form-control", placeholder="my-directory", {{ new_dir_form.name(class="form-control", placeholder="my-directory",
pattern="[0-9a-z-_]{3,}", pattern="[0-9a-z-_]{3,}",
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.", 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) }} {{ render_field_errors(new_dir_form.name) }}
<button class="btn btn-lg btn-success mt-2">Create</button> <button class="btn btn-lg btn-success mt-2">Create</button>
</form> </form>

View File

@ -134,12 +134,6 @@
<hr class="my-2"> <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"> <p class="small-text">
Created {{ gen_email.created_at | dt }} Created {{ gen_email.created_at | dt }}
{% if alias_info.highlight %} {% if alias_info.highlight %}
@ -157,10 +151,42 @@
</a> </a>
</div> </div>
{% if current_user.full_mailbox and mailboxes|length > 1 %}
<form method="post">
<div class="small-text mt-2">Current mailbox</div>
<div class="row">
<div class="col-lg-10">
<select class="form-control form-control-sm custom-select" name="mailbox">
{% for mailbox in mailboxes %}
<option value="{{ mailbox }}" {% if mailbox == alias_info.mailbox.email %} selected {% endif %}>
{{ mailbox }}
</option>
{% endfor %}
</select>
</div>
<div class="col-lg-2">
<input type="hidden" name="form-name" value="set-mailbox">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<button class="btn btn-sm btn-outline-info w-100">
Update
</button>
</div>
</div>
</form>
{% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %}
<div class="small-text">
Owned by <b>{{ alias_info.mailbox.email }}</b> mailbox
</div>
{% endif %}
<form method="post"> <form method="post">
<div class="row mt-2"> <div class="row mt-2">
<div class="col-10"> <div class="col-lg-10">
<textarea <textarea
name="note" name="note"
class="form-control" class="form-control"
@ -168,11 +194,11 @@
placeholder="Alias Note.">{{ gen_email.note or "" }}</textarea> placeholder="Alias Note.">{{ gen_email.note or "" }}</textarea>
</div> </div>
<div class="col-2"> <div class="col-lg-2">
<input type="hidden" name="form-name" value="set-note"> <input type="hidden" name="form-name" value="set-note">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}"> <input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<button class="btn btn-sm btn-outline-success"> <button class="btn btn-sm btn-outline-success w-100">
Save Save
</button> </button>
</div> </div>

View File

@ -20,13 +20,15 @@
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the 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> mailbox that <em>owns</em> this alias, i.e: <br>
- all emails sent to this alias will be forwarded to this mailbox <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> - from this mailbox, you can reply/send emails from the alias. <br><br>
By default, all aliases are owned by your email <b>{{ current_user.email }}</b>. <br><br> {% if current_user.full_mailbox %}
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
<br><br>
{% endif %}
The mailbox doesn't have to be your email: it can be your friend's email 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> if you want to create aliases for your buddy.
They just need to validate this mailbox when they receive the activation email.
</div> </div>
{% for mailbox in mailboxes %} {% for mailbox in mailboxes %}
@ -41,22 +43,46 @@
🚫 🚫
</span> </span>
{% endif %} {% endif %}
{% if mailbox.id == current_user.default_mailbox_id %}
<div class="badge badge-primary float-right" data-toggle="tooltip"
title="When a new random alias is created, it belongs to the default mailbox">Default Mailbox
</div>
{% endif %}
</h5> </h5>
<h6 class="card-subtitle mb-2 text-muted"> <h6 class="card-subtitle mb-2 text-muted">
Created {{ mailbox.created_at | dt }} <br> Created {{ mailbox.created_at | dt }} <br>
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases. <span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases. <br>
</h6> </h6>
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit ➡</a>
</div> </div>
<div class="card-footer p-0"> <div class="card-footer p-0">
<div class="row"> <div class="row">
{% if mailbox.verified and current_user.full_mailbox %}
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="set-default">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<button class="card-link btn btn-link
{% if mailbox.id == current_user.default_mailbox_id %} disabled {% endif %}"
>
Set As Default Mailbox
</button>
</form>
</div>
{% endif %}
<div class="col"> <div class="col">
<form method="post"> <form method="post">
<input type="hidden" name="form-name" value="delete"> <input type="hidden" name="form-name" value="delete">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}"> <input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}"> <input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<span class="card-link btn btn-link float-right delete-mailbox"> <span class="card-link btn btn-link text-danger float-right delete-mailbox
{% if mailbox.id == current_user.default_mailbox_id %} disabled {% endif %}">
Delete Delete
</span> </span>
</form> </form>
@ -81,8 +107,7 @@
A verification email will be sent to this email to make sure you have access to this email. A verification email will be sent to this email to make sure you have access to this email.
</div> </div>
{{ new_mailbox_form.email(class="form-control", placeholder="email@example.com", {{ new_mailbox_form.email(class="form-control", placeholder="email@example.com") }}
autofocus="1") }}
{{ render_field_errors(new_mailbox_form.email) }} {{ render_field_errors(new_mailbox_form.email) }}
<button class="btn btn-lg btn-success mt-2">Create</button> <button class="btn btn-lg btn-success mt-2">Create</button>
</form> </form>

View File

@ -0,0 +1,58 @@
{% extends 'default.html' %}
{% set active_page = "mailbox" %}
{% block title %}
Mailbox {{ mailbox.email }}
{% endblock %}
{% block default_content %}
<div class="col-md-8 offset-md-2 pb-3">
<h1 class="h3">{{ 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 %}
</h1>
<!-- Change email -->
<div class="card">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form-name" value="update-email">
{{ change_email_form.csrf_token }}
<div class="card-body">
<div class="card-title">
Change Mailbox Address
</div>
<div class="form-group">
<label class="form-label">Address</label>
<!-- Not allow user to change mailbox if there's a pending change -->
{{ change_email_form.email(class="form-control", value=mailbox.email, readonly=pending_email != None) }}
{{ render_field_errors(change_email_form.email) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
class="btn btn-secondary btn-sm">
Cancel mailbox change
</a>
</div>
{% endif %}
</div>
<button class="btn btn-primary">Change</button>
</div>
</form>
</div>
<!-- END Change email -->
</div>
{% endblock %}

View File

@ -43,9 +43,7 @@ def custom_alias():
) )
) )
mailboxes = [current_user.email] mailboxes = current_user.mailboxes()
for mailbox in Mailbox.query.filter_by(user_id=current_user.id, verified=True):
mailboxes.append(mailbox.email)
if request.method == "POST": if request.method == "POST":
alias_prefix = request.form.get("prefix") alias_prefix = request.form.get("prefix")
@ -85,7 +83,8 @@ def custom_alias():
LOG.d("Set alias %s domain to %s", full_alias, custom_domain) LOG.d("Set alias %s domain to %s", full_alias, custom_domain)
gen_email.custom_domain_id = custom_domain.id gen_email.custom_domain_id = custom_domain.id
if mailbox_email != current_user.email: # assign alias to a mailbox
if current_user.full_mailbox or mailbox_email != current_user.email:
mailbox = Mailbox.get_by(email=mailbox_email) mailbox = Mailbox.get_by(email=mailbox_email)
gen_email.mailbox_id = mailbox.id gen_email.mailbox_id = mailbox.id
LOG.d("Set alias %s mailbox to %s", full_alias, mailbox) LOG.d("Set alias %s mailbox to %s", full_alias, mailbox)

View File

@ -74,6 +74,16 @@ def index():
gen_email = GenEmail.create_new_random( gen_email = GenEmail.create_new_random(
user_id=current_user.id, scheme=scheme user_id=current_user.id, scheme=scheme
) )
if current_user.full_mailbox:
if not current_user.default_mailbox_id:
LOG.error(
"Full mailbox User %s does not have default mailbox ",
current_user,
)
else:
gen_email.mailbox_id = current_user.default_mailbox_id
db.session.commit() db.session.commit()
LOG.d("generate new email %s for user %s", gen_email, current_user) LOG.d("generate new email %s for user %s", gen_email, current_user)
@ -142,6 +152,31 @@ def index():
) )
) )
elif request.form.get("form-name") == "set-mailbox":
gen_email_id = request.form.get("gen-email-id")
gen_email: GenEmail = GenEmail.get(gen_email_id)
mailbox_email = request.form.get("mailbox")
mailbox = Mailbox.get_by(email=mailbox_email)
if not mailbox or mailbox.user_id != current_user.id:
flash("Something went wrong, please retry", "warning")
else:
gen_email.mailbox_id = mailbox.id
db.session.commit()
LOG.d("Set alias %s mailbox to %s", gen_email, mailbox)
flash(
f"Update mailbox for {gen_email.email} to {mailbox_email}",
"success",
)
return redirect(
url_for(
"dashboard.index",
highlight_gen_email_id=gen_email.id,
query=query,
)
)
return redirect(url_for("dashboard.index", query=query)) return redirect(url_for("dashboard.index", query=query))
client_users = ( client_users = (
@ -153,6 +188,8 @@ def index():
sorted(client_users, key=lambda cu: cu.client.name) sorted(client_users, key=lambda cu: cu.client.name)
mailboxes = current_user.mailboxes()
return render_template( return render_template(
"dashboard/index.html", "dashboard/index.html",
client_users=client_users, client_users=client_users,
@ -160,6 +197,7 @@ def index():
highlight_gen_email_id=highlight_gen_email_id, highlight_gen_email_id=highlight_gen_email_id,
query=query, query=query,
AliasGeneratorEnum=AliasGeneratorEnum, AliasGeneratorEnum=AliasGeneratorEnum,
mailboxes=mailboxes,
) )

View File

@ -27,7 +27,7 @@ class NewMailboxForm(FlaskForm):
@dashboard_bp.route("/mailbox", methods=["GET", "POST"]) @dashboard_bp.route("/mailbox", methods=["GET", "POST"])
@login_required @login_required
def mailbox_route(): def mailbox_route():
if not current_user.can_use_multiple_mailbox: if not current_user.can_use_multiple_mailbox and not current_user.full_mailbox:
flash("You don't have access to this page, redirect to home page", "warning") flash("You don't have access to this page, redirect to home page", "warning")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
@ -44,11 +44,36 @@ def mailbox_route():
flash("Unknown error. Refresh the page", "warning") flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.mailbox_route")) return redirect(url_for("dashboard.mailbox_route"))
if mailbox.id == current_user.default_mailbox_id:
flash("You cannot delete default mailbox", "error")
return redirect(url_for("dashboard.mailbox_route"))
email = mailbox.email email = mailbox.email
Mailbox.delete(mailbox_id) Mailbox.delete(mailbox_id)
db.session.commit() db.session.commit()
flash(f"Mailbox {email} has been deleted", "success") flash(f"Mailbox {email} has been deleted", "success")
return redirect(url_for("dashboard.mailbox_route"))
if request.form.get("form-name") == "set-default":
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"))
if mailbox.id == current_user.default_mailbox_id:
flash("This mailbox is already default one", "error")
return redirect(url_for("dashboard.mailbox_route"))
if not mailbox.verified:
flash("Cannot set unverified mailbox as default", "error")
return redirect(url_for("dashboard.mailbox_route"))
current_user.default_mailbox_id = mailbox.id
db.session.commit()
flash(f"Mailbox {mailbox.email} is set as Default Mailbox", "success")
return redirect(url_for("dashboard.mailbox_route")) return redirect(url_for("dashboard.mailbox_route"))
elif request.form.get("form-name") == "create": elif request.form.get("form-name") == "create":

View File

@ -0,0 +1,150 @@
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 FLASK_SECRET
from app.config import URL
from app.dashboard.base import dashboard_bp
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.email_utils import (
send_email,
render,
)
from app.extensions import db
from app.log import LOG
from app.models import (
GenEmail,
DeletedAlias,
)
from app.models import Mailbox
class ChangeEmailForm(FlaskForm):
email = EmailField(
"email", validators=[validators.DataRequired(), validators.Email()]
)
@dashboard_bp.route("/mailbox/<int:mailbox_id>/", methods=["GET", "POST"])
@login_required
def mailbox_detail_route(mailbox_id):
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
change_email_form = ChangeEmailForm()
if mailbox.new_email:
pending_email = mailbox.new_email
else:
pending_email = None
if change_email_form.validate_on_submit():
new_email = change_email_form.email.data
if new_email != mailbox.email and not pending_email:
# check if this email is not already used
if (
email_already_used(new_email)
or GenEmail.get_by(email=new_email)
or DeletedAlias.get_by(email=new_email)
):
flash(f"Email {new_email} already used", "error")
elif not can_be_used_as_personal_email(new_email):
flash(
"You cannot use this email address as your mailbox", "error",
)
else:
mailbox.new_email = new_email
db.session.commit()
s = Signer(FLASK_SECRET)
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
verification_url = (
URL
+ "/dashboard/mailbox/confirm_change"
+ f"?mailbox_id={mailbox_id_signed}"
)
send_email(
new_email,
f"Confirm mailbox change on SimpleLogin",
render(
"transactional/verify-mailbox-change.txt",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
render(
"transactional/verify-mailbox-change.html",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
)
flash(
f"You are going to receive an email to confirm {new_email}.",
"success",
)
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
return render_template("dashboard/mailbox_detail.html", **locals(),)
@dashboard_bp.route(
"/mailbox/<int:mailbox_id>/cancel_email_change", methods=["GET", "POST"]
)
@login_required
def cancel_mailbox_change_route(mailbox_id):
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
if mailbox.new_email:
mailbox.new_email = None
db.session.commit()
flash("Your mailbox change is cancelled", "success")
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
else:
flash("You have no pending mailbox change", "warning")
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
@dashboard_bp.route("/mailbox/confirm_change")
def mailbox_confirm_change_route():
s = Signer(FLASK_SECRET)
mailbox_id = request.args.get("mailbox_id")
try:
r_id = int(s.unsign(mailbox_id))
except BadSignature:
flash("Invalid link", "error")
else:
mailbox = Mailbox.get(r_id)
mailbox.email = mailbox.new_email
mailbox.new_email = None
# mark mailbox as verified if the change request is sent from an unverified mailbox
mailbox.verified = True
db.session.commit()
LOG.d("Mailbox change %s is verified", mailbox)
flash(
f"The {mailbox.email} is updated", "success",
)
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
)

View File

@ -185,6 +185,8 @@ def send_email(
) )
return return
LOG.d("send email to %s, subject %s", to_email, subject)
# host IP, setup via Docker network # host IP, setup via Docker network
smtp = SMTP(POSTFIX_SERVER, 25) smtp = SMTP(POSTFIX_SERVER, 25)
@ -214,11 +216,9 @@ def send_email(
msg["To"] = to_email msg["To"] = to_email
msg_id_header = make_msgid() msg_id_header = make_msgid()
LOG.d("message-id %s", msg_id_header)
msg["Message-ID"] = msg_id_header msg["Message-ID"] = msg_id_header
date_header = formatdate() date_header = formatdate()
LOG.d("Date header: %s", date_header)
msg["Date"] = date_header msg["Date"] = date_header
# add DKIM # add DKIM

View File

@ -137,6 +137,19 @@ class User(db.Model, ModelMixin, UserMixin):
db.Boolean, default=False, nullable=False, server_default="0" db.Boolean, default=False, nullable=False, server_default="0"
) )
# only use mailbox instead of default to user email
# this requires a migration before to:
# 1. create default mailbox for the user email address
# 2. assign existing aliases to this default mailbox
full_mailbox = db.Column(
db.Boolean, default=False, nullable=False, server_default="0"
)
# the mailbox used when create random alias
default_mailbox_id = db.Column(
db.ForeignKey("mailbox.id"), nullable=True, default=None
)
profile_picture = db.relationship(File) profile_picture = db.relationship(File)
@classmethod @classmethod
@ -154,6 +167,14 @@ class User(db.Model, ModelMixin, UserMixin):
GenEmail.create_new(user.id, prefix="my-first-alias") GenEmail.create_new(user.id, prefix="my-first-alias")
db.session.flush() db.session.flush()
# todo: uncomment when all existing users are full_mailbox
# to run just after migrating all existing user to full mailbox
# so new users are automatically full-mailbox
# mb = Mailbox.create(user_id=user.id, email=user.email, verified=True)
# db.session.flush()
# user.full_mailbox = True
# user.default_mailbox_id = mb.id
# Schedule onboarding emails # Schedule onboarding emails
Job.create( Job.create(
name=JOB_ONBOARDING_1, name=JOB_ONBOARDING_1,
@ -270,6 +291,18 @@ class User(db.Model, ModelMixin, UserMixin):
def verified_custom_domains(self): def verified_custom_domains(self):
return CustomDomain.query.filter_by(user_id=self.id, verified=True).all() return CustomDomain.query.filter_by(user_id=self.id, verified=True).all()
def mailboxes(self) -> [str]:
"""list of mailbox emails that user own"""
if self.full_mailbox:
mailboxes = []
else:
mailboxes = [self.email]
for mailbox in Mailbox.query.filter_by(user_id=self.id, verified=True):
mailboxes.append(mailbox.email)
return mailboxes
def __repr__(self): def __repr__(self):
return f"<User {self.id} {self.name} {self.email}>" return f"<User {self.id} {self.name} {self.email}>"
@ -491,7 +524,7 @@ class GenEmail(db.Model, ModelMixin):
mailbox = db.relationship("Mailbox") mailbox = db.relationship("Mailbox")
@classmethod @classmethod
def create_new(cls, user_id, prefix, note=None): def create_new(cls, user_id, prefix, note=None, mailbox_id=None):
if not prefix: if not prefix:
raise Exception("alias prefix cannot be empty") raise Exception("alias prefix cannot be empty")
@ -503,7 +536,9 @@ class GenEmail(db.Model, ModelMixin):
if not cls.get_by(email=email): if not cls.get_by(email=email):
break break
return GenEmail.create(user_id=user_id, email=email, note=note) return GenEmail.create(
user_id=user_id, email=email, note=note, mailbox_id=mailbox_id
)
@classmethod @classmethod
def create_new_random( def create_new_random(
@ -828,7 +863,8 @@ class Mailbox(db.Model, ModelMixin):
email = db.Column(db.String(256), unique=True, nullable=False) email = db.Column(db.String(256), unique=True, nullable=False)
verified = db.Column(db.Boolean, default=False, nullable=False) verified = db.Column(db.Boolean, default=False, nullable=False)
user = db.relationship(User) # used when user wants to update mailbox email
new_email = db.Column(db.String(256), unique=True)
def nb_alias(self): def nb_alias(self):
return GenEmail.filter_by(mailbox_id=self.id).count() return GenEmail.filter_by(mailbox_id=self.id).count()

View File

@ -106,7 +106,7 @@
<div class="col-sm-6" <div class="col-sm-6"
style="padding-left: 5px"> style="padding-left: 5px">
<select class="form-control" name="suffix"> <select class="form-control custom-select" name="suffix">
{% for suffix in suffixes %} {% for suffix in suffixes %}
<option value="{{ suffix[1] }}"> <option value="{{ suffix[1] }}">
{% if suffix[0] %} {% if suffix[0] %}

View File

@ -0,0 +1,33 @@
"""empty message
Revision ID: 903ec5f566e8
Revises: 3fa3a648c8e7
Create Date: 2020-02-23 14:11:46.332532
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '903ec5f566e8'
down_revision = '3fa3a648c8e7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('mailbox', sa.Column('new_email', sa.String(length=256), nullable=True))
op.create_unique_constraint(None, 'mailbox', ['new_email'])
op.add_column('users', sa.Column('full_mailbox', sa.Boolean(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'full_mailbox')
op.drop_constraint(None, 'mailbox', type_='unique')
op.drop_column('mailbox', 'new_email')
# ### end Alembic commands ###

View File

@ -0,0 +1,31 @@
"""empty message
Revision ID: f580030d9beb
Revises: 903ec5f566e8
Create Date: 2020-02-23 16:03:46.064813
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f580030d9beb'
down_revision = '903ec5f566e8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('default_mailbox_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'users', 'mailbox', ['default_mailbox_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', type_='foreignkey')
op.drop_column('users', 'default_mailbox_id')
# ### end Alembic commands ###

View File

@ -132,6 +132,7 @@ def fake_data():
is_admin=True, is_admin=True,
otp_secret="base32secret3232", otp_secret="base32secret3232",
can_use_multiple_mailbox=True, can_use_multiple_mailbox=True,
full_mailbox=True,
) )
db.session.commit() db.session.commit()
@ -156,9 +157,15 @@ def fake_data():
api_key = ApiKey.create(user_id=user.id, name="Firefox") api_key = ApiKey.create(user_id=user.id, name="Firefox")
api_key.code = "codeFF" api_key.code = "codeFF"
GenEmail.create_new(user.id, "e1@") m1 = Mailbox.create(user_id=user.id, email="m1@cd.ef", verified=True)
GenEmail.create_new(user.id, "e2@") m2 = Mailbox.create(user_id=user.id, email="m2@zt.com", verified=False)
GenEmail.create_new(user.id, "e3@") m3 = Mailbox.create(user_id=user.id, email="m3@cd.ef", verified=True)
db.session.commit()
user.default_mailbox_id = m1.id
GenEmail.create_new(user.id, "e1@", mailbox_id=m1.id)
GenEmail.create_new(user.id, "e2@", mailbox_id=m3.id)
CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
CustomDomain.create( CustomDomain.create(
@ -185,10 +192,6 @@ def fake_data():
client2.published = True client2.published = True
db.session.commit() db.session.commit()
Mailbox.create(user_id=user.id, email="ab@cd.ef", verified=True)
Mailbox.create(user_id=user.id, email="xy@zt.com", verified=False)
db.session.commit()
DeletedAlias.create(user_id=user.id, email="d1@ab.cd") DeletedAlias.create(user_id=user.id, email="d1@ab.cd")
DeletedAlias.create(user_id=user.id, email="d2@ab.cd") DeletedAlias.create(user_id=user.id, email="d2@ab.cd")
db.session.commit() db.session.commit()

View File

@ -40,6 +40,24 @@ def send_safari_extension_newsletter():
) )
def convert_user_full_mailbox(user):
# create a default mailbox
default_mb = Mailbox.get_by(user_id=user.id, email=user.email)
if not default_mb:
LOG.d("create default mailbox for user %s", user)
default_mb = Mailbox.create(user_id=user.id, email=user.email, verified=True)
db.session.commit()
# assign existing alias to this mailbox
for gen_email in GenEmail.query.filter_by(user_id=user.id):
gen_email.mailbox_id = default_mb.id
# finally set user to full_mailbox
user.full_mailbox = True
user.default_mailbox_id = default_mb.id
db.session.commit()
app = create_app() app = create_app()
with app.app_context(): with app.app_context():

View File

@ -21,7 +21,7 @@
and create your <b>business emails</b> using email alias. This is cheaper and more convenient than buying a dedicated solution like GSuite. By the way, all our business emails are actually aliases.') }} and create your <b>business emails</b> using email alias. This is cheaper and more convenient than buying a dedicated solution like GSuite. By the way, all our business emails are actually aliases.') }}
{% if user.in_trial() %} {% if user.in_trial() %}
{{ render_text('You can use all premium features like <em>custom domain</em> or <em>alias directory</em> during the <b>trial period</b>. Your trial will end ' + user.trial_end.humanize() + ".") }} {{ render_text('You can use all premium features like <em>custom domain</em> or <em>alias directory</em> during the <b>trial period</b>. Your trial will end ' + user.trial_end.humanize() + ". All aliases you create during the trial will continue to work normally when the trial ends.") }}
{% endif %} {% endif %}
{{ render_text('In the next coming days, you are going to receive some onboarding emails to quickly present SimpleLogin features. If you don\'t want to receive these emails, you can disable them on <a href="https://app.simplelogin.io/dashboard/setting#notification">Settings</a>.') }} {{ render_text('In the next coming days, you are going to receive some onboarding emails to quickly present SimpleLogin features. If you don\'t want to receive these emails, you can disable them on <a href="https://app.simplelogin.io/dashboard/setting#notification">Settings</a>.') }}

View File

@ -22,6 +22,7 @@ and create your business emails backed by your personal email! By the way, all o
{% if user.in_trial() %} {% if user.in_trial() %}
You can use all premium features like custom domain or alias directory during the trial period. You can use all premium features like custom domain or alias directory during the trial period.
Your trial will end {{ user.trial_end.humanize() }}. Your trial will end {{ user.trial_end.humanize() }}.
All aliases you create during the trial will continue to work normally when the trial ends.
{% endif %} {% endif %}
If there's anything that's bugging you, even the smallest of issues that could be done better, I want to hear about it - so hit the reply button. If there's anything that's bugging you, even the smallest of issues that could be done better, I want to hear about it - so hit the reply button.

View File

@ -11,7 +11,7 @@
{{ render_text("When the trial ends:") }} {{ render_text("When the trial ends:") }}
{{ render_text("- All aliases/domains/directories you have created are <b>kept</b> and continue working.") }} {{ render_text("- All aliases/domains/directories you have created are <b>kept</b> and continue working normally.") }}
{{ render_text("- You cannot create new aliases if you exceed the free plan limit, i.e. have more than 5 aliases.") }} {{ render_text("- You cannot create new aliases if you exceed the free plan limit, i.e. have more than 5 aliases.") }}
{{ render_text("- As features like <b>catch-all</b> or <b>directory</b> allow you to create aliases on-the-fly, those aliases cannot be automatically created if you have more than 5 aliases.") }} {{ render_text("- As features like <b>catch-all</b> or <b>directory</b> allow you to create aliases on-the-fly, those aliases cannot be automatically created if you have more than 5 aliases.") }}
{{ render_text("- You cannot add new domain or directory.") }} {{ render_text("- You cannot add new domain or directory.") }}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
{{ render_text("Hi " + user.name) }}
{{ render_text("You recently requested to change mailbox <b>"+ mailbox_email +"</b> to <b>" + mailbox_new_email + "</b>.") }}
{{ render_text("To confirm, please click on the button below.") }}
{{ render_button("Confirm mailbox change", link) }}
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
{{ raw_url(link) }}
{% endblock %}

View File

@ -0,0 +1,10 @@
Hi {{user.name}}
You recently requested to change mailbox {{mailbox_email}} to {{mailbox_new_email}}
To confirm, please click on this link:
{{link}}
Regards,
Son - SimpleLogin founder.

View File

@ -29,7 +29,7 @@
</a> </a>
</li> </li>
{% if current_user.can_use_multiple_mailbox %} {% if current_user.can_use_multiple_mailbox or current_user.full_mailbox %}
<li class="nav-item"> <li class="nav-item">
<a href="{{ url_for('dashboard.mailbox_route') }}" <a href="{{ url_for('dashboard.mailbox_route') }}"
class="nav-link {{ 'active' if active_page == 'mailbox' }}"> class="nav-link {{ 'active' if active_page == 'mailbox' }}">