Skip to content

Commit

Permalink
feature: create config page, add redirect option
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Jan 4, 2024
1 parent 1015990 commit 413b68b
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 151 deletions.
72 changes: 63 additions & 9 deletions ckanext/mailcraft/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from __future__ import annotations

from typing import Any

import ckan.plugins.toolkit as tk
from ckan import types

CONF_TEST_CONN = "ckanext.mailcraft.test_conn_on_startup"
DEF_TEST_CONN = False
Expand All @@ -12,13 +17,12 @@
CONF_MAIL_PER_PAGE = "ckanext.mailcraft.mail_per_page"
DEF_MAIL_PER_PAGE = 20

CONF_SAVE_TO_DASHBOARD = "ckanext.mailcraft.save_to_dashboard"
DEF_SAVE_TO_DASHBOARD = False
CONF_REDIRECT_EMAILS_TO = "ckanext.mailcraft.redirect_emails_to"


def get_conn_timeout() -> int:
"""Return a timeout for an SMTP connection"""
return tk.asint(tk.config.get(CONF_CONN_TIMEOUT, DEF_CONN_TIMEOUT))
return tk.asint(tk.config.get(CONF_CONN_TIMEOUT) or DEF_CONN_TIMEOUT)


def is_startup_conn_test_enabled() -> bool:
Expand All @@ -35,9 +39,59 @@ def stop_outgoing_emails() -> bool:

def get_mail_per_page() -> int:
"""Return a number of mails to show per page"""
return tk.asint(tk.config.get(CONF_MAIL_PER_PAGE, DEF_MAIL_PER_PAGE))


def is_save_to_dashboard_enabled() -> bool:
"""Check if we are saving outgoing emails to dashboard"""
return tk.asbool(tk.config.get(CONF_SAVE_TO_DASHBOARD, DEF_SAVE_TO_DASHBOARD))
return tk.asint(tk.config.get(CONF_MAIL_PER_PAGE) or DEF_MAIL_PER_PAGE)


def get_redirect_email() -> str | None:
"""Redirect outgoing emails to a specified email"""
return tk.config.get(CONF_REDIRECT_EMAILS_TO)


def get_config_options() -> dict[str, dict[str, Any]]:
"""Defines how we are going to render the global configuration
options for an extension."""
unicode_safe = tk.get_validator("unicode_safe")
boolean_validator = tk.get_validator("boolean_validator")
default = tk.get_validator("default")
int_validator = tk.get_validator("is_positive_integer")
email_validator = tk.get_validator("email_validator")

return {
"smtp_test": {
"key": CONF_TEST_CONN,
"label": "Test SMTP connection on CKAN startup",
"value": is_startup_conn_test_enabled(),
"validators": [default(DEF_TEST_CONN), boolean_validator], # type: ignore
"type": "select",
"options": [{"value": 1, "text": "Yes"}, {"value": 0, "text": "No"}],
},
"timeout": {
"key": CONF_CONN_TIMEOUT,
"label": "SMTP connection timeout",
"value": get_conn_timeout(),
"validators": [default(DEF_CONN_TIMEOUT), int_validator], # type: ignore
"type": "number",
},
"stop_outgoing": {
"key": CONF_STOP_OUTGOING,
"label": "Stop outgoing emails",
"value": stop_outgoing_emails(),
"validators": [default(DEF_STOP_OUTGOING), boolean_validator], # type: ignore
"type": "select",
"options": [{"value": 1, "text": "Yes"}, {"value": 0, "text": "No"}],
},
"mail_per_page": {
"key": CONF_MAIL_PER_PAGE,
"label": "Number of emails per page",
"value": get_mail_per_page(),
"validators": [default(DEF_MAIL_PER_PAGE), int_validator], # type: ignore
"type": "number",
},
"redirect_to": {
"key": CONF_REDIRECT_EMAILS_TO,
"label": "Redirect outgoing emails to",
"value": get_redirect_email(),
"validators": [unicode_safe, email_validator],
"type": "text",
},
}
90 changes: 15 additions & 75 deletions ckanext/mailcraft/mailer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from __future__ import annotations
from abc import ABC, abstractmethod

import codecs
import os
import logging
import mimetypes
import smtplib
Expand All @@ -27,9 +25,8 @@
log = logging.getLogger(__name__)


class Mailer(ABC):
class BaseMailer(ABC):
def __init__(self):
# TODO: replace with ext config, instead of using core ones
self.server = tk.config["smtp.server"]
self.start_tls = tk.config["smtp.starttls"]
self.user = tk.config["smtp.user"]
Expand Down Expand Up @@ -78,32 +75,8 @@ def mail_user(
) -> None:
pass

@abstractmethod
def send_reset_link(self, user: model.User) -> None:
pass

@abstractmethod
def create_reset_key(self, user: model.User) -> None:
pass

@abstractmethod
def verify_reset_link(self, user: model.User, key: Optional[str]) -> bool:
pass

def save_to_dashboard(
self,
msg: EmailMessage,
body_html: str,
state: str = mc_model.Email.State.success,
extras: Optional[dict[str, Any]] = None,
) -> None:
if not mc_config.is_save_to_dashboard_enabled():
return

mc_model.Email.save_mail(msg, body_html, state, extras or {})


class DefaultMailer(Mailer):
class DefaultMailer(BaseMailer):
def mail_recipients(
self,
subject: str,
Expand Down Expand Up @@ -141,19 +114,20 @@ def mail_recipients(
self.add_attachments(msg, attachments)

try:
# print(msg.get_body(("html",)).get_content()) # type: ignore
if mc_config.stop_outgoing_emails():
self.save_to_dashboard(
self._save_email(
msg, body_html, mc_model.Email.State.stopped, dict(msg.items())
)
else:
self._send_email(recipients, msg)
except MailerException:
self.save_to_dashboard(
self._save_email(
msg, body_html, mc_model.Email.State.failed, dict(msg.items())
)
else:
if not mc_config.stop_outgoing_emails():
self.save_to_dashboard(msg, body_html)
self._save_email(msg, body_html)

def add_attachments(self, msg: EmailMessage, attachments) -> None:
"""Add attachments on an email message
Expand Down Expand Up @@ -210,6 +184,15 @@ def get_connection(self) -> smtplib.SMTP:

return conn

def _save_email(
self,
msg: EmailMessage,
body_html: str,
state: str = mc_model.Email.State.success,
extras: Optional[dict[str, Any]] = None,
) -> None:
mc_model.Email.save_mail(msg, body_html, state, extras or {})

def _send_email(self, recipients, msg: EmailMessage):
conn = self.get_connection()

Expand Down Expand Up @@ -253,46 +236,3 @@ def mail_user(
headers=headers,
attachments=attachments,
)

def send_reset_link(self, user: model.User) -> None:
self.create_reset_key(user)

body = self._get_reset_link_body(user)
body_html = self._get_reset_link_body(user, html=True)

# Make sure we only use the first line
subject = tk.render(
"mailcraft/emails/reset_password/subject.txt",
{"site_title": self.site_title},
).split("\n")[0]

self.mail_user(user.name, subject, body, body_html=body_html)

def create_reset_key(self, user: model.User):
user.reset_key = codecs.encode(os.urandom(16), "hex").decode()
model.repo.commit_and_remove()

def _get_reset_link_body(self, user: model.User, html: bool = False) -> str:
extra_vars = {
"reset_link": tk.url_for(
"user.perform_reset", id=user.id, key=user.reset_key, qualified=True
),
"site_title": self.site_title,
"site_url": self.site_url,
"user_name": user.name,
}

return tk.render(
(
"mailcraft/emails/reset_password/body.html"
if html
else "mailcraft/emails/reset_password/body.txt"
),
extra_vars,
)

def verify_reset_link(self, user: model.User, key: Optional[str]) -> bool:
if not key or not user.reset_key or len(user.reset_key) < 5:
return False

return key.strip() == user.reset_key
28 changes: 10 additions & 18 deletions ckanext/mailcraft/plugin.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
from __future__ import annotations

from typing import Union

from flask import Blueprint

import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
from ckan.common import CKANConfig

if plugins.plugin_loaded("admin_panel"):
import ckanext.admin_panel.types as ap_types
from ckanext.admin_panel.interfaces import IAdminPanel
import ckanext.ap_main.types as ap_types
from ckanext.ap_main.interfaces import IAdminPanel

import ckanext.mailcraft.config as mc_config
from ckanext.mailcraft.mailer import DefaultMailer


@toolkit.blanket.blueprints
@toolkit.blanket.actions
@toolkit.blanket.auth_functions
@toolkit.blanket.validators
class MailcraftPlugin(plugins.SingletonPlugin):
plugins.implements(plugins.IConfigurer)
plugins.implements(plugins.IConfigurable)

if plugins.plugin_loaded("admin_panel"):
plugins.implements(plugins.IBlueprint)
plugins.implements(IAdminPanel, inherit=True)
plugins.implements(IAdminPanel, inherit=True)

# IConfigurer

Expand All @@ -34,20 +27,19 @@ def update_config(self, config_):
toolkit.add_public_directory(config_, "public")
toolkit.add_resource("assets", "mailcraft")

def update_config_schema(self, schema):
for _, config in mc_config.get_config_options().items():
schema.update({config["key"]: config["validators"]})

return schema

# IConfigurable

def configure(self, config: CKANConfig) -> None:
if mc_config.is_startup_conn_test_enabled():
mailer = DefaultMailer()
mailer.test_conn()

# IBlueprint

def get_blueprint(self) -> Union[list[Blueprint], Blueprint]:
from ckanext.mailcraft.views import get_blueprints

return get_blueprints()

# IAdminPanel

def register_config_sections(
Expand Down
26 changes: 26 additions & 0 deletions ckanext/mailcraft/templates/mailcraft/config.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends 'admin_panel/base.html' %}

{% import 'macros/autoform.html' as autoform %}
{% import 'macros/form.html' as form %}

{% block breadcrumb_content %}
<li>{% link_for _("Global settings"), named_route='mailcraft.config' %}</li>
{% endblock breadcrumb_content %}

{% block ap_content %}
<h1>{{ _("Mailcraft configuration") }}</h1>

<form method="POST">
{% for _, config in configs.items() %}
{% if config.type == "select" %}
{{ form.select(config.key, label=config.label, options=config.options, selected=data[config.key] | o if data else config.value, error=errors[config.key]) }}
{% elif config.type in ("text", "number") %}
{{ form.input(config.key, label=config.label, value=data[config.key] if data else config.value, error=errors[config.key], type=config.type) }}
{% else %}
{{ form.textarea(config.key, label=config.label, value=data[config.key] if data else config.value, error=errors[config.key]) }}
{% endif %}
{% endfor %}

<button type="submit" class="btn btn-primary">{{ _('Update') }}</button>
</form>
{% endblock ap_content %}
2 changes: 2 additions & 0 deletions ckanext/mailcraft/templates/mailcraft/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
{% block ap_content %}
<h1>{{ _("Dashboard") }}</h1>

<a href="{{ h.url_for('mailcraft.test') }}" class="btn btn-primary mb-3">{{ _("Send test email") }}</a>

<div class="row g-3">
<form action="{{ h.url_for('mailcraft.dashboard') }}" method="POST">
{{ h.csrf_input() }}
Expand Down
2 changes: 1 addition & 1 deletion ckanext/mailcraft/templates/mailcraft/mail_read.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{% block ap_content %}
{% if mail.extras %}
<div class="mailcraft-mail-meta">
<h3>{{ _("Email headers") }}</h3>
<h3>{{ _("Email meta") }}</h3>
<ul>
{% for key, value in mail.extras.items() %}
<li><b>{{ key }}</b>: {{ value }}</li>
Expand Down
Loading

0 comments on commit 413b68b

Please sign in to comment.