Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Added AGENTS.md with Cursor Cloud development environment instructions. [#1001](https://github.com/sourcebot-dev/sourcebot/pull/1001)
- Added support for configuring SMTP via individual environment variables (SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD) as an alternative to SMTP_CONNECTION_URL. [#1002](https://github.com/sourcebot-dev/sourcebot/pull/1002)

## [4.15.6] - 2026-03-13

Expand Down
6 changes: 5 additions & 1 deletion docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ The following environment variables allow you to configure your Sourcebot deploy
| `REDIS_REMOVE_ON_FAIL` | `100` | <p>Controls how many failed jobs are allowed to remain in Redis queues</p> |
| `REPO_SYNC_RETRY_BASE_SLEEP_SECONDS` | `60` | <p>The base sleep duration (in seconds) for exponential backoff when retrying repository sync operations that fail</p> |
| `GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS` | `600` | <p>The timeout duration (in seconds) for GitLab client queries</p> |
| `SMTP_CONNECTION_URL` | `-` | <p>The url to the SMTP service used for sending transactional emails. See [this doc](/docs/configuration/transactional-emails) for more info.</p> |
| `SMTP_CONNECTION_URL` | `-` | <p>The url to the SMTP service used for sending transactional emails. See [this doc](/docs/configuration/transactional-emails) for more info.</p><p>You can also use `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, and `SMTP_PASSWORD` to construct the SMTP connection url.</p> |
| `SMTP_HOST` | `-` | <p>The hostname of the SMTP server. Used to construct `SMTP_CONNECTION_URL` when individual SMTP variables are provided.</p> |
| `SMTP_PORT` | `-` | <p>The port of the SMTP server.</p> |
| `SMTP_USERNAME` | `-` | <p>The username for SMTP authentication.</p> |
| `SMTP_PASSWORD` | `-` | <p>The password for SMTP authentication.</p> |
| `SOURCEBOT_ENCRYPTION_KEY` | Automatically generated at startup if no value is provided. Generated using `openssl rand -base64 24` | <p>Used to encrypt connection secrets and generate API keys.</p> |
| `SOURCEBOT_PUBLIC_KEY_PATH` | `/app/public.pem` | <p>Sourcebot's public key that's used to verify encrypted license key signatures.</p> |
| `SOURCEBOT_LOG_LEVEL` | `info` | <p>The Sourcebot logging level. Valid values are `debug`, `info`, `warn`, `error`, in order of severity.</p> |
Expand Down
14 changes: 13 additions & 1 deletion docs/docs/configuration/transactional-emails.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,16 @@ To enable transactional emails in your deployment, set the following environment
| Variable | Description |
| :------- | :---------- |
| `SMTP_CONNECTION_URL` | SMTP server connection (`smtp://[user[:password]@]host[:port]`)|
| `EMAIL_FROM_ADDRESS` | The sender's email address |
| `EMAIL_FROM_ADDRESS` | The sender's email address |

You can also provide the SMTP connection details as individual environment variables instead of a full URL.

| Variable | Description |
| :------- | :---------- |
| `SMTP_HOST` | The hostname of the SMTP server |
| `SMTP_PORT` | The port of the SMTP server |
| `SMTP_USERNAME` | The username for SMTP authentication |
| `SMTP_PASSWORD` | The password for SMTP authentication |
| `EMAIL_FROM_ADDRESS` | The sender's email address |

If `SMTP_CONNECTION_URL` is set, it takes precedence over the individual variables.
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ if [ -n "$CONFIG_PATH" ]; then
fi
fi

# Descontruct the database URL from the individual variables if DATABASE_URL is not set
# Construct the database URL from the individual variables if DATABASE_URL is not set
if [ -z "$DATABASE_URL" ] && [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"

Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ const options = {
SOURCEBOT_PUBLIC_KEY_PATH: z.string(),

// Email
// Either SMTP_CONNECTION_URL or SMTP_HOST must be set to enable transactional emails.
// @see: shared/src/smtp.ts for more details.
SMTP_CONNECTION_URL: z.string().url().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),
SMTP_USERNAME: z.string().optional(),
SMTP_PASSWORD: z.string().optional(),
EMAIL_FROM_ADDRESS: z.string().email().optional(),

// Stripe
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export {
export {
getDBConnectionString,
} from "./db.js";
export {
getSMTPConnectionURL,
} from "./smtp.js";
export {
SOURCEBOT_VERSION,
} from "./version.js";
28 changes: 28 additions & 0 deletions packages/shared/src/smtp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { env } from "./env.server.js";
import { createLogger } from "./logger.js";

const logger = createLogger("smtp");

export const getSMTPConnectionURL = (): string | undefined => {
if (env.SMTP_CONNECTION_URL) {
return env.SMTP_CONNECTION_URL;
}

const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = env;
if (!SMTP_HOST && !SMTP_PORT && !SMTP_USERNAME && !SMTP_PASSWORD) {
return undefined;
}

const missing: string[] = [];
if (!SMTP_HOST) missing.push("SMTP_HOST");
if (!SMTP_PORT) missing.push("SMTP_PORT");
if (!SMTP_USERNAME) missing.push("SMTP_USERNAME");
if (!SMTP_PASSWORD) missing.push("SMTP_PASSWORD");

if (missing.length > 0) {
logger.error(`Missing required SMTP environment variables: ${missing.join(", ")}. All of SMTP_HOST, SMTP_PORT, SMTP_USERNAME, and SMTP_PASSWORD must be set when not using SMTP_CONNECTION_URL.`);
return undefined;
}

return `smtp://${SMTP_USERNAME}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}`;
}
17 changes: 10 additions & 7 deletions packages/web/src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use server';

import { getAuditService } from "@/ee/features/audit/factory";
import { env } from "@sourcebot/shared";
import { env, getSMTPConnectionURL } from "@sourcebot/shared";
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
import { ErrorCode } from "@/lib/errorCodes";
import { notAuthenticated, notFound, orgNotFound, ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError";
Expand Down Expand Up @@ -893,7 +893,8 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
});

// Send invites to recipients
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) {
const smtpConnectionUrl = getSMTPConnectionURL();
if (smtpConnectionUrl && env.EMAIL_FROM_ADDRESS) {
await Promise.all(emails.map(async (email) => {
const invite = await prisma.invite.findUnique({
where: {
Expand All @@ -917,7 +918,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{
},
});
const inviteLink = `${env.AUTH_URL}/redeem?invite_id=${invite.id}`;
const transport = createTransport(env.SMTP_CONNECTION_URL);
const transport = createTransport(smtpConnectionUrl);
const html = await render(InviteUserEmail({
host: {
name: user.name ?? undefined,
Expand Down Expand Up @@ -1272,7 +1273,8 @@ export const createAccountRequest = async (userId: string, domain: string) => se
},
});

if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) {
const smtpConnectionUrl = getSMTPConnectionURL();
if (smtpConnectionUrl && env.EMAIL_FROM_ADDRESS) {
// TODO: This is needed because we can't fetch the origin from the request headers when this is called
// on user creation (the header isn't set when next-auth calls onCreateUser for some reason)
const deploymentUrl = env.AUTH_URL;
Expand Down Expand Up @@ -1303,7 +1305,7 @@ export const createAccountRequest = async (userId: string, domain: string) => se
orgImageUrl: org.imageUrl ?? undefined,
}));

const transport = createTransport(env.SMTP_CONNECTION_URL);
const transport = createTransport(smtpConnectionUrl);
const result = await transport.sendMail({
to: owner.email!,
from: env.EMAIL_FROM_ADDRESS,
Expand Down Expand Up @@ -1414,7 +1416,8 @@ export const approveAccountRequest = async (requestId: string, domain: string) =
}

// Send approval email to the user
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) {
const smtpConnectionUrl = getSMTPConnectionURL();
if (smtpConnectionUrl && env.EMAIL_FROM_ADDRESS) {
const html = await render(JoinRequestApprovedEmail({
baseUrl: env.AUTH_URL,
user: {
Expand All @@ -1426,7 +1429,7 @@ export const approveAccountRequest = async (requestId: string, domain: string) =
orgDomain: org.domain
}));

const transport = createTransport(env.SMTP_CONNECTION_URL);
const transport = createTransport(smtpConnectionUrl);
const result = await transport.sendMail({
to: request.requestedBy.email!,
from: env.EMAIL_FROM_ADDRESS,
Expand Down
7 changes: 4 additions & 3 deletions packages/web/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import NextAuth, { DefaultSession, User as AuthJsUser } from "next-auth"
import Credentials from "next-auth/providers/credentials"
import EmailProvider from "next-auth/providers/nodemailer";
import { prisma } from "@/prisma";
import { env } from "@sourcebot/shared";
import { env, getSMTPConnectionURL } from "@sourcebot/shared";
import { User } from '@sourcebot/db';
import 'next-auth/jwt';
import type { Provider } from "next-auth/providers";
Expand Down Expand Up @@ -50,10 +50,11 @@ declare module 'next-auth/jwt' {
export const getProviders = () => {
const providers: IdentityProvider[] = [...eeIdentityProviders];

if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
const smtpConnectionUrl = getSMTPConnectionURL();
if (smtpConnectionUrl && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
providers.push({
provider: EmailProvider({
server: env.SMTP_CONNECTION_URL,
server: smtpConnectionUrl,
from: env.EMAIL_FROM_ADDRESS,
maxAge: 60 * 10,
generateVerificationToken: async () => {
Expand Down
Loading