#!/usr/bin/python3 # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from __future__ import annotations # This tool is based on the Superset send_email script: # https://github.com/apache/incubator-superset/blob/master/RELEASING/send_email.py import os import shutil import smtplib import ssl import sys try: import jinja2 except ModuleNotFoundError: sys.exit("Jinja2 is a required dependency for this script") try: import rich_click as click except ModuleNotFoundError: sys.exit("Click is a required dependency for this script") SMTP_PORT = 587 SMTP_SERVER = "mail-relay.apache.org" MAILING_LIST = {"dev": "dev@airflow.apache.org", "users": "users@airflow.apache.org"} def string_comma_to_list(message: str) -> list[str]: """ Split string to list """ return message.split(",") if message else [] def send_email( smtp_server: str, smpt_port: int, username: str, password: str, sender_email: str, receiver_email: str | list, message: str, ): """ Send a simple text email (SMTP) """ context = ssl.create_default_context() with smtplib.SMTP(smtp_server, smpt_port) as server: server.starttls(context=context) server.login(username, password) server.sendmail(sender_email, receiver_email, message) def render_template(template_file: str, **kwargs) -> str: """ Simple render template based on named parameters :param template_file: The template file location :kwargs: Named parameters to use when rendering the template :return: Rendered template """ dir_path = os.path.dirname(os.path.realpath(__file__)) template = jinja2.Template(open(os.path.join(dir_path, template_file)).read()) return template.render(kwargs) def show_message(entity: str, message: str): """ Show message on the Command Line """ width, _ = shutil.get_terminal_size() click.secho("-" * width, fg="blue") click.secho(f"{entity} Message:", fg="bright_red", bold=True) click.secho("-" * width, fg="blue") click.echo(message) click.secho("-" * width, fg="blue") def inter_send_email( username: str, password: str, sender_email: str, receiver_email: str | list, message: str ): """ Send email using SMTP """ show_message("SMTP", message) click.confirm("Is the Email message ok?", abort=True) try: send_email( SMTP_SERVER, SMTP_PORT, username, password, sender_email, receiver_email, message, ) click.secho("✅ Email sent successfully", fg="green") except smtplib.SMTPAuthenticationError: sys.exit("SMTP User authentication error, Email not sent!") except Exception as e: sys.exit(f"SMTP exception {e}") class BaseParameters: """ Base Class to send emails using Apache Creds and for Jinja templating """ def __init__(self, name=None, email=None, username=None, password=None, version=None, version_rc=None): self.name = name self.email = email self.username = username self.password = password self.version = version self.version_rc = version_rc self.template_arguments = {} def __repr__(self): return f"Apache Credentials: {self.email}/{self.username}/{self.version}/{self.version_rc}" @click.group(context_settings=dict(help_option_names=["-h", "--help"])) @click.pass_context @click.option( "-e", "--apache_email", prompt="Apache Email", envvar="APACHE_EMAIL", show_envvar=True, help="Your Apache email will be used for SMTP From", required=True, ) @click.option( "-u", "--apache_username", prompt="Apache Username", envvar="APACHE_USERNAME", show_envvar=True, help="Your LDAP Apache username", required=True, ) @click.password_option( # type: ignore "-p", "--apache_password", prompt="Apache Password", envvar="APACHE_PASSWORD", show_envvar=True, help="Your LDAP Apache password", required=True, ) @click.option( "-v", "--version", prompt="Version", envvar="AIRFLOW_VERSION", show_envvar=True, help="Release Version", required=True, ) @click.option( "-rc", "--version_rc", prompt="Version (with RC)", envvar="AIRFLOW_VERSION_RC", show_envvar=True, help="Release Candidate Version", required=True, ) @click.option( # type: ignore "-n", "--name", prompt="Your Name", default=lambda: os.environ.get("USER", ""), show_default="Current User", help="Name of the Release Manager", type=click.STRING, required=True, ) def cli( ctx, apache_email: str, apache_username: str, apache_password: str, version: str, version_rc: str, name: str, ): """ 🚀 CLI to send emails for the following: \b * Voting thread for the rc * Result of the voting for the rc * Announcing that the new version has been released """ base_parameters = BaseParameters( name, apache_email, apache_username, apache_password, version, version_rc ) base_parameters.template_arguments["version"] = base_parameters.version base_parameters.template_arguments["version_rc"] = base_parameters.version_rc base_parameters.template_arguments["sender_email"] = base_parameters.email base_parameters.template_arguments["release_manager"] = base_parameters.name ctx.obj = base_parameters @cli.command("vote") @click.option( "--receiver_email", default=MAILING_LIST.get("dev"), type=click.STRING, prompt="The receiver email (To:)", ) @click.pass_obj def vote(base_parameters, receiver_email: str): """ Send email calling for Votes on RC """ template_file = "templates/vote_email.j2" base_parameters.template_arguments["receiver_email"] = receiver_email message = render_template(template_file, **base_parameters.template_arguments) inter_send_email( base_parameters.username, base_parameters.password, base_parameters.template_arguments["sender_email"], base_parameters.template_arguments["receiver_email"], message, ) if click.confirm("Show Slack message for announcement?", default=True): base_parameters.template_arguments["slack_rc"] = False slack_msg = render_template("templates/slack.j2", **base_parameters.template_arguments) show_message("Slack", slack_msg) @cli.command("result") @click.option( "-re", "--receiver_email", default=MAILING_LIST.get("dev"), type=click.STRING, prompt="The receiver email (To:)", ) @click.option( "--vote_bindings", default="", type=click.STRING, prompt="A List of people with +1 binding vote (ex: Max,Grace,Krist)", ) @click.option( "--vote_nonbindings", default="", type=click.STRING, prompt="A List of people with +1 non binding vote (ex: Ville)", ) @click.option( "--vote_negatives", default="", type=click.STRING, prompt="A List of people with -1 vote (ex: John)", ) @click.pass_obj def result( base_parameters, receiver_email: str, vote_bindings: str, vote_nonbindings: str, vote_negatives: str, ): """ Send email with results of voting on RC """ template_file = "templates/result_email.j2" base_parameters.template_arguments["receiver_email"] = receiver_email base_parameters.template_arguments["vote_bindings"] = string_comma_to_list(vote_bindings) base_parameters.template_arguments["vote_nonbindings"] = string_comma_to_list(vote_nonbindings) base_parameters.template_arguments["vote_negatives"] = string_comma_to_list(vote_negatives) message = render_template(template_file, **base_parameters.template_arguments) inter_send_email( base_parameters.username, base_parameters.password, base_parameters.template_arguments["sender_email"], base_parameters.template_arguments["receiver_email"], message, ) @cli.command("announce") @click.option( "--receiver_email", default=",".join(MAILING_LIST.values()), prompt="The receiver email (To:)", help="Receiver's email address. If more than 1, separate them by comma", ) @click.pass_obj def announce(base_parameters, receiver_email: str): """ Send email to announce release of the new version """ receiver_emails: list[str] = string_comma_to_list(receiver_email) template_file = "templates/announce_email.j2" base_parameters.template_arguments["receiver_email"] = receiver_emails message = render_template(template_file, **base_parameters.template_arguments) inter_send_email( base_parameters.username, base_parameters.password, base_parameters.template_arguments["sender_email"], base_parameters.template_arguments["receiver_email"], message, ) if click.confirm("Show Slack message for announcement?", default=True): base_parameters.template_arguments["slack_rc"] = False slack_msg = render_template("templates/slack.j2", **base_parameters.template_arguments) show_message("Slack", slack_msg) if click.confirm("Show Twitter message for announcement?", default=True): twitter_msg = render_template("templates/twitter.j2", **base_parameters.template_arguments) show_message("Twitter", twitter_msg) if __name__ == "__main__": cli()