mirror of
https://github.com/simple-login/app.git
synced 2024-10-01 14:11:31 +02:00
Merge pull request #110 from simple-login/refused-email
Handle refused email
This commit is contained in:
commit
93765335ed
@ -17,4 +17,5 @@ from .views import (
|
|||||||
mailbox,
|
mailbox,
|
||||||
deleted_alias,
|
deleted_alias,
|
||||||
mailbox_detail,
|
mailbox_detail,
|
||||||
|
refused_email,
|
||||||
)
|
)
|
||||||
|
53
app/dashboard/templates/dashboard/refused_email.html
Normal file
53
app/dashboard/templates/dashboard/refused_email.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% extends 'default.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Refused Emails
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% set active_page = "setting" %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
<div style="max-width: 60em; margin: auto">
|
||||||
|
<h1 class="h3 mb-5"> Refused Emails </h1>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
This page shows all emails that have been <b>refused</b> (or bounced) by your mailbox. <br>
|
||||||
|
Usually this is because your mailbox thinks these emails are <b>spams</b>. <br>
|
||||||
|
- If a refused email is indeed spam, this means the alias is now in the hands of a spammer,
|
||||||
|
in this case you should <b>disable</b> this alias. <br>
|
||||||
|
- Otherwise, you should create a <b>filter</b> to avoid your email provider from blocking these emails. <br>
|
||||||
|
<a href="mailto:hi@simplelogin.io">Contact us↗</a> if you need any help.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if fels|length == 0 %}
|
||||||
|
<div class="my-4 p-4 card">
|
||||||
|
You don't have any refused email.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for fel in fels %}
|
||||||
|
{% set refused_email = fel.refused_email %}
|
||||||
|
{% set forward = fel.forward %}
|
||||||
|
|
||||||
|
<div class="card p-4 shadow-sm {% if fel.id == highlight_fel_id %} highlight-row {% endif %}">
|
||||||
|
From: {{ forward.website_from or forward.website_email }} <br>
|
||||||
|
To: {{ forward.gen_email.email }} <br>
|
||||||
|
<div class="small-text">
|
||||||
|
Sent {{ refused_email.created_at | dt }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if refused_email.deleted %}
|
||||||
|
Email deleted {{ refused_email.delete_at | dt }}
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ refused_email.get_url() }}" download
|
||||||
|
class="mt-4">Download →</a>
|
||||||
|
<div class="small-text">This will download a ".eml" file that you can open in your favorite email client</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -183,6 +183,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title">Refused Emails
|
||||||
|
<div class="small-text mt-1 mb-3" style="max-width: 40rem">
|
||||||
|
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. <br>
|
||||||
|
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. <br>
|
||||||
|
|
||||||
|
The emails are deleted in 7 days.
|
||||||
|
This is an exceptional case where SimpleLogin stores the email.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('dashboard.refused_email_route') }}" class="btn btn-outline-primary">
|
||||||
|
See refused emails
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">Export Data
|
<div class="card-title">Export Data
|
||||||
|
30
app/dashboard/views/refused_email.py
Normal file
30
app/dashboard/views/refused_email.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from flask import render_template, request
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from app.dashboard.base import dashboard_bp
|
||||||
|
from app.models import ForwardEmailLog
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/refused_email", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def refused_email_route():
|
||||||
|
# Highlight a refused email
|
||||||
|
highlight_fel_id = request.args.get("highlight_fel_id")
|
||||||
|
if highlight_fel_id:
|
||||||
|
highlight_fel_id = int(highlight_fel_id)
|
||||||
|
|
||||||
|
fels: [ForwardEmailLog] = ForwardEmailLog.query.filter(
|
||||||
|
ForwardEmailLog.refused_email_id != None
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# make sure the highlighted fel is the first fel
|
||||||
|
highlight_index = None
|
||||||
|
for ix, fel in enumerate(fels):
|
||||||
|
if fel.id == highlight_fel_id:
|
||||||
|
highlight_index = ix
|
||||||
|
break
|
||||||
|
|
||||||
|
if highlight_index:
|
||||||
|
fels.insert(0, fels.pop(highlight_index))
|
||||||
|
|
||||||
|
return render_template("dashboard/refused_email.html", **locals())
|
@ -307,6 +307,15 @@ def delete_header(msg: Message, header: str):
|
|||||||
del msg._headers[i]
|
del msg._headers[i]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_all_headers_except(msg: Message, headers: [str]):
|
||||||
|
headers = [h.lower() for h in headers]
|
||||||
|
|
||||||
|
for i in reversed(range(len(msg._headers))):
|
||||||
|
header_name = msg._headers[i][0].lower()
|
||||||
|
if header_name not in headers:
|
||||||
|
del msg._headers[i]
|
||||||
|
|
||||||
|
|
||||||
def email_belongs_to_alias_domains(email: str) -> bool:
|
def email_belongs_to_alias_domains(email: str) -> bool:
|
||||||
"""return True if an email ends with one of the alias domains provided by SimpleLogin"""
|
"""return True if an email ends with one of the alias domains provided by SimpleLogin"""
|
||||||
for domain in ALIAS_DOMAINS:
|
for domain in ALIAS_DOMAINS:
|
||||||
@ -363,3 +372,18 @@ def mailbox_already_used(email: str, user) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_orig_message_from_bounce(msg: Message) -> Message:
|
||||||
|
"""parse the original email from Bounce"""
|
||||||
|
i = 0
|
||||||
|
for part in msg.walk():
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# the original message is the 4th part
|
||||||
|
# 1st part is the root part, multipart/report
|
||||||
|
# 2nd is text/plain, Postfix log
|
||||||
|
# ...
|
||||||
|
# 7th is original message
|
||||||
|
if i == 7:
|
||||||
|
return part
|
||||||
|
@ -344,6 +344,10 @@ def _expiration_5m():
|
|||||||
return arrow.now().shift(minutes=5)
|
return arrow.now().shift(minutes=5)
|
||||||
|
|
||||||
|
|
||||||
|
def _expiration_7d():
|
||||||
|
return arrow.now().shift(days=7)
|
||||||
|
|
||||||
|
|
||||||
class ActivationCode(db.Model, ModelMixin):
|
class ActivationCode(db.Model, ModelMixin):
|
||||||
"""For activate user account"""
|
"""For activate user account"""
|
||||||
|
|
||||||
@ -758,6 +762,14 @@ class ForwardEmailLog(db.Model, ModelMixin):
|
|||||||
# usually because the forwarded email is too spammy
|
# usually because the forwarded email is too spammy
|
||||||
bounced = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
|
bounced = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
|
||||||
|
|
||||||
|
# Point to the email that has been refused
|
||||||
|
refused_email_id = db.Column(
|
||||||
|
db.ForeignKey("refused_email.id", ondelete="SET NULL"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
refused_email = db.relationship("RefusedEmail")
|
||||||
|
forward = db.relationship(ForwardEmail)
|
||||||
|
|
||||||
|
|
||||||
class Subscription(db.Model, ModelMixin):
|
class Subscription(db.Model, ModelMixin):
|
||||||
# Come from Paddle
|
# Come from Paddle
|
||||||
@ -958,3 +970,27 @@ class AccountActivation(db.Model, ModelMixin):
|
|||||||
CheckConstraint(tries >= 0, name="account_activation_tries_positive"),
|
CheckConstraint(tries >= 0, name="account_activation_tries_positive"),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RefusedEmail(db.Model, ModelMixin):
|
||||||
|
"""Store emails that have been refused, i.e. bounced or classified as spams"""
|
||||||
|
|
||||||
|
# Store the full report, including logs from Sending & Receiving MTA
|
||||||
|
full_report_path = db.Column(db.String(128), unique=True, nullable=False)
|
||||||
|
|
||||||
|
# The original email, to display to user
|
||||||
|
path = db.Column(db.String(128), unique=True, nullable=False)
|
||||||
|
|
||||||
|
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
|
||||||
|
|
||||||
|
# the email content will be deleted at this date
|
||||||
|
delete_at = db.Column(ArrowType, nullable=False, default=_expiration_7d)
|
||||||
|
|
||||||
|
# toggle this when email content (stored at full_report_path & path are deleted)
|
||||||
|
deleted = db.Column(db.Boolean, nullable=False, default=False, server_default="0")
|
||||||
|
|
||||||
|
def get_url(self, expires_in=3600):
|
||||||
|
return s3.get_url(self.path, expires_in)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Refused Email {self.id} {self.path} {self.delete_at}>"
|
||||||
|
31
app/s3.py
31
app/s3.py
@ -34,7 +34,28 @@ def upload_from_bytesio(key: str, bs: BytesIO, content_type="string"):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
_session.resource("s3").Bucket(BUCKET).put_object(
|
_session.resource("s3").Bucket(BUCKET).put_object(
|
||||||
Key=key, Body=bs, ContentType=content_type
|
Key=key, Body=bs, ContentType=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_email_from_bytesio(path: str, bs: BytesIO, filename):
|
||||||
|
bs.seek(0)
|
||||||
|
|
||||||
|
if LOCAL_FILE_UPLOAD:
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, path)
|
||||||
|
file_dir = os.path.dirname(file_path)
|
||||||
|
os.makedirs(file_dir, exist_ok=True)
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(bs.read())
|
||||||
|
|
||||||
|
else:
|
||||||
|
_session.resource("s3").Bucket(BUCKET).put_object(
|
||||||
|
Key=path,
|
||||||
|
Body=bs,
|
||||||
|
# Support saving a remote file using Http header
|
||||||
|
# Also supports Safari. More info at
|
||||||
|
# https://github.com/eligrey/FileSaver.js/wiki/Saving-a-remote-file#using-http-header
|
||||||
|
ContentDisposition=f'attachment; filename="{filename}.eml";',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -53,3 +74,11 @@ def get_url(key: str, expires_in=3600) -> str:
|
|||||||
ClientMethod="get_object",
|
ClientMethod="get_object",
|
||||||
Params={"Bucket": BUCKET, "Key": key},
|
Params={"Bucket": BUCKET, "Key": key},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(path: str):
|
||||||
|
if LOCAL_FILE_UPLOAD:
|
||||||
|
os.remove(os.path.join(UPLOAD_DIR, path))
|
||||||
|
else:
|
||||||
|
o = _session.resource("s3").Bucket(BUCKET).Object(path)
|
||||||
|
o.delete()
|
||||||
|
21
cron.py
21
cron.py
@ -2,6 +2,7 @@ import argparse
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
|
from app import s3
|
||||||
from app.config import IGNORED_EMAILS, ADMIN_EMAIL
|
from app.config import IGNORED_EMAILS, ADMIN_EMAIL
|
||||||
from app.email_utils import send_email, send_trial_end_soon_email, render
|
from app.email_utils import send_email, send_trial_end_soon_email, render
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
@ -15,6 +16,7 @@ from app.models import (
|
|||||||
CustomDomain,
|
CustomDomain,
|
||||||
Client,
|
Client,
|
||||||
ManualSubscription,
|
ManualSubscription,
|
||||||
|
RefusedEmail,
|
||||||
)
|
)
|
||||||
from server import create_app
|
from server import create_app
|
||||||
|
|
||||||
@ -30,6 +32,21 @@ def notify_trial_end():
|
|||||||
send_trial_end_soon_email(user)
|
send_trial_end_soon_email(user)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_refused_emails():
|
||||||
|
for refused_email in RefusedEmail.query.filter(RefusedEmail.deleted == False).all():
|
||||||
|
if arrow.now().shift(days=1) > refused_email.delete_at >= arrow.now():
|
||||||
|
LOG.d("Delete refused email %s", refused_email)
|
||||||
|
s3.delete(refused_email.path)
|
||||||
|
s3.delete(refused_email.full_report_path)
|
||||||
|
|
||||||
|
# do not set path and full_report_path to null
|
||||||
|
# so we can check later that the files are indeed deleted
|
||||||
|
refused_email.deleted = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
LOG.d("Finish delete_refused_emails")
|
||||||
|
|
||||||
|
|
||||||
def notify_premium_end():
|
def notify_premium_end():
|
||||||
"""sent to user who has canceled their subscription and who has their subscription ending soon"""
|
"""sent to user who has canceled their subscription and who has their subscription ending soon"""
|
||||||
for sub in Subscription.query.filter(Subscription.cancelled == True).all():
|
for sub in Subscription.query.filter(Subscription.cancelled == True).all():
|
||||||
@ -172,6 +189,7 @@ if __name__ == "__main__":
|
|||||||
"notify_trial_end",
|
"notify_trial_end",
|
||||||
"notify_manual_subscription_end",
|
"notify_manual_subscription_end",
|
||||||
"notify_premium_end",
|
"notify_premium_end",
|
||||||
|
"delete_refused_emails",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -191,3 +209,6 @@ if __name__ == "__main__":
|
|||||||
elif args.job == "notify_premium_end":
|
elif args.job == "notify_premium_end":
|
||||||
LOG.d("Notify users with premium ending soon")
|
LOG.d("Notify users with premium ending soon")
|
||||||
notify_premium_end()
|
notify_premium_end()
|
||||||
|
elif args.job == "delete_refused_emails":
|
||||||
|
LOG.d("Deleted refused emails")
|
||||||
|
delete_refused_emails()
|
||||||
|
@ -22,3 +22,9 @@ jobs:
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
schedule: "0 10 * * *"
|
schedule: "0 10 * * *"
|
||||||
captureStderr: true
|
captureStderr: true
|
||||||
|
|
||||||
|
- name: SimpleLogin Delete Refused Emails
|
||||||
|
command: python /code/cron.py -j delete_refused_emails
|
||||||
|
shell: /bin/bash
|
||||||
|
schedule: "0 11 * * *"
|
||||||
|
captureStderr: true
|
||||||
|
@ -30,6 +30,7 @@ It should contain the following info:
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import uuid
|
||||||
import time
|
import time
|
||||||
from email import encoders
|
from email import encoders
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
@ -37,21 +38,19 @@ from email.mime.application import MIMEApplication
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.parser import Parser
|
from email.parser import Parser
|
||||||
from email.policy import SMTPUTF8
|
from email.policy import SMTPUTF8
|
||||||
|
from io import BytesIO
|
||||||
from smtplib import SMTP
|
from smtplib import SMTP
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
import gnupg
|
|
||||||
|
|
||||||
|
from app import pgp_utils, s3
|
||||||
from app.config import (
|
from app.config import (
|
||||||
EMAIL_DOMAIN,
|
EMAIL_DOMAIN,
|
||||||
POSTFIX_SERVER,
|
POSTFIX_SERVER,
|
||||||
URL,
|
URL,
|
||||||
ALIAS_DOMAINS,
|
ALIAS_DOMAINS,
|
||||||
ADMIN_EMAIL,
|
|
||||||
SUPPORT_EMAIL,
|
|
||||||
POSTFIX_SUBMISSION_TLS,
|
POSTFIX_SUBMISSION_TLS,
|
||||||
GNUPGHOME,
|
|
||||||
)
|
)
|
||||||
from app.email_utils import (
|
from app.email_utils import (
|
||||||
get_email_name,
|
get_email_name,
|
||||||
@ -65,6 +64,8 @@ from app.email_utils import (
|
|||||||
send_cannot_create_domain_alias,
|
send_cannot_create_domain_alias,
|
||||||
email_belongs_to_alias_domains,
|
email_belongs_to_alias_domains,
|
||||||
render,
|
render,
|
||||||
|
get_orig_message_from_bounce,
|
||||||
|
delete_all_headers_except,
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
@ -76,10 +77,10 @@ from app.models import (
|
|||||||
Directory,
|
Directory,
|
||||||
User,
|
User,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
|
RefusedEmail,
|
||||||
)
|
)
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
from server import create_app
|
from server import create_app
|
||||||
from app import pgp_utils
|
|
||||||
|
|
||||||
|
|
||||||
# fix the database connection leak issue
|
# fix the database connection leak issue
|
||||||
@ -270,6 +271,17 @@ def prepare_pgp_message(orig_msg: Message, pgp_fingerprint: str):
|
|||||||
if header_name != "Content-Type".lower():
|
if header_name != "Content-Type".lower():
|
||||||
msg[header_name] = orig_msg._headers[i][1]
|
msg[header_name] = orig_msg._headers[i][1]
|
||||||
|
|
||||||
|
# Delete unnecessary headers in orig_msg except to save space
|
||||||
|
delete_all_headers_except(
|
||||||
|
orig_msg,
|
||||||
|
[
|
||||||
|
"MIME-Version",
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Disposition",
|
||||||
|
"Content-Transfer-Encoding",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
first = MIMEApplication(
|
first = MIMEApplication(
|
||||||
_subtype="pgp-encrypted", _encoder=encoders.encode_7or8bit, _data=""
|
_subtype="pgp-encrypted", _encoder=encoders.encode_7or8bit, _data=""
|
||||||
)
|
)
|
||||||
@ -406,7 +418,12 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
|||||||
# in this case Postfix will try to send a bounce report to original sender, which is
|
# in this case Postfix will try to send a bounce report to original sender, which is
|
||||||
# the "reply email"
|
# the "reply email"
|
||||||
if envelope.mail_from == "<>":
|
if envelope.mail_from == "<>":
|
||||||
LOG.error("Bounce when sending to alias %s, user %s", alias, gen_email.user)
|
LOG.error(
|
||||||
|
"Bounce when sending to alias %s from %s, user %s",
|
||||||
|
alias,
|
||||||
|
forward_email.website_from,
|
||||||
|
gen_email.user,
|
||||||
|
)
|
||||||
|
|
||||||
handle_bounce(
|
handle_bounce(
|
||||||
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
||||||
@ -513,7 +530,9 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
|||||||
def handle_bounce(
|
def handle_bounce(
|
||||||
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
||||||
):
|
):
|
||||||
ForwardEmailLog.create(forward_id=forward_email.id, bounced=True)
|
fel: ForwardEmailLog = ForwardEmailLog.create(
|
||||||
|
forward_id=forward_email.id, bounced=True
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
nb_bounced = ForwardEmailLog.filter_by(
|
nb_bounced = ForwardEmailLog.filter_by(
|
||||||
@ -521,6 +540,31 @@ def handle_bounce(
|
|||||||
).count()
|
).count()
|
||||||
disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
|
disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
|
||||||
|
|
||||||
|
# Store the bounced email
|
||||||
|
orig_msg = get_orig_message_from_bounce(msg)
|
||||||
|
# generate a name for the email
|
||||||
|
random_name = str(uuid.uuid4())
|
||||||
|
|
||||||
|
full_report_path = f"refused-emails/full-{random_name}.eml"
|
||||||
|
s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()), random_name)
|
||||||
|
|
||||||
|
file_path = f"refused-emails/{random_name}.eml"
|
||||||
|
s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()), random_name)
|
||||||
|
|
||||||
|
refused_email = RefusedEmail.create(
|
||||||
|
path=file_path, full_report_path=full_report_path, user_id=user.id
|
||||||
|
)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
fel.refused_email_id = refused_email.id
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
LOG.d("Create refused email %s", refused_email)
|
||||||
|
|
||||||
|
refused_email_url = (
|
||||||
|
URL + f"/dashboard/refused_email?highlight_fel_id=" + str(fel.id)
|
||||||
|
)
|
||||||
|
|
||||||
# inform user if this is the first bounced email
|
# inform user if this is the first bounced email
|
||||||
if nb_bounced == 1:
|
if nb_bounced == 1:
|
||||||
LOG.d(
|
LOG.d(
|
||||||
@ -530,7 +574,8 @@ def handle_bounce(
|
|||||||
alias,
|
alias,
|
||||||
)
|
)
|
||||||
send_email(
|
send_email(
|
||||||
mailbox_email,
|
# use user mail here as only user is authenticated to see the refused email
|
||||||
|
user.email,
|
||||||
f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox",
|
f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox",
|
||||||
render(
|
render(
|
||||||
"transactional/bounced-email.txt",
|
"transactional/bounced-email.txt",
|
||||||
@ -539,6 +584,8 @@ def handle_bounce(
|
|||||||
website_from=forward_email.website_from,
|
website_from=forward_email.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=forward_email.website_email,
|
||||||
disable_alias_link=disable_alias_link,
|
disable_alias_link=disable_alias_link,
|
||||||
|
refused_email_url=refused_email_url,
|
||||||
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
render(
|
render(
|
||||||
"transactional/bounced-email.html",
|
"transactional/bounced-email.html",
|
||||||
@ -547,8 +594,11 @@ def handle_bounce(
|
|||||||
website_from=forward_email.website_from,
|
website_from=forward_email.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=forward_email.website_email,
|
||||||
disable_alias_link=disable_alias_link,
|
disable_alias_link=disable_alias_link,
|
||||||
|
refused_email_url=refused_email_url,
|
||||||
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
bounced_email=msg,
|
# cannot include bounce email as it can contain spammy text
|
||||||
|
# bounced_email=msg,
|
||||||
)
|
)
|
||||||
# disable the alias the second time email is bounced
|
# disable the alias the second time email is bounced
|
||||||
elif nb_bounced >= 2:
|
elif nb_bounced >= 2:
|
||||||
@ -561,7 +611,8 @@ def handle_bounce(
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
mailbox_email,
|
# use user mail here as only user is authenticated to see the refused email
|
||||||
|
user.email,
|
||||||
f"Alias {alias} has been disabled due to second undelivered email from {forward_email.website_from}",
|
f"Alias {alias} has been disabled due to second undelivered email from {forward_email.website_from}",
|
||||||
render(
|
render(
|
||||||
"transactional/automatic-disable-alias.txt",
|
"transactional/automatic-disable-alias.txt",
|
||||||
@ -569,6 +620,8 @@ def handle_bounce(
|
|||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=forward_email.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=forward_email.website_email,
|
||||||
|
refused_email_url=refused_email_url,
|
||||||
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
render(
|
render(
|
||||||
"transactional/automatic-disable-alias.html",
|
"transactional/automatic-disable-alias.html",
|
||||||
@ -576,8 +629,11 @@ def handle_bounce(
|
|||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=forward_email.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=forward_email.website_email,
|
||||||
|
refused_email_url=refused_email_url,
|
||||||
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
bounced_email=msg,
|
# cannot include bounce email as it can contain spammy text
|
||||||
|
# bounced_email=msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ def load_pgp_public_keys(app):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
LOG.d("Finish load_pgp_public_keys")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
45
migrations/versions/2020_031416_11a35b448f83_.py
Normal file
45
migrations/versions/2020_031416_11a35b448f83_.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 11a35b448f83
|
||||||
|
Revises: 628a5438295c
|
||||||
|
Create Date: 2020-03-14 16:35:13.564982
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '11a35b448f83'
|
||||||
|
down_revision = '628a5438295c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('refused_email',
|
||||||
|
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('full_report_path', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('path', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('delete_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('full_report_path'),
|
||||||
|
sa.UniqueConstraint('path')
|
||||||
|
)
|
||||||
|
op.add_column('forward_email_log', sa.Column('refused_email_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'forward_email_log', 'refused_email', ['refused_email_id'], ['id'], ondelete='SET NULL')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'forward_email_log', type_='foreignkey')
|
||||||
|
op.drop_column('forward_email_log', 'refused_email_id')
|
||||||
|
op.drop_table('refused_email')
|
||||||
|
# ### end Alembic commands ###
|
29
migrations/versions/2020_031510_9081f1a90939_.py
Normal file
29
migrations/versions/2020_031510_9081f1a90939_.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 9081f1a90939
|
||||||
|
Revises: 11a35b448f83
|
||||||
|
Create Date: 2020-03-15 10:51:17.341046
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9081f1a90939'
|
||||||
|
down_revision = '11a35b448f83'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('refused_email', sa.Column('deleted', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('refused_email', 'deleted')
|
||||||
|
# ### end Alembic commands ###
|
@ -151,7 +151,7 @@ def fake_data():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
api_key = ApiKey.create(user_id=user.id, name="Chrome")
|
api_key = ApiKey.create(user_id=user.id, name="Chrome")
|
||||||
api_key.code = "codeCH"
|
api_key.code = "code"
|
||||||
|
|
||||||
api_key = ApiKey.create(user_id=user.id, name="Firefox")
|
api_key = ApiKey.create(user_id=user.id, name="Firefox")
|
||||||
api_key.code = "codeFF"
|
api_key.code = "codeFF"
|
||||||
|
@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ render_text("Hi " + name) }}
|
{{ render_text("Hi " + name) }}
|
||||||
{{ render_text("There are at least 2 emails sent to your alias <b>" + alias + "</b> from <b>" + website_email +
|
{{ 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 mailbox " + mailbox_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("This is usually due to the email being considered as <b>spam</b> by your email provider.") }}
|
||||||
|
|
||||||
|
{{ render_button("View the refused email", refused_email_url) }}
|
||||||
|
|
||||||
|
{{ 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('Please let us know if you have any question.') }}
|
||||||
|
|
||||||
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
|
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
|
||||||
|
{{ raw_url(refused_email_url) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
Hi {{name}}
|
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.
|
There are at least 2 emails sent to your alias {{alias}} from {{website_from}} that have been refused (or bounced) by your mailbox {{mailbox_email}}.
|
||||||
|
|
||||||
This is usually due to the email being considered as spam 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.
|
You can view this email here:
|
||||||
|
{{ refused_email_url }}
|
||||||
|
|
||||||
|
|
||||||
As security measure, we have disabled the alias {{alias}}.
|
As security measure, we have disabled the alias {{alias}}.
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ render_text("Hi " + name) }}
|
{{ 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("An email sent to your alias <b>" + alias + "</b> from <b>" + website_email + "</b> was <b>refused</b> (or bounced) by your mailbox " + mailbox_email + ".") }}
|
||||||
|
|
||||||
{{ 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('This is usually due to the email being considered as <b>spam</b> by your email provider.') }}
|
||||||
|
|
||||||
|
{{ render_button("View the refused email", refused_email_url) }}
|
||||||
|
|
||||||
{{ render_text('To avoid spams forwarded by SimpleLogin server, please consider the following options:') }}
|
{{ render_text('To avoid spams forwarded by SimpleLogin server, please consider the following options:') }}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
Hi {{name}}
|
Hi {{name}}
|
||||||
|
|
||||||
An email sent to your alias {{alias}} from {{website_from}} was refused (or bounced) by your email provider.
|
An email sent to your alias {{alias}} from {{website_from}} was refused (or bounced) by your mailbox {{mailbox_email}}.
|
||||||
|
|
||||||
This is usually due to the email being considered as spam 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.
|
You can view this email here:
|
||||||
|
{{ refused_email_url }}
|
||||||
|
|
||||||
To avoid spams forwarded by SimpleLogin server, please consider the following options:
|
To avoid spams forwarded by SimpleLogin server, please consider the following options:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user