def test_does_not_split_links(self): # token that causes html2text to make a line break if wrap_links option is not set token = '4522e1fa-9827-44ec-bf76-f7fa8fb132ff' url = 'http://localhost:8080/#/signup?invite={}&email=please%40join.com'.format( token) html = '<a href="{}">some link</a>'.format(url) text = generate_plaintext_from_html(html) self.assertIn(url, text)
def inbound_received(sender, event, esp_name, **kwargs): incoming_message = event.message # check local part of reply-to and extract conversation and user (fail if they don't exist) local_part = incoming_message.to[0].username try: conversation_id, user_id, thread_id = parse_local_part(local_part) except (UnicodeDecodeError, binascii.Error): sentry_client.captureException() return user = get_user_model().objects.get(id=user_id) thread = None if thread_id is not None: thread = ConversationMessage.objects.get(id=thread_id) conversation = thread.conversation else: conversation = Conversation.objects.get(id=conversation_id) # get email content as plain text if incoming_message.text is not None: text_content = incoming_message.text elif incoming_message.html is not None: # let's just make HTML into plain text html_content = incoming_message.html text_content = generate_plaintext_from_html(html_content) else: # Inform the user if we couldn't find any content notify_about_rejected_email(user, 'Karrot could not find any reply text') return # extract email reply text # Try out both talon and discourse email_reply_trimmer # Trimmers are conservative and sometimes keep more lines, leading to bloated replies. # We choose the trimmed reply that has fewer lines. trimmed_talon, line_count_talon = trim_with_talon(text_content) trimmed_discourse, line_count_discourse = trim_with_discourse(text_content) reply_plain = trimmed_discourse if line_count_discourse <= line_count_talon else trimmed_talon stats.incoming_email_trimmed({ 'line_count_original': len(text_content.splitlines()), 'line_count_talon': line_count_talon, 'line_count_discourse': line_count_discourse, 'from_html': 1 if incoming_message.text is None else 0 }) # add reply to conversation if conversation.is_closed: notify_about_rejected_email(user, reply_plain) return if not conversation.participants.filter(id=user.id).exists(): notify_about_rejected_email(user, reply_plain) return created_message = ConversationMessage.objects.create( author=user, conversation=conversation, thread=thread, content=reply_plain, received_via='email', ) incoming_message_serialized = dict(incoming_message) incoming_message_serialized['text'] = incoming_message.text incoming_message_serialized['html'] = incoming_message.html IncomingEmail.objects.create( user=user, message=created_message, payload=incoming_message_serialized, version=2, )
def post(self, request): """ Receive conversation replies via e-mail Request payload spec: https://developers.sparkpost.com/api/relay-webhooks/#header-relay-webhook-payload """ auth_key = request.META.get('HTTP_X_MESSAGESYSTEMS_WEBHOOK_TOKEN') if auth_key is None or auth_key != settings.SPARKPOST_RELAY_SECRET: return Response( status=status.HTTP_403_FORBIDDEN, data={ 'message': 'Invalid HTTP_X_MESSAGESYSTEMS_WEBHOOK_TOKEN header' }) for messages in [e['msys'].values() for e in request.data]: for incoming_message in messages: # 1. get email content and reply-to reply_to = parseaddr(incoming_message['rcpt_to'])[1] content = incoming_message['content'] # 2. check local part of reply-to and extract conversation and user (fail if they don't exist) local_part = reply_to.split('@')[0] try: conversation_id, user_id, thread_id = parse_local_part( local_part) except (UnicodeDecodeError, binascii.Error): sentry_client.captureException() continue user = get_user_model().objects.get(id=user_id) thread = None if thread_id is not None: thread = ConversationMessage.objects.get(id=thread_id) conversation = thread.conversation else: conversation = Conversation.objects.get(id=conversation_id) # 3. extract the email reply text # Try plain text first, most emails have that. if 'text' in content: # Try out both talon and discourse email_reply_trimmer # Trimmers are conservative and sometimes keep more lines, leading to bloated replies. # We choose the trimmed reply that has fewer lines. text_content = content['text'] trimmed_talon, line_count_talon = trim_with_talon( text_content) trimmed_discourse, line_count_discourse = trim_with_discourse( text_content) reply_plain = trimmed_discourse if line_count_discourse <= line_count_talon else trimmed_talon stats.incoming_email_trimmed({ 'line_count_original': len(text_content.splitlines()), 'line_count_talon': line_count_talon, 'line_count_discourse': line_count_discourse, }) else: # Fall back to html try: html_content = content['html'] except KeyError: # Inform the user if we couldn't find any content sentry_client.captureException() notify_about_rejected_email( user, 'Karrot could not find any reply text') continue reply_html = trim_html_with_talon(html_content) reply_plain = generate_plaintext_from_html(reply_html) stats.incoming_html_email_trimmed({ 'length_original': len(html_content.splitlines()), 'length_html_talon': len(reply_html), 'length_plain_talon': len(reply_plain), }) # 4. add reply to conversation if conversation.is_closed: notify_about_rejected_email(user, reply_plain) continue if not conversation.participants.filter(id=user.id).exists(): notify_about_rejected_email(user, reply_plain) continue created_message = ConversationMessage.objects.create( author=user, conversation=conversation, thread=thread, content=reply_plain, received_via='email', ) IncomingEmail.objects.create( user=user, message=created_message, payload=incoming_message, ) return Response(status=status.HTTP_200_OK, data={})