Add SMTP_SERVICE environment variable for well-known services (#8781)

* Add SMTP_SERVICE environment variable for well-known services

* Fix PR #8777: Restore code in teams.ts and users.ts

* The rest of the work

* fix validation

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
codegen-sh[bot]
2025-03-26 05:48:47 -07:00
committed by GitHub
parent 0dd6ef5196
commit aac95c2b2e
9 changed files with 66 additions and 19 deletions
+1 -5
View File
@@ -205,14 +205,10 @@ SENTRY_TUNNEL=
# To support sending outgoing transactional emails such as "document updated" or
# "you've been invited" you'll need to provide authentication for an SMTP server
SMTP_HOST=
SMTP_PORT=
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_REPLY_EMAIL=
SMTP_TLS_CIPHERS=
SMTP_SECURE=true
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
+4
View File
@@ -171,6 +171,10 @@
"description": "smtp.example.com (optional)",
"required": false
},
"SMTP_SERVICE": {
"description": "Well-known SMTP service name for nodemailer (optional, e.g. 'gmail', 'SES')",
"required": false
},
"SMTP_PORT": {
"description": "1234 (optional)",
"required": false
+1 -1
View File
@@ -3,7 +3,7 @@ import { Hook, PluginManager } from "@server/utils/PluginManager";
import config from "../plugin.json";
import router from "./auth/email";
const enabled = !!env.SMTP_HOST || env.isDevelopment;
const enabled = !!(env.SMTP_HOST || env.SMTP_SERVICE) || env.isDevelopment;
if (enabled) {
PluginManager.add({
+12 -1
View File
@@ -33,7 +33,7 @@ export class Mailer {
transporter: Transporter | undefined;
constructor() {
if (env.SMTP_HOST) {
if (env.SMTP_HOST || env.SMTP_SERVICE) {
this.transporter = nodemailer.createTransport(this.getOptions());
}
if (useTestEmailService) {
@@ -198,6 +198,17 @@ export class Mailer {
};
private getOptions(): SMTPTransport.Options {
// nodemailer will use the service config to determine host/port
if (env.SMTP_SERVICE) {
return {
service: env.SMTP_SERVICE,
auth: {
user: env.SMTP_USERNAME,
pass: env.SMTP_PASSWORD,
},
};
}
return {
name: env.SMTP_NAME,
host: env.SMTP_HOST,
+13 -3
View File
@@ -15,7 +15,7 @@ import {
} from "class-validator";
import uniq from "lodash/uniq";
import { languages } from "@shared/i18n";
import { CannotUseWithout } from "@server/utils/validators";
import { CannotUseWith, CannotUseWithout } from "@server/utils/validators";
import Deprecated from "./models/decorators/Deprecated";
import { getArg } from "./utils/args";
import { Public, PublicEnvironmentRegister } from "./utils/decorators/Public";
@@ -291,10 +291,19 @@ export class Environment {
/**
* The host of your SMTP server for enabling emails.
*/
public SMTP_HOST = environment.SMTP_HOST;
@CannotUseWith("SMTP_SERVICE")
public SMTP_HOST = this.toOptionalString(environment.SMTP_HOST);
/**
* The service name of a well-known SMTP service for nodemailer.
* See https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/
*/
@CannotUseWith("SMTP_HOST")
public SMTP_SERVICE = this.toOptionalString(environment.SMTP_SERVICE);
@Public
public EMAIL_ENABLED = !!this.SMTP_HOST || this.isDevelopment;
public EMAIL_ENABLED =
!!(this.SMTP_HOST || this.SMTP_SERVICE) || this.isDevelopment;
/**
* Optional hostname of the client, used for identifying to the server
@@ -307,6 +316,7 @@ export class Environment {
*/
@IsNumber()
@IsOptional()
@CannotUseWith("SMTP_SERVICE")
public SMTP_PORT = this.toOptionalNumber(environment.SMTP_PORT);
/**
+1 -1
View File
@@ -190,7 +190,7 @@ class Team extends ParanoidModel<
* @return {boolean} Whether to show email login options
*/
get emailSigninEnabled(): boolean {
return this.guestSignin && (!!env.SMTP_HOST || env.isDevelopment);
return this.guestSignin && env.EMAIL_ENABLED;
}
get url() {
+2 -3
View File
@@ -19,7 +19,6 @@ import { safeEqual } from "@server/utils/crypto";
import * as T from "./schema";
const router = new Router();
const emailEnabled = !!(env.SMTP_HOST || env.isDevelopment);
const handleTeamUpdate = async (ctx: APIContext<T.TeamsUpdateSchemaReq>) => {
const { transaction } = ctx.state;
@@ -68,7 +67,7 @@ router.post(
rateLimiter(RateLimiterStrategy.FivePerHour),
auth(),
async (ctx: APIContext) => {
if (!emailEnabled) {
if (!env.EMAIL_ENABLED) {
throw ValidationError("Email support is not setup for this instance");
}
@@ -101,7 +100,7 @@ router.post(
authorize(user, "delete", team);
if (emailEnabled) {
if (env.EMAIL_ENABLED) {
const deleteConfirmationCode = team.getDeleteConfirmationCode(user);
if (!safeEqual(code, deleteConfirmationCode)) {
+4 -5
View File
@@ -30,7 +30,6 @@ import pagination from "../middlewares/pagination";
import * as T from "./schema";
const router = new Router();
const emailEnabled = !!(env.SMTP_HOST || env.isDevelopment);
router.post(
"users.list",
@@ -210,7 +209,7 @@ router.post(
auth(),
validate(T.UsersUpdateEmailSchema),
async (ctx: APIContext<T.UsersUpdateEmailReq>) => {
if (!emailEnabled) {
if (!env.EMAIL_ENABLED) {
throw ValidationError("Email support is not setup for this instance");
}
@@ -252,7 +251,7 @@ router.get(
transaction(),
validate(T.UsersUpdateEmailConfirmSchema),
async (ctx: APIContext<T.UsersUpdateEmailConfirmReq>) => {
if (!emailEnabled) {
if (!env.EMAIL_ENABLED) {
throw ValidationError("Email support is not setup for this instance");
}
@@ -626,7 +625,7 @@ router.post(
rateLimiter(RateLimiterStrategy.FivePerHour),
auth(),
async (ctx: APIContext) => {
if (!emailEnabled) {
if (!env.EMAIL_ENABLED) {
throw ValidationError("Email support is not setup for this instance");
}
@@ -671,7 +670,7 @@ router.post(
// If we're attempting to delete our own account then a confirmation code
// is required. This acts as CSRF protection.
if ((!id || id === actor.id) && emailEnabled) {
if ((!id || id === actor.id) && env.EMAIL_ENABLED) {
const deleteConfirmationCode = user.deleteConfirmationCode;
if (!safeEqual(code, deleteConfirmationCode)) {
+28
View File
@@ -29,3 +29,31 @@ export function CannotUseWithout(
});
};
}
export function CannotUseWith(
property: string,
validationOptions?: ValidationOptions
) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: "cannotUseWith",
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate<T>(value: T, args: ValidationArguments) {
if (value === undefined) {
return true;
}
const obj = args.object as unknown as T;
const forbidden = args.constraints[0] as keyof T;
return obj[forbidden] === undefined;
},
defaultMessage(args: ValidationArguments) {
return `${propertyName} cannot be used with ${args.constraints[0]}.`;
},
},
});
};
}