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 %} + + + + +
+ + {{ new_contact_form.csrf_token }} + + + + {{ new_contact_form.email(class="form-control", placeholder="First Last ") }} + {{ render_field_errors(new_contact_form.email) }} + + + +
+ {% 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 %} +
+ +
+
+ + + + Delete + +
+
+ +
+
+ {% 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):