2020-07-12 18:53:04 +00:00
|
|
|
# Handles everything related to emails
|
2020-04-19 21:35:51 +00:00
|
|
|
import datetime
|
|
|
|
import pytz
|
|
|
|
import requests
|
|
|
|
import smtplib
|
|
|
|
import ssl
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
from pathlib import Path
|
|
|
|
from dateutil import parser as date_parser
|
2020-07-18 17:11:00 +00:00
|
|
|
from jinja2 import Template
|
2020-06-16 19:07:47 +00:00
|
|
|
from jellyfin_accounts import config
|
|
|
|
from jellyfin_accounts import email_log as log
|
2020-04-19 21:35:51 +00:00
|
|
|
|
|
|
|
|
2020-07-17 15:08:36 +00:00
|
|
|
def format_datetime(dt):
|
|
|
|
result = dt.strftime(config["email"]["date_format"])
|
|
|
|
if config.getboolean("email", "use_24h"):
|
|
|
|
result += f' {dt.strftime("%H:%M")}'
|
|
|
|
else:
|
|
|
|
result += f' {dt.strftime("%I:%M %p")}'
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
class Email:
|
2020-04-19 21:35:51 +00:00
|
|
|
def __init__(self, address):
|
|
|
|
self.address = address
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"{self.address}: Creating email")
|
2020-04-19 21:35:51 +00:00
|
|
|
self.content = {}
|
2020-06-21 19:29:53 +00:00
|
|
|
self.from_address = config["email"]["address"]
|
|
|
|
self.from_name = config["email"]["from"]
|
|
|
|
log.debug(
|
|
|
|
(
|
|
|
|
f"{self.address}: Sending from {self.from_address} "
|
|
|
|
+ f"({self.from_name})"
|
|
|
|
)
|
|
|
|
)
|
2020-07-18 17:11:00 +00:00
|
|
|
# sp = Path(config["invite_emails"]["email_
|
|
|
|
# template_loader = FileSystemLoader(searchpath=sp)
|
|
|
|
# template_loader = PackageLoader("jellyfin_accounts", "data")
|
|
|
|
# self.template_env = Environment(loader=template_loader)
|
2020-04-19 21:35:51 +00:00
|
|
|
|
2020-07-18 17:11:00 +00:00
|
|
|
def pretty_time(self, expiry, tzaware=False):
|
|
|
|
if tzaware:
|
|
|
|
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
|
|
|
|
else:
|
|
|
|
current_time = datetime.datetime.now()
|
2020-06-21 19:29:53 +00:00
|
|
|
date = expiry.strftime(config["email"]["date_format"])
|
|
|
|
if config.getboolean("email", "use_24h"):
|
|
|
|
log.debug(f"{self.address}: Using 24h time")
|
|
|
|
time = expiry.strftime("%H:%M")
|
2020-04-19 21:35:51 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"{self.address}: Using 12h time")
|
|
|
|
time = expiry.strftime("%-I:%M %p")
|
2020-04-19 21:35:51 +00:00
|
|
|
expiry_delta = (expiry - current_time).seconds
|
2020-06-21 19:29:53 +00:00
|
|
|
expires_in = {
|
|
|
|
"hours": expiry_delta // 3600,
|
|
|
|
"minutes": (expiry_delta // 60) % 60,
|
|
|
|
}
|
|
|
|
if expires_in["hours"] == 0:
|
2020-04-19 21:35:51 +00:00
|
|
|
expires_in = f'{str(expires_in["minutes"])}m'
|
|
|
|
else:
|
2020-07-12 18:53:04 +00:00
|
|
|
expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m'
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"{self.address}: Expires in {expires_in}")
|
|
|
|
return {"date": date, "time": time, "expires_in": expires_in}
|
2020-04-19 21:35:51 +00:00
|
|
|
|
|
|
|
def construct_invite(self, invite):
|
2020-06-21 19:29:53 +00:00
|
|
|
self.subject = config["invite_emails"]["subject"]
|
|
|
|
log.debug(f"{self.address}: Using subject {self.subject}")
|
|
|
|
log.debug(f"{self.address}: Constructing email content")
|
|
|
|
expiry = invite["expiry"]
|
2020-04-19 21:35:51 +00:00
|
|
|
expiry.replace(tzinfo=None)
|
|
|
|
pretty = self.pretty_time(expiry)
|
2020-06-21 19:29:53 +00:00
|
|
|
email_message = config["email"]["message"]
|
|
|
|
invite_link = config["invite_emails"]["url_base"]
|
|
|
|
invite_link += "/" + invite["code"]
|
|
|
|
for key in ["text", "html"]:
|
2020-07-18 17:11:00 +00:00
|
|
|
fpath = Path(config["invite_emails"]["email_" + key])
|
|
|
|
with open(fpath, 'r') as f:
|
|
|
|
template = Template(f.read())
|
2020-06-21 19:29:53 +00:00
|
|
|
c = template.render(
|
|
|
|
expiry_date=pretty["date"],
|
|
|
|
expiry_time=pretty["time"],
|
|
|
|
expires_in=pretty["expires_in"],
|
|
|
|
invite_link=invite_link,
|
|
|
|
message=email_message,
|
|
|
|
)
|
2020-04-19 21:35:51 +00:00
|
|
|
self.content[key] = c
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"{self.address}: {key} constructed")
|
2020-04-19 21:35:51 +00:00
|
|
|
|
2020-07-17 15:08:36 +00:00
|
|
|
def construct_expiry(self, invite):
|
|
|
|
self.subject = "Notice: Invite expired"
|
|
|
|
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
|
|
|
expiry = format_datetime(invite["expiry"])
|
|
|
|
for key in ["text", "html"]:
|
2020-07-18 17:11:00 +00:00
|
|
|
fpath = Path(config["notifications"]["expiry_" + key])
|
|
|
|
with open(fpath, 'r') as f:
|
|
|
|
template = Template(f.read())
|
2020-07-17 15:08:36 +00:00
|
|
|
c = template.render(code=invite["code"], expiry=expiry)
|
|
|
|
self.content[key] = c
|
|
|
|
log.info(f"{self.address}: {key} constructed")
|
|
|
|
return True
|
|
|
|
|
|
|
|
def construct_created(self, invite):
|
|
|
|
self.subject = "Notice: User created"
|
2020-07-18 17:11:00 +00:00
|
|
|
log.debug(f'Constructing user creation notification for {invite["code"]}')
|
2020-07-17 15:08:36 +00:00
|
|
|
created = format_datetime(invite["created"])
|
|
|
|
if config.getboolean("email", "no_username"):
|
|
|
|
email = "n/a"
|
|
|
|
else:
|
|
|
|
email = invite["address"]
|
|
|
|
for key in ["text", "html"]:
|
2020-07-18 17:11:00 +00:00
|
|
|
fpath = Path(config["notifications"]["created_" + key])
|
|
|
|
with open(fpath, 'r') as f:
|
|
|
|
template = Template(f.read())
|
2020-07-17 15:08:36 +00:00
|
|
|
c = template.render(
|
|
|
|
code=invite["code"],
|
|
|
|
username=invite["username"],
|
|
|
|
address=email,
|
|
|
|
time=created,
|
|
|
|
)
|
|
|
|
self.content[key] = c
|
|
|
|
log.info(f"{self.address}: {key} constructed")
|
|
|
|
return True
|
|
|
|
|
2020-04-19 21:35:51 +00:00
|
|
|
def construct_reset(self, reset):
|
2020-06-21 19:29:53 +00:00
|
|
|
self.subject = config["password_resets"]["subject"]
|
|
|
|
log.debug(f"{self.address}: Using subject {self.subject}")
|
|
|
|
log.debug(f"{self.address}: Constructing email content")
|
2020-04-19 21:35:51 +00:00
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
expiry = date_parser.parse(reset["ExpirationDate"])
|
2020-04-19 21:35:51 +00:00
|
|
|
except:
|
|
|
|
log.error(f"{self.address}: Couldn't parse expiry time")
|
|
|
|
return False
|
2020-07-18 17:11:00 +00:00
|
|
|
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
|
2020-04-19 21:35:51 +00:00
|
|
|
if expiry >= current_time:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"{self.address}: Invite valid")
|
2020-07-18 17:11:00 +00:00
|
|
|
pretty = self.pretty_time(expiry, tzaware=True)
|
2020-06-21 19:29:53 +00:00
|
|
|
email_message = config["email"]["message"]
|
|
|
|
for key in ["text", "html"]:
|
2020-07-18 17:11:00 +00:00
|
|
|
fpath = Path(config["password_resets"]["email_" + key])
|
|
|
|
with open(fpath, 'r') as f:
|
|
|
|
template = Template(f.read())
|
2020-06-21 19:29:53 +00:00
|
|
|
c = template.render(
|
|
|
|
username=reset["UserName"],
|
|
|
|
expiry_date=pretty["date"],
|
|
|
|
expiry_time=pretty["time"],
|
|
|
|
expires_in=pretty["expires_in"],
|
|
|
|
pin=reset["Pin"],
|
|
|
|
message=email_message,
|
|
|
|
)
|
2020-04-19 21:35:51 +00:00
|
|
|
self.content[key] = c
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"{self.address}: {key} constructed")
|
2020-04-19 21:35:51 +00:00
|
|
|
return True
|
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
err = (
|
|
|
|
f"{self.address}: "
|
|
|
|
+ "Reset has reportedly already expired. "
|
|
|
|
+ "Ensure timezones are correctly configured."
|
|
|
|
)
|
2020-04-19 21:35:51 +00:00
|
|
|
log.error(err)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class Mailgun(Email):
|
2020-07-18 17:11:00 +00:00
|
|
|
errors = {
|
|
|
|
400: "Mailgun failed with 400: Bad request",
|
|
|
|
401: "Mailgun failed with 401: Invalid API key",
|
|
|
|
}
|
|
|
|
|
2020-04-19 21:35:51 +00:00
|
|
|
def __init__(self, address):
|
|
|
|
super().__init__(address)
|
2020-06-21 19:29:53 +00:00
|
|
|
self.api_url = config["mailgun"]["api_url"]
|
|
|
|
self.api_key = config["mailgun"]["api_key"]
|
|
|
|
self.from_mg = f"{self.from_name} <{self.from_address}>"
|
2020-04-19 21:35:51 +00:00
|
|
|
|
|
|
|
def send(self):
|
2020-06-21 19:29:53 +00:00
|
|
|
response = requests.post(
|
|
|
|
self.api_url,
|
|
|
|
auth=("api", self.api_key),
|
|
|
|
data={
|
|
|
|
"from": self.from_mg,
|
|
|
|
"to": [self.address],
|
|
|
|
"subject": self.subject,
|
|
|
|
"text": self.content["text"],
|
|
|
|
"html": self.content["html"],
|
|
|
|
},
|
|
|
|
)
|
2020-04-19 21:35:51 +00:00
|
|
|
if response.ok:
|
2020-06-21 19:29:53 +00:00
|
|
|
log.info(f"{self.address}: Sent via mailgun.")
|
2020-04-19 21:35:51 +00:00
|
|
|
return True
|
2020-07-18 17:11:00 +00:00
|
|
|
elif response.status_code in Mailgun.errors:
|
|
|
|
log.error(f"{self.address}: {Mailgun.errors[response.status_code]}")
|
|
|
|
else:
|
|
|
|
log.error(
|
|
|
|
f"{self.address}: Mailgun failed with error {response.status_code}"
|
|
|
|
)
|
2020-04-19 21:35:51 +00:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
class Smtp(Email):
|
|
|
|
def __init__(self, address):
|
|
|
|
super().__init__(address)
|
2020-06-21 19:29:53 +00:00
|
|
|
self.server = config["smtp"]["server"]
|
|
|
|
self.password = config["smtp"]["password"]
|
2020-04-19 21:35:51 +00:00
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
self.port = int(config["smtp"]["port"])
|
2020-04-19 21:35:51 +00:00
|
|
|
except ValueError:
|
|
|
|
self.port = 465
|
2020-06-21 19:29:53 +00:00
|
|
|
log.debug(f"{self.address}: Defaulting to port {self.port}")
|
2020-04-19 21:35:51 +00:00
|
|
|
|
|
|
|
def send(self):
|
|
|
|
message = MIMEMultipart("alternative")
|
|
|
|
message["Subject"] = self.subject
|
|
|
|
message["From"] = self.from_address
|
|
|
|
message["To"] = self.address
|
2020-06-21 19:29:53 +00:00
|
|
|
text = MIMEText(self.content["text"], "plain")
|
|
|
|
html = MIMEText(self.content["html"], "html")
|
2020-04-19 21:35:51 +00:00
|
|
|
message.attach(text)
|
|
|
|
message.attach(html)
|
|
|
|
try:
|
2020-06-21 19:29:53 +00:00
|
|
|
if config["smtp"]["encryption"] == "ssl_tls":
|
2020-04-26 20:28:55 +00:00
|
|
|
self.context = ssl.create_default_context()
|
2020-06-21 19:29:53 +00:00
|
|
|
with smtplib.SMTP_SSL(
|
|
|
|
self.server, self.port, context=self.context
|
|
|
|
) as server:
|
2020-04-26 20:28:55 +00:00
|
|
|
server.ehlo()
|
|
|
|
server.login(self.from_address, self.password)
|
2020-06-21 19:29:53 +00:00
|
|
|
server.sendmail(
|
|
|
|
self.from_address, self.address, message.as_string()
|
|
|
|
)
|
|
|
|
log.info(f"{self.address}: Sent via smtp (ssl/tls)")
|
2020-04-26 20:28:55 +00:00
|
|
|
return True
|
2020-06-21 19:29:53 +00:00
|
|
|
elif config["smtp"]["encryption"] == "starttls":
|
|
|
|
with smtplib.SMTP(self.server, self.port) as server:
|
2020-04-26 20:28:55 +00:00
|
|
|
server.ehlo()
|
|
|
|
server.starttls()
|
|
|
|
server.login(self.from_address, self.password)
|
2020-06-21 19:29:53 +00:00
|
|
|
server.sendmail(
|
|
|
|
self.from_address, self.address, message.as_string()
|
|
|
|
)
|
|
|
|
log.info(f"{self.address}: Sent via smtp (starttls)")
|
2020-04-26 20:28:55 +00:00
|
|
|
return True
|
2020-04-19 21:35:51 +00:00
|
|
|
except Exception as e:
|
2020-07-18 17:11:00 +00:00
|
|
|
log.error(
|
|
|
|
f"{self.address}: Failed to send via smtp ({type(e).__name__}: {e.args})"
|
|
|
|
)
|
2020-05-02 17:32:58 +00:00
|
|
|
try:
|
|
|
|
log.error(e.smtp_error)
|
|
|
|
except:
|
|
|
|
pass
|
2020-05-05 10:37:13 +00:00
|
|
|
return False
|