Use persistent SMTP connection for bulk emails, add List-Unsubscribe header

This commit is contained in:
rubenwardy 2022-02-12 14:06:04 +00:00
parent 8ad066409c
commit 770d17b42a
4 changed files with 38 additions and 22 deletions

@ -21,7 +21,7 @@ from wtforms import TextAreaField, SubmitField, StringField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
from app.markdown import render_markdown from app.markdown import render_markdown
from app.tasks.emails import send_user_email from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk
from app.utils import rank_required, addAuditLog from app.utils import rank_required, addAuditLog
from . import bp from . import bp
from ...models import UserRank, User, AuditSeverity from ...models import UserRank, User, AuditSeverity
@ -70,8 +70,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.isnot(None)).all(): task_send_bulk.delay(form.subject.data, text, html)
send_user_email.delay(user.email, user.locale or "en", form.subject.data, text, html)
return redirect(url_for("admin.admin_page")) return redirect(url_for("admin.admin_page"))

@ -309,6 +309,11 @@ class EmailSubscription(db.Model):
self.blacklisted = False self.blacklisted = False
self.token = None self.token = None
@property
def url(self):
from ..utils import abs_url_for
return abs_url_for('users.unsubscribe', token=self.token)
class NotificationType(enum.Enum): class NotificationType(enum.Enum):
# Package / release / etc # Package / release / etc

@ -14,9 +14,10 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict
from flask import render_template, escape from flask import render_template, escape
from flask_babel import force_locale, gettext from flask_babel import force_locale, gettext, lazy_gettext
from flask_mail import Message from flask_mail import Message
from app import mail from app import mail
from app.models import Notification, db, EmailSubscription, User from app.models import Notification, db, EmailSubscription, User
@ -36,6 +37,10 @@ def get_email_subscription(email):
return ret return ret
def gen_headers(sub: EmailSubscription) -> Dict[str,str]:
return {"List-Help": f"<{abs_url_for('flatpage', path='help/faq/')}>", "List-Unsubscribe": f"<{sub.url}>"}
@celery.task() @celery.task()
def send_verify_email(email, token, locale): def send_verify_email(email, token, locale):
sub = get_email_subscription(email) sub = get_email_subscription(email)
@ -43,7 +48,7 @@ def send_verify_email(email, token, locale):
return return
with force_locale(locale or "en"): with force_locale(locale or "en"):
msg = Message("Confirm email address", recipients=[email]) msg = Message("Confirm email address", recipients=[email], extra_headers=gen_headers(sub))
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)
@ -67,7 +72,7 @@ def send_unsubscribe_verify(email, locale):
return return
with force_locale(locale or "en"): with force_locale(locale or "en"):
msg = Message("Confirm unsubscribe", recipients=[email]) msg = Message("Confirm unsubscribe", recipients=[email], extra_headers=gen_headers(sub))
msg.body = """ msg.body = """
We're sorry to see you go. You just need to do one more thing before your email is blacklisted. We're sorry to see you go. You just need to do one more thing before your email is blacklisted.
@ -80,33 +85,33 @@ def send_unsubscribe_verify(email, locale):
@celery.task(rate_limit="25/m") @celery.task(rate_limit="25/m")
def send_email_with_reason(email: str, locale: str, subject: str, text: str, html: str, reason: str): def send_email_with_reason(email: str, locale: str, subject: str, text: str, html: str, reason: str, conn: any):
sub = get_email_subscription(email) sub = get_email_subscription(email)
if sub.blacklisted: if sub.blacklisted:
return return
with force_locale(locale or "en"): with force_locale(locale or "en"):
from flask_mail import Message msg = Message(subject, recipients=[email], extra_headers=gen_headers(sub))
msg = Message(subject, recipients=[email])
msg.body = text msg.body = text
html = html or f"<pre>{escape(text)}</pre>" html = html or f"<pre>{escape(text)}</pre>"
msg.html = render_template("emails/base.html", subject=subject, content=html, reason=reason, sub=sub) msg.html = render_template("emails/base.html", subject=subject, content=html, reason=reason, sub=sub)
if conn:
conn.send(msg)
else:
mail.send(msg) mail.send(msg)
@celery.task(rate_limit="25/m") @celery.task(rate_limit="25/m")
def send_user_email(email: str, locale: str, subject: str, text: str, html=None): def send_user_email(email: str, locale: str, subject: str, text: str, html=None, conn=None):
with force_locale(locale or "en"):
return send_email_with_reason(email, locale, subject, text, html, return send_email_with_reason(email, locale, subject, text, html,
gettext("You are receiving this email because you are a registered user of ContentDB.")) lazy_gettext("You are receiving this email because you are a registered user of ContentDB."), conn)
@celery.task(rate_limit="25/m") @celery.task(rate_limit="25/m")
def send_anon_email(email: str, locale: str, subject: str, text: str, html=None): def send_anon_email(email: str, locale: str, subject: str, text: str, html=None):
with force_locale(locale or "en"):
return send_email_with_reason(email, locale, subject, text, html, return send_email_with_reason(email, locale, subject, text, html,
gettext("You are receiving this email because someone (hopefully you) entered your email address as a user's email.")) lazy_gettext("You are receiving this email because someone (hopefully you) entered your email address as a user's email."))
def send_single_email(notification, locale): def send_single_email(notification, locale):
@ -115,7 +120,7 @@ def send_single_email(notification, locale):
return return
with force_locale(locale or "en"): with force_locale(locale or "en"):
msg = Message(notification.title, recipients=[notification.user.email]) msg = Message(notification.title, recipients=[notification.user.email], extra_headers=gen_headers(sub))
msg.body = """ msg.body = """
New notification: {} New notification: {}
@ -187,3 +192,10 @@ def send_pending_notifications():
send_notification_digest(to_send, user.locale or "en") send_notification_digest(to_send, user.locale or "en")
elif len(to_send) > 0: elif len(to_send) > 0:
send_single_email(to_send[0], user.locale or "en") send_single_email(to_send[0], user.locale or "en")
@celery.task()
def send_bulk_email(subject: str, text: str, html=None):
with mail.connect() as conn:
for user in User.query.filter(User.email.isnot(None)).all():
send_user_email(user.email, user.locale or "en", subject, text, html, conn)

@ -59,7 +59,7 @@
<p> <p>
{% block footer %} {% block footer %}
{{ reason }} <br> {{ reason }} <br>
<a href="{{ abs_url_for('users.unsubscribe', token=sub.token) }}"> <a href="{{ sub.url }}">
{{ _("Unsubscribe") }} {{ _("Unsubscribe") }}
</a> </a>
{% endblock %} {% endblock %}