diff --git a/.github/workflows/build-server.yml b/.github/workflows/build-server.yml
index 1879e71b7..1a13cb62a 100644
--- a/.github/workflows/build-server.yml
+++ b/.github/workflows/build-server.yml
@@ -75,6 +75,11 @@ jobs:
--health-retries 5
ports:
- 5432:5432
+ mailer:
+ image: mailhog/mailhog
+ ports:
+ - 1025:1025
+ - 8025:8025
steps:
- uses: actions/checkout@v3
@@ -112,6 +117,7 @@ jobs:
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
+ ENABLE_LOCAL_EMAIL: true
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
@@ -139,6 +145,11 @@ jobs:
--health-retries 5
ports:
- 5432:5432
+ mailer:
+ image: mailhog/mailhog
+ ports:
+ - 1025:1025
+ - 8025:8025
steps:
- uses: actions/checkout@v3
@@ -181,6 +192,7 @@ jobs:
env:
COVERAGE: true
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
+ ENABLE_LOCAL_EMAIL: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
diff --git a/apps/server/.env.example b/apps/server/.env.example
index 2dcf75924..01e76c182 100644
--- a/apps/server/.env.example
+++ b/apps/server/.env.example
@@ -3,3 +3,4 @@ NEXTAUTH_URL="http://localhost:8080"
OAUTH_EMAIL_SENDER="noreply@toeverything.info"
OAUTH_EMAIL_LOGIN=""
OAUTH_EMAIL_PASSWORD=""
+ENABLE_LOCAL_EMAIL="true"
diff --git a/apps/server/scripts/test-send-local-mail.ts b/apps/server/scripts/test-send-local-mail.ts
new file mode 100644
index 000000000..50e0872ab
--- /dev/null
+++ b/apps/server/scripts/test-send-local-mail.ts
@@ -0,0 +1,18 @@
+import { createTransport } from 'nodemailer';
+
+const transport = createTransport({
+ host: '0.0.0.0',
+ port: 1025,
+ secure: false,
+ auth: {
+ user: 'himself65',
+ pass: '123456',
+ },
+});
+
+await transport.sendMail({
+ from: 'noreply@toeverything.info',
+ to: 'himself65@outlook.com',
+ subject: 'test',
+ html: `
hello world
`,
+});
diff --git a/apps/server/src/config/def.ts b/apps/server/src/config/def.ts
index e93047423..ef434575e 100644
--- a/apps/server/src/config/def.ts
+++ b/apps/server/src/config/def.ts
@@ -308,6 +308,11 @@ export interface AFFiNEConfig {
}
>
>;
+ /**
+ * whether to use local email service to send email
+ * local debug only
+ */
+ localEmail: boolean;
email: {
server: string;
port: number;
diff --git a/apps/server/src/config/default.ts b/apps/server/src/config/default.ts
index 25f16e2f6..3aae4b9d0 100644
--- a/apps/server/src/config/default.ts
+++ b/apps/server/src/config/default.ts
@@ -86,6 +86,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
'doc.manager.experimentalMergeWithJwstCodec',
'boolean',
],
+ ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
} satisfies AFFiNEConfig['ENV_MAP'],
affineEnv: 'dev',
get affine() {
@@ -152,6 +153,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
return this.privateKey;
},
oauthProviders: {},
+ localEmail: false,
email: {
server: 'smtp.gmail.com',
port: 465,
diff --git a/apps/server/src/modules/auth/mailer/mailer.ts b/apps/server/src/modules/auth/mailer/mailer.ts
index 6df367f5e..d1c8f65c3 100644
--- a/apps/server/src/modules/auth/mailer/mailer.ts
+++ b/apps/server/src/modules/auth/mailer/mailer.ts
@@ -15,6 +15,17 @@ export const MAILER: FactoryProvider<
> = {
provide: MAILER_SERVICE,
useFactory: (config: Config) => {
+ if (config.auth.localEmail) {
+ return createTransport({
+ host: '0.0.0.0',
+ port: 1025,
+ secure: false,
+ auth: {
+ user: config.auth.email.login,
+ pass: config.auth.email.password,
+ },
+ });
+ }
return createTransport({
service: 'gmail',
auth: {
diff --git a/apps/server/src/prelude.ts b/apps/server/src/prelude.ts
index 10c53c1be..0c0fae35f 100644
--- a/apps/server/src/prelude.ts
+++ b/apps/server/src/prelude.ts
@@ -4,3 +4,7 @@ import 'dotenv/config';
import { getDefaultAFFiNEConfig } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
+
+if (process.env.NODE_ENV === 'development') {
+ console.log('AFFiNE Config:', globalThis.AFFiNE);
+}
diff --git a/apps/server/src/tests/mailer.e2e.ts b/apps/server/src/tests/mailer.e2e.ts
new file mode 100644
index 000000000..8bc885271
--- /dev/null
+++ b/apps/server/src/tests/mailer.e2e.ts
@@ -0,0 +1,105 @@
+///
+// This test case is for testing the mailer service.
+// Please use local SMTP server for testing.
+// See: https://github.com/mailhog/MailHog
+import { Test, TestingModule } from '@nestjs/testing';
+import { PrismaClient } from '@prisma/client';
+import test from 'ava';
+
+import { ConfigModule } from '../config';
+import { GqlModule } from '../graphql.module';
+import { MetricsModule } from '../metrics';
+import { AuthModule } from '../modules/auth';
+import { AuthService } from '../modules/auth/service';
+import { PrismaModule } from '../prisma';
+import { RateLimiterModule } from '../throttler';
+
+let auth: AuthService;
+let module: TestingModule;
+let skip = false;
+
+test.before(async () => {
+ try {
+ const response = await fetch('http://localhost:8025/api/v2/messages');
+ if (!response.ok && !process.env.CI) {
+ console.warn('local mail not found, skip the mailer.e2e.ts');
+ skip = true;
+ }
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ (error.cause as any)?.code === 'ECONNREFUSED' &&
+ !process.env.CI
+ ) {
+ console.warn('local mail not found, skip the mailer.e2e.ts');
+ skip = true;
+ }
+ }
+});
+
+// cleanup database before each test
+test.beforeEach(async () => {
+ const client = new PrismaClient();
+ await client.$connect();
+ await client.user.deleteMany({});
+});
+
+test.beforeEach(async () => {
+ module = await Test.createTestingModule({
+ imports: [
+ ConfigModule.forRoot({
+ auth: {
+ accessTokenExpiresIn: 1,
+ refreshTokenExpiresIn: 1,
+ leeway: 1,
+ },
+ }),
+ PrismaModule,
+ GqlModule,
+ AuthModule,
+ MetricsModule,
+ RateLimiterModule,
+ ],
+ }).compile();
+ auth = module.get(AuthService);
+});
+
+test.afterEach(async () => {
+ await module.close();
+});
+
+const getCurrentMailMessageCount = async () => {
+ const response = await fetch('http://localhost:8025/api/v2/messages');
+ const data = await response.json();
+ return data.total;
+};
+
+const getLatestMailMessage = async () => {
+ const response = await fetch('http://localhost:8025/api/v2/messages');
+ const data = await response.json();
+ return data.items[0];
+};
+
+test('should include callbackUrl in sending email', async t => {
+ if (skip) {
+ return t.pass();
+ }
+ await auth.signUp('Alex Yang', 'alexyang@example.org', '123456');
+ for (const fn of [
+ 'sendSetPasswordEmail',
+ 'sendChangeEmail',
+ 'sendChangePasswordEmail',
+ ] as const) {
+ const prev = await getCurrentMailMessageCount();
+ await auth[fn]('alexyang@example.org', 'https://test.com/callback');
+ const current = await getCurrentMailMessageCount();
+ const mail = await getLatestMailMessage();
+ t.regex(
+ mail.Content.Body,
+ /https:\/\/test.com\/callback/,
+ `should include callbackUrl when calling ${fn}`
+ );
+ t.is(current, prev + 1, `calling ${fn}`);
+ }
+ return;
+});
diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts
index f88b60cea..7a63e657e 100644
--- a/tests/affine-cloud/playwright.config.ts
+++ b/tests/affine-cloud/playwright.config.ts
@@ -50,6 +50,7 @@ const config: PlaywrightTestConfig = {
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
+ ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
},