Merge pull request #4 from kirsle/feature/comment-deletion
User-editable Comments, HTML Emails and Admin Quick-delete
This commit is contained in:
commit
84d5b4a2cd
10
defaults.yml
10
defaults.yml
|
@ -74,6 +74,9 @@ rophako:
|
||||||
# key. Do NOT use that one, it was just an example. Make your own!
|
# key. Do NOT use that one, it was just an example. Make your own!
|
||||||
secret_key: 'for the love of Arceus, change this key!'
|
secret_key: 'for the love of Arceus, change this key!'
|
||||||
|
|
||||||
|
# How long the session key should last for (in days).
|
||||||
|
session_lifetime: 30
|
||||||
|
|
||||||
# Password strength: number of iterations for bcrypt password.
|
# Password strength: number of iterations for bcrypt password.
|
||||||
bcrypt_iterations: 12
|
bcrypt_iterations: 12
|
||||||
|
|
||||||
|
@ -141,10 +144,15 @@ rophako:
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
time_format: *DATE_FORMAT
|
time_format: *DATE_FORMAT
|
||||||
|
|
||||||
# We use Gravatar for comments if the user provides an e-mail address.
|
# We use Gravatar for comments if the user provides an e-mail address.
|
||||||
# Specify the URL to a fallback image to use in case they don't have
|
# Specify the URL to a fallback image to use in case they don't have
|
||||||
# a gravatar.
|
# a gravatar.
|
||||||
default_avatar:
|
default_avatar: ""
|
||||||
|
|
||||||
|
# The grace period window that users are allowed to modify or delete their
|
||||||
|
# own comments (in hours)
|
||||||
|
edit_period: 2
|
||||||
|
|
||||||
wiki:
|
wiki:
|
||||||
default_page: Main Page
|
default_page: Main Page
|
||||||
|
|
|
@ -80,6 +80,10 @@ Emoticons.load_theme()
|
||||||
def before_request():
|
def before_request():
|
||||||
"""Called before all requests. Initialize global template variables."""
|
"""Called before all requests. Initialize global template variables."""
|
||||||
|
|
||||||
|
# Session lifetime.
|
||||||
|
app.permanent_session_lifetime = datetime.timedelta(days=Config.security.session_lifetime)
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
# Default template vars.
|
# Default template vars.
|
||||||
g.info = rophako.utils.default_vars()
|
g.info = rophako.utils.default_vars()
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
"""Commenting models."""
|
"""Commenting models."""
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for, session
|
||||||
|
from itsdangerous import URLSafeSerializer
|
||||||
import time
|
import time
|
||||||
import hashlib
|
import hashlib
|
||||||
import urllib
|
import urllib
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
from rophako.settings import Config
|
from rophako.settings import Config
|
||||||
import rophako.jsondb as JsonDB
|
import rophako.jsondb as JsonDB
|
||||||
|
@ -18,18 +19,39 @@ import rophako.model.emoticons as Emoticons
|
||||||
from rophako.utils import send_email, render_markdown
|
from rophako.utils import send_email, render_markdown
|
||||||
from rophako.log import logger
|
from rophako.log import logger
|
||||||
|
|
||||||
|
def deletion_token():
|
||||||
|
"""Retrieves the comment deletion token for the current user's session.
|
||||||
|
|
||||||
def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
|
Deletion tokens are random strings saved with a comment's data that allows
|
||||||
|
its original commenter to delete or modify their comment on their own,
|
||||||
|
within a window of time configurable by the site owner
|
||||||
|
(in ``comment.edit_period``).
|
||||||
|
|
||||||
|
If the current session doesn't have a deletion token yet, this function
|
||||||
|
will generate and set one. Otherwise it returns the one set last time.
|
||||||
|
All comments posted by the same session would share the same deletion
|
||||||
|
token.
|
||||||
|
"""
|
||||||
|
if not "comment_token" in session:
|
||||||
|
session["comment_token"] = str(uuid.uuid4())
|
||||||
|
return session.get("comment_token")
|
||||||
|
|
||||||
|
|
||||||
|
def add_comment(thread, uid, name, subject, message, url, time, ip,
|
||||||
|
token=None, image=None):
|
||||||
"""Add a comment to a comment thread.
|
"""Add a comment to a comment thread.
|
||||||
|
|
||||||
* uid is 0 if it's a guest post, otherwise the UID of the user.
|
Parameters:
|
||||||
* name is the commenter's name (if a guest)
|
thread (str): the unique comment thread name.
|
||||||
* subject is for the e-mails that are sent out
|
uid (int): 0 for guest posts, otherwise the UID of the logged-in user.
|
||||||
* message is self explanatory.
|
name (str): the commenter's name (if a guest)
|
||||||
* url is the URL where the comment can be read.
|
subject (str)
|
||||||
* time, epoch time of comment.
|
message (str)
|
||||||
* ip is the IP address of the commenter.
|
url (str): the URL where the comment can be read (i.e. the blog post)
|
||||||
* image is a Gravatar image URL etc.
|
time (int): epoch time of the comment.
|
||||||
|
ip (str): the user's IP address.
|
||||||
|
token (str): the user's session's comment deletion token.
|
||||||
|
image (str): the URL to a Gravatar image, if any.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get the comments for this thread.
|
# Get the comments for this thread.
|
||||||
|
@ -48,6 +70,7 @@ def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
|
||||||
message=message,
|
message=message,
|
||||||
time=time or int(time.time()),
|
time=time or int(time.time()),
|
||||||
ip=ip,
|
ip=ip,
|
||||||
|
token=token,
|
||||||
)
|
)
|
||||||
write_comments(thread, comments)
|
write_comments(thread, comments)
|
||||||
|
|
||||||
|
@ -60,20 +83,25 @@ def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
|
||||||
# Send the e-mail to the site admins.
|
# Send the e-mail to the site admins.
|
||||||
send_email(
|
send_email(
|
||||||
to=Config.site.notify_address,
|
to=Config.site.notify_address,
|
||||||
subject="New comment: {}".format(subject),
|
subject="Comment Added: {}".format(subject),
|
||||||
message="""{name} has left a comment on: {subject}
|
message="""{name} has left a comment on: {subject}
|
||||||
|
|
||||||
{message}
|
{message}
|
||||||
|
|
||||||
To view this comment, please go to {url}
|
-----
|
||||||
|
|
||||||
=====================
|
To view this comment, please go to <{url}>
|
||||||
|
|
||||||
This e-mail was automatically generated. Do not reply to it.""".format(
|
Was this comment spam? [Delete it]({deletion_link}).""".format(
|
||||||
name=name,
|
name=name,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
message=message,
|
message=message,
|
||||||
url=url,
|
url=url,
|
||||||
|
deletion_link=url_for("comment.quick_delete",
|
||||||
|
token=make_quick_delete_token(thread, cid),
|
||||||
|
url=url,
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,28 +116,25 @@ This e-mail was automatically generated. Do not reply to it.""".format(
|
||||||
subject="New Comment: {}".format(subject),
|
subject="New Comment: {}".format(subject),
|
||||||
message="""Hello,
|
message="""Hello,
|
||||||
|
|
||||||
You are currently subscribed to the comment thread '{thread}', and somebody has
|
|
||||||
just added a new comment!
|
|
||||||
|
|
||||||
{name} has left a comment on: {subject}
|
{name} has left a comment on: {subject}
|
||||||
|
|
||||||
{message}
|
{message}
|
||||||
|
|
||||||
To view this comment, please go to {url}
|
-----
|
||||||
|
|
||||||
=====================
|
To view this comment, please go to <{url}>""".format(
|
||||||
|
|
||||||
This e-mail was automatically generated. Do not reply to it.
|
|
||||||
|
|
||||||
If you wish to unsubscribe from this comment thread, please visit the following
|
|
||||||
URL: {unsub}""".format(
|
|
||||||
thread=thread,
|
thread=thread,
|
||||||
name=name,
|
name=name,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
message=message,
|
message=message,
|
||||||
url=url,
|
url=url,
|
||||||
unsub=unsub,
|
unsub=unsub,
|
||||||
)
|
),
|
||||||
|
footer="You received this e-mail because you subscribed to the "
|
||||||
|
"comment thread that this comment was added to. You may "
|
||||||
|
"[**unsubscribe**]({unsub}) if you like.".format(
|
||||||
|
unsub=unsub,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +159,84 @@ def delete_comment(thread, cid):
|
||||||
write_comments(thread, comments)
|
write_comments(thread, comments)
|
||||||
|
|
||||||
|
|
||||||
|
def make_quick_delete_token(thread, cid):
|
||||||
|
"""Generate a unique tamper-proof token for quickly deleting comments.
|
||||||
|
|
||||||
|
This allows for an instant 'delete' link to be included in the notification
|
||||||
|
e-mail sent to the site admins, to delete obviously spammy comments
|
||||||
|
quickly.
|
||||||
|
|
||||||
|
It uses ``itsdangerous`` to create a unique token signed by the site's
|
||||||
|
secret key so that users can't forge their own tokens.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
thread (str): comment thread name.
|
||||||
|
cid (str): unique comment ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str
|
||||||
|
"""
|
||||||
|
s = URLSafeSerializer(Config.security.secret_key)
|
||||||
|
return s.dumps(dict(
|
||||||
|
t=thread,
|
||||||
|
c=cid,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_quick_delete_token(token):
|
||||||
|
"""Validate and decode a quick delete token.
|
||||||
|
|
||||||
|
If the token is valid, returns a dict of the thread name and comment ID,
|
||||||
|
as keys ``t`` and ``c`` respectively.
|
||||||
|
|
||||||
|
If not valid, returns ``None``.
|
||||||
|
"""
|
||||||
|
s = URLSafeSerializer(Config.security.secret_key)
|
||||||
|
try:
|
||||||
|
return s.loads(token)
|
||||||
|
except:
|
||||||
|
logger.exception("Failed to validate quick-delete token {}".format(token))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_editable(thread, cid, comment=None):
|
||||||
|
"""Determine if the comment is editable by the end user.
|
||||||
|
|
||||||
|
A comment is editable to its own author (even guests) for a window defined
|
||||||
|
by the site owner. In this event, the user's session has their
|
||||||
|
'comment deletion token' that matches the comment's saved token, and the
|
||||||
|
comment was posted recently.
|
||||||
|
|
||||||
|
Site admins (any logged-in user) can always edit all comments.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
thread (str): the unique comment thread name.
|
||||||
|
cid (str): the comment ID.
|
||||||
|
comment (dict): if you already have the comment object, you can provide
|
||||||
|
it here and save an extra DB lookup.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the user is logged in *OR* has a valid deletion token and
|
||||||
|
the comment is relatively new. Otherwise returns False.
|
||||||
|
"""
|
||||||
|
# Logged in users can always do it.
|
||||||
|
if session["login"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Get the comment, or bail if not found.
|
||||||
|
if comment is None:
|
||||||
|
comment = get_comment(thread, cid)
|
||||||
|
if not comment:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure the comment's token matches the user's, or bail.
|
||||||
|
if comment.get("token", "x") != deletion_token():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# And finally, make sure the comment is new enough.
|
||||||
|
return time.time() - comment["time"] < 60*60*Config.comment.edit_period
|
||||||
|
|
||||||
|
|
||||||
def count_comments(thread):
|
def count_comments(thread):
|
||||||
"""Count the comments on a thread."""
|
"""Count the comments on a thread."""
|
||||||
comments = get_comments(thread)
|
comments = get_comments(thread)
|
||||||
|
|
|
@ -8,8 +8,7 @@ import time
|
||||||
|
|
||||||
import rophako.model.user as User
|
import rophako.model.user as User
|
||||||
import rophako.model.comment as Comment
|
import rophako.model.comment as Comment
|
||||||
from rophako.utils import (template, pretty_time, login_required, sanitize_name,
|
from rophako.utils import (template, pretty_time, sanitize_name, remote_addr)
|
||||||
remote_addr)
|
|
||||||
from rophako.plugin import load_plugin
|
from rophako.plugin import load_plugin
|
||||||
from rophako.settings import Config
|
from rophako.settings import Config
|
||||||
|
|
||||||
|
@ -42,9 +41,18 @@ def preview():
|
||||||
|
|
||||||
# Gravatar?
|
# Gravatar?
|
||||||
gravatar = Comment.gravatar(form["contact"])
|
gravatar = Comment.gravatar(form["contact"])
|
||||||
|
if g.info["session"]["login"]:
|
||||||
|
form["name"] = g.info["session"]["name"]
|
||||||
|
gravatar = "/".join([
|
||||||
|
Config.photo.root_public,
|
||||||
|
User.get_picture(uid=g.info["session"]["uid"]),
|
||||||
|
])
|
||||||
|
|
||||||
# Are they submitting?
|
# Are they submitting?
|
||||||
if form["action"] == "submit":
|
if form["action"] == "submit":
|
||||||
|
# Make sure they have a deletion token in their session.
|
||||||
|
token = Comment.deletion_token()
|
||||||
|
|
||||||
Comment.add_comment(
|
Comment.add_comment(
|
||||||
thread=thread,
|
thread=thread,
|
||||||
uid=g.info["session"]["uid"],
|
uid=g.info["session"]["uid"],
|
||||||
|
@ -55,6 +63,7 @@ def preview():
|
||||||
subject=form["subject"],
|
subject=form["subject"],
|
||||||
message=form["message"],
|
message=form["message"],
|
||||||
url=form["url"],
|
url=form["url"],
|
||||||
|
token=token,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Are we subscribing to the thread?
|
# Are we subscribing to the thread?
|
||||||
|
@ -77,19 +86,45 @@ def preview():
|
||||||
|
|
||||||
|
|
||||||
@mod.route("/delete/<thread>/<cid>")
|
@mod.route("/delete/<thread>/<cid>")
|
||||||
@login_required
|
|
||||||
def delete(thread, cid):
|
def delete(thread, cid):
|
||||||
"""Delete a comment."""
|
"""Delete a comment."""
|
||||||
|
if not Comment.is_editable(thread, cid):
|
||||||
|
flash("Permission denied; maybe you need to log in?")
|
||||||
|
return redirect(url_for("account.login"))
|
||||||
|
|
||||||
url = request.args.get("url")
|
url = request.args.get("url")
|
||||||
Comment.delete_comment(thread, cid)
|
Comment.delete_comment(thread, cid)
|
||||||
flash("Comment deleted!")
|
flash("Comment deleted!")
|
||||||
return redirect(url or url_for("index"))
|
return redirect(url or url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@mod.route("/quickdelete/<token>")
|
||||||
|
def quick_delete(token):
|
||||||
|
"""Quick-delete a comment.
|
||||||
|
|
||||||
|
This is for the site admins: when a comment is posted, the admins' version
|
||||||
|
of the email contains a quick deletion link in case of spam. The ``token``
|
||||||
|
here is in relation to that. It's a signed hash via ``itsdangerous`` using
|
||||||
|
the site's secret key so that users can't forge their own tokens.
|
||||||
|
"""
|
||||||
|
data = Comment.validate_quick_delete_token(token)
|
||||||
|
if data is None:
|
||||||
|
flash("Permission denied: token not valid.")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
url = request.args.get("url")
|
||||||
|
Comment.delete_comment(data["t"], data["c"])
|
||||||
|
flash("Comment has been quick-deleted!")
|
||||||
|
return redirect(url or url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@mod.route("/edit/<thread>/<cid>", methods=["GET", "POST"])
|
@mod.route("/edit/<thread>/<cid>", methods=["GET", "POST"])
|
||||||
@login_required
|
|
||||||
def edit(thread, cid):
|
def edit(thread, cid):
|
||||||
"""Edit an existing comment."""
|
"""Edit an existing comment."""
|
||||||
|
if not Comment.is_editable(thread, cid):
|
||||||
|
flash("Permission denied; maybe you need to log in?")
|
||||||
|
return redirect(url_for("account.login"))
|
||||||
|
|
||||||
url = request.args.get("url")
|
url = request.args.get("url")
|
||||||
comment = Comment.get_comment(thread, cid)
|
comment = Comment.get_comment(thread, cid)
|
||||||
if not comment:
|
if not comment:
|
||||||
|
@ -172,11 +207,14 @@ def unsubscribe():
|
||||||
def partial_index(thread, subject, header=True, addable=True):
|
def partial_index(thread, subject, header=True, addable=True):
|
||||||
"""Partial template for including the index view of a comment thread.
|
"""Partial template for including the index view of a comment thread.
|
||||||
|
|
||||||
* thread: unique name for the comment thread
|
Parameters:
|
||||||
* subject: subject name for the comment thread
|
thread (str): the unique name for the comment thread.
|
||||||
* header: show the Comments h1 header
|
subject (str): subject name for the comment thread.
|
||||||
* addable: boolean, can new comments be added to the thread"""
|
header (bool): show the 'Comments' H1 header.
|
||||||
|
addable (bool): can new comments be added to the thread?
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get all the comments on this thread.
|
||||||
comments = Comment.get_comments(thread)
|
comments = Comment.get_comments(thread)
|
||||||
|
|
||||||
# Sort the comments by most recent on bottom.
|
# Sort the comments by most recent on bottom.
|
||||||
|
@ -200,6 +238,9 @@ def partial_index(thread, subject, header=True, addable=True):
|
||||||
# Format the message for display.
|
# Format the message for display.
|
||||||
comment["formatted_message"] = Comment.format_message(comment["message"])
|
comment["formatted_message"] = Comment.format_message(comment["message"])
|
||||||
|
|
||||||
|
# Was this comment posted by the current user viewing it?
|
||||||
|
comment["editable"] = Comment.is_editable(thread, cid, comment)
|
||||||
|
|
||||||
sorted_comments.append(comment)
|
sorted_comments.append(comment)
|
||||||
|
|
||||||
g.info["header"] = header
|
g.info["header"] = header
|
||||||
|
|
|
@ -25,12 +25,18 @@ There {% if comments|length == 1 %}is{% else %}are{% endif %}
|
||||||
{{ comment["formatted_message"]|safe }}
|
{{ comment["formatted_message"]|safe }}
|
||||||
|
|
||||||
<div class="clear">
|
<div class="clear">
|
||||||
{% if session["login"] %}
|
{% if session["login"] or comment["editable"] %}
|
||||||
[IP: {{ comment["ip"] }}
|
[
|
||||||
|
{% if session["login"] %}
|
||||||
|
IP: {{ comment["ip"] }}
|
||||||
|
{% else %}
|
||||||
|
<em class="comment-editable">You recently posted this</em>
|
||||||
|
{% endif %}
|
||||||
|
|
|
|
||||||
<a href="{{ url_for('comment.edit', thread=thread, cid=comment['id'], url=url) }}">Edit</a>
|
<a href="{{ url_for('comment.edit', thread=thread, cid=comment['id'], url=url) }}">Edit</a>
|
||||||
|
|
|
|
||||||
<a href="{{ url_for('comment.delete', thread=thread, cid=comment['id'], url=url) }}" onclick="return window.confirm('Are you sure?')">Delete</a>]
|
<a href="{{ url_for('comment.delete', thread=thread, cid=comment['id'], url=url) }}" onclick="return window.confirm('Are you sure?')">Delete</a>
|
||||||
|
]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div><p>
|
</div><p>
|
||||||
|
|
|
@ -8,7 +8,7 @@ This is a preview of what your comment is going to look like once posted.<p>
|
||||||
|
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<div class="comment-author">
|
<div class="comment-author">
|
||||||
{% if contact %}
|
{% if gravatar %}
|
||||||
<img src="{{ gravatar }}" alt="Avatar" width="96" height="96">
|
<img src="{{ gravatar }}" alt="Avatar" width="96" height="96">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="/static/avatars/default.png" alt="guest" width="96" height="96">
|
<img src="/static/avatars/default.png" alt="guest" width="96" height="96">
|
||||||
|
|
|
@ -48,12 +48,12 @@ def send():
|
||||||
subject="Contact Form on {}: {}".format(Config.site.site_name, subject),
|
subject="Contact Form on {}: {}".format(Config.site.site_name, subject),
|
||||||
message="""A visitor to {site_name} has sent you a message!
|
message="""A visitor to {site_name} has sent you a message!
|
||||||
|
|
||||||
IP Address: {ip}
|
* IP Address: `{ip}`
|
||||||
User Agent: {ua}
|
* User Agent: `{ua}`
|
||||||
Referrer: {referer}
|
* Referrer: <{referer}>
|
||||||
Name: {name}
|
* Name: {name}
|
||||||
E-mail: {email}
|
* E-mail: <{email}>
|
||||||
Subject: {subject}
|
* Subject: {subject}
|
||||||
|
|
||||||
{message}""".format(
|
{message}""".format(
|
||||||
site_name=Config.site.site_name,
|
site_name=Config.site.site_name,
|
||||||
|
|
|
@ -236,14 +236,47 @@ def parse_anchors(html):
|
||||||
return toc
|
return toc
|
||||||
|
|
||||||
|
|
||||||
def send_email(to, subject, message, sender=None, reply_to=None):
|
def send_email(to, subject, message, header=None, footer=None, sender=None,
|
||||||
"""Send an e-mail out."""
|
reply_to=None):
|
||||||
|
"""Send a (markdown-formatted) e-mail out.
|
||||||
|
|
||||||
|
This will deliver an HTML-formatted e-mail (using the ``email.inc.html``
|
||||||
|
template) using the rendered Markdown contents of ``message`` and
|
||||||
|
``footer``. It will also send a plain text version using the raw Markdown
|
||||||
|
formatting in case the user can't accept HTML.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
to ([]str): list of addresses to send the message to.
|
||||||
|
subject (str): email subject and title.
|
||||||
|
message (str): the email body, in Markdown format.
|
||||||
|
header (str): the header text for the HTML email (plain text).
|
||||||
|
footer (str): optional email footer, in Markdown format. The default
|
||||||
|
footer is defined in the ``email.inc.html`` template.
|
||||||
|
sender (str): optional sender email address. Defaults to the one
|
||||||
|
specified in the site configuration.
|
||||||
|
reply_to (str): optional Reply-To address header.
|
||||||
|
"""
|
||||||
if sender is None:
|
if sender is None:
|
||||||
sender = Config.mail.sender
|
sender = Config.mail.sender
|
||||||
|
|
||||||
if type(to) != list:
|
if type(to) != list:
|
||||||
to = [to]
|
to = [to]
|
||||||
|
|
||||||
|
# Render the Markdown bits.
|
||||||
|
if footer:
|
||||||
|
footer = render_markdown(footer)
|
||||||
|
|
||||||
|
# Default header matches the subject.
|
||||||
|
if not header:
|
||||||
|
header = subject
|
||||||
|
|
||||||
|
html_message = render_template("email.inc.html",
|
||||||
|
title=subject,
|
||||||
|
header=header,
|
||||||
|
message=render_markdown(message),
|
||||||
|
footer=footer,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Send email to {}".format(to))
|
logger.info("Send email to {}".format(to))
|
||||||
if Config.mail.method == "smtp":
|
if Config.mail.method == "smtp":
|
||||||
# Send mail with SMTP.
|
# Send mail with SMTP.
|
||||||
|
@ -260,6 +293,9 @@ def send_email(to, subject, message, sender=None, reply_to=None):
|
||||||
text = MIMEText(message, "plain", "utf-8")
|
text = MIMEText(message, "plain", "utf-8")
|
||||||
msg.attach(text)
|
msg.attach(text)
|
||||||
|
|
||||||
|
html = MIMEText(html_message, "html", "utf-8")
|
||||||
|
msg.attach(html)
|
||||||
|
|
||||||
# Send the e-mail.
|
# Send the e-mail.
|
||||||
try:
|
try:
|
||||||
server = smtplib.SMTP(Config.mail.server, Config.mail.port)
|
server = smtplib.SMTP(Config.mail.server, Config.mail.port)
|
||||||
|
|
43
rophako/www/email.inc.html
Normal file
43
rophako/www/email.inc.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail -->
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
</head>
|
||||||
|
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;">
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000">
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000">
|
||||||
|
<b>{{ header }}</b>
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#FEFEFE">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
{{ message|safe }}
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||||
|
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||||
|
{% if footer %}
|
||||||
|
{{ footer|safe }}
|
||||||
|
{% else %}
|
||||||
|
This e-mail was automatically generated; do not reply to it.
|
||||||
|
{% endif %}
|
||||||
|
</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user