def verify_mailbox_change(user, mailbox, new_email): s = Signer(MAILBOX_SECRET) mailbox_id_signed = s.sign(str(mailbox.id)).decode() verification_url = ( URL + "/dashboard/mailbox/confirm_change" + f"?mailbox_id={mailbox_id_signed}" ) send_email( new_email, f"Confirm mailbox change on SimpleLogin", render( "transactional/verify-mailbox-change.txt", user=user, link=verification_url, mailbox_email=mailbox.email, mailbox_new_email=new_email, ), render( "transactional/verify-mailbox-change.html", user=user, link=verification_url, mailbox_email=mailbox.email, mailbox_new_email=new_email, ), )
def onboarding_mailbox(user): send_email( user.email, f"Do you know SimpleLogin can manage several email addresses?", render("com/onboarding/mailbox.txt", user=user), render("com/onboarding/mailbox.html", user=user), )
def onboarding_send_from_alias(user): send_email( user.email, f"Do you know you can send emails to anyone from your alias?", render("com/onboarding/send-from-alias.txt", user=user), render("com/onboarding/send-from-alias.html", user=user), )
def onboarding_pgp(user): send_email( user.email, f"Do you know you can encrypt your emails so only you can read them?", render("com/onboarding/pgp.txt", user=user), render("com/onboarding/pgp.html", user=user), )
def handle_email_sent_to_ourself(alias, mailbox, msg: Message, user): # store the refused email random_name = str(uuid.uuid4()) full_report_path = f"refused-emails/cycle-{random_name}.eml" s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()), random_name) refused_email = RefusedEmail.create(path=None, full_report_path=full_report_path, user_id=alias.user_id) db.session.commit() LOG.d("Create refused email %s", refused_email) # link available for 6 days as it gets deleted in 7 days refused_email_url = refused_email.get_url(expires_in=518400) send_email_with_rate_control( user, ALERT_SEND_EMAIL_CYCLE, mailbox.email, f"Warning: email sent from {mailbox.email} to {alias.email} forms a cycle", render( "transactional/cycle-email.txt", name=user.name or "", alias=alias, mailbox=mailbox, refused_email_url=refused_email_url, ), render( "transactional/cycle-email.html", name=user.name or "", alias=alias, mailbox=mailbox, refused_email_url=refused_email_url, ), )
def notify_premium_end(): """sent to user who has canceled their subscription and who has their subscription ending soon""" for sub in Subscription.query.filter_by(cancelled=True).all(): if ( arrow.now().shift(days=3).date() > sub.next_bill_date >= arrow.now().shift(days=2).date() ): user = sub.user LOG.d(f"Send subscription ending soon email to user {user}") send_email( user.email, f"Your subscription will end soon", render( "transactional/subscription-end.txt", user=user, next_bill_date=sub.next_bill_date.strftime("%Y-%m-%d"), ), render( "transactional/subscription-end.html", user=user, next_bill_date=sub.next_bill_date.strftime("%Y-%m-%d"), ), )
def notify_manual_sub_end(): for manual_sub in ManualSubscription.query.all(): need_reminder = False if arrow.now().shift(days=14) > manual_sub.end_at > arrow.now().shift( days=13): need_reminder = True elif arrow.now().shift(days=4) > manual_sub.end_at > arrow.now().shift( days=3): need_reminder = True if need_reminder: user = manual_sub.user LOG.debug("Remind user %s that their manual sub is ending soon", user) send_email( user.email, f"Your subscription will end soon {user.name}", render( "transactional/manual-subscription-end.txt", name=user.name, user=user, manual_sub=manual_sub, ), render( "transactional/manual-subscription-end.html", name=user.name, user=user, manual_sub=manual_sub, ), ) extend_subscription_url = URL + "/dashboard/coinbase_checkout" for coinbase_subscription in CoinbaseSubscription.query.all(): need_reminder = False if (arrow.now().shift(days=14) > coinbase_subscription.end_at > arrow.now().shift(days=13)): need_reminder = True elif (arrow.now().shift(days=4) > coinbase_subscription.end_at > arrow.now().shift(days=3)): need_reminder = True if need_reminder: user = coinbase_subscription.user LOG.debug( "Remind user %s that their coinbase subscription is ending soon", user) send_email( user.email, "Your SimpleLogin subscription will end soon", render( "transactional/coinbase/reminder-subscription.txt", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), render( "transactional/coinbase/reminder-subscription.html", coinbase_subscription=coinbase_subscription, extend_subscription_url=extend_subscription_url, ), )
def notify_manual_sub_end(): for manual_sub in ManualSubscription.query.all(): need_reminder = False if arrow.now().shift(days=14) > manual_sub.end_at > arrow.now().shift(days=13): need_reminder = True elif arrow.now().shift(days=4) > manual_sub.end_at > arrow.now().shift(days=3): need_reminder = True if need_reminder: user = manual_sub.user LOG.debug("Remind user %s that their manual sub is ending soon", user) send_email( user.email, f"Your trial will end soon {user.name}", render( "transactional/manual-subscription-end.txt", name=user.name, user=user, manual_sub=manual_sub, ), render( "transactional/manual-subscription-end.html", name=user.name, user=user, manual_sub=manual_sub, ), )
def check_mailbox_valid_domain(): """detect if there's mailbox that's using an invalid domain""" mailbox_ids = (Session.query(Mailbox.id).filter( Mailbox.verified.is_(True), Mailbox.disabled.is_(False)).all()) mailbox_ids = [e[0] for e in mailbox_ids] # iterate over id instead of mailbox directly # as a mailbox can be deleted during the sleep time for mailbox_id in mailbox_ids: mailbox = Mailbox.get(mailbox_id) # a mailbox has been deleted if not mailbox: continue if email_can_be_used_as_mailbox(mailbox.email): LOG.d("Mailbox %s valid", mailbox) mailbox.nb_failed_checks = 0 else: mailbox.nb_failed_checks += 1 nb_email_log = nb_email_log_for_mailbox(mailbox) LOG.w( "issue with mailbox %s domain. #alias %s, nb email log %s", mailbox, mailbox.nb_alias(), nb_email_log, ) # send a warning if mailbox.nb_failed_checks == 5: if mailbox.user.email != mailbox.email: send_email( mailbox.user.email, f"Mailbox {mailbox.email} is disabled", render( "transactional/disable-mailbox-warning.txt.jinja2", mailbox=mailbox, ), render( "transactional/disable-mailbox-warning.html", mailbox=mailbox, ), retries=3, ) # alert if too much fail and nb_email_log > 100 if mailbox.nb_failed_checks > 10 and nb_email_log > 100: mailbox.disabled = True if mailbox.user.email != mailbox.email: send_email( mailbox.user.email, f"Mailbox {mailbox.email} is disabled", render("transactional/disable-mailbox.txt.jinja2", mailbox=mailbox), render("transactional/disable-mailbox.html", mailbox=mailbox), retries=3, ) Session.commit()
def onboarding_browser_extension(user): send_email( user.email, f"Do you know you can create aliases without leaving the browser?", render("com/onboarding/browser-extension.txt", user=user), render("com/onboarding/browser-extension.html", user=user), )
def send_safari_extension_newsletter(): for user in User.query.all(): send_email( user.email, "Quickly create alias with our Safari extension", render("com/safari-extension.txt", user=user), render("com/safari-extension.html", user=user), )
def handle_coinbase_event(event) -> bool: user_id = int(event["data"]["metadata"]["user_id"]) code = event["data"]["code"] user = User.get(user_id) if not user: LOG.exception("User not found %s", user_id) return False coinbase_subscription: CoinbaseSubscription = CoinbaseSubscription.get_by( user_id=user_id) if not coinbase_subscription: LOG.d("Create a coinbase subscription for %s", user) coinbase_subscription = CoinbaseSubscription.create( user_id=user_id, end_at=arrow.now().shift(years=1), code=code, commit=True) send_email( user.email, "Your SimpleLogin account has been upgraded", render( "transactional/coinbase/new-subscription.txt", coinbase_subscription=coinbase_subscription, ), render( "transactional/coinbase/new-subscription.html", coinbase_subscription=coinbase_subscription, ), ) else: if coinbase_subscription.code != code: LOG.d("Update code from %s to %s", coinbase_subscription.code, code) coinbase_subscription.code = code if coinbase_subscription.is_active(): coinbase_subscription.end_at = coinbase_subscription.end_at.shift( years=1) else: # already expired subscription coinbase_subscription.end_at = arrow.now().shift(years=1) db.session.commit() send_email( user.email, "Your SimpleLogin account has been extended", render( "transactional/coinbase/extend-subscription.txt", coinbase_subscription=coinbase_subscription, ), render( "transactional/coinbase/extend-subscription.html", coinbase_subscription=coinbase_subscription, ), ) return True
def onboarding_mailbox(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"Do you know you can have multiple mailboxes on SimpleLogin?", render("com/onboarding/mailbox.txt", user=user, to_email=to_email), render("com/onboarding/mailbox.html", user=user, to_email=to_email), )
def onboarding_1(user): if not user.notification: LOG.d("User %s disable notification setting", user) return send_email( user.email, f"Do you know you can send emails to anyone from your alias?", render("com/onboarding-1.txt", user=user), render("com/onboarding-1.html", user=user), )
def onboarding_browser_extension(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"Have you tried SimpleLogin Chrome/Firefox extensions and Android/iOS apps?", render("com/onboarding/browser-extension.txt", user=user, to_email=to_email), render("com/onboarding/browser-extension.html", user=user, to_email=to_email), )
def onboarding_send_from_alias(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"SimpleLogin Tip: Send emails from your alias", render("com/onboarding/send-from-alias.txt", user=user, to_email=to_email), render("com/onboarding/send-from-alias.html", user=user, to_email=to_email), )
def onboarding_send_from_alias(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"Do you know you can send emails from your alias?", render("com/onboarding/send-from-alias.txt", user=user, to_email=to_email), render("com/onboarding/send-from-alias.html", user=user, to_email=to_email), )
def onboarding_pgp(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"SimpleLogin Tip: Secure your emails with PGP", render("com/onboarding/pgp.txt", user=user, to_email=to_email), render("com/onboarding/pgp.html", user=user, to_email=to_email), )
def onboarding_mailbox(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"SimpleLogin Tip: Multiple mailboxes", render("com/onboarding/mailbox.txt", user=user, to_email=to_email), render("com/onboarding/mailbox.html", user=user, to_email=to_email), )
def onboarding_pgp(user): to_email = user.get_communication_email() if not to_email: return send_email( to_email, f"Do you know you can encrypt your emails so only you can read them?", render("com/onboarding/pgp.txt", user=user, to_email=to_email), render("com/onboarding/pgp.html", user=user, to_email=to_email), )
def handle_unsubscribe(envelope: Envelope): msg = email.message_from_bytes(envelope.original_content) # format: alias_id: subject = msg["Subject"] try: # subject has the format {alias.id}= if subject.endswith("="): alias_id = int(subject[:-1]) # some email providers might strip off the = suffix else: alias_id = int(subject) alias = Alias.get(alias_id) except Exception: LOG.warning("Cannot parse alias from subject %s", msg["Subject"]) return "550 SL E8" if not alias: LOG.warning("No such alias %s", alias_id) return "550 SL E9" # This sender cannot unsubscribe mail_from = envelope.mail_from.lower().strip() mailbox = Mailbox.get_by(user_id=alias.user_id, email=mail_from) if not mailbox or mailbox not in alias.mailboxes: LOG.d("%s cannot disable alias %s", envelope.mail_from, alias) return "550 SL E10" # Sender is owner of this alias alias.enabled = False db.session.commit() user = alias.user enable_alias_url = URL + f"/dashboard/?highlight_alias_id={alias.id}" for mailbox in alias.mailboxes: send_email( mailbox.email, f"Alias {alias.email} has been disabled successfully", render( "transactional/unsubscribe-disable-alias.txt", user=user, alias=alias.email, enable_alias_url=enable_alias_url, ), render( "transactional/unsubscribe-disable-alias.html", user=user, alias=alias.email, enable_alias_url=enable_alias_url, ), ) return "250 Unsubscribe request accepted"
def transfer(alias, new_user, new_mailboxes: [Mailbox]): # cannot transfer alias which is used for receiving newsletter if User.get_by(newsletter_alias_id=alias.id): raise Exception( "Cannot transfer alias that's used to receive newsletter") # update user_id db.session.query(Contact).filter(Contact.alias_id == alias.id).update( {"user_id": new_user.id}) db.session.query(AliasUsedOn).filter( AliasUsedOn.alias_id == alias.id).update({"user_id": new_user.id}) db.session.query(ClientUser).filter( ClientUser.alias_id == alias.id).update({"user_id": new_user.id}) # remove existing mailboxes from the alias db.session.query(AliasMailbox).filter( AliasMailbox.alias_id == alias.id).delete() # set mailboxes alias.mailbox_id = new_mailboxes.pop().id for mb in new_mailboxes: AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id) # alias has never been transferred before if not alias.original_owner_id: alias.original_owner_id = alias.user_id # inform previous owner old_user = alias.user send_email( old_user.email, f"Alias {alias.email} has been received", render( "transactional/alias-transferred.txt", alias=alias, ), render( "transactional/alias-transferred.html", alias=alias, ), ) # now the alias belongs to the new user alias.user_id = new_user.id # set some fields back to default alias.disable_pgp = False alias.pinned = False db.session.commit()
def onboarding_browser_extension(user): to_email, unsubscribe_link, via_email = user.get_communication_email() if not to_email: return send_email( to_email, "SimpleLogin Tip: Chrome/Firefox/Safari extensions and Android/iOS apps", render("com/onboarding/browser-extension.txt", user=user, to_email=to_email), render("com/onboarding/browser-extension.html", user=user, to_email=to_email), unsubscribe_link, via_email, )
def send_mailbox_newsletter(): for user in User.query.order_by(User.id).all(): if user.notification and user.activated: try: LOG.d("Send newsletter to %s", user) send_email( user.email, "Introducing Mailbox - our most requested feature", render("com/newsletter/mailbox.txt", user=user), render("com/newsletter/mailbox.html", user=user), ) except Exception: LOG.warning("Cannot send to user %s", user)
def check_custom_domain(): LOG.d("Check verified domain for DNS issues") for custom_domain in CustomDomain.query.filter_by( verified=True ): # type: CustomDomain mx_domains = get_mx_domains(custom_domain.domain) if sorted(mx_domains) != sorted(EMAIL_SERVERS_WITH_PRIORITY): user = custom_domain.user LOG.warning( "The MX record is not correctly set for %s %s %s", custom_domain, user, mx_domains, ) custom_domain.nb_failed_checks += 1 # send alert if fail for 5 consecutive days if custom_domain.nb_failed_checks > 5: domain_dns_url = f"{URL}/dashboard/domains/{custom_domain.id}/dns" LOG.warning( "Alert domain MX check fails %s about %s", user, custom_domain ) send_email_with_rate_control( user, AlERT_WRONG_MX_RECORD_CUSTOM_DOMAIN, user.email, f"Please update {custom_domain.domain} DNS on SimpleLogin", render( "transactional/custom-domain-dns-issue.txt", custom_domain=custom_domain, domain_dns_url=domain_dns_url, ), render( "transactional/custom-domain-dns-issue.html", custom_domain=custom_domain, domain_dns_url=domain_dns_url, ), max_nb_alert=1, nb_day=30, ) # reset checks custom_domain.nb_failed_checks = 0 else: # reset checks custom_domain.nb_failed_checks = 0 db.session.commit()
def send_pgp_newsletter(): for user in User.query.order_by(User.id).all(): if user.notification and user.activated: try: LOG.d("Send PGP newsletter to %s", user) send_email( user.email, "Introducing PGP - encrypt your emails so only you can read them", render("com/newsletter/pgp.txt", user=user), render("com/newsletter/pgp.html", user=user), ) sleep(1) except Exception: LOG.warning("Cannot send to user %s", user)
def onboarding_pgp(user): to_email, unsubscribe_link, via_email = user.get_communication_email() if not to_email: return send_email( to_email, "SimpleLogin Tip: Secure your emails with PGP", render("com/onboarding/pgp.txt", user=user, to_email=to_email), render("com/onboarding/pgp.html", user=user, to_email=to_email), unsubscribe_link, via_email, retries=3, ignore_smtp_error=True, )
def onboarding_mailbox(user): to_email, unsubscribe_link, via_email = user.get_communication_email() if not to_email: return send_email( to_email, "SimpleLogin Tip: Multiple mailboxes", render("com/onboarding/mailbox.txt", user=user, to_email=to_email), render("com/onboarding/mailbox.html", user=user, to_email=to_email), unsubscribe_link, via_email, retries=3, ignore_smtp_error=True, )
def handle_unknown_mailbox(envelope, msg, reply_email: str, user: User, alias: Alias): LOG.warning( f"Reply email can only be used by mailbox. " f"Actual mail_from: %s. msg from header: %s, reverse-alias %s, %s %s", envelope.mail_from, msg["From"], reply_email, alias, user, ) send_email_with_rate_control( user, ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, user.email, f"Reply from your alias {alias.email} only works from your mailbox", render( "transactional/reply-must-use-personal-email.txt", name=user.name, alias=alias, sender=envelope.mail_from, ), render( "transactional/reply-must-use-personal-email.html", name=user.name, alias=alias, sender=envelope.mail_from, ), ) # Notify sender that they cannot send emails to this address send_email_with_rate_control( user, ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, envelope.mail_from, f"Your email ({envelope.mail_from}) is not allowed to send emails to {reply_email}", render( "transactional/send-from-alias-from-unknown-sender.txt", sender=envelope.mail_from, reply_email=reply_email, ), render( "transactional/send-from-alias-from-unknown-sender.html", sender=envelope.mail_from, reply_email=reply_email, ), )
def auth_register(): """ User signs up - will need to activate their account with an activation code. Input: email password Output: 200: user needs to confirm their account """ data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 email = sanitize_email(data.get("email")) password = data.get("password") if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 if not email_can_be_used_as_mailbox(email) or personal_email_already_used( email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 if not password or len(password) < 8: return jsonify(error="password too short"), 400 if len(password) > 100: return jsonify(error="password too long"), 400 LOG.d("create user %s", email) user = User.create(email=email, name="", password=password) Session.flush() # create activation code code = "".join([str(random.randint(0, 9)) for _ in range(6)]) AccountActivation.create(user_id=user.id, code=code) Session.commit() send_email( email, "Just one more step to join SimpleLogin", render("transactional/code-activation.txt.jinja2", code=code), render("transactional/code-activation.html", code=code), ) return jsonify(msg="User needs to confirm their account"), 200