From f02545899896ae731a9e7096fd12025227dbd41d Mon Sep 17 00:00:00 2001 From: MrMeeb Date: Fri, 10 Feb 2023 12:00:04 +0000 Subject: [PATCH] 4.22.1 --- app/README.md | 6 +++ app/app/api/views/mailbox.py | 2 +- app/app/config.py | 6 ++- app/app/dashboard/views/index.py | 8 +++- app/app/dashboard/views/mailbox.py | 15 +++++--- app/app/mail_sender.py | 10 ++--- app/app/models.py | 2 + app/email_handler.py | 35 ++++++++++------- app/job_runner.py | 12 +++--- app/tests/test_email_handler.py | 60 ++++++++++++++++++++++++++++++ 10 files changed, 122 insertions(+), 34 deletions(-) diff --git a/app/README.md b/app/README.md index 55c371c..a49ac5d 100644 --- a/app/README.md +++ b/app/README.md @@ -334,6 +334,12 @@ smtpd_recipient_restrictions = permit ``` +Check that the ssl certificates `/etc/ssl/certs/ssl-cert-snakeoil.pem` and `/etc/ssl/private/ssl-cert-snakeoil.key` exist. Depending on the linux distribution you are using they may or may not be present. If they are not, you will need to generate them with this command: + +```bash +openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem +``` + Create the `/etc/postfix/pgsql-relay-domains.cf` file with the following content. Make sure that the database config is correctly set, replace `mydomain.com` with your domain, update 'myuser' and 'mypassword' with your postgres credentials. diff --git a/app/app/api/views/mailbox.py b/app/app/api/views/mailbox.py index e99fd82..27475b8 100644 --- a/app/app/api/views/mailbox.py +++ b/app/app/api/views/mailbox.py @@ -86,7 +86,7 @@ def delete_mailbox(mailbox_id): """ user = g.user - mailbox = Mailbox.get(id=mailbox_id) + mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != user.id: return jsonify(error="Forbidden"), 403 diff --git a/app/app/config.py b/app/app/config.py index 775ca50..5fdb7ab 100644 --- a/app/app/config.py +++ b/app/app/config.py @@ -111,11 +111,15 @@ POSTFIX_SERVER = os.environ.get("POSTFIX_SERVER", "240.0.0.1") DISABLE_REGISTRATION = "DISABLE_REGISTRATION" in os.environ # allow using a different postfix port, useful when developing locally -POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", 25)) # Use port 587 instead of 25 when sending emails through Postfix # Useful when calling Postfix from an external network POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ +if POSTFIX_SUBMISSION_TLS: + default_postfix_port = 587 +else: + default_postfix_port = 25 +POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", default_postfix_port)) POSTFIX_TIMEOUT = os.environ.get("POSTFIX_TIMEOUT", 3) # ["domain1.com", "domain2.com"] diff --git a/app/app/dashboard/views/index.py b/app/app/dashboard/views/index.py index 6bd6637..ceeef89 100644 --- a/app/app/dashboard/views/index.py +++ b/app/app/dashboard/views/index.py @@ -150,7 +150,13 @@ def index(): flash(f"Alias {alias.email} has been disabled", "success") return redirect( - url_for("dashboard.index", query=query, sort=sort, filter=alias_filter) + url_for( + "dashboard.index", + query=query, + sort=sort, + filter=alias_filter, + page=page, + ) ) mailboxes = current_user.mailboxes() diff --git a/app/app/dashboard/views/mailbox.py b/app/app/dashboard/views/mailbox.py index e6f41ed..0be92f4 100644 --- a/app/app/dashboard/views/mailbox.py +++ b/app/app/dashboard/views/mailbox.py @@ -69,17 +69,20 @@ def mailbox_route(): transfer_mailbox = Mailbox.get(transfer_mailbox_id) if not transfer_mailbox or transfer_mailbox.user_id != current_user.id: - flash("You must transfer the aliases to a mailbox you own.") + flash( + "You must transfer the aliases to a mailbox you own.", "error" + ) return redirect(url_for("dashboard.mailbox_route")) if transfer_mailbox.id == mailbox.id: flash( - "You can not transfer the aliases to the mailbox you want to delete." + "You can not transfer the aliases to the mailbox you want to delete.", + "error", ) return redirect(url_for("dashboard.mailbox_route")) if not transfer_mailbox.verified: - flash("Your new mailbox is not verified") + flash("Your new mailbox is not verified", "error") return redirect(url_for("dashboard.mailbox_route")) # Schedule delete account job @@ -147,12 +150,12 @@ def mailbox_route(): elif not email_can_be_used_as_mailbox(mailbox_email): flash(f"You cannot use {mailbox_email}.", "error") else: - transfer_mailbox = Mailbox.create( + new_mailbox = Mailbox.create( email=mailbox_email, user_id=current_user.id ) Session.commit() - send_verification_email(current_user, transfer_mailbox) + send_verification_email(current_user, new_mailbox) flash( f"You are going to receive an email to confirm {mailbox_email}.", @@ -162,7 +165,7 @@ def mailbox_route(): return redirect( url_for( "dashboard.mailbox_detail_route", - mailbox_id=transfer_mailbox.id, + mailbox_id=new_mailbox.id, ) ) diff --git a/app/app/mail_sender.py b/app/app/mail_sender.py index a6737ed..2705ddd 100644 --- a/app/app/mail_sender.py +++ b/app/app/mail_sender.py @@ -117,14 +117,12 @@ class MailSender: return True def _send_to_smtp(self, send_request: SendRequest, retries: int) -> bool: - if config.POSTFIX_SUBMISSION_TLS and config.POSTFIX_PORT == 25: - smtp_port = 587 - else: - smtp_port = config.POSTFIX_PORT try: start = time.time() with SMTP( - config.POSTFIX_SERVER, smtp_port, timeout=config.POSTFIX_TIMEOUT + config.POSTFIX_SERVER, + config.POSTFIX_PORT, + timeout=config.POSTFIX_TIMEOUT, ) as smtp: if config.POSTFIX_SUBMISSION_TLS: smtp.starttls() @@ -170,7 +168,7 @@ class MailSender: LOG.e(f"Ignore smtp error {e}") return False LOG.e( - f"Could not send message to smtp server {config.POSTFIX_SERVER}:{smtp_port}" + f"Could not send message to smtp server {config.POSTFIX_SERVER}:{config.POSTFIX_PORT}" ) self._save_request_to_unsent_dir(send_request) return False diff --git a/app/app/models.py b/app/app/models.py index 1ad906e..4f99b6f 100644 --- a/app/app/models.py +++ b/app/app/models.py @@ -1641,6 +1641,8 @@ class Contact(Base, ModelMixin): Store configuration of sender (website-email) and alias. """ + MAX_NAME_LENGTH = 512 + __tablename__ = "contact" __table_args__ = ( diff --git a/app/email_handler.py b/app/email_handler.py index 7c6bf50..0b5eec2 100644 --- a/app/email_handler.py +++ b/app/email_handler.py @@ -168,7 +168,7 @@ from app.pgp_utils import ( sign_data, load_public_key_and_check, ) -from app.utils import sanitize_email +from app.utils import sanitize_email, canonicalize_email from init_app import load_pgp_public_keys from server import create_light_app @@ -182,6 +182,10 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con except ValueError: contact_name, contact_email = "", "" + # Ensure contact_name is within limits + if len(contact_name) >= Contact.MAX_NAME_LENGTH: + contact_name = contact_name[0 : Contact.MAX_NAME_LENGTH] + if not is_valid_email(contact_email): # From header is wrongly formatted, try with mail_from if mail_from and mail_from != "<>": @@ -1384,21 +1388,26 @@ def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]: """return the corresponding mailbox given the mail_from and alias Usually the mail_from=mailbox.email but it can also be one of the authorized address """ - for mailbox in alias.mailboxes: - if mailbox.email == mail_from: - return mailbox - for authorized_address in mailbox.authorized_addresses: - if authorized_address.email == mail_from: - LOG.d( - "Found an authorized address for %s %s %s", - alias, - mailbox, - authorized_address, - ) + def __check(email_address: str, alias: Alias) -> Optional[Mailbox]: + for mailbox in alias.mailboxes: + if mailbox.email == email_address: return mailbox - return None + for authorized_address in mailbox.authorized_addresses: + if authorized_address.email == email_address: + LOG.d( + "Found an authorized address for %s %s %s", + alias, + mailbox, + authorized_address, + ) + return mailbox + return None + + # We need to first check for the uncanonicalized version because we still have users in the db with the + # email non canonicalized. So if it matches the already existing one use that, otherwise check the canonical one + return __check(mail_from, alias) or __check(canonicalize_email(mail_from), alias) def handle_unknown_mailbox( diff --git a/app/job_runner.py b/app/job_runner.py index 0a710c3..590611d 100644 --- a/app/job_runner.py +++ b/app/job_runner.py @@ -159,9 +159,9 @@ def delete_mailbox_job(job: Job): user.email, f"Your mailbox {mailbox_email} has been deleted", f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}. - Regards, - SimpleLogin team. - """, +Regards, +SimpleLogin team. +""", retries=3, ) else: @@ -169,9 +169,9 @@ def delete_mailbox_job(job: Job): user.email, f"Your mailbox {mailbox_email} has been deleted", f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully. - Regards, - SimpleLogin team. - """, +Regards, +SimpleLogin team. +""", retries=3, ) diff --git a/app/tests/test_email_handler.py b/app/tests/test_email_handler.py index f38ab80..2e131d0 100644 --- a/app/tests/test_email_handler.py +++ b/app/tests/test_email_handler.py @@ -22,6 +22,7 @@ from app.models import ( Contact, SentAlert, ) +from app.utils import random_string, canonicalize_email from email_handler import ( get_mailbox_from_mail_from, should_ignore, @@ -308,3 +309,62 @@ def test_replace_contacts_and_user_in_reply_phase(flask_client): payload = sent_mails[0].msg.get_payload()[0].get_payload() assert payload.find("Contact is {}".format(contact_real_mail)) > -1 assert payload.find("Other contact is {}".format(contact2_real_mail)) > -1 + + +@mail_sender.store_emails_test_decorator +def test_send_email_from_non_canonical_address_on_reply(flask_client): + email_address = f"{random_string(10)}.suf@gmail.com" + user = create_new_user(email=canonicalize_email(email_address)) + alias = Alias.create_new_random(user) + Session.commit() + contact = Contact.create( + user_id=user.id, + alias_id=alias.id, + website_email=random_email(), + reply_email=f"{random_string(10)}@{EMAIL_DOMAIN}", + commit=True, + ) + envelope = Envelope() + envelope.mail_from = email_address + envelope.rcpt_tos = [contact.reply_email] + msg = EmailMessage() + msg[headers.TO] = contact.reply_email + msg[headers.SUBJECT] = random_string() + result = email_handler.handle(envelope, msg) + assert result == status.E200 + sent_mails = mail_sender.get_stored_emails() + assert len(sent_mails) == 1 + email_logs = EmailLog.filter_by(user_id=user.id).all() + assert len(email_logs) == 1 + assert email_logs[0].alias_id == alias.id + assert email_logs[0].mailbox_id == user.default_mailbox_id + + +@mail_sender.store_emails_test_decorator +def test_send_email_from_non_canonical_matches_already_existing_user(flask_client): + email_address = f"{random_string(10)}.suf@gmail.com" + create_new_user(email=canonicalize_email(email_address)) + user = create_new_user(email=email_address) + alias = Alias.create_new_random(user) + Session.commit() + contact = Contact.create( + user_id=user.id, + alias_id=alias.id, + website_email=random_email(), + reply_email=f"{random_string(10)}@{EMAIL_DOMAIN}", + commit=True, + ) + envelope = Envelope() + envelope.mail_from = email_address + envelope.rcpt_tos = [contact.reply_email] + msg = EmailMessage() + msg[headers.TO] = contact.reply_email + msg[headers.SUBJECT] = random_string() + result = email_handler.handle(envelope, msg) + assert result == status.E200 + sent_mails = mail_sender.get_stored_emails() + assert len(sent_mails) == 1 + email_logs = EmailLog.filter_by(user_id=user.id).all() + assert len(email_logs) == 1 + assert email_logs[0].alias_id == alias.id + assert email_logs[0].mailbox_id == user.default_mailbox_id