# Copyright The IETF Trust 2010-2020, All Rights Reserved # -*- coding: utf-8 -*- # generation of mails import datetime import textwrap from django.template.loader import render_to_string from django.utils.html import strip_tags from django.conf import settings from django.urls import reverse as urlreverse from django.utils.encoding import force_str, force_text import debug # pyflakes:ignore from ietf.utils.mail import send_mail, send_mail_text, get_payload from ietf.ipr.utils import iprs_from_docs, related_docs from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent from ietf.doc.utils import needed_ballot_positions from ietf.group.models import Role from ietf.doc.models import Document from ietf.mailtrigger.utils import gather_address_lists def email_state_changed(request, doc, text, mailtrigger_id=None): (to,cc) = gather_address_lists(mailtrigger_id or 'doc_state_edited',doc=doc) if not to: return text = strip_tags(text) send_mail(request, to, None, "Datatracker State Update Notice: %s" % doc.file_tag(), "doc/mail/state_changed_email.txt", dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def email_ad_approved_doc(request, doc, text): to = "iesg@iesg.org" bcc = "iesg-secretary@ietf.org" frm = request.user.person.formatted_email() send_mail(request, to, frm, "Approved: %s" % doc.filename_with_rev(), "doc/mail/ad_approval_email.txt", dict(text=text, docname=doc.filename_with_rev()), bcc=bcc) def email_stream_changed(request, doc, old_stream, new_stream, text=""): """Email the change text to the notify group and to the stream chairs""" streams = [] if old_stream: streams.append(old_stream.slug) if new_stream: streams.append(new_stream.slug) (to,cc) = gather_address_lists('doc_stream_changed',doc=doc,streams=streams) if not to: return if not text: text = "Stream changed to %s from %s" % (new_stream, old_stream) text = strip_tags(text) send_mail(request, to, None, "ID Tracker Stream Change Notice: %s" % doc.file_tag(), "doc/mail/stream_changed_email.txt", dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state): extra=extra_automation_headers(doc) addrs = gather_address_lists('doc_pulled_from_rfc_queue',doc=doc) extra['Cc'] = addrs.cc send_mail(request, addrs.to , None, "%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name), "doc/mail/pulled_from_rfc_queue_email.txt", dict(doc=doc, prev_state=prev_state, next_state=next_state, comment=comment, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), extra=extra) def email_iesg_processing_document(request, doc, changes): addrs = gather_address_lists('doc_iesg_processing_started',doc=doc) tagless_changes = [] for c in changes: tagless_changes.append(strip_tags(c)) send_mail(request, addrs.to, None, 'IESG processing details changed for %s' % doc.name, 'doc/mail/email_iesg_processing.txt', dict(doc=doc, changes=tagless_changes, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=addrs.cc) def html_to_text(html): return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("
", "\n")) def email_update_telechat(request, doc, text): (to, cc) = gather_address_lists('doc_telechat_details_changed',doc=doc) if not to: return text = strip_tags(text) send_mail(request, to, None, "Telechat update notice: %s" % doc.file_tag(), "doc/mail/update_telechat.txt", dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def generate_ballot_writeup(request, doc): e = doc.latest_event(type="iana_review") iana = e.desc if e else "" e = WriteupDocEvent() e.type = "changed_ballot_writeup_text" e.by = request.user.person e.doc = doc e.rev = doc.rev e.desc = "Ballot writeup was generated" e.text = force_text(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana})) # caller is responsible for saving, if necessary return e def generate_ballot_rfceditornote(request, doc): e = WriteupDocEvent() e.type = "changed_ballot_rfceditornote_text" e.by = request.user.person e.doc = doc e.rev = doc.rev e.desc = "RFC Editor Note for ballot was generated" e.text = force_text(render_to_string("doc/mail/ballot_rfceditornote.txt")) e.save() return e def generate_last_call_announcement(request, doc): expiration_date = datetime.date.today() + datetime.timedelta(days=14) if doc.group.type_id in ("individ", "area"): group = "an individual submitter" expiration_date += datetime.timedelta(days=14) else: group = "the %s %s (%s)" % (doc.group.name, doc.group.type.name, doc.group.acronym) doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3) iprs = iprs_from_docs(related_docs(DocAlias.objects.get(name=doc.canonical_name()))) if iprs: ipr_links = [ urlreverse("ietf.ipr.views.show", kwargs=dict(id=i.id)) for i in iprs] ipr_links = [ settings.IDTRACKER_BASE_URL+url if not url.startswith("http") else url for url in ipr_links ] else: ipr_links = None downrefs = [rel for rel in doc.relateddocument_set.all() if rel.is_downref() and not rel.is_approved_downref()] addrs = gather_address_lists('last_call_issued',doc=doc).as_strings() mail = render_to_string("doc/mail/last_call_announcement.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + "ballot/", expiration_date=expiration_date.strftime("%Y-%m-%d"), #.strftime("%B %-d, %Y"), to=addrs.to, cc=addrs.cc, group=group, docs=[ doc ], urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], ipr_links=ipr_links, downrefs=downrefs, ) ) e = WriteupDocEvent() e.type = "changed_last_call_text" e.by = request.user.person e.doc = doc e.rev = doc.rev e.desc = "Last call announcement was generated" e.text = force_text(mail) # caller is responsible for saving, if necessary return e DO_NOT_PUBLISH_IESG_STATES = ("nopubadw", "nopubanw") def generate_approval_mail(request, doc): if doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES or doc.stream_id in ('ise','irtf'): mail = generate_approval_mail_rfc_editor(request, doc) else: mail = generate_approval_mail_approved(request, doc) e = WriteupDocEvent() e.type = "changed_ballot_approval_text" e.by = request.user.person e.doc = doc e.rev = doc.rev e.desc = "Ballot approval text was generated" e.text = force_text(mail) # caller is responsible for saving, if necessary return e def generate_approval_mail_approved(request, doc): if doc.intended_std_level_id in ("std", "ds", "ps", "bcp"): action_type = "Protocol" else: action_type = "Document" # the second check catches some area working groups (like # Transport Area Working Group) if doc.group.type_id not in ("area", "individ", "ag", "rg") and not doc.group.name.endswith("Working Group"): doc.group.name_with_wg = doc.group.name + " Working Group" else: doc.group.name_with_wg = doc.group.name doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3) if doc.group.type_id in ("individ", "area"): made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group." else: made_by = "This document is the product of the %s." % doc.group.name_with_wg responsible_directors = set([doc.ad,]) if doc.group.type_id not in ("individ","area"): responsible_directors.update([x.person for x in Role.objects.filter(group=doc.group.parent,name='ad')]) responsible_directors = [x.plain_name() for x in responsible_directors if x] if len(responsible_directors)>1: contacts = "The IESG contact persons are "+", ".join(responsible_directors[:-1])+" and "+responsible_directors[-1]+"." else: contacts = "The IESG contact person is %s." % responsible_directors[0] doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft" addrs = gather_address_lists('ballot_approved_ietf_stream',doc=doc).as_strings() return render_to_string("doc/mail/approval_mail.txt", dict(doc=doc, docs=[doc], doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), to = addrs.to, cc = addrs.cc, doc_type=doc_type, made_by=made_by, contacts=contacts, action_type=action_type, ) ) def generate_approval_mail_rfc_editor(request, doc): # This is essentially dead code - it is only exercised if the IESG ballots on some other stream's document, # which does not happen now that we have conflict reviews. disapproved = doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft" addrs = gather_address_lists('ballot_approved_conflrev', doc=doc).as_strings() return render_to_string("doc/mail/approval_mail_rfc_editor.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), doc_type=doc_type, disapproved=disapproved, to = addrs.to, cc = addrs.cc, ) ) def generate_publication_request(request, doc): group_description = "" if doc.group and doc.group.acronym != "none": group_description = doc.group.name if doc.group.type_id not in ("ietf", "irtf", "iab",): group_description += " %s (%s)" % (doc.group.type, doc.group.acronym) e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") consensus = e.consensus if e else None if doc.stream_id == "irtf": approving_body = "IRSG" consensus_body = doc.group.acronym.upper() else: approving_body = str(doc.stream) consensus_body = approving_body e = doc.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text") rfcednote = e.text if e else "" return render_to_string("doc/mail/publication_request.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), group_description=group_description, approving_body=approving_body, consensus_body=consensus_body, consensus=consensus, rfc_editor_note=rfcednote, ) ) def send_last_call_request(request, doc): (to, cc) = gather_address_lists('last_call_requested',doc=doc) frm = '"DraftTracker Mail System" ' send_mail(request, to, frm, "Last Call: %s" % doc.file_tag(), "doc/mail/last_call_request.txt", dict(docs=[doc], doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), ), cc=cc) def email_resurrect_requested(request, doc, by): (to, cc) = gather_address_lists('resurrection_requested',doc=doc) if by.role_set.filter(name="secr", group__acronym="secretariat"): e = by.role_email("secr", group="secretariat") else: e = by.role_email("ad") frm = e.address send_mail(request, to, e.formatted_email(), "I-D Resurrection Request", "doc/mail/resurrect_request_email.txt", dict(doc=doc, by=frm, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def email_resurrection_completed(request, doc, requester): (to, cc) = gather_address_lists('resurrection_completed',doc=doc) frm = "I-D Administrator " send_mail(request, to, frm, "I-D Resurrection Completed - %s" % doc.file_tag(), "doc/mail/resurrect_completed_email.txt", dict(doc=doc, by=frm, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def email_ballot_deferred(request, doc, by, telechat_date): (to, cc) = gather_address_lists('ballot_deferred',doc=doc) frm = "DraftTracker Mail System " send_mail(request, to, frm, "IESG Deferred Ballot notification: %s" % doc.file_tag(), "doc/mail/ballot_deferred_email.txt", dict(doc=doc, by=by, action='deferred', telechat_date=telechat_date), cc=cc) def email_ballot_undeferred(request, doc, by, telechat_date): (to, cc) = gather_address_lists('ballot_deferred',doc=doc) frm = "DraftTracker Mail System " send_mail(request, to, frm, "IESG Undeferred Ballot notification: %s" % doc.file_tag(), "doc/mail/ballot_deferred_email.txt", dict(doc=doc, by=by, action='undeferred', telechat_date=telechat_date), cc=cc) def generate_issue_ballot_mail(request, doc, ballot): e = doc.latest_event(LastCallDocEvent, type="sent_last_call") last_call_expires = e.expires if e else None last_call_has_expired = last_call_expires and last_call_expires < datetime.datetime.now() return render_to_string("doc/mail/issue_ballot_mail.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), last_call_expires=last_call_expires, last_call_has_expired=last_call_has_expired, needed_ballot_positions= needed_ballot_positions(doc, list(doc.active_ballot().active_balloter_positions().values()) ), ) ) def email_iana(request, doc, to, msg, cc=None): # fix up message and send it with extra info on doc in headers import email parsed_msg = email.message_from_string(force_str(msg)) parsed_msg.set_charset('UTF-8') extra = extra_automation_headers(doc) extra["Reply-To"] = ["noreply@ietf.org", ] send_mail_text(request, to, parsed_msg["From"], parsed_msg["Subject"], get_payload(parsed_msg), extra=extra, cc=cc) def extra_automation_headers(doc): extra = {} extra["X-IETF-Draft-string"] = [ doc.name, ] extra["X-IETF-Draft-revision"] = [ doc.rev, ] return extra def email_last_call_expired(doc): if not doc.type_id in ['draft','statchg']: return text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.get_state("draft-iesg" if doc.type_id == 'draft' else "statchg").name addrs = gather_address_lists('last_call_expired',doc=doc) send_mail(None, addrs.to, "DraftTracker Mail System ", "Last Call Expired: %s" % doc.file_tag(), "doc/mail/change_notice.txt", dict(text=text, doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc = addrs.cc) def email_intended_status_changed(request, doc, text): (to,cc) = gather_address_lists('doc_intended_status_changed',doc=doc) if not to: return text = strip_tags(text) send_mail(request, to, None, "Intended Status for %s changed to %s" % (doc.file_tag(),doc.intended_std_level), "doc/mail/intended_status_changed_email.txt", dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc=cc) def email_comment(request, doc, comment): (to, cc) = gather_address_lists('doc_added_comment',doc=doc) send_mail(request, to, None, "Comment added to %s history"%doc.name, "doc/mail/comment_added_email.txt", dict( comment=comment, doc=doc, by=request.user.person, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), ), cc = cc) def email_adopted(request, doc, prev_state, new_state, by, comment=""): (to, cc) = gather_address_lists('doc_adopted_by_group',doc=doc) state_type = (prev_state or new_state).type send_mail(request, to, settings.DEFAULT_FROM_EMAIL, 'The %s %s has placed %s in state "%s"' % (doc.group.acronym.upper(),doc.group.type_id.upper(), doc.name, new_state or "None"), 'doc/mail/doc_adopted_email.txt', dict(doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), state_type=state_type, prev_state=prev_state, new_state=new_state, by=by, comment=comment), cc=cc) def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""): (to, cc)= gather_address_lists('doc_stream_state_edited',doc=doc) state_type = (prev_state or new_state).type send_mail(request, to, settings.DEFAULT_FROM_EMAIL, "%s changed for %s" % (state_type.label, doc.name), 'doc/mail/stream_state_changed_email.txt', dict(doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), state_type=state_type, prev_state=prev_state, new_state=new_state, by=by, comment=comment), cc=cc) def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""): (to, cc) = gather_address_lists('doc_stream_state_edited',doc=doc) send_mail(request, to, settings.DEFAULT_FROM_EMAIL, "Tags changed for %s" % doc.name, 'doc/mail/stream_tags_changed_email.txt', dict(doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), added=added_tags, removed=removed_tags, by=by, comment=comment), cc=cc) def send_review_possibly_replaces_request(request, doc, submitter_info): addrs = gather_address_lists('doc_replacement_suggested',doc=doc) to = set(addrs.to) cc = set(addrs.cc) possibly_replaces = Document.objects.filter(name__in=[alias.name for alias in doc.related_that_doc("possibly-replaces")]) for other_doc in possibly_replaces: (other_to, other_cc) = gather_address_lists('doc_replacement_suggested',doc=other_doc) to.update(other_to) cc.update(other_cc) send_mail(request, list(to), settings.DEFAULT_FROM_EMAIL, 'Review of suggested possible replacements for %s-%s needed' % (doc.name, doc.rev), 'doc/mail/review_possibly_replaces_request.txt', dict(doc= doc, submitter_info=submitter_info, possibly_replaces=doc.related_that_doc("possibly-replaces"), review_url=settings.IDTRACKER_BASE_URL + urlreverse('ietf.doc.views_draft.review_possibly_replaces', kwargs={ "name": doc.name })), cc=list(cc),) def email_charter_internal_review(request, charter): addrs = gather_address_lists('charter_internal_review',doc=charter,group=charter.group) charter_text = charter.text_or_error() # pyflakes:ignore send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, 'Internal %s Review: %s (%s)'%(charter.group.type.name,charter.group.name,charter.group.acronym), 'doc/mail/charter_internal_review.txt', dict(charter=charter, charter_text=charter_text, review_type = "new" if charter.group.state_id == "proposed" else "recharter", charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), chairs=charter.group.role_set.filter(name="chair"), secr=charter.group.role_set.filter(name="secr"), ads=charter.group.role_set.filter(name='ad'), parent_ads=charter.group.parent.role_set.filter(name='ad'), techadv=charter.group.role_set.filter(name="techadv"), milestones=charter.group.groupmilestone_set.filter(state="charter"), ), cc=addrs.cc, extra={'Reply-To': ["irsg@irtf.org" if charter.group.type_id == 'rg' else "iesg@ietf.org", ]}, )