diff --git a/esp/esp/dbmail/admin.py b/esp/esp/dbmail/admin.py
index b8954b825a..c02d4e58d4 100644
--- a/esp/esp/dbmail/admin.py
+++ b/esp/esp/dbmail/admin.py
@@ -61,8 +61,8 @@ class MessageRequestAdmin(admin.ModelAdmin):
admin_site.register(MessageRequest, MessageRequestAdmin)
class TextOfEmailAdmin(admin.ModelAdmin):
- list_display = ('id', 'send_from', 'send_to', 'subject', 'sent')
- search_fields = ('=id', 'send_from', 'send_to', 'subject')
+ list_display = ('id', 'send_from', 'send_to', 'subject', 'sent', 'user')
+ search_fields = ('=id', 'send_from', 'send_to', 'subject', 'user')
date_hierarchy = 'sent'
list_filter = ('send_from',)
admin_site.register(TextOfEmail, TextOfEmailAdmin)
diff --git a/esp/esp/dbmail/migrations/0006_textofemail_user.py b/esp/esp/dbmail/migrations/0006_textofemail_user.py
new file mode 100644
index 0000000000..f2740b3c46
--- /dev/null
+++ b/esp/esp/dbmail/migrations/0006_textofemail_user.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-02-08 20:52
+from __future__ import unicode_literals
+
+from django.db import migrations
+import django.db.models.deletion
+import esp.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0034_auto_20230525_2024'),
+ ('dbmail', '0005_auto_20220719_2056'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='textofemail',
+ name='user',
+ field=esp.db.fields.AjaxForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.ESPUser'),
+ ),
+ ]
diff --git a/esp/esp/dbmail/models.py b/esp/esp/dbmail/models.py
index 3279fcae88..42d13461ac 100644
--- a/esp/esp/dbmail/models.py
+++ b/esp/esp/dbmail/models.py
@@ -52,22 +52,37 @@
from django.conf import settings
from django.contrib.sites.models import Site
+from django.utils.html import strip_tags
from django.core.mail import get_connection
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.core.mail.message import sanitize_address
from django.core.exceptions import ImproperlyConfigured
+# `user` is required for marketing and subscribed messages to add unsubscribe headers
+# this includes all comm panel emails
+# https://support.google.com/a/answer/81126?visit_id=638428689824104778-3542874255&rd=1#subscriptions
def send_mail(subject, message, from_email, recipient_list, fail_silently=False, bcc=None,
- return_path=settings.DEFAULT_EMAIL_ADDRESSES['bounces'], extra_headers={},
+ return_path=settings.DEFAULT_EMAIL_ADDRESSES['bounces'], extra_headers={}, user=None,
*args, **kwargs):
from_email = from_email.strip()
+ # the from_email must match one of our DMARC domains/subdomains
+ # or the email may be rejected by email clients
+ if not re.match(r"(^.+@%s>?$)|(^.+@(\w+\.)?learningu\.org>?$)" % settings.SITE_INFO[1].replace(".", "\."), from_email):
+ raise ESPError("Invalid 'From' email address (" + from_email + "). The 'From' email address must " +
+ "end in @" + settings.SITE_INFO[1] + " (your website), " +
+ "@learningu.org, or a valid subdomain of learningu.org " +
+ "(i.e., @subdomain.learningu.org).")
+
if 'Reply-To' in extra_headers:
extra_headers['Reply-To'] = extra_headers['Reply-To'].strip()
if isinstance(recipient_list, basestring):
new_list = [ recipient_list ]
else:
new_list = [ x for x in recipient_list ]
+ if user is not None:
+ extra_headers['List-Unsubscribe-Post'] = "List-Unsubscribe=One-Click"
+ extra_headers['List-Unsubscribe'] = '<%s>' % (user.unsubscribe_oneclick())
# remove duplicate email addresses (sendgrid doesn't like them)
recipients = []
@@ -83,17 +98,24 @@ def send_mail(subject, message, from_email, recipient_list, fail_silently=False,
recipients.append(x)
emails.remove(tmp)
- from django.core.mail import EmailMessage #send_mail as django_send_mail
+ from django.core.mail import EmailMessage, EmailMultiAlternatives #send_mail as django_send_mail
logger.info("Sent mail to %s", recipients)
# Get whatever type of email connection Django provides.
# Normally this will be SMTP, but it also has an in-memory backend for testing.
connection = get_connection(fail_silently=fail_silently, return_path=return_path)
- msg = EmailMessage(subject, message, from_email, recipients, bcc=bcc, connection=connection, headers=extra_headers)
# Detect HTML tags in message and change content-type if they are found
if '' in message:
- msg.content_subtype = 'html'
+ # Generate a plaintext version of the email
+ # Remove html tags and continuous whitespaces
+ text_only = re.sub('[ \t]+', ' ', strip_tags(message))
+ # Strip single spaces in the beginning of each line
+ message_text = text_only.replace('\n ', '\n').strip()
+ msg = EmailMultiAlternatives(subject, message_text, from_email, recipients, bcc=bcc, connection=connection, headers=extra_headers)
+ msg.attach_alternative(message, "text/html")
+ else:
+ msg = EmailMessage(subject, message, from_email, recipients, bcc=bcc, connection=connection, headers=extra_headers)
msg.send()
@@ -350,6 +372,7 @@ def process(self):
newemailrequest = {'target': user, 'msgreq': self}
send_to = ESPUser.email_sendto_address(*address_pair)
newtxt = {
+ 'user': user,
'send_to': send_to,
'send_from': send_from,
'subject': subject,
@@ -383,6 +406,7 @@ def process(self):
class TextOfEmail(models.Model):
""" Contains the processed form of an EmailRequest, ready to be sent. SmartText becomes plain text. """
+ user = AjaxForeignKey(ESPUser, blank=True, null=True) # blank=True because there isn't an easy way to backfill this
send_to = models.CharField(max_length=1024) # Valid email address, "Name"
send_from = models.CharField(max_length=1024) # Valid email address
subject = models.TextField() # Email subject; plain text
@@ -430,7 +454,8 @@ def send(self):
self.send_from,
self.send_to,
False,
- extra_headers=extra_headers)
+ extra_headers=extra_headers,
+ user = self.user)
except Exception as e:
self.tries += 1
self.save()
diff --git a/esp/esp/local_settings.py.travis b/esp/esp/local_settings.py.travis
index 8c3aae3b82..bd5c376b6a 100644
--- a/esp/esp/local_settings.py.travis
+++ b/esp/esp/local_settings.py.travis
@@ -48,6 +48,10 @@ SITE_INFO = (1, 'testserver.learningu.org', 'ESP Test Server')
CACHE_PREFIX = "Test"
SECRET_KEY = 'b36cfdfb78aba27ddbde330b23352bc6d40973b4'
+SERVER_EMAIL = 'server@testserver.learningu.org'
+EMAIL_HOST = 'testserver.learningu.org'
+EMAIL_HOST_SENDER = 'testserver.learningu.org'
+
###################
# Plugin settings #
###################
diff --git a/esp/esp/program/forms.py b/esp/esp/program/forms.py
index b8f10d08d1..f4881275a1 100644
--- a/esp/esp/program/forms.py
+++ b/esp/esp/program/forms.py
@@ -36,6 +36,7 @@
import re
import unicodedata
+from django.conf import settings
from esp.users.models import StudentInfo, K12School, RecordType
from esp.program.models import Program, ProgramModule, ClassFlag, ClassFlagType, ClassCategories
from esp.dbmail.models import PlainRedirect
@@ -150,9 +151,10 @@ class Meta:
'program_modules': forms.SelectMultiple(attrs={'class': 'hidden-field'}),
}
model = Program
-ProgramCreationForm.base_fields['director_email'].widget = forms.TextInput(attrs={'size': 40})
-ProgramCreationForm.base_fields['director_cc_email'].widget = forms.TextInput(attrs={'size': 40})
-ProgramCreationForm.base_fields['director_confidential_email'].widget = forms.TextInput(attrs={'size': 40})
+ProgramCreationForm.base_fields['director_email'].widget = forms.EmailInput(attrs={'size': 40,
+ 'pattern': r'(^.+@%s$)|(^.+@(\w+\.)+learningu\.org$)' % settings.SITE_INFO[1].replace('.', '\.')})
+ProgramCreationForm.base_fields['director_cc_email'].widget = forms.EmailInput(attrs={'size': 40})
+ProgramCreationForm.base_fields['director_confidential_email'].widget = forms.EmailInput(attrs={'size': 40})
'''
ProgramCreationForm.base_fields['term'].line_group = -4
ProgramCreationForm.base_fields['term_friendly'].line_group = -4
diff --git a/esp/esp/program/migrations/0027_auto_20240220_2124.py b/esp/esp/program/migrations/0027_auto_20240220_2124.py
new file mode 100644
index 0000000000..ad627b6874
--- /dev/null
+++ b/esp/esp/program/migrations/0027_auto_20240220_2124.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2024-02-20 21:24
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.core import validators
+from django.db import migrations, models
+import django.db.models.fields
+import re
+
+def replace_director_emails(apps, schema_editor):
+ Program = apps.get_model('program', 'Program')
+ for prog in Program.objects.all():
+ if not re.match(r"(^.+@%s$)|(^.+@(\w+\.)*learningu\.org$)" % settings.SITE_INFO[1].replace(".", "\."), prog.director_email):
+ prog.director_email = 'info@' + settings.SITE_INFO[1]
+ prog.save()
+
+def create_info_redirect(apps, schema_editor):
+ PlainRedirect = apps.get_model('dbmail', 'PlainRedirect')
+ prs = PlainRedirect.objects.filter(original = "info")
+ if not prs.exists():
+ redirect = PlainRedirect.objects.create(original = "info", destination = settings.DEFAULT_EMAIL_ADDRESSES['default'])
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('program', '0026_auto_20221122_2100'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='program',
+ name='director_email',
+ field=models.EmailField(default=b'info@' + settings.SITE_INFO[1], max_length=75,
+ help_text=b'The director email address must end in @' + settings.SITE_INFO[1] +
+ ' (your website), @learningu.org, or a valid subdomain of learningu.org (i.e., @subdomain.learningu.org). The default is info@' + settings.SITE_INFO[1] +
+ ', which redirects to the "default" email address from your site\'s settings by default. You can create and manage your email redirects here.',
+ validators=[validators.RegexValidator(r'(^.+@%s$)|(^.+@(\w+\.)?learningu\.org$)' % settings.SITE_INFO[1].replace('.', '\.'))]),
+ ),
+ # This will run backwards, but won't do anything
+ migrations.RunPython(replace_director_emails, lambda a, s: None),
+ migrations.RunPython(create_info_redirect, lambda a, s: None),
+ ]
diff --git a/esp/esp/program/models/__init__.py b/esp/esp/program/models/__init__.py
index c30cc1d353..f8935dc995 100644
--- a/esp/esp/program/models/__init__.py
+++ b/esp/esp/program/models/__init__.py
@@ -249,7 +249,14 @@ class Program(models.Model, CustomFormsLinkModel):
name = models.CharField(max_length=80)
grade_min = models.IntegerField()
grade_max = models.IntegerField()
- director_email = models.EmailField(max_length=75) # director contact email address used for from field and display
+ # director contact email address used for from field and display
+ director_email = models.EmailField(default='info@' + settings.SITE_INFO[1], max_length=75,
+ validators=[validators.RegexValidator(r'(^.+@%s$)|(^.+@(\w+\.)?learningu\.org$)' % settings.SITE_INFO[1].replace('.', '\.'))],
+ help_text=mark_safe('The director email address must end in @' + settings.SITE_INFO[1] + ' (your website), ' +
+ '@learningu.org, or a valid subdomain of learningu.org (i.e., @subdomain.learningu.org). ' +
+ 'The default is info@' + settings.SITE_INFO[1] + ', which redirects to the "default" ' +
+ 'email address from your site\'s settings by default. ' +
+ 'You can create and manage your email redirects here.'))
director_cc_email = models.EmailField(blank=True, default='', max_length=75, help_text=mark_safe('If set, automated outgoing mail (except class cancellations) will be sent to this address instead of the director email. Use this if you do not want to spam the director email with teacher class registration emails. Otherwise, leave this field blank.')) # "carbon-copy" address for most automated outgoing mail to or CC'd to directors (except class cancellations)
director_confidential_email = models.EmailField(blank=True, default='', max_length=75, help_text='If set, confidential emails such as financial aid applications will be sent to this address instead of the director email.')
program_size_max = models.IntegerField(null=True, help_text='Set to 0 for no cap. Student registration performance is best when no cap is set.')
diff --git a/esp/esp/program/modules/forms/admincore.py b/esp/esp/program/modules/forms/admincore.py
index 1042cae5c9..300f6aba54 100644
--- a/esp/esp/program/modules/forms/admincore.py
+++ b/esp/esp/program/modules/forms/admincore.py
@@ -1,5 +1,6 @@
from decimal import Decimal
from django import forms
+from django.conf import settings
from django.contrib import admin
from django.template import Template, Context
from django.template.loader import select_template
@@ -93,6 +94,7 @@ class Meta:
'program_modules': forms.SelectMultiple(attrs={'class': 'hidden-field'}),
}
model = Program
+ProgramSettingsForm.base_fields['director_email'].widget = forms.EmailInput(attrs={'pattern': r'(^.+@%s$)|(^.+@(\w+\.)?learningu\.org$)' % settings.SITE_INFO[1].replace('.', '\.')})
class TeacherRegSettingsForm(BetterModelForm):
""" Form for changing teacher class registration settings. """
diff --git a/esp/esp/program/modules/handlers/commmodule.py b/esp/esp/program/modules/handlers/commmodule.py
index b7528b4cee..25cb8bd548 100644
--- a/esp/esp/program/modules/handlers/commmodule.py
+++ b/esp/esp/program/modules/handlers/commmodule.py
@@ -35,7 +35,7 @@
from esp.program.modules.base import ProgramModuleObj, needs_admin, main_call, aux_call
from esp.program.modules.handlers.listgenmodule import ListGenModule
from esp.utils.web import render_to_response
-from esp.dbmail.models import MessageRequest
+from esp.dbmail.models import MessageRequest, PlainRedirect
from esp.users.models import ESPUser, PersistentQueryFilter
from esp.users.controllers.usersearch import UserSearchController
from esp.users.views.usersearch import get_user_checklist
@@ -45,6 +45,8 @@
from esp.middleware import ESPError
from django.template import loader
+import re
+
class CommModule(ProgramModuleObj):
doc = """Email users that match specific search criteria."""
""" Want to email all ESP students within a 60 mile radius of NYC?
@@ -79,9 +81,17 @@ def commprev(self, request, tl, one, two, module, extra, prog):
# Set From address
if request.POST.get('from', '').strip():
fromemail = request.POST['from']
+ if not re.match(r"(^.+@%s$)|(^.+@(\w+\.)?learningu\.org$)" % settings.SITE_INFO[1].replace(".", "\."), fromemail):
+ raise ESPError("Invalid 'From' email address. The 'From' email address must " +
+ "end in @" + settings.SITE_INFO[1] + " (your website), " +
+ "@learningu.org, or a valid subdomain of learningu.org " +
+ "(i.e., @subdomain.learningu.org).")
else:
- # String together an address like username@esp.mit.edu
- fromemail = '%s@%s' % (request.user.username, settings.SITE_INFO[1])
+ # Use the info redirect (make one for the default email address if it doesn't exist)
+ prs = PlainRedirect.objects.filter(original = "info")
+ if not prs.exists():
+ redirect = PlainRedirect.objects.create(original = "info", destination = settings.DEFAULT_EMAIL_ADDRESSES['default'])
+ fromemail = '%s@%s' % ("info", settings.SITE_INFO[1])
# Set Reply-To address
if request.POST.get('replyto', '').strip():
@@ -241,6 +251,7 @@ def commpanel_old(self, request, tl, one, two, module, extra, prog):
@main_call
@needs_admin
def commpanel(self, request, tl, one, two, module, extra, prog):
+ from django.conf import settings
usc = UserSearchController()
@@ -267,6 +278,11 @@ def commpanel(self, request, tl, one, two, module, extra, prog):
context['sendto_fn_name'] = sendto_fn_name
context['listcount'] = self.approx_num_of_recipients(filterObj, sendto_fn)
context['selected'] = selected
+ # Use the info redirect (make one for the default email address if it doesn't exist)
+ prs = PlainRedirect.objects.filter(original = "info")
+ if not prs.exists():
+ redirect = PlainRedirect.objects.create(original = "info", destination = settings.DEFAULT_EMAIL_ADDRESSES['default'])
+ context['from'] = '%s@%s' % ("info", settings.SITE_INFO[1])
return render_to_response(self.baseDir()+'step2.html', request, context)
## Prepare a message starting from an earlier request
diff --git a/esp/esp/program/modules/handlers/teachereventsmodule.py b/esp/esp/program/modules/handlers/teachereventsmodule.py
index 42a39b8e39..524c87cb4c 100644
--- a/esp/esp/program/modules/handlers/teachereventsmodule.py
+++ b/esp/esp/program/modules/handlers/teachereventsmodule.py
@@ -41,6 +41,7 @@
from esp.users.models import ESPUser, UserAvailability
from esp.middleware.threadlocalrequest import get_current_request
from django.contrib.auth.models import Group
+from django.conf import settings
class TeacherEventsModule(ProgramModuleObj):
doc = """Allows teachers to sign up for one or more teacher events (e.g. interviews, training)."""
@@ -132,8 +133,8 @@ def event_signup(self, request, tl, one, two, module, extra, prog):
send_mail('['+self.program.niceName()+'] Teacher Interview for ' + request.user.first_name + ' ' + request.user.last_name + ': ' + event_name, \
"""Teacher Interview Registration Notification\n--------------------------------- \n\nTeacher: %s %s\n\nTime: %s\n\n""" % \
(request.user.first_name, request.user.last_name, event_name) , \
- (request.user.get_email_sendto_address()), \
- [self.program.getDirectorCCEmail()], True)
+ '%s Registration System ' % (self.program.program_type, settings.EMAIL_HOST_SENDER), \
+ [self.program.getDirectorCCEmail()], True, extra_headers = {'Reply-To': request.user.get_email_sendto_address()})
# Register for training
if data['training']:
diff --git a/esp/esp/program/modules/tests/commpanel.py b/esp/esp/program/modules/tests/commpanel.py
index 920d8fca3d..bfd3e44dc1 100644
--- a/esp/esp/program/modules/tests/commpanel.py
+++ b/esp/esp/program/modules/tests/commpanel.py
@@ -90,8 +90,8 @@ def runTest(self):
post_data = {
'subject': 'Test Subject 123',
'body': 'Test Body 123',
- 'from': 'from@email-server',
- 'replyto': 'replyto@email-server',
+ 'from': 'info@testserver.learningu.org',
+ 'replyto': 'replyto@testserver.learningu.org',
'filterid': filterid,
}
response = self.client.post('/manage/%s/%s' % (self.program.getUrlBase(), 'commfinal'), post_data)
@@ -113,8 +113,8 @@ def runTest(self):
msg = mail.outbox[0]
self.assertEqual(msg.subject, 'Test Subject 123')
self.assertEqual(msg.body, 'Test Body 123')
- self.assertEqual(msg.from_email, 'from@email-server')
- self.assertEqual(msg.extra_headers.get('Reply-To', ''), 'replyto@email-server')
+ self.assertEqual(msg.from_email, 'info@testserver.learningu.org')
+ self.assertEqual(msg.extra_headers.get('Reply-To', ''), 'replyto@testserver.learningu.org')
# Check that the MessageRequest was marked as processed
m = MessageRequest.objects.filter(recipients__id=filterid, subject='Test Subject 123')
diff --git a/esp/esp/program/tests.py b/esp/esp/program/tests.py
index 8b1df8b1f5..1b05f0814a 100644
--- a/esp/esp/program/tests.py
+++ b/esp/esp/program/tests.py
@@ -328,7 +328,7 @@ def makeprogram(self):
'term_friendly': 'Winter 3001',
'grade_min': '7',
'grade_max': '12',
- 'director_email': '123456789-223456789-323456789-423456789-523456789-623456789-7234567@mit.edu',
+ 'director_email': 'info@test.learningu.org',
'program_size_max': '3000',
'program_type': 'Prubbogrubbam!',
'program_modules': [x.id for x in ProgramModule.objects.all()],
@@ -604,7 +604,7 @@ def setUp(self, *args, **kwargs):
'term_friendly': settings['program_instance_label'],
'grade_min': '7',
'grade_max': '12',
- 'director_email': '123456789-223456789-323456789-423456789-523456789-623456789-7234567@mit.edu',
+ 'director_email': 'info@test.learningu.org',
'program_size_max': '3000',
'program_type': settings['program_type'],
'program_modules': settings['modules'],
@@ -767,7 +767,7 @@ def create_past_program(self):
'term_friendly': 'Spring 1901',
'grade_min': '7',
'grade_max': '12',
- 'director_email': '123456789-223456789-323456789-423456789-523456789-623456789-7234568@mit.edu',
+ 'director_email': 'info@test.learningu.org',
'program_size_max': '3000',
'program_type': 'TestProgramPast',
'program_modules': [x.id for x in ProgramModule.objects.all()],
diff --git a/esp/esp/users/models/__init__.py b/esp/esp/users/models/__init__.py
index a37f08169d..b2b18355f8 100644
--- a/esp/esp/users/models/__init__.py
+++ b/esp/esp/users/models/__init__.py
@@ -64,6 +64,8 @@
from django.core import urlresolvers
from django.utils.functional import SimpleLazyObject
from django.utils.safestring import mark_safe
+from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
+from django.urls import reverse
from esp.cal.models import Event, EventType
@@ -386,6 +388,8 @@ def get_msg_vars(self, otheruser, key):
(settings.DEFAULT_HOST, otheruser.password)
elif key == 'recover_query':
return "?code=%s" % otheruser.password
+ elif key == 'unsubscribe_link':
+ return otheruser.unsubscribe_link_full()
return u''
def getTaughtPrograms(self):
@@ -1142,6 +1146,32 @@ def userHash(self, program):
user_hash = hash(str(self.id) + str(program.id))
return '{0:06d}'.format(abs(user_hash))[:6]
+ # modified from here: https://www.grokcode.com/819/one-click-unsubscribes-for-django-apps/
+ def unsubscribe_link(self):
+ username, token = self.make_token().split(":", 1)
+ return reverse('unsubscribe', kwargs={'username': username, 'token': token,})
+
+ def unsubscribe_link_full(self):
+ unsub_link = self.unsubscribe_link()
+ return 'https://%s%s' % (Site.objects.get_current().domain, unsub_link)
+
+ # this is an insecure version that accepts a POST from external sources
+ def unsubscribe_oneclick(self):
+ unsub_link = self.unsubscribe_link()
+ unsub_link = unsub_link.replace("unsubscribe", "unsubscribe_oneclick")
+ return 'http://%s%s' % (Site.objects.get_current().domain, unsub_link)
+
+ def make_token(self):
+ return TimestampSigner().sign(self.username)
+
+ def check_token(self, token):
+ try:
+ key = '%s:%s' % (self.username, token)
+ TimestampSigner().unsign(key, max_age=60 * 60 * 24 * 7) # Valid for 7 days
+ except (BadSignature, SignatureExpired):
+ return False
+ return True
+
class ESPUser(User, BaseESPUser):
""" Create a user of the ESP Website
This user extends the auth.User of django"""
@@ -2816,7 +2846,7 @@ def send_confirmation_email(self):
subject, message = self._confirmation_email_content()
send_mail(subject,
message,
- settings.DEFAULT_FROM_EMAIL,
+ 'info@' + settings.SITE_INFO[1],
[self.requesting_student.email, ])
def get_admin_url(self):
diff --git a/esp/esp/users/urls.py b/esp/esp/users/urls.py
index fff6586c9b..3d0cf7eb22 100644
--- a/esp/esp/users/urls.py
+++ b/esp/esp/users/urls.py
@@ -22,12 +22,14 @@
name='esp.users.views.resend_activation_view'),
url(r'^signout/?$', views.signout),
url(r'^signedout/?$', views.signed_out_message),
- url(r'^login/?$', views.login_checked),
+ url(r'^login/?$', views.login_checked, name="login"),
url(r'^disableaccount/?$', views.disable_account),
url(r'^grade_change_request/?$', GradeChangeRequestView.as_view(), name = 'grade_change_request'),
url(r'^makeadmin/?$', views.make_admin),
url(r'^loginhelp', TemplateView.as_view(template_name='users/loginhelp.html'), name='Login Help'),
url(r'^morph/?$', views.morph_into_user),
+ url(r'^unsubscribe/(?P[\w.@+-]+)/(?P[\w.:\-_=]+)/$', views.unsubscribe, name="unsubscribe"),
+ url(r'^unsubscribe_oneclick/(?P[\w.@+-]+)/(?P[\w.:\-_=]+)/$', views.unsubscribe_oneclick, name="unsubscribe_oneclick"),
]
urlpatterns += [
diff --git a/esp/esp/users/views/__init__.py b/esp/esp/users/views/__init__.py
index 68d2e09f5a..1a7494b09c 100644
--- a/esp/esp/users/views/__init__.py
+++ b/esp/esp/users/views/__init__.py
@@ -4,6 +4,8 @@
from django.contrib.auth.views import login
from django.http import HttpResponseRedirect, HttpResponse
from django.template import RequestContext
+from django.urls import reverse
+from django.views.decorators.csrf import csrf_exempt
from esp.program.models import Program, RegistrationProfile
from esp.tagdict.models import Tag
@@ -162,6 +164,54 @@ def disable_account(request):
return render_to_response('users/disable_account.html', request, context)
+# modified from here: https://www.grokcode.com/819/one-click-unsubscribes-for-django-apps/
+def unsubscribe(request, username, token, oneclick = False):
+ """
+ User is immediately unsubscribed if they are logged in as username, or
+ if they came from an unexpired unsubscribe link. Otherwise, they are
+ redirected to the login page and unsubscribed as soon as they log in.
+ """
+
+ # render our own error message if the username doesn't match
+ users = ESPUser.objects.filter(username=username)
+ if users.exists():
+ users_active = users.filter(is_active=True)
+ if users_active.exists():
+ user = users_active[0]
+ else:
+ raise ESPError("User " + users[0].username + " is already unsubscribed.")
+ else:
+ raise ESPError("No user matching that unsubscribe request.")
+
+ # if POSTing, they clicked the confirm button
+ # if oneclick=True, then they came here from an email client
+ if request.POST.get("List-Unsubscribe") == "One-Click" or oneclick == True:
+ # "unsubscribe" them (deactivate their account)
+ user.is_active = False
+ user.save()
+ return render_to_response('users/unsubscribe.html', request, context = {'user': user, 'deactivated': True})
+
+ # otherwise show them a confirmation button
+ # if they are logged into the correct account or the token is valid
+ if ( (request.user.is_authenticated() and request.user == user) or user.check_token(token)):
+ return render_to_response('users/unsubscribe.html', request, context = {'user': user})
+ # if they are logged into a different account
+ # tell them to log out and try again
+ elif request.user.is_authenticated() and request.user != user:
+ raise ESPError("You are logged into a different account than the one you are trying to unsubscribe. Please log out and try your request again.")
+ # otherwise they will need to log in (or find a more recent link)
+ # so show the login page (with a custom alert message)
+ else:
+ next_url = reverse('unsubscribe', kwargs={'username': username, 'token': token,})
+ return HttpResponseRedirect('%s?next=%s' % (reverse('login'), next_url))
+
+# have an email client (etc) POST to this view to process a
+# "oneclick" unsubscribe
+@csrf_exempt
+def unsubscribe_oneclick(request, username, token):
+ if request.POST.get("List-Unsubscribe") == "One-Click":
+ return unsubscribe(request, username, token, oneclick = True)
+ raise ESPError("Invalid oneclick data.")
@admin_required
def morph_into_user(request):
diff --git a/esp/esp/web/templatetags/main.py b/esp/esp/web/templatetags/main.py
index 0fd161f3df..2fd85a6ba0 100644
--- a/esp/esp/web/templatetags/main.py
+++ b/esp/esp/web/templatetags/main.py
@@ -62,6 +62,10 @@ def bool_and(obj1,obj2):
def get_field(object, field):
return getattr(object, field)
+@register.filter
+def regexsite(str):
+ return str.replace(".", "\.")
+
@register.filter
def extract_theme(url):
# Get the appropriate color scheme out of the Tag that controls nav structure
diff --git a/esp/templates/program/modules/commmodule/step2.html b/esp/templates/program/modules/commmodule/step2.html
index 1fd1d38a83..40c197092b 100644
--- a/esp/templates/program/modules/commmodule/step2.html
+++ b/esp/templates/program/modules/commmodule/step2.html
@@ -22,7 +22,7 @@
{% endblock %}
{% block content %}
-
+{% load main %}
Communications Portal
@@ -38,6 +38,14 @@ Step 2:
Tip: The default {{ settings.ORGANIZATION_SHORT_NAME }} template (which you can modify here) is automatically wrapped around your message below.
+
+
+Note, the "From" email address must end in
@{{ settings.SITE_INFO.1 }} (your website),
@learningu.org, or a valid subdomain of learningu.org (i.e.,
@subdomain.learningu.org).
+By default the "From" email address is
info@{{ current_site.domain }}, which redirects to the "default" email address from your site's settings.
+You can create and manage your email redirects
here.
+The "Reply-To" field can be any email address (by default it is the same as the "From" email address).
+
+