diff --git a/app/api/views/alias.py b/app/api/views/alias.py index c6c21550..f4f127b0 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -13,7 +13,7 @@ from app.dashboard.views.index import get_alias_info, AliasInfo from app.extensions import db from app.log import LOG from app.models import ForwardEmailLog -from app.models import GenEmail, ForwardEmail +from app.models import GenEmail, Contact from app.utils import random_string @@ -199,7 +199,7 @@ def update_alias(alias_id): return jsonify(note=new_note), 200 -def serialize_forward_email(fe: ForwardEmail) -> dict: +def serialize_contact(fe: Contact) -> dict: res = { "creation_date": fe.created_at.format(), @@ -220,15 +220,15 @@ def serialize_forward_email(fe: ForwardEmail) -> dict: def get_alias_contacts(gen_email, page_id: int) -> [dict]: q = ( - ForwardEmail.query.filter_by(gen_email_id=gen_email.id) - .order_by(ForwardEmail.id.desc()) + Contact.query.filter_by(gen_email_id=gen_email.id) + .order_by(Contact.id.desc()) .limit(PAGE_LIMIT) .offset(page_id * PAGE_LIMIT) ) res = [] for fe in q.all(): - res.append(serialize_forward_email(fe)) + res.append(serialize_contact(fe)) return res @@ -299,16 +299,16 @@ def create_contact_route(alias_id): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" - if not ForwardEmail.get_by(reply_email=reply_email): + if not Contact.get_by(reply_email=reply_email): break _, website_email = parseaddr(contact_email) # already been added - if ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email): + if Contact.get_by(gen_email_id=gen_email.id, website_email=website_email): return jsonify(error="Contact already added"), 409 - forward_email = ForwardEmail.create( + contact = Contact.create( gen_email_id=gen_email.id, website_email=website_email, website_from=contact_email, @@ -318,4 +318,4 @@ def create_contact_route(alias_id): LOG.d("create reverse-alias for %s %s", contact_email, gen_email) db.session.commit() - return jsonify(**serialize_forward_email(forward_email)), 201 + return jsonify(**serialize_contact(contact)), 201 diff --git a/app/dashboard/templates/dashboard/alias_contact_manager.html b/app/dashboard/templates/dashboard/alias_contact_manager.html index 1de72a56..a868b6ce 100644 --- a/app/dashboard/templates/dashboard/alias_contact_manager.html +++ b/app/dashboard/templates/dashboard/alias_contact_manager.html @@ -47,33 +47,33 @@
- {% for forward_email in forward_emails %} + {% for contact in contacts %}
-
+
- ************************* + data-clipboard-text="{{ contact.website_send_to() }}"> Copy reverse-alias
- âž¡ {{ forward_email.website_from or forward_email.website_email }} + âž¡ {{ contact.website_from or contact.website_email }}
- Created {{ forward_email.created_at | dt }}
+ Created {{ contact.created_at | dt }}
- {% if forward_email.last_reply() %} - {% set email_log = forward_email.last_reply() %} + {% if contact.last_reply() %} + {% set email_log = contact.last_reply() %} Last email sent {{ email_log.created_at | dt }} {% endif %}
@@ -81,7 +81,7 @@
- + Delete diff --git a/app/dashboard/views/alias_contact_manager.py b/app/dashboard/views/alias_contact_manager.py index 0db9f93a..206e13ac 100644 --- a/app/dashboard/views/alias_contact_manager.py +++ b/app/dashboard/views/alias_contact_manager.py @@ -10,7 +10,7 @@ from app.config import EMAIL_DOMAIN from app.dashboard.base import dashboard_bp from app.extensions import db from app.log import LOG -from app.models import GenEmail, ForwardEmail +from app.models import GenEmail, Contact from app.utils import random_string @@ -47,10 +47,10 @@ class NewContactForm(FlaskForm): @dashboard_bp.route("/alias_contact_manager//", methods=["GET", "POST"]) @dashboard_bp.route( - "/alias_contact_manager//", methods=["GET", "POST"] + "/alias_contact_manager//", methods=["GET", "POST"] ) @login_required -def alias_contact_manager(alias_id, forward_email_id=None): +def alias_contact_manager(alias_id, contact_id=None): gen_email = GenEmail.get(alias_id) # sanity check @@ -71,15 +71,16 @@ def alias_contact_manager(alias_id, forward_email_id=None): # generate a reply_email, make sure it is unique # not use while to avoid infinite loop + reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" - if not ForwardEmail.get_by(reply_email=reply_email): + if not Contact.get_by(reply_email=reply_email): break _, website_email = parseaddr(contact_email) # already been added - if ForwardEmail.get_by( + if Contact.get_by( gen_email_id=gen_email.id, website_email=website_email ): flash(f"{website_email} is already added", "error") @@ -87,7 +88,7 @@ def alias_contact_manager(alias_id, forward_email_id=None): url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) - forward_email = ForwardEmail.create( + contact = Contact.create( gen_email_id=gen_email.id, website_email=website_email, website_from=contact_email, @@ -102,26 +103,26 @@ def alias_contact_manager(alias_id, forward_email_id=None): url_for( "dashboard.alias_contact_manager", alias_id=alias_id, - forward_email_id=forward_email.id, + contact_id=contact.id, ) ) elif request.form.get("form-name") == "delete": - forward_email_id = request.form.get("forward-email-id") - forward_email = ForwardEmail.get(forward_email_id) + contact_id = request.form.get("contact-id") + contact = Contact.get(contact_id) - if not forward_email: + if not contact: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) - elif forward_email.gen_email_id != gen_email.id: + elif contact.gen_email_id != gen_email.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) - contact_name = forward_email.website_from - ForwardEmail.delete(forward_email_id) + contact_name = contact.website_from + Contact.delete(contact_id) db.session.commit() flash(f"Reverse-alias for {contact_name} has been deleted", "success") @@ -130,19 +131,17 @@ def alias_contact_manager(alias_id, forward_email_id=None): url_for("dashboard.alias_contact_manager", alias_id=alias_id) ) - # make sure highlighted forward_email is at array start - forward_emails = gen_email.forward_emails + # make sure highlighted contact is at array start + contacts = gen_email.contacts - if forward_email_id: - forward_emails = sorted( - forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True - ) + if contact_id: + contacts = sorted(contacts, key=lambda fe: fe.id == contact_id, reverse=True) return render_template( "dashboard/alias_contact_manager.html", - forward_emails=forward_emails, + contacts=contacts, alias=gen_email.email, gen_email=gen_email, new_contact_form=new_contact_form, - forward_email_id=forward_email_id, + contact_id=contact_id, ) diff --git a/app/dashboard/views/alias_log.py b/app/dashboard/views/alias_log.py index 65ef1748..6ce6e671 100644 --- a/app/dashboard/views/alias_log.py +++ b/app/dashboard/views/alias_log.py @@ -5,7 +5,7 @@ from flask_login import login_required, current_user from app.config import PAGE_LIMIT from app.dashboard.base import dashboard_bp from app.extensions import db -from app.models import GenEmail, ForwardEmailLog, ForwardEmail +from app.models import GenEmail, ForwardEmailLog, Contact class AliasLog: @@ -42,9 +42,9 @@ def alias_log(alias_id, page_id): logs = get_alias_log(gen_email, page_id) base = ( - db.session.query(ForwardEmail, ForwardEmailLog) - .filter(ForwardEmail.id == ForwardEmailLog.forward_id) - .filter(ForwardEmail.gen_email_id == gen_email.id) + db.session.query(Contact, ForwardEmailLog) + .filter(Contact.id == ForwardEmailLog.forward_id) + .filter(Contact.gen_email_id == gen_email.id) ) total = base.count() email_forwarded = ( @@ -66,9 +66,9 @@ def get_alias_log(gen_email: GenEmail, page_id=0): mailbox = gen_email.mailbox_email() q = ( - db.session.query(ForwardEmail, ForwardEmailLog) - .filter(ForwardEmail.id == ForwardEmailLog.forward_id) - .filter(ForwardEmail.gen_email_id == gen_email.id) + db.session.query(Contact, ForwardEmailLog) + .filter(Contact.id == ForwardEmailLog.forward_id) + .filter(Contact.gen_email_id == gen_email.id) .order_by(ForwardEmailLog.id.desc()) .limit(PAGE_LIMIT) .offset(page_id * PAGE_LIMIT) diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py index 695d97e5..237bc757 100644 --- a/app/dashboard/views/index.py +++ b/app/dashboard/views/index.py @@ -12,7 +12,7 @@ from app.log import LOG from app.models import ( GenEmail, ClientUser, - ForwardEmail, + Contact, ForwardEmailLog, DeletedAlias, AliasGeneratorEnum, @@ -202,11 +202,9 @@ def get_alias_info( aliases = {} # dict of alias and AliasInfo q = ( - 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 - ) + db.session.query(GenEmail, Contact, ForwardEmailLog, Mailbox) + .join(Contact, GenEmail.id == Contact.gen_email_id, isouter=True) + .join(ForwardEmailLog, Contact.id == ForwardEmailLog.forward_id, isouter=True) .join(Mailbox, GenEmail.mailbox_id == Mailbox.id, isouter=True) .filter(GenEmail.user_id == user.id) .order_by(GenEmail.created_at.desc()) diff --git a/app/models.py b/app/models.py index 7ae2a450..7561a959 100644 --- a/app/models.py +++ b/app/models.py @@ -693,7 +693,7 @@ class ClientUser(db.Model, ModelMixin): return res -class ForwardEmail(db.Model, ModelMixin): +class Contact(db.Model, ModelMixin): """ Store configuration of sender (website-email) and alias. """ @@ -719,7 +719,7 @@ class ForwardEmail(db.Model, ModelMixin): # it has the prefix "reply+" to distinguish with other email reply_email = db.Column(db.String(512), nullable=False) - gen_email = db.relationship(GenEmail, backref="forward_emails") + gen_email = db.relationship(GenEmail, backref="contacts") def website_send_to(self): """return the email address with name. @@ -750,7 +750,7 @@ class ForwardEmail(db.Model, ModelMixin): class ForwardEmailLog(db.Model, ModelMixin): forward_id = db.Column( - db.ForeignKey(ForwardEmail.id, ondelete="cascade"), nullable=False + db.ForeignKey(Contact.id, ondelete="cascade"), nullable=False ) # whether this is a reply @@ -769,7 +769,7 @@ class ForwardEmailLog(db.Model, ModelMixin): ) refused_email = db.relationship("RefusedEmail") - forward = db.relationship(ForwardEmail) + forward = db.relationship(Contact) class Subscription(db.Model, ModelMixin): diff --git a/cron.py b/cron.py index 3bd08891..eb725fc2 100644 --- a/cron.py +++ b/cron.py @@ -12,7 +12,7 @@ from app.models import ( User, GenEmail, ForwardEmailLog, - ForwardEmail, + Contact, CustomDomain, Client, ManualSubscription, @@ -119,9 +119,9 @@ def stats(): LOG.d("total number alias %s", nb_gen_email) # nb mails forwarded - q = db.session.query(ForwardEmailLog, ForwardEmail, GenEmail, User).filter( - ForwardEmailLog.forward_id == ForwardEmail.id, - ForwardEmail.gen_email_id == GenEmail.id, + q = db.session.query(ForwardEmailLog, Contact, GenEmail, User).filter( + ForwardEmailLog.forward_id == Contact.id, + Contact.gen_email_id == GenEmail.id, GenEmail.user_id == User.id, ) for ie in IGNORED_EMAILS: diff --git a/email_handler.py b/email_handler.py index 231e75b2..fb653467 100644 --- a/email_handler.py +++ b/email_handler.py @@ -70,7 +70,7 @@ from app.extensions import db from app.log import LOG from app.models import ( GenEmail, - ForwardEmail, + Contact, ForwardEmailLog, CustomDomain, Directory, @@ -207,21 +207,17 @@ def try_auto_create_catch_all_domain(alias: str) -> Optional[GenEmail]: return gen_email -def get_or_create_forward_email( - website_from_header: str, gen_email: GenEmail -) -> ForwardEmail: +def get_or_create_contact(website_from_header: str, gen_email: GenEmail) -> Contact: """ website_from_header can be the full-form email, i.e. "First Last " """ _, website_email = parseaddr(website_from_header) - forward_email = ForwardEmail.get_by( - gen_email_id=gen_email.id, website_email=website_email - ) - if forward_email: + contact = Contact.get_by(gen_email_id=gen_email.id, website_email=website_email) + if contact: # update the website_from if needed - if forward_email.website_from != website_from_header: - LOG.d("Update From header for %s", forward_email) - forward_email.website_from = website_from_header + if contact.website_from != website_from_header: + LOG.d("Update From header for %s", contact) + contact.website_from = website_from_header db.session.commit() else: LOG.debug( @@ -234,12 +230,12 @@ def get_or_create_forward_email( # not use while loop to avoid infinite loop reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" for _ in range(1000): - if not ForwardEmail.get_by(reply_email=reply_email): + if not Contact.get_by(reply_email=reply_email): # found! break reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" - forward_email = ForwardEmail.create( + contact = Contact.create( gen_email_id=gen_email.id, website_email=website_email, website_from=website_from_header, @@ -247,7 +243,7 @@ def get_or_create_forward_email( ) db.session.commit() - return forward_email + return contact def should_append_alias(msg, alias): @@ -317,8 +313,8 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: LOG.d("Encrypt message using mailbox %s", mailbox) msg = prepare_pgp_message(msg, mailbox.pgp_finger_print) - forward_email = get_or_create_forward_email(msg["From"], gen_email) - forward_log = ForwardEmailLog.create(forward_id=forward_email.id) + contact = get_or_create_contact(msg["From"], gen_email) + forward_log = ForwardEmailLog.create(forward_id=contact.id) if gen_email.enabled: # add custom header @@ -338,7 +334,7 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: + (" - " if website_name else "") + website_email.replace("@", " at ") ) - from_header = formataddr((new_website_name, forward_email.reply_email)) + from_header = formataddr((new_website_name, contact.reply_email)) add_or_replace_header(msg, "From", from_header) LOG.d("new from header:%s", from_header) @@ -373,7 +369,7 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( - forward_email.reply_email, + contact.reply_email, mailbox_email, msg_raw, envelope.mail_options, @@ -395,12 +391,12 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: LOG.warning(f"Reply email {reply_email} has wrong domain") return "550 wrong reply email" - forward_email = ForwardEmail.get_by(reply_email=reply_email) - if not forward_email: + contact = Contact.get_by(reply_email=reply_email) + if not contact: LOG.warning(f"No such forward-email with {reply_email} as reply-email") return "550 wrong reply email" - alias: str = forward_email.gen_email.email + alias: str = contact.gen_email.email alias_domain = alias[alias.find("@") + 1 :] # alias must end with one of the ALIAS_DOMAINS or custom-domain @@ -408,7 +404,7 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: if not CustomDomain.get_by(domain=alias_domain): return "550 alias unknown by SimpleLogin" - gen_email = forward_email.gen_email + gen_email = contact.gen_email user = gen_email.user mailbox_email = gen_email.mailbox_email() @@ -420,12 +416,12 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: LOG.error( "Bounce when sending to alias %s from %s, user %s", alias, - forward_email.website_from, + contact.website_from, gen_email.user, ) handle_bounce( - alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email + alias, envelope, contact, gen_email, msg, smtp, user, mailbox_email ) return "550 ignored" @@ -485,10 +481,10 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: # remove sender header if present as this could reveal user real email delete_header(msg, "Sender") - add_or_replace_header(msg, "To", forward_email.website_email) + add_or_replace_header(msg, "To", contact.website_email) # add List-Unsubscribe header - unsubscribe_link = f"{URL}/dashboard/unsubscribe/{forward_email.gen_email_id}" + unsubscribe_link = f"{URL}/dashboard/unsubscribe/{contact.gen_email_id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click") @@ -498,7 +494,7 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: LOG.d( "send email from %s to %s, mail_options:%s,rcpt_options:%s", alias, - forward_email.website_email, + contact.website_email, envelope.mail_options, envelope.rcpt_options, ) @@ -514,29 +510,23 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: msg_raw = msg.as_string().encode() smtp.sendmail( alias, - forward_email.website_email, + contact.website_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) - ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True) + ForwardEmailLog.create(forward_id=contact.id, is_reply=True) db.session.commit() return "250 Message accepted for delivery" -def handle_bounce( - alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email -): - fel: ForwardEmailLog = ForwardEmailLog.create( - forward_id=forward_email.id, bounced=True - ) +def handle_bounce(alias, envelope, contact, gen_email, msg, smtp, user, mailbox_email): + fel: ForwardEmailLog = ForwardEmailLog.create(forward_id=contact.id, bounced=True) db.session.commit() - nb_bounced = ForwardEmailLog.filter_by( - forward_id=forward_email.id, bounced=True - ).count() + nb_bounced = ForwardEmailLog.filter_by(forward_id=contact.id, bounced=True).count() disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" # Store the bounced email @@ -569,19 +559,19 @@ def handle_bounce( LOG.d( "Inform user %s about bounced email sent by %s to alias %s", user, - forward_email.website_from, + contact.website_from, alias, ) send_email( # use user mail here as only user is authenticated to see the refused email user.email, - f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox", + f"Email from {contact.website_from} to {alias} cannot be delivered to your inbox", render( "transactional/bounced-email.txt", name=user.name, alias=alias, - website_from=forward_email.website_from, - website_email=forward_email.website_email, + website_from=contact.website_from, + website_email=contact.website_email, disable_alias_link=disable_alias_link, refused_email_url=refused_email_url, mailbox_email=mailbox_email, @@ -590,8 +580,8 @@ def handle_bounce( "transactional/bounced-email.html", name=user.name, alias=alias, - website_from=forward_email.website_from, - website_email=forward_email.website_email, + website_from=contact.website_from, + website_email=contact.website_email, disable_alias_link=disable_alias_link, refused_email_url=refused_email_url, mailbox_email=mailbox_email, @@ -604,7 +594,7 @@ def handle_bounce( LOG.d( "Bounce happens again with alias %s from %s. Disable alias now ", alias, - forward_email.website_from, + contact.website_from, ) gen_email.enabled = False db.session.commit() @@ -612,13 +602,13 @@ def handle_bounce( send_email( # use user mail here as only user is authenticated to see the refused email user.email, - f"Alias {alias} has been disabled due to second undelivered email from {forward_email.website_from}", + f"Alias {alias} has been disabled due to second undelivered email from {contact.website_from}", render( "transactional/automatic-disable-alias.txt", name=user.name, alias=alias, - website_from=forward_email.website_from, - website_email=forward_email.website_email, + website_from=contact.website_from, + website_email=contact.website_email, refused_email_url=refused_email_url, mailbox_email=mailbox_email, ), @@ -626,8 +616,8 @@ def handle_bounce( "transactional/automatic-disable-alias.html", name=user.name, alias=alias, - website_from=forward_email.website_from, - website_email=forward_email.website_email, + website_from=contact.website_from, + website_email=contact.website_email, refused_email_url=refused_email_url, mailbox_email=mailbox_email, ), diff --git a/tests/api/test_alias.py b/tests/api/test_alias.py index 4a6aa67e..d759a29b 100644 --- a/tests/api/test_alias.py +++ b/tests/api/test_alias.py @@ -4,7 +4,7 @@ from flask import url_for from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, PAGE_LIMIT from app.extensions import db -from app.models import User, ApiKey, GenEmail, ForwardEmail, ForwardEmailLog +from app.models import User, ApiKey, GenEmail, Contact, ForwardEmailLog from app.utils import random_word @@ -128,7 +128,7 @@ def test_alias_activities(flask_client): db.session.commit() # create some alias log - forward_email = ForwardEmail.create( + contact = Contact.create( website_email="marketing@example.com", reply_email="reply@a.b", gen_email_id=gen_email.id, @@ -136,10 +136,10 @@ def test_alias_activities(flask_client): db.session.commit() for _ in range(int(PAGE_LIMIT / 2)): - ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True) + ForwardEmailLog.create(forward_id=contact.id, is_reply=True) for _ in range(int(PAGE_LIMIT / 2) + 2): - ForwardEmailLog.create(forward_id=forward_email.id, blocked=True) + ForwardEmailLog.create(forward_id=contact.id, blocked=True) r = flask_client.get( url_for("api.get_alias_activities", alias_id=gen_email.id, page_id=0), @@ -200,14 +200,14 @@ def test_alias_contacts(flask_client): # create some alias log for i in range(PAGE_LIMIT + 1): - forward_email = ForwardEmail.create( + contact = Contact.create( website_email=f"marketing-{i}@example.com", reply_email=f"reply-{i}@a.b", gen_email_id=gen_email.id, ) db.session.commit() - ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True) + ForwardEmailLog.create(forward_id=contact.id, is_reply=True) db.session.commit() r = flask_client.get(