commit
773e24dd9a
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <<<")
|
||||
|
|
|
@ -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 ###
|
|
@ -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. We’ve 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;">
|
||||
|
|
|
@ -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 %}
|
|
@ -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.
|
|
@ -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 %}
|
|
@ -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:
|
Loading…
Reference in New Issue