mirror of
https://github.com/simple-login/app.git
synced 2024-11-02 20:01:01 +01:00
159 lines
5.4 KiB
Python
159 lines
5.4 KiB
Python
import uuid
|
|
from io import BytesIO
|
|
from typing import Optional, Tuple
|
|
|
|
from aiosmtpd.handlers import Message
|
|
from aiosmtpd.smtp import Envelope
|
|
|
|
from app import s3
|
|
from app.config import (
|
|
DMARC_CHECK_ENABLED,
|
|
ALERT_QUARANTINE_DMARC,
|
|
ALERT_DMARC_FAILED_REPLY_PHASE,
|
|
)
|
|
from app.email import headers, status
|
|
from app.email_utils import (
|
|
get_header_unicode,
|
|
send_email_with_rate_control,
|
|
render,
|
|
add_or_replace_header,
|
|
add_header,
|
|
)
|
|
from app.handler.spamd_result import SpamdResult, Phase, DmarcCheckResult
|
|
from app.log import LOG
|
|
from app.message_utils import message_to_bytes
|
|
from app.models import Alias, Contact, Notification, EmailLog, RefusedEmail
|
|
|
|
|
|
def apply_dmarc_policy_for_forward_phase(
|
|
alias: Alias, contact: Contact, envelope: Envelope, msg: Message
|
|
) -> Tuple[Message, Optional[str]]:
|
|
spam_result = SpamdResult.extract_from_headers(msg, Phase.forward)
|
|
if not DMARC_CHECK_ENABLED or not spam_result:
|
|
return msg, None
|
|
|
|
from_header = get_header_unicode(msg[headers.FROM])
|
|
|
|
if spam_result.dmarc == DmarcCheckResult.soft_fail:
|
|
LOG.w(
|
|
f"dmarc forward: soft_fail from contact {contact.email} to alias {alias.email}."
|
|
f"mail_from:{envelope.mail_from}, from_header: {from_header}"
|
|
)
|
|
changed_msg = add_header(
|
|
msg,
|
|
f"""This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
|
More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
|
""",
|
|
f"""
|
|
<p style="color:red">
|
|
This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
|
More info on <a href="https://simplelogin.io/docs/getting-started/anti-phishing/">anti-phishing measure</a>
|
|
</p>
|
|
""",
|
|
)
|
|
return changed_msg, None
|
|
|
|
if spam_result.dmarc in (
|
|
DmarcCheckResult.quarantine,
|
|
DmarcCheckResult.reject,
|
|
):
|
|
LOG.w(
|
|
f"dmarc forward: put email from {contact} to {alias} to quarantine. {spam_result.event_data()}, "
|
|
f"mail_from:{envelope.mail_from}, from_header: {msg[headers.FROM]}"
|
|
)
|
|
email_log = quarantine_dmarc_failed_forward_email(alias, contact, envelope, msg)
|
|
Notification.create(
|
|
user_id=alias.user_id,
|
|
title=f"{alias.email} has a new mail in quarantine",
|
|
message=Notification.render(
|
|
"notification/message-quarantine.html", alias=alias
|
|
),
|
|
commit=True,
|
|
)
|
|
user = alias.user
|
|
send_email_with_rate_control(
|
|
user,
|
|
ALERT_QUARANTINE_DMARC,
|
|
user.email,
|
|
f"An email sent to {alias.email} has been quarantined",
|
|
render(
|
|
"transactional/message-quarantine-dmarc.txt.jinja2",
|
|
from_header=from_header,
|
|
alias=alias,
|
|
refused_email_url=email_log.get_dashboard_url(),
|
|
),
|
|
render(
|
|
"transactional/message-quarantine-dmarc.html",
|
|
from_header=from_header,
|
|
alias=alias,
|
|
refused_email_url=email_log.get_dashboard_url(),
|
|
),
|
|
max_nb_alert=10,
|
|
ignore_smtp_error=True,
|
|
)
|
|
return msg, status.E215
|
|
|
|
return msg, None
|
|
|
|
|
|
def quarantine_dmarc_failed_forward_email(alias, contact, envelope, msg) -> EmailLog:
|
|
add_or_replace_header(msg, headers.SL_DIRECTION, "Forward")
|
|
msg[headers.SL_ENVELOPE_FROM] = envelope.mail_from
|
|
random_name = str(uuid.uuid4())
|
|
s3_report_path = f"refused-emails/full-{random_name}.eml"
|
|
s3.upload_email_from_bytesio(
|
|
s3_report_path, BytesIO(message_to_bytes(msg)), f"full-{random_name}"
|
|
)
|
|
refused_email = RefusedEmail.create(
|
|
full_report_path=s3_report_path, user_id=alias.user_id, flush=True
|
|
)
|
|
return EmailLog.create(
|
|
user_id=alias.user_id,
|
|
mailbox_id=alias.mailbox_id,
|
|
contact_id=contact.id,
|
|
alias_id=alias.id,
|
|
message_id=str(msg[headers.MESSAGE_ID]),
|
|
refused_email_id=refused_email.id,
|
|
is_spam=True,
|
|
blocked=True,
|
|
commit=True,
|
|
)
|
|
|
|
|
|
def apply_dmarc_policy_for_reply_phase(
|
|
alias_from: Alias, contact_recipient: Contact, envelope: Envelope, msg: Message
|
|
) -> Optional[str]:
|
|
spam_result = SpamdResult.extract_from_headers(msg, Phase.reply)
|
|
if not DMARC_CHECK_ENABLED or not spam_result:
|
|
return None
|
|
|
|
if spam_result.dmarc not in (
|
|
DmarcCheckResult.quarantine,
|
|
DmarcCheckResult.reject,
|
|
DmarcCheckResult.soft_fail,
|
|
):
|
|
return None
|
|
LOG.w(
|
|
f"dmarc reply: Put email from {alias_from.email} to {contact_recipient} into quarantine. {spam_result.event_data()}, "
|
|
f"mail_from:{envelope.mail_from}, from_header: {msg[headers.FROM]}"
|
|
)
|
|
send_email_with_rate_control(
|
|
alias_from.user,
|
|
ALERT_DMARC_FAILED_REPLY_PHASE,
|
|
alias_from.user.email,
|
|
f"Attempt to send an email to your contact {contact_recipient.email} from {envelope.mail_from}",
|
|
render(
|
|
"transactional/spoof-reply.txt.jinja2",
|
|
contact=contact_recipient,
|
|
alias=alias_from,
|
|
sender=envelope.mail_from,
|
|
),
|
|
render(
|
|
"transactional/spoof-reply.html",
|
|
contact=contact_recipient,
|
|
alias=alias_from,
|
|
sender=envelope.mail_from,
|
|
),
|
|
)
|
|
return status.E215
|