-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
554 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import jinja2 | ||
|
||
from syftbox.server.users.user import User | ||
|
||
from ..users.user import User | ||
from .email_templates import create_token_email | ||
|
||
jinja_env = jinja2.Environment(loader=jinja2.PackageLoader("syftbox", "server/templates/email")) | ||
|
||
|
||
def create_token_email(user: User) -> str: | ||
template = jinja_env.get_template("token_email.html") | ||
return template.render(email=user.email, token=user.token) | ||
|
||
|
||
def send_token_email(user: User) -> None: | ||
email_body = create_token_email(user) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
from __future__ import annotations | ||
|
||
import copy | ||
import logging | ||
import os | ||
import re | ||
from urllib.parse import urlparse | ||
|
||
import requests | ||
from typing_extensions import Self | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def str_to_bool(bool_str: str | None) -> bool: | ||
result = False | ||
bool_str = str(bool_str).lower() | ||
if bool_str == "true" or bool_str == "1": | ||
result = True | ||
return result | ||
|
||
|
||
def verify_tls() -> bool: | ||
return not str_to_bool(str(os.environ.get("IGNORE_TLS_ERRORS", "0"))) | ||
|
||
|
||
class ServerURL: | ||
@classmethod | ||
def from_url(cls, url: str | ServerURL) -> ServerURL: | ||
if isinstance(url, ServerURL): | ||
return url | ||
try: | ||
# urlparse doesnt handle no protocol properly | ||
if "://" not in url: | ||
url = "http://" + url | ||
parts = urlparse(url) | ||
host_or_ip_parts = parts.netloc.split(":") | ||
# netloc is host:port | ||
port = 80 | ||
if len(host_or_ip_parts) > 1: | ||
port = int(host_or_ip_parts[1]) | ||
host_or_ip = host_or_ip_parts[0] | ||
if parts.scheme == "https": | ||
port = 443 | ||
return ServerURL( | ||
host_or_ip=host_or_ip, | ||
path=parts.path, | ||
port=port, | ||
protocol=parts.scheme, | ||
query=getattr(parts, "query", ""), | ||
) | ||
except Exception as e: | ||
logger.error(f"Failed to convert url: {url} to ServerURL. {e}") | ||
raise e | ||
|
||
def __init__( | ||
self, | ||
protocol: str = "http", | ||
host_or_ip: str = "localhost", | ||
port: int | None = 80, | ||
path: str = "", | ||
query: str = "", | ||
) -> None: | ||
# in case a preferred port is listed but its not clear if an alternative | ||
# port was included in the supplied host_or_ip:port combo passed in earlier | ||
match_port = re.search(":[0-9]{1,5}", host_or_ip) | ||
if match_port: | ||
sub_server_url: ServerURL = ServerURL.from_url(host_or_ip) | ||
host_or_ip = str(sub_server_url.host_or_ip) # type: ignore | ||
port = int(sub_server_url.port) # type: ignore | ||
protocol = str(sub_server_url.protocol) # type: ignore | ||
path = str(sub_server_url.path) # type: ignore | ||
|
||
prtcl_pattrn = "://" | ||
if prtcl_pattrn in host_or_ip: | ||
protocol = host_or_ip[: host_or_ip.find(prtcl_pattrn)] | ||
start_index = host_or_ip.find(prtcl_pattrn) + len(prtcl_pattrn) | ||
host_or_ip = host_or_ip[start_index:] | ||
|
||
self.host_or_ip = host_or_ip | ||
self.path: str = path | ||
self.port = port | ||
self.protocol = protocol | ||
self.query = query | ||
|
||
def with_path(self, path: str) -> Self: | ||
dupe = copy.copy(self) | ||
dupe.path = path | ||
return dupe | ||
|
||
def as_container_host(self, container_host: str | None = None) -> Self: | ||
if self.host_or_ip not in [ | ||
"localhost", | ||
"host.docker.internal", | ||
"host.k3d.internal", | ||
]: | ||
return self | ||
|
||
if container_host is None: | ||
# TODO: we could move config.py to syft and then the Settings singleton | ||
# could be importable in all parts of the code | ||
container_host = os.getenv("CONTAINER_HOST", None) | ||
|
||
if container_host: | ||
if container_host == "docker": | ||
hostname = "host.docker.internal" | ||
elif container_host == "podman": | ||
hostname = "host.containers.internal" | ||
else: | ||
hostname = "host.k3d.internal" | ||
else: | ||
# convert it back for non container clients | ||
hostname = "localhost" | ||
|
||
return self.__class__( | ||
protocol=self.protocol, | ||
host_or_ip=hostname, | ||
port=self.port, | ||
path=self.path, | ||
) | ||
|
||
@property | ||
def query_string(self) -> str: | ||
query_string = "" | ||
if len(self.query) > 0: | ||
query_string = f"?{self.query}" | ||
return query_string | ||
|
||
@property | ||
def url(self) -> str: | ||
return f"{self.base_url}{self.path}{self.query_string}" | ||
|
||
@property | ||
def url_no_port(self) -> str: | ||
return f"{self.base_url_no_port}{self.path}{self.query_string}" | ||
|
||
@property | ||
def base_url(self) -> str: | ||
return f"{self.protocol}://{self.host_or_ip}:{self.port}" | ||
|
||
@property | ||
def base_url_no_port(self) -> str: | ||
return f"{self.protocol}://{self.host_or_ip}" | ||
|
||
@property | ||
def url_no_protocol(self) -> str: | ||
return f"{self.host_or_ip}:{self.port}{self.path}" | ||
|
||
@property | ||
def url_path(self) -> str: | ||
return f"{self.path}{self.query_string}" | ||
|
||
def to_tls(self) -> ServerURL: | ||
if self.protocol == "https": | ||
return self | ||
|
||
# TODO: only ignore ssl in dev mode | ||
r = requests.get( # nosec | ||
self.base_url, verify=verify_tls() | ||
) # ignore ssl cert if its fake | ||
new_base_url = r.url | ||
if new_base_url.endswith("/"): | ||
new_base_url = new_base_url[0:-1] | ||
return self.__class__.from_url(url=f"{new_base_url}{self.path}{self.query_string}") | ||
|
||
def __repr__(self) -> str: | ||
return f"<{type(self).__name__} {self.url}>" | ||
|
||
def __str__(self) -> str: | ||
return self.url | ||
|
||
def __hash__(self) -> int: | ||
return hash(self.__str__()) | ||
|
||
def __copy__(self) -> ServerURL: | ||
return self.__class__.from_url(self.url) | ||
|
||
def set_port(self, port: int) -> Self: | ||
self.port = port | ||
return self |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# stdlib | ||
import logging | ||
import smtplib | ||
from email.mime.multipart import MIMEMultipart | ||
from email.mime.text import MIMEText | ||
|
||
from pydantic import BaseModel | ||
|
||
from .server_url import ServerURL | ||
|
||
SOCKET_TIMEOUT = 5 # seconds | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class SMTPClient(BaseModel): | ||
server: str | ||
port: int | ||
password: str | None = None | ||
username: str | None = None | ||
|
||
def create_email(self, sender: str, receiver: list[str], subject: str, body: str) -> MIMEMultipart: | ||
msg = MIMEMultipart("alternative") | ||
msg["From"] = sender | ||
msg["To"] = ", ".join(receiver) | ||
msg["Subject"] = subject | ||
msg.attach(MIMEText(body, "html")) | ||
return msg | ||
|
||
def send(self, sender: str, receiver: list[str], subject: str, body: str) -> None: | ||
if not (subject and body and receiver): | ||
raise ValueError("Subject, body, and recipient email(s) are required") | ||
|
||
msg = self.create_email(sender, receiver, subject, body) | ||
|
||
mail_url = ServerURL.from_url(f"smtp://{self.server}:{self.port}") | ||
mail_url = mail_url.as_container_host() | ||
try: | ||
with smtplib.SMTP(mail_url.host_or_ip, mail_url.port, timeout=SOCKET_TIMEOUT) as server: | ||
server.ehlo() | ||
if server.has_extn("STARTTLS"): | ||
server.starttls() | ||
server.ehlo() | ||
if self.username and self.password: | ||
server.login(self.username, self.password) | ||
text = msg.as_string() | ||
server.sendmail(sender, ", ".join(receiver), text) | ||
return None | ||
except Exception as e: | ||
logger.error(f"Unable to send email. {e}") | ||
raise e | ||
|
||
@classmethod | ||
def check_credentials(cls, server: str, port: int, username: str, password: str) -> bool: | ||
"""Check if the credentials are valid. | ||
Returns: | ||
bool: True if the credentials are valid, False otherwise. | ||
""" | ||
try: | ||
mail_url = ServerURL.from_url(f"smtp://{server}:{port}") | ||
mail_url = mail_url.as_container_host() | ||
|
||
print(f"> Validating SMTP settings: {mail_url}") | ||
with smtplib.SMTP(mail_url.host_or_ip, mail_url.port, timeout=SOCKET_TIMEOUT) as smtp_server: | ||
smtp_server.ehlo() | ||
if smtp_server.has_extn("STARTTLS"): | ||
smtp_server.starttls() | ||
smtp_server.ehlo() | ||
smtp_server.login(username, password) | ||
return True | ||
except Exception as e: | ||
message = f"SMTP check_credentials failed. {e}" | ||
logger.error(message) | ||
raise e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from pathlib import Path | ||
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
||
|
||
class SMTPSettings(BaseSettings): | ||
model_config = SettingsConfigDict(env_prefix="SMTP_") | ||
tls: bool = True | ||
port: int = 587 | ||
host: str | ||
username: str | ||
password: str | ||
sender: str | ||
|
||
|
||
class ServerSettings(BaseSettings): | ||
model_config = SettingsConfigDict(env_prefix="SYFTBOX_") | ||
data_folder: Path = Path("data") | ||
snapshot_folder: Path = Path("data/snapshot") | ||
user_file_path: Path = Path("data/users.json") | ||
smtp: "SMTPSettings" = SMTPSettings() | ||
|
||
@property | ||
def folders(self): | ||
return [self.data_folder, self.snapshot_folder] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<style> | ||
body { | ||
font-family: Arial, sans-serif; | ||
background-color: #f4f4f4; | ||
color: #333; | ||
margin: 0; | ||
padding: 0; | ||
} | ||
.container { | ||
max-width: 600px; | ||
margin: 50px auto; | ||
background-color: #ffffff; | ||
padding: 20px; | ||
border-radius: 8px; | ||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||
} | ||
h1 { | ||
color: #333; | ||
text-align: center; | ||
} | ||
p { | ||
font-size: 16px; | ||
line-height: 1.5; | ||
} | ||
code { | ||
display: block; | ||
background-color: #f0f0f0; | ||
color: #ff8c00; | ||
padding: 10px; | ||
font-size: 14px; | ||
margin: 20px auto; | ||
border-radius: 4px; | ||
width: 90%; | ||
word-wrap: break-word; | ||
} | ||
.footer { | ||
text-align: center; | ||
font-size: 12px; | ||
color: #aaa; | ||
margin-top: 20px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div class="container"> | ||
<h1>Welcome!</h1> | ||
<p> | ||
Use the following command in your CLI to complete your registration: | ||
</p> | ||
<code> syftbox register --email {{ email }} --token {{ token }} </code> | ||
<p>If you did not request this, please ignore this email.</p> | ||
</div> | ||
</body> | ||
</html> |
Oops, something went wrong.