From ca245b22663d02ad0440198e35c3ce983e3b4245 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Wed, 30 Mar 2022 21:41:39 -0400 Subject: [PATCH 01/36] Create initial SMTP handler --- SMTP_handler.py | 539 +++++++++++++++++++++++++++++++++++++++++++++++ app/config.py | 4 + app/models.py | 32 +++ email_handler.py | 54 +++-- 4 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 SMTP_handler.py diff --git a/SMTP_handler.py b/SMTP_handler.py new file mode 100644 index 000000000..67644fc2f --- /dev/null +++ b/SMTP_handler.py @@ -0,0 +1,539 @@ +""" +Enable SMTP access for aliases. + +This will allow a MUA to send directly from the alias without the need for the user to create a reverse-alias. + +""" + +import argparse +import time +import ssl +import uuid +import email + +import newrelic.agent + +from aiosmtpd.controller import Controller +from aiosmtpd.smtp import AuthResult, LoginPassword, Envelope + +from sqlalchemy.orm.exc import ObjectDeletedError +from email.message import Message +from email.utils import formataddr, formatdate +from init_app import load_pgp_public_keys +from server import create_light_app +from app.db import Session +from app.log import LOG,set_message_id +from app.email import status, headers +from app.utils import sanitize_email +from app.email.spam import get_spam_score +from app.pgp_utils import PGPException +from app.models import ( + Alias, + SMTPCredentials, + EmailLog, + Contact, + Mailbox +) +from app.config import ( + SMTP_SSL_KEY_FILE, + SMTP_SSL_CERT_FILE, + NOREPLY, + ENABLE_SPAM_ASSASSIN, + SPAMASSASSIN_HOST, + MAX_REPLY_PHASE_SPAM_SCORE, + BOUNCE_PREFIX_FOR_REPLY_PHASE, + LOAD_PGP_EMAIL_HANDLER, +) +from app.email_utils import ( + copy, + save_email_for_debugging, + sanitize_header, + get_header_unicode, + parse_full_address, + is_valid_alias_address_domain, + get_spam_info, + delete_all_headers_except, + sl_sendmail, + add_or_replace_header, + should_add_dkim_signature, + add_dkim_signature, + send_email, + render, + get_email_domain_part, + should_ignore_bounce, +) + +from email_handler import ( + send_no_reply_response, + get_or_create_contact, + handle_spam, + prepare_pgp_message, + replace_original_message_id +) + + +def handle_SMTP(envelope, msg: Message, rcpt_to: str) -> (bool, str): + """ + Return whether an email has been delivered and + the smtp status ("250 Message accepted", "550 Non-existent email address", etc) + """ + website_email = rcpt_to + + # region Create variables For Logging Purpose + alias_address: str = envelope.mail_from + alias = Alias.get_by(email=alias_address) + if not alias: + LOG.e("Alias: %s isn't known", alias_address) + return False, status.E503 + + alias_domain = alias_address[alias_address.find("@") + 1:] + + # Sanity check: verify alias domain is managed by SimpleLogin + # scenario: a user have removed a domain but due to a bug, the aliases are still there + if not is_valid_alias_address_domain(alias.email): + LOG.e("%s domain isn't known", alias) + return False, status.E503 + + contact = Contact.get_by(website_email=website_email) + if not contact: + LOG.w(f"No contact with {website_email} as website email") + try: + LOG.d("Create or get contact for website_email:%s", website_email) + contact = get_or_create_contact("", website_email, alias, msg) + except ObjectDeletedError: + LOG.d("maybe alias was deleted in the meantime") + alias = Alias.get_by(email=alias_address) + if not alias: + LOG.i("Alias %s was deleted in the meantime", alias_address) + if should_ignore_bounce(envelope.mail_from): + return [(True, status.E207)] + else: + return [(False, status.E515)] + + user = alias.user + mailbox = Mailbox.get_by(id=alias.mailbox_id) + # endregion + + if user.disabled: + LOG.e( + "User %s disabled, disable sending emails from %s to %s", + user, + alias, + contact, + ) + return [(False, status.E504)] + + email_log = EmailLog.create( + contact_id=contact.id, + alias_id=contact.alias_id, + is_SMTP=True, + user_id=contact.user_id, + mailbox_id=mailbox.id, + message_id=msg[headers.MESSAGE_ID], + commit=True, + ) + LOG.d("Create %s for %s, %s, %s", email_log, contact, user, mailbox) + + # Spam check + if ENABLE_SPAM_ASSASSIN: + spam_status = "" + is_spam = False + + # do not use user.max_spam_score here + if SPAMASSASSIN_HOST: + start = time.time() + spam_score, spam_report = get_spam_score(msg, email_log) + LOG.d( + "%s -> %s - spam score %s in %s seconds. Spam report %s", + alias, + contact, + spam_score, + time.time() - start, + spam_report, + ) + email_log.spam_score = spam_score + if spam_score > MAX_REPLY_PHASE_SPAM_SCORE: + is_spam = True + # only set the spam report for spam + email_log.spam_report = spam_report + else: + is_spam, spam_status = get_spam_info( + msg, max_score=MAX_REPLY_PHASE_SPAM_SCORE + ) + + if is_spam: + LOG.w( + "Email detected as spam. SMTP phase. %s -> %s. Spam Score: %s, Spam Report: %s", + alias, + contact, + email_log.spam_score, + email_log.spam_report, + ) + + email_log.is_spam = True + email_log.spam_status = spam_status + Session.commit() + + handle_spam(contact, alias, msg, user, mailbox, email_log, is_SMTP=True) + return False, status.E506 + + delete_all_headers_except( + msg, + [ + headers.FROM, + headers.TO, + headers.CC, + headers.SUBJECT, + headers.DATE, + # do not delete original message id + headers.MESSAGE_ID, + # References and In-Reply-To are used for keeping the email thread + headers.REFERENCES, + headers.IN_REPLY_TO, + ] + + headers.MIME_HEADERS, + ) + + # create PGP email if needed + if contact.pgp_finger_print and user.is_premium(): + LOG.d("Encrypt message for contact %s", contact) + try: + msg = prepare_pgp_message( + msg, contact.pgp_finger_print, contact.pgp_public_key + ) + except PGPException: + LOG.e( + "Cannot encrypt message %s -> %s. %s %s", alias, contact, mailbox, user + ) + # programming error, user shouldn't see a new email log + EmailLog.delete(email_log.id, commit=True) + # return 421 so the client can retry later + return False, status.E402 + + Session.commit() + + # make the email comes from alias <- Can be kept for SMTP as well + from_header = alias.email + # add alias name from alias + if alias.name: + LOG.d("Put alias name %s in from header", alias.name) + from_header = formataddr((alias.name, alias.email)) + elif alias.custom_domain: + # add alias name from domain + if alias.custom_domain.name: + LOG.d( + "Put domain default alias name %s in from header", + alias.custom_domain.name, + ) + from_header = formataddr((alias.custom_domain.name, alias.email)) + + LOG.d("From header is %s", from_header) + add_or_replace_header(msg, headers.FROM, from_header) + + replace_original_message_id(alias, email_log, msg) + + if not msg[headers.DATE]: + date_header = formatdate() + LOG.w("missing date header, add one") + msg[headers.DATE] = date_header + + msg[headers.SL_DIRECTION] = "Reply" + msg[headers.SL_EMAIL_LOG_ID] = str(email_log.id) + + LOG.d( + "send email from %s to %s, mail_options:%s,rcpt_options:%s", + alias.email, + contact.website_email, + envelope.mail_options, + envelope.rcpt_options, + ) + + if should_add_dkim_signature(alias_domain): + add_dkim_signature(msg, alias_domain) + + # generate a mail_from for VERP <- Can be kept for SMTP as well + verp_mail_from = f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+{email_log.id}+@{alias_domain}" + + try: + sl_sendmail( + verp_mail_from, + contact.website_email, + msg, + envelope.mail_options, + envelope.rcpt_options, + is_forward=False, + ) + except Exception: + LOG.w("Cannot send email from %s to %s", alias, contact) + EmailLog.delete(email_log.id, commit=True) + send_email( + mailbox.email, + f"Email cannot be sent to {contact.email} from {alias.email}", + render( + "transactional/reply-error.txt.jinja2", + user=user, + alias=alias, + contact=contact, + contact_domain=get_email_domain_part(contact.email), + ), + render( + "transactional/reply-error.html", + user=user, + alias=alias, + contact=contact, + contact_domain=get_email_domain_part(contact.email), + ), + ) + + # return 250 even if error as user is already informed of the incident and can retry sending the email + return True, status.E200 + + +class SMTPAuthenticator: + def fail_nothandled(self,message=None) -> AuthResult: + return AuthResult(success=False, handled=False, message=message) + + def __call__(self, server, session, envelope, mechanism, auth_data): + if mechanism not in ("LOGIN", "PLAIN"): + LOG.e( + "mechanism %s not supported.", + mechanism + ) + return self.fail_nothandled("550 Mechanism not supported") + + if not isinstance(auth_data, LoginPassword): + LOG.e("Incorrect Format for Credentials") + return self.fail_nothandled(status.E501) + + username = auth_data.login + password = auth_data.password + + alias = Alias.get_by(email=username) + if not alias: + LOG.e( + "alias %s does not exist.", + alias.email + ) + return self.fail_nothandled(status.E502) + + user = alias.user + + if not user or user.disabled: + LOG.e( + "User for alias %s is disabled", + username + ) + return self.fail_nothandled(status.E504) + + is_smtp_enabled_for_aliases = user.enable_SMTP_aliases + + if not is_smtp_enabled_for_aliases: + LOG.e("SMTP disabled by user") + return self.fail_nothandled("521 SMTP disabled by user") + + SMTPCred = SMTPCredentials.get_by(alias_id=alias.id) + if not SMTPCred or not SMTPCred.check_password(password): + LOG.e( + "Credentials Mismatch for alias %s", + username, + ) + return self.fail_nothandled("535 5.7.8 Authentication credentials invalid") + + return AuthResult(success=True,auth_data=auth_data) + + +def handle(envelope: Envelope, msg: Message) -> str: + """Return SMTP status""" + + # sanitize mail_from, rcpt_tos + mail_from = sanitize_email(envelope.mail_from) + rcpt_tos = [sanitize_email(rcpt_to) for rcpt_to in envelope.rcpt_tos] + envelope.mail_from = mail_from + envelope.rcpt_tos = rcpt_tos + + # sanitize email headers + sanitize_header(msg, "from") + sanitize_header(msg, "to") + sanitize_header(msg, "cc") + sanitize_header(msg, "reply-to") + + LOG.d( + "==>> Handle mail_from:%s, rcpt_tos:%s, header_from:%s, header_to:%s, " + "cc:%s, reply-to:%s, message_id:%s, client_ip:%s, headers:%s, mail_options:%s, rcpt_options:%s", + mail_from, + rcpt_tos, + msg[headers.FROM], + msg[headers.TO], + msg[headers.CC], + msg[headers.REPLY_TO], + msg[headers.MESSAGE_ID], + msg[headers.SL_CLIENT_IP], + msg._headers, + envelope.mail_options, + envelope.rcpt_options, + ) + + res: [(bool, str)] = [] + + nb_rcpt_tos = len(rcpt_tos) + for rcpt_index, rcpt_to in enumerate(rcpt_tos): + if rcpt_to == NOREPLY: + LOG.i("email sent to {} address from {}".format(NOREPLY, mail_from)) + send_no_reply_response(mail_from, msg) + return status.E200 + + # create a copy of msg for each recipient except the last one + # as copy() is a slow function + if rcpt_index < nb_rcpt_tos - 1: + LOG.d("copy message for rcpt %s", rcpt_to) + copy_msg = copy(msg) + else: + copy_msg = msg + + # SMTP case + LOG.d( + "SMTP phase %s(%s) -> %s", mail_from, copy_msg[headers.FROM], rcpt_to + ) + is_delivered, smtp_status = handle_SMTP(envelope, copy_msg, rcpt_to) + res.append((is_delivered, smtp_status)) + + # to know whether both successful and unsuccessful deliveries can happen at the same time + nb_success = len([is_success for (is_success, smtp_status) in res if is_success]) + nb_non_success = len( + [is_success for (is_success, smtp_status) in res if not is_success] + ) + + if nb_success > 0 and nb_non_success > 0: + LOG.e(f"some deliveries fail and some success, {mail_from}, {rcpt_tos}, {res}") + + for (is_success, smtp_status) in res: + # Consider all deliveries successful if 1 delivery is successful + if is_success: + return smtp_status + + # Failed delivery for all, return the first failure + return res[0][1] + + +class SMTPHandler: + async def handle_DATA(self, server, session, envelope: Envelope): + username = session.auth_data.login + msg = email.message_from_bytes(envelope.original_content) + try: + ret = self.check_and_handle(envelope, msg, username) + return ret + except Exception as e: + LOG.e( + "email handling fail with error:%s " + "mail_from:%s, rcpt_tos:%s, header_from:%s, header_to:%s, saved to %s", + e, + envelope.mail_from, + envelope.rcpt_tos, + msg[headers.FROM], + msg[headers.TO], + save_email_for_debugging( + msg, file_name_prefix=e.__class__.__name__ + ), # todo: remove + ) + return status.E404 + + def check_and_handle(self, envelope: Envelope, msg: Message, username) -> str: + """Return SMTP status""" + mail_from = sanitize_email(envelope.mail_from) + envelope.mail_from = mail_from + + # sanitize email headers + sanitize_header(msg, "from") + + # If Sending from MUA, "mail_from", "from" and "username" should match <- This should prevent Spoofing + from_header = get_header_unicode(msg[headers.FROM]) + if from_header: + try: + _, from_header_address = parse_full_address(from_header) + except ValueError: + LOG.w("cannot parse the From header %s", from_header) + return status.E501 # Could be changed + else: + if mail_from != username or from_header_address != username: + LOG.e( + "Mail_From: '%s' , From Header: '%s' and username '%s' does not match", + mail_from, + msg[headers.FROM], username + ) + return status.E509 + + return self._handle(envelope, msg) + + @newrelic.agent.background_task() + def _handle(self, envelope: Envelope, msg: Message): + start = time.time() + + # generate a different message_id to keep track of an email lifecycle + message_id = str(uuid.uuid4()) + set_message_id(message_id) + + LOG.d("====>=====>====>====>====>====>====>====>") + LOG.i( + "New message, mail from %s, rctp tos %s ", + envelope.mail_from, + envelope.rcpt_tos, + ) + newrelic.agent.record_custom_metric( + "Custom/nb_rcpt_tos", len(envelope.rcpt_tos) + ) + + with create_light_app().app_context(): + ret = handle(envelope, msg) + elapsed = time.time() - start + + LOG.i( + "Finish mail_from %s, rcpt_tos %s, takes %s seconds <<===", + envelope.mail_from, + envelope.rcpt_tos, + elapsed, + ) + newrelic.agent.record_custom_metric("Custom/email_handler_time", elapsed) + newrelic.agent.record_custom_metric("Custom/number_incoming_email", 1) + return ret + + + +def main(port: int): + """Use aiosmtpd Controller""" + handler = SMTPHandler() + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain( + certfile=SMTP_SSL_CERT_FILE, + keyfile=SMTP_SSL_KEY_FILE + ) + controller = Controller( + handler, + hostname="0.0.0.0", + port=port, + ssl_context=ssl_context, # Implicit SSL/TLS + authenticator=SMTPAuthenticator(), + auth_required=True, + # Below param needs to be set in case of implicit SSL/TLS as per (https://github.com/aio-libs/aiosmtpd/issues/281) + auth_require_tls=False + ) + + controller.start() + LOG.d("Start SMTP controller %s %s", controller.hostname, controller.port) + + if LOAD_PGP_EMAIL_HANDLER: + LOG.w("LOAD PGP keys") + load_pgp_public_keys() + + while True: + time.sleep(2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-p", "--port", help="SMTP port to listen for", type=int, default=8465 + ) + args = parser.parse_args() + + LOG.i("Listen for port %s", args.port) + main(port=args.port) \ No newline at end of file diff --git a/app/config.py b/app/config.py index 4a83f985e..3f1b4dfe3 100644 --- a/app/config.py +++ b/app/config.py @@ -444,3 +444,7 @@ def setup_nameservers(): NAMESERVERS = setup_nameservers() + +# For SMTP support +SMTP_SSL_CERT_FILE = os.environ.get("SMTP_SSL_CERT_FILE") or "/smtp_ssl_cert.pem" +SMTP_SSL_KEY_FILE = os.environ.get("SMTP_SSL_KEY_FILE") or "/smtp_ssl_privkey.key" diff --git a/app/models.py b/app/models.py index b40ca7410..ac0fd37df 100644 --- a/app/models.py +++ b/app/models.py @@ -417,6 +417,10 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle): sa.Boolean, default=False, nullable=False, server_default="0" ) + enable_SMTP_aliases = sa.Column( + sa.Boolean, default=False, nullable=False, server_default="0" + ) + referral_id = sa.Column( sa.ForeignKey("referral.id", ondelete="SET NULL"), nullable=True, default=None ) @@ -1305,6 +1309,9 @@ class Alias(Base, ModelMixin): TSVector(), sa.Computed("to_tsvector('english', note)", persisted=True) ) + # Enable SMTP for alias + enable_SMTP = sa.Column(sa.Boolean(), default=False, nullable=False, server_default="0") + __table_args__ = ( Index("ix_video___ts_vector__", ts_vector, postgresql_using="gin"), # index on note column using pg_trgm @@ -1354,6 +1361,12 @@ def pgp_enabled(self) -> bool: return True return False + def SMTP_enabled(self) -> bool: + """return True is SMTP is enabled for the alias""" + if self.enable_SMTP: + return True + return False + @staticmethod def get_custom_domain(alias_address) -> Optional["CustomDomain"]: alias_domain = validate_email( @@ -1784,6 +1797,9 @@ class EmailLog(Base, ModelMixin): # whether this is a reply is_reply = sa.Column(sa.Boolean, nullable=False, default=False) + # whether this is sent from SMTP + is_SMTP = sa.Column(sa.Boolean, nullable=False, default=False) + # for ex if alias is disabled, this forwarding is blocked blocked = sa.Column(sa.Boolean, nullable=False, default=False) @@ -2557,6 +2573,22 @@ class AliasMailbox(Base, ModelMixin): alias = orm.relationship(Alias) +class SMTPCredentials(Base, ModelMixin, PasswordOracle): + __tablename__ = "SMTP_credentials" + __table_args__ = ( + sa.UniqueConstraint("alias_id", name="uq_alias"), + ) + + alias_id = sa.Column( + sa.ForeignKey(Alias.id, ondelete="cascade"), nullable=False, index=True + ) + + # override, SMTP password should not be null + password = sa.Column(sa.String(128), nullable=False) + + alias = orm.relationship(Alias) + + class AliasHibp(Base, ModelMixin): __tablename__ = "alias_hibp" diff --git a/email_handler.py b/email_handler.py index ddba3e4b6..054a0a509 100644 --- a/email_handler.py +++ b/email_handler.py @@ -775,6 +775,11 @@ def forward_email_to_mailbox( else: return False, status.E518 + # check if alias has SMTP enabled + is_SMTP_enabled_for_alias = False + if user.enable_SMTP_aliases and alias.SMTP_enabled(): + is_SMTP_enabled_for_alias = True + # sanity check: make sure mailbox is not actually an alias if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email): LOG.w( @@ -927,28 +932,30 @@ def forward_email_to_mailbox( replace_sl_message_id_by_original_message_id(msg) - # change the from_header so the email comes from a reverse-alias - # replace the email part in from: header - old_from_header = msg[headers.FROM] - new_from_header = contact.new_addr() - add_or_replace_header(msg, "From", new_from_header) - LOG.d("From header, new:%s, old:%s", new_from_header, old_from_header) - - if reply_to_contact: - reply_to_header = msg[headers.REPLY_TO] - new_reply_to_header = reply_to_contact.new_addr() - add_or_replace_header(msg, "Reply-To", new_reply_to_header) - LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header) - - # replace CC & To emails by reverse-alias for all emails that are not alias - try: - replace_header_when_forward(msg, alias, "Cc") - replace_header_when_forward(msg, alias, "To") - except CannotCreateContactForReverseAlias: - LOG.d("CannotCreateContactForReverseAlias error, delete %s", email_log) - EmailLog.delete(email_log.id) - Session.commit() - raise + # Don't Change Headers when SMTP is enabled for alias + if not is_SMTP_enabled_for_alias: + # change the from_header so the email comes from a reverse-alias + # replace the email part in from: header + old_from_header = msg[headers.FROM] + new_from_header = contact.new_addr() + add_or_replace_header(msg, "From", new_from_header) + LOG.d("From header, new:%s, old:%s", new_from_header, old_from_header) + + if reply_to_contact: + reply_to_header = msg[headers.REPLY_TO] + new_reply_to_header = reply_to_contact.new_addr() + add_or_replace_header(msg, "Reply-To", new_reply_to_header) + LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header) + + # replace CC & To emails by reverse-alias for all emails that are not alias + try: + replace_header_when_forward(msg, alias, "Cc") + replace_header_when_forward(msg, alias, "To") + except CannotCreateContactForReverseAlias: + LOG.d("CannotCreateContactForReverseAlias error, delete %s", email_log) + EmailLog.delete(email_log.id) + Session.commit() + raise # add List-Unsubscribe header if user.one_click_unsubscribe_block_sender: @@ -1866,6 +1873,7 @@ def handle_spam( mailbox: Mailbox, email_log: EmailLog, is_reply=False, # whether the email is in forward or reply phase + is_SMTP=False, # whether the email is sent via SMTP ): # Store the report & original email orig_msg = get_orig_message_from_spamassassin_report(msg) @@ -1895,7 +1903,7 @@ def handle_spam( refused_email_url = f"{URL}/dashboard/refused_email?highlight_id={email_log.id}" disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}" - if is_reply: + if is_reply or is_SMTP: LOG.d( "Inform %s (%s) about spam email sent from alias %s to %s. %s", mailbox, From a93696e69e7e8b38f23e9eb193f6309ab9237e5d Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Thu, 31 Mar 2022 22:35:12 -0400 Subject: [PATCH 02/36] Add helper functions to SMTPCredentials class --- app/models.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index ac0fd37df..89e4506e3 100644 --- a/app/models.py +++ b/app/models.py @@ -2583,11 +2583,28 @@ class SMTPCredentials(Base, ModelMixin, PasswordOracle): sa.ForeignKey(Alias.id, ondelete="cascade"), nullable=False, index=True ) - # override, SMTP password should not be null - password = sa.Column(sa.String(128), nullable=False) - alias = orm.relationship(Alias) + @classmethod + def create(cls, alias_id, password, **kwargs): + smtp_cred: SMTPCredentials = super(SMTPCredentials, cls).create(alias_id=alias_id, **kwargs) + + if password and len(password) == 21: + smtp_cred.set_password(password) + else: + return None + + Session.flush() + + return smtp_cred + + @classmethod + def delete_by_alias_id(cls, alias_id, commit=False): + Session.query(cls).filter(cls.alias_id == alias_id).delete() + + if commit: + Session.commit() + class AliasHibp(Base, ModelMixin): __tablename__ = "alias_hibp" From fbfc714acefa830a6dac73c89722653a7d66203f Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Fri, 1 Apr 2022 04:07:12 -0400 Subject: [PATCH 03/36] Modify Front-end for SMTP support --- app/api/views/alias.py | 18 ++- app/dashboard/views/setting.py | 10 ++ static/package-lock.json | 214 ++++++++++++++++++++++++++++++- static/package.json | 1 + templates/dashboard/index.html | 110 ++++++++++++++++ templates/dashboard/setting.html | 28 ++++ 6 files changed, 377 insertions(+), 4 deletions(-) diff --git a/app/api/views/alias.py b/app/api/views/alias.py index cff85601a..3dc72292e 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -24,7 +24,7 @@ ) from app.errors import CannotCreateContactForReverseAlias from app.log import LOG -from app.models import Alias, Contact, Mailbox, AliasMailbox +from app.models import Alias, Contact, Mailbox, AliasMailbox, SMTPCredentials from app.utils import sanitize_email @@ -242,6 +242,7 @@ def update_alias(alias_id): name (optional): in body mailbox_id (optional): in body disable_pgp (optional): in body + enable_SMTP & SMTP_password (optional): in body Output: 200 """ @@ -318,6 +319,21 @@ def update_alias(alias_id): alias.pinned = data.get("pinned") changed = True + if "enable_SMTP" in data: + enable_SMTP = data.get("enable_SMTP") + if enable_SMTP: + if "SMTP_password" in data: + SMTP_password = data.get("SMTP_password") + if SMTP_password and len(SMTP_password) != 21: # 21 is default password length set for nanoid. + return jsonify(error="Invalid SMTP Password Length"), 400 + SMTPCredentials.create(alias_id=alias.id, password=SMTP_password) + else: + return jsonify(error="SMTP password not supplied"), 400 + else: + SMTPCredentials.delete_by_alias_id(alias_id=alias.id) + alias.enable_SMTP = enable_SMTP + changed = True + if changed: Session.commit() diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index b9af1e0bf..0c1d92b6e 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -258,6 +258,16 @@ def setting(): flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "smtp-for-aliases": + choose = request.form.get("smtp-for-aliases") + if choose == "on": + current_user.enable_SMTP_aliases = True + else: + current_user.enable_SMTP_aliases = False + Session.commit() + flash("Your preference has been updated", "success") + return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "sender-in-ra": choose = request.form.get("enable") if choose == "on": diff --git a/static/package-lock.json b/static/package-lock.json index 4d1dd1ca3..19b1584f8 100644 --- a/static/package-lock.json +++ b/static/package-lock.json @@ -1,8 +1,209 @@ { "name": "simplelogin", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "simplelogin", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@sentry/browser": "^5.30.0", + "bootbox": "^5.5.2", + "font-awesome": "^4.7.0", + "htmx.org": "^1.6.1", + "intro.js": "^2.9.3", + "multiple-select": "^1.5.2", + "nanoid": "^3.3.2", + "parsleyjs": "^2.9.2", + "qrious": "^4.0.2", + "toastr": "^2.1.4", + "vue": "^2.6.14" + } + }, + "node_modules/@sentry/browser": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz", + "integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==", + "dependencies": { + "@sentry/core": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/core": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", + "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", + "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "dependencies": { + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", + "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bootbox": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/bootbox/-/bootbox-5.5.2.tgz", + "integrity": "sha512-q8d9VO2A4+q6S0XvovLtqtBUp7uRy0wtDOuuycnoheK2TiAm3um0jOlAOu9ORn9XoT92tdil+p15Dle1mRgSPQ==", + "dependencies": { + "bootstrap": "^4.4.0", + "jquery": "^3.5.1", + "popper.js": "^1.16.0" + } + }, + "node_modules/bootstrap": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", + "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=", + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/htmx.org": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.6.1.tgz", + "integrity": "sha512-i+1k5ee2eFWaZbomjckyrDjUpa3FMDZWufatUSBmmsjXVksn89nsXvr1KLGIdAajiz+ZSL7TE4U/QaZVd2U2sA==" + }, + "node_modules/intro.js": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-2.9.3.tgz", + "integrity": "sha512-hC+EXWnEuJeA3CveGMat3XHePd2iaXNFJIVfvJh2E9IzBMGLTlhWvPIVHAgKlOpO4lNayCxEqzr4N02VmHFr9Q==" + }, + "node_modules/jquery": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" + }, + "node_modules/multiple-select": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/multiple-select/-/multiple-select-1.5.2.tgz", + "integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==", + "peerDependencies": { + "jquery": "1.9.1 - 3" + } + }, + "node_modules/nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parsleyjs": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/parsleyjs/-/parsleyjs-2.9.2.tgz", + "integrity": "sha512-DKS2XXTjEUZ1BJWUzgXAr+550kFBZrom2WYweubqdV7WzdNC1hjOajZDfeBPoAZMkXumJPlB3v37IKatbiW8zQ==", + "dependencies": { + "jquery": ">=1.8.0" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/qrious": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", + "integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g==" + }, + "node_modules/toastr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", + "dependencies": { + "jquery": ">=1.12.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/vue": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==" + } + }, "dependencies": { "@sentry/browser": { "version": "5.30.0", @@ -74,7 +275,8 @@ "bootstrap": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", - "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" + "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==", + "requires": {} }, "font-awesome": { "version": "4.7.0", @@ -99,7 +301,13 @@ "multiple-select": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/multiple-select/-/multiple-select-1.5.2.tgz", - "integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==" + "integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==", + "requires": {} + }, + "nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==" }, "parsleyjs": { "version": "2.9.2", diff --git a/static/package.json b/static/package.json index b48b6dc7a..6fac196ce 100644 --- a/static/package.json +++ b/static/package.json @@ -22,6 +22,7 @@ "htmx.org": "^1.6.1", "intro.js": "^2.9.3", "multiple-select": "^1.5.2", + "nanoid": "^3.3.2", "parsleyjs": "^2.9.2", "qrious": "^4.0.2", "toastr": "^2.1.4", diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index 225c3cf02..a5f54674c 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -27,6 +27,11 @@ } + + + + {% endblock %} {% block title %} @@ -190,6 +195,13 @@ + + {% if current_user.enable_SMTP_aliases %} + + {% endif %} + @@ -277,6 +289,13 @@ title="This alias is pinned"> {% endif %} + {% if current_user.enable_SMTP_aliases %} + {% if alias.enable_SMTP %} + + {% endif %} + {% endif %} + {% if alias.hibp_breaches | length > 0 %} + {% if current_user.enable_SMTP_aliases %} +
+ Enable SMTP for this alias + +
+
+ +
+ {% endif %}
+ {% if current_user.enable_SMTP_aliases %} + + {% endif %} {% endblock %} diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index da0b10970..42dbeb554 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -351,6 +351,34 @@
+ +
+
+
SMTP for Aliases +
Experimental
+
+
+ This will enable SMTP access for aliases. + If this option is enabled, emails can be sent directly from aliases via SMTP without the need for creation of reverse-alias. +
+ Note: After enabling this, you would still need to enable SMTP individually per alias. +
+ This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original address will be displayed in "From:". +
+
+
+ +
+ + +
+ +
+
+
+ +
From 334ab9391bc6869d249799b89fb29877fee491fa Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Fri, 1 Apr 2022 14:45:36 -0400 Subject: [PATCH 04/36] Fix smtp-for-aliases-section checkbox issue --- templates/dashboard/setting.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 42dbeb554..507c84394 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -371,7 +371,7 @@
- +
From d07af5197aa0ce39a6010d56e7a1a7bb5e547124 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Fri, 1 Apr 2022 14:47:04 -0400 Subject: [PATCH 05/36] Change script type to module Also change way of importing nanoid --- templates/dashboard/index.html | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index a5f54674c..845f6f472 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -27,11 +27,6 @@ } - - - - {% endblock %} {% block title %} @@ -625,7 +620,10 @@ $('.highlighted').tooltip("show"); {% if current_user.enable_SMTP_aliases %} - - - - $('.highlighted').tooltip("show"); - - {% if current_user.enable_SMTP_aliases %} + + {% if current_user.enable_SMTP_aliases %} {% endif %} {% endblock %} diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 507c84394..2d76dd0fc 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -3,627 +3,653 @@ {% set active_page = "setting" %} {% block title %} - Settings + Settings {% endblock %} {% block head %} - + {% endblock %} {% block default_content %} -
- -
-
-
Current Plan
- - {% if current_user.lifetime %} - You have lifetime access to the Premium plan. - {% elif current_user.lifetime_or_active_subscription() %} - {% if current_user.get_subscription() %} -
- {{ current_user.get_subscription().plan_name() }} plan subscribed via Paddle. - - Manage Subscription âž¡ - +
+ +
+
+
Current Plan
+ + {% if current_user.lifetime %} + You have lifetime access to the Premium plan. + {% elif current_user.lifetime_or_active_subscription() %} + {% if current_user.get_subscription() %} +
+ {{ current_user.get_subscription().plan_name() }} plan subscribed via Paddle. + + Manage Subscription âž¡ + +
+ {% endif %} + + {% if manual_sub and manual_sub.is_active() %} +
+ Manual plan which expires {{ manual_sub.end_at | dt }} + ({{ manual_sub.end_at.format("YYYY-MM-DD") }}). + {% if manual_sub.is_giveaway %} +
+ To gain additional features and support SimpleLogin you can upgrade to a Premium plan. +
+ Upgrade + {% endif %} +
+ {% endif %} + + {% if apple_sub and apple_sub.is_valid() %} +
+ Premium plan subscribed via Apple which expires {{ apple_sub.expires_date | dt }} + ({{ apple_sub.expires_date.format("YYYY-MM-DD") }}). +
+ If you want to subscribe via the Web instead, please make sure to cancel your + subscription + on Apple first. + Upgrade +
+
+ {% endif %} + + {% if coinbase_sub and coinbase_sub.is_active() %} +
+ Yearly plan subscribed with cryptocurrency which expires on + {{ coinbase_sub.end_at.format("YYYY-MM-DD") }}. + + Extend Subscription + +
+ {% endif %} + {% elif current_user.in_trial() %} + Your Premium trial expires {{ current_user.trial_end | dt }}. + {% else %} + You are on the Free plan. + {% endif %}
- {% endif %} - - {% if manual_sub and manual_sub.is_active() %} -
- Manual plan which expires {{ manual_sub.end_at | dt }} - ({{ manual_sub.end_at.format("YYYY-MM-DD") }}). - {% if manual_sub.is_giveaway %} -
- To gain additional features and support SimpleLogin you can upgrade to a Premium plan.
- Upgrade - {% endif %} -
- {% endif %} - - {% if apple_sub and apple_sub.is_valid() %} -
- Premium plan subscribed via Apple which expires {{ apple_sub.expires_date | dt }} - ({{ apple_sub.expires_date.format("YYYY-MM-DD") }}). -
- If you want to subscribe via the Web instead, please make sure to cancel your subscription - on Apple first. - Upgrade -
+
+ + + +
+
+
Two Factor Authentication
+
+ Secure your account with 2FA, you'll be asked for a code generated through an app when you login. +
+
+ {% if not current_user.enable_otp %} + Setup TOTP + {% else %} + Disable TOTP + Recovery + Codes + {% endif %}
- {% endif %} - - {% if coinbase_sub and coinbase_sub.is_active() %} -
- Yearly plan subscribed with cryptocurrency which expires on - {{ coinbase_sub.end_at.format("YYYY-MM-DD") }}. - - Extend Subscription - +
+ + + +
+
+
Security Key (WebAuthn)
+
+ You can secure your account by linking either your FIDO-supported physical key such as Yubikey, + Google + Titan, + or a device with appropriate hardware to your account. +
+ {% if current_user.fido_uuid is none %} + Setup WebAuthn + {% else %} + Manage WebAuthn + Recovery + Codes + {% endif %}
- {% endif %} - {% elif current_user.in_trial() %} - Your Premium trial expires {{ current_user.trial_end | dt }}. - {% else %} - You are on the Free plan. - {% endif %} -
-
- - - -
-
-
Two Factor Authentication
-
- Secure your account with 2FA, you'll be asked for a code generated through an app when you login.
- {% if not current_user.enable_otp %} - Setup TOTP - {% else %} - Disable TOTP - Recovery Codes - {% endif %} -
-
- - - -
-
-
Security Key (WebAuthn)
-
- You can secure your account by linking either your FIDO-supported physical key such as Yubikey, Google - Titan, - or a device with appropriate hardware to your account. + + + + +
+
+
Newsletters
+
+ We will occasionally send you emails with new feature announcements. +
+
+ +
+ + +
+ +
+
- {% if current_user.fido_uuid is none %} - Setup WebAuthn - {% else %} - Manage WebAuthn - Recovery Codes - {% endif %} -
-
- - - - -
-
-
Newsletters
-
- We will occasionally send you emails with new feature announcements. + + + +
+
+ {{ form.csrf_token }} + + +
+
+ Profile +
+
+ This information will be filled in automatically when you use the + Sign in with SimpleLogin button. +
+
+ + {{ form.name(class="form-control", value=current_user.name) }} + {{ render_field_errors(form.name) }} +
+ +
+
Profile picture
+ {{ form.profile_picture(class="form-control-file") }} + {{ render_field_errors(form.profile_picture) }} + {% if current_user.profile_picture_id %} + + {% endif %} +
+ +
+
-
- -
- - -
- -
-
-
- - - -
-
- {{ form.csrf_token }} - - -
-
- Profile -
-
- This information will be filled in automatically when you use the - Sign in with SimpleLogin button. -
-
- - {{ form.name(class="form-control", value=current_user.name) }} - {{ render_field_errors(form.name) }} -
- -
-
Profile picture
- {{ form.profile_picture(class="form-control-file") }} - {{ render_field_errors(form.profile_picture) }} - {% if current_user.profile_picture_id %} - - {% endif %} -
- + + + +
+ + + {{ change_email_form.csrf_token }} + +
+
+ Account Email +
+
+ This email address is used to log in to SimpleLogin.
+ If you want to change the mailbox that emails are forwarded to, use the + + Mailboxes page + instead. +
+
+ + {{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }} + {{ render_field_errors(change_email_form.email) }} + + {% if pending_email %} +
+ Pending email change: {{ pending_email }} + Resend + confirmation email + Cancel email + change +
+ {% endif %} +
+ +
+
- -
- - - -
-
- - {{ change_email_form.csrf_token }} - -
-
- Account Email -
-
- This email address is used to log in to SimpleLogin.
- If you want to change the mailbox that emails are forwarded to, use the - - Mailboxes page - instead. -
-
- - {{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }} - {{ render_field_errors(change_email_form.email) }} - - {% if pending_email %} -
- Pending email change: {{ pending_email }} - Resend - confirmation email - Cancel email - change -
- {% endif %} -
- + + + +
+
+
+ Password +
+
+ You will receive an email containing instructions on how to change your password. +
+ + + + +
- -
- + + + +
+
+
Aliases
+ +
Change the way random aliases are generated by default.
+
+ + + +
+ +
Select the default domain for aliases.
+
+ + + +
+ +
Select the default suffix generator for aliases.
+
+ + + +
- -
-
-
- Password +
-
- You will receive an email containing instructions on how to change your password. + + + +
+
+
Sender Address Format
+
+ When your alias receives an email, say from: John Wick <john@wick.com>, + SimpleLogin forwards it to your mailbox.
+ + Due to some email constraints, SimpleLogin cannot keep the sender email address + in the original form and needs to transform it to one of the formats below. +
+ +
+ + + + + +
+
-
- - -
-
-
- - - -
-
-
Aliases
- -
Change the way random aliases are generated by default.
-
- - - -
- -
Select the default domain for aliases.
-
- - - -
- -
Select the default suffix generator for aliases.
-
- - - -
- -
-
- - - -
-
-
Sender Address Format
-
- When your alias receives an email, say from: John Wick <john@wick.com>, - SimpleLogin forwards it to your mailbox.
- - Due to some email constraints, SimpleLogin cannot keep the sender email address - in the original form and needs to transform it to one of the formats below. + + + +
+
+
Reverse Alias Replacement +
Experimental
+
+
+ When replying to a forwarded email, the reverse-alias can be automatically included + in the attached message by your email client. + If this option is enabled, SimpleLogin will try to replace the reverse-alias by your contact + email. +
+
+
+ +
+ + +
+ +
+
- -
- - - - - -
-
-
- - - -
-
-
Reverse Alias Replacement -
Experimental
+ + + +
+
+
SMTP for Aliases +
Experimental
+
+
+ This will enable SMTP access for aliases. + If this option is enabled, emails can be sent directly from aliases via SMTP without the need for + creation of reverse-alias. +
+ Note: After enabling this, you would still need to enable SMTP individually per alias. +
+ This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original + address will be displayed in "From:". +
+
+
+ +
+ + +
+ +
+
-
- When replying to a forwarded email, the reverse-alias can be automatically included - in the attached message by your email client. - If this option is enabled, SimpleLogin will try to replace the reverse-alias by your contact email. -
+ + + +
+
+
Include sender address in reverse-alias
+
+ By default, the reverse-alias is randomly generated and doesn't contain any information about + the sender.
+ + Enabling this option will include the sender address in the reverse-alias.
+ + This can be useful if you want to set up email filters based on the reverse-alias. + It will also make the reverse-alias more readable. +
+
+ +
+ + +
+ +
+
-
- -
- - -
- -
-
-
- - - -
-
-
SMTP for Aliases -
Experimental
+ + + +
+
+
Always expand alias info
+
+ By default, additional alias info is shown after clicking on the "More" button.
+ When this option is enabled, alias additional info will always be shown.
+
+
+ +
+ + +
+ +
+
-
- This will enable SMTP access for aliases. - If this option is enabled, emails can be sent directly from aliases via SMTP without the need for creation of reverse-alias. -
- Note: After enabling this, you would still need to enable SMTP individually per alias. -
- This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original address will be displayed in "From:". -
+ + + +
+
+
+ Include website address in one-click alias creation on browser extension +
+
+ If enabled, the website name will be used as alias prefix + when you create an alias via SimpleLogin browser extension via the email input field
+ +
+
+ +
+ + +
+ +
+
-
- -
- - -
- -
-
-
- - - -
-
-
Include sender address in reverse-alias
-
- By default, the reverse-alias is randomly generated and doesn't contain any information about - the sender.
- - Enabling this option will include the sender address in the reverse-alias.
- - This can be useful if you want to set up email filters based on the reverse-alias. - It will also make the reverse-alias more readable. + + + + {#
#} + {#
#} + {#
Ignore Loop Emails
#} + {#
#} + {# On some email clients, "Reply All" automatically includes your alias that#} + {# would send the same email to your mailbox.#} + {#
#} + {# You can disable these "loop" emails by enabling this option.#} + {#
#} + {#
#} + {# #} + {#
#} + {# #} + {# #} + {#
#} + {# #} + {#
#} + {#
#} + {#
#} + + + +
+
+
One-click unsubscribe
+
+ On email clients that support the + One-click + unsubscribe button, + clicking on it will disable the alias that receives the emails. +
+ You can choose to block the sender instead of disabling the alias. +
+
+ +
+ + +
+ +
+
-
- -
- - -
- -
-
-
- - - -
-
-
Always expand alias info
-
- By default, additional alias info is shown after clicking on the "More" button.
- When this option is enabled, alias additional info will always be shown.
+ + +
+
+
Quarantine
+
+ When an email sent to your alias is classified as spam or refused by your email provider, + it usually means your alias has been leaked to a spammer.
+ In this case SimpleLogin will keep a copy of this email (so it isn't lost) + and notify you so you can take a look at its content and take appropriate actions.
+ + The emails are deleted in 7 days. + This is an exceptional case where SimpleLogin stores the email. +
+ + See refused emails + +
-
- -
- - -
- -
-
-
- - -
-
-
- Include website address in one-click alias creation on browser extension -
-
- If enabled, the website name will be used as alias prefix - when you create an alias via SimpleLogin browser extension via the email input field
- -
-
- -
- - -
- -
-
-
- - - - {#
#} - {#
#} - {#
Ignore Loop Emails
#} - {#
#} - {# On some email clients, "Reply All" automatically includes your alias that#} - {# would send the same email to your mailbox.#} - {#
#} - {# You can disable these "loop" emails by enabling this option.#} - {#
#} - {#
#} - {# #} - {#
#} - {# #} - {# #} - {#
#} - {# #} - {#
#} - {#
#} - {#
#} - - - -
-
-
One-click unsubscribe
-
- On email clients that support the - One-click unsubscribe button, - clicking on it will disable the alias that receives the emails. -
- You can choose to block the sender instead of disabling the alias. -
-
- -
- - -
- -
-
-
- - -
-
-
Quarantine
-
- When an email sent to your alias is classified as spam or refused by your email provider, - it usually means your alias has been leaked to a spammer.
- In this case SimpleLogin will keep a copy of this email (so it isn't lost) - and notify you so you can take a look at its content and take appropriate actions.
- - The emails are deleted in 7 days. - This is an exceptional case where SimpleLogin stores the email. +
+
+
Disabled alias/Blocked contact
+
+ When an email is sent to a disabled alias or sent from a blocked contact, you can + decide what response the sender should see.
+ Ignore means they will see the message as delivered, but SimpleLogin won't actually forward + it to you. + This is the default option as you can start receiving the emails again + by re-enabling the alias or unblocking a contact.
+ Reject means SimpleLogin will tell them that the alias does not exist.
+
+
+ + + + + +
+
- - See refused emails - -
-
-
-
-
Disabled alias/Blocked contact
-
- When an email is sent to a disabled alias or sent from a blocked contact, you can decide what response the sender should see.
- Ignore means they will see the message as delivered, but SimpleLogin won't actually forward it to you. - This is the default option as you can start receiving the emails again - by re-enabling the alias or unblocking a contact.
- Reject means SimpleLogin will tell them that the alias does not exist.
+
+
+
Include original sender in email headers +
+
+ SimpleLogin forwards emails to your mailbox from the reverse-alias and not from the original + sender address.
+ If this option is enabled, the original sender addresses is stored in the email header X-SimpleLogin-Envelope-From. + You can choose to display this header in your email client.
+ As email headers aren't encrypted, your mailbox service can know the sender address via this header. +
+
+ + +
+ + +
+ + +
+
-
- - - - - -
-
-
-
-
-
Include original sender in email headers -
-
- SimpleLogin forwards emails to your mailbox from the reverse-alias and not from the original sender address.
- If this option is enabled, the original sender addresses is stored in the email header X-SimpleLogin-Envelope-From. - You can choose to display this header in your email client.
- As email headers aren't encrypted, your mailbox service can know the sender address via this header. -
-
- - -
- - -
- - -
-
-
+
+
+
Alias Import
+
+ You can import your aliases created on other platforms into SimpleLogin. +
+ + Batch Import + -
-
-
Alias Import
-
- You can import your aliases created on other platforms into SimpleLogin. +
- - Batch Import - -
-
+
+
+
Data Export
+
+ You can download all aliases you have created on SimpleLogin along with other data. +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
-
-
-
Data Export
-
- You can download all aliases you have created on SimpleLogin along with other data. -
-
-
-
- - -
-
-
-
- - -
-
+
+
+
+
Account Deletion
+
+ If SimpleLogin isn't the right fit for you, you can simply delete your account. +
-
-
- -
-
-
Account Deletion
-
- If SimpleLogin isn't the right fit for you, you can simply delete your account. + Delete account +
- Delete account -
-
- {% endblock %} From 25a56ec7fc910b2bb9b6ab819b1cd13a67192f2c Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sat, 2 Apr 2022 22:53:09 -0400 Subject: [PATCH 26/36] Add auto-generated migrations --- .../versions/2022_040221_b26e402d2e0e_.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 migrations/versions/2022_040221_b26e402d2e0e_.py diff --git a/migrations/versions/2022_040221_b26e402d2e0e_.py b/migrations/versions/2022_040221_b26e402d2e0e_.py new file mode 100644 index 000000000..7e77b07d6 --- /dev/null +++ b/migrations/versions/2022_040221_b26e402d2e0e_.py @@ -0,0 +1,58 @@ +"""empty message + +Revision ID: b26e402d2e0e +Revises: b500363567e3 +Create Date: 2022-04-02 21:47:30.243843 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b26e402d2e0e' +down_revision = 'b500363567e3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('SMTP_credentials', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False), + sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True), + sa.Column('alias_id', sa.Integer(), nullable=False), + sa.Column('password', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['alias_id'], ['alias.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('alias_id', name='uq_alias') + ) + op.create_index(op.f('ix_SMTP_credentials_alias_id'), 'SMTP_credentials', ['alias_id'], unique=True) + op.alter_column('admin_audit_log', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=True) + op.drop_index('admin_audit_log_admin_user_id_idx', table_name='admin_audit_log') + op.drop_constraint('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', type_='foreignkey') + op.create_foreign_key(None, 'admin_audit_log', 'users', ['admin_user_id'], ['id']) + op.add_column('alias', sa.Column('enable_SMTP', sa.Boolean(), server_default='0', nullable=False)) + op.add_column('email_log', sa.Column('is_SMTP', sa.Boolean(), nullable=False)) + op.add_column('users', sa.Column('enable_SMTP_aliases', sa.Boolean(), server_default='0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'enable_SMTP_aliases') + op.drop_column('email_log', 'is_SMTP') + op.drop_column('alias', 'enable_SMTP') + op.drop_constraint(None, 'admin_audit_log', type_='foreignkey') + op.create_foreign_key('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', 'users', ['admin_user_id'], ['id'], ondelete='CASCADE') + op.create_index('admin_audit_log_admin_user_id_idx', 'admin_audit_log', ['admin_user_id'], unique=False) + op.alter_column('admin_audit_log', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=False) + op.drop_index(op.f('ix_SMTP_credentials_alias_id'), table_name='SMTP_credentials') + op.drop_table('SMTP_credentials') + # ### end Alembic commands ### From a00fc102fd8318442bd845d6702fc16a32e261b1 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sat, 2 Apr 2022 23:26:13 -0400 Subject: [PATCH 27/36] Get alias domain by get_email_domain_part in SMTP_handler Also: - remove should_ignore_bounce - Remove region comment - move "user.disabled" higher to avoid the unnecessary contact creation - Remove unnecessary parenthesis --- SMTP_handler.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/SMTP_handler.py b/SMTP_handler.py index ae47ce254..b877ab20b 100644 --- a/SMTP_handler.py +++ b/SMTP_handler.py @@ -73,14 +73,23 @@ def handle_SMTP(envelope, msg: Message, rcpt_to: str) -> (bool, str): """ website_email = rcpt_to - # region Create variables For Logging Purpose alias_address: str = envelope.mail_from alias = Alias.get_by(email=alias_address) if not alias: LOG.e("Alias: %s isn't known", alias_address) return False, status.E503 - alias_domain = alias_address[alias_address.find("@") + 1 :] + user = alias.user + if user.disabled: + LOG.e( + "User %s disabled, disable sending emails from %s to %s", + user, + alias, + website_email, + ) + return [(False, status.E504)] + + alias_domain = get_email_domain_part(alias_address) # Sanity check: verify alias domain is managed by SimpleLogin # scenario: a user have removed a domain but due to a bug, the aliases are still there @@ -99,23 +108,9 @@ def handle_SMTP(envelope, msg: Message, rcpt_to: str) -> (bool, str): alias = Alias.get_by(email=alias_address) if not alias: LOG.i("Alias %s was deleted in the meantime", alias_address) - if should_ignore_bounce(envelope.mail_from): - return [(True, status.E207)] - else: - return [(False, status.E515)] + return [(False, status.E515)] - user = alias.user mailbox = Mailbox.get_by(id=alias.mailbox_id) - # endregion - - if user.disabled: - LOG.e( - "User %s disabled, disable sending emails from %s to %s", - user, - alias, - contact, - ) - return [(False, status.E504)] email_log = EmailLog.create( contact_id=contact.id, @@ -297,8 +292,8 @@ def __call__(self, server, session, envelope, mechanism, auth_data): LOG.e("Incorrect Format for Credentials") return self.fail_nothandled(status.E501) - username = (auth_data.login).decode("utf-8") - password = (auth_data.password).decode("utf-8") + username = auth_data.login.decode("utf-8") + password = auth_data.password.decode("utf-8") alias = Alias.get_by(email=username) if not alias: @@ -401,7 +396,7 @@ def handle(envelope: Envelope, msg: Message) -> str: class SMTPHandler: async def handle_DATA(self, server, session, envelope: Envelope): - username = (session.auth_data.login).decode("utf-8") + username = session.auth_data.login.decode("utf-8") msg = email.message_from_bytes(envelope.original_content) try: ret = self.check_and_handle(envelope, msg, username) From 66b31bb685c2276ffd93a234c602afd3e1c5c20f Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sat, 2 Apr 2022 23:47:47 -0400 Subject: [PATCH 28/36] Add "Send a test email via SMTP" in CONTRIBUTING --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33d901d5b..9e316c20d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,4 +174,11 @@ python email_handler.py swaks --to e1@sl.local --from hey@google.com --server 127.0.0.1:20381 ``` +4) Send a test email via SMTP + * Make sure SMTP is enabled in settings and password is generated for the alias + +```bash +swaks --to hey@google.com --from e1@sl.local --server 127.0.0.1:465 --tls-on-connect --auth --auth-user e1@sl.local +``` + Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you should see the forwarded email. \ No newline at end of file From 82da27b49e114556e03494f7388870d475402a72 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sat, 2 Apr 2022 22:52:27 -0400 Subject: [PATCH 29/36] Revert "ReFormat index and setting htmls using Pycharm" This reverts commit aec499e6bfdfe4612008064c056da3dbc205b05e. --- templates/dashboard/index.html | 1245 +++++++++++++++--------------- templates/dashboard/setting.html | 1184 ++++++++++++++-------------- 2 files changed, 1197 insertions(+), 1232 deletions(-) diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index d8d5a0831..fc40d3965 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -3,706 +3,697 @@ {% set active_page = "dashboard" %} {% block head %} - + {% endblock %} {% block title %} - Alias + Alias {% endblock %} {% block default_content %} - -
-
-
-
-
-
- Aliases -
-
- All time -
-
-
{{ stats.nb_alias }}
-
+ +
+
+
+
+
+
+ Aliases +
+
+ All time
+
+
{{ stats.nb_alias }}
+
+
-
-
-
-
-
- Forwards -
-
- Last 14 days -
-
-
{{ stats.nb_forward }}
-
+
+
+
+
+
+ Forwards
+
+ Last 14 days +
+
+
{{ stats.nb_forward }}
+
+
-
-
-
-
-
- Replies/Sent -
-
- Last 14 days -
-
-
{{ stats.nb_reply }}
-
+
+
+
+
+
+ Replies/Sent
+
+ Last 14 days +
+
+
{{ stats.nb_reply }}
+
+
-
-
-
-
-
- Blocks -
-
- Last 14 days -
-
-
{{ stats.nb_block }}
-
+
+
+
+
+
+ Blocks +
+
+ Last 14 days
+
+
{{ stats.nb_block }}
+
- - - -
-
-
-
-
-
- - -
-
-
- - -
- - -
-
+
+ + + +
+
+
+
+
+
+ + +
+
+
+ + +
+ + +
-
- -
-
- - - - - - - -
- -
- - {% if query or sort or filter %} - Reset - {% endif %} - - - - -
-
+ +
- - -
- {% for alias_info in alias_infos %} - {% set alias = alias_info.alias %} - -
-
- -
-
+
+ +
+
+ + + + + + + +
+ +
+ + {% if query or sort or filter %} + Reset + {% endif %} + + + + +
+
+
+
+ + + +
+ {% for alias_info in alias_infos %} + {% set alias = alias_info.alias %} + +
+
+ +
+
{{ alias.email }} - {% if alias.automatic_creation %} - - {% endif %} - - {% if alias.pinned %} - - {% endif %} - - {% if current_user.enable_SMTP_aliases %} - {% if alias.enable_SMTP %} - - {% endif %} - {% endif %} - - {% if alias.hibp_breaches | length > 0 %} - + {% if alias.automatic_creation %} + + {% endif %} + + {% if alias.pinned %} + + {% endif %} + + {% if current_user.enable_SMTP_aliases %} + {% if alias.enable_SMTP %} + + {% endif %} + {% endif %} + + {% if alias.hibp_breaches | length > 0 %} + - - {% endif %} - - {% if alias.custom_domain and not alias.custom_domain.verified %} - - {% endif %} -
-
-
+
+ -
-
- - -
-
-
- {% if alias_info.latest_email_log != None %} - {% set email_log = alias_info.latest_email_log %} - {% set contact = alias_info.latest_contact %} - - {% if email_log.is_reply %} - {{ contact.website_email }} - - {{ email_log.created_at | dt }} - {% elif email_log.bounced %} - + data-step="3" + {% endif %} + style="padding-left: 0px" + > + + + + +
+
+ + +
+
+
+ {% if alias_info.latest_email_log != None %} + {% set email_log = alias_info.latest_email_log %} + {% set contact = alias_info.latest_contact %} + + {% if email_log.is_reply %} + {{ contact.website_email }} + + {{ email_log.created_at | dt }} + {% elif email_log.bounced %} + {{ contact.website_email }} {{ email_log.created_at | dt }} - {% elif email_log.blocked %} - {{ contact.website_email }} - - {{ email_log.created_at | dt }} - {% else %} - {{ contact.website_email }} - - {{ email_log.created_at | dt }} - {% include 'partials/toggle_contact.html' %} - - {% endif %} - {% else %} - No emails received/sent in the last 14 days. Created {{ alias.created_at | dt }}. - {% endif %} - -
-
-
- - -
Alias description
-
-
+ {% elif email_log.blocked %} + {{ contact.website_email }} + + {{ email_log.created_at | dt }} + {% else %} + {{ contact.website_email }} + + {{ email_log.created_at | dt }} + {% include 'partials/toggle_contact.html' %} + + {% endif %} + {% else %} + No emails received/sent in the last 14 days. Created {{ alias.created_at | dt }}. + {% endif %} + +
+
+
+ + +
Alias description
+
+
-
- - -
- - - + + + - - - -
- - {% if alias_info.latest_email_log != None %} -
- Alias created {{ alias.created_at | dt }} -
- {% endif %} - - {{ alias_info.nb_forward }} forwards, - {{ alias_info.nb_blocked }} blocks, - {{ alias_info.nb_reply }} sents - - See All  â†’ - - - {% if mailboxes|length > 1 %} -
Current mailbox
-
-
- -
- - - -
- {% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %} -
- Owned by {{ alias_info.mailbox.email }} mailbox -
- {% endif %} - -
- Display name - -
- -
-
- -
- - - -
- - {% if alias.mailbox_support_pgp() %} -
- PGP - -
-
- -
- {% endif %} - -
- Pin this alias - -
-
- -
- - {% if current_user.enable_SMTP_aliases %} -
- Enable SMTP for this alias - -
-
- -
- {% endif %} -
-
- - Transfer - - - - -
- - - - - - Delete    - -
+ +
+ {% endif %} +
+ + + +
+ + {% if alias_info.latest_email_log != None %} +
+ Alias created {{ alias.created_at | dt }} +
+ {% endif %} + + {{ alias_info.nb_forward }} forwards, + {{ alias_info.nb_blocked }} blocks, + {{ alias_info.nb_reply }} sents + + See All  â†’ + + + {% if mailboxes|length > 1 %} +
Current mailbox
+
+
+ +
-
-
+ +
+ {% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %} +
+ Owned by {{ alias_info.mailbox.email }} mailbox +
+ {% endif %} + +
+ Display name + +
-
- -
+
+
+ +
+ + +
- {% endfor %} -
- - - {% if page > 0 or not last_page %} -
-
- + {% if alias.mailbox_support_pgp() %} +
+ PGP + +
+
+ +
+ {% endif %} + +
+ Pin this alias + +
+
+ +
+ + {% if current_user.enable_SMTP_aliases %} +
+ Enable SMTP for this alias + +
+
+
+ {% endif %} +
+
+ + Transfer + + + + +
+ + + + + + Delete    + +
+ +
+
+ + +
+
- {% endif %} +
+ {% endfor %} +
+ + + + {% if page > 0 or not last_page %} +
+
+ +
+
+ {% endif %} {% endblock %} {% block script %} - - - + + - {% if current_user.enable_SMTP_aliases %} + $('.highlighted').tooltip("show"); + + {% if current_user.enable_SMTP_aliases %} {% endif %} {% endblock %} diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 2d76dd0fc..507c84394 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -3,653 +3,627 @@ {% set active_page = "setting" %} {% block title %} - Settings + Settings {% endblock %} {% block head %} - + {% endblock %} {% block default_content %} -
- -
-
-
Current Plan
- - {% if current_user.lifetime %} - You have lifetime access to the Premium plan. - {% elif current_user.lifetime_or_active_subscription() %} - {% if current_user.get_subscription() %} -
- {{ current_user.get_subscription().plan_name() }} plan subscribed via Paddle. - - Manage Subscription âž¡ - -
- {% endif %} - - {% if manual_sub and manual_sub.is_active() %} -
- Manual plan which expires {{ manual_sub.end_at | dt }} - ({{ manual_sub.end_at.format("YYYY-MM-DD") }}). - {% if manual_sub.is_giveaway %} -
- To gain additional features and support SimpleLogin you can upgrade to a Premium plan. -
- Upgrade - {% endif %} -
- {% endif %} - - {% if apple_sub and apple_sub.is_valid() %} -
- Premium plan subscribed via Apple which expires {{ apple_sub.expires_date | dt }} - ({{ apple_sub.expires_date.format("YYYY-MM-DD") }}). -
- If you want to subscribe via the Web instead, please make sure to cancel your - subscription - on Apple first. - Upgrade -
-
- {% endif %} - - {% if coinbase_sub and coinbase_sub.is_active() %} -
- Yearly plan subscribed with cryptocurrency which expires on - {{ coinbase_sub.end_at.format("YYYY-MM-DD") }}. - - Extend Subscription - -
- {% endif %} - {% elif current_user.in_trial() %} - Your Premium trial expires {{ current_user.trial_end | dt }}. - {% else %} - You are on the Free plan. - {% endif %} +
+ +
+
+
Current Plan
+ + {% if current_user.lifetime %} + You have lifetime access to the Premium plan. + {% elif current_user.lifetime_or_active_subscription() %} + {% if current_user.get_subscription() %} +
+ {{ current_user.get_subscription().plan_name() }} plan subscribed via Paddle. + + Manage Subscription âž¡ +
-
- - - -
-
-
Two Factor Authentication
-
- Secure your account with 2FA, you'll be asked for a code generated through an app when you login. -
-
- {% if not current_user.enable_otp %} - Setup TOTP - {% else %} - Disable TOTP - Recovery - Codes - {% endif %} + {% endif %} + + {% if manual_sub and manual_sub.is_active() %} +
+ Manual plan which expires {{ manual_sub.end_at | dt }} + ({{ manual_sub.end_at.format("YYYY-MM-DD") }}). + {% if manual_sub.is_giveaway %} +
+ To gain additional features and support SimpleLogin you can upgrade to a Premium plan.
+ Upgrade + {% endif %}
-
- - - -
-
-
Security Key (WebAuthn)
-
- You can secure your account by linking either your FIDO-supported physical key such as Yubikey, - Google - Titan, - or a device with appropriate hardware to your account. -
- {% if current_user.fido_uuid is none %} - Setup WebAuthn - {% else %} - Manage WebAuthn - Recovery - Codes - {% endif %} + {% endif %} + + {% if apple_sub and apple_sub.is_valid() %} +
+ Premium plan subscribed via Apple which expires {{ apple_sub.expires_date | dt }} + ({{ apple_sub.expires_date.format("YYYY-MM-DD") }}). +
+ If you want to subscribe via the Web instead, please make sure to cancel your subscription + on Apple first. + Upgrade +
-
- - - - -
-
-
Newsletters
-
- We will occasionally send you emails with new feature announcements. -
-
- -
- - -
- -
+ {% endif %} + + {% if coinbase_sub and coinbase_sub.is_active() %} +
+ Yearly plan subscribed with cryptocurrency which expires on + {{ coinbase_sub.end_at.format("YYYY-MM-DD") }}. + + Extend Subscription +
+ {% endif %} + {% elif current_user.in_trial() %} + Your Premium trial expires {{ current_user.trial_end | dt }}. + {% else %} + You are on the Free plan. + {% endif %} +
+
+ + + +
+
+
Two Factor Authentication
+
+ Secure your account with 2FA, you'll be asked for a code generated through an app when you login.
- - - -
-
- {{ form.csrf_token }} - - -
-
- Profile -
-
- This information will be filled in automatically when you use the - Sign in with SimpleLogin button. -
-
- - {{ form.name(class="form-control", value=current_user.name) }} - {{ render_field_errors(form.name) }} -
- -
-
Profile picture
- {{ form.profile_picture(class="form-control-file") }} - {{ render_field_errors(form.profile_picture) }} - {% if current_user.profile_picture_id %} - - {% endif %} -
- -
-
+ {% if not current_user.enable_otp %} + Setup TOTP + {% else %} + Disable TOTP + Recovery Codes + {% endif %} +
+
+ + + +
+
+
Security Key (WebAuthn)
+
+ You can secure your account by linking either your FIDO-supported physical key such as Yubikey, Google + Titan, + or a device with appropriate hardware to your account.
- - - -
-
- - {{ change_email_form.csrf_token }} - -
-
- Account Email -
-
- This email address is used to log in to SimpleLogin.
- If you want to change the mailbox that emails are forwarded to, use the - - Mailboxes page - instead. -
-
- - {{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }} - {{ render_field_errors(change_email_form.email) }} - - {% if pending_email %} -
- Pending email change: {{ pending_email }} - Resend - confirmation email - Cancel email - change -
- {% endif %} -
- -
-
+ {% if current_user.fido_uuid is none %} + Setup WebAuthn + {% else %} + Manage WebAuthn + Recovery Codes + {% endif %} +
+
+ + + + +
+
+
Newsletters
+
+ We will occasionally send you emails with new feature announcements.
- - - -
-
-
- Password -
-
- You will receive an email containing instructions on how to change your password. -
-
- - -
-
+
+ +
+ + +
+ +
+
+
+ + + +
+
+ {{ form.csrf_token }} + + +
+
+ Profile +
+
+ This information will be filled in automatically when you use the + Sign in with SimpleLogin button. +
+
+ + {{ form.name(class="form-control", value=current_user.name) }} + {{ render_field_errors(form.name) }} +
+ +
+
Profile picture
+ {{ form.profile_picture(class="form-control-file") }} + {{ render_field_errors(form.profile_picture) }} + {% if current_user.profile_picture_id %} + + {% endif %} +
+ +
+
+
+ + + +
+
+ + {{ change_email_form.csrf_token }} + +
+
+ Account Email +
+
+ This email address is used to log in to SimpleLogin.
+ If you want to change the mailbox that emails are forwarded to, use the + + Mailboxes page + instead. +
+
+ + {{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }} + {{ render_field_errors(change_email_form.email) }} + + {% if pending_email %} +
+ Pending email change: {{ pending_email }} + Resend + confirmation email + Cancel email + change +
+ {% endif %} +
+
- - - -
-
-
Aliases
- -
Change the way random aliases are generated by default.
- - - - - - -
Select the default domain for aliases.
-
- - - -
- -
Select the default suffix generator for aliases.
-
- - - -
+ +
+ -
+ +
+
+
+ Password
- - - -
-
-
Sender Address Format
-
- When your alias receives an email, say from: John Wick <john@wick.com>, - SimpleLogin forwards it to your mailbox.
- - Due to some email constraints, SimpleLogin cannot keep the sender email address - in the original form and needs to transform it to one of the formats below. -
- -
- - - - - -
-
+
+ You will receive an email containing instructions on how to change your password.
- - - -
-
-
Reverse Alias Replacement -
Experimental
-
-
- When replying to a forwarded email, the reverse-alias can be automatically included - in the attached message by your email client. - If this option is enabled, SimpleLogin will try to replace the reverse-alias by your contact - email. -
-
-
- -
- - -
- -
-
+
+ + +
+
+
+ + + +
+
+
Aliases
+ +
Change the way random aliases are generated by default.
+
+ + + +
+ +
Select the default domain for aliases.
+
+ + + +
+ +
Select the default suffix generator for aliases.
+
+ + + +
+ +
+
+ + + +
+
+
Sender Address Format
+
+ When your alias receives an email, say from: John Wick <john@wick.com>, + SimpleLogin forwards it to your mailbox.
+ + Due to some email constraints, SimpleLogin cannot keep the sender email address + in the original form and needs to transform it to one of the formats below.
- - - -
-
-
SMTP for Aliases -
Experimental
-
-
- This will enable SMTP access for aliases. - If this option is enabled, emails can be sent directly from aliases via SMTP without the need for - creation of reverse-alias. -
- Note: After enabling this, you would still need to enable SMTP individually per alias. -
- This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original - address will be displayed in "From:". -
-
-
- -
- - -
- -
-
+ +
+ + + + + +
+
+
+ + + +
+
+
Reverse Alias Replacement +
Experimental
- - - -
-
-
Include sender address in reverse-alias
-
- By default, the reverse-alias is randomly generated and doesn't contain any information about - the sender.
- - Enabling this option will include the sender address in the reverse-alias.
- - This can be useful if you want to set up email filters based on the reverse-alias. - It will also make the reverse-alias more readable. -
-
- -
- - -
- -
-
+
+ When replying to a forwarded email, the reverse-alias can be automatically included + in the attached message by your email client. + If this option is enabled, SimpleLogin will try to replace the reverse-alias by your contact email. +
- - - -
-
-
Always expand alias info
-
- By default, additional alias info is shown after clicking on the "More" button.
- When this option is enabled, alias additional info will always be shown.
-
-
- -
- - -
- -
-
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
SMTP for Aliases +
Experimental
- - - -
-
-
- Include website address in one-click alias creation on browser extension -
-
- If enabled, the website name will be used as alias prefix - when you create an alias via SimpleLogin browser extension via the email input field
- -
-
- -
- - -
- -
-
+
+ This will enable SMTP access for aliases. + If this option is enabled, emails can be sent directly from aliases via SMTP without the need for creation of reverse-alias. +
+ Note: After enabling this, you would still need to enable SMTP individually per alias. +
+ This will also disable receiving via reverse-alias for that particular alias. i.e. Sender's original address will be displayed in "From:". +
- - - - {#
#} - {#
#} - {#
Ignore Loop Emails
#} - {#
#} - {# On some email clients, "Reply All" automatically includes your alias that#} - {# would send the same email to your mailbox.#} - {#
#} - {# You can disable these "loop" emails by enabling this option.#} - {#
#} - {#
#} - {# #} - {#
#} - {# #} - {# #} - {#
#} - {# #} - {#
#} - {#
#} - {#
#} - - - -
-
-
One-click unsubscribe
-
- On email clients that support the - One-click - unsubscribe button, - clicking on it will disable the alias that receives the emails. -
- You can choose to block the sender instead of disabling the alias. -
-
- -
- - -
- -
-
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
Include sender address in reverse-alias
+
+ By default, the reverse-alias is randomly generated and doesn't contain any information about + the sender.
+ + Enabling this option will include the sender address in the reverse-alias.
+ + This can be useful if you want to set up email filters based on the reverse-alias. + It will also make the reverse-alias more readable.
- - -
-
-
Quarantine
-
- When an email sent to your alias is classified as spam or refused by your email provider, - it usually means your alias has been leaked to a spammer.
- In this case SimpleLogin will keep a copy of this email (so it isn't lost) - and notify you so you can take a look at its content and take appropriate actions.
- - The emails are deleted in 7 days. - This is an exceptional case where SimpleLogin stores the email. -
- - See refused emails - -
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
Always expand alias info
+
+ By default, additional alias info is shown after clicking on the "More" button.
+ When this option is enabled, alias additional info will always be shown.
+
+ +
+ + +
+ +
+
+
+ -
-
-
Disabled alias/Blocked contact
-
- When an email is sent to a disabled alias or sent from a blocked contact, you can - decide what response the sender should see.
- Ignore means they will see the message as delivered, but SimpleLogin won't actually forward - it to you. - This is the default option as you can start receiving the emails again - by re-enabling the alias or unblocking a contact.
- Reject means SimpleLogin will tell them that the alias does not exist.
-
-
- - - - - -
-
+ +
+
+
+ Include website address in one-click alias creation on browser extension +
+
+ If enabled, the website name will be used as alias prefix + when you create an alias via SimpleLogin browser extension via the email input field
+ +
+
+ +
+ + +
+ +
+
+
+ + + + {#
#} + {#
#} + {#
Ignore Loop Emails
#} + {#
#} + {# On some email clients, "Reply All" automatically includes your alias that#} + {# would send the same email to your mailbox.#} + {#
#} + {# You can disable these "loop" emails by enabling this option.#} + {#
#} + {#
#} + {# #} + {#
#} + {# #} + {# #} + {#
#} + {# #} + {#
#} + {#
#} + {#
#} + + + +
+
+
One-click unsubscribe
+
+ On email clients that support the + One-click unsubscribe button, + clicking on it will disable the alias that receives the emails. +
+ You can choose to block the sender instead of disabling the alias. +
+
+ +
+ + +
+ +
+
+
+ + +
+
+
Quarantine
+
+ When an email sent to your alias is classified as spam or refused by your email provider, + it usually means your alias has been leaked to a spammer.
+ In this case SimpleLogin will keep a copy of this email (so it isn't lost) + and notify you so you can take a look at its content and take appropriate actions.
+ + The emails are deleted in 7 days. + This is an exceptional case where SimpleLogin stores the email.
+ + See refused emails + +
+
-
-
-
Include original sender in email headers -
-
- SimpleLogin forwards emails to your mailbox from the reverse-alias and not from the original - sender address.
- If this option is enabled, the original sender addresses is stored in the email header X-SimpleLogin-Envelope-From. - You can choose to display this header in your email client.
- As email headers aren't encrypted, your mailbox service can know the sender address via this header. -
-
- - -
- - -
- - -
-
+
+
+
Disabled alias/Blocked contact
+
+ When an email is sent to a disabled alias or sent from a blocked contact, you can decide what response the sender should see.
+ Ignore means they will see the message as delivered, but SimpleLogin won't actually forward it to you. + This is the default option as you can start receiving the emails again + by re-enabling the alias or unblocking a contact.
+ Reject means SimpleLogin will tell them that the alias does not exist.
+
+ + + + + +
+
+
-
-
-
Alias Import
-
- You can import your aliases created on other platforms into SimpleLogin. -
- - Batch Import - +
+
+
Include original sender in email headers +
+
+ SimpleLogin forwards emails to your mailbox from the reverse-alias and not from the original sender address.
+ If this option is enabled, the original sender addresses is stored in the email header X-SimpleLogin-Envelope-From. + You can choose to display this header in your email client.
+ As email headers aren't encrypted, your mailbox service can know the sender address via this header. +
+
+ + +
+ + +
+ + +
+
+
-
+
+
+
Alias Import
+
+ You can import your aliases created on other platforms into SimpleLogin.
+ + Batch Import + -
-
-
Data Export
-
- You can download all aliases you have created on SimpleLogin along with other data. -
- -
-
-
- - -
-
-
-
- - -
-
-
+
+
+
+
+
Data Export
+
+ You can download all aliases you have created on SimpleLogin along with other data. +
-
+
+
+
+ + +
+
+
+
+ + +
+
-
-
-
Account Deletion
-
- If SimpleLogin isn't the right fit for you, you can simply delete your account. -
- Delete account -
+
+
+ +
+
+
Account Deletion
+
+ If SimpleLogin isn't the right fit for you, you can simply delete your account.
+ Delete account +
+
+ {% endblock %} From 6180a807aa03bfc6b02acf0a7ea2a7d3bcee3fcd Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sun, 3 Apr 2022 04:08:11 -0400 Subject: [PATCH 30/36] Re-format again with PyCharm - Changed PyCharm's reformat settings to match the repo --- templates/dashboard/index.html | 155 ++++++++++++++++--------------- templates/dashboard/setting.html | 2 +- 2 files changed, 79 insertions(+), 78 deletions(-) diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index fc40d3965..adacbd40b 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -191,11 +191,11 @@ Pinned Aliases - {% if current_user.enable_SMTP_aliases %} - - {% endif %} + {% if current_user.enable_SMTP_aliases %} + + {% endif %}
{% if current_user.enable_SMTP_aliases %} -
Enable SMTP for this alias @@ -538,12 +538,12 @@ - - + {{ "checked" if alias.enable_SMTP else "" }}> +
- {% endif %} + {% endif %} +
- {% if current_user.enable_SMTP_aliases %} - - {% endif %} + } catch (e) { + toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error"); + // reset to the original value + $(this).prop("checked", oldValue); + } + }); + + {% endif %} {% endblock %} diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 507c84394..72c8e9812 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -351,7 +351,7 @@
- +
SMTP for Aliases From 8a5efb2bb2928bf95be98d42604cde95ca9b63c9 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sun, 3 Apr 2022 04:10:18 -0400 Subject: [PATCH 31/36] Redo Change JS vars to camelcase - Had to do this again after commit revert --- templates/dashboard/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index adacbd40b..e36078cfc 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -632,11 +632,11 @@ let newValue = !oldValue; if(newValue) { - var smtp_generated_password = nanoid(21); + var smtpGeneratedPassword = nanoid(21); var message = `SMTP credentials for alias (This will be visible only once, so please save it):

Username (same as alias): ${alias} -
Password: ${smtp_generated_password}
+
Password: ${smtpGeneratedPassword}
`; } try { @@ -648,7 +648,7 @@ }, body: JSON.stringify({ enable_SMTP: newValue, - smtp_password: smtp_generated_password, + smtp_password: smtpGeneratedPassword, }), }); } @@ -680,7 +680,7 @@ toastr.success(`SMTP Enabled for ${alias}`); } }); - smtp_generated_password = null; // Clear the password. + smtpGeneratedPassword = null; // Clear the password. } else { toastr.info(`SMTP Disabled for ${alias}`); } From b8ec8aad10ac54c9422d464429dcd5300b56f206 Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Thu, 14 Apr 2022 21:51:28 -0400 Subject: [PATCH 32/36] Remove `__table_args__` when `unique=True` in models.py --- app/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models.py b/app/models.py index ff5dd159c..681664b46 100644 --- a/app/models.py +++ b/app/models.py @@ -2577,7 +2577,6 @@ class AliasMailbox(Base, ModelMixin): class SMTPCredentials(Base, ModelMixin, PasswordOracle): __tablename__ = "SMTP_credentials" - __table_args__ = (sa.UniqueConstraint("alias_id", name="uq_alias"),) alias_id = sa.Column( sa.ForeignKey(Alias.id, ondelete="cascade"), unique=True, nullable=False, index=True From 18c0a0204dead8afb27cd47cb157a7dc48503faa Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Thu, 14 Apr 2022 21:59:25 -0400 Subject: [PATCH 33/36] Revert keeping original header in email_handler.py --- email_handler.py | 53 +++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/email_handler.py b/email_handler.py index 103e091e8..96cb0c6f5 100644 --- a/email_handler.py +++ b/email_handler.py @@ -775,11 +775,6 @@ def forward_email_to_mailbox( else: return False, status.E518 - # check if alias has SMTP enabled - is_SMTP_enabled_for_alias = False - if user.enable_SMTP_aliases and alias.SMTP_enabled(): - is_SMTP_enabled_for_alias = True - # sanity check: make sure mailbox is not actually an alias if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email): LOG.w( @@ -932,32 +927,30 @@ def forward_email_to_mailbox( replace_sl_message_id_by_original_message_id(msg) - # Don't Change Headers when SMTP is enabled for alias - if not is_SMTP_enabled_for_alias: - # change the from_header so the email comes from a reverse-alias - # replace the email part in from: header - old_from_header = msg[headers.FROM] - new_from_header = contact.new_addr() - add_or_replace_header(msg, "From", new_from_header) - LOG.d("From header, new:%s, old:%s", new_from_header, old_from_header) - - if reply_to_contact: - reply_to_header = msg[headers.REPLY_TO] - new_reply_to_header = reply_to_contact.new_addr() - add_or_replace_header(msg, "Reply-To", new_reply_to_header) - LOG.d( - "Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header - ) + # change the from_header so the email comes from a reverse-alias + # replace the email part in from: header + old_from_header = msg[headers.FROM] + new_from_header = contact.new_addr() + add_or_replace_header(msg, "From", new_from_header) + LOG.d("From header, new:%s, old:%s", new_from_header, old_from_header) + + if reply_to_contact: + reply_to_header = msg[headers.REPLY_TO] + new_reply_to_header = reply_to_contact.new_addr() + add_or_replace_header(msg, "Reply-To", new_reply_to_header) + LOG.d( + "Reply-To header, new:%s, old:%s", new_reply_to_header, reply_to_header + ) - # replace CC & To emails by reverse-alias for all emails that are not alias - try: - replace_header_when_forward(msg, alias, "Cc") - replace_header_when_forward(msg, alias, "To") - except CannotCreateContactForReverseAlias: - LOG.d("CannotCreateContactForReverseAlias error, delete %s", email_log) - EmailLog.delete(email_log.id) - Session.commit() - raise + # replace CC & To emails by reverse-alias for all emails that are not alias + try: + replace_header_when_forward(msg, alias, "Cc") + replace_header_when_forward(msg, alias, "To") + except CannotCreateContactForReverseAlias: + LOG.d("CannotCreateContactForReverseAlias error, delete %s", email_log) + EmailLog.delete(email_log.id) + Session.commit() + raise # add List-Unsubscribe header if user.one_click_unsubscribe_block_sender: From a2901e27fd85cde990ed2aa328d367a2b7c8b83b Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Thu, 14 Apr 2022 23:17:45 -0400 Subject: [PATCH 34/36] Handle sending to reverse alias in SMTP_handler - Also create a stripped down function `get_or_create_contact_for_SMTP_phase` - Also change the log to debug instead of warning --- SMTP_handler.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/SMTP_handler.py b/SMTP_handler.py index b877ab20b..cebc4d7c5 100644 --- a/SMTP_handler.py +++ b/SMTP_handler.py @@ -17,6 +17,7 @@ from aiosmtpd.smtp import AuthResult, LoginPassword, Envelope from sqlalchemy.orm.exc import ObjectDeletedError +from sqlalchemy.exc import IntegrityError from email.message import Message from email.utils import formataddr, formatdate from init_app import load_pgp_public_keys @@ -54,18 +55,68 @@ send_email, render, get_email_domain_part, - should_ignore_bounce, + is_valid_email, + generate_reply_email, ) from email_handler import ( send_no_reply_response, - get_or_create_contact, handle_spam, prepare_pgp_message, replace_original_message_id, + replace_header_when_reply, ) +def get_or_create_contact_for_SMTP_phase(mail_from: str, alias: Alias) -> Contact: + """ + Create Contact for SMTP phase + """ + contact_email = mail_from + + if not is_valid_email(contact_email): + LOG.w( + "invalid contact email %s.", + contact_email, + ) + # either reuse a contact with empty email or create a new contact with empty email + contact_email = "" + + contact_email = sanitize_email(contact_email, not_lower=True) + + contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) + if not contact: + try: + contact = Contact.create( + user_id=alias.user_id, + alias_id=alias.id, + website_email=contact_email, + mail_from=mail_from, + reply_email=generate_reply_email(contact_email, alias.user) + if is_valid_email(contact_email) + else NOREPLY, + automatic_created=True, + ) + if not contact_email: + LOG.d("Create a contact with invalid email for %s", alias) + contact.invalid_email = True + + LOG.d( + "create contact %s for %s, reverse alias:%s", + contact_email, + alias, + contact.reply_email, + ) + + Session.commit() + except IntegrityError: + LOG.w("Contact %s %s already exist", alias, contact_email) + Session.rollback() + contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) + + return contact + + def handle_SMTP(envelope, msg: Message, rcpt_to: str) -> (bool, str): """ Return whether an email has been delivered and @@ -98,11 +149,21 @@ def handle_SMTP(envelope, msg: Message, rcpt_to: str) -> (bool, str): return False, status.E503 contact = Contact.get_by(website_email=website_email) + + # Check if website email id a reverse alias. + if not contact: + contact = Contact.get_by(reply_email=website_email) + if contact: + LOG.d(f"{website_email} is a reverse-alias. Changing 'To:' to actual website email.") + website_email = contact.website_email + replace_header_when_reply(msg, alias, headers.TO) + replace_header_when_reply(msg, alias, headers.CC) + if not contact: - LOG.w(f"No contact with {website_email} as website email") + LOG.d(f"No contact with {website_email} as website email") try: LOG.d("Create or get contact for website_email:%s", website_email) - contact = get_or_create_contact("", website_email, alias, msg) + contact = get_or_create_contact_for_SMTP_phase(website_email, alias) except ObjectDeletedError: LOG.d("maybe alias was deleted in the meantime") alias = Alias.get_by(email=alias_address) From e0b7fc3131f98eb2818b9c316288625d1158f18a Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Sat, 2 Apr 2022 22:53:09 -0400 Subject: [PATCH 35/36] Revert "Add auto-generated migrations" This reverts commit 25a56ec7fc910b2bb9b6ab819b1cd13a67192f2c. --- .../versions/2022_040221_b26e402d2e0e_.py | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 migrations/versions/2022_040221_b26e402d2e0e_.py diff --git a/migrations/versions/2022_040221_b26e402d2e0e_.py b/migrations/versions/2022_040221_b26e402d2e0e_.py deleted file mode 100644 index 7e77b07d6..000000000 --- a/migrations/versions/2022_040221_b26e402d2e0e_.py +++ /dev/null @@ -1,58 +0,0 @@ -"""empty message - -Revision ID: b26e402d2e0e -Revises: b500363567e3 -Create Date: 2022-04-02 21:47:30.243843 - -""" -import sqlalchemy_utils -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = 'b26e402d2e0e' -down_revision = 'b500363567e3' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('SMTP_credentials', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False), - sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True), - sa.Column('alias_id', sa.Integer(), nullable=False), - sa.Column('password', sa.String(length=128), nullable=False), - sa.ForeignKeyConstraint(['alias_id'], ['alias.id'], ondelete='cascade'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('alias_id', name='uq_alias') - ) - op.create_index(op.f('ix_SMTP_credentials_alias_id'), 'SMTP_credentials', ['alias_id'], unique=True) - op.alter_column('admin_audit_log', 'data', - existing_type=postgresql.JSONB(astext_type=sa.Text()), - nullable=True) - op.drop_index('admin_audit_log_admin_user_id_idx', table_name='admin_audit_log') - op.drop_constraint('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', type_='foreignkey') - op.create_foreign_key(None, 'admin_audit_log', 'users', ['admin_user_id'], ['id']) - op.add_column('alias', sa.Column('enable_SMTP', sa.Boolean(), server_default='0', nullable=False)) - op.add_column('email_log', sa.Column('is_SMTP', sa.Boolean(), nullable=False)) - op.add_column('users', sa.Column('enable_SMTP_aliases', sa.Boolean(), server_default='0', nullable=False)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('users', 'enable_SMTP_aliases') - op.drop_column('email_log', 'is_SMTP') - op.drop_column('alias', 'enable_SMTP') - op.drop_constraint(None, 'admin_audit_log', type_='foreignkey') - op.create_foreign_key('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', 'users', ['admin_user_id'], ['id'], ondelete='CASCADE') - op.create_index('admin_audit_log_admin_user_id_idx', 'admin_audit_log', ['admin_user_id'], unique=False) - op.alter_column('admin_audit_log', 'data', - existing_type=postgresql.JSONB(astext_type=sa.Text()), - nullable=False) - op.drop_index(op.f('ix_SMTP_credentials_alias_id'), table_name='SMTP_credentials') - op.drop_table('SMTP_credentials') - # ### end Alembic commands ### From 32d6a73a93f77fe34d820662f494e3dc8706f4bd Mon Sep 17 00:00:00 2001 From: Sahil Phule Date: Thu, 14 Apr 2022 23:32:28 -0400 Subject: [PATCH 36/36] Add auto-generated migrations --- .../versions/2022_041423_6dff9b5f827a_.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 migrations/versions/2022_041423_6dff9b5f827a_.py diff --git a/migrations/versions/2022_041423_6dff9b5f827a_.py b/migrations/versions/2022_041423_6dff9b5f827a_.py new file mode 100644 index 000000000..00df4d610 --- /dev/null +++ b/migrations/versions/2022_041423_6dff9b5f827a_.py @@ -0,0 +1,57 @@ +"""empty message + +Revision ID: 6dff9b5f827a +Revises: b500363567e3 +Create Date: 2022-04-14 23:31:57.185111 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '6dff9b5f827a' +down_revision = 'b500363567e3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('SMTP_credentials', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False), + sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True), + sa.Column('alias_id', sa.Integer(), nullable=False), + sa.Column('password', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['alias_id'], ['alias.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_SMTP_credentials_alias_id'), 'SMTP_credentials', ['alias_id'], unique=True) + op.alter_column('admin_audit_log', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=True) + op.drop_index('admin_audit_log_admin_user_id_idx', table_name='admin_audit_log') + op.drop_constraint('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', type_='foreignkey') + op.create_foreign_key(None, 'admin_audit_log', 'users', ['admin_user_id'], ['id']) + op.add_column('alias', sa.Column('enable_SMTP', sa.Boolean(), server_default='0', nullable=False)) + op.add_column('email_log', sa.Column('is_SMTP', sa.Boolean(), nullable=False)) + op.add_column('users', sa.Column('enable_SMTP_aliases', sa.Boolean(), server_default='0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'enable_SMTP_aliases') + op.drop_column('email_log', 'is_SMTP') + op.drop_column('alias', 'enable_SMTP') + op.drop_constraint(None, 'admin_audit_log', type_='foreignkey') + op.create_foreign_key('admin_audit_log_admin_user_id_fkey', 'admin_audit_log', 'users', ['admin_user_id'], ['id'], ondelete='CASCADE') + op.create_index('admin_audit_log_admin_user_id_idx', 'admin_audit_log', ['admin_user_id'], unique=False) + op.alter_column('admin_audit_log', 'data', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=False) + op.drop_index(op.f('ix_SMTP_credentials_alias_id'), table_name='SMTP_credentials') + op.drop_table('SMTP_credentials') + # ### end Alembic commands ###