* fix: drop, not bounce, uninteresting ipr emails * chore: log to address * chore: unused import
224 lines
8 KiB
Python
224 lines
8 KiB
Python
# Copyright The IETF Trust 2014-2020, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import base64
|
|
import datetime
|
|
from dateutil.tz import tzoffset
|
|
import os
|
|
import re
|
|
|
|
from email import message_from_bytes
|
|
from email.utils import parsedate_tz
|
|
|
|
from django.template.loader import render_to_string
|
|
from django.utils.encoding import force_str, force_bytes
|
|
|
|
import debug # pyflakes:ignore
|
|
|
|
from ietf.ipr.models import IprEvent
|
|
from ietf.message.models import Message
|
|
from ietf.person.models import Person
|
|
from ietf.utils.log import log
|
|
from ietf.mailtrigger.utils import get_base_ipr_request_address
|
|
|
|
# ----------------------------------------------------------------
|
|
# Date Functions
|
|
# ----------------------------------------------------------------
|
|
def get_body(msg):
|
|
"""Returns the body of the message. A Basic routine to walk parts of a MIME message
|
|
concatenating text/plain parts"""
|
|
body = ''
|
|
for part in msg.walk():
|
|
if part.get_content_type() == 'text/plain':
|
|
body = body + part.get_payload() + '\n'
|
|
return body
|
|
|
|
def is_aware(date):
|
|
"""Returns True if the date object passed in timezone aware, False if naive.
|
|
See http://docs.python.org/2/library/datetime.html section 8.1.1
|
|
"""
|
|
if not isinstance(date,datetime.datetime):
|
|
return False
|
|
if date.tzinfo and date.tzinfo.utcoffset(date) is not None:
|
|
return True
|
|
return False
|
|
|
|
def parsedate_to_datetime(date):
|
|
"""Returns a datetime object from string. May return naive or aware datetime.
|
|
|
|
This function is from email standard library v3.3, converted to 2.x
|
|
http://python.readthedocs.org/en/latest/library/email.util.html
|
|
"""
|
|
try:
|
|
tuple = parsedate_tz(date)
|
|
if not tuple:
|
|
return None
|
|
tz = tuple[-1]
|
|
if tz is None:
|
|
return datetime.datetime(*tuple[:6])
|
|
return datetime.datetime(*tuple[:6],tzinfo=tzoffset(None,tz))
|
|
except ValueError:
|
|
return None
|
|
|
|
def utc_from_string(s):
|
|
date = parsedate_to_datetime(s)
|
|
if date is None:
|
|
return None
|
|
elif is_aware(date):
|
|
return date.astimezone(datetime.timezone.utc)
|
|
else:
|
|
return date.replace(tzinfo=datetime.timezone.utc)
|
|
|
|
# ----------------------------------------------------------------
|
|
# Email Functions
|
|
# ----------------------------------------------------------------
|
|
def get_holders(ipr):
|
|
"""Recursive function to follow chain of disclosure updates and return holder emails"""
|
|
items = []
|
|
for x in [ y.target.get_child() for y in ipr.updates]:
|
|
items.extend(get_holders(x))
|
|
return ([ipr.holder_contact_email] if hasattr(ipr,'holder_contact_email') else []) + items
|
|
|
|
def get_pseudo_submitter(ipr):
|
|
"""Returns a tuple (name, email) contact for this disclosure. Order of preference
|
|
is submitter, ietfer, holder (per legacy app)"""
|
|
name = 'UNKNOWN NAME - NEED ASSISTANCE HERE'
|
|
email = 'UNKNOWN EMAIL - NEED ASSISTANCE HERE'
|
|
if ipr.submitter_email:
|
|
name = ipr.submitter_name
|
|
email = ipr.submitter_email
|
|
elif hasattr(ipr, 'ietfer_contact_email') and ipr.ietfer_contact_email:
|
|
name = ipr.ietfer_name
|
|
email = ipr.ietfer_contact_email
|
|
elif hasattr(ipr, 'holder_contact_email') and ipr.holder_contact_email:
|
|
name = ipr.holder_contact_name
|
|
email = ipr.holder_contact_email
|
|
|
|
return (name,email)
|
|
|
|
def get_reply_to():
|
|
"""Returns a new reply-to address for use with an outgoing message. This is an
|
|
address with "plus addressing" using a random string. Guaranteed to be unique"""
|
|
local,domain = get_base_ipr_request_address().split('@')
|
|
while True:
|
|
rand = force_str(base64.urlsafe_b64encode(os.urandom(12)))
|
|
address = "{}+{}@{}".format(local,rand,domain)
|
|
q = Message.objects.filter(reply_to=address)
|
|
if not q:
|
|
break
|
|
return address
|
|
|
|
def get_update_cc_addrs(ipr):
|
|
"""Returns list (as a string) of email addresses to use in CC: for an IPR update.
|
|
Logic is from legacy tool. Append submitter or ietfer email of first-order updated
|
|
IPR, append holder of updated IPR, follow chain of updates, appending holder emails
|
|
"""
|
|
emails = []
|
|
if not ipr.updates:
|
|
return ''
|
|
for rel in ipr.updates:
|
|
if rel.target.submitter_email:
|
|
emails.append(rel.target.submitter_email)
|
|
elif hasattr(rel.target,'ietfer_email') and rel.target.ietfer_email:
|
|
emails.append(rel.target.ietfer_email)
|
|
emails = emails + get_holders(ipr)
|
|
|
|
return ','.join(list(set(emails)))
|
|
|
|
def get_update_submitter_emails(ipr):
|
|
"""Returns list of messages, as flat strings, to submitters of IPR(s) being
|
|
updated"""
|
|
messages = []
|
|
email_to_iprs = {}
|
|
email_to_name = {}
|
|
for related in ipr.updates:
|
|
name, email = get_pseudo_submitter(related.target)
|
|
email_to_name[email] = name
|
|
if email in email_to_iprs:
|
|
email_to_iprs[email].append(related.target)
|
|
else:
|
|
email_to_iprs[email] = [related.target]
|
|
|
|
# TODO: This has not been converted to use mailtrigger. It is complicated.
|
|
# When converting it, it will need something like ipr_submitter_ietfer_or_holder perhaps
|
|
for email in email_to_iprs:
|
|
context = dict(
|
|
to_email=email,
|
|
to_name=email_to_name[email],
|
|
iprs=email_to_iprs[email],
|
|
new_ipr=ipr,
|
|
reply_to=get_reply_to())
|
|
text = render_to_string('ipr/update_submitter_email.txt',context)
|
|
messages.append(text)
|
|
return messages
|
|
|
|
def message_from_message(message,by=None):
|
|
"""Returns a ietf.message.models.Message. msg=email.Message"""
|
|
if not by:
|
|
by = Person.objects.get(name="(System)")
|
|
msg = Message.objects.create(
|
|
by = by,
|
|
subject = message.get('subject',''),
|
|
frm = message.get('from',''),
|
|
to = message.get('to',''),
|
|
cc = message.get('cc',''),
|
|
bcc = message.get('bcc',''),
|
|
reply_to = message.get('reply_to',''),
|
|
body = get_body(message),
|
|
time = utc_from_string(message['date']),
|
|
content_type = message.get('content_type', 'text/plain'),
|
|
)
|
|
return msg
|
|
|
|
|
|
class UndeliverableIprResponseError(Exception):
|
|
"""Response email could not be delivered and should be treated as an error"""
|
|
|
|
|
|
def process_response_email(msg):
|
|
"""Save an incoming IPR response email message
|
|
|
|
Message "To" field is expected to be in the format ietf-ipr+[identifier]@ietf.org. If
|
|
the address or identifier is missing, the message will be silently dropped.
|
|
|
|
Expect to find a message with a matching value in the reply_to field, associated to an
|
|
IPR disclosure through IprEvent. If it cannot be matched, raises UndeliverableIprResponseError
|
|
|
|
Creates a Message object for the incoming message and associates it to
|
|
the original message via new IprEvent
|
|
"""
|
|
message = message_from_bytes(force_bytes(msg))
|
|
to = message.get('To', '')
|
|
|
|
# exit if this isn't a response we're interested in (with plus addressing)
|
|
local, domain = get_base_ipr_request_address().split('@')
|
|
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
|
|
_from = message.get("From", "<unknown>")
|
|
log(f"Ignoring IPR email without a message identifier from {_from} to {to}")
|
|
return
|
|
|
|
try:
|
|
to_message = Message.objects.get(reply_to=to)
|
|
except Message.DoesNotExist:
|
|
log('Error finding matching message ({})'.format(to))
|
|
raise UndeliverableIprResponseError(f"Unable to find message matching {to}")
|
|
|
|
try:
|
|
disclosure = to_message.msgevents.first().disclosure
|
|
except:
|
|
log('Error processing message ({})'.format(to))
|
|
raise UndeliverableIprResponseError("Error processing message for {to}")
|
|
|
|
ietf_message = message_from_message(message)
|
|
IprEvent.objects.create(
|
|
type_id = 'msgin',
|
|
by = Person.objects.get(name="(System)"),
|
|
disclosure = disclosure,
|
|
message = ietf_message,
|
|
in_reply_to = to_message
|
|
)
|
|
|
|
log("Received IPR email from %s" % ietf_message.frm)
|
|
|