mirror of
https://github.com/minetest/contentdb.git
synced 2024-12-22 22:12:24 +01:00
Add unsubscribe
This commit is contained in:
parent
5fe3b0b459
commit
085f0b49c6
@ -55,7 +55,7 @@ def send_single_email():
|
|||||||
|
|
||||||
text = form.text.data
|
text = form.text.data
|
||||||
html = render_markdown(text)
|
html = render_markdown(text)
|
||||||
task = send_user_email.delay([user.email], form.subject.data, text, html)
|
task = send_user_email.delay(user.email, form.subject.data, text, html)
|
||||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||||
|
|
||||||
return render_template("admin/send_email.html", form=form, user=user)
|
return render_template("admin/send_email.html", form=form, user=user)
|
||||||
@ -72,7 +72,7 @@ def send_bulk_email():
|
|||||||
text = form.text.data
|
text = form.text.data
|
||||||
html = render_markdown(text)
|
html = render_markdown(text)
|
||||||
for user in User.query.filter(User.email != None).all():
|
for user in User.query.filter(User.email != None).all():
|
||||||
send_user_email.delay([user.email], form.subject.data, text, html)
|
send_user_email.delay(user.email, form.subject.data, text, html)
|
||||||
|
|
||||||
return redirect(url_for("admin.admin_page"))
|
return redirect(url_for("admin.admin_page"))
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from wtforms import *
|
|||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks.emails import sendVerifyEmail, send_anon_email
|
from app.tasks.emails import sendVerifyEmail, send_anon_email, sendUnsubscribeVerifyEmail
|
||||||
from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash, addAuditLog
|
from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash, addAuditLog
|
||||||
from passlib.pwd import genphrase
|
from passlib.pwd import genphrase
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ def register():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(email=form.email.data).first()
|
user = User.query.filter_by(email=form.email.data).first()
|
||||||
if user:
|
if user:
|
||||||
send_anon_email.delay([form.email.data], "Email already in use",
|
send_anon_email.delay(form.email.data, "Email already in use",
|
||||||
"We were unable to create the account as the email is already in use by {}. Try a different email address.".format(user.display_name))
|
"We were unable to create the account as the email is already in use by {}. Try a different email address.".format(user.display_name))
|
||||||
else:
|
else:
|
||||||
user = User(form.username.data, False, form.email.data, make_flask_login_password(form.password.data))
|
user = User(form.username.data, False, form.email.data, make_flask_login_password(form.password.data))
|
||||||
@ -159,7 +159,7 @@ def forgot_password():
|
|||||||
|
|
||||||
sendVerifyEmail.delay(form.email.data, token)
|
sendVerifyEmail.delay(form.email.data, token)
|
||||||
else:
|
else:
|
||||||
send_anon_email.delay([email], "Unable to find account", """
|
send_anon_email.delay(email, "Unable to find account", """
|
||||||
<p>
|
<p>
|
||||||
We were unable to perform the password reset as we could not find an account
|
We were unable to perform the password reset as we could not find an account
|
||||||
associated with this email.
|
associated with this email.
|
||||||
@ -296,3 +296,51 @@ def verify_email():
|
|||||||
return redirect(url_for("users.login"))
|
return redirect(url_for("users.login"))
|
||||||
else:
|
else:
|
||||||
return redirect(url_for("homepage.home"))
|
return redirect(url_for("homepage.home"))
|
||||||
|
|
||||||
|
|
||||||
|
class UnsubscribeForm(FlaskForm):
|
||||||
|
email = StringField("Email", [InputRequired(), Email()])
|
||||||
|
submit = SubmitField("Send")
|
||||||
|
|
||||||
|
|
||||||
|
def unsubscribe_verify():
|
||||||
|
form = UnsubscribeForm(request.form)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
email = form.email.data
|
||||||
|
sub = EmailSubscription.query.filter_by(email=email).first()
|
||||||
|
if not sub:
|
||||||
|
sub = EmailSubscription(email)
|
||||||
|
db.session.add(sub)
|
||||||
|
|
||||||
|
sub.token = randomString(32)
|
||||||
|
db.session.commit()
|
||||||
|
sendUnsubscribeVerifyEmail.delay(form.email.data)
|
||||||
|
|
||||||
|
flash("Check your email address to continue the unsubscribe", "success")
|
||||||
|
return redirect(url_for("homepage.home"))
|
||||||
|
|
||||||
|
return render_template("users/unsubscribe.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
|
def unsubscribe_manage(sub: EmailSubscription):
|
||||||
|
user = User.query.filter_by(email=sub.email).first()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
sub.blacklisted = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash("That email is now blacklisted. Please contact an admin if you wish to undo this.", "success")
|
||||||
|
return redirect(url_for("homepage.home"))
|
||||||
|
|
||||||
|
return render_template("users/unsubscribe.html", user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/unsubscribe/", methods=["GET", "POST"])
|
||||||
|
def unsubscribe():
|
||||||
|
token = request.args.get("token")
|
||||||
|
if token:
|
||||||
|
sub = EmailSubscription.query.filter_by(token=token).first()
|
||||||
|
if sub:
|
||||||
|
return unsubscribe_manage(sub)
|
||||||
|
|
||||||
|
return unsubscribe_verify()
|
||||||
|
@ -82,7 +82,8 @@ class FlaskMailHandler(logging.Handler):
|
|||||||
|
|
||||||
text = self.format(record) if self.formatter else None
|
text = self.format(record) if self.formatter else None
|
||||||
html = self.html_formatter.format(record) if self.html_formatter else None
|
html = self.html_formatter.format(record) if self.html_formatter else None
|
||||||
send_user_email.delay(self.send_to, self.getSubject(record), text, html)
|
for email in self.send_to:
|
||||||
|
send_user_email.delay(email, self.getSubject(record), text, html)
|
||||||
|
|
||||||
|
|
||||||
def register_mail_error_handler(app, mailer):
|
def register_mail_error_handler(app, mailer):
|
||||||
|
@ -25,9 +25,11 @@ from flask_migrate import Migrate
|
|||||||
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||||
from sqlalchemy_searchable import SearchQueryMixin, make_searchable
|
from sqlalchemy_searchable import SearchQueryMixin, make_searchable
|
||||||
from sqlalchemy_utils.types import TSVectorType
|
from sqlalchemy_utils.types import TSVectorType
|
||||||
from . import app, gravatar, login_manager
|
|
||||||
|
from . import app, gravatar
|
||||||
|
|
||||||
# Initialise database
|
# Initialise database
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
make_searchable(db.metadata)
|
make_searchable(db.metadata)
|
||||||
@ -276,6 +278,18 @@ class UserEmailVerification(db.Model):
|
|||||||
is_password_reset = db.Column(db.Boolean, nullable=False, default=False)
|
is_password_reset = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailSubscription(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(100), nullable=False, unique=True)
|
||||||
|
blacklisted = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
token = db.Column(db.String(32), nullable=True, default=None)
|
||||||
|
|
||||||
|
def __init__(self, email):
|
||||||
|
self.email = email
|
||||||
|
self.blacklisted = False
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(enum.Enum):
|
class NotificationType(enum.Enum):
|
||||||
# Package / release / etc
|
# Package / release / etc
|
||||||
PACKAGE_EDIT = 1
|
PACKAGE_EDIT = 1
|
||||||
@ -385,7 +399,7 @@ class Notification(db.Model):
|
|||||||
|
|
||||||
class UserNotificationPreferences(db.Model):
|
class UserNotificationPreferences(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
user = db.relationship("User", back_populates="notification_preferences")
|
user = db.relationship("User", back_populates="notification_preferences")
|
||||||
|
|
||||||
# 2 = immediate emails
|
# 2 = immediate emails
|
||||||
|
@ -18,14 +18,30 @@
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from app import mail
|
from app import mail
|
||||||
from app.models import Notification, db
|
from app.models import Notification, db, EmailSubscription
|
||||||
from app.tasks import celery
|
from app.tasks import celery
|
||||||
from app.utils import abs_url_for, abs_url
|
from app.utils import abs_url_for, abs_url, randomString
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_subscription(email):
|
||||||
|
assert type(email) == str
|
||||||
|
ret = EmailSubscription.query.filter_by(email=email).first()
|
||||||
|
if not ret:
|
||||||
|
ret = EmailSubscription(email)
|
||||||
|
ret.token = randomString(32)
|
||||||
|
db.session.add(ret)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def sendVerifyEmail(newEmail, token):
|
def sendVerifyEmail(email, token):
|
||||||
msg = Message("Confirm email address", recipients=[newEmail])
|
sub = get_email_subscription(email)
|
||||||
|
if sub.blacklisted:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = Message("Confirm email address", recipients=[email])
|
||||||
|
|
||||||
msg.body = """
|
msg.body = """
|
||||||
This email has been sent to you because someone (hopefully you)
|
This email has been sent to you because someone (hopefully you)
|
||||||
@ -38,34 +54,60 @@ def sendVerifyEmail(newEmail, token):
|
|||||||
{}
|
{}
|
||||||
""".format(abs_url_for('users.verify_email', token=token))
|
""".format(abs_url_for('users.verify_email', token=token))
|
||||||
|
|
||||||
msg.html = render_template("emails/verify.html", token=token)
|
msg.html = render_template("emails/verify.html", token=token, sub=sub)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def send_email_with_reason(to, subject, text, html, reason):
|
def sendUnsubscribeVerifyEmail(email):
|
||||||
|
sub = get_email_subscription(email)
|
||||||
|
if sub.blacklisted:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = Message("Confirm unsubscribe", recipients=[email])
|
||||||
|
|
||||||
|
msg.body = """
|
||||||
|
We're sorry to see you go. You just need to do one more thing before your email is blacklisted.
|
||||||
|
|
||||||
|
Click this link to blacklist email: {}
|
||||||
|
""".format(abs_url_for('users.unsubscribe', token=sub.token))
|
||||||
|
|
||||||
|
msg.html = render_template("emails/verify_unsubscribe.html", sub=sub)
|
||||||
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def send_email_with_reason(email, subject, text, html, reason):
|
||||||
|
sub = get_email_subscription(email)
|
||||||
|
if sub.blacklisted:
|
||||||
|
return
|
||||||
|
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
msg = Message(subject, recipients=to)
|
msg = Message(subject, recipients=[email])
|
||||||
|
|
||||||
msg.body = text
|
msg.body = text
|
||||||
html = html or text
|
html = html or text
|
||||||
msg.html = render_template("emails/base.html", subject=subject, content=html, reason=reason)
|
msg.html = render_template("emails/base.html", subject=subject, content=html, reason=reason, sub=sub)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def send_user_email(to, subject, text, html=None):
|
def send_user_email(email: str, subject: str, text: str, html=None):
|
||||||
return send_email_with_reason(to, subject, text, html,
|
return send_email_with_reason(email, subject, text, html,
|
||||||
"You are receiving this email because you are a registered user of ContentDB.")
|
"You are receiving this email because you are a registered user of ContentDB.")
|
||||||
|
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def send_anon_email(to, subject, text, html=None):
|
def send_anon_email(email: str, subject: str, text: str, html=None):
|
||||||
return send_email_with_reason(to, subject, text, html,
|
return send_email_with_reason(email, subject, text, html,
|
||||||
"You are receiving this email because someone (hopefully you) entered your email address as a user's email.")
|
"You are receiving this email because someone (hopefully you) entered your email address as a user's email.")
|
||||||
|
|
||||||
|
|
||||||
def sendNotificationEmail(notification):
|
def sendNotificationEmail(notification):
|
||||||
|
sub = get_email_subscription(notification.user.email)
|
||||||
|
if sub.blacklisted:
|
||||||
|
return
|
||||||
|
|
||||||
msg = Message(notification.title, recipients=[notification.user.email])
|
msg = Message(notification.title, recipients=[notification.user.email])
|
||||||
|
|
||||||
msg.body = """
|
msg.body = """
|
||||||
@ -74,10 +116,12 @@ def sendNotificationEmail(notification):
|
|||||||
View: {}
|
View: {}
|
||||||
|
|
||||||
Manage email settings: {}
|
Manage email settings: {}
|
||||||
|
Unsubscribe: {}
|
||||||
""".format(notification.title, abs_url(notification.url),
|
""".format(notification.title, abs_url(notification.url),
|
||||||
abs_url_for("users.email_notifications", username=notification.user.username))
|
abs_url_for("users.email_notifications", username=notification.user.username),
|
||||||
|
abs_url_for("users.unsubscribe", token=sub.token))
|
||||||
|
|
||||||
msg.html = render_template("emails/notification.html", notification=notification)
|
msg.html = render_template("emails/notification.html", notification=notification, sub=sub)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,7 +58,10 @@
|
|||||||
<div style="margin-top: 3em;font-size: 80%;color: #666;">
|
<div style="margin-top: 3em;font-size: 80%;color: #666;">
|
||||||
<p>
|
<p>
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
{{ reason }}
|
{{ reason }} <br>
|
||||||
|
<a href="{{ abs_url_for('users.unsubscribe', token=sub.token) }}">
|
||||||
|
{{ _("Unsubscribe") }}
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -24,8 +24,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
You are receiving this email because you are a registered user of ContentDB, and have email notifications enabled.<br>
|
You are receiving this email because you are a registered user of ContentDB,
|
||||||
|
and have email notifications enabled. <br>
|
||||||
|
|
||||||
<a href="{{ abs_url_for('users.email_notifications', username=notification.user.username) }}">
|
<a href="{{ abs_url_for('users.email_notifications', username=notification.user.username) }}">
|
||||||
{{ _("Manage your preferences") }}
|
{{ _("Manage your preferences") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
<a href="{{ abs_url_for('users.unsubscribe', token=sub.token) }}">
|
||||||
|
{{ _("Unsubscribe") }}
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -21,11 +21,14 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p style="font-size: 80%;">
|
<p style="font-size: 80%;">
|
||||||
Or paste this into your browser: {{ abs_url_for('users.verify_email', token=token) }}
|
Or paste this into your browser: <code>{{ abs_url_for('users.verify_email', token=token) }}</code>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
You are receiving this email because someone (hopefully you) entered your email address as a user's email.
|
You are receiving this email because someone (hopefully you) entered your email address as a user's email. <br>
|
||||||
|
<a href="{{ abs_url_for('users.unsubscribe', token=sub.token) }}">
|
||||||
|
{{ _("Unsubscribe") }}
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
22
app/templates/emails/verify_unsubscribe.html
Normal file
22
app/templates/emails/verify_unsubscribe.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "emails/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 style="margin-top: 0;">Hello!</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
We're sorry to see you go. You just need to do one more thing before your email is blacklisted.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn" href="{{ abs_url_for('users.unsubscribe', token=sub.token) }}">
|
||||||
|
Unsubscribe
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p style="font-size: 80%;">
|
||||||
|
Or paste this into your browser: <code>{{ abs_url_for('users.unsubscribe', token=sub.token) }}</code>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
You are receiving this email because someone (hopefully you) entered your email address in the unsubscribe form.
|
||||||
|
{% endblock %}
|
65
app/templates/users/unsubscribe.html
Normal file
65
app/templates/users/unsubscribe.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ _("Unsubscribe") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ self.title() }}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ _("This will blacklist an email address, preventing ContentDB from ever sending emails to it - including password resets.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if form %}
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
<form action="" method="POST" class="form" role="form">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ _("Please enter the email address you wish to blacklist.") }}
|
||||||
|
{{ _("You will then need to confirm the email") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ render_field(form.email, tabindex=220) }}
|
||||||
|
|
||||||
|
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="" method="POST" class="form" role="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ _("You may now unsubscribe.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if user %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>
|
||||||
|
<strong>Unsubscribing may prevent you from being able to sign into the
|
||||||
|
account '{{ user.display_name }}'</strong>.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
ContentDB will no longer be able to send "forget password" and other essential system emails.
|
||||||
|
Consider editing your email notification preferences instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="alert alert-warning">
|
||||||
|
You won't be able to use this email with ContentDB anymore.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="button-group mt-4">
|
||||||
|
{% if user %}
|
||||||
|
<a class="btn btn-primary mr-3" href="{{ url_for('users.email_notifications', username=user.username) }}">
|
||||||
|
Edit Notification Preferences
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<input class="btn btn-danger" type="submit" value="Unsubscribe">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
35
migrations/versions/06d23947e7ef_.py
Normal file
35
migrations/versions/06d23947e7ef_.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 06d23947e7ef
|
||||||
|
Revises: 5d7233cf8a00
|
||||||
|
Create Date: 2020-12-05 20:30:12.166357
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '06d23947e7ef'
|
||||||
|
down_revision = '5d7233cf8a00'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('email_subscription',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('blacklisted', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=32), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('email')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('email_subscription')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in New Issue
Block a user