mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-23 17:13:40 +03:00
Adds support for e-mail sending (#1050)
This commit is contained in:
parent
b5967d395b
commit
6dbeedca60
@ -21,6 +21,34 @@ export const fooBar : FooBar = (req, res, context) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Adds support for sending e-mails
|
||||
|
||||
Wasp now supports sending e-mails! You can use the `emailSender` app property to configure the e-mail provider and optionally the `defaultFrom` address. Then, you can use the `send` function in your backend code to send e-mails.
|
||||
|
||||
```ts
|
||||
// main.wasp
|
||||
app MyApp {
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
defaultFrom: {
|
||||
name: "My App",
|
||||
email: "myapp@domain.com"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// server/actions.ts
|
||||
import { emailSender } from '@wasp/email/index.js'
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
to: 'user@domain.com',
|
||||
subject: 'Saying hello',
|
||||
text: 'Hello world',
|
||||
html: 'Hello <strong>world</strong>'
|
||||
})
|
||||
```
|
||||
|
||||
### `wasp start db` -> Wasp can now run your dev database for you with a single command
|
||||
|
||||
Moving from SQLite to PostgreSQL with Wasp can feel like increase in complexity, because suddenly you have to care about running your PostgreSQL database, providing connection URL for it via env var, and if you checkout somebody's else Wasp project, or your old Wasp project that you have no memory of any more, you also have to figure all that out.
|
||||
|
@ -13,6 +13,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.{= databaseUrlEnvVarName =},
|
||||
frontendUrl: undefined,
|
||||
|
@ -0,0 +1,37 @@
|
||||
{{={= =}=}}
|
||||
import { EmailFromField } from "./types";
|
||||
|
||||
// Formats an email address and an optional name into a string that can be used
|
||||
// as the "from" field in an email.
|
||||
// { email: "test@test.com, name: "Test" } -> "Test <test@test.com>"
|
||||
export function formatFromField({
|
||||
email,
|
||||
name,
|
||||
}: {
|
||||
email: string;
|
||||
name?: string;
|
||||
}): string {
|
||||
if (name) {
|
||||
return `${name} <${email}>`;
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
{=# isDefaultFromFieldDefined =}
|
||||
export function getDefaultFromField(): EmailFromField {
|
||||
return {
|
||||
email: "{= defaultFromField.email =}",
|
||||
{=# defaultFromField.isNameDefined =}
|
||||
name: "{= defaultFromField.name =}",
|
||||
{=/ defaultFromField.isNameDefined =}
|
||||
}
|
||||
}
|
||||
{=/ isDefaultFromFieldDefined =}
|
||||
{=^ isDefaultFromFieldDefined =}
|
||||
export function getDefaultFromField(): EmailFromField {
|
||||
return {
|
||||
email: "",
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
{=/ isDefaultFromFieldDefined =}
|
@ -0,0 +1,10 @@
|
||||
{{={= =}=}}
|
||||
{=# isSmtpProviderUsed =}
|
||||
export { initSmtpEmailSender as initEmailSender } from "./providers/smtp.js";
|
||||
{=/ isSmtpProviderUsed =}
|
||||
{=# isSendGridProviderUsed =}
|
||||
export { initSendGridEmailSender as initEmailSender } from "./providers/sendgrid.js";
|
||||
{=/ isSendGridProviderUsed =}
|
||||
{=# isMailgunProviderUsed =}
|
||||
export { initMailgunEmailSender as initEmailSender } from "./providers/mailgun.js";
|
||||
{=/ isMailgunProviderUsed =}
|
@ -0,0 +1,24 @@
|
||||
import { EmailSender } from "../types.js";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
|
||||
export function initDummyEmailSender(): EmailSender {
|
||||
const defaultFromField = getDefaultFromField();
|
||||
return {
|
||||
send: async (email) => {
|
||||
const fromField = email.from || defaultFromField;
|
||||
console.log('Test email (not sent):', {
|
||||
from: {
|
||||
email: fromField.email,
|
||||
name: fromField.name,
|
||||
},
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { NodeMailgun } from "ts-mailgun";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
import type { MailgunEmailProvider, EmailSender } from "../types.js";
|
||||
|
||||
export function initMailgunEmailSender(
|
||||
config: MailgunEmailProvider
|
||||
): EmailSender {
|
||||
const mailer = new NodeMailgun(config.apiKey, config.domain);
|
||||
|
||||
const defaultFromField = getDefaultFromField();
|
||||
|
||||
return {
|
||||
async send(email) {
|
||||
const fromField = email.from || defaultFromField;
|
||||
mailer.fromEmail = fromField.email;
|
||||
mailer.fromTitle = fromField.name;
|
||||
mailer.init();
|
||||
return mailer.send(email.to, email.subject, email.html);
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import SendGrid from "@sendgrid/mail";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
import type { SendGridProvider, EmailSender } from "../types.js";
|
||||
|
||||
export function initSendGridEmailSender(
|
||||
provider: SendGridProvider
|
||||
): EmailSender {
|
||||
SendGrid.setApiKey(provider.apiKey);
|
||||
|
||||
const defaultFromField = getDefaultFromField();
|
||||
|
||||
return {
|
||||
async send(email) {
|
||||
const fromField = email.from || defaultFromField;
|
||||
return SendGrid.send({
|
||||
from: {
|
||||
email: fromField.email,
|
||||
name: fromField.name,
|
||||
},
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
import { formatFromField, getDefaultFromField } from "../helpers.js";
|
||||
import type { SMTPEmailProvider, EmailSender } from "../types.js";
|
||||
|
||||
export function initSmtpEmailSender(config: SMTPEmailProvider): EmailSender {
|
||||
const transporter = createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
auth: {
|
||||
user: config.username,
|
||||
pass: config.password,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultFromField = getDefaultFromField();
|
||||
|
||||
return {
|
||||
async send(email) {
|
||||
return transporter.sendMail({
|
||||
from: formatFromField(email.from || defaultFromField),
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{{={= =}=}}
|
||||
export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider;
|
||||
|
||||
export type SMTPEmailProvider = {
|
||||
type: "smtp";
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SendGridProvider = {
|
||||
type: "sendgrid";
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type MailgunEmailProvider = {
|
||||
type: "mailgun";
|
||||
apiKey: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type EmailSender = {
|
||||
send: (email: Email) => Promise<SentMessageInfo>;
|
||||
};
|
||||
|
||||
export type SentMessageInfo = any;
|
||||
|
||||
export type Email = {
|
||||
{=# isDefaultFromFieldDefined =}
|
||||
from?: EmailFromField;
|
||||
{=/ isDefaultFromFieldDefined =}
|
||||
{=^ isDefaultFromFieldDefined =}
|
||||
from: EmailFromField;
|
||||
{=/ isDefaultFromFieldDefined =}
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export type EmailFromField = {
|
||||
name?: string;
|
||||
email: string;
|
||||
}
|
35
waspc/data/Generator/templates/server/src/email/index.ts
Normal file
35
waspc/data/Generator/templates/server/src/email/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
{{={= =}=}}
|
||||
import { initEmailSender } from "./core/index.js";
|
||||
|
||||
import waspServerConfig from '../config.js';
|
||||
import { initDummyEmailSender } from "./core/providers/dummy.js";
|
||||
|
||||
{=# isSmtpProviderUsed =}
|
||||
const emailProvider = {
|
||||
type: "smtp",
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT, 10),
|
||||
username: process.env.SMTP_USERNAME,
|
||||
password: process.env.SMTP_PASSWORD,
|
||||
} as const;
|
||||
{=/ isSmtpProviderUsed =}
|
||||
{=# isSendGridProviderUsed =}
|
||||
const emailProvider = {
|
||||
type: "sendgrid",
|
||||
apiKey: process.env.SENDGRID_API_KEY,
|
||||
} as const;
|
||||
{=/ isSendGridProviderUsed =}
|
||||
{=# isMailgunProviderUsed =}
|
||||
const emailProvider = {
|
||||
type: "mailgun",
|
||||
apiKey: process.env.MAILGUN_API_KEY,
|
||||
domain: process.env.MAILGUN_DOMAIN,
|
||||
} as const;
|
||||
{=/ isMailgunProviderUsed =}
|
||||
|
||||
const areEmailsSentInDevelopment = process.env.SEND_EMAILS_IN_DEVELOPMENT === "true";
|
||||
const isDummyEmailSenderUsed = waspServerConfig.isDevelopment && !areEmailsSentInDevelopment;
|
||||
|
||||
export const emailSender = isDummyEmailSenderUsed
|
||||
? initDummyEmailSender()
|
||||
: initEmailSender(emailProvider);
|
@ -24,6 +24,7 @@ waspComplexTest = do
|
||||
]
|
||||
<++> addServerEnvFile
|
||||
<++> addDependencies
|
||||
<++> addEmailSender
|
||||
<++> addClientSetup
|
||||
<++> addServerSetup
|
||||
<++> addGoogleAuth
|
||||
@ -129,7 +130,8 @@ addServerEnvFile = do
|
||||
-- on the location where the tests are being run.
|
||||
-- Therefore, we make sure to set custom database url here, to avoid .env
|
||||
-- changing between different machines / setups.
|
||||
databaseUrlEnvVarName <> "=" <> "mock-database-url"
|
||||
databaseUrlEnvVarName <> "=" <> "mock-database-url",
|
||||
"SENDGRID_API_KEY=sendgrid_api_key"
|
||||
]
|
||||
|
||||
addGoogleAuth :: ShellCommandBuilder [ShellCommand]
|
||||
@ -233,12 +235,6 @@ addDependencies = do
|
||||
" ],"
|
||||
]
|
||||
|
||||
insertCodeIntoWaspFileAfterVersion :: String -> ShellCommandBuilder ShellCommand
|
||||
insertCodeIntoWaspFileAfterVersion = insertCodeIntoWaspFileAtLineNumber lineNumberInWaspFileAfterVersion
|
||||
where
|
||||
lineNumberInWaspFileAfterVersion :: Int
|
||||
lineNumberInWaspFileAfterVersion = 5
|
||||
|
||||
addApi :: ShellCommandBuilder [ShellCommand]
|
||||
addApi = do
|
||||
sequence
|
||||
@ -271,3 +267,26 @@ addApi = do
|
||||
" res.json({ msg: 'Hello, stranger!' })",
|
||||
"}"
|
||||
]
|
||||
|
||||
addEmailSender :: ShellCommandBuilder [ShellCommand]
|
||||
addEmailSender = do
|
||||
sequence
|
||||
[ insertCodeIntoWaspFileAfterVersion emailSender
|
||||
]
|
||||
where
|
||||
emailSender =
|
||||
unlines
|
||||
[ " emailSender: {",
|
||||
" provider: SendGrid,",
|
||||
" defaultFrom: {",
|
||||
" name: \"Hello\",",
|
||||
" email: \"hello@itsme.com\"",
|
||||
" },",
|
||||
" },"
|
||||
]
|
||||
|
||||
insertCodeIntoWaspFileAfterVersion :: String -> ShellCommandBuilder ShellCommand
|
||||
insertCodeIntoWaspFileAfterVersion = insertCodeIntoWaspFileAtLineNumber lineNumberInWaspFileAfterVersion
|
||||
where
|
||||
lineNumberInWaspFileAfterVersion :: Int
|
||||
lineNumberInWaspFileAfterVersion = 5
|
||||
|
@ -102,7 +102,7 @@
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"db65648bfa899b556f499abfde8a4cc6360b01dce88d9318456a8920e6a7d942"
|
||||
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -12,6 +12,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
|
@ -109,7 +109,7 @@
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"db65648bfa899b556f499abfde8a4cc6360b01dce88d9318456a8920e6a7d942"
|
||||
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -12,6 +12,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
|
@ -34,6 +34,12 @@ waspComplexTest/.wasp/out/server/src/core/HttpError.js
|
||||
waspComplexTest/.wasp/out/server/src/core/auth.js
|
||||
waspComplexTest/.wasp/out/server/src/core/auth/prismaMiddleware.js
|
||||
waspComplexTest/.wasp/out/server/src/dbClient.js
|
||||
waspComplexTest/.wasp/out/server/src/email/core/helpers.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/core/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/core/providers/dummy.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/core/providers/sendgrid.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/core/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/entities/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/actions/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/apis.ts
|
||||
|
@ -1,4 +1,5 @@
|
||||
GOOGLE_CLIENT_ID=google_client_id
|
||||
GOOGLE_CLIENT_SECRET=google_client_secret
|
||||
DATABASE_URL=mock-database-url
|
||||
SENDGRID_API_KEY=sendgrid_api_key
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
"file",
|
||||
"server/.env"
|
||||
],
|
||||
"368bf0f88f43e6cc3083d535f348905e51a1d3747abb479749dc250253a0c042"
|
||||
"d9649485a674ce3c6b8d668a1253cde3bd8695cb3d2d3a6b7c573a3e2805c7ec"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -60,7 +60,7 @@
|
||||
"file",
|
||||
"server/package.json"
|
||||
],
|
||||
"94c2733af39f289c2691be23909fd6a1d8b3dcbb19144ca66ba5a48e2a26727e"
|
||||
"9ebd5e39b263bee5baf7a60d86850051050f665b9a2e0274a8e80b47b93ae90d"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -186,7 +186,7 @@
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"feb61b839f6ce3c1f49b279fd08275479308089a6f12188b312b64bdf571e6c5"
|
||||
"b5e2d31201460b018e781532b7bb6348cf3f7eb45851a81711a18584c0f4f5ac"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -223,6 +223,48 @@
|
||||
],
|
||||
"5fb53eff5e5eae318e281a8fc1cc433b69688d93c3c82d72415f710a6e75e2af"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/core/helpers.ts"
|
||||
],
|
||||
"b59fd329fc96e47f2839ba0b06ea5e42935e4959d228676be9c13ddb30d9e02c"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/core/index.ts"
|
||||
],
|
||||
"d524dd9ef27cd311340060411276df0e8ef22db503473f44281832338b954bb7"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/core/providers/dummy.ts"
|
||||
],
|
||||
"e93a7a02f50c8466f3e8e89255b98bebde598b25f9969ec117b16f07691575ae"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/core/providers/sendgrid.ts"
|
||||
],
|
||||
"b1a16455eede9723f6ae43c03e628b4129fd96f7300f004f052f1eab31d9b95b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/core/types.ts"
|
||||
],
|
||||
"c343f0d87b65d7563816159a88f410b65d78d897822c0bbcd723ca7752e00a20"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/email/index.ts"
|
||||
],
|
||||
"c4864d5c83b96a61b1ddfaac7b52c0898f5cff04320c166c8f658b017952ee05"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -417,7 +459,7 @@
|
||||
"file",
|
||||
"web-app/.env"
|
||||
],
|
||||
"368bf0f88f43e6cc3083d535f348905e51a1d3747abb479749dc250253a0c042"
|
||||
"d9649485a674ce3c6b8d668a1253cde3bd8695cb3d2d3a6b7c573a3e2805c7ec"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1 +1 @@
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
|
||||
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"}]}}
|
@ -1,3 +1,4 @@
|
||||
GOOGLE_CLIENT_ID="google_client_id"
|
||||
GOOGLE_CLIENT_SECRET="google_client_secret"
|
||||
DATABASE_URL="mock-database-url"
|
||||
DATABASE_URL="mock-database-url"
|
||||
SENDGRID_API_KEY="sendgrid_api_key"
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"cookie-parser": "~1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "16.0.2",
|
||||
|
@ -12,6 +12,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { EmailFromField } from "./types";
|
||||
|
||||
// Formats an email address and an optional name into a string that can be used
|
||||
// as the "from" field in an email.
|
||||
// { email: "test@test.com, name: "Test" } -> "Test <test@test.com>"
|
||||
export function formatFromField({
|
||||
email,
|
||||
name,
|
||||
}: {
|
||||
email: string;
|
||||
name?: string;
|
||||
}): string {
|
||||
if (name) {
|
||||
return `${name} <${email}>`;
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
export function getDefaultFromField(): EmailFromField {
|
||||
return {
|
||||
email: "hello@itsme.com",
|
||||
name: "Hello",
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { initSendGridEmailSender as initEmailSender } from "./providers/sendgrid.js";
|
@ -0,0 +1,24 @@
|
||||
import { EmailSender } from "../types.js";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
|
||||
export function initDummyEmailSender(): EmailSender {
|
||||
const defaultFromField = getDefaultFromField();
|
||||
return {
|
||||
send: async (email) => {
|
||||
const fromField = email.from || defaultFromField;
|
||||
console.log('Test email (not sent):', {
|
||||
from: {
|
||||
email: fromField.email,
|
||||
name: fromField.name,
|
||||
},
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import SendGrid from "@sendgrid/mail";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
import type { SendGridProvider, EmailSender } from "../types.js";
|
||||
|
||||
export function initSendGridEmailSender(
|
||||
provider: SendGridProvider
|
||||
): EmailSender {
|
||||
SendGrid.setApiKey(provider.apiKey);
|
||||
|
||||
const defaultFromField = getDefaultFromField();
|
||||
|
||||
return {
|
||||
async send(email) {
|
||||
const fromField = email.from || defaultFromField;
|
||||
return SendGrid.send({
|
||||
from: {
|
||||
email: fromField.email,
|
||||
name: fromField.name,
|
||||
},
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
export type EmailProvider = SMTPEmailProvider | SendGridProvider | MailgunEmailProvider;
|
||||
|
||||
export type SMTPEmailProvider = {
|
||||
type: "smtp";
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SendGridProvider = {
|
||||
type: "sendgrid";
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type MailgunEmailProvider = {
|
||||
type: "mailgun";
|
||||
apiKey: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type EmailSender = {
|
||||
send: (email: Email) => Promise<SentMessageInfo>;
|
||||
};
|
||||
|
||||
export type SentMessageInfo = any;
|
||||
|
||||
export type Email = {
|
||||
from?: EmailFromField;
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export type EmailFromField = {
|
||||
name?: string;
|
||||
email: string;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { initEmailSender } from "./core/index.js";
|
||||
|
||||
import waspServerConfig from '../config.js';
|
||||
import { initDummyEmailSender } from "./core/providers/dummy.js";
|
||||
|
||||
const emailProvider = {
|
||||
type: "sendgrid",
|
||||
apiKey: process.env.SENDGRID_API_KEY,
|
||||
} as const;
|
||||
|
||||
const areEmailsSentInDevelopment = process.env.SEND_EMAILS_IN_DEVELOPMENT === "true";
|
||||
const isDummyEmailSenderUsed = waspServerConfig.isDevelopment && !areEmailsSentInDevelopment;
|
||||
|
||||
export const emailSender = isDummyEmailSenderUsed
|
||||
? initDummyEmailSender()
|
||||
: initEmailSender(emailProvider);
|
@ -1,3 +1,4 @@
|
||||
GOOGLE_CLIENT_ID="google_client_id"
|
||||
GOOGLE_CLIENT_SECRET="google_client_secret"
|
||||
DATABASE_URL="mock-database-url"
|
||||
DATABASE_URL="mock-database-url"
|
||||
SENDGRID_API_KEY="sendgrid_api_key"
|
@ -21,6 +21,14 @@ app waspComplexTest {
|
||||
rootComponent: import App from "@client/App.jsx"
|
||||
},
|
||||
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
defaultFrom: {
|
||||
name: "Hello",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
},
|
||||
|
||||
dependencies: [
|
||||
("redux", "^4.0.5"),
|
||||
("react-redux", "^7.1.3")
|
||||
|
@ -109,7 +109,7 @@
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"db65648bfa899b556f499abfde8a4cc6360b01dce88d9318456a8920e6a7d942"
|
||||
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -12,6 +12,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
|
@ -109,7 +109,7 @@
|
||||
"file",
|
||||
"server/src/config.js"
|
||||
],
|
||||
"db65648bfa899b556f499abfde8a4cc6360b01dce88d9318456a8920e6a7d942"
|
||||
"85e22f3e8e87902ed0b58a3e529d9c2167b553383f33fbb4261a5031a3c5ba27"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -12,6 +12,7 @@ const env = process.env.NODE_ENV || 'development'
|
||||
const config = {
|
||||
all: {
|
||||
env,
|
||||
isDevelopment: env === 'development',
|
||||
port: parseInt(process.env.PORT) || 3001,
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: undefined,
|
||||
|
@ -5,29 +5,37 @@ import {
|
||||
CreateTask,
|
||||
DeleteCompletedTasks,
|
||||
ToggleAllTasks,
|
||||
UpdateTaskIsDone
|
||||
UpdateTaskIsDone,
|
||||
} from '@wasp/actions/types'
|
||||
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>> = async (task, context) => {
|
||||
export const createTask: CreateTask<Pick<Task, 'description'>> = async (
|
||||
task,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
|
||||
console.log('New task created! Btw, current value of someResource is: ' + getSomeResource())
|
||||
console.log(
|
||||
'New task created! Btw, current value of someResource is: ' +
|
||||
getSomeResource()
|
||||
)
|
||||
|
||||
return Task.create({
|
||||
data: {
|
||||
description: task.description,
|
||||
user: {
|
||||
connect: { id: context.user.id }
|
||||
}
|
||||
}
|
||||
connect: { id: context.user.id },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTaskIsDone: UpdateTaskIsDone<Pick<Task, 'id' | 'isDone'>> = async ({ id, isDone }, context) => {
|
||||
export const updateTaskIsDone: UpdateTaskIsDone<
|
||||
Pick<Task, 'id' | 'isDone'>
|
||||
> = async ({ id, isDone }, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
@ -37,20 +45,24 @@ export const updateTaskIsDone: UpdateTaskIsDone<Pick<Task, 'id' | 'isDone'>> = a
|
||||
// await sleep(3000);
|
||||
|
||||
const Task = context.entities.Task
|
||||
return Task.updateMany({
|
||||
const updateResult = await Task.updateMany({
|
||||
where: { id, user: { id: context.user.id } },
|
||||
data: { isDone }
|
||||
data: { isDone },
|
||||
})
|
||||
return updateResult
|
||||
}
|
||||
|
||||
export const deleteCompletedTasks: DeleteCompletedTasks = async (_args, context) => {
|
||||
export const deleteCompletedTasks: DeleteCompletedTasks = async (
|
||||
_args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
|
||||
const Task = context.entities.Task
|
||||
await Task.deleteMany({
|
||||
where: { isDone: true, user: { id: context.user.id } }
|
||||
where: { isDone: true, user: { id: context.user.id } },
|
||||
})
|
||||
}
|
||||
|
||||
@ -62,8 +74,8 @@ export const toggleAllTasks: ToggleAllTasks = async (_args, context) => {
|
||||
const whereIsDone = (isDone: boolean) => ({
|
||||
isDone,
|
||||
user: { id: context.user.id },
|
||||
});
|
||||
const Task = context.entities.Task;
|
||||
})
|
||||
const Task = context.entities.Task
|
||||
const notDoneTasksCount = await Task.count({ where: whereIsDone(false) })
|
||||
|
||||
if (notDoneTasksCount > 0) {
|
||||
|
@ -34,7 +34,7 @@ app todoApp {
|
||||
},
|
||||
db: {
|
||||
system: PostgreSQL
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
@ -167,4 +167,4 @@ job mySpecialScheduledJob {
|
||||
pgBoss: {=json { "retryLimit": 2 } json=}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,12 +14,14 @@ import Wasp.AppSpec.Action (Action)
|
||||
import Wasp.AppSpec.Api (Api, HttpMethod)
|
||||
import Wasp.AppSpec.App (App)
|
||||
import Wasp.AppSpec.App.Db (DbSystem)
|
||||
import Wasp.AppSpec.App.EmailSender (EmailProvider)
|
||||
import Wasp.AppSpec.Entity (Entity)
|
||||
import Wasp.AppSpec.Job (Job, JobExecutor)
|
||||
import Wasp.AppSpec.Page (Page)
|
||||
import Wasp.AppSpec.Query (Query)
|
||||
import Wasp.AppSpec.Route (Route)
|
||||
|
||||
makeEnumType ''EmailProvider
|
||||
makeEnumType ''DbSystem
|
||||
makeDeclType ''App
|
||||
makeDeclType ''Page
|
||||
@ -48,5 +50,6 @@ stdTypes =
|
||||
TD.addDeclType @Job $
|
||||
TD.addEnumType @HttpMethod $
|
||||
TD.addDeclType @Api $
|
||||
TD.addEnumType @EmailProvider $
|
||||
TD.empty
|
||||
{- ORMOLU_ENABLE -}
|
||||
|
@ -7,6 +7,7 @@ import Wasp.AppSpec.App.Auth (Auth)
|
||||
import Wasp.AppSpec.App.Client (Client)
|
||||
import Wasp.AppSpec.App.Db (Db)
|
||||
import Wasp.AppSpec.App.Dependency (Dependency)
|
||||
import Wasp.AppSpec.App.EmailSender (EmailSender)
|
||||
import Wasp.AppSpec.App.Server (Server)
|
||||
import Wasp.AppSpec.App.Wasp (Wasp)
|
||||
import Wasp.AppSpec.Core.Decl (IsDecl)
|
||||
@ -19,6 +20,7 @@ data App = App
|
||||
server :: Maybe Server,
|
||||
client :: Maybe Client,
|
||||
db :: Maybe Db,
|
||||
emailSender :: Maybe EmailSender,
|
||||
dependencies :: Maybe [Dependency]
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
25
waspc/src/Wasp/AppSpec/App/EmailSender.hs
Normal file
25
waspc/src/Wasp/AppSpec/App/EmailSender.hs
Normal file
@ -0,0 +1,25 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
|
||||
module Wasp.AppSpec.App.EmailSender
|
||||
( EmailSender (..),
|
||||
EmailProvider (..),
|
||||
EmailFromField (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Data (Data)
|
||||
|
||||
data EmailSender = EmailSender
|
||||
{ provider :: EmailProvider,
|
||||
defaultFrom :: Maybe EmailFromField
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
data EmailProvider = SMTP | SendGrid | Mailgun
|
||||
deriving (Eq, Data, Show)
|
||||
|
||||
data EmailFromField = EmailFromField
|
||||
{ name :: Maybe String,
|
||||
email :: String
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
@ -56,6 +56,7 @@ import Wasp.Generator.ServerGenerator.Auth.OAuthAuthG (depsRequiredByPassport)
|
||||
import Wasp.Generator.ServerGenerator.AuthG (genAuth)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
|
||||
import Wasp.Generator.ServerGenerator.EmailSenderG (depsRequiredByEmail, genEmailSender)
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy)
|
||||
import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs)
|
||||
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson)
|
||||
@ -152,7 +153,8 @@ npmDepsForWasp spec =
|
||||
("lodash.merge", "^4.6.2")
|
||||
]
|
||||
++ depsRequiredByPassport spec
|
||||
++ depsRequiredByJobs spec,
|
||||
++ depsRequiredByJobs spec
|
||||
++ depsRequiredByEmail spec,
|
||||
N.waspDevDependencies =
|
||||
AS.Dependency.fromList
|
||||
[ ("nodemon", "^2.0.19"),
|
||||
@ -200,6 +202,7 @@ genSrcDir spec =
|
||||
<++> genOperationsRoutes spec
|
||||
<++> genOperations spec
|
||||
<++> genAuth spec
|
||||
<++> genEmailSender spec
|
||||
where
|
||||
genFileCopy = return . C.mkSrcTmplFd
|
||||
|
||||
|
@ -0,0 +1,69 @@
|
||||
module Wasp.Generator.ServerGenerator.EmailSender.Providers
|
||||
( smtp,
|
||||
sendGrid,
|
||||
mailgun,
|
||||
providersDirInServerSrc,
|
||||
EmailSenderProvider (..),
|
||||
)
|
||||
where
|
||||
|
||||
import StrongPath (Dir, File', Path', Rel, reldir, relfile)
|
||||
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
|
||||
data EmailSenderProvider = EmailSenderProvider
|
||||
{ npmDependency :: AS.Dependency.Dependency,
|
||||
setupFnFile :: Path' (Rel ProvidersDir) File',
|
||||
-- We have to use explicit boolean keys in templates (e.g. "isSMTPProviderEnabled") so each
|
||||
-- provider provides its own key which we pass to the template.
|
||||
isEnabledKey :: String
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
data ProvidersDir
|
||||
|
||||
smtp :: EmailSenderProvider
|
||||
smtp =
|
||||
EmailSenderProvider
|
||||
{ npmDependency = nodeMailerDependency,
|
||||
setupFnFile = [relfile|smtp.ts|],
|
||||
isEnabledKey = "isSmtpProviderUsed"
|
||||
}
|
||||
where
|
||||
nodeMailerVersionRange :: SV.Range
|
||||
nodeMailerVersionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 6 9 1)]
|
||||
|
||||
nodeMailerDependency :: AS.Dependency.Dependency
|
||||
nodeMailerDependency = AS.Dependency.make ("nodemailer", show nodeMailerVersionRange)
|
||||
|
||||
sendGrid :: EmailSenderProvider
|
||||
sendGrid =
|
||||
EmailSenderProvider
|
||||
{ npmDependency = sendGridDependency,
|
||||
setupFnFile = [relfile|sendgrid.ts|],
|
||||
isEnabledKey = "isSendGridProviderUsed"
|
||||
}
|
||||
where
|
||||
sendGridVersionRange :: SV.Range
|
||||
sendGridVersionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 7 7 0)]
|
||||
|
||||
sendGridDependency :: AS.Dependency.Dependency
|
||||
sendGridDependency = AS.Dependency.make ("@sendgrid/mail", show sendGridVersionRange)
|
||||
|
||||
mailgun :: EmailSenderProvider
|
||||
mailgun =
|
||||
EmailSenderProvider
|
||||
{ npmDependency = mailgunDependency,
|
||||
setupFnFile = [relfile|mailgun.ts|],
|
||||
isEnabledKey = "isMailgunProviderUsed"
|
||||
}
|
||||
where
|
||||
mailgunVersionRange :: SV.Range
|
||||
mailgunVersionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 0 5 1)]
|
||||
|
||||
mailgunDependency :: AS.Dependency.Dependency
|
||||
mailgunDependency = AS.Dependency.make ("ts-mailgun", show mailgunVersionRange)
|
||||
|
||||
providersDirInServerSrc :: Path' (Rel C.ServerTemplatesSrcDir) (Dir ProvidersDir)
|
||||
providersDirInServerSrc = [reldir|email/core/providers|]
|
114
waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs
Normal file
114
waspc/src/Wasp/Generator/ServerGenerator/EmailSenderG.hs
Normal file
@ -0,0 +1,114 @@
|
||||
module Wasp.Generator.ServerGenerator.EmailSenderG where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Maybe (fromMaybe, isJust, maybeToList)
|
||||
import qualified Data.Text
|
||||
import StrongPath (File', Path', Rel, relfile, (</>))
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
|
||||
import Wasp.AppSpec.App.EmailSender (EmailSender)
|
||||
import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender
|
||||
import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import qualified Wasp.Generator.ServerGenerator.EmailSender.Providers as Providers
|
||||
import Wasp.Util ((<++>))
|
||||
|
||||
genEmailSender :: AppSpec -> Generator [FileDraft]
|
||||
genEmailSender spec = case maybeEmailSender of
|
||||
Just emailSender ->
|
||||
sequence
|
||||
[ genIndex emailSender
|
||||
]
|
||||
<++> genCore emailSender
|
||||
Nothing -> return []
|
||||
where
|
||||
maybeEmailSender = AS.App.emailSender $ snd $ getApp spec
|
||||
|
||||
genIndex :: EmailSender -> Generator FileDraft
|
||||
genIndex email = return $ C.mkTmplFdWithData tmplPath (Just tmplData)
|
||||
where
|
||||
tmplPath = [relfile|src/email/index.ts|]
|
||||
tmplData = getEmailProvidersJson email
|
||||
|
||||
genCore :: EmailSender -> Generator [FileDraft]
|
||||
genCore email =
|
||||
sequence
|
||||
[ genCoreIndex email,
|
||||
genCoreTypes email,
|
||||
genCoreHelpers email,
|
||||
copyTmplFile [relfile|email/core/providers/dummy.ts|]
|
||||
]
|
||||
<++> genEmailSenderProviderSetupFn email
|
||||
|
||||
genCoreIndex :: EmailSender -> Generator FileDraft
|
||||
genCoreIndex email = return $ C.mkTmplFdWithData tmplPath (Just tmplData)
|
||||
where
|
||||
tmplPath = [relfile|src/email/core/index.ts|]
|
||||
tmplData = getEmailProvidersJson email
|
||||
|
||||
genCoreTypes :: EmailSender -> Generator FileDraft
|
||||
genCoreTypes email = return $ C.mkTmplFdWithData tmplPath (Just tmplData)
|
||||
where
|
||||
tmplPath = [relfile|src/email/core/types.ts|]
|
||||
tmplData =
|
||||
object ["isDefaultFromFieldDefined" .= isDefaultFromFieldDefined]
|
||||
isDefaultFromFieldDefined = isJust defaultFromField
|
||||
defaultFromField = AS.EmailSender.defaultFrom email
|
||||
|
||||
genCoreHelpers :: EmailSender -> Generator FileDraft
|
||||
genCoreHelpers email = return $ C.mkTmplFdWithData tmplPath (Just tmplData)
|
||||
where
|
||||
tmplPath = [relfile|src/email/core/helpers.ts|]
|
||||
tmplData =
|
||||
object
|
||||
[ "defaultFromField"
|
||||
.= object
|
||||
[ "email" .= fromMaybe "" maybeEmail,
|
||||
"name" .= fromMaybe "" maybeName,
|
||||
"isNameDefined" .= isJust maybeName
|
||||
],
|
||||
"isDefaultFromFieldDefined" .= isDefaultFromFieldDefined
|
||||
]
|
||||
isDefaultFromFieldDefined = isJust defaultFromField
|
||||
maybeEmail = AS.EmailSender.email <$> defaultFromField
|
||||
maybeName = defaultFromField >>= AS.EmailSender.name
|
||||
defaultFromField = AS.EmailSender.defaultFrom email
|
||||
|
||||
genEmailSenderProviderSetupFn :: EmailSender -> Generator [FileDraft]
|
||||
genEmailSenderProviderSetupFn email =
|
||||
sequence
|
||||
[ copyTmplFile tmplPath
|
||||
]
|
||||
where
|
||||
provider :: Providers.EmailSenderProvider
|
||||
provider = getEmailSenderProvider email
|
||||
|
||||
tmplPath = Providers.providersDirInServerSrc </> Providers.setupFnFile provider
|
||||
|
||||
depsRequiredByEmail :: AppSpec -> [AS.Dependency.Dependency]
|
||||
depsRequiredByEmail spec = maybeToList maybeNpmDepedency
|
||||
where
|
||||
maybeProvider :: Maybe Providers.EmailSenderProvider
|
||||
maybeProvider = getEmailSenderProvider <$> (AS.App.emailSender . snd . getApp $ spec)
|
||||
maybeNpmDepedency = Providers.npmDependency <$> maybeProvider
|
||||
|
||||
getEmailProvidersJson :: EmailSender -> Aeson.Value
|
||||
getEmailProvidersJson email =
|
||||
object [isEnabledKey .= True]
|
||||
where
|
||||
provider :: Providers.EmailSenderProvider
|
||||
provider = getEmailSenderProvider email
|
||||
isEnabledKey = Data.Text.pack $ Providers.isEnabledKey provider
|
||||
|
||||
getEmailSenderProvider :: EmailSender -> Providers.EmailSenderProvider
|
||||
getEmailSenderProvider email = case AS.EmailSender.provider email of
|
||||
AS.EmailSender.SMTP -> Providers.smtp
|
||||
AS.EmailSender.SendGrid -> Providers.sendGrid
|
||||
AS.EmailSender.Mailgun -> Providers.mailgun
|
||||
|
||||
copyTmplFile :: Path' (Rel C.ServerTemplatesSrcDir) File' -> Generator FileDraft
|
||||
copyTmplFile = return . C.mkSrcTmplFd
|
@ -18,6 +18,7 @@ import qualified Wasp.AppSpec.App.Auth as Auth
|
||||
import qualified Wasp.AppSpec.App.Client as Client
|
||||
import qualified Wasp.AppSpec.App.Db as Db
|
||||
import qualified Wasp.AppSpec.App.Dependency as Dependency
|
||||
import qualified Wasp.AppSpec.App.EmailSender as EmailSender
|
||||
import qualified Wasp.AppSpec.App.Server as Server
|
||||
import qualified Wasp.AppSpec.App.Wasp as Wasp
|
||||
import Wasp.AppSpec.Core.Ref (Ref (..))
|
||||
@ -62,6 +63,13 @@ spec_Analyzer = do
|
||||
" },",
|
||||
" db: {",
|
||||
" system: PostgreSQL",
|
||||
" },",
|
||||
" emailSender: {",
|
||||
" provider: SendGrid,",
|
||||
" defaultFrom: {",
|
||||
" email: \"test@test.com\",",
|
||||
" name: \"Test\"",
|
||||
" }",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
@ -154,7 +162,18 @@ spec_Analyzer = do
|
||||
Just $
|
||||
ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx")
|
||||
},
|
||||
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL}
|
||||
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL},
|
||||
App.emailSender =
|
||||
Just
|
||||
EmailSender.EmailSender
|
||||
{ EmailSender.provider = EmailSender.SendGrid,
|
||||
EmailSender.defaultFrom =
|
||||
Just
|
||||
EmailSender.EmailFromField
|
||||
{ EmailSender.email = "test@test.com",
|
||||
EmailSender.name = Just "Test"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
|
@ -185,7 +185,8 @@ spec_AppSpecValid = do
|
||||
AS.App.client = Nothing,
|
||||
AS.App.auth = Nothing,
|
||||
AS.App.dependencies = Nothing,
|
||||
AS.App.head = Nothing
|
||||
AS.App.head = Nothing,
|
||||
AS.App.emailSender = Nothing
|
||||
}
|
||||
|
||||
basicAppDecl = AS.Decl.makeDecl "TestApp" basicApp
|
||||
|
@ -40,7 +40,8 @@ spec_WebAppGenerator = do
|
||||
AS.App.client = Nothing,
|
||||
AS.App.auth = Nothing,
|
||||
AS.App.dependencies = Nothing,
|
||||
AS.App.head = Nothing
|
||||
AS.App.head = Nothing,
|
||||
AS.App.emailSender = Nothing
|
||||
}
|
||||
],
|
||||
AS.waspProjectDir = systemSPRoot SP.</> [SP.reldir|test/|],
|
||||
|
@ -189,6 +189,7 @@ library
|
||||
Wasp.AppSpec.App.Auth
|
||||
Wasp.AppSpec.App.Client
|
||||
Wasp.AppSpec.App.Db
|
||||
Wasp.AppSpec.App.EmailSender
|
||||
Wasp.AppSpec.App.Dependency
|
||||
Wasp.AppSpec.App.Server
|
||||
Wasp.AppSpec.App.Wasp
|
||||
@ -252,6 +253,8 @@ library
|
||||
Wasp.Generator.ServerGenerator.AuthG
|
||||
Wasp.Generator.ServerGenerator.Auth.OAuthAuthG
|
||||
Wasp.Generator.ServerGenerator.Auth.LocalAuthG
|
||||
Wasp.Generator.ServerGenerator.EmailSenderG
|
||||
Wasp.Generator.ServerGenerator.EmailSender.Providers
|
||||
Wasp.Generator.ServerGenerator.Common
|
||||
Wasp.Generator.ServerGenerator.ConfigG
|
||||
Wasp.Generator.ServerGenerator.ExternalCodeGenerator
|
||||
|
7
web/docs/_sendingEmailsInDevelopment.md
Normal file
7
web/docs/_sendingEmailsInDevelopment.md
Normal file
@ -0,0 +1,7 @@
|
||||
:::info Sending emails while developing
|
||||
|
||||
When you run your app in development mode, the e-mails are not actually sent. Instead, they are logged to the console.
|
||||
|
||||
In order to enable sending e-mails in development mode, you need to set the `SEND_EMAILS_IN_DEVELOPMENT` env variable to `true` in your `.env.server` file.
|
||||
|
||||
:::
|
147
web/docs/guides/sending-emails.md
Normal file
147
web/docs/guides/sending-emails.md
Normal file
@ -0,0 +1,147 @@
|
||||
---
|
||||
title: Sending Emails
|
||||
---
|
||||
|
||||
import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
|
||||
# Sending Emails
|
||||
|
||||
With Wasp's email-sending feature, you can easily integrate email functionality into your web application.
|
||||
|
||||
```js title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: <provider>,
|
||||
defaultFrom: {
|
||||
name: "Example",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Choose from one of the providers:
|
||||
- `Mailgun`,
|
||||
- `SendGrid`
|
||||
- or the good old `SMTP`.
|
||||
|
||||
Optionally, define the `defaultFrom` field, so you don't need to provide it whenever sending an e-mail.
|
||||
|
||||
## Sending e-mails
|
||||
|
||||
<SendingEmailsInDevelopment />
|
||||
|
||||
Before jumping into details about setting up various providers, let's see how easy it is to send e-mails.
|
||||
|
||||
You import the `emailSender` that is provided by the `@wasp/email` module and call the `send` method on it.
|
||||
|
||||
```ts title="src/actions/sendEmail.js"
|
||||
import { emailSender } from '@wasp/email/index.js'
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
from: {
|
||||
name: 'John Doe',
|
||||
email: 'john@doe.com',
|
||||
},
|
||||
to: 'user@domain.com',
|
||||
subject: 'Saying hello',
|
||||
text: 'Hello world',
|
||||
html: 'Hello <strong>world</strong>'
|
||||
})
|
||||
```
|
||||
|
||||
Let's see what the `send` method accepts:
|
||||
|
||||
- `from` - the sender's details.
|
||||
- `name` - the name of the sender
|
||||
- `email` - the e-mail address of the sender
|
||||
- If you set up `defaultFrom` field in the `main.wasp`, this field is optional.
|
||||
- `to` - the recipient's e-mail address
|
||||
- `subject` - the subject of the e-mail
|
||||
- `text` - the text version of the e-mail
|
||||
- `html` - the HTML version of the e-mail
|
||||
|
||||
The `send` method returns an object with the status of the sent e-mail. It varies depending on the provider you use.
|
||||
|
||||
## Providers
|
||||
|
||||
For each provider, you'll need to set up env variables in the `.env.server` file at the root of your project.
|
||||
|
||||
## Using the SMTP provider
|
||||
|
||||
First, set the provider to `SMTP` in your `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, add the following env variables to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SMTP_HOST=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_PORT=
|
||||
```
|
||||
|
||||
Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.
|
||||
|
||||
## Using the Mailgun provider
|
||||
|
||||
Set the provider to `Mailgun` in the `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: Mailgun,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, get the Mailgun API key and domain and add them to your `.env.server` file.
|
||||
|
||||
### Getting the API key and domain
|
||||
|
||||
1. Go to [Mailgun](https://www.mailgun.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
4. Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain.
|
||||
5. Copy the domain and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
MAILGUN_API_KEY=
|
||||
MAILGUN_DOMAIN=
|
||||
```
|
||||
|
||||
## Using the SendGrid provider
|
||||
|
||||
Set the provider field to `SendGrid` in your `main.wasp` file.
|
||||
|
||||
```js title="main.wasp"
|
||||
app Example {
|
||||
...
|
||||
emailSender: {
|
||||
provider: SendGrid,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, get the SendGrid API key and add it to your `.env.server` file.
|
||||
|
||||
### Getting the API key
|
||||
|
||||
1. Go to [SendGrid](https://sendgrid.com/) and create an account.
|
||||
2. Go to [API Keys](https://app.sendgrid.com/settings/api_keys) and create a new API key.
|
||||
3. Copy the API key and add it to your `.env.server` file.
|
||||
|
||||
```properties title=".env.server"
|
||||
SENDGRID_API_KEY=
|
||||
```
|
@ -4,6 +4,7 @@ title: Features
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import SendingEmailsInDevelopment from '../_sendingEmailsInDevelopment.md'
|
||||
|
||||
## App
|
||||
|
||||
@ -58,6 +59,10 @@ Check [`app.db`](/docs/language/features#database-configuration) for more detail
|
||||
List of dependencies (external libraries).
|
||||
Check [`app.dependencies`](/docs/language/features#dependencies) for more details.
|
||||
|
||||
#### `emailSender: dict` (optional)
|
||||
Email sender configuration.
|
||||
Check [`app.emailSender`](/docs/language/features#email-sender) for more details.
|
||||
|
||||
## Page
|
||||
|
||||
`page` declaration is the top-level layout abstraction. Your app can have multiple pages.
|
||||
@ -1766,8 +1771,72 @@ The easiest way to provide the needed `DATABASE_URL` environment variable is by
|
||||
|
||||
### Migrating from SQLite to PostgreSQL
|
||||
To run Wasp app in production, you will need to switch from `SQLite` to `PostgreSQL`.
|
||||
|
||||
1. Set `app.db.system` to `PostgreSQL`.
|
||||
3. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: `rm -r migrations/`.
|
||||
3. Run `wasp start db` to start your new db running (or check instructions above if you prefer using your custom db). Leave it running, since we need it for the next step.
|
||||
4. In a different terminal, run `wasp db migrate-dev` to apply new changes and create new, initial migration.
|
||||
5. That is it, you are all done!
|
||||
|
||||
## Email sender
|
||||
|
||||
#### `provider: EmailProvider` (required)
|
||||
|
||||
We support multiple different providers for sending e-mails: `SMTP`, `SendGrid` and `Mailgun`.
|
||||
|
||||
### SMTP
|
||||
|
||||
SMTP e-mail sender uses your SMTP server to send e-mails.
|
||||
|
||||
Read [our guide](/docs/guides/sending-emails#using-the-smtp-provider) for setting up SMTP for more details.
|
||||
|
||||
|
||||
### SendGrid
|
||||
|
||||
SendGrid is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.
|
||||
|
||||
Check out [our guide](/docs/guides/sending-emails#using-the-sendgrid-provider) for setting up Sendgrid for more details.
|
||||
|
||||
### Mailgun
|
||||
|
||||
Mailgun is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.
|
||||
|
||||
Check out [our guide](/docs/guides/sending-emails#using-the-mailgun-provider) for setting up Mailgun for more details.
|
||||
|
||||
#### `defaultSender: EmailFromField` (optional)
|
||||
|
||||
You can optionally provide a default sender info that will be used when you don't provide it explicitly when sending an e-mail.
|
||||
|
||||
```c
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
emailSender: {
|
||||
provider: SMTP,
|
||||
defaultFrom: {
|
||||
name: "Hello",
|
||||
email: "hello@itsme.com"
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
After you set up the email sender, you can use it in your code to send e-mails. For example, you can send an e-mail when a user signs up, or when a user resets their password.
|
||||
|
||||
### Sending e-mails
|
||||
|
||||
<SendingEmailsInDevelopment />
|
||||
|
||||
To send an e-mail, you can use the `emailSender` that is provided by the `@wasp/email` module.
|
||||
|
||||
```ts title="src/actions/sendEmail.js"
|
||||
import { emailSender } from '@wasp/email/index.js'
|
||||
|
||||
// In some action handler...
|
||||
const info = await emailSender.send({
|
||||
to: 'user@domain.com',
|
||||
subject: 'Saying hello',
|
||||
text: 'Hello world',
|
||||
html: 'Hello <strong>world</strong>'
|
||||
})
|
||||
```
|
3
web/package-lock.json
generated
3
web/package-lock.json
generated
@ -33,8 +33,7 @@
|
||||
"@docusaurus/module-type-aliases": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0",
|
||||
"npm": "^8.19.2"
|
||||
"node": "^18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/autocomplete-core": {
|
||||
|
@ -39,8 +39,7 @@
|
||||
"@docusaurus/module-type-aliases": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0",
|
||||
"npm": "^8.19.2"
|
||||
"node": "^18.12.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -58,6 +58,7 @@ module.exports = {
|
||||
'integrations/css-frameworks',
|
||||
'deploying',
|
||||
'typescript',
|
||||
'guides/sending-emails'
|
||||
],
|
||||
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user