Take into account spamassassin spam report
This commit is contained in:
parent
33a80236d3
commit
9500cc6cee
|
@ -365,6 +365,21 @@ def get_orig_message_from_bounce(msg: Message) -> Message:
|
|||
return part
|
||||
|
||||
|
||||
def get_orig_message_from_spamassassin_report(msg: Message) -> Message:
|
||||
"""parse the original email from Spamassassin report"""
|
||||
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, SpamAssassin part
|
||||
# 3rd is the original message in message/rfc822 content type
|
||||
# 4th is original message
|
||||
if i == 4:
|
||||
return part
|
||||
|
||||
|
||||
def new_addr(old_addr, new_email, user: User) -> str:
|
||||
"""replace First Last <first@example.com> by
|
||||
first@example.com by SimpleLogin <new_email>
|
||||
|
@ -399,3 +414,21 @@ def get_addrs_from_header(msg: Message, header) -> [str]:
|
|||
|
||||
# do not return empty string
|
||||
return [r for r in ret if r]
|
||||
|
||||
|
||||
def get_spam_info(msg: Message) -> (bool, str):
|
||||
"""parse SpamAssassin header to detect whether a message is classified as spam.
|
||||
Return (is spam, spam status detail)
|
||||
The header format is
|
||||
```X-Spam-Status: No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
||||
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
||||
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2```
|
||||
"""
|
||||
spamassassin_status = msg["X-Spam-Status"]
|
||||
if not spamassassin_status:
|
||||
return False, ""
|
||||
|
||||
# yes or no
|
||||
spamassassin_answer = spamassassin_status[: spamassassin_status.find(",")]
|
||||
|
||||
return spamassassin_answer.lower() == "yes", spamassassin_status
|
||||
|
|
|
@ -68,6 +68,8 @@ from app.email_utils import (
|
|||
delete_all_headers_except,
|
||||
new_addr,
|
||||
get_addrs_from_header,
|
||||
get_spam_info,
|
||||
get_orig_message_from_spamassassin_report,
|
||||
)
|
||||
from app.extensions import db
|
||||
from app.log import LOG
|
||||
|
@ -424,12 +426,25 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, s
|
|||
LOG.d("Forward from %s to %s, nothing to do", envelope.mail_from, mailbox_email)
|
||||
return False, "550 SL ignored"
|
||||
|
||||
contact = get_or_create_contact(msg["From"], alias)
|
||||
|
||||
spam_check = True
|
||||
|
||||
# create PGP email if needed
|
||||
if mailbox.pgp_finger_print and user.is_premium():
|
||||
LOG.d("Encrypt message using mailbox %s", mailbox)
|
||||
msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)
|
||||
|
||||
contact = get_or_create_contact(msg["From"], alias)
|
||||
# no need to spam check for encrypted message
|
||||
spam_check = False
|
||||
|
||||
if spam_check:
|
||||
is_spam, spam_status = get_spam_info(msg)
|
||||
if is_spam:
|
||||
LOG.warning("Email detected as spam. Alias: %s, from: %s", alias, contact)
|
||||
handle_spam(contact, alias, msg, user, mailbox_email, spam_status)
|
||||
return False, "550 SL ignored"
|
||||
|
||||
forward_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id)
|
||||
|
||||
if alias.enabled:
|
||||
|
@ -750,6 +765,81 @@ def handle_bounce(
|
|||
)
|
||||
|
||||
|
||||
def handle_spam(
|
||||
contact: Contact,
|
||||
alias: Alias,
|
||||
msg: Message,
|
||||
user: User,
|
||||
mailbox_email: str,
|
||||
spam_status: str,
|
||||
):
|
||||
email_log: EmailLog = EmailLog.create(
|
||||
contact_id=contact.id,
|
||||
user_id=contact.user_id,
|
||||
is_spam=True,
|
||||
spam_status=spam_status,
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# Store the report & original email
|
||||
orig_msg = get_orig_message_from_spamassassin_report(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 = None
|
||||
if orig_msg:
|
||||
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()
|
||||
|
||||
email_log.refused_email_id = refused_email.id
|
||||
db.session.commit()
|
||||
|
||||
LOG.d("Create spam email %s", refused_email)
|
||||
|
||||
refused_email_url = URL + f"/dashboard/refused_email?highlight_id=" + str(email_log.id)
|
||||
disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"
|
||||
|
||||
# inform user
|
||||
LOG.d(
|
||||
"Inform user %s about spam email sent by %s to alias %s",
|
||||
user,
|
||||
contact.website_from,
|
||||
alias.email,
|
||||
)
|
||||
send_email(
|
||||
mailbox_email,
|
||||
f"Email from {contact.website_from} to {alias.email} is detected as spam",
|
||||
render(
|
||||
"transactional/spam-email.txt",
|
||||
name=user.name,
|
||||
alias=alias,
|
||||
website_from=contact.website_from,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
refused_email_url=refused_email_url,
|
||||
),
|
||||
render(
|
||||
"transactional/spam-email.html",
|
||||
name=user.name,
|
||||
alias=alias,
|
||||
website_from=contact.website_from,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
refused_email_url=refused_email_url,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle_unsubscribe(envelope):
|
||||
message_data = envelope.content.decode("utf8", errors="replace")
|
||||
msg = Parser(policy=SMTPUTF8).parsestr(message_data)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ render_text("Hi " + name) }}
|
||||
{{ render_text("An email sent to your alias <b>" + alias.email + "</b> from <b>" + website_email + "</b> is detected as spam by our Spam Detection Engine (SpamAssassin).") }}
|
||||
|
||||
{{ render_text('In most of the cases, the email will be refused by your email provider.') }}
|
||||
|
||||
{{ render_button("View the email", refused_email_url) }}
|
||||
|
||||
{{ render_text('The email is automatically deleted in 7 days.') }}
|
||||
|
||||
{{ render_text('Your alias <b>' + alias.email + '</b> is probably in the hands of a spammer now. In this case, you should disable or delete the alias immediately.') }}
|
||||
|
||||
{{ render_button("Disable alias", disable_alias_link) }}
|
||||
|
||||
{{ render_text('Please let us know if you have any question by replying to this email.') }}
|
||||
|
||||
{{ render_text('Thanks, <br />SimpleLogin Team.') }}
|
||||
{{ raw_url(disable_alias_link) }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
Hi {{name}}
|
||||
|
||||
An email sent to your alias {{alias.email}} from {{website_from}} is detected as spam by our Spam Detection Engine (SpamAssassin).
|
||||
|
||||
In most of the cases, the email will be refused by your email provider.
|
||||
|
||||
You can view this email here:
|
||||
{{ refused_email_url }}
|
||||
|
||||
The email is automatically deleted in 7 days.
|
||||
|
||||
Your alias {{alias}} is probably in the hands of a spammer now. In this case, you should disable or delete the alias immediately.
|
||||
|
||||
{{disable_alias_link}}
|
||||
|
||||
Please let us know if you have any question by replying to this email.
|
||||
|
||||
Best,
|
||||
SimpleLogin team.
|
Loading…
Reference in New Issue