We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add email sender address override
Right now the email sender is set to the From address which isn't right in all situations. We need a way to override the email sender if it's not supposed to be the same as the From address. 

https://help.mailgun.com/hc/en-us/articles/202236494-What-is-the-difference-between-the-From-and-Sender-
https://stackoverflow.com/questions/4367358/whats-the-difference-between-sender-from-and-return-path

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `CTFd/utils/email/smtp.py`
Content:
```
1 import smtplib
2 from email.message import EmailMessage
3 from email.utils import formataddr
4 from socket import timeout
5 
6 from CTFd.utils import get_app_config, get_config
7 
8 
9 def get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):
10     if SSL is None:
11         smtp = smtplib.SMTP(host, port, timeout=3)
12     else:
13         smtp = smtplib.SMTP_SSL(host, port, timeout=3)
14 
15     if TLS:
16         smtp.starttls()
17 
18     if auth:
19         smtp.login(username, password)
20     return smtp
21 
22 
23 def sendmail(addr, text, subject):
24     ctf_name = get_config("ctf_name")
25     mailfrom_addr = get_config("mailfrom_addr") or get_app_config("MAILFROM_ADDR")
26     mailfrom_addr = formataddr((ctf_name, mailfrom_addr))
27 
28     data = {
29         "host": get_config("mail_server") or get_app_config("MAIL_SERVER"),
30         "port": int(get_config("mail_port") or get_app_config("MAIL_PORT")),
31     }
32     username = get_config("mail_username") or get_app_config("MAIL_USERNAME")
33     password = get_config("mail_password") or get_app_config("MAIL_PASSWORD")
34     TLS = get_config("mail_tls") or get_app_config("MAIL_TLS")
35     SSL = get_config("mail_ssl") or get_app_config("MAIL_SSL")
36     auth = get_config("mail_useauth") or get_app_config("MAIL_USEAUTH")
37 
38     if username:
39         data["username"] = username
40     if password:
41         data["password"] = password
42     if TLS:
43         data["TLS"] = TLS
44     if SSL:
45         data["SSL"] = SSL
46     if auth:
47         data["auth"] = auth
48 
49     try:
50         smtp = get_smtp(**data)
51 
52         msg = EmailMessage()
53         msg.set_content(text)
54 
55         msg["Subject"] = subject
56         msg["From"] = mailfrom_addr
57         msg["To"] = addr
58 
59         smtp.send_message(msg)
60 
61         smtp.quit()
62         return True, "Email sent"
63     except smtplib.SMTPException as e:
64         return False, str(e)
65     except timeout:
66         return False, "SMTP server connection timed out"
67     except Exception as e:
68         return False, str(e)
69
```

Path: `CTFd/config.py`
Content:
```
1 import configparser
2 import os
3 from distutils.util import strtobool
4 
5 
6 class EnvInterpolation(configparser.BasicInterpolation):
7     """Interpolation which expands environment variables in values."""
8 
9     def before_get(self, parser, section, option, value, defaults):
10         value = super().before_get(parser, section, option, value, defaults)
11         envvar = os.getenv(option)
12         if value == "" and envvar:
13             return process_string_var(envvar)
14         else:
15             return value
16 
17 
18 def process_string_var(value):
19     if value == "":
20         return None
21 
22     if value.isdigit():
23         return int(value)
24     elif value.replace(".", "", 1).isdigit():
25         return float(value)
26 
27     try:
28         return bool(strtobool(value))
29     except ValueError:
30         return value
31 
32 
33 def process_boolean_str(value):
34     if type(value) is bool:
35         return value
36 
37     if value is None:
38         return False
39 
40     if value == "":
41         return None
42 
43     return bool(strtobool(value))
44 
45 
46 def empty_str_cast(value, default=None):
47     if value == "":
48         return default
49     return value
50 
51 
52 def gen_secret_key():
53     # Attempt to read the secret from the secret file
54     # This will fail if the secret has not been written
55     try:
56         with open(".ctfd_secret_key", "rb") as secret:
57             key = secret.read()
58     except (OSError, IOError):
59         key = None
60 
61     if not key:
62         key = os.urandom(64)
63         # Attempt to write the secret file
64         # This will fail if the filesystem is read-only
65         try:
66             with open(".ctfd_secret_key", "wb") as secret:
67                 secret.write(key)
68                 secret.flush()
69         except (OSError, IOError):
70             pass
71     return key
72 
73 
74 config_ini = configparser.ConfigParser(interpolation=EnvInterpolation())
75 config_ini.optionxform = str  # Makes the key value case-insensitive
76 path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini")
77 config_ini.read(path)
78 
79 
80 # fmt: off
81 class ServerConfig(object):
82     SECRET_KEY: str = empty_str_cast(config_ini["server"]["SECRET_KEY"]) \
83         or gen_secret_key()
84 
85     DATABASE_URL: str = empty_str_cast(config_ini["server"]["DATABASE_URL"]) \
86         or f"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db"
87 
88     REDIS_URL: str = empty_str_cast(config_ini["server"]["REDIS_URL"])
89 
90     SQLALCHEMY_DATABASE_URI = DATABASE_URL
91     CACHE_REDIS_URL = REDIS_URL
92     if CACHE_REDIS_URL:
93         CACHE_TYPE: str = "redis"
94     else:
95         CACHE_TYPE: str = "filesystem"
96         CACHE_DIR: str = os.path.join(
97             os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache"
98         )
99         # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.
100         CACHE_THRESHOLD: int = 0
101 
102     # === SECURITY ===
103     SESSION_COOKIE_HTTPONLY: bool = config_ini["security"].getboolean("SESSION_COOKIE_HTTPONLY", fallback=True)
104 
105     SESSION_COOKIE_SAMESITE: str = empty_str_cast(config_ini["security"]["SESSION_COOKIE_SAMESITE"]) \
106         or "Lax"
107 
108     PERMANENT_SESSION_LIFETIME: int = config_ini["security"].getint("PERMANENT_SESSION_LIFETIME") \
109         or 604800
110 
111     """
112     TRUSTED_PROXIES:
113     Defines a set of regular expressions used for finding a user's IP address if the CTFd instance
114     is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove
115     some proxies from the list.
116 
117     CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based
118     solely on IP addresses unless you know what you are doing.
119     """
120     TRUSTED_PROXIES = [
121         r"^127\.0\.0\.1$",
122         # Remove the following proxies if you do not trust the local network
123         # For example if you are running a CTF on your laptop and the teams are
124         # all on the same network
125         r"^::1$",
126         r"^fc00:",
127         r"^10\.",
128         r"^172\.(1[6-9]|2[0-9]|3[0-1])\.",
129         r"^192\.168\.",
130     ]
131 
132     # === EMAIL ===
133     MAILFROM_ADDR: str = config_ini["email"]["MAILFROM_ADDR"] \
134         or "noreply@ctfd.io"
135 
136     MAIL_SERVER: str = empty_str_cast(config_ini["email"]["MAIL_SERVER"])
137 
138     MAIL_PORT: int = empty_str_cast(config_ini["email"]["MAIL_PORT"])
139 
140     MAIL_USEAUTH: bool = process_boolean_str(config_ini["email"]["MAIL_USEAUTH"])
141 
142     MAIL_USERNAME: str = empty_str_cast(config_ini["email"]["MAIL_USERNAME"])
143 
144     MAIL_PASSWORD: str = empty_str_cast(config_ini["email"]["MAIL_PASSWORD"])
145 
146     MAIL_TLS: bool = process_boolean_str(config_ini["email"]["MAIL_TLS"])
147 
148     MAIL_SSL: bool = process_boolean_str(config_ini["email"]["MAIL_SSL"])
149 
150     MAILGUN_API_KEY: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
151 
152     MAILGUN_BASE_URL: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
153 
154     # === LOGS ===
155     LOG_FOLDER: str = empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \
156         or os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
157 
158     # === UPLOADS ===
159     UPLOAD_PROVIDER: str = empty_str_cast(config_ini["uploads"]["UPLOAD_PROVIDER"]) \
160         or "filesystem"
161 
162     UPLOAD_FOLDER: str = empty_str_cast(config_ini["uploads"]["UPLOAD_FOLDER"]) \
163         or os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads")
164 
165     if UPLOAD_PROVIDER == "s3":
166         AWS_ACCESS_KEY_ID: str = empty_str_cast(config_ini["uploads"]["AWS_ACCESS_KEY_ID"])
167 
168         AWS_SECRET_ACCESS_KEY: str = empty_str_cast(config_ini["uploads"]["AWS_SECRET_ACCESS_KEY"])
169 
170         AWS_S3_BUCKET: str = empty_str_cast(config_ini["uploads"]["AWS_S3_BUCKET"])
171 
172         AWS_S3_ENDPOINT_URL: str = empty_str_cast(config_ini["uploads"]["AWS_S3_ENDPOINT_URL"])
173 
174     # === OPTIONAL ===
175     REVERSE_PROXY: bool = empty_str_cast(config_ini["optional"]["REVERSE_PROXY"], default=False)
176 
177     TEMPLATES_AUTO_RELOAD: bool = empty_str_cast(config_ini["optional"]["TEMPLATES_AUTO_RELOAD"], default=True)
178 
179     SQLALCHEMY_TRACK_MODIFICATIONS: bool = empty_str_cast(config_ini["optional"]["SQLALCHEMY_TRACK_MODIFICATIONS"], default=False)
180 
181     SWAGGER_UI: bool = empty_str_cast(config_ini["optional"]["SWAGGER_UI"], default=False)
182 
183     SWAGGER_UI_ENDPOINT: str = "/" if SWAGGER_UI else None
184 
185     UPDATE_CHECK: bool = empty_str_cast(config_ini["optional"]["UPDATE_CHECK"], default=True)
186 
187     APPLICATION_ROOT: str = empty_str_cast(config_ini["optional"]["APPLICATION_ROOT"], default="/")
188 
189     SERVER_SENT_EVENTS: bool = empty_str_cast(config_ini["optional"]["SERVER_SENT_EVENTS"], default=True)
190 
191     HTML_SANITIZATION: bool = empty_str_cast(config_ini["optional"]["HTML_SANITIZATION"], default=False)
192 
193     if DATABASE_URL.startswith("sqlite") is False:
194         SQLALCHEMY_ENGINE_OPTIONS = {
195             "max_overflow": int(empty_str_cast(config_ini["optional"]["SQLALCHEMY_MAX_OVERFLOW"], default=20)),  # noqa: E131
196             "pool_pre_ping": empty_str_cast(config_ini["optional"]["SQLALCHEMY_POOL_PRE_PING"], default=True),  # noqa: E131
197         }
198 
199     # === OAUTH ===
200     OAUTH_CLIENT_ID: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_ID"])
201     OAUTH_CLIENT_SECRET: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_SECRET"])
202 # fmt: on
203 
204 
205 class TestingConfig(ServerConfig):
206     SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA"
207     PRESERVE_CONTEXT_ON_EXCEPTION = False
208     TESTING = True
209     DEBUG = True
210     SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://"
211     SERVER_NAME = "localhost"
212     UPDATE_CHECK = False
213     REDIS_URL = None
214     CACHE_TYPE = "simple"
215     CACHE_THRESHOLD = 500
216     SAFE_MODE = True
217 
218 
219 # Actually initialize ServerConfig to allow us to add more attributes on
220 Config = ServerConfig()
221 for k, v in config_ini.items("extra"):
222     setattr(Config, k, process_string_var(v))
223
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/CTFd/config.py b/CTFd/config.py
--- a/CTFd/config.py
+++ b/CTFd/config.py
@@ -147,6 +147,8 @@
 
     MAIL_SSL: bool = process_boolean_str(config_ini["email"]["MAIL_SSL"])
 
+    MAILSENDER_ADDR: str = empty_str_cast(config_ini["email"]["MAILSENDER_ADDR"])
+
     MAILGUN_API_KEY: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
 
     MAILGUN_BASE_URL: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"])
diff --git a/CTFd/utils/email/smtp.py b/CTFd/utils/email/smtp.py
--- a/CTFd/utils/email/smtp.py
+++ b/CTFd/utils/email/smtp.py
@@ -56,7 +56,15 @@
         msg["From"] = mailfrom_addr
         msg["To"] = addr
 
-        smtp.send_message(msg)
+        # 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"

{"golden_diff": "diff --git a/CTFd/config.py b/CTFd/config.py\n--- a/CTFd/config.py\n+++ b/CTFd/config.py\n@@ -147,6 +147,8 @@\n \n     MAIL_SSL: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_SSL\"])\n \n+    MAILSENDER_ADDR: str = empty_str_cast(config_ini[\"email\"][\"MAILSENDER_ADDR\"])\n+\n     MAILGUN_API_KEY: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\n \n     MAILGUN_BASE_URL: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\ndiff --git a/CTFd/utils/email/smtp.py b/CTFd/utils/email/smtp.py\n--- a/CTFd/utils/email/smtp.py\n+++ b/CTFd/utils/email/smtp.py\n@@ -56,7 +56,15 @@\n         msg[\"From\"] = mailfrom_addr\n         msg[\"To\"] = addr\n \n-        smtp.send_message(msg)\n+        # Check whether we are using an admin-defined SMTP server\n+        custom_smtp = bool(get_config(\"mail_server\"))\n+\n+        # We should only consider the MAILSENDER_ADDR value on servers defined in config\n+        if custom_smtp:\n+            smtp.send_message(msg)\n+        else:\n+            mailsender_addr = get_app_config(\"MAILSENDER_ADDR\")\n+            smtp.send_message(msg, from_addr=mailsender_addr)\n \n         smtp.quit()\n         return True, \"Email sent\"\n", "issue": "Add email sender address override\nRight now the email sender is set to the From address which isn't right in all situations. We need a way to override the email sender if it's not supposed to be the same as the From address. \r\n\r\nhttps://help.mailgun.com/hc/en-us/articles/202236494-What-is-the-difference-between-the-From-and-Sender-\r\nhttps://stackoverflow.com/questions/4367358/whats-the-difference-between-sender-from-and-return-path\n", "before_files": [{"content": "import smtplib\nfrom email.message import EmailMessage\nfrom email.utils import formataddr\nfrom socket import timeout\n\nfrom CTFd.utils import get_app_config, get_config\n\n\ndef get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):\n    if SSL is None:\n        smtp = smtplib.SMTP(host, port, timeout=3)\n    else:\n        smtp = smtplib.SMTP_SSL(host, port, timeout=3)\n\n    if TLS:\n        smtp.starttls()\n\n    if auth:\n        smtp.login(username, password)\n    return smtp\n\n\ndef sendmail(addr, text, subject):\n    ctf_name = get_config(\"ctf_name\")\n    mailfrom_addr = get_config(\"mailfrom_addr\") or get_app_config(\"MAILFROM_ADDR\")\n    mailfrom_addr = formataddr((ctf_name, mailfrom_addr))\n\n    data = {\n        \"host\": get_config(\"mail_server\") or get_app_config(\"MAIL_SERVER\"),\n        \"port\": int(get_config(\"mail_port\") or get_app_config(\"MAIL_PORT\")),\n    }\n    username = get_config(\"mail_username\") or get_app_config(\"MAIL_USERNAME\")\n    password = get_config(\"mail_password\") or get_app_config(\"MAIL_PASSWORD\")\n    TLS = get_config(\"mail_tls\") or get_app_config(\"MAIL_TLS\")\n    SSL = get_config(\"mail_ssl\") or get_app_config(\"MAIL_SSL\")\n    auth = get_config(\"mail_useauth\") or get_app_config(\"MAIL_USEAUTH\")\n\n    if username:\n        data[\"username\"] = username\n    if password:\n        data[\"password\"] = password\n    if TLS:\n        data[\"TLS\"] = TLS\n    if SSL:\n        data[\"SSL\"] = SSL\n    if auth:\n        data[\"auth\"] = auth\n\n    try:\n        smtp = get_smtp(**data)\n\n        msg = EmailMessage()\n        msg.set_content(text)\n\n        msg[\"Subject\"] = subject\n        msg[\"From\"] = mailfrom_addr\n        msg[\"To\"] = addr\n\n        smtp.send_message(msg)\n\n        smtp.quit()\n        return True, \"Email sent\"\n    except smtplib.SMTPException as e:\n        return False, str(e)\n    except timeout:\n        return False, \"SMTP server connection timed out\"\n    except Exception as e:\n        return False, str(e)\n", "path": "CTFd/utils/email/smtp.py"}, {"content": "import configparser\nimport os\nfrom distutils.util import strtobool\n\n\nclass EnvInterpolation(configparser.BasicInterpolation):\n    \"\"\"Interpolation which expands environment variables in values.\"\"\"\n\n    def before_get(self, parser, section, option, value, defaults):\n        value = super().before_get(parser, section, option, value, defaults)\n        envvar = os.getenv(option)\n        if value == \"\" and envvar:\n            return process_string_var(envvar)\n        else:\n            return value\n\n\ndef process_string_var(value):\n    if value == \"\":\n        return None\n\n    if value.isdigit():\n        return int(value)\n    elif value.replace(\".\", \"\", 1).isdigit():\n        return float(value)\n\n    try:\n        return bool(strtobool(value))\n    except ValueError:\n        return value\n\n\ndef process_boolean_str(value):\n    if type(value) is bool:\n        return value\n\n    if value is None:\n        return False\n\n    if value == \"\":\n        return None\n\n    return bool(strtobool(value))\n\n\ndef empty_str_cast(value, default=None):\n    if value == \"\":\n        return default\n    return value\n\n\ndef gen_secret_key():\n    # Attempt to read the secret from the secret file\n    # This will fail if the secret has not been written\n    try:\n        with open(\".ctfd_secret_key\", \"rb\") as secret:\n            key = secret.read()\n    except (OSError, IOError):\n        key = None\n\n    if not key:\n        key = os.urandom(64)\n        # Attempt to write the secret file\n        # This will fail if the filesystem is read-only\n        try:\n            with open(\".ctfd_secret_key\", \"wb\") as secret:\n                secret.write(key)\n                secret.flush()\n        except (OSError, IOError):\n            pass\n    return key\n\n\nconfig_ini = configparser.ConfigParser(interpolation=EnvInterpolation())\nconfig_ini.optionxform = str  # Makes the key value case-insensitive\npath = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"config.ini\")\nconfig_ini.read(path)\n\n\n# fmt: off\nclass ServerConfig(object):\n    SECRET_KEY: str = empty_str_cast(config_ini[\"server\"][\"SECRET_KEY\"]) \\\n        or gen_secret_key()\n\n    DATABASE_URL: str = empty_str_cast(config_ini[\"server\"][\"DATABASE_URL\"]) \\\n        or f\"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db\"\n\n    REDIS_URL: str = empty_str_cast(config_ini[\"server\"][\"REDIS_URL\"])\n\n    SQLALCHEMY_DATABASE_URI = DATABASE_URL\n    CACHE_REDIS_URL = REDIS_URL\n    if CACHE_REDIS_URL:\n        CACHE_TYPE: str = \"redis\"\n    else:\n        CACHE_TYPE: str = \"filesystem\"\n        CACHE_DIR: str = os.path.join(\n            os.path.dirname(__file__), os.pardir, \".data\", \"filesystem_cache\"\n        )\n        # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.\n        CACHE_THRESHOLD: int = 0\n\n    # === SECURITY ===\n    SESSION_COOKIE_HTTPONLY: bool = config_ini[\"security\"].getboolean(\"SESSION_COOKIE_HTTPONLY\", fallback=True)\n\n    SESSION_COOKIE_SAMESITE: str = empty_str_cast(config_ini[\"security\"][\"SESSION_COOKIE_SAMESITE\"]) \\\n        or \"Lax\"\n\n    PERMANENT_SESSION_LIFETIME: int = config_ini[\"security\"].getint(\"PERMANENT_SESSION_LIFETIME\") \\\n        or 604800\n\n    \"\"\"\n    TRUSTED_PROXIES:\n    Defines a set of regular expressions used for finding a user's IP address if the CTFd instance\n    is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove\n    some proxies from the list.\n\n    CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based\n    solely on IP addresses unless you know what you are doing.\n    \"\"\"\n    TRUSTED_PROXIES = [\n        r\"^127\\.0\\.0\\.1$\",\n        # Remove the following proxies if you do not trust the local network\n        # For example if you are running a CTF on your laptop and the teams are\n        # all on the same network\n        r\"^::1$\",\n        r\"^fc00:\",\n        r\"^10\\.\",\n        r\"^172\\.(1[6-9]|2[0-9]|3[0-1])\\.\",\n        r\"^192\\.168\\.\",\n    ]\n\n    # === EMAIL ===\n    MAILFROM_ADDR: str = config_ini[\"email\"][\"MAILFROM_ADDR\"] \\\n        or \"noreply@ctfd.io\"\n\n    MAIL_SERVER: str = empty_str_cast(config_ini[\"email\"][\"MAIL_SERVER\"])\n\n    MAIL_PORT: int = empty_str_cast(config_ini[\"email\"][\"MAIL_PORT\"])\n\n    MAIL_USEAUTH: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_USEAUTH\"])\n\n    MAIL_USERNAME: str = empty_str_cast(config_ini[\"email\"][\"MAIL_USERNAME\"])\n\n    MAIL_PASSWORD: str = empty_str_cast(config_ini[\"email\"][\"MAIL_PASSWORD\"])\n\n    MAIL_TLS: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_TLS\"])\n\n    MAIL_SSL: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_SSL\"])\n\n    MAILGUN_API_KEY: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\n\n    MAILGUN_BASE_URL: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\n\n    # === LOGS ===\n    LOG_FOLDER: str = empty_str_cast(config_ini[\"logs\"][\"LOG_FOLDER\"]) \\\n        or os.path.join(os.path.dirname(os.path.abspath(__file__)), \"logs\")\n\n    # === UPLOADS ===\n    UPLOAD_PROVIDER: str = empty_str_cast(config_ini[\"uploads\"][\"UPLOAD_PROVIDER\"]) \\\n        or \"filesystem\"\n\n    UPLOAD_FOLDER: str = empty_str_cast(config_ini[\"uploads\"][\"UPLOAD_FOLDER\"]) \\\n        or os.path.join(os.path.dirname(os.path.abspath(__file__)), \"uploads\")\n\n    if UPLOAD_PROVIDER == \"s3\":\n        AWS_ACCESS_KEY_ID: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_ACCESS_KEY_ID\"])\n\n        AWS_SECRET_ACCESS_KEY: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_SECRET_ACCESS_KEY\"])\n\n        AWS_S3_BUCKET: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_S3_BUCKET\"])\n\n        AWS_S3_ENDPOINT_URL: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_S3_ENDPOINT_URL\"])\n\n    # === OPTIONAL ===\n    REVERSE_PROXY: bool = empty_str_cast(config_ini[\"optional\"][\"REVERSE_PROXY\"], default=False)\n\n    TEMPLATES_AUTO_RELOAD: bool = empty_str_cast(config_ini[\"optional\"][\"TEMPLATES_AUTO_RELOAD\"], default=True)\n\n    SQLALCHEMY_TRACK_MODIFICATIONS: bool = empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_TRACK_MODIFICATIONS\"], default=False)\n\n    SWAGGER_UI: bool = empty_str_cast(config_ini[\"optional\"][\"SWAGGER_UI\"], default=False)\n\n    SWAGGER_UI_ENDPOINT: str = \"/\" if SWAGGER_UI else None\n\n    UPDATE_CHECK: bool = empty_str_cast(config_ini[\"optional\"][\"UPDATE_CHECK\"], default=True)\n\n    APPLICATION_ROOT: str = empty_str_cast(config_ini[\"optional\"][\"APPLICATION_ROOT\"], default=\"/\")\n\n    SERVER_SENT_EVENTS: bool = empty_str_cast(config_ini[\"optional\"][\"SERVER_SENT_EVENTS\"], default=True)\n\n    HTML_SANITIZATION: bool = empty_str_cast(config_ini[\"optional\"][\"HTML_SANITIZATION\"], default=False)\n\n    if DATABASE_URL.startswith(\"sqlite\") is False:\n        SQLALCHEMY_ENGINE_OPTIONS = {\n            \"max_overflow\": int(empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_MAX_OVERFLOW\"], default=20)),  # noqa: E131\n            \"pool_pre_ping\": empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_POOL_PRE_PING\"], default=True),  # noqa: E131\n        }\n\n    # === OAUTH ===\n    OAUTH_CLIENT_ID: str = empty_str_cast(config_ini[\"oauth\"][\"OAUTH_CLIENT_ID\"])\n    OAUTH_CLIENT_SECRET: str = empty_str_cast(config_ini[\"oauth\"][\"OAUTH_CLIENT_SECRET\"])\n# fmt: on\n\n\nclass TestingConfig(ServerConfig):\n    SECRET_KEY = \"AAAAAAAAAAAAAAAAAAAA\"\n    PRESERVE_CONTEXT_ON_EXCEPTION = False\n    TESTING = True\n    DEBUG = True\n    SQLALCHEMY_DATABASE_URI = os.getenv(\"TESTING_DATABASE_URL\") or \"sqlite://\"\n    SERVER_NAME = \"localhost\"\n    UPDATE_CHECK = False\n    REDIS_URL = None\n    CACHE_TYPE = \"simple\"\n    CACHE_THRESHOLD = 500\n    SAFE_MODE = True\n\n\n# Actually initialize ServerConfig to allow us to add more attributes on\nConfig = ServerConfig()\nfor k, v in config_ini.items(\"extra\"):\n    setattr(Config, k, process_string_var(v))\n", "path": "CTFd/config.py"}], "after_files": [{"content": "import smtplib\nfrom email.message import EmailMessage\nfrom email.utils import formataddr\nfrom socket import timeout\n\nfrom CTFd.utils import get_app_config, get_config\n\n\ndef get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):\n    if SSL is None:\n        smtp = smtplib.SMTP(host, port, timeout=3)\n    else:\n        smtp = smtplib.SMTP_SSL(host, port, timeout=3)\n\n    if TLS:\n        smtp.starttls()\n\n    if auth:\n        smtp.login(username, password)\n    return smtp\n\n\ndef sendmail(addr, text, subject):\n    ctf_name = get_config(\"ctf_name\")\n    mailfrom_addr = get_config(\"mailfrom_addr\") or get_app_config(\"MAILFROM_ADDR\")\n    mailfrom_addr = formataddr((ctf_name, mailfrom_addr))\n\n    data = {\n        \"host\": get_config(\"mail_server\") or get_app_config(\"MAIL_SERVER\"),\n        \"port\": int(get_config(\"mail_port\") or get_app_config(\"MAIL_PORT\")),\n    }\n    username = get_config(\"mail_username\") or get_app_config(\"MAIL_USERNAME\")\n    password = get_config(\"mail_password\") or get_app_config(\"MAIL_PASSWORD\")\n    TLS = get_config(\"mail_tls\") or get_app_config(\"MAIL_TLS\")\n    SSL = get_config(\"mail_ssl\") or get_app_config(\"MAIL_SSL\")\n    auth = get_config(\"mail_useauth\") or get_app_config(\"MAIL_USEAUTH\")\n\n    if username:\n        data[\"username\"] = username\n    if password:\n        data[\"password\"] = password\n    if TLS:\n        data[\"TLS\"] = TLS\n    if SSL:\n        data[\"SSL\"] = SSL\n    if auth:\n        data[\"auth\"] = auth\n\n    try:\n        smtp = get_smtp(**data)\n\n        msg = EmailMessage()\n        msg.set_content(text)\n\n        msg[\"Subject\"] = subject\n        msg[\"From\"] = mailfrom_addr\n        msg[\"To\"] = addr\n\n        # Check whether we are using an admin-defined SMTP server\n        custom_smtp = bool(get_config(\"mail_server\"))\n\n        # We should only consider the MAILSENDER_ADDR value on servers defined in config\n        if custom_smtp:\n            smtp.send_message(msg)\n        else:\n            mailsender_addr = get_app_config(\"MAILSENDER_ADDR\")\n            smtp.send_message(msg, from_addr=mailsender_addr)\n\n        smtp.quit()\n        return True, \"Email sent\"\n    except smtplib.SMTPException as e:\n        return False, str(e)\n    except timeout:\n        return False, \"SMTP server connection timed out\"\n    except Exception as e:\n        return False, str(e)\n", "path": "CTFd/utils/email/smtp.py"}, {"content": "import configparser\nimport os\nfrom distutils.util import strtobool\n\n\nclass EnvInterpolation(configparser.BasicInterpolation):\n    \"\"\"Interpolation which expands environment variables in values.\"\"\"\n\n    def before_get(self, parser, section, option, value, defaults):\n        value = super().before_get(parser, section, option, value, defaults)\n        envvar = os.getenv(option)\n        if value == \"\" and envvar:\n            return process_string_var(envvar)\n        else:\n            return value\n\n\ndef process_string_var(value):\n    if value == \"\":\n        return None\n\n    if value.isdigit():\n        return int(value)\n    elif value.replace(\".\", \"\", 1).isdigit():\n        return float(value)\n\n    try:\n        return bool(strtobool(value))\n    except ValueError:\n        return value\n\n\ndef process_boolean_str(value):\n    if type(value) is bool:\n        return value\n\n    if value is None:\n        return False\n\n    if value == \"\":\n        return None\n\n    return bool(strtobool(value))\n\n\ndef empty_str_cast(value, default=None):\n    if value == \"\":\n        return default\n    return value\n\n\ndef gen_secret_key():\n    # Attempt to read the secret from the secret file\n    # This will fail if the secret has not been written\n    try:\n        with open(\".ctfd_secret_key\", \"rb\") as secret:\n            key = secret.read()\n    except (OSError, IOError):\n        key = None\n\n    if not key:\n        key = os.urandom(64)\n        # Attempt to write the secret file\n        # This will fail if the filesystem is read-only\n        try:\n            with open(\".ctfd_secret_key\", \"wb\") as secret:\n                secret.write(key)\n                secret.flush()\n        except (OSError, IOError):\n            pass\n    return key\n\n\nconfig_ini = configparser.ConfigParser(interpolation=EnvInterpolation())\nconfig_ini.optionxform = str  # Makes the key value case-insensitive\npath = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"config.ini\")\nconfig_ini.read(path)\n\n\n# fmt: off\nclass ServerConfig(object):\n    SECRET_KEY: str = empty_str_cast(config_ini[\"server\"][\"SECRET_KEY\"]) \\\n        or gen_secret_key()\n\n    DATABASE_URL: str = empty_str_cast(config_ini[\"server\"][\"DATABASE_URL\"]) \\\n        or f\"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db\"\n\n    REDIS_URL: str = empty_str_cast(config_ini[\"server\"][\"REDIS_URL\"])\n\n    SQLALCHEMY_DATABASE_URI = DATABASE_URL\n    CACHE_REDIS_URL = REDIS_URL\n    if CACHE_REDIS_URL:\n        CACHE_TYPE: str = \"redis\"\n    else:\n        CACHE_TYPE: str = \"filesystem\"\n        CACHE_DIR: str = os.path.join(\n            os.path.dirname(__file__), os.pardir, \".data\", \"filesystem_cache\"\n        )\n        # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.\n        CACHE_THRESHOLD: int = 0\n\n    # === SECURITY ===\n    SESSION_COOKIE_HTTPONLY: bool = config_ini[\"security\"].getboolean(\"SESSION_COOKIE_HTTPONLY\", fallback=True)\n\n    SESSION_COOKIE_SAMESITE: str = empty_str_cast(config_ini[\"security\"][\"SESSION_COOKIE_SAMESITE\"]) \\\n        or \"Lax\"\n\n    PERMANENT_SESSION_LIFETIME: int = config_ini[\"security\"].getint(\"PERMANENT_SESSION_LIFETIME\") \\\n        or 604800\n\n    \"\"\"\n    TRUSTED_PROXIES:\n    Defines a set of regular expressions used for finding a user's IP address if the CTFd instance\n    is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove\n    some proxies from the list.\n\n    CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based\n    solely on IP addresses unless you know what you are doing.\n    \"\"\"\n    TRUSTED_PROXIES = [\n        r\"^127\\.0\\.0\\.1$\",\n        # Remove the following proxies if you do not trust the local network\n        # For example if you are running a CTF on your laptop and the teams are\n        # all on the same network\n        r\"^::1$\",\n        r\"^fc00:\",\n        r\"^10\\.\",\n        r\"^172\\.(1[6-9]|2[0-9]|3[0-1])\\.\",\n        r\"^192\\.168\\.\",\n    ]\n\n    # === EMAIL ===\n    MAILFROM_ADDR: str = config_ini[\"email\"][\"MAILFROM_ADDR\"] \\\n        or \"noreply@ctfd.io\"\n\n    MAIL_SERVER: str = empty_str_cast(config_ini[\"email\"][\"MAIL_SERVER\"])\n\n    MAIL_PORT: int = empty_str_cast(config_ini[\"email\"][\"MAIL_PORT\"])\n\n    MAIL_USEAUTH: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_USEAUTH\"])\n\n    MAIL_USERNAME: str = empty_str_cast(config_ini[\"email\"][\"MAIL_USERNAME\"])\n\n    MAIL_PASSWORD: str = empty_str_cast(config_ini[\"email\"][\"MAIL_PASSWORD\"])\n\n    MAIL_TLS: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_TLS\"])\n\n    MAIL_SSL: bool = process_boolean_str(config_ini[\"email\"][\"MAIL_SSL\"])\n\n    MAILSENDER_ADDR: str = empty_str_cast(config_ini[\"email\"][\"MAILSENDER_ADDR\"])\n\n    MAILGUN_API_KEY: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\n\n    MAILGUN_BASE_URL: str = empty_str_cast(config_ini[\"email\"][\"MAILGUN_API_KEY\"])\n\n    # === LOGS ===\n    LOG_FOLDER: str = empty_str_cast(config_ini[\"logs\"][\"LOG_FOLDER\"]) \\\n        or os.path.join(os.path.dirname(os.path.abspath(__file__)), \"logs\")\n\n    # === UPLOADS ===\n    UPLOAD_PROVIDER: str = empty_str_cast(config_ini[\"uploads\"][\"UPLOAD_PROVIDER\"]) \\\n        or \"filesystem\"\n\n    UPLOAD_FOLDER: str = empty_str_cast(config_ini[\"uploads\"][\"UPLOAD_FOLDER\"]) \\\n        or os.path.join(os.path.dirname(os.path.abspath(__file__)), \"uploads\")\n\n    if UPLOAD_PROVIDER == \"s3\":\n        AWS_ACCESS_KEY_ID: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_ACCESS_KEY_ID\"])\n\n        AWS_SECRET_ACCESS_KEY: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_SECRET_ACCESS_KEY\"])\n\n        AWS_S3_BUCKET: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_S3_BUCKET\"])\n\n        AWS_S3_ENDPOINT_URL: str = empty_str_cast(config_ini[\"uploads\"][\"AWS_S3_ENDPOINT_URL\"])\n\n    # === OPTIONAL ===\n    REVERSE_PROXY: bool = empty_str_cast(config_ini[\"optional\"][\"REVERSE_PROXY\"], default=False)\n\n    TEMPLATES_AUTO_RELOAD: bool = empty_str_cast(config_ini[\"optional\"][\"TEMPLATES_AUTO_RELOAD\"], default=True)\n\n    SQLALCHEMY_TRACK_MODIFICATIONS: bool = empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_TRACK_MODIFICATIONS\"], default=False)\n\n    SWAGGER_UI: bool = empty_str_cast(config_ini[\"optional\"][\"SWAGGER_UI\"], default=False)\n\n    SWAGGER_UI_ENDPOINT: str = \"/\" if SWAGGER_UI else None\n\n    UPDATE_CHECK: bool = empty_str_cast(config_ini[\"optional\"][\"UPDATE_CHECK\"], default=True)\n\n    APPLICATION_ROOT: str = empty_str_cast(config_ini[\"optional\"][\"APPLICATION_ROOT\"], default=\"/\")\n\n    SERVER_SENT_EVENTS: bool = empty_str_cast(config_ini[\"optional\"][\"SERVER_SENT_EVENTS\"], default=True)\n\n    HTML_SANITIZATION: bool = empty_str_cast(config_ini[\"optional\"][\"HTML_SANITIZATION\"], default=False)\n\n    if DATABASE_URL.startswith(\"sqlite\") is False:\n        SQLALCHEMY_ENGINE_OPTIONS = {\n            \"max_overflow\": int(empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_MAX_OVERFLOW\"], default=20)),  # noqa: E131\n            \"pool_pre_ping\": empty_str_cast(config_ini[\"optional\"][\"SQLALCHEMY_POOL_PRE_PING\"], default=True),  # noqa: E131\n        }\n\n    # === OAUTH ===\n    OAUTH_CLIENT_ID: str = empty_str_cast(config_ini[\"oauth\"][\"OAUTH_CLIENT_ID\"])\n    OAUTH_CLIENT_SECRET: str = empty_str_cast(config_ini[\"oauth\"][\"OAUTH_CLIENT_SECRET\"])\n# fmt: on\n\n\nclass TestingConfig(ServerConfig):\n    SECRET_KEY = \"AAAAAAAAAAAAAAAAAAAA\"\n    PRESERVE_CONTEXT_ON_EXCEPTION = False\n    TESTING = True\n    DEBUG = True\n    SQLALCHEMY_DATABASE_URI = os.getenv(\"TESTING_DATABASE_URL\") or \"sqlite://\"\n    SERVER_NAME = \"localhost\"\n    UPDATE_CHECK = False\n    REDIS_URL = None\n    CACHE_TYPE = \"simple\"\n    CACHE_THRESHOLD = 500\n    SAFE_MODE = True\n\n\n# Actually initialize ServerConfig to allow us to add more attributes on\nConfig = ServerConfig()\nfor k, v in config_ini.items(\"extra\"):\n    setattr(Config, k, process_string_var(v))\n", "path": "CTFd/config.py"}]}