mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 17:10:11 +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:
parent
561e87984c
commit
e8ad3f98d6
41
README.md
41
README.md
@ -85,6 +85,7 @@ optional arguments:
|
|||||||
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
|
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
|
||||||
|
|
||||||
On first run, the default configuration is copied to `~/.jf-accounts/config.ini`.
|
On first run, the default configuration is copied to `~/.jf-accounts/config.ini`.
|
||||||
|
|
||||||
```
|
```
|
||||||
[jellyfin]
|
[jellyfin]
|
||||||
; It is reccommended to create a limited admin account for this program.
|
; It is reccommended to create a limited admin account for this program.
|
||||||
@ -98,7 +99,7 @@ device = jf-accounts
|
|||||||
device_id = jf-accounts-0.1
|
device_id = jf-accounts-0.1
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; Set to 0.0.0.0 to run localhost
|
; Set 0.0.0.0 to run localhost
|
||||||
host = 0.0.0.0
|
host = 0.0.0.0
|
||||||
port = 8056
|
port = 8056
|
||||||
username = your username
|
username = your username
|
||||||
@ -127,28 +128,42 @@ number = 1
|
|||||||
special = 0
|
special = 0
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
; Leave this whole section if you aren't using any email-related features.
|
||||||
enabled = true
|
|
||||||
; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory
|
|
||||||
watch_directory = /path/to/jellyfin
|
|
||||||
use_24h = true
|
use_24h = true
|
||||||
; Date format follows datetime's strftime.
|
; Date format follows datetime's strftime.
|
||||||
date_format = %-d/%-m/%-y
|
date_format = %-d/%-m/%-y
|
||||||
; Path to custom email html. If blank, uses the internal template.
|
|
||||||
email_template =
|
|
||||||
; Path to alternate plaintext email. If blank, uses the internal template.
|
|
||||||
email_plaintext =
|
|
||||||
; Displayed at bottom of emails
|
; Displayed at bottom of emails
|
||||||
message = Need help? contact me.
|
message = Need help? contact me.
|
||||||
; Mail methods: mailgun, smtp
|
; Mail methods: mailgun, smtp
|
||||||
method = mailgun
|
method = smtp
|
||||||
; Subject of emails
|
|
||||||
subject = Password Reset - Jellyfin
|
|
||||||
; Address to send from
|
; Address to send from
|
||||||
address = jellyfin@jellyf.in
|
address = jellyfin@jellyf.in
|
||||||
; The name of the sender
|
; The name of the sender
|
||||||
from = Jellyfin
|
from = Jellyfin
|
||||||
|
|
||||||
|
[password_resets]
|
||||||
|
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
||||||
|
enabled = true
|
||||||
|
; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory
|
||||||
|
watch_directory = /path/to/jellyfin
|
||||||
|
; Path to custom email html. If blank, uses the internal template.
|
||||||
|
email_html =
|
||||||
|
; Path to alternate plaintext email. If blank, uses the internal template.
|
||||||
|
email_text =
|
||||||
|
; Subject of emails
|
||||||
|
subject = Password Reset - Jellyfin
|
||||||
|
|
||||||
|
[invite_emails]
|
||||||
|
; If enabled, allows one to send an invite directly to an email address.
|
||||||
|
enabled = true
|
||||||
|
; Path to custom email html. If blank, uses the internal template.
|
||||||
|
email_html =
|
||||||
|
; Path to alternate plaintext email. If blank, uses the internal template.
|
||||||
|
email_text =
|
||||||
|
subject = Invite - Jellyfin
|
||||||
|
; Base url for jf-accounts. This necessary because most will use a reverse proxy, so the program has no other way of knowing what URL to send.
|
||||||
|
url_base = http://accounts.jellyf.in:8056/invite
|
||||||
|
|
||||||
[mailgun]
|
[mailgun]
|
||||||
|
|
||||||
api_url = https://api.mailgun.net...
|
api_url = https://api.mailgun.net...
|
||||||
@ -167,7 +182,7 @@ password = smtp password
|
|||||||
|
|
||||||
; Path to store valid invites.
|
; Path to store valid invites.
|
||||||
invites =
|
invites =
|
||||||
; Path to store email addresses in JSON
|
; Path to store emails addresses in JSON
|
||||||
emails =
|
emails =
|
||||||
; Path to the user policy template. Can be acquired with get-template.
|
; Path to the user policy template. Can be acquired with get-template.
|
||||||
user_template =
|
user_template =
|
||||||
|
@ -39,28 +39,42 @@ number = 1
|
|||||||
special = 0
|
special = 0
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
; Leave this whole section if you aren't using any email-related features.
|
||||||
enabled = true
|
|
||||||
; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory
|
|
||||||
watch_directory = /path/to/jellyfin
|
|
||||||
use_24h = true
|
use_24h = true
|
||||||
; Date format follows datetime's strftime.
|
; Date format follows datetime's strftime.
|
||||||
date_format = %-d/%-m/%-y
|
date_format = %-d/%-m/%-y
|
||||||
; Path to custom email html. If blank, uses the internal template.
|
|
||||||
email_template =
|
|
||||||
; Path to alternate plaintext email. If blank, uses the internal template.
|
|
||||||
email_plaintext =
|
|
||||||
; Displayed at bottom of emails
|
; Displayed at bottom of emails
|
||||||
message = Need help? contact me.
|
message = Need help? contact me.
|
||||||
; Mail methods: mailgun, smtp
|
; Mail methods: mailgun, smtp
|
||||||
method = smtp
|
method = smtp
|
||||||
; Subject of emails
|
|
||||||
subject = Password Reset - Jellyfin
|
|
||||||
; Address to send from
|
; Address to send from
|
||||||
address = jellyfin@jellyf.in
|
address = jellyfin@jellyf.in
|
||||||
; The name of the sender
|
; The name of the sender
|
||||||
from = Jellyfin
|
from = Jellyfin
|
||||||
|
|
||||||
|
[password_resets]
|
||||||
|
; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin
|
||||||
|
enabled = true
|
||||||
|
; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory
|
||||||
|
watch_directory = /path/to/jellyfin
|
||||||
|
; Path to custom email html. If blank, uses the internal template.
|
||||||
|
email_html =
|
||||||
|
; Path to alternate plaintext email. If blank, uses the internal template.
|
||||||
|
email_text =
|
||||||
|
; Subject of emails
|
||||||
|
subject = Password Reset - Jellyfin
|
||||||
|
|
||||||
|
[invite_emails]
|
||||||
|
; If enabled, allows one to send an invite directly to an email address.
|
||||||
|
enabled = true
|
||||||
|
; Path to custom email html. If blank, uses the internal template.
|
||||||
|
email_html =
|
||||||
|
; Path to alternate plaintext email. If blank, uses the internal template.
|
||||||
|
email_text =
|
||||||
|
subject = Invite - Jellyfin
|
||||||
|
; Base url for jf-accounts. This necessary because most will use a reverse proxy, so the program has no other way of knowing what URL to send.
|
||||||
|
url_base = http://accounts.jellyf.in:8056/invite
|
||||||
|
|
||||||
[mailgun]
|
[mailgun]
|
||||||
|
|
||||||
api_url = https://api.mailgun.net...
|
api_url = https://api.mailgun.net...
|
||||||
|
@ -149,7 +149,7 @@
|
|||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div style="font-family:Noto Sans;font-size:15px;line-height:1;text-align:left;color:#000000;">
|
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
|
||||||
<p>Hi {{ username }},</p>
|
<p>Hi {{ username }},</p>
|
||||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||||
<p>If this was you, enter the below pin into the prompt.</p>
|
<p>If this was you, enter the below pin into the prompt.</p>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</mj-section>
|
</mj-section>
|
||||||
<mj-section>
|
<mj-section>
|
||||||
<mj-column>
|
<mj-column>
|
||||||
<mj-text font-size="15px" font-family="Noto Sans">
|
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||||
<p>Hi {{ username }},</p>
|
<p>Hi {{ username }},</p>
|
||||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||||
<p>If this was you, enter the below pin into the prompt.</p>
|
<p>If this was you, enter the below pin into the prompt.</p>
|
||||||
|
239
data/invite-email.html
Normal file
239
data/invite-email.html
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<!-- FILE: invite-email.mjml -->
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
</title>
|
||||||
|
<!--[if !mso]><!-- -->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet" type="text/css">
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||||
|
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table
|
||||||
|
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="" style="vertical-align:top;width:600px;"
|
||||||
|
>
|
||||||
|
<![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;font-style:bold;line-height:1;text-align:left;color:#000000;">Jellyfin</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table
|
||||||
|
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="" style="vertical-align:top;width:600px;"
|
||||||
|
>
|
||||||
|
<![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p>Hi,</p>
|
||||||
|
<h2>You've been invited to Jellyfin.</h2>
|
||||||
|
<p>To join, click the button below.</p>
|
||||||
|
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle">
|
||||||
|
<a href="{{ invite_link }}" style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Setup your account </a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table
|
||||||
|
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="" style="vertical-align:top;width:600px;"
|
||||||
|
>
|
||||||
|
<![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:10px;font-style:italic;line-height:1;text-align:left;color:#000000;">{{ message }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
33
data/invite-email.mjml
Normal file
33
data/invite-email.mjml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" />
|
||||||
|
</mj-head>
|
||||||
|
<mj-body>
|
||||||
|
<mj-section background-color="#f0f0f0">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-style="bold" font-size="20px">
|
||||||
|
Jellyfin
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||||
|
<p>Hi,</p>
|
||||||
|
<h2>You've been invited to Jellyfin.</h2>
|
||||||
|
<p>To join, click the button below.</p>
|
||||||
|
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
|
||||||
|
</mj-text>
|
||||||
|
<mj-button href="{{ invite_link }}">Setup your account</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section background-color="#f0f0f0">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-style="italic" font-size="10px">
|
||||||
|
{{ message }}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</body>
|
||||||
|
</mjml>
|
||||||
|
|
8
data/invite-email.txt
Normal file
8
data/invite-email.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Hi,
|
||||||
|
You've been invited to Jellyfin.
|
||||||
|
To join, follow the below link.
|
||||||
|
This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.
|
||||||
|
|
||||||
|
{{ invite_link }}
|
||||||
|
|
||||||
|
{{ message }}
|
@ -2,7 +2,7 @@ function parseInvite(invite, empty = false) {
|
|||||||
if (empty === true) {
|
if (empty === true) {
|
||||||
return ["None", "", "1"]
|
return ["None", "", "1"]
|
||||||
} else {
|
} else {
|
||||||
var i = ["", "", "0"];
|
var i = ["", "", "0", invite['email']];
|
||||||
i[0] = invite['code'];
|
i[0] = invite['code'];
|
||||||
if (invite['hours'] == 0) {
|
if (invite['hours'] == 0) {
|
||||||
i[1] = invite['minutes'] + 'm';
|
i[1] = invite['minutes'] + 'm';
|
||||||
@ -40,6 +40,13 @@ function addItem(invite) {
|
|||||||
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
||||||
codeCopy.classList.add('fa', 'fa-clipboard');
|
codeCopy.classList.add('fa', 'fa-clipboard');
|
||||||
listCode.appendChild(codeCopy);
|
listCode.appendChild(codeCopy);
|
||||||
|
console.log(invite[3]);
|
||||||
|
if (typeof(invite[3]) != 'undefined') {
|
||||||
|
var sentTo = document.createElement('span');
|
||||||
|
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
|
||||||
|
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
|
||||||
|
listCode.appendChild(sentTo);
|
||||||
|
};
|
||||||
var listDelete = document.createElement('button');
|
var listDelete = document.createElement('button');
|
||||||
listDelete.onclick = function(){deleteInvite(invite[0])};
|
listDelete.onclick = function(){deleteInvite(invite[0])};
|
||||||
listDelete.classList.add('btn', 'btn-outline-danger');
|
listDelete.classList.add('btn', 'btn-outline-danger');
|
||||||
@ -153,7 +160,11 @@ function toClipboard(str) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
$("form#inviteForm").submit(function() {
|
$("form#inviteForm").submit(function() {
|
||||||
var send = $("form#inviteForm").serializeJSON();
|
var send_object = $("form#inviteForm").serializeObject();
|
||||||
|
if (document.getElementById('send_to_address_enabled').checked) {
|
||||||
|
send_object['email'] = document.getElementById('send_to_address').value;
|
||||||
|
}
|
||||||
|
var send = JSON.stringify(send_object);
|
||||||
$.ajax('/generateInvite', {
|
$.ajax('/generateInvite', {
|
||||||
data : send,
|
data : send,
|
||||||
contentType : 'application/json',
|
contentType : 'application/json',
|
||||||
|
@ -14,22 +14,30 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
.pageContainer {
|
||||||
margin: 20%;
|
margin: 20%;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.pageContainer {
|
||||||
|
margin: 2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
/*margin: 20%;*/
|
||||||
margin-bottom: 5%;
|
margin-bottom: 5%;
|
||||||
}
|
}
|
||||||
.linkGroup {
|
.linkGroup {
|
||||||
margin: 20%;
|
/*margin: 20%;*/
|
||||||
margin-bottom: 5%;
|
margin-bottom: 5%;
|
||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
}
|
}
|
||||||
.linkForm {
|
.linkForm {
|
||||||
margin: 20%;
|
/*margin: 20%;*/
|
||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
margin-bottom: 5%;
|
margin-bottom: 5%;
|
||||||
}
|
}
|
||||||
.contactBox {
|
.contactBox {
|
||||||
margin: 20%;
|
/*margin: 20%;*/
|
||||||
margin-top: 5%;
|
margin-top: 5%;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
@ -65,36 +73,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<div class="pageContainer">
|
||||||
Accounts admin
|
<h1>
|
||||||
</h1>
|
Accounts admin
|
||||||
<div class="card bg-light mb-3 linkGroup">
|
</h1>
|
||||||
<div class="card-header">Current Invites</div>
|
<div class="card bg-light mb-3 linkGroup">
|
||||||
<ul class="list-group list-group-flush" id="invites">
|
<div class="card-header">Current Invites</div>
|
||||||
</ul>
|
<ul class="list-group list-group-flush" id="invites">
|
||||||
</div>
|
</ul>
|
||||||
<div class="linkForm">
|
</div>
|
||||||
<div class="card bg-light mb-3">
|
<div class="linkForm">
|
||||||
<div class="card-header">Generate Invite</div>
|
<div class="card bg-light mb-3">
|
||||||
<div class="card-body">
|
<div class="card-header">Generate Invite</div>
|
||||||
<form action="#" method="POST" id="inviteForm">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<form action="#" method="POST" id="inviteForm">
|
||||||
<label for="hours">Hours</label>
|
<div class="form-group">
|
||||||
<select class="form-control" id="hours" name="hours">
|
<label for="hours">Hours</label>
|
||||||
</select>
|
<select class="form-control" id="hours" name="hours">
|
||||||
</div>
|
</select>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="minutes">Minutes</label>
|
<div class="form-group">
|
||||||
<select class="form-control" id="minutes" name="minutes">
|
<label for="minutes">Minutes</label>
|
||||||
</select>
|
<select class="form-control" id="minutes" name="minutes">
|
||||||
</div>
|
</select>
|
||||||
<input type="submit" class="btn btn-primary" value="Generate">
|
</div>
|
||||||
</form>
|
<div class="form-group">
|
||||||
|
<label for="send_to_address">Send invite to address</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="btn btn-primary" value="Generate">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="contactBox">
|
||||||
<div class="contactBox">
|
<p>{{ contactMessage }}</p>
|
||||||
<p>{{ contactMessage }}</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="admin.js"></script>
|
<script src="admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
.pageContainer {
|
.pageContainer {
|
||||||
margin: 20%;
|
margin: 20%;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.pageContainer {
|
||||||
|
margin: 2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.contactBox {
|
.contactBox {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
@ -54,7 +59,7 @@
|
|||||||
<form action="#" method="POST">
|
<form action="#" method="POST">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputEmail">Email</label>
|
<label for="inputEmail">Email</label>
|
||||||
<input type="email" class="form-control" id="inputEmail" name="email" placeholder="Email" required>
|
<input type="email" class="form-control" id="inputEmail" name="email" placeholder="Email" value="{{ email }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inputUsername">Username</label>
|
<label for="inputUsername">Username</label>
|
||||||
|
BIN
images/admin.png
BIN
images/admin.png
Binary file not shown.
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 50 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.5 MiB |
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 time
|
||||||
import json
|
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.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
from dateutil import parser as date_parser
|
from jellyfin_accounts.email import Mailgun, Smtp
|
||||||
from jinja2 import Environment, FileSystemLoader, Template
|
|
||||||
from __main__ import config
|
from __main__ import config
|
||||||
from __main__ import email_log as log
|
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:
|
class Watcher:
|
||||||
def __init__(self, dir):
|
def __init__(self, dir):
|
||||||
self.observer = Observer()
|
self.observer = Observer()
|
||||||
@ -152,7 +15,10 @@ class Watcher:
|
|||||||
def run(self):
|
def run(self):
|
||||||
event_handler = Handler()
|
event_handler = Handler()
|
||||||
self.observer.schedule(event_handler, self.dir, recursive=True)
|
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:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
@ -181,7 +47,7 @@ class Handler(FileSystemEventHandler):
|
|||||||
email = Mailgun(address)
|
email = Mailgun(address)
|
||||||
elif method == 'smtp':
|
elif method == 'smtp':
|
||||||
email = Smtp(address)
|
email = Smtp(address)
|
||||||
if email.construct(reset):
|
if email.construct_reset(reset):
|
||||||
email.send()
|
email.send()
|
||||||
except (FileNotFoundError,
|
except (FileNotFoundError,
|
||||||
json.decoder.JSONDecodeError,
|
json.decoder.JSONDecodeError,
|
||||||
@ -190,6 +56,6 @@ class Handler(FileSystemEventHandler):
|
|||||||
log.error(err)
|
log.error(err)
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
log.info(f'Monitoring {config["email"]["watch_directory"]}')
|
log.info(f'Monitoring {config["password_resets"]["watch_directory"]}')
|
||||||
w = Watcher(config['email']['watch_directory'])
|
w = Watcher(config['password_resets']['watch_directory'])
|
||||||
w.run()
|
w.run()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, send_from_directory, render_template
|
from flask import Flask, send_from_directory, render_template
|
||||||
from __main__ import config, app, g
|
from __main__ import config, app, g
|
||||||
@ -31,11 +32,23 @@ from jellyfin_accounts.web_api import checkInvite
|
|||||||
def inviteProxy(path):
|
def inviteProxy(path):
|
||||||
if checkInvite(path):
|
if checkInvite(path):
|
||||||
log.info(f'Invite {path} used to request form')
|
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',
|
return render_template('form.html',
|
||||||
contactMessage=config['ui']['contact_message'],
|
contactMessage=config['ui']['contact_message'],
|
||||||
helpMessage=config['ui']['help_message'],
|
helpMessage=config['ui']['help_message'],
|
||||||
successMessage=config['ui']['success_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:
|
elif 'admin.html' not in path and 'admin.html' not in path:
|
||||||
return app.send_static_file(path)
|
return app.send_static_file(path)
|
||||||
else:
|
else:
|
||||||
|
@ -62,7 +62,7 @@ while attempts != 3:
|
|||||||
except Jellyfin.AuthenticationError:
|
except Jellyfin.AuthenticationError:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
log.error(('Failed to authenticate with ' +
|
log.error(('Failed to authenticate with ' +
|
||||||
config['jellyfin']['server'] +
|
config['jellyfin']['server'] +
|
||||||
'. Retrying...'))
|
'. Retrying...'))
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
@ -140,12 +140,26 @@ def newUser():
|
|||||||
def generateInvite():
|
def generateInvite():
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
delta = datetime.timedelta(hours=int(data['hours']),
|
delta = datetime.timedelta(hours=int(data['hours']),
|
||||||
minutes=int(data['minutes']))
|
minutes=int(data['minutes']))
|
||||||
invite = {'code': secrets.token_urlsafe(16)}
|
invite = {'code': secrets.token_urlsafe(16)}
|
||||||
log.debug(f'Creating new invite: {invite["code"]}')
|
log.debug(f'Creating new invite: {invite["code"]}')
|
||||||
invite['valid_till'] = (current_time +
|
valid_till = current_time + delta
|
||||||
delta).strftime('%Y-%m-%dT%H:%M:%S.%f')
|
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:
|
try:
|
||||||
with open(config['files']['invites'], 'r') as f:
|
with open(config['files']['invites'], 'r') as f:
|
||||||
invites = json.load(f)
|
invites = json.load(f)
|
||||||
@ -178,13 +192,15 @@ def getInvites():
|
|||||||
del invites['invites'][index]
|
del invites['invites'][index]
|
||||||
else:
|
else:
|
||||||
valid_for = expiry - current_time
|
valid_for = expiry - current_time
|
||||||
response['invites'].append({
|
invite = {'code': i['code'],
|
||||||
'code': i['code'],
|
'hours': valid_for.seconds//3600,
|
||||||
'hours': valid_for.seconds//3600,
|
'minutes': (valid_for.seconds//60) % 60}
|
||||||
'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:
|
with open(config['files']['invites'], 'w') as f:
|
||||||
f.write(json.dumps(invites, indent=4, default=str))
|
f.write(json.dumps(invites, indent=4, default=str))
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/deleteInvite', methods=['POST'])
|
@app.route('/deleteInvite', methods=['POST'])
|
||||||
@ -235,5 +251,3 @@ def modifyConfig():
|
|||||||
temp_config.write(config_file)
|
temp_config.write(config_file)
|
||||||
log.debug('Config written')
|
log.debug('Config written')
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
28
jf-accounts
28
jf-accounts
@ -87,12 +87,26 @@ for key in config['files']:
|
|||||||
log.debug(f'Using default {key}')
|
log.debug(f'Using default {key}')
|
||||||
config['files'][key] = str(data_dir / (key + '.json'))
|
config['files'][key] = str(data_dir / (key + '.json'))
|
||||||
|
|
||||||
if config['email']['email_template'] == '':
|
if ('email_html' not in config['password_resets'] or
|
||||||
log.debug('Using default email HTML template')
|
config['password_resets']['email_html'] == ''):
|
||||||
config['email']['email_template'] = str(local_dir / 'email.html')
|
log.debug('Using default password reset email HTML template')
|
||||||
if config['email']['email_plaintext'] == '':
|
config['password_resets']['email_html'] = str(local_dir / 'email.html')
|
||||||
log.debug('Using default email plaintext template')
|
if ('email_text' not in config['password_resets'] or
|
||||||
config['email']['email_plaintext'] = str(local_dir / 'email.txt')
|
config['password_resets']['email_text'] == ''):
|
||||||
|
log.debug('Using default password reset email plaintext template')
|
||||||
|
config['password_resets']['email_text'] = str(local_dir / 'email.txt')
|
||||||
|
|
||||||
|
if ('email_html' not in config['invite_emails'] or
|
||||||
|
config['invite_emails']['email_html'] == ''):
|
||||||
|
log.debug('Using default invite email HTML template')
|
||||||
|
config['invite_emails']['email_html'] = str(local_dir /
|
||||||
|
'invite-email.html')
|
||||||
|
if ('email_text' not in config['invite_emails'] or
|
||||||
|
config['invite_emails']['email_text'] == ''):
|
||||||
|
log.debug('Using default invite email plaintext template')
|
||||||
|
config['invite_emails']['email_text'] = str(local_dir /
|
||||||
|
'invite-email.txt')
|
||||||
|
|
||||||
|
|
||||||
if args.get_policy:
|
if args.get_policy:
|
||||||
import json
|
import json
|
||||||
@ -136,7 +150,7 @@ else:
|
|||||||
host = config['ui']['host']
|
host = config['ui']['host']
|
||||||
port = config['ui']['port']
|
port = config['ui']['port']
|
||||||
log.info(f'Starting web UI on {host}:{port}')
|
log.info(f'Starting web UI on {host}:{port}')
|
||||||
if config.getboolean('email', 'enabled'):
|
if config.getboolean('password_resets', 'enabled'):
|
||||||
def start_pwr():
|
def start_pwr():
|
||||||
import jellyfin_accounts.pw_reset
|
import jellyfin_accounts.pw_reset
|
||||||
jellyfin_accounts.pw_reset.start()
|
jellyfin_accounts.pw_reset.start()
|
||||||
|
5
setup.py
5
setup.py
@ -23,7 +23,10 @@ setup(
|
|||||||
data_files=[('data', ['data/config-default.ini',
|
data_files=[('data', ['data/config-default.ini',
|
||||||
'data/email.html',
|
'data/email.html',
|
||||||
'data/email.mjml',
|
'data/email.mjml',
|
||||||
'data/email.txt']),
|
'data/email.txt',
|
||||||
|
'data/invite-email.html',
|
||||||
|
'data/invite-email.mjml',
|
||||||
|
'data/invite-email.txt']),
|
||||||
('data/static', ['data/static/admin.js']),
|
('data/static', ['data/static/admin.js']),
|
||||||
('data/templates', [
|
('data/templates', [
|
||||||
'data/templates/404.html',
|
'data/templates/404.html',
|
||||||
|
Loading…
Reference in New Issue
Block a user