Finally got mail working for good

This commit is contained in:
Kumi 2020-05-21 14:54:59 +02:00
parent d549897186
commit 89f971117f
20 changed files with 135 additions and 30 deletions

View file

@ -30,6 +30,8 @@ class BaseMailProvider:
for header, value in headers.items(): for header, value in headers.items():
if value: if value:
message.add_header(header, value) message.add_header(header, value)
message.set_charset("base64")
return self.send_message(message) return self.send_message(message)
class SMTPMailProvider(BaseMailProvider): class SMTPMailProvider(BaseMailProvider):
@ -43,4 +45,4 @@ class SMTPMailProvider(BaseMailProvider):
return "SMTP Mail" return "SMTP Mail"
def send_message(self, message): def send_message(self, message):
return self.smtp.send_message(message) return self.smtp.send_message(message, rcpt_options=['NOTIFY=SUCCESS,DELAY,FAILURE'])

2
core/exceptions/mail.py Normal file
View file

@ -0,0 +1,2 @@
class NoSuchTemplate(ValueError):
pass

View file

@ -1,2 +1,11 @@
from core.helpers.mail import get_template
from core.helpers.urls import relative_to_absolute as reltoabs
from django.urls import reverse
from dbsettings.functions import getValue
def generate_pwreset_mail(user, token): def generate_pwreset_mail(user, token):
pass link = reltoabs(reverse("pwreset", kwargs={"pk": str(token.token)}))
template = get_template("backend/auth/pwreset", first_name=user.first_name, link=link, sitename=getValue("core.title", "Expephalon"))
return template

View file

@ -1,8 +1,13 @@
from core.modules.mail import providers from django.conf import settings
from core.modules.mail import providers, templates
from core.tasks.mail import send_mail as send_mail_task from core.tasks.mail import send_mail as send_mail_task
from core.exceptions.mail import NoSuchTemplate
from dbsettings.functions import getValue from dbsettings.functions import getValue
import os.path
def get_provider_by_name(name, fallback=True): def get_provider_by_name(name, fallback=True):
return providers.get(name, None) or providers["smtp"] return providers.get(name, None) or providers["smtp"]
@ -16,5 +21,16 @@ def send_mail(provider=get_default_provider(), **kwargs):
def simple_send_mail(subject, content, recipients, cc=[], bcc=[], headers={}): def simple_send_mail(subject, content, recipients, cc=[], bcc=[], headers={}):
return send_mail(subject=subject, content=content, recipients=recipients, cc=cc, bcc=bcc, headers=headers) return send_mail(subject=subject, content=content, recipients=recipients, cc=cc, bcc=bcc, headers=headers)
def fetch_templates(template_name): def get_template(template_name, format="txt", **kwargs):
pass try:
template = templates[template_name][format]
except KeyError:
raise NoSuchTemplate(f"No email template called {template_name} of format {format} loaded")
with open(template, "r") as templatefile:
templatetext = templatefile.read()
for key, value in kwargs.items():
templatetext = templatetext.replace('{§%s§}' % key, value)
return templatetext

6
core/helpers/urls.py Normal file
View file

@ -0,0 +1,6 @@
from urllib.parse import urljoin
from dbsettings.functions import getValue
def relative_to_absolute(path, domain=getValue("core.base_url", "http://localhost:8000")):
return urljoin(domain, path)

18
core/mixins/auth.py Normal file
View file

@ -0,0 +1,18 @@
from django.contrib.auth.mixins import AccessMixin
from django.contrib.messages import error
from core.models.profiles import AdminProfile
class AdminMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
self.permission_denied_message = "You must be logged in to access this area."
else:
try:
AdminProfile.objects.get(user=request.user)
return super().dispatch(request, *args, **kwargs)
except AdminProfile.DoesNotExist:
self.permission_denied_message = "You must be an administrator to access this area."
return self.handle_no_permission()

View file

@ -1,10 +1,28 @@
from core.classes.mail import SMTPMailProvider from core.classes.mail import SMTPMailProvider
import importlib import importlib
import pathlib
import os.path
import logging
from django.conf import settings from django.conf import settings
providers = { "smtp": SMTPMailProvider } providers = { "smtp": SMTPMailProvider }
templates = {}
logger = logging.getLogger(__name__)
for module in settings.EXPEPHALON_MODULES + [""]:
for template in pathlib.Path(os.path.join(settings.BASE_DIR, module, "templates/mail/")).rglob("*.*"):
if os.path.isfile(template):
template_name = str(template).rsplit("templates/mail/")[-1].rsplit(".")[0]
template_format = str(template).rsplit(".")[-1].lower()
if not template_name in templates.keys():
templates[template_name] = dict()
if template_format in templates[template_name].keys():
logger.warning("Mail Template %s, that was seen at %s, was also found at %s. Using latter.",
template_name, templates[template_name][template_format], str(template))
templates[template_name][template_format] = str(template)
for module in settings.EXPEPHALON_MODULES: for module in settings.EXPEPHALON_MODULES:
try: try:

View file

@ -10,6 +10,7 @@ from core.views import (
LogoutView, LogoutView,
OTPValidatorView, OTPValidatorView,
PWResetView, PWResetView,
PWRequestView,
BackendNotImplementedView, BackendNotImplementedView,
AdminListView, AdminListView,
AdminDeleteView, AdminDeleteView,
@ -29,7 +30,8 @@ URLPATTERNS.append(path('login/', LoginView.as_view(), name="login"))
URLPATTERNS.append(path('login/otp/select/', OTPSelectorView.as_view(), name="otpselector")) URLPATTERNS.append(path('login/otp/select/', OTPSelectorView.as_view(), name="otpselector"))
URLPATTERNS.append(path('login/otp/validate/', OTPValidatorView.as_view(), name="otpvalidator")) URLPATTERNS.append(path('login/otp/validate/', OTPValidatorView.as_view(), name="otpvalidator"))
URLPATTERNS.append(path('logout/', LogoutView.as_view(), name="logout")) URLPATTERNS.append(path('logout/', LogoutView.as_view(), name="logout"))
URLPATTERNS.append(path('login/reset/', PWResetView.as_view(), name="pwreset")) URLPATTERNS.append(path('login/reset/', PWRequestView.as_view(), name="pwrequest"))
URLPATTERNS.append(path('login/reset/<pk>/', PWResetView.as_view(), name="pwreset"))
# Base Backend URLs # Base Backend URLs

View file

@ -5,6 +5,8 @@ from django.conf import settings
from core.views.dbsettings import * from core.views.dbsettings import *
from core.views.auth import * from core.views.auth import *
from core.views.profiles import * from core.views.profiles import *
from core.views.generic import *
from core.mixins.auth import AdminMixin
# Create your views here. # Create your views here.
@ -16,7 +18,7 @@ class IndexView(TemplateView):
context["title"] = "Home" context["title"] = "Home"
return context return context
class DashboardView(TemplateView): class DashboardView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/index.html" template_name = f"{settings.EXPEPHALON_BACKEND}/index.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -24,10 +26,10 @@ class DashboardView(TemplateView):
context["title"] = "Dashboard" context["title"] = "Dashboard"
return context return context
class BackendNotImplementedView(TemplateView): class BackendNotImplementedView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/notimplemented.html" template_name = f"{settings.EXPEPHALON_BACKEND}/notimplemented.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Oops!" context["title"] = "Oops!"
return context return context

View file

@ -9,7 +9,7 @@ from django.utils import timezone
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm
from core.models.auth import LoginSession, PWResetToken from core.models.auth import LoginSession, PWResetToken
from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name
from core.helpers.mail import send_mail from core.helpers.mail import simple_send_mail
from core.helpers.auth import generate_pwreset_mail from core.helpers.auth import generate_pwreset_mail
from dbsettings.functions import getValue from dbsettings.functions import getValue
@ -164,7 +164,10 @@ class PWRequestView(FormView):
try: try:
user = get_user_model().objects.get(username=form.cleaned_data["email"]) user = get_user_model().objects.get(username=form.cleaned_data["email"])
token = PWResetToken.objects.create(user=user) token = PWResetToken.objects.create(user=user)
mail = generate_pwreset_mail(user, token)
finally: simple_send_mail("Password Reset", mail, user.email)
messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.") except:
return redirect("login") raise
# finally:
# messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.")
# return redirect("login")

17
core/views/generic.py Normal file
View file

@ -0,0 +1,17 @@
from django.views.generic import TemplateView, ListView, CreateView, FormView, DeleteView
from core.mixins.auth import AdminMixin
class BackendTemplateView(AdminMixin, TemplateView):
pass
class BackendListView(AdminMixin, ListView):
pass
class BackendCreateView(AdminMixin, CreateView):
pass
class BackendFormView(AdminMixin, FormView):
pass
class BackendDeleteView(AdminMixin, DeleteView):
pass

View file

@ -1,10 +1,10 @@
from django.conf import settings from django.conf import settings
from django.views.generic import FormView, ListView, DeleteView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from core.models import AdminProfile from core.models import AdminProfile
from core.forms import AdminEditForm from core.forms import AdminEditForm
from core.views.generic import BackendFormView as FormView, BackendListView as ListView, BackendDeleteView as DeleteView
class AdminListView(ListView): class AdminListView(ListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html" template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html"

View file

@ -16,7 +16,6 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
@app.task(bind=True) @app.task(bind=True)
def debug_task(self): def debug_task(self):
print('Request: {0!r}'.format(self.request)) print('Request: {0!r}'.format(self.request))

View file

@ -1,5 +1,7 @@
import os import os
from django.urls import reverse_lazy
from expephalon.custom_settings import * # pylint: disable=unused-wildcard-import from expephalon.custom_settings import * # pylint: disable=unused-wildcard-import
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -141,6 +143,14 @@ if MEMCACHED_LOCATION:
CELERY_TASK_SERIALIZER = "pickle" CELERY_TASK_SERIALIZER = "pickle"
CELERY_RESULT_SERIALIZER = "pickle" CELERY_RESULT_SERIALIZER = "pickle"
CELERY_ACCEPT_CONTENT = ['pickle'] CELERY_ACCEPT_CONTENT = ['pickle']
CELERY_CACHE_BACKEND = 'default' CELERY_RESULT_BACKEND = 'django-db'
CELERY_CACHE_BACKEND = 'django-cache'
CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_LOCATION}/{RABBITMQ_VHOST}" CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_LOCATION}/{RABBITMQ_VHOST}"
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_TASK_RESULT_EXPIRES = 12 * 60 * 60
# Auth URLs
LOGIN_REDIRECT_URL = reverse_lazy('dashboard')
LOGIN_URL = reverse_lazy('login')
LOGOUT_URL = reverse_lazy('logout')

View file

@ -1,11 +0,0 @@
Hi {{ first_name }},
Somebody (hopefully you) requested a new password for your {{ sitename }} account. If this was you, please click the following link to reset your password:
{{ link }}
If it was not you, you can ignore this message. The link will expire in 24 hours.
Best regards
Your {{ sitename }} Team

View file

@ -11,3 +11,4 @@ git+https://kumig.it/kumisystems/django-dbsettings.git
celery celery
django-celery-results django-celery-results
django-celery-beat django-celery-beat
python-memcached

View file

@ -24,7 +24,7 @@
<div class="position-relative form-check"><input name="check" id="exampleCheck" type="checkbox" class="form-check-input"><label for="exampleCheck" class="form-check-label">Keep me logged in</label></div> <div class="position-relative form-check"><input name="check" id="exampleCheck" type="checkbox" class="form-check-input"><label for="exampleCheck" class="form-check-label">Keep me logged in</label></div>
<div class="divider row"></div> <div class="divider row"></div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="ml-auto"><a href="javascript:void(0);" class="btn-lg btn btn-link">Recover Password</a> <div class="ml-auto"><a href="{% url "pwrequest" %}" class="btn-lg btn btn-link">Recover Password</a>
<button class="btn btn-primary btn-lg">Login to Dashboard</button> <button class="btn btn-primary btn-lg">Login to Dashboard</button>
</div> </div>
</div> </div>

View file

View file

@ -0,0 +1,11 @@
Hi {§first_name§},
Somebody (hopefully you) requested a new password for your {§sitename§} account. If this was you, please click the following link to reset your password:
{§link§}
If it was not you, you can ignore this message. The link will expire in 24 hours.
Best regards
Your {§sitename§} Team