datatracker/ietf/submit/mail.py
Lars Eggert f8b48f4c43
fix: use Internet-Draft more consistently across the UI (#5104)
* s/Internet Draft/Internet-Draft/i

* s/draft/Internet-Draft/i or s/draft/I-D/i

* s/ID/I-D/

* Fix tests

* a -> an

* Undo case-change to ASCII

* Address code review comments

* Add migrations

* Add merged migration

* fix: straighten out migrations

* fix: finish straightening out migrations

---------

Co-authored-by: Robert Sparks <rjsparks@nostrum.com>
2023-02-11 10:09:28 -06:00

377 lines
14 KiB
Python

# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import re
import email
import base64
import os
import pyzmail
from django.conf import settings
from django.urls import reverse as urlreverse
from django.core.exceptions import ValidationError
from django.contrib.sites.models import Site
from django.template.loader import render_to_string
from django.utils.encoding import force_text, force_str
import debug # pyflakes:ignore
from ietf.utils.log import log
from ietf.utils.mail import send_mail, send_mail_message
from ietf.doc.models import Document
from ietf.ipr.mail import utc_from_string
from ietf.person.models import Person
from ietf.message.models import Message, MessageAttachment
from ietf.utils.accesstoken import generate_access_token
from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address
from ietf.submit.models import SubmissionEmailEvent, Submission
from ietf.submit.checkers import DraftIdnitsChecker
from ietf.utils.timezone import date_today
def send_submission_confirmation(request, submission, chair_notice=False):
subject = 'Confirm submission of I-D %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email, cc) = gather_address_lists('sub_confirmation_requested',submission=submission)
confirmation_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
status_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/confirm_submission.txt',
{
'submission': submission,
'confirmation_url': confirmation_url,
'status_url': status_url,
'chair_notice': chair_notice,
},
cc=cc)
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_full_url(request, submission):
subject = 'Full URL for managing submission of Internet-Draft %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email, cc) = gather_address_lists('sub_management_url_requested',submission=submission)
url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/full_url.txt',
{
'submission': submission,
'url': url,
},
cc=cc)
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_approval_request(request, submission, replaced_doc=None):
"""Send an approval request for a submission
If replaced_doc is not None, requests will be sent to the wg chairs or ADs
responsible for that doc's group instead of the submission.
"""
subject = 'New Internet-Draft waiting for approval: %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
# Sort out which MailTrigger to use
mt_kwargs = dict(submission=submission)
if replaced_doc:
mt_kwargs['doc'] = replaced_doc
if submission.state_id == 'ad-appr':
approval_type = 'ad'
if replaced_doc:
mt_slug = 'sub_replaced_doc_director_approval_requested'
else:
mt_slug = 'sub_director_approval_requested'
else:
approval_type = 'chair'
if replaced_doc:
mt_slug = 'sub_replaced_doc_chair_approval_requested'
else:
mt_slug = 'sub_chair_approval_requested'
(to_email,cc) = gather_address_lists(mt_slug, **mt_kwargs)
if not to_email:
return to_email
send_mail(request, to_email, from_email, subject, 'submit/approval_request.txt',
{
'submission': submission,
'domain': Site.objects.get_current().domain,
'approval_type': approval_type,
},
cc=cc)
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_manual_post_request(request, submission, errors):
subject = 'Manual Post Requested for %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email,cc) = gather_address_lists('sub_manual_post_requested',submission=submission)
checker = DraftIdnitsChecker(options=[]) # don't use the default --submitcheck limitation
file_name = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (submission.name, submission.rev))
nitspass, nitsmsg, nitserr, nitswarn, nitsresult = checker.check_file_txt(file_name)
send_mail(request, to_email, from_email, subject, 'submit/manual_post_request.txt', {
'submission': submission,
'url': settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk)),
'errors': errors,
'idnits': nitsmsg,
}, cc=cc)
def announce_to_lists(request, submission):
m = Message()
m.by = Person.objects.get(name="(System)")
if request.user.is_authenticated:
try:
m.by = request.user.person
except Person.DoesNotExist:
pass
m.subject = 'I-D Action: %s-%s.txt' % (submission.name, submission.rev)
m.frm = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
(m.to, m.cc) = gather_address_lists('sub_announced',submission=submission).as_strings()
if m.cc:
m.reply_to = m.cc
m.body = render_to_string('submit/announce_to_lists.txt',
dict(submission=submission,
settings=settings))
m.save()
m.related_docs.add(Document.objects.get(name=submission.name))
send_mail_message(request, m)
def announce_new_wg_00(request, submission):
m = Message()
m.by = Person.objects.get(name="(System)")
if request.user.is_authenticated:
try:
m.by = request.user.person
except Person.DoesNotExist:
pass
m.subject = 'I-D Action: %s-%s.txt' % (submission.name, submission.rev)
m.frm = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
(m.to, m.cc) = gather_address_lists('sub_new_wg_00',submission=submission).as_strings()
if m.cc:
m.reply_to = m.cc
m.body = render_to_string('submit/announce_to_lists.txt',
dict(submission=submission,
settings=settings))
m.save()
m.related_docs.add(Document.objects.get(name=submission.name))
send_mail_message(request, m)
def announce_new_version(request, submission, draft, state_change_msg):
(to_email,cc) = gather_address_lists('sub_new_version',doc=draft,submission=submission)
if to_email:
subject = 'New Version Notification - %s-%s.txt' % (submission.name, submission.rev)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt',
{'submission': submission,
'msg': state_change_msg},
cc=cc)
def announce_to_authors(request, submission):
(to_email, cc) = gather_address_lists('sub_announced_to_authors',submission=submission)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
subject = 'New Version Notification for %s-%s.txt' % (submission.name, submission.rev)
if submission.group:
group = submission.group.acronym
elif submission.name.startswith('draft-iesg'):
group = 'IESG'
else:
group = 'Individual Submission'
send_mail(request, to_email, from_email, subject, 'submit/announce_to_authors.txt',
{'submission': submission,
'group': group},
cc=cc)
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_submission_message_address().split('@')
while True:
rand = force_text(base64.urlsafe_b64encode(os.urandom(12)))
address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address)
if not q:
return address
def process_response_email(msg):
"""Saves an incoming message. msg=string. Message "To" field is expected to
be in the format ietf-submit+[identifier]@ietf.org. Expect to find a message with
a matching value in the reply_to field, associated to a submission.
Create a Message object for the incoming message and associate it to
the original message via new SubmissionEvent"""
message = email.message_from_string(force_str(msg))
to = message.get('To')
# exit if this isn't a response we're interested in (with plus addressing)
local,domain = get_base_submission_message_address().split('@')
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
return None
try:
to_message = Message.objects.get(reply_to=to)
except Message.DoesNotExist:
log('Error finding matching message ({})'.format(to))
return None
try:
submission = to_message.manualevents.first().submission
except:
log('Error processing message ({})'.format(to))
return None
if not submission:
log('Error processing message - no submission ({})'.format(to))
return None
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
by = Person.objects.get(name="(System)")
msg = submit_message_from_message(message, body, by)
desc = "Email: received message - manual post - {}-{}".format(
submission.name,
submission.rev)
submission_email_event = SubmissionEmailEvent.objects.create(
submission = submission,
desc = desc,
msgtype = 'msgin',
by = by,
message = msg,
in_reply_to = to_message
)
save_submission_email_attachments(submission_email_event, parts)
log("Received submission email from %s" % msg.frm)
return msg
def add_submission_email(request, remote_ip, name, rev, submission_pk, message, by, msgtype):
"""Add email to submission history"""
#in_reply_to = form.cleaned_data['in_reply_to']
# create Message
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
msg = submit_message_from_message(message, body, by)
if (submission_pk != None):
# Must exist - we're adding a message to an existing submission
submission = Submission.objects.get(pk=submission_pk)
else:
# Must not exist
submissions = Submission.objects.filter(name=name,rev=rev).exclude(state_id='cancel')
if submissions.count() > 0:
raise ValidationError("Submission {} already exists".format(name))
# create Submission using the name
try:
submission = Submission.objects.create(
state_id="waiting-for-draft",
remote_ip=remote_ip,
name=name,
rev=rev,
title=name,
note="",
submission_date=date_today(),
replaces="",
)
from ietf.submit.utils import create_submission_event, docevent_from_submission
desc = "Submission created for rev {} in response to email".format(rev)
create_submission_event(request,
submission,
desc)
docevent_from_submission(submission, desc)
except Exception as e:
log("Exception: %s\n" % e)
raise
if msgtype == 'msgin':
rs = "Received"
else:
rs = "Sent"
desc = "{} message - manual post - {}-{}".format(rs, name, rev)
submission_email_event = SubmissionEmailEvent.objects.create(
desc = desc,
submission = submission,
msgtype = msgtype,
by = by,
message = msg)
#in_reply_to = in_reply_to
save_submission_email_attachments(submission_email_event, parts)
return submission, submission_email_event
def submit_message_from_message(message,body,by=None):
"""Returns a ietf.message.models.Message. msg=email.Message
A copy of mail.message_from_message with different body handling
"""
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 = body,
time = utc_from_string(message.get('date', '')),
content_type = message.get('content_type', 'text/plain'),
)
return msg
def save_submission_email_attachments(submission_email_event, parts):
for part in parts:
if part.disposition != 'attachment':
continue
if part.type == 'text/plain':
payload, used_charset = pyzmail.decode_text(part.get_payload(),
part.charset,
None)
encoding = ""
else:
# Need a better approach - for the moment we'll just handle these
# and encode as base64
payload = base64.b64encode(part.get_payload())
encoding = "base64"
#name = submission_email_event.submission.name
MessageAttachment.objects.create(message = submission_email_event.message,
content_type = part.type,
encoding = encoding,
filename=part.filename,
body=payload)