mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-08 11:39:33 +00:00
added option to send invite to an address
The admin page now has the option to send an invite to an email address. Since there are now two email types (invites and pw resets), the new sections have been added to config.ini, and email_template and email_plaintext have been renamed to email_html and email_text respectively.
This commit is contained in:
166
jellyfin_accounts/email.py
Normal file
166
jellyfin_accounts/email.py
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from __main__ import config
|
||||
from __main__ import email_log as log
|
||||
|
||||
|
||||
class Email():
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
log.debug(f'{self.address}: Creating email')
|
||||
self.content = {}
|
||||
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})'))
|
||||
|
||||
def pretty_time(self, expiry):
|
||||
current_time = datetime.datetime.now()
|
||||
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')
|
||||
else:
|
||||
log.debug(f'{self.address}: Using 12h time')
|
||||
time = expiry.strftime('%-I:%M %p')
|
||||
expiry_delta = (expiry - current_time).seconds
|
||||
expires_in = {'hours': expiry_delta//3600,
|
||||
'minutes': (expiry_delta//60) % 60}
|
||||
if expires_in['hours'] == 0:
|
||||
expires_in = f'{str(expires_in["minutes"])}m'
|
||||
else:
|
||||
expires_in = (f'{str(expires_in["hours"])}h ' +
|
||||
f'{str(expires_in["minutes"])}m')
|
||||
log.debug(f'{self.address}: Expires in {expires_in}')
|
||||
return {'date': date, 'time': time, 'expires_in': expires_in}
|
||||
|
||||
def construct_invite(self, invite):
|
||||
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']
|
||||
expiry.replace(tzinfo=None)
|
||||
pretty = self.pretty_time(expiry)
|
||||
email_message = config['email']['message']
|
||||
invite_link = config['invite_emails']['url_base']
|
||||
invite_link += '/' + invite['code']
|
||||
for key in ['text', 'html']:
|
||||
sp = Path(config['invite_emails']['email_' + key]) / '..'
|
||||
sp = str(sp.resolve()) + '/'
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config['invite_emails']['email_' + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
c = template.render(expiry_date=pretty['date'],
|
||||
expiry_time=pretty['time'],
|
||||
expires_in=pretty['expires_in'],
|
||||
invite_link=invite_link,
|
||||
message=email_message)
|
||||
self.content[key] = c
|
||||
log.info(f'{self.address}: {key} constructed')
|
||||
|
||||
def construct_reset(self, reset):
|
||||
self.subject = config['password_resets']['subject']
|
||||
log.debug(f'{self.address}: Using subject {self.subject}')
|
||||
log.debug(f'{self.address}: Constructing email content')
|
||||
try:
|
||||
expiry = date_parser.parse(reset['ExpirationDate'])
|
||||
expiry = expiry.replace(tzinfo=None)
|
||||
except:
|
||||
log.error(f"{self.address}: Couldn't parse expiry time")
|
||||
return False
|
||||
current_time = datetime.datetime.now()
|
||||
if expiry >= current_time:
|
||||
log.debug(f'{self.address}: Invite valid')
|
||||
pretty = self.pretty_time(expiry)
|
||||
email_message = config['email']['message']
|
||||
for key in ['text', 'html']:
|
||||
sp = Path(config['password_resets']['email_' + key]) / '..'
|
||||
sp = str(sp.resolve()) + '/'
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config['password_resets']['email_' + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
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)
|
||||
self.content[key] = c
|
||||
log.info(f'{self.address}: {key} constructed')
|
||||
return True
|
||||
else:
|
||||
err = ((f"{self.address}: " +
|
||||
"Reset has reportedly already expired. " +
|
||||
"Ensure timezones are correctly configured."))
|
||||
log.error(err)
|
||||
return False
|
||||
|
||||
|
||||
class Mailgun(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.api_url = config['mailgun']['api_url']
|
||||
self.api_key = config['mailgun']['api_key']
|
||||
self.from_mg = f'{self.from_name} <{self.from_address}>'
|
||||
|
||||
def send(self):
|
||||
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']})
|
||||
if response.ok:
|
||||
log.info(f'{self.address}: Sent via mailgun.')
|
||||
return True
|
||||
log.debug(f'{self.address}: Mailgun: {response.status_code}')
|
||||
return response
|
||||
|
||||
|
||||
class Smtp(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.server = config['smtp']['server']
|
||||
self.password = config['smtp']['password']
|
||||
try:
|
||||
self.port = int(config['smtp']['port'])
|
||||
except ValueError:
|
||||
self.port = 465
|
||||
log.debug(f'{self.address}: Defaulting to port {self.port}')
|
||||
self.context = ssl.create_default_context()
|
||||
|
||||
def send(self):
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = self.subject
|
||||
message["From"] = self.from_address
|
||||
message["To"] = self.address
|
||||
text = MIMEText(self.content['text'], 'plain')
|
||||
html = MIMEText(self.content['html'], 'html')
|
||||
message.attach(text)
|
||||
message.attach(html)
|
||||
try:
|
||||
with smtplib.SMTP_SSL(self.server,
|
||||
self.port,
|
||||
context=self.context) as server:
|
||||
server.login(self.from_address, self.password)
|
||||
server.sendmail(self.from_address,
|
||||
self.address,
|
||||
message.as_string())
|
||||
log.info(f'{self.address}: Sent via smtp')
|
||||
return True
|
||||
except Exception as e:
|
||||
err = f'{self.address}: Failed to send via smtp: '
|
||||
err += type(e).__name__
|
||||
log.error(err)
|
||||
@@ -1,149 +1,12 @@
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
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 watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from dateutil import parser as date_parser
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
from jellyfin_accounts.email import Mailgun, Smtp
|
||||
from __main__ import config
|
||||
from __main__ import email_log as log
|
||||
|
||||
|
||||
class Email():
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
log.debug(f'{self.address}: Creating email')
|
||||
self.content = {}
|
||||
self.subject = config['email']['subject']
|
||||
log.debug(f'{self.address}: Using subject {self.subject}')
|
||||
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})'))
|
||||
|
||||
def construct(self, reset):
|
||||
log.debug(f'{self.address}: Constructing email content')
|
||||
try:
|
||||
expiry = date_parser.parse(reset['ExpirationDate'])
|
||||
expiry = expiry.replace(tzinfo=None)
|
||||
except:
|
||||
log.error(f"{self.address}: Couldn't parse expiry time")
|
||||
return False
|
||||
current_time = datetime.datetime.now()
|
||||
if expiry >= current_time:
|
||||
log.debug(f'{self.address}: Invite valid')
|
||||
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')
|
||||
else:
|
||||
log.debug(f'{self.address}: Using 12h time')
|
||||
time = expiry.strftime('%-I:%M %p')
|
||||
expiry_delta = (expiry - current_time).seconds
|
||||
expires_in = {'hours': expiry_delta//3600,
|
||||
'minutes': (expiry_delta//60) % 60}
|
||||
if expires_in['hours'] == 0:
|
||||
expires_in = f'{str(expires_in["minutes"])}m'
|
||||
else:
|
||||
expires_in = (f'{str(expires_in["hours"])}h ' +
|
||||
f'{str(expires_in["minutes"])}m')
|
||||
log.debug(f'{self.address}: Expires in {expires_in}')
|
||||
sp = Path(config['email']['email_template']) / '..'
|
||||
sp = str(sp.resolve()) + '/'
|
||||
templateLoader = FileSystemLoader(searchpath=sp)
|
||||
templateEnv = Environment(loader=templateLoader)
|
||||
file_text = Path(config['email']['email_plaintext']).name
|
||||
file_html = Path(config['email']['email_template']).name
|
||||
template = {}
|
||||
template['text'] = templateEnv.get_template(file_text)
|
||||
template['html'] = templateEnv.get_template(file_html)
|
||||
email_message = config['email']['message']
|
||||
for key in template:
|
||||
c = template[key].render(username=reset['UserName'],
|
||||
expiry_date=date,
|
||||
expiry_time=time,
|
||||
expires_in=expires_in,
|
||||
pin=reset['Pin'],
|
||||
message=email_message)
|
||||
self.content[key] = c
|
||||
log.info(f'{self.address}: {key} constructed')
|
||||
return True
|
||||
else:
|
||||
err = ((f"{self.address}: " +
|
||||
"Reset has reportedly already expired. " +
|
||||
"Ensure timezones are correctly configured."))
|
||||
log.error(err)
|
||||
return False
|
||||
|
||||
|
||||
class Mailgun(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.api_url = config['mailgun']['api_url']
|
||||
self.api_key = config['mailgun']['api_key']
|
||||
self.from_mg = f'{self.from_name} <{self.from_address}>'
|
||||
|
||||
def send(self):
|
||||
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']})
|
||||
if response.ok:
|
||||
log.info(f'{self.address}: Sent via mailgun.')
|
||||
return True
|
||||
log.debug(f'{self.address}: Mailgun: {response.status_code}')
|
||||
return response
|
||||
|
||||
|
||||
class Smtp(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.server = config['smtp']['server']
|
||||
self.password = config['smtp']['password']
|
||||
try:
|
||||
self.port = int(config['smtp']['port'])
|
||||
except ValueError:
|
||||
self.port = 465
|
||||
log.debug(f'{self.address}: Defaulting to port {self.port}')
|
||||
self.context = ssl.create_default_context()
|
||||
|
||||
def send(self):
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = self.subject
|
||||
message["From"] = self.from_address
|
||||
message["To"] = self.address
|
||||
text = MIMEText(self.content['text'], 'plain')
|
||||
html = MIMEText(self.content['html'], 'html')
|
||||
message.attach(text)
|
||||
message.attach(html)
|
||||
try:
|
||||
with smtplib.SMTP_SSL(self.server,
|
||||
self.port,
|
||||
context=self.context) as server:
|
||||
server.login(self.from_address, self.password)
|
||||
server.sendmail(self.from_address,
|
||||
self.address,
|
||||
message.as_string())
|
||||
log.info(f'{self.address}: Sent via smtp')
|
||||
return True
|
||||
except Exception as e:
|
||||
err = f'{self.address}: Failed to send via smtp: '
|
||||
err += type(e).__name__
|
||||
log.error(err)
|
||||
|
||||
|
||||
class Watcher:
|
||||
def __init__(self, dir):
|
||||
self.observer = Observer()
|
||||
@@ -152,7 +15,10 @@ class Watcher:
|
||||
def run(self):
|
||||
event_handler = Handler()
|
||||
self.observer.schedule(event_handler, self.dir, recursive=True)
|
||||
self.observer.start()
|
||||
try:
|
||||
self.observer.start()
|
||||
except NotADirectoryError:
|
||||
log.error(f'Directory {self.dir} does not exist')
|
||||
try:
|
||||
while True:
|
||||
time.sleep(5)
|
||||
@@ -181,7 +47,7 @@ class Handler(FileSystemEventHandler):
|
||||
email = Mailgun(address)
|
||||
elif method == 'smtp':
|
||||
email = Smtp(address)
|
||||
if email.construct(reset):
|
||||
if email.construct_reset(reset):
|
||||
email.send()
|
||||
except (FileNotFoundError,
|
||||
json.decoder.JSONDecodeError,
|
||||
@@ -190,6 +56,6 @@ class Handler(FileSystemEventHandler):
|
||||
log.error(err)
|
||||
|
||||
def start():
|
||||
log.info(f'Monitoring {config["email"]["watch_directory"]}')
|
||||
w = Watcher(config['email']['watch_directory'])
|
||||
log.info(f'Monitoring {config["password_resets"]["watch_directory"]}')
|
||||
w = Watcher(config['password_resets']['watch_directory'])
|
||||
w.run()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
from __main__ import config, app, g
|
||||
@@ -31,11 +32,23 @@ from jellyfin_accounts.web_api import checkInvite
|
||||
def inviteProxy(path):
|
||||
if checkInvite(path):
|
||||
log.info(f'Invite {path} used to request form')
|
||||
try:
|
||||
with open(config['files']['invites'], 'r') as f:
|
||||
invites = json.load(f)
|
||||
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||
invites = {'invites': []}
|
||||
for invite in invites['invites']:
|
||||
if invite['code'] == path:
|
||||
try:
|
||||
email = invite['email']
|
||||
except KeyError:
|
||||
email = ""
|
||||
return render_template('form.html',
|
||||
contactMessage=config['ui']['contact_message'],
|
||||
helpMessage=config['ui']['help_message'],
|
||||
successMessage=config['ui']['success_message'],
|
||||
jfLink=config['jellyfin']['server'])
|
||||
jfLink=config['jellyfin']['server'],
|
||||
email=email)
|
||||
elif 'admin.html' not in path and 'admin.html' not in path:
|
||||
return app.send_static_file(path)
|
||||
else:
|
||||
|
||||
@@ -62,7 +62,7 @@ while attempts != 3:
|
||||
except Jellyfin.AuthenticationError:
|
||||
attempts += 1
|
||||
log.error(('Failed to authenticate with ' +
|
||||
config['jellyfin']['server'] +
|
||||
config['jellyfin']['server'] +
|
||||
'. Retrying...'))
|
||||
time.sleep(5)
|
||||
|
||||
@@ -140,12 +140,26 @@ def newUser():
|
||||
def generateInvite():
|
||||
current_time = datetime.datetime.now()
|
||||
data = request.get_json()
|
||||
delta = datetime.timedelta(hours=int(data['hours']),
|
||||
delta = datetime.timedelta(hours=int(data['hours']),
|
||||
minutes=int(data['minutes']))
|
||||
invite = {'code': secrets.token_urlsafe(16)}
|
||||
log.debug(f'Creating new invite: {invite["code"]}')
|
||||
invite['valid_till'] = (current_time +
|
||||
delta).strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
valid_till = current_time + delta
|
||||
invite['valid_till'] = valid_till.strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
if 'email' in data and config.getboolean('invite_emails', 'enabled'):
|
||||
address = data['email']
|
||||
invite['email'] = address
|
||||
log.info(f'Sending invite to {address}')
|
||||
method = config['email']['method']
|
||||
if method == 'mailgun':
|
||||
from jellyfin_accounts.email import Mailgun
|
||||
email = Mailgun(address)
|
||||
elif method == 'smtp':
|
||||
from jellyfin_accounts.email import Smtp
|
||||
email = Smtp(address)
|
||||
email.construct_invite({'expiry': valid_till,
|
||||
'code': invite['code']})
|
||||
email.send()
|
||||
try:
|
||||
with open(config['files']['invites'], 'r') as f:
|
||||
invites = json.load(f)
|
||||
@@ -178,13 +192,15 @@ def getInvites():
|
||||
del invites['invites'][index]
|
||||
else:
|
||||
valid_for = expiry - current_time
|
||||
response['invites'].append({
|
||||
'code': i['code'],
|
||||
'hours': valid_for.seconds//3600,
|
||||
'minutes': (valid_for.seconds//60) % 60})
|
||||
invite = {'code': i['code'],
|
||||
'hours': valid_for.seconds//3600,
|
||||
'minutes': (valid_for.seconds//60) % 60}
|
||||
if 'email' in i:
|
||||
invite['email'] = i['email']
|
||||
response['invites'].append(invite)
|
||||
with open(config['files']['invites'], 'w') as f:
|
||||
f.write(json.dumps(invites, indent=4, default=str))
|
||||
return jsonify(response)
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@app.route('/deleteInvite', methods=['POST'])
|
||||
@@ -235,5 +251,3 @@ def modifyConfig():
|
||||
temp_config.write(config_file)
|
||||
log.debug('Config written')
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user