def test_html_template(self): '''Assert that we may add an additional text template to the email engine. ''' factory = EmailFactory('*****@*****.**', 'test_subject.txt', 'test.txt', 'plugin.email') factory.add_text_template('test.html', 'html') msg = factory.build('*****@*****.**', test_parameter='value') # Assert that the message is multipart self.assertTrue(msg.is_multipart()) self.assertEqual(2, len(msg.get_payload())) payload_text = msg.get_payload(0) self.assertEqual('text/plain; charset="utf-8"', payload_text.get('Content-Type')) self.assertEqual(b'value', payload_text.get_payload(decode=True)) payload_html = msg.get_payload(1) self.assertEqual('text/html; charset="utf-8"', payload_html.get('Content-Type')) self.assertEqual(b'value', payload_html.get_payload(decode=True))
def test_simple_build(self): """Assert that a simple build provides an email. """ factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') # Assert that the message is multipart self.assertTrue(msg.is_multipart()) self.assertEqual(1, len(msg.get_payload())) # Test message headers self.assertEqual('*****@*****.**', msg.get('From')) self.assertEqual('*****@*****.**', msg.get('To')) self.assertEqual('test_subject', msg.get('Subject')) self.assertEqual('auto-generated', msg.get('Auto-Submitted')) self.assertEqual('multipart/alternative', msg.get('Content-Type')) self.assertIsNotNone(msg.get('Date')) # This will vary payload_text = msg.get_payload(0) self.assertEqual('text/plain; charset="utf-8"', payload_text.get('Content-Type')) self.assertEqual(b'value', payload_text.get_payload(decode=True)) # Assert that there's only one payload. self.assertEqual(1, len(msg.get_payload()))
def test_html_template(self): '''Assert that we may add an additional text template to the email engine. ''' factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') factory.add_text_template('test.html', 'html') msg = factory.build('*****@*****.**', test_parameter='value') # Assert that the message is multipart self.assertTrue(msg.is_multipart()) self.assertEqual(2, len(msg.get_payload())) payload_text = msg.get_payload(0) self.assertEqual('text/plain; charset="utf-8"', payload_text.get('Content-Type')) self.assertEqual(b'value', payload_text.get_payload(decode=True)) payload_html = msg.get_payload(1) self.assertEqual('text/html; charset="utf-8"', payload_html.get('Content-Type')) self.assertEqual(b'value', payload_html.get_payload(decode=True))
def test_simple_build(self): """Assert that a simple build provides an email. """ factory = EmailFactory('*****@*****.**', 'test_subject.txt', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') # Assert that the message is multipart self.assertTrue(msg.is_multipart()) self.assertEqual(1, len(msg.get_payload())) # Test message headers self.assertEqual('*****@*****.**', msg.get('From')) self.assertEqual('*****@*****.**', msg.get('To')) self.assertEqual('value', msg.get('Subject')) self.assertEqual('auto-generated', msg.get('Auto-Submitted')) self.assertEqual('multipart/alternative', msg.get('Content-Type')) self.assertIsNotNone(msg.get('Date')) # This will vary payload_text = msg.get_payload(0) self.assertEqual('text/plain; charset="utf-8"', payload_text.get('Content-Type')) self.assertEqual(b'value', payload_text.get_payload(decode=True)) # Assert that there's only one payload. self.assertEqual(1, len(msg.get_payload()))
def test_uuid_ids(self): '''Assert that our message id's are not thread-based.''' outbox = Outbox() self.assertIsNotNone(outbox) factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') for i in range(0, 100): recipient = '*****@*****.**' % (i,) message = factory.build(recipient, test_parameter=i) # This will throw an error if the message ID already exists. key = outbox.add(message) parts = re.match(r'^([0-9]+)M([0-9]+)_(.*)$', key) self.assertIsNotNone(parts) # The first part should be a timestamp. timestamp = float(parts.group(1)) + (float(parts.group(2)) / 1e6) send_date = datetime.datetime.fromtimestamp(timestamp) self.assertIsNotNone(send_date) # The last part should be a UUID. Trying to construct it will # cause a value error if it is not. uuid.UUID(parts.group(3), version=4) outbox.flush()
def test_subject_template(self): """Assert that the subject is templateable.""" factory = EmailFactory('*****@*****.**', 'test_subject.txt', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('value', msg.get('Subject')) # Assert that the subject is trimmed. and appended with an ellipsis. factory = EmailFactory('*****@*****.**', 'test_long_subject.txt', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual(78, len(msg.get('Subject'))) self.assertEqual('...', msg.get('Subject')[-3:]) # Assert that the subject has newlines trimmed factory = EmailFactory('*****@*****.**', 'test_subject_newline.txt', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('with newline', msg.get('Subject'))
def test_basic_outbox_functions(self): '''Assert that we can list elements of our outbox.''' outbox = Outbox() self.assertIsNotNone(outbox) factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') for i in range(0, 100): recipient = '*****@*****.**' % (i,) message = factory.build(recipient, test_parameter=i) # This will throw an error if the message ID already exists. outbox.add(message) outbox.flush() values = [] for key, email in six.iteritems(outbox): # Assert that the type of the email is correct. self.assertIsInstance(email, Message) # Assert that the message has a date header. While we can extract # this from the ID, it's also helpful for us to timebox the # emails to send by worker. self.assertIsNotNone(email.get('Date')) # Make sure we only have one payload (since that's what the # factory was configured with) self.assertEqual(1, len(email.get_payload())) # Pull the ID from the payload and check it against all the # emails we've found so far. text_part = email.get_payload(0) key = int(text_part.get_payload(decode=True)) self.assertNotIn(key, values) # Store the key for later comparison. values.append(key) self.assertEqual(100, len(values))
def test_no_template(self): """Assert that attempting to load an invalid template raises an exception. """ try: EmailFactory('*****@*****.**', 'test_subject', 'invalid.txt', 'plugin.email') self.assertFalse(True) except TemplateNotFound: self.assertFalse(False) try: factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') factory.add_text_template('invalid.html', 'html') self.assertFalse(True) except TemplateNotFound: self.assertFalse(False)
def test_no_template(self): """Assert that attempting to load an invalid template raises an exception. """ try: EmailFactory('*****@*****.**', 'invalid_subject.txt', 'invalid.txt', 'plugin.email') self.assertFalse(True) except TemplateNotFound: self.assertFalse(False) try: factory = EmailFactory('*****@*****.**', 'test_subject.txt', 'test.txt', 'plugin.email') factory.add_text_template('invalid.html', 'html') self.assertFalse(True) except TemplateNotFound: self.assertFalse(False)
def test_subject_template(self): """Assert that the subject is templateable.""" factory = EmailFactory('*****@*****.**', '{{test_parameter}}', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('value', msg.get('Subject')) # Assert that the subject is trimmed. and appended with an ellipsis. test_subject = ('a' * 100) factory = EmailFactory('*****@*****.**', test_subject, 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual(78, len(msg.get('Subject'))) self.assertEqual('...', msg.get('Subject')[-3:]) # Assert that the subject has unix newlines trimmed factory = EmailFactory('*****@*****.**', 'with\nnewline', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('with newline', msg.get('Subject')) # Assert that the subject has windows returns trimmed factory = EmailFactory('*****@*****.**', 'with\r\nnewline', 'test.txt', 'plugin.email') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('with newline', msg.get('Subject'))
def test_custom_headers(self): """Assert that we can set custom headers.""" factory = EmailFactory('*****@*****.**', 'test_subject.txt', 'test.txt', 'plugin.email') custom_headers = { 'X-Custom-Header': 'test-header-value' } for name, value in six.iteritems(custom_headers): factory.add_header(name, value) msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('test-header-value', msg.get('X-Custom-Header')) # test that headers may be overridden, and that we don't end up with # duplicate subjects. factory.add_header('Subject', 'new_subject') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('new_subject', msg.get('Subject'))
def test_custom_headers(self): """Assert that we can set custom headers.""" factory = EmailFactory('*****@*****.**', 'test_subject', 'test.txt', 'plugin.email') custom_headers = { 'X-Custom-Header': 'test-header-value' } for name, value in six.iteritems(custom_headers): factory.add_header(name, value) msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('test-header-value', msg.get('X-Custom-Header')) # test that headers may be overridden, and that we don't end up with # duplicate subjects. factory.add_header('Subject', 'new_subject') msg = factory.build('*****@*****.**', test_parameter='value') self.assertEqual('new_subject', msg.get('Subject'))
def handle_email(self, session, author, subscribers, method, url, path, query_string, status, resource, resource_id, sub_resource=None, sub_resource_id=None, resource_before=None, resource_after=None): """Send an email for a specific event. We assume that filtering logic has already occurred when this method is invoked. :param session: An event-specific SQLAlchemy session. :param author: The author's user record. :param subscribers: A list of subscribers that should receive an email. :param method: The HTTP Method. :param url: The Referer header from the request. :param path: The full HTTP Path requested. :param query_string: The query string from the request. :param status: The returned HTTP Status of the response. :param resource: The resource type. :param resource_id: The ID of the resource. :param sub_resource: The subresource type. :param sub_resource_id: The ID of the subresource. :param resource_before: The resource state before this event occurred. :param resource_after: The resource state after this event occurred. """ email_config = CONF.plugin_email # Retrieve the template names. (subject_template, text_template, html_template) = \ self.get_templates(method=method, resource_name=resource, sub_resource_name=sub_resource) # Build our factory. If an HTML template exists, add it. If it can't # find the template, skip. try: factory = EmailFactory(sender=email_config.sender, subject=subject_template, text_template=text_template) except TemplateNotFound: LOG.error("Templates not found [%s, %s]" % (subject_template, text_template)) return # Try to add an HTML template try: factory.add_text_template(html_template, 'html') except TemplateNotFound: LOG.debug('Template %s not found' % (html_template, )) # If there's a reply-to in our config, add that. if email_config.reply_to: factory.add_header('Reply-To', email_config.reply_to) # If there is a fallback URL configured, use it if needed if email_config.default_url and url is None: url = email_config.default_url # Resolve the resource instance resource_instance = self.resolve_resource_by_name( session, resource, resource_id) sub_resource_instance = self.resolve_resource_by_name( session, sub_resource, sub_resource_id) # Set In-Reply-To message id for 'task', 'story', and # 'worklist' resources story_id = None worklist_id = None if resource == 'task' and method == 'DELETE': # FIXME(pedroalvarez): Workaround the fact that the task won't be # in the database anymore if it has been deleted. # We should archive instead of delete to solve this. story_id = resource_before['story_id'] created_at = self.resolve_resource_by_name(session, 'story', story_id).created_at elif resource == 'task': story_id = resource_instance.story.id created_at = resource_instance.story.created_at elif resource == 'story': story_id = resource_instance.id created_at = resource_instance.created_at elif resource == 'worklist': worklist_id = resource_instance.id created_at = resource_instance.created_at if story_id and created_at: thread_id = "<storyboard.story.%s.%s@%s>" % ( created_at.strftime("%Y%m%d%H%M"), story_id, getfqdn()) elif worklist_id and created_at: thread_id = "<storyboard.worklist.%s.%s@%s>" % ( created_at.strftime("%Y%m%d%H%M"), story_id, getfqdn()) else: thread_id = make_msgid() factory.add_header("In-Reply-To", thread_id) factory.add_header("X-StoryBoard-Subscription-Type", resource) # Figure out the diff between old and new. before, after = self.get_changed_properties(resource_before, resource_after) # For each subscriber, create the email and send it. with smtp.get_smtp_client() as smtp_client: for subscriber in subscribers: # Make sure this subscriber's preferences indicate they want # email and they're not receiving digests. if not self.get_preference('plugin_email_enable', subscriber) \ or self.get_preference('plugin_email_digest', subscriber): continue send_notification = self.get_preference( 'receive_notifications_worklists', subscriber) if send_notification != 'true' and story_id is None: continue # Don't send a notification if the user isn't allowed to see # the thing this event is about. if 'event_type' in resource: event = events_api.event_get(resource['id'], current_user=subscriber.id, session=session) if not events_api.is_visible( event, subscriber.id, session=session): continue try: # Build an email. message = factory.build(recipient=subscriber.email, author=author, resource=resource_instance, sub_resource=sub_resource_instance, url=url, query_string=query_string, before=before, after=after) # Send the email. from_addr = message.get('From') to_addrs = message.get('To') try: smtp_client.sendmail(from_addr=from_addr, to_addrs=to_addrs, msg=message.as_string()) except smtplib.SMTPException as e: LOG.error('Cannot send email, discarding: %s' % (e, )) except Exception as e: # Skip, keep going. LOG.error("Cannot schedule email: %s" % (e.message, ))
def handle_email(self, session, author, subscribers, method, url, path, query_string, status, resource, resource_id, sub_resource=None, sub_resource_id=None, resource_before=None, resource_after=None): """Send an email for a specific event. We assume that filtering logic has already occurred when this method is invoked. :param session: An event-specific SQLAlchemy session. :param author: The author's user record. :param subscribers: A list of subscribers that should receive an email. :param method: The HTTP Method. :param url: The Referer header from the request. :param path: The full HTTP Path requested. :param query_string: The query string from the request. :param status: The returned HTTP Status of the response. :param resource: The resource type. :param resource_id: The ID of the resource. :param sub_resource: The subresource type. :param sub_resource_id: The ID of the subresource. :param resource_before: The resource state before this event occurred. :param resource_after: The resource state after this event occurred. """ email_config = CONF.plugin_email # Retrieve the template names. (subject_template, text_template, html_template) = \ self.get_templates(method=method, resource_name=resource, sub_resource_name=sub_resource) # Build our factory. If an HTML template exists, add it. If it can't # find the template, skip. try: factory = EmailFactory(sender=email_config.sender, subject=subject_template, text_template=text_template) except TemplateNotFound: LOG.error("Templates not found [%s, %s]" % (subject_template, text_template)) return # Try to add an HTML template try: factory.add_text_template(html_template, 'html') except TemplateNotFound: LOG.debug('Template %s not found' % (html_template, )) # If there's a reply-to in our config, add that. if email_config.reply_to: factory.add_header('Reply-To', email_config.reply_to) # Resolve the resource instance resource_instance = self.resolve_resource_by_name( session, resource, resource_id) sub_resource_instance = self.resolve_resource_by_name( session, sub_resource, sub_resource_id) # Figure out the diff between old and new. before, after = self.get_changed_properties(resource_before, resource_after) # For each subscriber, create the email and send it. with smtp.get_smtp_client() as smtp_client: for subscriber in subscribers: # Make sure this subscriber's preferences indicate they want # email and they're not receiving digests. if not self.get_preference('plugin_email_enable', subscriber) \ or self.get_preference('plugin_email_digest', subscriber): continue try: # Build an email. message = factory.build(recipient=subscriber.email, author=author, resource=resource_instance, sub_resource=sub_resource_instance, url=url, query_string=query_string, before=before, after=after) # Send the email. from_addr = message.get('From') to_addrs = message.get('To') try: smtp_client.sendmail(from_addr=from_addr, to_addrs=to_addrs, msg=message.as_string()) except smtplib.SMTPException as e: LOG.error('Cannot send email, discarding: %s' % (e, )) except Exception as e: # Skip, keep going. LOG.error("Cannot schedule email: %s" % (e.message, ))
def handle_email(self, session, author, subscribers, method, url, path, query_string, status, resource, resource_id, sub_resource=None, sub_resource_id=None, resource_before=None, resource_after=None): """Send an email for a specific event. We assume that filtering logic has already occurred when this method is invoked. :param session: An event-specific SQLAlchemy session. :param author: The author's user record. :param subscribers: A list of subscribers that should receive an email. :param method: The HTTP Method. :param url: The Referer header from the request. :param path: The full HTTP Path requested. :param query_string: The query string from the request. :param status: The returned HTTP Status of the response. :param resource: The resource type. :param resource_id: The ID of the resource. :param sub_resource: The subresource type. :param sub_resource_id: The ID of the subresource. :param resource_before: The resource state before this event occurred. :param resource_after: The resource state after this event occurred. """ email_config = CONF.plugin_email # Retrieve the template names. (subject_template, text_template, html_template) = \ self.get_templates(method=method, resource_name=resource, sub_resource_name=sub_resource) # Build our factory. If an HTML template exists, add it. If it can't # find the template, skip. try: factory = EmailFactory(sender=email_config.sender, subject=subject_template, text_template=text_template) except TemplateNotFound: LOG.error("Templates not found [%s, %s]" % (subject_template, text_template)) return # Try to add an HTML template try: factory.add_text_template(html_template, 'html') except TemplateNotFound: LOG.debug('Template %s not found' % (html_template,)) # If there's a reply-to in our config, add that. if email_config.reply_to: factory.add_header('Reply-To', email_config.reply_to) # Resolve the resource instance resource_instance = self.resolve_resource_by_name(session, resource, resource_id) sub_resource_instance = self.resolve_resource_by_name(session, sub_resource, sub_resource_id) # Figure out the diff between old and new. before, after = self.get_changed_properties(resource_before, resource_after) # For each subscriber, create the email and send it. with smtp.get_smtp_client() as smtp_client: for subscriber in subscribers: # Make sure this subscriber's preferences indicate they want # email and they're not receiving digests. if not self.get_preference('plugin_email_enable', subscriber) \ or self.get_preference('plugin_email_digest', subscriber): continue try: # Build an email. message = factory.build(recipient=subscriber.email, author=author, resource=resource_instance, sub_resource=sub_resource_instance, url=url, query_string=query_string, before=before, after=after) # Send the email. from_addr = message.get('From') to_addrs = message.get('To') try: smtp_client.sendmail(from_addr=from_addr, to_addrs=to_addrs, msg=message.as_string()) except smtplib.SMTPException as e: LOG.error('Cannot send email, discarding: %s' % (e,)) except Exception as e: # Skip, keep going. LOG.error("Cannot schedule email: %s" % (e.message,))