2019-03-22 16:33:57 +01:00
|
|
|
from typing import Dict, Optional, Tuple, List
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
|
2019-01-07 19:17:21 +01:00
|
|
|
from email.header import decode_header, make_header
|
2019-01-03 15:53:27 +01:00
|
|
|
from email.utils import getaddresses
|
2016-06-05 21:16:54 +02:00
|
|
|
import email.message as message
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
from django.conf import settings
|
2019-12-26 13:46:55 +01:00
|
|
|
from django.utils.timezone import timedelta, now as timezone_now
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2020-02-10 16:49:19 +01:00
|
|
|
from zerver.lib.actions import internal_send_private_message, \
|
2020-03-28 07:46:04 +01:00
|
|
|
internal_send_stream_message, internal_send_huddle_message
|
2019-03-21 10:24:56 +01:00
|
|
|
from zerver.lib.email_mirror_helpers import decode_email_address, \
|
2019-03-21 11:28:14 +01:00
|
|
|
get_email_gateway_message_string_from_address, ZulipEmailForwardError
|
2019-03-15 18:51:39 +01:00
|
|
|
from zerver.lib.email_notifications import convert_html_to_markdown
|
2017-04-18 17:28:55 +02:00
|
|
|
from zerver.lib.queue import queue_json_publish
|
2019-03-23 18:50:05 +01:00
|
|
|
from zerver.lib.utils import generate_random_token
|
2019-12-26 13:46:55 +01:00
|
|
|
from zerver.lib.upload import upload_message_file
|
2017-06-26 19:43:32 +02:00
|
|
|
from zerver.lib.send_email import FromAddress
|
2020-03-04 14:05:25 +01:00
|
|
|
from zerver.lib.rate_limiter import RateLimitedObject
|
2019-03-16 11:39:09 +01:00
|
|
|
from zerver.lib.exceptions import RateLimited
|
2020-03-28 07:46:04 +01:00
|
|
|
from zerver.lib.message import truncate_body, truncate_topic
|
2019-12-26 13:46:55 +01:00
|
|
|
from zerver.models import Stream, Recipient, MissedMessageEmailAddress, \
|
|
|
|
get_display_recipient, \
|
2019-02-08 03:13:14 +01:00
|
|
|
Message, Realm, UserProfile, get_system_bot, get_user, get_stream_by_id_in_realm
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-09-21 02:00:00 +02:00
|
|
|
from zproject.backends import is_user_active
|
|
|
|
|
2013-12-16 23:32:08 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2019-03-22 16:33:57 +01:00
|
|
|
def redact_email_address(error_message: str) -> str:
|
|
|
|
if not settings.EMAIL_GATEWAY_EXTRA_PATTERN_HACK:
|
|
|
|
domain = settings.EMAIL_GATEWAY_PATTERN.rsplit('@')[-1]
|
|
|
|
else:
|
|
|
|
# EMAIL_GATEWAY_EXTRA_PATTERN_HACK is of the form '@example.com'
|
|
|
|
domain = settings.EMAIL_GATEWAY_EXTRA_PATTERN_HACK[1:]
|
|
|
|
|
|
|
|
address_match = re.search('\\b(\\S*?)@' + domain, error_message)
|
|
|
|
if address_match:
|
|
|
|
email_address = address_match.group(0)
|
|
|
|
# Annotate basic info about the address before scrubbing:
|
|
|
|
if is_missed_message_address(email_address):
|
|
|
|
redacted_message = error_message.replace(email_address,
|
|
|
|
"{} <Missed message address>".format(email_address))
|
|
|
|
else:
|
|
|
|
try:
|
2020-01-10 10:25:56 +01:00
|
|
|
target_stream_id = decode_stream_email_address(email_address)[0].id
|
2019-03-22 16:33:57 +01:00
|
|
|
annotated_address = "{} <Address to stream id: {}>".format(email_address, target_stream_id)
|
|
|
|
redacted_message = error_message.replace(email_address, annotated_address)
|
|
|
|
except ZulipEmailForwardError:
|
|
|
|
redacted_message = error_message.replace(email_address,
|
|
|
|
"{} <Invalid address>".format(email_address))
|
|
|
|
|
|
|
|
# Scrub the address from the message, to the form XXXXX@example.com:
|
|
|
|
string_to_scrub = address_match.groups()[0]
|
|
|
|
redacted_message = redacted_message.replace(string_to_scrub, "X" * len(string_to_scrub))
|
|
|
|
return redacted_message
|
|
|
|
|
2013-12-16 23:32:08 +01:00
|
|
|
return error_message
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def report_to_zulip(error_message: str) -> None:
|
2016-08-23 06:24:20 +02:00
|
|
|
if settings.ERROR_BOT is None:
|
|
|
|
return
|
2017-05-22 23:37:15 +02:00
|
|
|
error_bot = get_system_bot(settings.ERROR_BOT)
|
2016-08-23 06:24:20 +02:00
|
|
|
error_stream = Stream.objects.get(name="errors", realm=error_bot.realm)
|
2020-02-10 16:49:19 +01:00
|
|
|
send_zulip(
|
|
|
|
error_bot,
|
|
|
|
error_stream,
|
|
|
|
"email mirror error",
|
|
|
|
"""~~~\n%s\n~~~""" % (error_message,)
|
|
|
|
)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-03-22 16:33:57 +01:00
|
|
|
def log_and_report(email_message: message.Message, error_message: str, to: Optional[str]) -> None:
|
|
|
|
recipient = to or "No recipient found"
|
|
|
|
error_message = "Sender: {}\nTo: {}\n{}".format(email_message.get("From"),
|
|
|
|
recipient, error_message)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-03-22 16:33:57 +01:00
|
|
|
error_message = redact_email_address(error_message)
|
|
|
|
logger.error(error_message)
|
|
|
|
report_to_zulip(error_message)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2014-07-25 10:40:40 +02:00
|
|
|
# Temporary missed message addresses
|
|
|
|
|
2019-12-26 13:46:55 +01:00
|
|
|
def generate_missed_message_token() -> str:
|
|
|
|
return 'mm' + generate_random_token(32)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def is_missed_message_address(address: str) -> bool:
|
2019-03-21 11:28:14 +01:00
|
|
|
try:
|
|
|
|
msg_string = get_email_gateway_message_string_from_address(address)
|
|
|
|
except ZulipEmailForwardError:
|
|
|
|
return False
|
|
|
|
|
2016-09-22 18:11:09 +02:00
|
|
|
return is_mm_32_format(msg_string)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def is_mm_32_format(msg_string: Optional[str]) -> bool:
|
2016-09-22 18:11:09 +02:00
|
|
|
'''
|
|
|
|
Missed message strings are formatted with a little "mm" prefix
|
|
|
|
followed by a randomly generated 32-character string.
|
|
|
|
'''
|
2017-05-24 23:48:45 +02:00
|
|
|
return msg_string is not None and msg_string.startswith('mm') and len(msg_string) == 34
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def get_missed_message_token_from_address(address: str) -> str:
|
2015-10-14 17:11:50 +02:00
|
|
|
msg_string = get_email_gateway_message_string_from_address(address)
|
|
|
|
|
2016-09-22 18:41:10 +02:00
|
|
|
if not is_mm_32_format(msg_string):
|
2014-07-25 10:40:40 +02:00
|
|
|
raise ZulipEmailForwardError('Could not parse missed message address')
|
|
|
|
|
2019-12-26 13:46:55 +01:00
|
|
|
return msg_string
|
|
|
|
|
2020-01-10 10:36:35 +01:00
|
|
|
def get_usable_missed_message_address(address: str) -> MissedMessageEmailAddress:
|
2019-12-26 13:46:55 +01:00
|
|
|
token = get_missed_message_token_from_address(address)
|
|
|
|
try:
|
2020-01-10 10:36:35 +01:00
|
|
|
mm_address = MissedMessageEmailAddress.objects.select_related().get(
|
2019-12-26 13:46:55 +01:00
|
|
|
email_token=token,
|
|
|
|
timestamp__gt=timezone_now() - timedelta(seconds=MissedMessageEmailAddress.EXPIRY_SECONDS)
|
|
|
|
)
|
|
|
|
except MissedMessageEmailAddress.DoesNotExist:
|
2020-01-10 10:21:47 +01:00
|
|
|
raise ZulipEmailForwardError("Missed message address expired or doesn't exist.")
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2020-01-10 10:36:35 +01:00
|
|
|
if not mm_address.is_usable():
|
|
|
|
# Technical, this also checks whether the event is expired,
|
|
|
|
# but that case is excluded by the logic above.
|
|
|
|
raise ZulipEmailForwardError("Missed message address out of uses.")
|
|
|
|
|
|
|
|
return mm_address
|
|
|
|
|
2017-11-05 11:15:10 +01:00
|
|
|
def create_missed_message_address(user_profile: UserProfile, message: Message) -> str:
|
2016-07-31 16:49:31 +02:00
|
|
|
if settings.EMAIL_GATEWAY_PATTERN == '':
|
2017-10-28 00:50:15 +02:00
|
|
|
logger.warning("EMAIL_GATEWAY_PATTERN is an empty string, using "
|
|
|
|
"NOREPLY_EMAIL_ADDRESS in the 'from' field.")
|
2017-06-26 19:43:32 +02:00
|
|
|
return FromAddress.NOREPLY
|
2016-07-31 16:49:31 +02:00
|
|
|
|
2019-12-26 13:46:55 +01:00
|
|
|
mm_address = MissedMessageEmailAddress.objects.create(message=message,
|
|
|
|
user_profile=user_profile,
|
|
|
|
email_token=generate_missed_message_token())
|
|
|
|
return str(mm_address)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2019-03-09 22:35:45 +01:00
|
|
|
def construct_zulip_body(message: message.Message, realm: Realm, show_sender: bool=False,
|
2020-01-15 16:28:46 +01:00
|
|
|
include_quotes: bool=False, include_footer: bool=False,
|
|
|
|
prefer_text: bool=True) -> str:
|
|
|
|
body = extract_body(message, include_quotes, prefer_text)
|
2017-10-04 00:05:46 +02:00
|
|
|
# Remove null characters, since Zulip will reject
|
|
|
|
body = body.replace("\x00", "")
|
2019-06-06 12:14:12 +02:00
|
|
|
if not include_footer:
|
2019-05-26 18:07:21 +02:00
|
|
|
body = filter_footer(body)
|
|
|
|
|
2020-01-14 13:05:14 +01:00
|
|
|
if not body.endswith('\n'):
|
|
|
|
body += '\n'
|
2017-10-04 00:03:00 +02:00
|
|
|
body += extract_and_upload_attachments(message, realm)
|
2017-10-19 06:13:03 +02:00
|
|
|
body = body.strip()
|
2017-10-04 00:03:00 +02:00
|
|
|
if not body:
|
|
|
|
body = '(No email body)'
|
2019-02-08 14:13:33 +01:00
|
|
|
|
|
|
|
if show_sender:
|
2020-01-30 15:01:07 +01:00
|
|
|
sender = handle_header_content(message.get("From", ""))
|
2019-02-08 14:13:33 +01:00
|
|
|
body = "From: %s\n%s" % (sender, body)
|
|
|
|
|
2017-10-04 00:03:00 +02:00
|
|
|
return body
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2020-01-30 15:01:07 +01:00
|
|
|
def handle_header_content(content: str) -> str:
|
|
|
|
"""
|
|
|
|
Deals with converting encoded headers to readable python string.
|
|
|
|
"""
|
|
|
|
return str(make_header(decode_header(content)))
|
|
|
|
|
2013-12-16 23:32:08 +01:00
|
|
|
## Sending the Zulip ##
|
|
|
|
|
2019-01-05 19:29:08 +01:00
|
|
|
class ZulipEmailForwardUserError(ZulipEmailForwardError):
|
|
|
|
pass
|
|
|
|
|
2020-02-10 16:49:19 +01:00
|
|
|
def send_zulip(sender: UserProfile, stream: Stream, topic: str, content: str) -> None:
|
|
|
|
internal_send_stream_message(
|
2017-01-24 07:06:13 +01:00
|
|
|
stream.realm,
|
|
|
|
sender,
|
2020-02-10 16:49:19 +01:00
|
|
|
stream,
|
2018-11-25 07:40:16 +01:00
|
|
|
truncate_topic(topic),
|
|
|
|
truncate_body(content),
|
2017-11-03 12:13:17 +01:00
|
|
|
email_gateway=True)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def get_message_part_by_type(message: message.Message, content_type: str) -> Optional[str]:
|
2013-12-16 23:32:08 +01:00
|
|
|
charsets = message.get_charsets()
|
|
|
|
|
|
|
|
for idx, part in enumerate(message.walk()):
|
|
|
|
if part.get_content_type() == content_type:
|
|
|
|
content = part.get_payload(decode=True)
|
2017-11-09 09:03:33 +01:00
|
|
|
assert isinstance(content, bytes)
|
2013-12-16 23:32:08 +01:00
|
|
|
if charsets[idx]:
|
2017-07-14 07:19:49 +02:00
|
|
|
return content.decode(charsets[idx], errors="ignore")
|
2019-05-09 16:01:34 +02:00
|
|
|
# If no charset has been specified in the header, assume us-ascii,
|
|
|
|
# by RFC6657: https://tools.ietf.org/html/rfc6657
|
|
|
|
else:
|
|
|
|
return content.decode("us-ascii", errors="ignore")
|
|
|
|
|
2017-03-05 00:18:18 +01:00
|
|
|
return None
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2020-01-15 16:28:46 +01:00
|
|
|
def extract_body(message: message.Message, include_quotes: bool=False, prefer_text: bool=True) -> str:
|
2020-01-16 13:07:04 +01:00
|
|
|
plaintext_content = extract_plaintext_body(message, include_quotes)
|
|
|
|
html_content = extract_html_body(message, include_quotes)
|
|
|
|
|
|
|
|
if plaintext_content is None and html_content is None:
|
|
|
|
logging.warning("Content types: %s" % ([part.get_content_type() for part in message.walk()],))
|
|
|
|
raise ZulipEmailForwardUserError("Unable to find plaintext or HTML message body")
|
|
|
|
if not plaintext_content and not html_content:
|
|
|
|
raise ZulipEmailForwardUserError("Email has no nonempty body sections; ignoring.")
|
|
|
|
|
|
|
|
if prefer_text:
|
|
|
|
if plaintext_content:
|
|
|
|
return plaintext_content
|
|
|
|
else:
|
|
|
|
assert html_content # Needed for mypy. Ensured by the validating block above.
|
|
|
|
return html_content
|
|
|
|
else:
|
|
|
|
if html_content:
|
|
|
|
return html_content
|
|
|
|
else:
|
|
|
|
assert plaintext_content # Needed for mypy. Ensured by the validating block above.
|
|
|
|
return plaintext_content
|
|
|
|
|
|
|
|
talon_initialized = False
|
|
|
|
def extract_plaintext_body(message: message.Message, include_quotes: bool=False) -> Optional[str]:
|
2018-08-08 22:11:29 +02:00
|
|
|
import talon
|
|
|
|
global talon_initialized
|
|
|
|
if not talon_initialized:
|
|
|
|
talon.init()
|
|
|
|
talon_initialized = True
|
|
|
|
|
2013-12-16 23:32:08 +01:00
|
|
|
plaintext_content = get_message_part_by_type(message, "text/plain")
|
2020-01-16 13:07:04 +01:00
|
|
|
if plaintext_content is not None:
|
2019-07-14 03:51:53 +02:00
|
|
|
if include_quotes:
|
2019-03-09 22:35:45 +01:00
|
|
|
return plaintext_content
|
2019-05-26 18:28:39 +02:00
|
|
|
else:
|
|
|
|
return talon.quotations.extract_from_plain(plaintext_content)
|
2020-01-16 13:07:04 +01:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def extract_html_body(message: message.Message, include_quotes: bool=False) -> Optional[str]:
|
|
|
|
import talon
|
|
|
|
global talon_initialized
|
|
|
|
if not talon_initialized: # nocoverage
|
|
|
|
talon.init()
|
|
|
|
talon_initialized = True
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
html_content = get_message_part_by_type(message, "text/html")
|
2020-01-16 13:07:04 +01:00
|
|
|
if html_content is not None:
|
2019-07-14 03:51:53 +02:00
|
|
|
if include_quotes:
|
2019-03-09 22:35:45 +01:00
|
|
|
return convert_html_to_markdown(html_content)
|
2019-05-26 18:28:39 +02:00
|
|
|
else:
|
|
|
|
return convert_html_to_markdown(talon.quotations.extract_from_html(html_content))
|
2020-01-16 13:07:04 +01:00
|
|
|
else:
|
|
|
|
return None
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def filter_footer(text: str) -> str:
|
2013-12-16 23:32:08 +01:00
|
|
|
# Try to filter out obvious footers.
|
2019-03-09 22:35:45 +01:00
|
|
|
possible_footers = [line for line in text.split("\n") if line.strip() == "--"]
|
2013-12-16 23:32:08 +01:00
|
|
|
if len(possible_footers) != 1:
|
|
|
|
# Be conservative and don't try to scrub content if there
|
|
|
|
# isn't a trivial footer structure.
|
|
|
|
return text
|
|
|
|
|
|
|
|
return text.partition("--")[0].strip()
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def extract_and_upload_attachments(message: message.Message, realm: Realm) -> str:
|
2017-05-22 23:37:15 +02:00
|
|
|
user_profile = get_system_bot(settings.EMAIL_GATEWAY_BOT)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2020-01-14 16:33:48 +01:00
|
|
|
attachment_links = []
|
|
|
|
for part in message.walk():
|
2013-12-16 23:32:08 +01:00
|
|
|
content_type = part.get_content_type()
|
2020-01-30 15:37:33 +01:00
|
|
|
encoded_filename = part.get_filename()
|
|
|
|
if not encoded_filename:
|
|
|
|
continue
|
|
|
|
|
|
|
|
filename = handle_header_content(encoded_filename)
|
2013-12-16 23:32:08 +01:00
|
|
|
if filename:
|
2016-07-04 17:13:24 +02:00
|
|
|
attachment = part.get_payload(decode=True)
|
2017-11-09 09:03:33 +01:00
|
|
|
if isinstance(attachment, bytes):
|
2018-03-28 18:14:17 +02:00
|
|
|
s3_url = upload_message_file(filename, len(attachment), content_type,
|
|
|
|
attachment,
|
|
|
|
user_profile,
|
|
|
|
target_realm=realm)
|
2017-11-04 05:34:38 +01:00
|
|
|
formatted_link = "[%s](%s)" % (filename, s3_url)
|
2016-07-04 17:13:24 +02:00
|
|
|
attachment_links.append(formatted_link)
|
|
|
|
else:
|
|
|
|
logger.warning("Payload is not bytes (invalid attachment %s in message from %s)." %
|
|
|
|
(filename, message.get("From")))
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2020-01-14 16:33:48 +01:00
|
|
|
return '\n'.join(attachment_links)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2020-01-10 10:25:56 +01:00
|
|
|
def decode_stream_email_address(email: str) -> Tuple[Stream, Dict[str, bool]]:
|
2019-05-26 16:25:23 +02:00
|
|
|
token, options = decode_email_address(email)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-03-17 10:36:16 +01:00
|
|
|
try:
|
|
|
|
stream = Stream.objects.get(email_token=token)
|
|
|
|
except Stream.DoesNotExist:
|
2013-12-16 23:32:08 +01:00
|
|
|
raise ZulipEmailForwardError("Bad stream token from email recipient " + email)
|
|
|
|
|
2019-05-26 16:25:23 +02:00
|
|
|
return stream, options
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def find_emailgateway_recipient(message: message.Message) -> str:
|
2013-12-16 23:32:08 +01:00
|
|
|
# We can't use Delivered-To; if there is a X-Gm-Original-To
|
|
|
|
# it is more accurate, so try to find the most-accurate
|
|
|
|
# recipient list in descending priority order
|
2020-03-20 04:15:24 +01:00
|
|
|
recipient_headers = ["X-Gm-Original-To", "Delivered-To", "Envelope-To",
|
2019-01-03 15:53:27 +01:00
|
|
|
"Resent-To", "Resent-CC", "To", "CC"]
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
pattern_parts = [re.escape(part) for part in settings.EMAIL_GATEWAY_PATTERN.split('%s')]
|
|
|
|
match_email_re = re.compile(".*?".join(pattern_parts))
|
2019-01-03 15:53:27 +01:00
|
|
|
|
|
|
|
header_addresses = [str(addr)
|
|
|
|
for recipient_header in recipient_headers
|
|
|
|
for addr in message.get_all(recipient_header, [])]
|
|
|
|
|
|
|
|
for addr_tuple in getaddresses(header_addresses):
|
|
|
|
if match_email_re.match(addr_tuple[1]):
|
|
|
|
return addr_tuple[1]
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
raise ZulipEmailForwardError("Missing recipient in mirror email")
|
|
|
|
|
2019-01-04 10:46:35 +01:00
|
|
|
def strip_from_subject(subject: str) -> str:
|
|
|
|
# strips RE and FWD from the subject
|
|
|
|
# from: https://stackoverflow.com/questions/9153629/regex-code-for-removing-fwd-re-etc-from-email-subject
|
|
|
|
reg = r"([\[\(] *)?\b(RE|FWD?) *([-:;)\]][ :;\])-]*|$)|\]+ *$"
|
|
|
|
stripped = re.sub(reg, "", subject, flags = re.IGNORECASE | re.MULTILINE)
|
|
|
|
return stripped.strip()
|
|
|
|
|
2019-03-09 22:35:45 +01:00
|
|
|
def is_forwarded(subject: str) -> bool:
|
|
|
|
# regex taken from strip_from_subject, we use it to detect various forms
|
|
|
|
# of FWD at the beginning of the subject.
|
|
|
|
reg = r"([\[\(] *)?\b(FWD?) *([-:;)\]][ :;\])-]*|$)|\]+ *$"
|
|
|
|
return bool(re.match(reg, subject, flags=re.IGNORECASE))
|
|
|
|
|
2019-03-22 11:22:14 +01:00
|
|
|
def process_stream_message(to: str, message: message.Message) -> None:
|
2020-01-30 15:01:07 +01:00
|
|
|
subject_header = handle_header_content(message.get("Subject", ""))
|
2019-03-09 16:52:54 +01:00
|
|
|
subject = strip_from_subject(subject_header) or "(no topic)"
|
|
|
|
|
2020-01-10 10:25:56 +01:00
|
|
|
stream, options = decode_stream_email_address(to)
|
2019-05-26 18:28:39 +02:00
|
|
|
# Don't remove quotations if message is forwarded, unless otherwise specified:
|
2019-07-14 03:51:53 +02:00
|
|
|
if 'include_quotes' not in options:
|
|
|
|
options['include_quotes'] = is_forwarded(subject_header)
|
2019-05-26 18:28:39 +02:00
|
|
|
|
2019-05-26 16:25:23 +02:00
|
|
|
body = construct_zulip_body(message, stream.realm, **options)
|
2020-02-10 16:49:19 +01:00
|
|
|
send_zulip(
|
|
|
|
get_system_bot(settings.EMAIL_GATEWAY_BOT),
|
|
|
|
stream, subject, body)
|
2017-10-28 00:50:15 +02:00
|
|
|
logger.info("Successfully processed email to %s (%s)" % (
|
2017-03-13 17:42:14 +01:00
|
|
|
stream.name, stream.realm.string_id))
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2019-12-26 13:46:55 +01:00
|
|
|
def process_missed_message(to: str, message: message.Message) -> None:
|
2020-01-10 10:36:35 +01:00
|
|
|
mm_address = get_usable_missed_message_address(to)
|
2019-12-26 14:08:41 +01:00
|
|
|
mm_address.increment_times_used()
|
|
|
|
|
|
|
|
user_profile = mm_address.user_profile
|
|
|
|
topic = mm_address.message.topic_name()
|
|
|
|
|
|
|
|
if mm_address.message.recipient.type == Recipient.PERSONAL:
|
|
|
|
# We need to reply to the sender so look up their personal recipient_id
|
|
|
|
recipient = mm_address.message.sender.recipient
|
|
|
|
else:
|
|
|
|
recipient = mm_address.message.recipient
|
|
|
|
|
|
|
|
if not is_user_active(user_profile):
|
|
|
|
logger.warning("Sending user is not active. Ignoring this missed message email.")
|
|
|
|
return
|
|
|
|
|
|
|
|
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,
|
|
|
|
topic, 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 user %s to %s" % (
|
|
|
|
user_profile.id, recipient_str))
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2019-12-26 13:46:55 +01:00
|
|
|
def process_message(message: message.Message, rcpt_to: Optional[str]=None) -> None:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
to: Optional[str] = None
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
try:
|
2013-12-17 22:37:29 +01:00
|
|
|
if rcpt_to is not None:
|
|
|
|
to = rcpt_to
|
|
|
|
else:
|
|
|
|
to = find_emailgateway_recipient(message)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
|
|
|
if is_missed_message_address(to):
|
2019-12-26 13:46:55 +01:00
|
|
|
process_missed_message(to, message)
|
2014-07-25 10:40:40 +02:00
|
|
|
else:
|
2019-03-22 11:22:14 +01:00
|
|
|
process_stream_message(to, message)
|
2015-11-01 17:08:33 +01:00
|
|
|
except ZulipEmailForwardError as e:
|
2019-01-05 19:29:08 +01:00
|
|
|
if isinstance(e, ZulipEmailForwardUserError):
|
|
|
|
# TODO: notify sender of error, retry if appropriate.
|
|
|
|
logging.warning(str(e))
|
|
|
|
else:
|
2019-03-22 16:33:57 +01:00
|
|
|
log_and_report(message, str(e), to)
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-01-10 10:21:47 +01:00
|
|
|
def validate_to_address(rcpt_to: str) -> None:
|
2017-04-18 17:28:55 +02:00
|
|
|
if is_missed_message_address(rcpt_to):
|
2020-01-10 10:36:35 +01:00
|
|
|
get_usable_missed_message_address(rcpt_to)
|
2017-04-18 17:28:55 +02:00
|
|
|
else:
|
2020-01-10 10:25:56 +01:00
|
|
|
decode_stream_email_address(rcpt_to)
|
2020-01-10 10:21:47 +01:00
|
|
|
|
|
|
|
def mirror_email_message(data: Dict[str, str]) -> Dict[str, str]:
|
|
|
|
rcpt_to = data['recipient']
|
|
|
|
try:
|
|
|
|
validate_to_address(rcpt_to)
|
|
|
|
except ZulipEmailForwardError as e:
|
|
|
|
return {
|
|
|
|
"status": "error",
|
|
|
|
"msg": "5.1.1 Bad destination mailbox address: {}".format(e)
|
|
|
|
}
|
|
|
|
|
2017-04-18 17:28:55 +02:00
|
|
|
queue_json_publish(
|
|
|
|
"email_mirror",
|
|
|
|
{
|
|
|
|
"message": data['msg_text'],
|
|
|
|
"rcpt_to": rcpt_to
|
2017-11-24 13:18:46 +01:00
|
|
|
}
|
2017-04-18 17:28:55 +02:00
|
|
|
)
|
|
|
|
return {"status": "success"}
|
2019-03-16 11:39:09 +01:00
|
|
|
|
|
|
|
# Email mirror rate limiter code:
|
|
|
|
|
|
|
|
class RateLimitedRealmMirror(RateLimitedObject):
|
|
|
|
def __init__(self, realm: Realm) -> None:
|
|
|
|
self.realm = realm
|
2020-03-05 13:38:20 +01:00
|
|
|
super().__init__()
|
2019-03-16 11:39:09 +01:00
|
|
|
|
2020-03-06 10:49:04 +01:00
|
|
|
def key(self) -> str:
|
2020-03-06 10:56:36 +01:00
|
|
|
return "{}:{}".format(type(self).__name__, self.realm.string_id)
|
2019-03-16 11:39:09 +01:00
|
|
|
|
|
|
|
def rules(self) -> List[Tuple[int, int]]:
|
|
|
|
return settings.RATE_LIMITING_MIRROR_REALM_RULES
|
|
|
|
|
|
|
|
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
|
2020-03-04 14:05:25 +01:00
|
|
|
ratelimited = RateLimitedRealmMirror(recipient_realm).rate_limit()[0]
|
2019-03-16 11:39:09 +01:00
|
|
|
|
|
|
|
if ratelimited:
|
|
|
|
raise RateLimited()
|