diff --git a/common/templates/student/edx_ace/calendarsync/email/body.html b/common/templates/student/edx_ace/calendarsync/email/body.html
deleted file mode 100644
index 9ee009defc..0000000000
--- a/common/templates/student/edx_ace/calendarsync/email/body.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends 'ace_common/edx_ace/common/base_body.html' %}
-
-{% load django_markup %}
-{% load i18n %}
-{% load static %}
-{% block content %}
-
-
-
-
- {% filter force_escape %}{{ calendar_sync_headline }}{% endfilter %}
-
-
- {% filter force_escape %}
- {{ calendar_sync_body }}
- {% endfilter %}
-
-
- |
-
-
-{% endblock %}
diff --git a/common/templates/student/edx_ace/calendarsync/email/body.txt b/common/templates/student/edx_ace/calendarsync/email/body.txt
deleted file mode 100644
index d2e6f46bb1..0000000000
--- a/common/templates/student/edx_ace/calendarsync/email/body.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-{% load i18n %}{% autoescape off %}
-{{ calendar_sync_body }}
diff --git a/common/templates/student/edx_ace/calendarsync/email/from_name.txt b/common/templates/student/edx_ace/calendarsync/email/from_name.txt
deleted file mode 100644
index dcbc23c004..0000000000
--- a/common/templates/student/edx_ace/calendarsync/email/from_name.txt
+++ /dev/null
@@ -1 +0,0 @@
-{{ platform_name }}
diff --git a/common/templates/student/edx_ace/calendarsync/email/head.html b/common/templates/student/edx_ace/calendarsync/email/head.html
deleted file mode 100644
index 366ada7ad9..0000000000
--- a/common/templates/student/edx_ace/calendarsync/email/head.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends 'ace_common/edx_ace/common/base_head.html' %}
diff --git a/common/templates/student/edx_ace/calendarsync/email/subject.txt b/common/templates/student/edx_ace/calendarsync/email/subject.txt
deleted file mode 100644
index 7ef3d19b84..0000000000
--- a/common/templates/student/edx_ace/calendarsync/email/subject.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-{% load i18n %}
-{% autoescape off %}
-{{ calendar_sync_subject }}
-{% endautoescape %}
diff --git a/openedx/features/calendar_sync/docs/decisions/0001-calendar-sync-emails-using-ses.rst b/openedx/features/calendar_sync/docs/decisions/0001-calendar-sync-emails-using-ses.rst
new file mode 100644
index 0000000000..3491383abf
--- /dev/null
+++ b/openedx/features/calendar_sync/docs/decisions/0001-calendar-sync-emails-using-ses.rst
@@ -0,0 +1,24 @@
+1. Amazon SES for calendar sync emails
+================================================
+
+Status
+------
+
+Proposed
+
+Context
+-------
+
+For calendar sync functionality, we would like to send users
+emails with .ics file attachments, which will in turn allow the user
+to easily add/update course deadline dates on their personal calendars.
+
+Decision
+--------
+
+We will use Amazon SES to send these emails. Originally, we had hoped to use
+Sailthru, but found that file attachments were not supported. While emails
+with attachments are not currently sent from platform at the time this doc is
+being written, they are however sent from other services such as
+enterprise-data using Amazon SES. We will use a similar approach for our
+calendar sync feature.
diff --git a/openedx/features/calendar_sync/message_types.py b/openedx/features/calendar_sync/message_types.py
deleted file mode 100644
index 72af6e8009..0000000000
--- a/openedx/features/calendar_sync/message_types.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""
-ACE message types for the calendar_sync module.
-"""
-
-
-from openedx.core.djangoapps.ace_common.message import BaseMessageType
-
-
-class CalendarSync(BaseMessageType):
- def __init__(self, *args, **kwargs):
- super(CalendarSync, self).__init__(*args, **kwargs)
-
- self.options['transactional'] = True
diff --git a/openedx/features/calendar_sync/tasks.py b/openedx/features/calendar_sync/tasks.py
deleted file mode 100644
index 834d4e5c01..0000000000
--- a/openedx/features/calendar_sync/tasks.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""
-This file contains celery tasks for sending email
-"""
-
-
-import logging
-
-from celery.exceptions import MaxRetriesExceededError
-from celery.task import task
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.contrib.sites.models import Site
-from edx_ace import ace
-from edx_ace.errors import RecoverableChannelDeliveryError
-from edx_ace.message import Message
-from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
-from openedx.core.lib.celery.task_utils import emulate_http_request
-
-log = logging.getLogger('edx.celery.task')
-
-
-@task(bind=True)
-def send_calendar_sync_email(self, msg_string, from_address=None):
- """
- Sending calendar sync email to the user.
- """
- msg = Message.from_string(msg_string)
-
- max_retries = settings.RETRY_CALENDAR_SYNC_EMAIL_MAX_ATTEMPTS
- retries = self.request.retries
-
- if from_address is None:
- from_address = configuration_helpers.get_value('ACTIVATION_EMAIL_FROM_ADDRESS') or (
- configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
- )
- msg.options['from_address'] = from_address
-
- dest_addr = msg.recipient.email_address
-
- site = Site.objects.get_current()
- user = User.objects.get(username=msg.recipient.username)
-
- try:
- with emulate_http_request(site=site, user=user):
- ace.send(msg)
- # Log that the Activation Email has been sent to user without an exception
- log.info("Calendar Sync Email has been sent to User {user_email}".format(
- user_email=dest_addr
- ))
- except RecoverableChannelDeliveryError:
- log.info('Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
- dest_addr=dest_addr,
- attempt=retries,
- max_attempts=max_retries
- ))
- try:
- self.retry(countdown=settings.RETRY_ACTIVATION_EMAIL_TIMEOUT, max_retries=max_retries)
- except MaxRetriesExceededError:
- log.error(
- 'Unable to send calendar sync email to user from "%s" to "%s"',
- from_address,
- dest_addr,
- exc_info=True
- )
- except Exception:
- log.exception(
- 'Unable to send calendar sync email to user from "%s" to "%s"',
- from_address,
- dest_addr,
- )
- raise Exception
diff --git a/openedx/features/calendar_sync/tests/test_tasks.py b/openedx/features/calendar_sync/tests/test_tasks.py
deleted file mode 100644
index 787e68f548..0000000000
--- a/openedx/features/calendar_sync/tests/test_tasks.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-Tests for the Sending activation email celery tasks
-"""
-
-
-import mock
-from django.conf import settings
-from django.test import TestCase
-from six.moves import range
-
-from edx_ace.errors import ChannelError, RecoverableChannelDeliveryError
-from lms.djangoapps.courseware.tests.factories import UserFactory
-from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
-from openedx.features.calendar_sync.tasks import send_calendar_sync_email
-from openedx.features.calendar_sync.views.management import compose_calendar_sync_email
-
-
-class SendCalendarSyncEmailTestCase(TestCase):
- """
- Test for send activation email to user
- """
- def setUp(self):
- """ Setup components used by each test."""
- super(SendCalendarSyncEmailTestCase, self).setUp()
- self.user = UserFactory()
- self.course_overview = CourseOverviewFactory()
- self.msg = compose_calendar_sync_email(self.user, self.course_overview)
-
- @mock.patch('time.sleep', mock.Mock(return_value=None))
- @mock.patch('openedx.features.calendar_sync.tasks.log')
- @mock.patch(
- 'openedx.features.calendar_sync.tasks.ace.send',
- mock.Mock(side_effect=RecoverableChannelDeliveryError(None, None))
- )
- def test_RetrySendUntilFail(self, mock_log):
- """
- Tests retries when the activation email doesn't send
- """
- from_address = 'task_testing@example.com'
- email_max_attempts = settings.RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
-
- send_calendar_sync_email.delay(str(self.msg), from_address=from_address)
-
- # Asserts sending email retry logging.
- for attempt in range(email_max_attempts):
- mock_log.info.assert_any_call(
- 'Retrying sending email to user {dest_addr}, attempt # {attempt} of {max_attempts}'.format(
- dest_addr=self.user.email,
- attempt=attempt,
- max_attempts=email_max_attempts
- ))
- self.assertEqual(mock_log.info.call_count, 6)
-
- # Asserts that the error was logged on crossing max retry attempts.
- mock_log.error.assert_called_with(
- 'Unable to send calendar sync email to user from "%s" to "%s"',
- from_address,
- self.user.email,
- exc_info=True
- )
- self.assertEqual(mock_log.error.call_count, 1)
-
- @mock.patch('openedx.features.calendar_sync.tasks.log')
- @mock.patch('openedx.features.calendar_sync.tasks.ace.send', mock.Mock(side_effect=ChannelError))
- def test_UnrecoverableSendError(self, mock_log):
- """
- Tests that a major failure of the send is logged
- """
- from_address = 'task_testing@example.com'
-
- send_calendar_sync_email.delay(str(self.msg), from_address=from_address)
-
- # Asserts that the error was logged
- mock_log.exception.assert_called_with(
- 'Unable to send calendar sync email to user from "%s" to "%s"',
- from_address,
- self.user.email
- )
-
- # Assert that nothing else was logged
- self.assertEqual(mock_log.info.call_count, 0)
- self.assertEqual(mock_log.error.call_count, 0)
- self.assertEqual(mock_log.exception.call_count, 1)
diff --git a/openedx/features/calendar_sync/tests/test_views.py b/openedx/features/calendar_sync/tests/test_views.py
index 85d730105b..d9892c050d 100644
--- a/openedx/features/calendar_sync/tests/test_views.py
+++ b/openedx/features/calendar_sync/tests/test_views.py
@@ -8,10 +8,7 @@ import ddt
from django.test import TestCase
from django.urls import reverse
-from lms.djangoapps.courseware.tests.factories import UserFactory
-from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.features.calendar_sync.api import SUBSCRIBE, UNSUBSCRIBE
-from openedx.features.calendar_sync.views.management import compose_calendar_sync_email
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -52,39 +49,3 @@ class TestCalendarSyncView(SharedModuleStoreTestCase, TestCase):
response = self.client.post(self.calendar_sync_url, data)
self.assertEqual(response.status_code, expected_status_code)
self.assertIn(contained_text, str(response.content))
-
-
-@ddt.ddt
-class CalendarSyncEmailTestCase(TestCase):
- """
- Test for send activation email to user
- """
-
- @ddt.data(False, True)
- def test_compose_calendar_sync_email(self, is_update):
- """
- Tests that attributes of the message are being filled correctly in compose_activation_email
- """
- user = UserFactory()
- course_overview = CourseOverviewFactory()
- course_name = course_overview.display_name
- if is_update:
- calendar_sync_subject = 'Updates for Your {course} Schedule'.format(course=course_name)
- calendar_sync_headline = 'Update Your Calendar'
- calendar_sync_body = ('Your assignment due dates for {course} were recently adjusted. Update your calendar'
- 'with your new schedule to ensure that you stay on track!').format(course=course_name)
- else:
- calendar_sync_subject = 'Stay on Track'
- calendar_sync_headline = 'Mark Your Calendar'
- calendar_sync_body = (
- 'Sticking to a schedule is the best way to ensure that you successfully complete your '
- 'self-paced course. This schedule of assignment due dates for {course} will help you '
- 'stay on track!'.format(course=course_name))
-
- msg = compose_calendar_sync_email(user, course_overview, is_update)
-
- self.assertEqual(msg.context['calendar_sync_subject'], calendar_sync_subject)
- self.assertEqual(msg.context['calendar_sync_headline'], calendar_sync_headline)
- self.assertEqual(msg.context['calendar_sync_body'], calendar_sync_body)
- self.assertEqual(msg.recipient.username, user.username)
- self.assertEqual(msg.recipient.email_address, user.email)
diff --git a/openedx/features/calendar_sync/utils.py b/openedx/features/calendar_sync/utils.py
new file mode 100644
index 0000000000..7bade9b7fd
--- /dev/null
+++ b/openedx/features/calendar_sync/utils.py
@@ -0,0 +1,79 @@
+import logging
+from email.mime.application import MIMEApplication
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from django.conf import settings
+from django.utils.html import format_html
+from django.utils.translation import ugettext_lazy as _
+import os
+
+import boto3
+
+logger = logging.getLogger(__name__)
+
+
+def calendar_sync_initial_email_content(course_name):
+ subject = _('Stay on Track')
+ body_text = _('Sticking to a schedule is the best way to ensure that you successfully complete your self-paced '
+ 'course. This schedule of assignment due dates for {course} will help you stay on track!'
+ ).format(course=course_name)
+ body = format_html('{text}
', text=body_text)
+ return subject, body
+
+
+def calendar_sync_update_email_content(course_name):
+ subject = _('Updates for Your {course} Schedule').format(course=course_name)
+ body_text = _('Your assignment due dates for {course} were recently adjusted. Update your calendar with your new '
+ 'schedule to ensure that you stay on track!').format(course=course_name)
+ body = format_html('{text}
', text=body_text)
+ return subject, body
+
+
+def prepare_attachments(attachment_data):
+ """
+ Helper function to create a list contain file attachment objects
+ for use with MIMEMultipart
+ Returns a list of MIMEApplication objects
+ """
+
+ attachments = []
+ for filename, data in attachment_data.items():
+ msg_attachment = MIMEApplication(data)
+ msg_attachment.add_header(
+ 'Content-Disposition',
+ 'attachment',
+ filename=os.path.basename(filename)
+ )
+ msg_attachment.set_type('text/calendar')
+ attachments.append(msg_attachment)
+
+ return attachments
+
+
+def send_email_with_attachment(to_emails, attachment_data, course_name, is_update=False):
+ # connect to SES
+ client = boto3.client('ses', region_name=settings.AWS_SES_REGION_NAME)
+
+ subject, body = (calendar_sync_update_email_content(course_name) if is_update else
+ calendar_sync_initial_email_content(course_name))
+
+ # build email body as html
+ msg_body = MIMEText(body, 'html')
+
+ attachments = prepare_attachments(attachment_data)
+
+ # iterate over each email in the list to send emails independently
+ for email in to_emails:
+ msg = MIMEMultipart()
+ msg['Subject'] = str(subject)
+ msg['From'] = settings.BULK_EMAIL_DEFAULT_FROM_EMAIL
+ msg['To'] = email
+
+ # attach the message body and attachment
+ msg.attach(msg_body)
+ for msg_attachment in attachments:
+ msg.attach(msg_attachment)
+
+ # send the email
+ result = client.send_raw_email(Source=msg['From'], Destinations=[email], RawMessage={'Data': msg.as_string()})
+ logger.debug(result)
diff --git a/openedx/features/calendar_sync/views/management.py b/openedx/features/calendar_sync/views/management.py
deleted file mode 100644
index 9e4527c796..0000000000
--- a/openedx/features/calendar_sync/views/management.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-Calendar Sync Email Management
-"""
-
-
-from django.utils.translation import ugettext_lazy as _
-
-from edx_ace.recipient import Recipient
-from student.models import CourseEnrollment, CourseOverview
-from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
-from openedx.core.djangoapps.user_api.preferences import api as preferences_api
-from openedx.features.calendar_sync.message_types import CalendarSync
-from openedx.features.calendar_sync.tasks import send_calendar_sync_email
-
-
-def compose_calendar_sync_email(user, course: CourseOverview, is_update=False):
- """
- Construct all the required params for the calendar
- sync email through celery task
- """
-
- course_name = course.display_name
- if is_update:
- calendar_sync_subject = _('Updates for Your {course} Schedule').format(course=course_name)
- calendar_sync_headline = _('Update Your Calendar')
- calendar_sync_body = _('Your assignment due dates for {course} were recently adjusted. Update your calendar'
- 'with your new schedule to ensure that you stay on track!').format(course=course_name)
- else:
- calendar_sync_subject = _('Stay on Track')
- calendar_sync_headline = _('Mark Your Calendar')
- calendar_sync_body = _('Sticking to a schedule is the best way to ensure that you successfully complete your '
- 'self-paced course. This schedule of assignment due dates for {course} will help you '
- 'stay on track!').format(course=course_name)
- email_context = {
- 'calendar_sync_subject': calendar_sync_subject,
- 'calendar_sync_headline': calendar_sync_headline,
- 'calendar_sync_body': calendar_sync_body,
- }
-
- msg = CalendarSync().personalize(
- recipient=Recipient(user.username, user.email),
- language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
- user_context=email_context,
- )
-
- return msg
-
-
-def compose_and_send_calendar_sync_email(user, course: CourseOverview, is_update=False):
- """
- Construct all the required params and send the activation email
- through celery task
-
- Arguments:
- user: current logged-in user
- course: course overview object
- is_update: if this should be an 'update' email
- """
- if not CourseEnrollment.objects.filter(user=user, course=course).exists():
- return
-
- msg = compose_calendar_sync_email(user, course, is_update)
-
- send_calendar_sync_email.delay(str(msg))