Merge pull request #90 from simple-login/handle-bounced

Handle bounced
This commit is contained in:
Son Nguyen Kim 2020-02-22 22:37:11 +07:00 committed by GitHub
commit 773e24dd9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 34 deletions

View File

@ -146,7 +146,9 @@ def get_alias_activities(alias_id):
activity["to"] = alias_log.alias
activity["from"] = alias_log.website_from or alias_log.website_email
if alias_log.blocked:
if alias_log.bounced:
activity["action"] = "bounced"
elif alias_log.blocked:
activity["action"] = "block"
else:
activity["action"] = "forward"

View File

@ -108,11 +108,21 @@
{% for log in logs %}
<div class="col-12">
<div class="my-2 p-2 card border-light">
<div class="font-weight-bold">{{ log.when | dt }}</div>
<div>
<span class="mr-2">{{ log.website_from or log.website_email }}</span>
<div class="font-weight-bold">{{ log.when | dt }}
{% if log.bounced %} ⚠️ {% endif %}
</div>
<span>
<div>
{% if log.bounced %}
<span class="mr-2">{{ log.website_from or log.website_email }}</span>
<img src="{{ url_for('static', filename='arrows/forward-arrow.svg') }}" class="arrow">
<span class="ml-2">{{ log.alias }}</span>
<img src="{{ url_for('static', filename='arrows/blocked-arrow.svg') }}" class="arrow">
<span class="ml-2">{{ log.mailbox }}</span>
{% else %}
<span class="mr-2">{{ log.website_from or log.website_email }}</span>
<span>
{% if log.is_reply %}
<img src="{{ url_for('static', filename='arrows/reply-arrow.svg') }}" class="arrow">
{% elif log.blocked %}
@ -122,9 +132,8 @@
{% endif %}
</span>
<span class="ml-2">
{{ log.alias }}
</span>
<span class="ml-2">{{ log.alias }}</span>
{% endif %}
</div>
</div>
</div>

View File

@ -15,6 +15,8 @@ class AliasLog:
when: arrow.Arrow
is_reply: bool
blocked: bool
bounced: bool
mailbox: str
def __init__(self, **kwargs):
for k, v in kwargs.items():
@ -61,6 +63,7 @@ def alias_log(alias_id, page_id):
def get_alias_log(gen_email: GenEmail, page_id=0):
logs: [AliasLog] = []
mailbox = gen_email.mailbox_email()
q = (
db.session.query(ForwardEmail, ForwardEmailLog)
@ -79,6 +82,8 @@ def get_alias_log(gen_email: GenEmail, page_id=0):
when=fel.created_at,
is_reply=fel.is_reply,
blocked=fel.blocked,
bounced=fel.bounced,
mailbox=mailbox,
)
logs.append(al)
logs = sorted(logs, key=lambda l: l.when, reverse=True)

View File

@ -1,7 +1,13 @@
import email
import os
from email.message import EmailMessage, Message
from email.mime.application import MIMEApplication
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import make_msgid, formatdate
from smtplib import SMTP
from typing import Optional
import dkim
from jinja2 import Environment, FileSystemLoader
@ -167,7 +173,9 @@ def send_cannot_create_domain_alias(user, alias, domain):
)
def send_email(to_email, subject, plaintext, html):
def send_email(
to_email, subject, plaintext, html, bounced_email: Optional[Message] = None
):
if NOT_SEND_EMAIL:
LOG.d(
"send email with subject %s to %s, plaintext: %s",
@ -179,16 +187,32 @@ def send_email(to_email, subject, plaintext, html):
# host IP, setup via Docker network
smtp = SMTP(POSTFIX_SERVER, 25)
msg = EmailMessage()
if bounced_email:
msg = MIMEMultipart("mixed")
# add email main body
body = MIMEMultipart("alternative")
body.attach(MIMEText(plaintext, "text"))
if html:
body.attach(MIMEText(html, "html"))
msg.attach(body)
# add attachment
rfcmessage = MIMEBase("message", "rfc822")
rfcmessage.attach(bounced_email)
msg.attach(rfcmessage)
else:
msg = MIMEMultipart("alternative")
msg.attach(MIMEText(plaintext, "text"))
if html:
msg.attach(MIMEText(html, "html"))
msg["Subject"] = subject
msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>"
msg["To"] = to_email
msg.set_content(plaintext)
if html:
msg.add_alternative(html, subtype="html")
msg_id_header = make_msgid()
LOG.d("message-id %s", msg_id_header)
msg["Message-ID"] = msg_id_header

View File

@ -513,6 +513,12 @@ class GenEmail(db.Model, ModelMixin):
random_email = generate_email(scheme=scheme, in_hex=in_hex)
return GenEmail.create(user_id=user_id, email=random_email)
def mailbox_email(self):
if self.mailbox_id:
return self.mailbox.email
else:
return self.user.email
def __repr__(self):
return f"<GenEmail {self.id} {self.email}>"
@ -665,6 +671,10 @@ class ForwardEmailLog(db.Model, ModelMixin):
# for ex if alias is disabled, this forwarding is blocked
blocked = db.Column(db.Boolean, nullable=False, default=False)
# can happen when user email service refuses the forwarded email
# usually because the forwarded email is too spammy
bounced = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
class Subscription(db.Model, ModelMixin):
# Come from Paddle

View File

@ -250,11 +250,7 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
return "510 Email not exist"
if gen_email.mailbox_id:
mailbox_email = gen_email.mailbox.email
else:
mailbox_email = gen_email.user.email
mailbox_email = gen_email.mailbox_email()
forward_email = get_or_create_forward_email(msg["From"], gen_email)
forward_log = ForwardEmailLog.create(forward_id=forward_email.id)
@ -336,10 +332,8 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
return "550 alias unknown by SimpleLogin"
gen_email = forward_email.gen_email
if gen_email.mailbox_id:
mailbox_email = gen_email.mailbox.email
else:
mailbox_email = gen_email.user.email
user = gen_email.user
mailbox_email = gen_email.mailbox_email()
# bounce email initiated by Postfix
# can happen in case emails cannot be delivered to user-email
@ -352,17 +346,9 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
gen_email.user,
msg["From"],
)
# send the bounce email payload to admin
msg.replace_header("From", SUPPORT_EMAIL)
msg.replace_header("To", ADMIN_EMAIL)
add_dkim_signature(msg, get_email_domain_part(SUPPORT_EMAIL))
smtp.sendmail(
SUPPORT_EMAIL,
ADMIN_EMAIL,
msg.as_string().encode(),
envelope.mail_options,
envelope.rcpt_options,
handle_bounce(
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
)
return "550 ignored"
@ -460,6 +446,77 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
return "250 Message accepted for delivery"
def handle_bounce(
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
):
ForwardEmailLog.create(forward_id=forward_email.id, bounced=True)
db.session.commit()
nb_bounced = ForwardEmailLog.filter_by(
forward_id=forward_email.id, bounced=True
).count()
disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
# inform user if this is the first bounced email
if nb_bounced == 1:
LOG.d(
"Inform user %s about bounced email sent by %s to alias %s",
user,
forward_email.website_from,
alias,
)
send_email(
mailbox_email,
f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox",
render(
"transactional/bounced-email.txt",
name=user.name,
alias=alias,
website_from=forward_email.website_from,
website_email=forward_email.website_email,
disable_alias_link=disable_alias_link,
),
render(
"transactional/bounced-email.html",
name=user.name,
alias=alias,
website_from=forward_email.website_from,
website_email=forward_email.website_email,
disable_alias_link=disable_alias_link,
),
bounced_email=msg,
)
# disable the alias the second time email is bounced
elif nb_bounced >= 2:
LOG.d(
"Bounce happens again with alias %s from %s. Disable alias now ",
alias,
forward_email.website_from,
)
gen_email.enabled = False
db.session.commit()
send_email(
mailbox_email,
f"Alias {alias} has been disabled due to second undelivered email from {forward_email.website_from}",
render(
"transactional/automatic-disable-alias.txt",
name=user.name,
alias=alias,
website_from=forward_email.website_from,
website_email=forward_email.website_email,
),
render(
"transactional/automatic-disable-alias.html",
name=user.name,
alias=alias,
website_from=forward_email.website_from,
website_email=forward_email.website_email,
),
bounced_email=msg,
)
class MailHandler:
async def handle_DATA(self, server, session, envelope):
LOG.debug(">>> New message <<<")

View File

@ -0,0 +1,29 @@
"""empty message
Revision ID: 3fa3a648c8e7
Revises: 3c9542fc54e9
Create Date: 2020-02-22 12:53:31.293693
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3fa3a648c8e7'
down_revision = '3c9542fc54e9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('forward_email_log', sa.Column('bounced', sa.Boolean(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('forward_email_log', 'bounced')
# ### end Alembic commands ###

View File

@ -441,7 +441,7 @@
</style>
</head>
<body style="width: 100% !important; height: 100%; -webkit-text-size-adjust: none; font-family: Helvetica, Arial, sans-serif; background-color: #F2F4F6; color: #51545E; margin: 0;" bgcolor="#F2F4F6">
<span class="preheader" style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">Thanks for trying out SimpleLogin. Weve pulled together some information and resources to help you get started.</span>
<span class="preheader" style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">{{pre_header}}</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #F2F4F6; margin: 0; padding: 0;" bgcolor="#F2F4F6">
<tr>
<td align="center" style="word-break: break-word; font-family: Helvetica, Arial, sans-serif; font-size: 16px;">

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
{{ render_text("Hi " + name) }}
{{ render_text("There are at least 2 emails sent to your alias <b>" + alias + "</b> from <b>" + website_email +
"</b> that have been <b>refused</b> (or bounced) by your email provider.") }}
{{ render_text("This is usually due to the email being considered as <b>spam</b> by your email provider.
The email is included at the end of this message so you can take a look at its content.") }}
{{ render_text('As security measure, we have <b>disabled</b> the alias ' + alias) }}
{{ render_text('Please let us know if you have any question.') }}
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
{% endblock %}

View File

@ -0,0 +1,13 @@
Hi {{name}}
There are at least 2 emails sent to your alias {{alias}} from {{website_from}} that have been refused (or bounced) by your email provider.
This is usually due to the email being considered as spam by your email provider.
The email is included at the end of this message so you can take a look at its content.
As security measure, we have disabled the alias {{alias}}.
Please let us know if you have any question.
Best,
SimpleLogin team.

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
{{ render_text("Hi " + name) }}
{{ render_text("An email sent to your alias <b>" + alias + "</b> from <b>" + website_email + "</b> was <b>refused</b> (or <em>bounced</em>) by your email provider.") }}
{{ render_text("This is usually due to the email being considered as <b>spam</b> by your email provider. The email is included at the end of this message so you can take a look at its content.") }}
{{ render_text('To avoid spams forwarded by SimpleLogin server, please consider the following options:') }}
{{ render_text('1. If the email is not spam at all, it means your email provider has wrongly classified it as spam. In this case you can <b>create a filter to whitelist</b> it. The filter could be based on the sender, email subject, etc. As how to create the filter differs for each email provider, please check with your email provider on how to whitelist an email. Let us know if you need help to setup the filter by replying to this email.') }}
{{ render_text('2. If this email is indeed spam, it means your alias <b>' + alias + '</b> is now in the hands of a spammer. In this case, you should <b>disable</b> or delete the alias immediately. Or, do nothing and we\'ll <b>automatically</b> disable this alias the second time the email is refused. Don\'t worry, we\'ll send you another email when that happens.') }}
{{ render_button("Disable alias", disable_alias_link) }}
{{ render_text('Please let us know if you have any question.') }}
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
{{ raw_url(disable_alias_link) }}
{% endblock %}

View File

@ -0,0 +1,21 @@
Hi {{name}}
An email sent to your alias {{alias}} from {{website_from}} was refused (or bounced) by your email provider.
This is usually due to the email being considered as spam by your email provider.
The email is included at the end of this message so you can take a look at its content.
To avoid spams forwarded by SimpleLogin server, please consider the following options:
1. If the email is not spam at all, it means your email provider has wrongly classified it as spam. In this case you can create a "filter" to whitelist it. The filter could be based on the sender, email subject, etc. As how to create the filter differs for each email provider, please check with your email provider on how to whitelist an email. Let us know if you need help to setup the filter by replying to this email.
2. If this email is spam, it means your alias {{alias}} is now in the hands of a spammer. In this case, you should disable or delete the alias immediately. Or, do nothing and we'll automatically disable this alias the second time the email is refused. Don't worry, we'll send you another email when that happens. You can disable the alias using this link:
{{disable_alias_link}}
Please let us know if you have any question.
Best,
SimpleLogin team.
---------------------------------------------------------------------
Below if the email that was refused: