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-03-21 10:24:56 +01:00
|
|
|
from zerver.lib.actions import internal_send_message, internal_send_private_message, \
|
2018-11-25 07:40:16 +01:00
|
|
|
internal_send_stream_message, internal_send_huddle_message, \
|
|
|
|
truncate_body, truncate_topic
|
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
|
2014-07-25 10:40:40 +02:00
|
|
|
from zerver.lib.redis_utils import get_redis_client
|
2018-03-28 18:14:17 +02:00
|
|
|
from zerver.lib.upload import upload_message_file
|
2019-03-23 18:50:05 +01:00
|
|
|
from zerver.lib.utils import generate_random_token
|
2017-06-26 19:43:32 +02:00
|
|
|
from zerver.lib.send_email import FromAddress
|
2019-03-23 18:50:05 +01:00
|
|
|
from zerver.lib.rate_limiter import RateLimitedObject, rate_limit_entity
|
2019-03-16 11:39:09 +01:00
|
|
|
from zerver.lib.exceptions import RateLimited
|
2017-05-22 23:37:15 +02:00
|
|
|
from zerver.models import Stream, Recipient, \
|
2017-10-28 21:31:21 +02:00
|
|
|
get_user_profile_by_id, get_display_recipient, get_personal_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
|
|
|
|
|
|
|
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:
|
|
|
|
target_stream_id = extract_and_validate(email_address)[0].id
|
|
|
|
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)
|
2017-11-04 05:34:38 +01:00
|
|
|
send_zulip(settings.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
|
|
|
|
|
|
|
|
redis_client = get_redis_client()
|
|
|
|
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def missed_message_redis_key(token: str) -> str:
|
2014-07-25 10:40:40 +02:00
|
|
|
return 'missed_message:' + token
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
2015-10-14 17:11:50 +02:00
|
|
|
# strip off the 'mm' before returning the redis key
|
|
|
|
return msg_string[2:]
|
2014-07-25 10:40:40 +02:00
|
|
|
|
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
|
|
|
|
2014-08-11 14:15:16 +02:00
|
|
|
if message.recipient.type == Recipient.PERSONAL:
|
|
|
|
# We need to reply to the sender so look up their personal recipient_id
|
2017-10-28 21:31:21 +02:00
|
|
|
recipient_id = get_personal_recipient(message.sender_id).id
|
2014-08-11 14:15:16 +02:00
|
|
|
else:
|
|
|
|
recipient_id = message.recipient_id
|
|
|
|
|
2014-07-25 10:40:40 +02:00
|
|
|
data = {
|
|
|
|
'user_profile_id': user_profile.id,
|
2014-08-11 14:15:16 +02:00
|
|
|
'recipient_id': recipient_id,
|
2018-11-01 16:18:20 +01:00
|
|
|
'subject': message.topic_name().encode('utf-8'),
|
2014-07-25 10:40:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
while True:
|
|
|
|
token = generate_random_token(32)
|
|
|
|
key = missed_message_redis_key(token)
|
|
|
|
if redis_client.hsetnx(key, 'uses_left', 1):
|
|
|
|
break
|
|
|
|
|
|
|
|
with redis_client.pipeline() as pipeline:
|
|
|
|
pipeline.hmset(key, data)
|
|
|
|
pipeline.expire(key, 60 * 60 * 24 * 5)
|
|
|
|
pipeline.execute()
|
|
|
|
|
2017-11-03 03:12:25 +01:00
|
|
|
address = 'mm' + token
|
2016-05-04 23:16:27 +02:00
|
|
|
return settings.EMAIL_GATEWAY_PATTERN % (address,)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def mark_missed_message_address_as_used(address: str) -> None:
|
2014-07-25 10:40:40 +02:00
|
|
|
token = get_missed_message_token_from_address(address)
|
|
|
|
key = missed_message_redis_key(token)
|
|
|
|
with redis_client.pipeline() as pipeline:
|
|
|
|
pipeline.hincrby(key, 'uses_left', -1)
|
|
|
|
pipeline.expire(key, 60 * 60 * 24 * 5)
|
|
|
|
new_value = pipeline.execute()[0]
|
2014-08-11 08:06:07 +02:00
|
|
|
if new_value < 0:
|
2014-07-25 10:40:40 +02:00
|
|
|
redis_client.delete(key)
|
|
|
|
raise ZulipEmailForwardError('Missed message address has already been used')
|
|
|
|
|
2019-03-09 22:35:45 +01:00
|
|
|
def construct_zulip_body(message: message.Message, realm: Realm, show_sender: bool=False,
|
2019-05-26 18:28:39 +02:00
|
|
|
include_quotations: bool=False, include_footers: bool=False) -> str:
|
|
|
|
body = extract_body(message, include_quotations)
|
2017-10-04 00:05:46 +02:00
|
|
|
# Remove null characters, since Zulip will reject
|
|
|
|
body = body.replace("\x00", "")
|
2019-05-26 18:07:21 +02:00
|
|
|
if not include_footers:
|
|
|
|
body = filter_footer(body)
|
|
|
|
|
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:
|
|
|
|
sender = message.get("From")
|
|
|
|
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
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def send_to_missed_message_address(address: str, message: message.Message) -> None:
|
2014-07-25 10:40:40 +02:00
|
|
|
token = get_missed_message_token_from_address(address)
|
|
|
|
key = missed_message_redis_key(token)
|
2014-08-11 08:06:07 +02:00
|
|
|
result = redis_client.hmget(key, 'user_profile_id', 'recipient_id', 'subject')
|
2014-08-11 14:15:16 +02:00
|
|
|
if not all(val is not None for val in result):
|
2014-07-25 10:40:40 +02:00
|
|
|
raise ZulipEmailForwardError('Missing missed message address data')
|
2017-08-26 00:16:15 +02:00
|
|
|
user_profile_id, recipient_id, subject_b = result # type: (bytes, bytes, bytes)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
|
|
|
user_profile = get_user_profile_by_id(user_profile_id)
|
|
|
|
recipient = Recipient.objects.get(id=recipient_id)
|
|
|
|
|
2017-10-04 00:03:00 +02:00
|
|
|
body = construct_zulip_body(message, user_profile.realm)
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2014-08-11 14:15:16 +02:00
|
|
|
if recipient.type == Recipient.STREAM:
|
2019-02-08 03:13:14 +01:00
|
|
|
stream = get_stream_by_id_in_realm(recipient.type_id, user_profile.realm)
|
|
|
|
internal_send_stream_message(
|
2019-02-09 03:01:35 +01:00
|
|
|
user_profile.realm, user_profile, stream,
|
|
|
|
subject_b.decode('utf-8'), body
|
2019-02-08 03:13:14 +01:00
|
|
|
)
|
2019-02-13 22:16:55 +01:00
|
|
|
recipient_str = stream.name
|
2017-11-27 01:41:07 +01:00
|
|
|
elif recipient.type == Recipient.PERSONAL:
|
2019-02-08 03:13:14 +01:00
|
|
|
display_recipient = get_display_recipient(recipient)
|
2017-11-27 01:41:07 +01:00
|
|
|
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:
|
2019-02-08 03:13:14 +01:00
|
|
|
display_recipient = get_display_recipient(recipient)
|
2017-11-27 01:41:07 +01:00
|
|
|
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)
|
2014-08-11 14:15:16 +02:00
|
|
|
else:
|
2017-11-27 01:41:07 +01:00
|
|
|
raise AssertionError("Invalid recipient type!")
|
2014-08-11 14:15:16 +02:00
|
|
|
|
2017-10-28 00:50:15 +02:00
|
|
|
logger.info("Successfully processed email from %s to %s" % (
|
2016-08-27 07:47:09 +02:00
|
|
|
user_profile.email, recipient_str))
|
2014-07-25 10:40:40 +02:00
|
|
|
|
2013-12-16 23:32:08 +01:00
|
|
|
## Sending the Zulip ##
|
|
|
|
|
2019-01-05 19:29:08 +01:00
|
|
|
class ZulipEmailForwardUserError(ZulipEmailForwardError):
|
|
|
|
pass
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def send_zulip(sender: str, stream: Stream, topic: str, content: str) -> None:
|
2014-02-26 16:37:23 +01:00
|
|
|
internal_send_message(
|
2017-01-24 07:06:13 +01:00
|
|
|
stream.realm,
|
|
|
|
sender,
|
|
|
|
"stream",
|
|
|
|
stream.name,
|
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
|
|
|
|
2018-08-08 22:11:29 +02:00
|
|
|
talon_initialized = False
|
2019-05-26 18:28:39 +02:00
|
|
|
def extract_body(message: message.Message, include_quotations: bool=False) -> 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
|
|
|
# If the message contains a plaintext version of the body, use
|
|
|
|
# that.
|
|
|
|
plaintext_content = get_message_part_by_type(message, "text/plain")
|
|
|
|
if plaintext_content:
|
2019-05-26 18:28:39 +02:00
|
|
|
if include_quotations:
|
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)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
|
|
|
# If we only have an HTML version, try to make that look nice.
|
|
|
|
html_content = get_message_part_by_type(message, "text/html")
|
|
|
|
if html_content:
|
2019-05-26 18:28:39 +02:00
|
|
|
if include_quotations:
|
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))
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-01-15 20:19:49 +01:00
|
|
|
if plaintext_content is not None or html_content is not None:
|
|
|
|
raise ZulipEmailForwardUserError("Email has no nonempty body sections; ignoring.")
|
|
|
|
|
2019-04-20 01:00:46 +02:00
|
|
|
logging.warning("Content types: %s" % ([part.get_content_type() for part in message.walk()],))
|
2019-01-05 19:29:08 +01:00
|
|
|
raise ZulipEmailForwardUserError("Unable to find plaintext or HTML message body")
|
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
|
|
|
attachment_links = []
|
|
|
|
|
|
|
|
payload = message.get_payload()
|
|
|
|
if not isinstance(payload, list):
|
|
|
|
# This is not a multipart message, so it can't contain attachments.
|
|
|
|
return ""
|
|
|
|
|
|
|
|
for part in payload:
|
|
|
|
content_type = part.get_content_type()
|
|
|
|
filename = part.get_filename()
|
|
|
|
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
|
|
|
|
2017-11-04 05:34:38 +01:00
|
|
|
return "\n".join(attachment_links)
|
2013-12-16 23:32:08 +01:00
|
|
|
|
2019-05-26 16:25:23 +02:00
|
|
|
def extract_and_validate(email: str) -> Tuple[Stream, Dict[str, bool]]:
|
|
|
|
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
|
2019-01-03 15:53:27 +01:00
|
|
|
recipient_headers = ["X-Gm-Original-To", "Delivered-To",
|
|
|
|
"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:
|
2019-03-09 16:52:54 +01:00
|
|
|
subject_header = str(make_header(decode_header(message.get("Subject", ""))))
|
|
|
|
subject = strip_from_subject(subject_header) or "(no topic)"
|
|
|
|
|
2019-05-26 16:25:23 +02:00
|
|
|
stream, options = extract_and_validate(to)
|
2019-05-26 18:28:39 +02:00
|
|
|
# Don't remove quotations if message is forwarded, unless otherwise specified:
|
|
|
|
if 'include_quotations' not in options:
|
|
|
|
options['include_quotations'] = is_forwarded(subject_header)
|
|
|
|
|
2019-05-26 16:25:23 +02:00
|
|
|
body = construct_zulip_body(message, stream.realm, **options)
|
2016-08-23 06:24:20 +02:00
|
|
|
send_zulip(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
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def process_missed_message(to: str, message: message.Message, pre_checked: bool) -> None:
|
2014-07-25 10:40:40 +02:00
|
|
|
if not pre_checked:
|
|
|
|
mark_missed_message_address_as_used(to)
|
|
|
|
send_to_missed_message_address(to, message)
|
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def process_message(message: message.Message, rcpt_to: Optional[str]=None, pre_checked: bool=False) -> None:
|
2019-03-22 16:33:57 +01:00
|
|
|
to = None # type: Optional[str]
|
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):
|
|
|
|
process_missed_message(to, message, pre_checked)
|
|
|
|
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
|
|
|
|
2018-05-10 19:13:36 +02:00
|
|
|
def mirror_email_message(data: Dict[str, str]) -> Dict[str, str]:
|
2017-04-18 17:28:55 +02:00
|
|
|
rcpt_to = data['recipient']
|
|
|
|
if is_missed_message_address(rcpt_to):
|
|
|
|
try:
|
|
|
|
mark_missed_message_address_as_used(rcpt_to)
|
|
|
|
except ZulipEmailForwardError:
|
|
|
|
return {
|
|
|
|
"status": "error",
|
|
|
|
"msg": "5.1.1 Bad destination mailbox address: "
|
|
|
|
"Bad or expired missed message address."
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
extract_and_validate(rcpt_to)
|
|
|
|
except ZulipEmailForwardError:
|
|
|
|
return {
|
|
|
|
"status": "error",
|
|
|
|
"msg": "5.1.1 Bad destination mailbox address: "
|
|
|
|
"Please use the address specified in your Streams page."
|
|
|
|
}
|
|
|
|
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
|
|
|
|
|
|
|
|
def key_fragment(self) -> str:
|
|
|
|
return "emailmirror:{}:{}".format(type(self.realm), self.realm.id)
|
|
|
|
|
|
|
|
def rules(self) -> List[Tuple[int, int]]:
|
|
|
|
return settings.RATE_LIMITING_MIRROR_REALM_RULES
|
|
|
|
|
2019-03-23 18:50:05 +01:00
|
|
|
def __str__(self) -> str:
|
|
|
|
return self.realm.string_id
|
2019-03-16 11:39:09 +01:00
|
|
|
|
|
|
|
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
|
|
|
|
entity = RateLimitedRealmMirror(recipient_realm)
|
2019-03-23 18:50:05 +01:00
|
|
|
ratelimited = rate_limit_entity(entity)[0]
|
2019-03-16 11:39:09 +01:00
|
|
|
|
|
|
|
if ratelimited:
|
|
|
|
raise RateLimited()
|