From 81e24a5c166786dff1d0c9fa629b42d6129f38e0 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Mon, 13 Oct 2025 02:30:06 +0600 Subject: [PATCH 1/2] Added email sending via SMTP --- app/config.py | 4 ++++ app/routers/auth.py | 15 ++++++-------- app/routers/team.py | 48 ++++++++++++++++++++++----------------------- app/routers/user.py | 40 ++++++++++++++++--------------------- app/utils/email.py | 33 +++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 57 deletions(-) create mode 100644 app/utils/email.py diff --git a/app/config.py b/app/config.py index 8ac9911..523d6b4 100644 --- a/app/config.py +++ b/app/config.py @@ -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 = "" diff --git a/app/routers/auth.py b/app/routers/auth.py index 5b110c9..a5f55a8 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -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__) @@ -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, @@ -170,7 +167,7 @@ async def auth_login( "app_url": f"{settings.url_scheme}://{settings.app_hostname}", } ), - } + settings = settings, ) flash( request, diff --git a/app/routers/team.py b/app/routers/team.py index 5092b98..dc7b6fb 100644 --- a/app/routers/team.py +++ b/app/routers/team.py @@ -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 ( @@ -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, @@ -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, _( diff --git a/app/routers/user.py b/app/routers/user.py index 36680f8..aa0556a 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -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 ( @@ -30,6 +29,7 @@ UserRevokeOAuthAccessForm, ) from forms.team import TeamLeaveForm, TeamInviteAcceptForm +from utils.email import send_email logger = logging.getLogger(__name__) @@ -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, diff --git a/app/utils/email.py b/app/utils/email.py new file mode 100644 index 0000000..939c7e3 --- /dev/null +++ b/app/utils/email.py @@ -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) From 769c6c68b68c348687297ba1efd00fa2c066b324 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Mon, 13 Oct 2025 02:30:45 +0600 Subject: [PATCH 2/2] Updated env requirements for email settings --- README.md | 4 ++++ scripts/prod/check-env.sh | 22 ++++++++++++++++++++-- scripts/prod/install.sh | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b53b2d..52d2ba8 100644 --- a/README.md +++ b/README.md @@ -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). | `""` diff --git a/scripts/prod/check-env.sh b/scripts/prod/check-env.sh index 7094a5f..b96f861 100755 --- a/scripts/prod/check-env.sh +++ b/scripts/prod/check-env.sh @@ -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 @@ -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 diff --git a/scripts/prod/install.sh b/scripts/prod/install.sh index a2150d0..1c7140b 100755 --- a/scripts/prod/install.sh +++ b/scripts/prod/install.sh @@ -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}" \ No newline at end of file