Change sendmail functions into classes that can be overriden from a plugin (#2221)

* Change sendmail functions into classes that can be overriden from a plugin
* Deprecate `CTFd.utils.email.mailgun.sendmail`
* Deprecate `CTFd.utils.email.smtp.sendmail`
This commit is contained in:
Kevin Chung
2022-11-06 17:37:15 -05:00
committed by GitHub
parent dfa7f87823
commit e4a605e235
9 changed files with 156 additions and 115 deletions

View File

@@ -102,6 +102,12 @@ MAILGUN_API_KEY =
# Installations using the Mailgun API should migrate over to SMTP settings.
MAILGUN_BASE_URL =
# MAIL_PROVIDER
# Specifies the email provider that CTFd will use to send email.
# By default CTFd will automatically detect the correct email provider based on the other settings
# specified here or in the configuration panel. This setting can be used to force a specific provider.
MAIL_PROVIDER =
[uploads]
# UPLOAD_PROVIDER
# Specifies the service that CTFd should use to store files.

View File

@@ -154,6 +154,8 @@ class ServerConfig(object):
MAILGUN_BASE_URL: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
MAIL_PROVIDER: str = empty_str_cast(config_ini["email"].get("MAIL_PROVIDER"))
# === LOGS ===
LOG_FOLDER: str = empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \
or os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")

View File

@@ -60,6 +60,9 @@ def can_send_mail():
def get_mail_provider():
mail_provider = app.config.get("MAIL_PROVIDER")
if mail_provider:
return mail_provider
if get_config("mail_server") and get_config("mail_port"):
return "smtp"
if get_config("mailgun_api_key") and get_config("mailgun_base_url"):

View File

@@ -2,10 +2,13 @@ from flask import url_for
from CTFd.utils import get_config
from CTFd.utils.config import get_mail_provider
from CTFd.utils.email import mailgun, smtp
from CTFd.utils.email.providers.mailgun import MailgunEmailProvider
from CTFd.utils.email.providers.smtp import SMTPEmailProvider
from CTFd.utils.formatters import safe_format
from CTFd.utils.security.signing import serialize
PROVIDERS = {"smtp": SMTPEmailProvider, "mailgun": MailgunEmailProvider}
DEFAULT_VERIFICATION_EMAIL_SUBJECT = "Confirm your account for {ctf_name}"
DEFAULT_VERIFICATION_EMAIL_BODY = (
"Welcome to {ctf_name}!\n\n"
@@ -42,11 +45,10 @@ DEFAULT_PASSWORD_CHANGE_ALERT_BODY = (
def sendmail(addr, text, subject="Message from {ctf_name}"):
subject = safe_format(subject, ctf_name=get_config("ctf_name"))
provider = get_mail_provider()
if provider == "smtp":
return smtp.sendmail(addr, text, subject)
if provider == "mailgun":
return mailgun.sendmail(addr, text, subject)
return False, "No mail settings configured"
EmailProvider = PROVIDERS.get(provider)
if EmailProvider is None:
return False, "No mail settings configured"
return EmailProvider.sendmail(addr, text, subject)
def password_change_alert(email):

View File

@@ -1,40 +1,8 @@
from email.utils import formataddr
import requests
from CTFd.utils import get_app_config, get_config
from CTFd.utils.email.providers.mailgun import MailgunEmailProvider
def sendmail(addr, text, subject):
ctf_name = get_config("ctf_name")
mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR")
mailfrom_addr = formataddr((ctf_name, mailfrom_addr))
mailgun_base_url = get_config("mailgun_base_url") or get_app_config(
"MAILGUN_BASE_URL"
print(
"CTFd.utils.email.mailgun.sendmail will raise an exception in a future minor release of CTFd and then be removed in CTFd v4.0"
)
mailgun_api_key = get_config("mailgun_api_key") or get_app_config("MAILGUN_API_KEY")
try:
r = requests.post(
mailgun_base_url + "/messages",
auth=("api", mailgun_api_key),
data={
"from": mailfrom_addr,
"to": [addr],
"subject": subject,
"text": text,
},
timeout=1.0,
)
except requests.RequestException as e:
return (
False,
"{error} exception occured while handling your request".format(
error=type(e).__name__
),
)
if r.status_code == 200:
return True, "Email sent"
else:
return False, "Mailgun settings are incorrect"
return MailgunEmailProvider.sendmail(addr, text, subject)

View File

@@ -0,0 +1,4 @@
class EmailProvider:
@staticmethod
def sendmail(addr, text, subject):
raise NotImplementedError

View File

@@ -0,0 +1,45 @@
from email.utils import formataddr
import requests
from CTFd.utils import get_app_config, get_config
from CTFd.utils.email.providers import EmailProvider
class MailgunEmailProvider(EmailProvider):
@staticmethod
def sendmail(addr, text, subject):
ctf_name = get_config("ctf_name")
mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR")
mailfrom_addr = formataddr((ctf_name, mailfrom_addr))
mailgun_base_url = get_config("mailgun_base_url") or get_app_config(
"MAILGUN_BASE_URL"
)
mailgun_api_key = get_config("mailgun_api_key") or get_app_config(
"MAILGUN_API_KEY"
)
try:
r = requests.post(
mailgun_base_url + "/messages",
auth=("api", mailgun_api_key),
data={
"from": mailfrom_addr,
"to": [addr],
"subject": subject,
"text": text,
},
timeout=1.0,
)
except requests.RequestException as e:
return (
False,
"{error} exception occured while handling your request".format(
error=type(e).__name__
),
)
if r.status_code == 200:
return True, "Email sent"
else:
return False, "Mailgun settings are incorrect"

View File

@@ -0,0 +1,79 @@
import smtplib
from email.message import EmailMessage
from email.utils import formataddr
from socket import timeout
from CTFd.utils import get_app_config, get_config
from CTFd.utils.email.providers import EmailProvider
class SMTPEmailProvider(EmailProvider):
@staticmethod
def sendmail(addr, text, subject):
ctf_name = get_config("ctf_name")
mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR")
mailfrom_addr = formataddr((ctf_name, mailfrom_addr))
data = {
"host": get_config("mail_server") or get_app_config("MAIL_SERVER"),
"port": int(get_config("mail_port") or get_app_config("MAIL_PORT")),
}
username = get_config("mail_username") or get_app_config("MAIL_USERNAME")
password = get_config("mail_password") or get_app_config("MAIL_PASSWORD")
TLS = get_config("mail_tls") or get_app_config("MAIL_TLS")
SSL = get_config("mail_ssl") or get_app_config("MAIL_SSL")
auth = get_config("mail_useauth") or get_app_config("MAIL_USEAUTH")
if username:
data["username"] = username
if password:
data["password"] = password
if TLS:
data["TLS"] = TLS
if SSL:
data["SSL"] = SSL
if auth:
data["auth"] = auth
try:
smtp = get_smtp(**data)
msg = EmailMessage()
msg.set_content(text)
msg["Subject"] = subject
msg["From"] = mailfrom_addr
msg["To"] = addr
# Check whether we are using an admin-defined SMTP server
custom_smtp = bool(get_config("mail_server"))
# We should only consider the MAILSENDER_ADDR value on servers defined in config
if custom_smtp:
smtp.send_message(msg)
else:
mailsender_addr = get_app_config("MAILSENDER_ADDR")
smtp.send_message(msg, from_addr=mailsender_addr)
smtp.quit()
return True, "Email sent"
except smtplib.SMTPException as e:
return False, str(e)
except timeout:
return False, "SMTP server connection timed out"
except Exception as e:
return False, str(e)
def get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):
if SSL is None:
smtp = smtplib.SMTP(host, port, timeout=3)
else:
smtp = smtplib.SMTP_SSL(host, port, timeout=3)
if TLS:
smtp.starttls()
if auth:
smtp.login(username, password)
return smtp

View File

@@ -1,76 +1,8 @@
import smtplib
from email.message import EmailMessage
from email.utils import formataddr
from socket import timeout
from CTFd.utils import get_app_config, get_config
def get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):
if SSL is None:
smtp = smtplib.SMTP(host, port, timeout=3)
else:
smtp = smtplib.SMTP_SSL(host, port, timeout=3)
if TLS:
smtp.starttls()
if auth:
smtp.login(username, password)
return smtp
from CTFd.utils.email.providers.smtp import SMTPEmailProvider
def sendmail(addr, text, subject):
ctf_name = get_config("ctf_name")
mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR")
mailfrom_addr = formataddr((ctf_name, mailfrom_addr))
data = {
"host": get_config("mail_server") or get_app_config("MAIL_SERVER"),
"port": int(get_config("mail_port") or get_app_config("MAIL_PORT")),
}
username = get_config("mail_username") or get_app_config("MAIL_USERNAME")
password = get_config("mail_password") or get_app_config("MAIL_PASSWORD")
TLS = get_config("mail_tls") or get_app_config("MAIL_TLS")
SSL = get_config("mail_ssl") or get_app_config("MAIL_SSL")
auth = get_config("mail_useauth") or get_app_config("MAIL_USEAUTH")
if username:
data["username"] = username
if password:
data["password"] = password
if TLS:
data["TLS"] = TLS
if SSL:
data["SSL"] = SSL
if auth:
data["auth"] = auth
try:
smtp = get_smtp(**data)
msg = EmailMessage()
msg.set_content(text)
msg["Subject"] = subject
msg["From"] = mailfrom_addr
msg["To"] = addr
# Check whether we are using an admin-defined SMTP server
custom_smtp = bool(get_config("mail_server"))
# We should only consider the MAILSENDER_ADDR value on servers defined in config
if custom_smtp:
smtp.send_message(msg)
else:
mailsender_addr = get_app_config("MAILSENDER_ADDR")
smtp.send_message(msg, from_addr=mailsender_addr)
smtp.quit()
return True, "Email sent"
except smtplib.SMTPException as e:
return False, str(e)
except timeout:
return False, "SMTP server connection timed out"
except Exception as e:
return False, str(e)
print(
"CTFd.utils.email.smtp.sendmail will raise an exception in a future minor release of CTFd and then be removed in CTFd v4.0"
)
return SMTPEmailProvider.sendmail(addr, text, subject)