216 lines
7.0 KiB
Python
216 lines
7.0 KiB
Python
"""Basic Email Sending Support"""
|
|
|
|
from datetime import datetime
|
|
import os
|
|
import smtplib
|
|
import ssl
|
|
from uuid import UUID
|
|
from typing import Optional, Union
|
|
|
|
from email.message import EmailMessage
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
import aiohttp
|
|
from fastapi import HTTPException
|
|
|
|
from .models import CreateReplicaJob, DeleteReplicaJob, Organization, InvitePending
|
|
from .utils import is_bool, get_origin
|
|
|
|
|
|
# pylint: disable=too-few-public-methods, too-many-instance-attributes
|
|
class EmailSender:
|
|
"""SMTP Email Sender"""
|
|
|
|
sender: str
|
|
password: str
|
|
reply_to: str
|
|
smtp_server: Optional[str]
|
|
smtp_port: int
|
|
smtp_use_tls: bool
|
|
support_email: str
|
|
survey_url: str
|
|
|
|
email_template_endpoint: str
|
|
|
|
log_sent_emails: bool
|
|
|
|
def __init__(self):
|
|
self.sender = os.environ.get("EMAIL_SENDER") or "Browsertrix admin"
|
|
self.password = os.environ.get("EMAIL_PASSWORD") or ""
|
|
self.reply_to = os.environ.get("EMAIL_REPLY_TO") or self.sender
|
|
self.support_email = os.environ.get("EMAIL_SUPPORT") or self.reply_to
|
|
self.survey_url = os.environ.get("USER_SURVEY_URL") or ""
|
|
self.smtp_server = os.environ.get("EMAIL_SMTP_HOST")
|
|
self.smtp_port = int(os.environ.get("EMAIL_SMTP_PORT", 587))
|
|
self.smtp_use_tls = is_bool(os.environ.get("EMAIL_SMTP_USE_TLS"))
|
|
|
|
self.log_sent_emails = is_bool(os.environ.get("LOG_SENT_EMAILS"))
|
|
|
|
email_template_endpoint = os.environ.get("EMAIL_TEMPLATE_ENDPOINT")
|
|
if not email_template_endpoint:
|
|
raise ValueError(
|
|
"Environment variable EMAIL_TEMPLATE_ENDPOINT is required but not set"
|
|
)
|
|
self.email_template_endpoint = email_template_endpoint
|
|
|
|
async def _send_encrypted(self, receiver: str, name: str, **kwargs) -> None:
|
|
"""Send Encrypted SMTP Message using given template name"""
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
self.email_template_endpoint + "/" + name,
|
|
json=kwargs,
|
|
) as resp:
|
|
if resp.status != 200:
|
|
raise HTTPException(
|
|
status_code=resp.status,
|
|
detail=await resp.text(),
|
|
)
|
|
|
|
json = await resp.json()
|
|
|
|
html = json["html"]
|
|
text = json["plainText"]
|
|
subject = json["subject"]
|
|
|
|
if self.log_sent_emails:
|
|
print(text, flush=True)
|
|
|
|
if not self.smtp_server:
|
|
print(
|
|
f'Email: created "{name}" msg for "{receiver}", '
|
|
f"but not sent (no SMTP server set)",
|
|
flush=True,
|
|
)
|
|
return
|
|
|
|
msg: Union[EmailMessage, MIMEMultipart]
|
|
|
|
if html:
|
|
msg = MIMEMultipart("alternative")
|
|
msg.attach(MIMEText(text.strip(), "plain"))
|
|
msg.attach(MIMEText(html.strip(), "html"))
|
|
else:
|
|
msg = EmailMessage()
|
|
msg.set_content(text.strip())
|
|
|
|
msg["Subject"] = subject.strip()
|
|
msg["From"] = self.reply_to
|
|
msg["To"] = receiver
|
|
msg["Reply-To"] = msg["From"]
|
|
|
|
context = ssl.create_default_context()
|
|
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
|
if self.smtp_use_tls:
|
|
server.ehlo()
|
|
server.starttls(context=context)
|
|
server.ehlo()
|
|
if self.password:
|
|
server.login(self.sender, self.password)
|
|
server.send_message(msg)
|
|
# server.sendmail(self.sender, receiver, message)
|
|
# pylint: disable=broad-exception-caught
|
|
except Exception as exc:
|
|
print("Error fetching email template", exc)
|
|
raise exc
|
|
|
|
async def send_user_validation(
|
|
self, receiver_email: str, token: str, headers: Optional[dict] = None
|
|
):
|
|
"""Send email to validate registration email address"""
|
|
|
|
origin = get_origin(headers)
|
|
|
|
await self._send_encrypted(
|
|
receiver_email,
|
|
"verifyEmail",
|
|
origin=origin,
|
|
token=token,
|
|
receiver_email=receiver_email,
|
|
)
|
|
|
|
# pylint: disable=too-many-arguments
|
|
async def send_user_invite(
|
|
self,
|
|
invite: InvitePending,
|
|
token: UUID,
|
|
org_name: str,
|
|
is_new: bool,
|
|
headers: Optional[dict] = None,
|
|
):
|
|
"""Send email to invite new user"""
|
|
|
|
origin = get_origin(headers)
|
|
|
|
receiver_email = invite.email or ""
|
|
|
|
invite_url = (
|
|
f"{origin}/join/{token}?email={receiver_email}"
|
|
if is_new
|
|
else f"{origin}/invite/accept/{token}?email={receiver_email}"
|
|
)
|
|
|
|
await self._send_encrypted(
|
|
receiver_email,
|
|
"invite",
|
|
invite_url=invite_url,
|
|
is_new=is_new,
|
|
sender=invite.inviterEmail if not invite.fromSuperuser else "",
|
|
org_name=org_name,
|
|
support_email=self.support_email,
|
|
)
|
|
|
|
async def send_user_forgot_password(self, receiver_email, token, headers=None):
|
|
"""Send password reset email with token"""
|
|
origin = get_origin(headers)
|
|
|
|
await self._send_encrypted(
|
|
receiver_email,
|
|
"passwordReset",
|
|
origin=origin,
|
|
token=token,
|
|
support_email=self.support_email,
|
|
)
|
|
|
|
async def send_background_job_failed(
|
|
self,
|
|
job: Union[CreateReplicaJob, DeleteReplicaJob],
|
|
finished: datetime,
|
|
receiver_email: str,
|
|
org: Optional[Organization] = None,
|
|
):
|
|
"""Send background job failed email to superuser"""
|
|
await self._send_encrypted(
|
|
receiver_email,
|
|
"failedBgJob",
|
|
job=job,
|
|
org=str(org.id) if org else None,
|
|
finished=finished.isoformat(),
|
|
)
|
|
|
|
async def send_subscription_will_be_canceled(
|
|
self,
|
|
cancel_date: datetime,
|
|
user_name: str,
|
|
receiver_email: str,
|
|
org: Organization,
|
|
headers=None,
|
|
):
|
|
"""Send email indicating subscription is cancelled and all org data will be deleted"""
|
|
|
|
origin = get_origin(headers)
|
|
org_url = f"{origin}/orgs/{org.slug}/"
|
|
|
|
await self._send_encrypted(
|
|
receiver_email,
|
|
"subscriptionCancel",
|
|
org_url=org_url,
|
|
user_name=user_name,
|
|
org_name=org.name,
|
|
cancel_date=cancel_date.isoformat(),
|
|
support_email=self.support_email,
|
|
survey_url=self.survey_url,
|
|
)
|