Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ Variable | Comments | Default
`EMAIL_SENDER_NAME` | Name displayed as email sender for invites/login. | `""`
`EMAIL_SENDER_ADDRESS` | Email sender used for invites/login. | `""`
`RESEND_API_KEY` | API key for [Resend](https://resend.com). | `""`
`SMTP_HOST` | SMTP host. If SMTP data is provided, Resend is disabled automatically.| `""`
`SMTP_PORT` | SMTP port. | `587`
`SMTP_USERNAME` | SMTP username. | `""`
`SMTP_PASSWORD` | SMTP password. | `""`
`GITHUB_APP_ID` | GitHub App ID. | `""`
`GITHUB_APP_NAME` | GitHub App name. | `""`
`GITHUB_APP_PRIVATE_KEY` | GitHub App private key (PEM format). | `""`
Expand Down
4 changes: 4 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class Settings(BaseSettings):
google_client_id: str = ""
google_client_secret: str = ""
resend_api_key: str = ""
smtp_host: str = ""
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
email_logo: str = ""
email_sender_name: str = "/dev/push"
email_sender_address: str = ""
Expand Down
15 changes: 6 additions & 9 deletions app/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from forms.auth import EmailLoginForm
from utils.user import sanitize_username, get_user_by_email, get_user_by_provider
from utils.access import is_email_allowed, notify_denied
from utils.email import send_email

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,15 +151,11 @@ async def auth_login(
)
)

resend.api_key = settings.resend_api_key

try:
resend.Emails.send(
{
"from": f"{settings.email_sender_name} <{settings.email_sender_address}>",
"to": [email],
"subject": _("Sign in to %(app_name)s", app_name=settings.app_name),
"html": templates.get_template("email/login.html").render(
send_email(
email = email,
subject = _("Sign in to %(app_name)s", app_name=settings.app_name),
data = templates.get_template("email/login.html").render(
{
"request": request,
"email": email,
Expand All @@ -170,7 +167,7 @@ async def auth_login(
"app_url": f"{settings.url_scheme}://{settings.app_hostname}",
}
),
}
settings = settings,
)
flash(
request,
Expand Down
48 changes: 23 additions & 25 deletions app/routers/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any
from authlib.jose import jwt
from datetime import timedelta
import resend

from models import Project, Deployment, User, Team, TeamMember, utc_now, TeamInvite
from dependencies import (
Expand All @@ -26,6 +25,7 @@
from config import get_settings, Settings
from db import get_db
from utils.pagination import paginate
from utils.email import send_email
from utils.team import get_latest_teams
from forms.team import (
TeamDeleteForm,
Expand Down Expand Up @@ -513,33 +513,31 @@ def _send_member_invite(
)
)

resend.api_key = settings.resend_api_key

try:
resend.Emails.send(
{
"from": f"{settings.email_sender_name} <{settings.email_sender_address}>",
"to": [invite.email],
"subject": _(
'You have been invited to join the "%(team_name)s" team',
team_name=team.name,
),
"html": templates.get_template("email/team-invite.html").render(
{
"request": request,
"email": invite.email,
"invite_link": invite_link,
"inviter_name": current_user.name,
"team_name": team.name,
"email_logo": settings.email_logo
or request.url_for("assets", path="logo-email.png"),
"app_name": settings.app_name,
"app_description": settings.app_description,
"app_url": f"{settings.url_scheme}://{settings.app_hostname}",
}
),
}
send_email(
email = invite.email,
subject = _(
'You have been invited to join the "%(team_name)s" team',
team_name=team.name,
),
data = templates.get_template("email/team-invite.html").render(
{
"request": request,
"email": invite.email,
"invite_link": invite_link,
"inviter_name": current_user.name,
"team_name": team.name,
"email_logo": settings.email_logo
or request.url_for("assets", path="logo-email.png"),
"app_name": settings.app_name,
"app_description": settings.app_description,
"app_url": f"{settings.url_scheme}://{settings.app_hostname}",
}
),
settings = settings,
)

flash(
request,
_(
Expand Down
40 changes: 17 additions & 23 deletions app/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any
from authlib.jose import jwt
from datetime import timedelta
import resend

from config import Settings, get_settings
from dependencies import (
Expand All @@ -30,6 +29,7 @@
UserRevokeOAuthAccessForm,
)
from forms.team import TeamLeaveForm, TeamInviteAcceptForm
from utils.email import send_email

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -206,29 +206,23 @@ async def user_settings(
)
)

resend.api_key = settings.resend_api_key

try:
resend.Emails.send(
{
"from": f"{settings.email_sender_name} <{settings.email_sender_address}>",
"to": [new_email],
"subject": _("Verify your new email address"),
"html": templates.get_template(
"email/email-change.html"
).render(
{
"request": request,
"email": new_email,
"verify_link": verify_link,
"email_logo": f"{settings.email_logo}"
or request.url_for("assets", path="logo-email.png"),
"app_name": settings.app_name,
"app_description": settings.app_description,
"app_url": f"{settings.url_scheme}://{settings.app_hostname}",
}
),
}
send_email(
email = new_email,
subject = _("Verify your new email address"),
data = templates.get_template("email/email-change.html").render(
{
"request": request,
"email": new_email,
"verify_link": verify_link,
"email_logo": settings.email_logo
or request.url_for("assets", path="logo-email.png"),
"app_name": settings.app_name,
"app_description": settings.app_description,
"app_url": f"{settings.url_scheme}://{settings.app_hostname}",
}
),
settings = settings,
)
flash(
request,
Expand Down
33 changes: 33 additions & 0 deletions app/utils/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import resend
from config import Settings
from smtplib import SMTP
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

def send_email_by_resend(email: str, subject: str, data: str, settings: Settings):
resend.api_key = settings.resend_api_key
resend.Emails.send(
{
"from": f"{settings.email_sender_name} <{settings.email_sender_address}>",
"to": [email],
"subject": subject,
"html": data
}
)

def send_email_by_smtp(email: str, subject: str, data: str, settings: Settings):
msg = MIMEMultipart()
msg['From'] = f"{settings.email_sender_name} <{settings.email_sender_address}>"
msg['To'] = email
msg['Subject'] = subject
msg.attach(MIMEText(data, 'html'))
with SMTP(settings.smtp_host, settings.smtp_port) as server:
server.starttls()
server.login(settings.smtp_username, settings.smtp_password)
server.send_message(msg)

def send_email(email: str, subject: str, data: str, settings: Settings):
if all([settings.smtp_host, settings.smtp_port, settings.smtp_username, settings.smtp_password]):
send_email_by_smtp(email, subject, data, settings)
else:
send_email_by_resend(email, subject, data, settings)
22 changes: 20 additions & 2 deletions scripts/prod/check-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ done

[[ -f "$envf" ]] || { err "Not found: $envf"; exit 1; }

# Required keys
# Required keys (excluding email config which has alternatives)
req=(
LE_EMAIL APP_HOSTNAME DEPLOY_DOMAIN EMAIL_SENDER_ADDRESS RESEND_API_KEY
LE_EMAIL APP_HOSTNAME DEPLOY_DOMAIN EMAIL_SENDER_ADDRESS
GITHUB_APP_ID GITHUB_APP_NAME GITHUB_APP_PRIVATE_KEY GITHUB_APP_WEBHOOK_SECRET
GITHUB_APP_CLIENT_ID GITHUB_APP_CLIENT_SECRET
SECRET_KEY ENCRYPTION_KEY POSTGRES_PASSWORD SERVER_IP
Expand All @@ -44,6 +44,24 @@ for k in "${req[@]}"; do
[[ -n "$v" ]] || missing+=("$k")
done

# Check email configuration - either RESEND_API_KEY or all SMTP settings
resend_key="$(awk -F= -v k="RESEND_API_KEY" '$1==k{sub(/^[^=]*=/,""); print}' "$envf" | sed 's/^"\|"$//g')"
smtp_host="$(awk -F= -v k="SMTP_HOST" '$1==k{sub(/^[^=]*=/,""); print}' "$envf" | sed 's/^"\|"$//g')"
smtp_port="$(awk -F= -v k="SMTP_PORT" '$1==k{sub(/^[^=]*=/,""); print}' "$envf" | sed 's/^"\|"$//g')"
smtp_username="$(awk -F= -v k="SMTP_USERNAME" '$1==k{sub(/^[^=]*=/,""); print}' "$envf" | sed 's/^"\|"$//g')"
smtp_password="$(awk -F= -v k="SMTP_PASSWORD" '$1==k{sub(/^[^=]*=/,""); print}' "$envf" | sed 's/^"\|"$//g')"

if [[ -n "$resend_key" ]]; then
# RESEND_API_KEY is set, email config is valid
:
elif [[ -n "$smtp_host" && -n "$smtp_port" && -n "$smtp_username" && -n "$smtp_password" ]]; then
# All SMTP settings are set, email config is valid
:
else
# Neither RESEND_API_KEY nor complete SMTP config is present
missing+=("RESEND_API_KEY or (SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_PASSWORD)")
fi

if ((${#missing[@]})); then
err "Missing values in $envf: ${missing[*]}"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion scripts/prod/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,5 @@ echo ""
info "Next steps:"
echo "1. Switch to the app user: ${BLD}sudo -iu ${user}${NC}"
echo "2. Change dir and edit .env: ${BLD}cd devpush && vi .env${NC}"
echo " Set LE_EMAIL, APP_HOSTNAME, DEPLOY_DOMAIN, EMAIL_SENDER_ADDRESS, RESEND_API_KEY, GitHub App settings."
echo " Set LE_EMAIL, APP_HOSTNAME, DEPLOY_DOMAIN, EMAIL_SENDER_ADDRESS, SMTP Details or RESEND_API_KEY, GitHub App settings."
echo "3. Start the application: ${BLD}./scripts/prod/start.sh --migrate${NC}"