Example #1
0
def get_apns_alert_title(message: Message) -> str:
    """
    On an iOS notification, this is the first bolded line.
    """
    if message.recipient.type == Recipient.HUDDLE:
        recipients = cast(List[Dict[str, Any]], get_display_recipient(message.recipient))
        return ', '.join(sorted(r['full_name'] for r in recipients))
    elif message.is_stream_message():
        return "#%s > %s" % (get_display_recipient(message.recipient), message.topic_name(),)
    # For personal PMs, we just show the sender name.
    return message.sender.full_name
Example #2
0
def get_gcm_payload(user_profile, message):
    # type: (UserProfile, Message) -> Dict[str, Any]
    content = message.content
    content_truncated = (len(content) > 200)
    if content_truncated:
        content = content[:200] + "..."

    android_data = {
        'user': user_profile.email,
        'event': 'message',
        'alert': get_alert_from_message(message),
        'zulip_message_id': message.id,  # message_id is reserved for CCS
        'time': datetime_to_timestamp(message.pub_date),
        'content': content,
        'content_truncated': content_truncated,
        'sender_email': message.sender.email,
        'sender_full_name': message.sender.full_name,
        'sender_avatar_url': absolute_avatar_url(message.sender),
    }

    if message.recipient.type == Recipient.STREAM:
        android_data['recipient_type'] = "stream"
        android_data['stream'] = get_display_recipient(message.recipient)
        android_data['topic'] = message.subject
    elif message.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL):
        android_data['recipient_type'] = "private"

    return android_data
Example #3
0
def send_to_missed_message_address(address, message):
    # type: (text_type, message.Message) -> None
    token = get_missed_message_token_from_address(address)
    key = missed_message_redis_key(token)
    result = redis_client.hmget(key, 'user_profile_id', 'recipient_id', 'subject')
    if not all(val is not None for val in result):
        raise ZulipEmailForwardError('Missing missed message address data')
    user_profile_id, recipient_id, subject = result

    user_profile = get_user_profile_by_id(user_profile_id)
    recipient = Recipient.objects.get(id=recipient_id)
    display_recipient = get_display_recipient(recipient)

    # Testing with basestring so we don't depend on the list return type from
    # get_display_recipient
    if not isinstance(display_recipient, six.string_types):
        recipient_str = ','.join([user['email'] for user in display_recipient])
    else:
        recipient_str = display_recipient

    body = filter_footer(extract_body(message))
    body += extract_and_upload_attachments(message, user_profile.realm)
    if not body:
        body = '(No email body)'

    if recipient.type == Recipient.STREAM:
        recipient_type_name = 'stream'
    else:
        recipient_type_name = 'private'

    internal_send_message(user_profile.email, recipient_type_name,
                          recipient_str, subject, body)
Example #4
0
def export_messages_single_user(user_profile, chunk_size=1000, output_dir=None):
    # type: (UserProfile, int, Path) -> None
    user_message_query = UserMessage.objects.filter(user_profile=user_profile)
    min_id = -1
    dump_file_id = 1
    while True:
        actual_query = user_message_query.select_related("message", "message__sending_client").filter(id__gt=min_id)[0:chunk_size]
        user_message_chunk = [um for um in actual_query]
        user_message_ids = set(um.id for um in user_message_chunk)

        if len(user_message_chunk) == 0:
            break

        message_chunk = []
        for user_message in user_message_chunk:
            item = model_to_dict(user_message.message)
            item['flags'] = user_message.flags_list()
            item['flags_mask'] = user_message.flags.mask
            # Add a few nice, human-readable details
            item['sending_client_name'] = user_message.message.sending_client.name
            item['display_recipient'] = get_display_recipient(user_message.message.recipient)
            message_chunk.append(item)

        message_filename = os.path.join(output_dir, "messages-%06d.json" % (dump_file_id,))
        logging.info("Fetched Messages for %s" % (message_filename,))

        output = {'zerver_message': message_chunk}
        floatify_datetime_fields(output, 'zerver_message')

        write_message_export(message_filename, output)
        min_id = max(user_message_ids)
        dump_file_id += 1
Example #5
0
 def message_header(user_profile, message):
     # type: (UserProfile, Message) -> Dict[str, Any]
     disp_recipient = get_display_recipient(message.recipient)
     if message.recipient.type == Recipient.PERSONAL:
         header = u"You and %s" % (message.sender.full_name)
         html_link = pm_narrow_url(user_profile.realm, [message.sender.email])
         header_html = u"<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
     elif message.recipient.type == Recipient.HUDDLE:
         assert not isinstance(disp_recipient, text_type)
         other_recipients = [r['full_name'] for r in disp_recipient
                                 if r['email'] != user_profile.email]
         header = u"You and %s" % (", ".join(other_recipients),)
         html_link = pm_narrow_url(user_profile.realm, [r["email"] for r in disp_recipient
                                    if r["email"] != user_profile.email])
         header_html = u"<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
     else:
         assert isinstance(disp_recipient, text_type)
         header = u"%s > %s" % (disp_recipient, message.topic_name())
         stream_link = stream_narrow_url(user_profile.realm, disp_recipient)
         topic_link = topic_narrow_url(user_profile.realm, disp_recipient, message.subject)
         header_html = u"<a href='%s'>%s</a> > <a href='%s'>%s</a>" % (
             stream_link, disp_recipient, topic_link, message.subject)
     return {"plain": header,
             "html": header_html,
             "stream_message": message.recipient.type_name() == "stream"}
Example #6
0
    def test_receive_stream_email_messages_success(self) -> None:

        # build dummy messages for stream
        # test valid incoming stream message is processed properly
        user_profile = self.example_user('hamlet')
        self.login(user_profile.email)
        self.subscribe(user_profile, "Denmark")
        stream = get_stream("Denmark", user_profile.realm)

        stream_to_address = encode_email_address(stream)

        incoming_valid_message = MIMEText('TestStreamEmailMessages Body')  # type: Any # https://github.com/python/typeshed/issues/275

        incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject'
        incoming_valid_message['From'] = self.example_email('hamlet')
        incoming_valid_message['To'] = stream_to_address
        incoming_valid_message['Reply-to'] = self.example_email('othello')

        process_message(incoming_valid_message)

        # Hamlet is subscribed to this stream so should see the email message from Othello.
        message = most_recent_message(user_profile)

        self.assertEqual(message.content, "TestStreamEmailMessages Body")
        self.assertEqual(get_display_recipient(message.recipient), stream.name)
        self.assertEqual(message.topic_name(), incoming_valid_message['Subject'])
Example #7
0
    def test_receive_stream_email_messages_success(self):

        # build dummy messages for stream
        # test valid incoming stream message is processed properly
        self.login("*****@*****.**")
        user_profile = get_user_profile_by_email("*****@*****.**")
        self.subscribe_to_stream(user_profile.email, "Denmark")
        stream = get_stream("Denmark", user_profile.realm)

        stream_to_address = encode_email_address(stream)

        incoming_valid_message = MIMEText('TestStreamEmailMessages Body')

        incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject'
        incoming_valid_message['From'] = "*****@*****.**"
        incoming_valid_message['To'] = stream_to_address
        incoming_valid_message['Reply-to'] = "*****@*****.**"

        process_message(incoming_valid_message)

        # Hamlet is subscribed to this stream so should see the email message from Othello.
        message = most_recent_message(user_profile)

        self.assertEqual(message.content, "TestStreamEmailMessages Body")
        self.assertEqual(get_display_recipient(message.recipient), stream.name)
        self.assertEqual(message.subject, incoming_valid_message['Subject'])
Example #8
0
    def message_header(user_profile: UserProfile, message: Message) -> Dict[str, Any]:
        if message.recipient.type == Recipient.PERSONAL:
            header = "You and %s" % (message.sender.full_name,)
            html_link = personal_narrow_url(
                realm=user_profile.realm,
                sender=message.sender,
            )
            header_html = "<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
        elif message.recipient.type == Recipient.HUDDLE:
            disp_recipient = get_display_recipient(message.recipient)
            assert not isinstance(disp_recipient, str)
            other_recipients = [r['full_name'] for r in disp_recipient
                                if r['id'] != user_profile.id]
            header = "You and %s" % (", ".join(other_recipients),)
            other_user_ids = [r['id'] for r in disp_recipient
                              if r['id'] != user_profile.id]
            html_link = huddle_narrow_url(
                realm=user_profile.realm,
                other_user_ids=other_user_ids,
            )

            header_html = "<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
        else:
            stream = Stream.objects.only('id', 'name').get(id=message.recipient.type_id)
            header = "%s > %s" % (stream.name, message.topic_name())
            stream_link = stream_narrow_url(user_profile.realm, stream)
            topic_link = topic_narrow_url(user_profile.realm, stream, message.topic_name())
            header_html = "<a href='%s'>%s</a> > <a href='%s'>%s</a>" % (
                stream_link, stream.name, topic_link, message.topic_name())
        return {"plain": header,
                "html": header_html,
                "stream_message": message.recipient.type_name() == "stream"}
Example #9
0
    def send_json_payload(self, user_profile: UserProfile, url: str,
                          payload: Union[str, Dict[str, Any]],
                          stream_name: Optional[str]=None, **post_params: Any) -> Message:
        if stream_name is not None:
            self.subscribe(user_profile, stream_name)

        prior_msg = self.get_last_message()

        result = self.client_post(url, payload, **post_params)
        self.assert_json_success(result)

        # Check the correct message was sent
        msg = self.get_last_message()

        if msg.id == prior_msg.id:
            raise Exception('''
                Your test code called an endpoint that did
                not write any new messages.  It is probably
                broken (but still returns 200 due to exception
                handling).
                ''')  # nocoverage

        self.assertEqual(msg.sender.email, user_profile.email)
        if stream_name is not None:
            self.assertEqual(get_display_recipient(msg.recipient), stream_name)
        # TODO: should also validate recipient for private messages

        return msg
Example #10
0
def send_to_missed_message_address(address, message):
    # type: (text_type, message.Message) -> None
    token = get_missed_message_token_from_address(address)
    key = missed_message_redis_key(token)
    result = redis_client.hmget(key, "user_profile_id", "recipient_id", "subject")
    if not all(val is not None for val in result):
        raise ZulipEmailForwardError("Missing missed message address data")
    user_profile_id, recipient_id, subject = result

    user_profile = get_user_profile_by_id(user_profile_id)
    recipient = Recipient.objects.get(id=recipient_id)
    display_recipient = get_display_recipient(recipient)

    # Testing with basestring so we don't depend on the list return type from
    # get_display_recipient
    if not isinstance(display_recipient, six.string_types):
        recipient_str = ",".join([user["email"] for user in display_recipient])
    else:
        recipient_str = display_recipient

    body = filter_footer(extract_body(message))
    body += extract_and_upload_attachments(message, user_profile.realm)
    if not body:
        body = "(No email body)"

    if recipient.type == Recipient.STREAM:
        recipient_type_name = "stream"
    else:
        recipient_type_name = "private"

    internal_send_message(user_profile.email, recipient_type_name, recipient_str, subject, body)
    logging.info("Successfully processed email from %s to %s" % (user_profile.email, recipient_str))
Example #11
0
def do_send_missedmessage_events(user_profile, missed_messages, message_count):
    """
    Send a reminder email and/or push notifications to a user if she's missed some PMs by being offline

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of Message objects to remind about
    """
    # Disabled missedmessage emails internally
    if not user_profile.enable_offline_email_notifications:
        return

    senders = set(m.sender.full_name for m in missed_messages)
    sender_str = ", ".join(senders)
    plural_messages = 's' if len(missed_messages) > 1 else ''
    template_payload = {'name': user_profile.full_name,
                        'messages': build_message_list(user_profile, missed_messages),
                        'message_count': message_count,
                        'url': 'https://%s' % (settings.EXTERNAL_HOST,),
                        'reply_warning': False,
                        'external_host': settings.EXTERNAL_HOST}
    headers = {}
    if all(msg.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL)
            for msg in missed_messages):
        # If we have one huddle, set a reply-to to all of the members
        # of the huddle except the user herself
        disp_recipients = [", ".join(recipient['email']
                                for recipient in get_display_recipient(mesg.recipient)
                                    if recipient['email'] != user_profile.email)
                                 for mesg in missed_messages]
        if all(msg.recipient.type == Recipient.HUDDLE for msg in missed_messages) and \
            len(set(disp_recipients)) == 1:
            headers['Reply-To'] = disp_recipients[0]
        elif len(senders) == 1:
            headers['Reply-To'] = missed_messages[0].sender.email
        else:
            template_payload['reply_warning'] = True
    else:
        # There are some @-mentions mixed in with personals
        template_payload['mention'] = True
        template_payload['reply_warning'] = True
        headers['Reply-To'] = "Nobody <%s>" % (settings.NOREPLY_EMAIL_ADDRESS,)

    # Give users a one-click unsubscribe link they can use to stop getting
    # missed message emails without having to log in first.
    unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages")
    template_payload["unsubscribe_link"] = unsubscribe_link

    subject = "Missed Zulip%s from %s" % (plural_messages, sender_str)
    from_email = "%s (via Zulip) <%s>" % (sender_str, settings.NOREPLY_EMAIL_ADDRESS)

    text_content = loader.render_to_string('zerver/missed_message_email.txt', template_payload)
    html_content = loader.render_to_string('zerver/missed_message_email_html.txt', template_payload)

    msg = EmailMultiAlternatives(subject, text_content, from_email, [user_profile.email],
                                 headers = headers)
    msg.attach_alternative(html_content, "text/html")
    msg.send()

    user_profile.last_reminder = datetime.datetime.now()
    user_profile.save(update_fields=['last_reminder'])
Example #12
0
def send_to_missed_message_address(address, message):
    # type: (Text, message.Message) -> None
    token = get_missed_message_token_from_address(address)
    key = missed_message_redis_key(token)
    result = redis_client.hmget(key, 'user_profile_id', 'recipient_id', 'subject')
    if not all(val is not None for val in result):
        raise ZulipEmailForwardError('Missing missed message address data')
    user_profile_id, recipient_id, subject_b = result  # type: (bytes, bytes, bytes)

    user_profile = get_user_profile_by_id(user_profile_id)
    recipient = Recipient.objects.get(id=recipient_id)
    display_recipient = get_display_recipient(recipient)

    # Testing with basestring so we don't depend on the list return type from
    # get_display_recipient
    if not isinstance(display_recipient, str):
        recipient_str = u','.join([user['email'] for user in display_recipient])
    else:
        recipient_str = display_recipient

    body = construct_zulip_body(message, user_profile.realm)

    if recipient.type == Recipient.STREAM:
        recipient_type_name = 'stream'
    else:
        recipient_type_name = 'private'

    internal_send_message(user_profile.realm, user_profile.email,
                          recipient_type_name, recipient_str,
                          subject_b.decode('utf-8'), body)
    logger.info("Successfully processed email from %s to %s" % (
        user_profile.email, recipient_str))
Example #13
0
    def test_receive_stream_email_multiple_recipient_success(self) -> None:
        user_profile = self.example_user('hamlet')
        self.login(user_profile.email)
        self.subscribe(user_profile, "Denmark")
        stream = get_stream("Denmark", user_profile.realm)

        # stream address is angle-addr within multiple addresses
        stream_to_addresses = ["A.N. Other <*****@*****.**>",
                               "Denmark <{}>".format(encode_email_address(stream))]

        incoming_valid_message = MIMEText('TestStreamEmailMessages Body')

        incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject'
        incoming_valid_message['From'] = self.example_email('hamlet')
        incoming_valid_message['To'] = ", ".join(stream_to_addresses)
        incoming_valid_message['Reply-to'] = self.example_email('othello')

        process_message(incoming_valid_message)

        # Hamlet is subscribed to this stream so should see the email message from Othello.
        message = most_recent_message(user_profile)

        self.assertEqual(message.content, "TestStreamEmailMessages Body")
        self.assertEqual(get_display_recipient(message.recipient), stream.name)
        self.assertEqual(message.topic_name(), incoming_valid_message['Subject'])
Example #14
0
 def get_streams(self, email: str, realm: Realm) -> List[str]:
     """
     Helper function to get the stream names for a user
     """
     user_profile = get_user(email, realm)
     subs = get_stream_subscriptions_for_user(user_profile).filter(
         active=True,
     )
     return [cast(str, get_display_recipient(sub.recipient)) for sub in subs]
 def test_stream_message_to_embedded_bot(self) -> None:
     self.send_stream_message(self.user_profile.email, "Denmark",
                              content="@**{}** foo".format(self.bot_profile.full_name),
                              topic_name="bar")
     last_message = self.get_last_message()
     self.assertEqual(last_message.content, "beep boop")
     self.assertEqual(last_message.sender_id, self.bot_profile.id)
     self.assertEqual(last_message.subject, "bar")
     display_recipient = get_display_recipient(last_message.recipient)
     self.assertEqual(display_recipient, "Denmark")
 def test_stream_message_to_outgoing_webhook_bot(self, mock_requests_request: mock.Mock) -> None:
     self.send_stream_message(self.user_profile.email, "Denmark",
                              content="@**{}** foo".format(self.bot_profile.full_name),
                              topic_name="bar")
     last_message = self.get_last_message()
     self.assertEqual(last_message.content, "Success! Hidley ho, I'm a webhook responding!")
     self.assertEqual(last_message.sender_id, self.bot_profile.id)
     self.assertEqual(last_message.subject, "bar")
     display_recipient = get_display_recipient(last_message.recipient)
     self.assertEqual(display_recipient, "Denmark")
 def test_pm_to_outgoing_webhook_bot(self, mock_requests_request: mock.Mock) -> None:
     self.send_personal_message(self.user_profile.email, self.bot_profile.email,
                                content="foo")
     last_message = self.get_last_message()
     self.assertEqual(last_message.content, "Success! Hidley ho, I'm a webhook responding!")
     self.assertEqual(last_message.sender_id, self.bot_profile.id)
     display_recipient = get_display_recipient(last_message.recipient)
     # The next two lines error on mypy because the display_recipient is of type Union[Text, List[Dict[str, Any]]].
     # In this case, we know that display_recipient will be of type List[Dict[str, Any]].
     # Otherwise this test will error, which is wanted behavior anyway.
     self.assert_length(display_recipient, 1)  # type: ignore
     self.assertEqual(display_recipient[0]['email'], self.user_profile.email)   # type: ignore
 def test_pm_to_embedded_bot(self) -> None:
     self.send_personal_message(self.user_profile.email, self.bot_profile.email,
                                content="help")
     last_message = self.get_last_message()
     self.assertEqual(last_message.content, "beep boop")
     self.assertEqual(last_message.sender_id, self.bot_profile.id)
     display_recipient = get_display_recipient(last_message.recipient)
     # The next two lines error on mypy because the display_recipient is of type Union[Text, List[Dict[str, Any]]].
     # In this case, we know that display_recipient will be of type List[Dict[str, Any]].
     # Otherwise this test will error, which is wanted behavior anyway.
     self.assert_length(display_recipient, 1)  # type: ignore
     self.assertEqual(display_recipient[0]['email'], self.user_profile.email)   # type: ignore
Example #19
0
def get_gcm_alert(message: Message) -> str:
    """
    Determine what alert string to display based on the missed messages.
    """
    sender_str = message.sender.full_name
    if message.recipient.type == Recipient.HUDDLE and message.trigger == 'private_message':
        return "New private group message from %s" % (sender_str,)
    elif message.recipient.type == Recipient.PERSONAL and message.trigger == 'private_message':
        return "New private message from %s" % (sender_str,)
    elif message.is_stream_message() and message.trigger == 'mentioned':
        return "New mention from %s" % (sender_str,)
    else:  # message.is_stream_message() and message.trigger == 'stream_push_notify'
        return "New stream message from %s in %s" % (sender_str, get_display_recipient(message.recipient),)
Example #20
0
def send_to_missed_message_address(address: str, message: message.Message) -> None:
    token = get_missed_message_token_from_address(address)
    key = missed_message_redis_key(token)
    result = redis_client.hmget(key, 'user_profile_id', 'recipient_id', 'subject')
    if not all(val is not None for val in result):
        raise ZulipEmailForwardError('Missing missed message address data')
    user_profile_id, recipient_id, subject_b = result  # type: (bytes, bytes, bytes)

    user_profile = get_user_profile_by_id(user_profile_id)
    recipient = Recipient.objects.get(id=recipient_id)

    body = construct_zulip_body(message, user_profile.realm)

    if recipient.type == Recipient.STREAM:
        stream = get_stream_by_id_in_realm(recipient.type_id, user_profile.realm)
        internal_send_stream_message(
            user_profile.realm, user_profile, stream,
            subject_b.decode('utf-8'), body
        )
        recipient_str = stream.name
    elif recipient.type == Recipient.PERSONAL:
        display_recipient = get_display_recipient(recipient)
        assert not isinstance(display_recipient, str)
        recipient_str = display_recipient[0]['email']
        recipient_user = get_user(recipient_str, user_profile.realm)
        internal_send_private_message(user_profile.realm, user_profile,
                                      recipient_user, body)
    elif recipient.type == Recipient.HUDDLE:
        display_recipient = get_display_recipient(recipient)
        assert not isinstance(display_recipient, str)
        emails = [user_dict['email'] for user_dict in display_recipient]
        recipient_str = ', '.join(emails)
        internal_send_huddle_message(user_profile.realm, user_profile,
                                     emails, body)
    else:
        raise AssertionError("Invalid recipient type!")

    logger.info("Successfully processed email from %s to %s" % (
        user_profile.email, recipient_str))
Example #21
0
    def test_recipient_for_user_ids(self) -> None:
        hamlet = self.example_user('hamlet')
        othello = self.example_user('othello')
        cross_realm_bot = self.example_user('welcome_bot')
        sender = self.example_user('iago')
        recipient_user_ids = [hamlet.id, othello.id, cross_realm_bot.id]

        result = recipient_for_user_ids(recipient_user_ids, sender)
        recipient = get_display_recipient(result)
        recipient_ids = [recipient[0]['id'], recipient[1]['id'],  # type: ignore
                         recipient[2]['id'], recipient[3]['id']]  # type: ignore

        expected_recipient_ids = [hamlet.id, othello.id,
                                  sender.id, cross_realm_bot.id]
        self.assertEqual(set(recipient_ids), set(expected_recipient_ids))
Example #22
0
def get_message_payload(message: Message) -> Dict[str, Any]:
    '''Common fields for `message` payloads, for all platforms.'''
    data = get_base_payload(message.sender.realm)

    # `sender_id` is preferred, but some existing versions use `sender_email`.
    data['sender_id'] = message.sender.id
    data['sender_email'] = message.sender.email

    if message.recipient.type == Recipient.STREAM:
        data['recipient_type'] = "stream"
        data['stream'] = get_display_recipient(message.recipient)
        data['topic'] = message.topic_name()
    elif message.recipient.type == Recipient.HUDDLE:
        data['recipient_type'] = "private"
        data['pm_users'] = huddle_users(message.recipient.id)
    else:  # Recipient.PERSONAL
        data['recipient_type'] = "private"

    return data
Example #23
0
def get_common_payload(message: Message) -> Dict[str, Any]:
    data = {}  # type: Dict[str, Any]

    # These will let the app support logging into multiple realms and servers.
    data['server'] = settings.EXTERNAL_HOST
    data['realm_id'] = message.sender.realm.id

    # `sender_id` is preferred, but some existing versions use `sender_email`.
    data['sender_id'] = message.sender.id
    data['sender_email'] = message.sender.email

    if message.is_stream_message():
        data['recipient_type'] = "stream"
        data['stream'] = get_display_recipient(message.recipient)
        data['topic'] = message.subject
    else:
        data['recipient_type'] = "private"

    return data
Example #24
0
 def message_header(user_profile, message):
     disp_recipient = get_display_recipient(message.recipient)
     if message.recipient.type == Recipient.PERSONAL:
         header = "You and %s" % (message.sender.full_name)
         html_link = pm_narrow_url([message.sender.email])
         header_html = "<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
     elif message.recipient.type == Recipient.HUDDLE:
         other_recipients = [r['full_name'] for r in disp_recipient
                                 if r['email'] != user_profile.email]
         header = "You and %s" % (", ".join(other_recipients),)
         html_link = pm_narrow_url([r["email"] for r in disp_recipient
                                    if r["email"] != user_profile.email])
         header_html = "<a style='color: #ffffff;' href='%s'>%s</a>" % (html_link, header)
     else:
         header = "%s > %s" % (disp_recipient, message.subject)
         stream_link = stream_narrow_url(disp_recipient)
         topic_link = topic_narrow_url(disp_recipient, message.subject)
         header_html = "<a href='%s'>%s</a> > <a href='%s'>%s</a>" % (
             stream_link, disp_recipient, topic_link, message.subject)
     return {"plain": header,
             "html": header_html,
             "stream_message": message.recipient.type_name() == "stream"}
Example #25
0
    def test_receive_stream_email_messages_blank_subject_success(self) -> None:
        user_profile = self.example_user('hamlet')
        self.login(user_profile.email)
        self.subscribe(user_profile, "Denmark")
        stream = get_stream("Denmark", user_profile.realm)

        stream_to_address = encode_email_address(stream)

        incoming_valid_message = MIMEText('TestStreamEmailMessages Body')

        incoming_valid_message['Subject'] = ''
        incoming_valid_message['From'] = self.example_email('hamlet')
        incoming_valid_message['To'] = stream_to_address
        incoming_valid_message['Reply-to'] = self.example_email('othello')

        process_message(incoming_valid_message)

        # Hamlet is subscribed to this stream so should see the email message from Othello.
        message = most_recent_message(user_profile)

        self.assertEqual(message.content, "TestStreamEmailMessages Body")
        self.assertEqual(get_display_recipient(message.recipient), stream.name)
        self.assertEqual(message.topic_name(), "(no topic)")
Example #26
0
def get_common_payload(message: Message) -> Dict[str, Any]:
    data = {}  # type: Dict[str, Any]

    # These will let the app support logging into multiple realms and servers.
    data['server'] = settings.EXTERNAL_HOST
    data['realm_id'] = message.sender.realm.id
    data['realm_uri'] = message.sender.realm.uri

    # `sender_id` is preferred, but some existing versions use `sender_email`.
    data['sender_id'] = message.sender.id
    data['sender_email'] = message.sender.email

    if message.recipient.type == Recipient.STREAM:
        data['recipient_type'] = "stream"
        data['stream'] = get_display_recipient(message.recipient)
        data['topic'] = message.topic_name()
    elif message.recipient.type == Recipient.HUDDLE:
        data['recipient_type'] = "private"
        data['pm_users'] = huddle_users(message.recipient.id)
    else:  # Recipient.PERSONAL
        data['recipient_type'] = "private"

    return data
Example #27
0
    def test_receive_stream_email_show_sender_success(self) -> None:
        user_profile = self.example_user('hamlet')
        self.login(user_profile.email)
        self.subscribe(user_profile, "Denmark")
        stream = get_stream("Denmark", user_profile.realm)

        stream_to_address = encode_email_address(stream)
        parts = stream_to_address.split('@')
        parts[0] += "+show-sender"
        stream_to_address = '@'.join(parts)

        incoming_valid_message = MIMEText('TestStreamEmailMessages Body')
        incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject'
        incoming_valid_message['From'] = self.example_email('hamlet')
        incoming_valid_message['To'] = stream_to_address
        incoming_valid_message['Reply-to'] = self.example_email('othello')

        process_message(incoming_valid_message)
        message = most_recent_message(user_profile)

        self.assertEqual(message.content, "From: %s\n%s" % (self.example_email('hamlet'),
                                                            "TestStreamEmailMessages Body"))
        self.assertEqual(get_display_recipient(message.recipient), stream.name)
        self.assertEqual(message.topic_name(), incoming_valid_message['Subject'])
Example #28
0
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile,
                                                missed_messages: List[Message],
                                                message_count: int) -> None:
    """
    Send a reminder email to a user if she's missed some PMs by being offline.

    The email will have its reply to address set to a limited used email
    address that will send a zulip message to the correct recipient. This
    allows the user to respond to missed PMs, huddles, and @-mentions directly
    from the email.

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of Message objects to remind about they should
                      all have the same recipient and subject
    """
    from zerver.context_processors import common_context
    # Disabled missedmessage emails internally
    if not user_profile.enable_offline_email_notifications:
        return

    recipients = set((msg.recipient_id, msg.subject) for msg in missed_messages)
    if len(recipients) != 1:
        raise ValueError(
            'All missed_messages must have the same recipient and subject %r' %
            recipients
        )

    unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages")
    context = common_context(user_profile)
    context.update({
        'name': user_profile.full_name,
        'message_count': message_count,
        'mention': missed_messages[0].is_stream_message(),
        'unsubscribe_link': unsubscribe_link,
        'realm_name_in_notifications': user_profile.realm_name_in_notifications,
        'show_message_content': user_profile.message_content_in_email_notifications,
    })

    # If this setting (email mirroring integration) is enabled, only then
    # can users reply to email to send message to Zulip. Thus, one must
    # ensure to display warning in the template.
    if settings.EMAIL_GATEWAY_PATTERN:
        context.update({
            'reply_warning': False,
            'reply_to_zulip': True,
        })
    else:
        context.update({
            'reply_warning': True,
            'reply_to_zulip': False,
        })

    from zerver.lib.email_mirror import create_missed_message_address
    reply_to_address = create_missed_message_address(user_profile, missed_messages[0])
    if reply_to_address == FromAddress.NOREPLY:
        reply_to_name = None
    else:
        reply_to_name = "Zulip"

    senders = list(set(m.sender for m in missed_messages))
    if (missed_messages[0].recipient.type == Recipient.HUDDLE):
        display_recipient = get_display_recipient(missed_messages[0].recipient)
        # Make sure that this is a list of strings, not a string.
        assert not isinstance(display_recipient, str)
        other_recipients = [r['full_name'] for r in display_recipient
                            if r['id'] != user_profile.id]
        context.update({'group_pm': True})
        if len(other_recipients) == 2:
            huddle_display_name = "%s" % (" and ".join(other_recipients))
            context.update({'huddle_display_name': huddle_display_name})
        elif len(other_recipients) == 3:
            huddle_display_name = "%s, %s, and %s" % (
                other_recipients[0], other_recipients[1], other_recipients[2])
            context.update({'huddle_display_name': huddle_display_name})
        else:
            huddle_display_name = "%s, and %s others" % (
                ', '.join(other_recipients[:2]), len(other_recipients) - 2)
            context.update({'huddle_display_name': huddle_display_name})
    elif (missed_messages[0].recipient.type == Recipient.PERSONAL):
        context.update({'private_message': True})
    else:
        # Keep only the senders who actually mentioned the user
        #
        # TODO: When we add wildcard mentions that send emails, add
        # them to the filter here.
        senders = list(set(m.sender for m in missed_messages if
                           UserMessage.objects.filter(message=m, user_profile=user_profile,
                                                      flags=UserMessage.flags.mentioned).exists()))
        context.update({'at_mention': True})

    # If message content is disabled, then flush all information we pass to email.
    if not user_profile.message_content_in_email_notifications:
        context.update({
            'reply_to_zulip': False,
            'messages': [],
            'sender_str': "",
            'realm_str': user_profile.realm.name,
            'huddle_display_name': "",
        })
    else:
        context.update({
            'messages': build_message_list(user_profile, missed_messages),
            'sender_str': ", ".join(sender.full_name for sender in senders),
            'realm_str': user_profile.realm.name,
        })

    from_name = "Zulip missed messages"  # type: str
    from_address = FromAddress.NOREPLY
    if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER:
        # If this setting is enabled, you can reply to the Zulip
        # missed message emails directly back to the original sender.
        # However, one must ensure the Zulip server is in the SPF
        # record for the domain, or there will be spam/deliverability
        # problems.
        sender = senders[0]
        from_name, from_address = (sender.full_name, sender.email)
        context.update({
            'reply_warning': False,
            'reply_to_zulip': False,
        })

    email_dict = {
        'template_prefix': 'zerver/emails/missed_message',
        'to_user_id': user_profile.id,
        'from_name': from_name,
        'from_address': from_address,
        'reply_to_email': formataddr((reply_to_name, reply_to_address)),
        'context': context}
    queue_json_publish("email_senders", email_dict)

    user_profile.last_reminder = timezone_now()
    user_profile.save(update_fields=['last_reminder'])
Example #29
0
def do_send_missedmessage_events(user_profile, missed_messages, message_count):
    # type: (UserProfile, List[Message], int) -> None
    """
    Send a reminder email and/or push notifications to a user if she's missed some PMs by being offline

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of Message objects to remind about
    """
    # Disabled missedmessage emails internally
    if not user_profile.enable_offline_email_notifications:
        return

    senders = set(m.sender.full_name for m in missed_messages)
    sender_str = ", ".join(senders)
    plural_messages = 's' if len(missed_messages) > 1 else ''
    template_payload = {
        'name': user_profile.full_name,
        'messages': build_message_list(user_profile, missed_messages),
        'message_count': message_count,
        'url': 'https://%s' % (settings.EXTERNAL_HOST, ),
        'reply_warning': False,
        'external_host': settings.EXTERNAL_HOST
    }
    headers = {}
    if all(msg.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL)
           for msg in missed_messages):
        # If we have one huddle, set a reply-to to all of the members
        # of the huddle except the user herself
        disp_recipients = [
            ", ".join(recipient['email'] for recipient in cast(
                Sequence[Mapping[str,
                                 Any]], get_display_recipient(mesg.recipient))
                      if recipient['email'] != user_profile.email)
            for mesg in missed_messages
        ]
        if all(msg.recipient.type == Recipient.HUDDLE for msg in missed_messages) and \
            len(set(disp_recipients)) == 1:
            headers['Reply-To'] = disp_recipients[0]
        elif len(senders) == 1:
            headers['Reply-To'] = missed_messages[0].sender.email
        else:
            template_payload['reply_warning'] = True
    else:
        # There are some @-mentions mixed in with personals
        template_payload['mention'] = True
        template_payload['reply_warning'] = True
        headers['Reply-To'] = "Nobody <%s>" % (
            settings.NOREPLY_EMAIL_ADDRESS, )

    # Give users a one-click unsubscribe link they can use to stop getting
    # missed message emails without having to log in first.
    unsubscribe_link = one_click_unsubscribe_link(user_profile,
                                                  "missed_messages")
    template_payload["unsubscribe_link"] = unsubscribe_link

    subject = "Missed Zulip%s from %s" % (plural_messages, sender_str)
    from_email = "%s (via Zulip) <%s>" % (sender_str,
                                          settings.NOREPLY_EMAIL_ADDRESS)

    text_content = loader.render_to_string('zerver/missed_message_email.txt',
                                           template_payload)
    html_content = loader.render_to_string(
        'zerver/missed_message_email_html.txt', template_payload)

    msg = EmailMultiAlternatives(subject,
                                 text_content,
                                 from_email, [user_profile.email],
                                 headers=headers)
    msg.attach_alternative(html_content, "text/html")
    msg.send()

    user_profile.last_reminder = datetime.datetime.now()
    user_profile.save(update_fields=['last_reminder'])
Example #30
0
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages,
                                                message_count):
    # type: (UserProfile, List[Message], int) -> None
    """
    Send a reminder email to a user if she's missed some PMs by being offline.

    The email will have its reply to address set to a limited used email
    address that will send a zulip message to the correct recipient. This
    allows the user to respond to missed PMs, huddles, and @-mentions directly
    from the email.

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of Message objects to remind about they should
                      all have the same recipient and subject
    """
    from zerver.context_processors import common_context
    # Disabled missedmessage emails internally
    if not user_profile.enable_offline_email_notifications:
        return

    recipients = set(
        (msg.recipient_id, msg.subject) for msg in missed_messages)
    if len(recipients) != 1:
        raise ValueError(
            'All missed_messages must have the same recipient and subject %r' %
            recipients)

    unsubscribe_link = one_click_unsubscribe_link(user_profile,
                                                  "missed_messages")
    context = common_context(user_profile)
    context.update({
        'name':
        user_profile.full_name,
        'messages':
        build_message_list(user_profile, missed_messages),
        'message_count':
        message_count,
        'mention':
        missed_messages[0].recipient.type == Recipient.STREAM,
        'unsubscribe_link':
        unsubscribe_link,
    })

    # If this setting (email mirroring integration) is enabled, only then
    # can users reply to email to send message to Zulip. Thus, one must
    # ensure to display warning in the template.
    if settings.EMAIL_GATEWAY_PATTERN:
        context.update({
            'reply_warning': False,
            'reply_to_zulip': True,
        })
    else:
        context.update({
            'reply_warning': True,
            'reply_to_zulip': False,
        })

    from zerver.lib.email_mirror import create_missed_message_address
    reply_to_address = create_missed_message_address(user_profile,
                                                     missed_messages[0])
    if reply_to_address == FromAddress.NOREPLY:
        reply_to_name = None
    else:
        reply_to_name = "Zulip"

    senders = list(set(m.sender for m in missed_messages))
    if (missed_messages[0].recipient.type == Recipient.HUDDLE):
        display_recipient = get_display_recipient(missed_messages[0].recipient)
        # Make sure that this is a list of strings, not a string.
        assert not isinstance(display_recipient, Text)
        other_recipients = [
            r['full_name'] for r in display_recipient
            if r['id'] != user_profile.id
        ]
        context.update({'group_pm': True})
        if len(other_recipients) == 2:
            huddle_display_name = u"%s" % (" and ".join(other_recipients))
            context.update({'huddle_display_name': huddle_display_name})
        elif len(other_recipients) == 3:
            huddle_display_name = u"%s, %s, and %s" % (
                other_recipients[0], other_recipients[1], other_recipients[2])
            context.update({'huddle_display_name': huddle_display_name})
        else:
            huddle_display_name = u"%s, and %s others" % (', '.join(
                other_recipients[:2]), len(other_recipients) - 2)
            context.update({'huddle_display_name': huddle_display_name})
    elif (missed_messages[0].recipient.type == Recipient.PERSONAL):
        context.update({'private_message': True})
    else:
        # Keep only the senders who actually mentioned the user
        #
        # TODO: When we add wildcard mentions that send emails, add
        # them to the filter here.
        senders = list(
            set(m.sender for m in missed_messages
                if UserMessage.objects.filter(
                    message=m,
                    user_profile=user_profile,
                    flags=UserMessage.flags.mentioned).exists()))
        context.update({'at_mention': True})

    context.update({
        'sender_str':
        ", ".join(sender.full_name for sender in senders),
        'realm_str':
        user_profile.realm.name,
    })

    from_name, from_address = None, None
    if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER:
        # If this setting is enabled, you can reply to the Zulip
        # missed message emails directly back to the original sender.
        # However, one must ensure the Zulip server is in the SPF
        # record for the domain, or there will be spam/deliverability
        # problems.
        sender = senders[0]
        from_name, from_address = (sender.full_name, sender.email)
        context.update({
            'reply_warning': False,
            'reply_to_zulip': False,
        })

    email_dict = {
        'template_prefix': 'zerver/emails/missed_message',
        'to_user_id': user_profile.id,
        'from_name': from_name,
        'from_address': from_address,
        'reply_to_email': formataddr((reply_to_name, reply_to_address)),
        'context': context
    }
    queue_json_publish("missedmessage_email_senders", email_dict,
                       send_email_from_dict)

    user_profile.last_reminder = timezone_now()
    user_profile.save(update_fields=['last_reminder'])
Example #31
0
def handle_push_notification(user_profile_id, missed_message):
    # type: (int, Dict[str, Any]) -> None
    try:
        user_profile = get_user_profile_by_id(user_profile_id)
        if not (receives_offline_notifications(user_profile)
                or receives_online_notifications(user_profile)):
            return

        umessage = UserMessage.objects.get(
            user_profile=user_profile,
            message__id=missed_message['message_id'])
        message = umessage.message
        if umessage.flags.read:
            return
        sender_str = message.sender.full_name

        android_devices = [
            device for device in PushDeviceToken.objects.filter(
                user=user_profile, kind=PushDeviceToken.GCM)
        ]
        apple_devices = list(
            PushDeviceToken.objects.filter(user=user_profile,
                                           kind=PushDeviceToken.APNS))

        if apple_devices or android_devices:
            # TODO: set badge count in a better way
            # Determine what alert string to display based on the missed messages
            if message.recipient.type == Recipient.HUDDLE:
                alert = "New private group message from %s" % (sender_str, )
            elif message.recipient.type == Recipient.PERSONAL:
                alert = "New private message from %s" % (sender_str, )
            elif message.recipient.type == Recipient.STREAM:
                alert = "New mention from %s" % (sender_str, )
            else:
                alert = "New Zulip mentions and private messages from %s" % (
                    sender_str, )

            if apple_devices:
                apple_extra_data = {
                    'alert': alert,
                    'message_ids': [message.id],
                }
                send_apple_push_notification(user_profile.id,
                                             apple_devices,
                                             badge=1,
                                             zulip=apple_extra_data)

            if android_devices:
                content = message.content
                content_truncated = (len(content) > 200)
                if content_truncated:
                    content = content[:200] + "..."

                android_data = {
                    'user': user_profile.email,
                    'event': 'message',
                    'alert': alert,
                    'zulip_message_id':
                    message.id,  # message_id is reserved for CCS
                    'time': datetime_to_timestamp(message.pub_date),
                    'content': content,
                    'content_truncated': content_truncated,
                    'sender_email': message.sender.email,
                    'sender_full_name': message.sender.full_name,
                    'sender_avatar_url': avatar_url(message.sender),
                }

                if message.recipient.type == Recipient.STREAM:
                    android_data['recipient_type'] = "stream"
                    android_data['stream'] = get_display_recipient(
                        message.recipient)
                    android_data['topic'] = message.subject
                elif message.recipient.type in (Recipient.HUDDLE,
                                                Recipient.PERSONAL):
                    android_data['recipient_type'] = "private"

                send_android_push_notification(android_devices, android_data)

    except UserMessage.DoesNotExist:
        logging.error("Could not find UserMessage with message_id %s" %
                      (missed_message['message_id'], ))
Example #32
0
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile,
                                                missed_messages: List[Dict[
                                                    str, Any]],
                                                message_count: int) -> None:
    """
    Send a reminder email to a user if she's missed some PMs by being offline.

    The email will have its reply to address set to a limited used email
    address that will send a zulip message to the correct recipient. This
    allows the user to respond to missed PMs, huddles, and @-mentions directly
    from the email.

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of dictionaries to Message objects and other data
                      for a group of messages that share a recipient (and topic)
    """
    from zerver.context_processors import common_context
    # Disabled missedmessage emails internally
    if not user_profile.enable_offline_email_notifications:
        return

    recipients = set((msg['message'].recipient_id, msg['message'].topic_name())
                     for msg in missed_messages)
    if len(recipients) != 1:
        raise ValueError(
            'All missed_messages must have the same recipient and topic %r' %
            (recipients, ))

    # This link is no longer a part of the email, but keeping the code in case
    # we find a clean way to add it back in the future
    unsubscribe_link = one_click_unsubscribe_link(user_profile,
                                                  "missed_messages")
    context = common_context(user_profile)
    context.update({
        'name':
        user_profile.full_name,
        'message_count':
        message_count,
        'unsubscribe_link':
        unsubscribe_link,
        'realm_name_in_notifications':
        user_profile.realm_name_in_notifications,
        'show_message_content':
        message_content_allowed_in_missedmessage_emails(user_profile)
    })

    triggers = list(message['trigger'] for message in missed_messages)
    unique_triggers = set(triggers)
    context.update({
        'mention':
        'mentioned' in unique_triggers
        or 'wildcard_mentioned' in unique_triggers,
        'stream_email_notify':
        'stream_email_notify' in unique_triggers,
        'mention_count':
        triggers.count('mentioned') + triggers.count("wildcard_mentioned"),
    })

    # If this setting (email mirroring integration) is enabled, only then
    # can users reply to email to send message to Zulip. Thus, one must
    # ensure to display warning in the template.
    if settings.EMAIL_GATEWAY_PATTERN:
        context.update({
            'reply_to_zulip': True,
        })
    else:
        context.update({
            'reply_to_zulip': False,
        })

    from zerver.lib.email_mirror import create_missed_message_address
    reply_to_address = create_missed_message_address(
        user_profile, missed_messages[0]['message'])
    if reply_to_address == FromAddress.NOREPLY:
        reply_to_name = None
    else:
        reply_to_name = "Zulip"

    narrow_url = get_narrow_url(user_profile, missed_messages[0]['message'])
    context.update({
        'narrow_url': narrow_url,
    })

    senders = list(set(m['message'].sender for m in missed_messages))
    if (missed_messages[0]['message'].recipient.type == Recipient.HUDDLE):
        display_recipient = get_display_recipient(
            missed_messages[0]['message'].recipient)
        # Make sure that this is a list of strings, not a string.
        assert not isinstance(display_recipient, str)
        other_recipients = [
            r['full_name'] for r in display_recipient
            if r['id'] != user_profile.id
        ]
        context.update({'group_pm': True})
        if len(other_recipients) == 2:
            huddle_display_name = " and ".join(other_recipients)
            context.update({'huddle_display_name': huddle_display_name})
        elif len(other_recipients) == 3:
            huddle_display_name = "%s, %s, and %s" % (
                other_recipients[0], other_recipients[1], other_recipients[2])
            context.update({'huddle_display_name': huddle_display_name})
        else:
            huddle_display_name = "%s, and %s others" % (', '.join(
                other_recipients[:2]), len(other_recipients) - 2)
            context.update({'huddle_display_name': huddle_display_name})
    elif (missed_messages[0]['message'].recipient.type == Recipient.PERSONAL):
        context.update({'private_message': True})
    elif (context['mention'] or context['stream_email_notify']):
        # Keep only the senders who actually mentioned the user
        if context['mention']:
            senders = list(
                set(m['message'].sender for m in missed_messages
                    if m['trigger'] == 'mentioned'
                    or m['trigger'] == 'wildcard_mentioned'))
        message = missed_messages[0]['message']
        stream = Stream.objects.only('id',
                                     'name').get(id=message.recipient.type_id)
        stream_header = "%s > %s" % (stream.name, message.topic_name())
        context.update({
            'stream_header': stream_header,
        })
    else:
        raise AssertionError("Invalid messages!")

    # If message content is disabled, then flush all information we pass to email.
    if not message_content_allowed_in_missedmessage_emails(user_profile):
        context.update({
            'reply_to_zulip': False,
            'messages': [],
            'sender_str': "",
            'realm_str': user_profile.realm.name,
            'huddle_display_name': "",
        })
    else:
        context.update({
            'messages':
            build_message_list(user_profile,
                               list(m['message'] for m in missed_messages)),
            'sender_str':
            ", ".join(sender.full_name for sender in senders),
            'realm_str':
            user_profile.realm.name,
        })

    from_name = "Zulip missed messages"  # type: str
    from_address = FromAddress.NOREPLY
    if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER:
        # If this setting is enabled, you can reply to the Zulip
        # missed message emails directly back to the original sender.
        # However, one must ensure the Zulip server is in the SPF
        # record for the domain, or there will be spam/deliverability
        # problems.
        sender = senders[0]
        from_name, from_address = (sender.full_name, sender.email)
        context.update({
            'reply_to_zulip': False,
        })

    email_dict = {
        'template_prefix': 'zerver/emails/missed_message',
        'to_user_ids': [user_profile.id],
        'from_name': from_name,
        'from_address': from_address,
        'reply_to_email': formataddr((reply_to_name, reply_to_address)),
        'context': context
    }
    queue_json_publish("email_senders", email_dict)

    user_profile.last_reminder = timezone_now()
    user_profile.save(update_fields=['last_reminder'])
Example #33
0
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile,
                                                missed_messages: List[Dict[
                                                    str, Any]],
                                                message_count: int) -> None:
    """
    Send a reminder email to a user if she's missed some PMs by being offline.

    The email will have its reply to address set to a limited used email
    address that will send a Zulip message to the correct recipient. This
    allows the user to respond to missed PMs, huddles, and @-mentions directly
    from the email.

    `user_profile` is the user to send the reminder to
    `missed_messages` is a list of dictionaries to Message objects and other data
                      for a group of messages that share a recipient (and topic)
    """
    from zerver.context_processors import common_context

    recipients = {(msg["message"].recipient_id, msg["message"].topic_name())
                  for msg in missed_messages}
    if len(recipients) != 1:
        raise ValueError(
            f"All missed_messages must have the same recipient and topic {recipients!r}",
        )

    # This link is no longer a part of the email, but keeping the code in case
    # we find a clean way to add it back in the future
    unsubscribe_link = one_click_unsubscribe_link(user_profile,
                                                  "missed_messages")
    context = common_context(user_profile)
    context.update(
        name=user_profile.full_name,
        message_count=message_count,
        unsubscribe_link=unsubscribe_link,
        realm_name_in_notifications=user_profile.realm_name_in_notifications,
    )

    mentioned_user_group_name = get_mentioned_user_group_name(
        missed_messages, user_profile)
    triggers = [message["trigger"] for message in missed_messages]
    unique_triggers = set(triggers)

    context.update(
        mention="mentioned" in unique_triggers
        or "wildcard_mentioned" in unique_triggers,
        stream_email_notify="stream_email_notify" in unique_triggers,
        mention_count=triggers.count("mentioned") +
        triggers.count("wildcard_mentioned"),
        mentioned_user_group_name=mentioned_user_group_name,
    )

    # If this setting (email mirroring integration) is enabled, only then
    # can users reply to email to send message to Zulip. Thus, one must
    # ensure to display warning in the template.
    if settings.EMAIL_GATEWAY_PATTERN:
        context.update(reply_to_zulip=True, )
    else:
        context.update(reply_to_zulip=False, )

    from zerver.lib.email_mirror import create_missed_message_address

    reply_to_address = create_missed_message_address(
        user_profile, missed_messages[0]["message"])
    if reply_to_address == FromAddress.NOREPLY:
        reply_to_name = ""
    else:
        reply_to_name = "Zulip"

    narrow_url = get_narrow_url(user_profile, missed_messages[0]["message"])
    context.update(narrow_url=narrow_url, )

    senders = list({m["message"].sender for m in missed_messages})
    if missed_messages[0]["message"].recipient.type == Recipient.HUDDLE:
        display_recipient = get_display_recipient(
            missed_messages[0]["message"].recipient)
        # Make sure that this is a list of strings, not a string.
        assert not isinstance(display_recipient, str)
        other_recipients = [
            r["full_name"] for r in display_recipient
            if r["id"] != user_profile.id
        ]
        context.update(group_pm=True)
        if len(other_recipients) == 2:
            huddle_display_name = " and ".join(other_recipients)
            context.update(huddle_display_name=huddle_display_name)
        elif len(other_recipients) == 3:
            huddle_display_name = (
                f"{other_recipients[0]}, {other_recipients[1]}, and {other_recipients[2]}"
            )
            context.update(huddle_display_name=huddle_display_name)
        else:
            huddle_display_name = "{}, and {} others".format(
                ", ".join(other_recipients[:2]),
                len(other_recipients) - 2)
            context.update(huddle_display_name=huddle_display_name)
    elif missed_messages[0]["message"].recipient.type == Recipient.PERSONAL:
        context.update(private_message=True)
    elif context["mention"] or context["stream_email_notify"]:
        # Keep only the senders who actually mentioned the user
        if context["mention"]:
            senders = list({
                m["message"].sender
                for m in missed_messages if m["trigger"] == "mentioned"
                or m["trigger"] == "wildcard_mentioned"
            })
        message = missed_messages[0]["message"]
        stream = Stream.objects.only("id",
                                     "name").get(id=message.recipient.type_id)
        stream_header = f"{stream.name} > {message.topic_name()}"
        context.update(stream_header=stream_header, )
    else:
        raise AssertionError("Invalid messages!")

    # If message content is disabled, then flush all information we pass to email.
    if not message_content_allowed_in_missedmessage_emails(user_profile):
        realm = user_profile.realm
        context.update(
            reply_to_zulip=False,
            messages=[],
            sender_str="",
            realm_str=realm.name,
            huddle_display_name="",
            show_message_content=False,
            message_content_disabled_by_user=not user_profile.
            message_content_in_email_notifications,
            message_content_disabled_by_realm=not realm.
            message_content_allowed_in_email_notifications,
        )
    else:
        context.update(
            messages=build_message_list(
                user=user_profile,
                messages=[m["message"] for m in missed_messages],
                stream_map={},
            ),
            sender_str=", ".join(sender.full_name for sender in senders),
            realm_str=user_profile.realm.name,
            show_message_content=True,
        )

    with override_language(user_profile.default_language):
        from_name: str = _("Zulip notifications")
    from_address = FromAddress.NOREPLY
    if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER:
        # If this setting is enabled, you can reply to the Zulip
        # message notification emails directly back to the original sender.
        # However, one must ensure the Zulip server is in the SPF
        # record for the domain, or there will be spam/deliverability
        # problems.
        #
        # Also, this setting is not really compatible with
        # EMAIL_ADDRESS_VISIBILITY_ADMINS.
        sender = senders[0]
        from_name, from_address = (sender.full_name, sender.email)
        context.update(reply_to_zulip=False, )

    email_dict = {
        "template_prefix":
        "zerver/emails/missed_message",
        "to_user_ids": [user_profile.id],
        "from_name":
        from_name,
        "from_address":
        from_address,
        "reply_to_email":
        str(Address(display_name=reply_to_name, addr_spec=reply_to_address)),
        "context":
        context,
    }
    queue_json_publish("email_senders", email_dict)

    user_profile.last_reminder = timezone_now()
    user_profile.save(update_fields=["last_reminder"])
Example #34
0
def handle_push_notification(user_profile_id, missed_message):
    # type: (int, Dict[str, Any]) -> None
    try:
        user_profile = get_user_profile_by_id(user_profile_id)
        if not (receives_offline_notifications(user_profile) or receives_online_notifications(user_profile)):
            return

        umessage = UserMessage.objects.get(user_profile=user_profile,
                                           message__id=missed_message['message_id'])
        message = umessage.message
        if umessage.flags.read:
            return
        sender_str = message.sender.full_name

        android_devices = [device for device in
                           PushDeviceToken.objects.filter(user=user_profile,
                                                          kind=PushDeviceToken.GCM)]
        apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                            kind=PushDeviceToken.APNS))

        if apple_devices or android_devices:
            # TODO: set badge count in a better way
            # Determine what alert string to display based on the missed messages
            if message.recipient.type == Recipient.HUDDLE:
                alert = "New private group message from %s" % (sender_str,)
            elif message.recipient.type == Recipient.PERSONAL:
                alert = "New private message from %s" % (sender_str,)
            elif message.recipient.type == Recipient.STREAM:
                alert = "New mention from %s" % (sender_str,)
            else:
                alert = "New Zulip mentions and private messages from %s" % (sender_str,)

            if apple_devices:
                apple_extra_data = {
                    'alert': alert,
                    'message_ids': [message.id],
                }
                send_apple_push_notification(user_profile.id, apple_devices,
                                             badge=1, zulip=apple_extra_data)

            if android_devices:
                content = message.content
                content_truncated = (len(content) > 200)
                if content_truncated:
                    content = content[:200] + "..."

                android_data = {
                    'user': user_profile.email,
                    'event': 'message',
                    'alert': alert,
                    'zulip_message_id': message.id, # message_id is reserved for CCS
                    'time': datetime_to_timestamp(message.pub_date),
                    'content': content,
                    'content_truncated': content_truncated,
                    'sender_email': message.sender.email,
                    'sender_full_name': message.sender.full_name,
                    'sender_avatar_url': avatar_url(message.sender),
                }

                if message.recipient.type == Recipient.STREAM:
                    android_data['recipient_type'] = "stream"
                    android_data['stream'] = get_display_recipient(message.recipient)
                    android_data['topic'] = message.subject
                elif message.recipient.type in (Recipient.HUDDLE, Recipient.PERSONAL):
                    android_data['recipient_type'] = "private"

                send_android_push_notification(android_devices, android_data)

    except UserMessage.DoesNotExist:
        logging.error("Could not find UserMessage with message_id %s" % (missed_message['message_id'],))