diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py
index 09eab975..f8576933 100644
--- a/app/dashboard/__init__.py
+++ b/app/dashboard/__init__.py
@@ -8,4 +8,5 @@ from .views import (
unsubscribe,
api_key,
custom_domain,
+ alias_contact_manager,
)
diff --git a/app/dashboard/templates/dashboard/alias_contact_manager.html b/app/dashboard/templates/dashboard/alias_contact_manager.html
new file mode 100644
index 00000000..fad8039b
--- /dev/null
+++ b/app/dashboard/templates/dashboard/alias_contact_manager.html
@@ -0,0 +1,101 @@
+{% extends 'default.html' %}
+
+{% set active_page = "dashboard" %}
+
+{% block title %}
+ Alias Contact Manager
+{% endblock %}
+
+{% block default_content %}
+
+
+
+
+ To send an email from your alias, just send the email to a special email address that we call reverse-alias
+ and SimpleLogin will send it from the alias.
+
+
+ Make sure you send the email from your personal email address ({{ current_user.email }}).
+ This special email address can only be used by you.
+
+
+
+
+
+
+ {% for forward_email in forward_emails %}
+
+
+
+
+ *****
+
+
+ Copy reverse-alias
+
+
+
+
+
+ → {{ forward_email.website_from or forward_email.website_email }}
+
+
+
+ Created {{ forward_email.created_at | dt }}
+
+ {% if forward_email.last_reply() %}
+ {% set email_log = forward_email.last_reply() %}
+ Last email sent {{ email_log.created_at | dt }}
+ {% endif %}
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+{% endblock %}
+
+
+{% block script %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/dashboard/views/alias_contact_manager.py b/app/dashboard/views/alias_contact_manager.py
new file mode 100644
index 00000000..13148658
--- /dev/null
+++ b/app/dashboard/views/alias_contact_manager.py
@@ -0,0 +1,146 @@
+import re
+
+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, ValidationError
+
+from app.config import EMAIL_DOMAIN
+from app.dashboard.base import dashboard_bp
+from app.email_utils import get_email_part
+from app.extensions import db
+from app.log import LOG
+from app.models import GenEmail, ForwardEmail
+from app.utils import random_string
+
+
+def email_validator():
+ """validate email address. Handle both only email and email with name:
+ - ab@cd.com
+ - AB CD
+
+ """
+ message = "Invalid email format. Email must be either email@example.com or *First Last *"
+
+ def _check(form, field):
+ email = field.data
+ email_part = email
+
+ if "<" in email and ">" in email:
+ if email.find("<") + 1 < email.find(">"):
+ email_part = email[email.find("<") + 1 : email.find(">")].strip()
+
+ if re.match(r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$", email_part):
+ return
+
+ raise ValidationError(message)
+
+ return _check
+
+
+class NewContactForm(FlaskForm):
+ email = StringField(
+ "Email", validators=[validators.DataRequired(), email_validator()]
+ )
+
+
+@dashboard_bp.route("/alias_contact_manager//", methods=["GET", "POST"])
+@dashboard_bp.route(
+ "/alias_contact_manager//", methods=["GET", "POST"]
+)
+@login_required
+def alias_contact_manager(alias, forward_email_id=None):
+ gen_email = GenEmail.get_by(email=alias)
+
+ # sanity check
+ if not gen_email:
+ flash("You do not have access to this page", "warning")
+ return redirect(url_for("dashboard.index"))
+
+ if gen_email.user_id != current_user.id:
+ flash("You do not have access to this page", "warning")
+ return redirect(url_for("dashboard.index"))
+
+ new_contact_form = NewContactForm()
+
+ if request.method == "POST":
+ if request.form.get("form-name") == "create":
+ if new_contact_form.validate():
+ contact_email = new_contact_form.email.data
+
+ # generate a reply_email, make sure it is unique
+ # not use while to avoid infinite loop
+ for _ in range(1000):
+ reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
+ if not ForwardEmail.get_by(reply_email=reply_email):
+ break
+
+ website_email = get_email_part(contact_email)
+
+ # already been added
+ if ForwardEmail.get_by(
+ gen_email_id=gen_email.id, website_email=website_email
+ ):
+ flash(f"{website_email} is already added", "error")
+ return redirect(
+ url_for("dashboard.alias_contact_manager", alias=alias)
+ )
+
+ forward_email = ForwardEmail.create(
+ gen_email_id=gen_email.id,
+ website_email=website_email,
+ website_from=contact_email,
+ reply_email=reply_email,
+ )
+
+ LOG.d("create reverse-alias for %s", contact_email)
+ db.session.commit()
+ flash(
+ f"Reverse alias for {contact_email} is created successfully",
+ "success",
+ )
+
+ return redirect(
+ url_for(
+ "dashboard.alias_contact_manager",
+ alias=alias,
+ forward_email_id=forward_email.id,
+ )
+ )
+ elif request.form.get("form-name") == "delete":
+ forward_email_id = request.form.get("forward-email-id")
+ forward_email = ForwardEmail.get(forward_email_id)
+
+ if not forward_email:
+ flash("Unknown error. Refresh the page", "warning")
+ return redirect(url_for("dashboard.alias_contact_manager", alias=alias))
+ elif forward_email.gen_email_id != gen_email.id:
+ flash("You cannot delete reverse-alias", "warning")
+ return redirect(url_for("dashboard.alias_contact_manager", alias=alias))
+
+ contact_name = forward_email.website_from
+ ForwardEmail.delete(forward_email_id)
+ db.session.commit()
+
+ flash(
+ f"Reverse-alias for {contact_name} has been deleted successfully",
+ "success",
+ )
+
+ return redirect(url_for("dashboard.alias_contact_manager", alias=alias))
+
+ # make sure highlighted forward_email is at array start
+ forward_emails = gen_email.forward_emails
+
+ if forward_email_id:
+ forward_emails = sorted(
+ forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True
+ )
+
+ return render_template(
+ "dashboard/alias_contact_manager.html",
+ forward_emails=forward_emails,
+ alias=gen_email.email,
+ new_contact_form=new_contact_form,
+ forward_email_id=forward_email_id,
+ )
diff --git a/app/models.py b/app/models.py
index 164cae30..7485a65d 100644
--- a/app/models.py
+++ b/app/models.py
@@ -5,11 +5,12 @@ import arrow
import bcrypt
from flask import url_for
from flask_login import UserMixin
-from sqlalchemy import text
+from sqlalchemy import text, desc
from sqlalchemy_utils import ArrowType
from app import s3
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, URL, AVATAR_URL_EXPIRATION
+from app.email_utils import get_email_name
from app.extensions import db
from app.log import LOG
from app.oauth_models import Scope
@@ -540,7 +541,25 @@ class ForwardEmail(db.Model, ModelMixin):
# it has the prefix "reply+" to distinguish with other email
reply_email = db.Column(db.String(128), nullable=False)
- gen_email = db.relationship(GenEmail)
+ gen_email = db.relationship(GenEmail, backref="forward_emails")
+
+ def website_send_to(self):
+ """return the email address with name.
+ to use when user wants to send an email from the alias"""
+ if self.website_from:
+ name = get_email_name(self.website_from)
+ if name:
+ return name + " " + self.website_email + " " + f"<{self.reply_email}>"
+
+ return self.reply_email
+
+ def last_reply(self) -> "ForwardEmailLog":
+ """return the most recent reply"""
+ return (
+ ForwardEmailLog.query.filter_by(forward_id=self.id, is_reply=True)
+ .order_by(desc(ForwardEmailLog.created_at))
+ .first()
+ )
class ForwardEmailLog(db.Model, ModelMixin):