Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: arbitrary emails on server side and dynamic message configs #112

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
260 changes: 202 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@
<!-- /BADGES -->

<!-- DESCRIPTION/ -->
Adds email sending capability to a Nuxt.js app. Adds a server route, an injected variable, and uses nodemailer to send emails.
Adds email sending capabilities to a Nuxt.js app.
<!-- /DESCRIPTION -->

Does not work for static sites (via `nuxt generate`) because the module creates a server route.
Does not work for static sites (via `nuxt generate`) because emails are sent server-side.

<!-- INSTALL/ -->
## Install
Expand All @@ -69,15 +69,12 @@ $ yarn add nuxt-mail

## Usage

Add the module to the `modules` array in your `nuxt.config.js`. Note to add it to `modules` instead of `buildModules`, otherwise the server route will not be generated. We also have to install the [@nuxtjs/axios](https://www.npmjs.com/package/@nuxtjs/axios) module because it is used internally to call the server route:
Add the module to the `modules` array in your `nuxt.config.js`. Note that you need to add it to `modules` instead of `buildModules` because it generates a serverMiddleware that is called from the client.

```js
export default {
modules: [
'@nuxtjs/axios',
['nuxt-mail', {
message: {
to: '[email protected]',
},
smtp: {
host: "smtp.example.com",
port: 587,
Expand All @@ -86,9 +83,6 @@ export default {
],
// or use the top-level option:
mail: {
message: {
to: '[email protected]',
},
smtp: {
host: "smtp.example.com",
port: 587,
Expand All @@ -97,99 +91,253 @@ export default {
}
```

The `smtp` options are required and directly passed to [nodemailer](https://nodemailer.com/smtp/). Refer to their documentation for available options. Also, you have to pass at least `to`, `cc` or `bcc` via the `message` config. This has security reasons, this way the client cannot send emails from your SMTP server to arbitrary recipients. You can actually preconfigure the message via the `message` config, so if you always want to send emails with the same subject or from address, you can configure them here.

The module injects the `$mail` variable, which we now use to send emails:
The `smtp` options are required and directly passed to [nodemailer](https://nodemailer.com/smtp/). Refer to their documentation for available options. The module injects a `$mail` variable, which is used to send emails like so:

```js
// Inside a component
this.$mail.send({
this.$mail.send('config', {
from: 'John Doe',
subject: 'Incredible',
text: 'This is an incredible test message',
to: 'foo@@bar.de',
})
```

You can also directly call the generated `/mail/send` post route:
We will see in a minute what the `config` parameter does.

## Sending emails from the client

Sending emails has security implications, which means that server side and client side work a bit differently.

On the server side you can basically do anything you can also do with nodemailer, but you have to be careful. On the client side, you shouldn't pass recipient addresses like `to`, `cc`, and `bcc` from the client because that would allow an attacker to send emails to many users from your SMTP server. The client should only pass fields like `text` or `replyTo`.

To solve this problem, we define so-called **message configs** that are triggered by the client but are actually executed on the server. They can be plain message objects or functions with parameters returning a message object. You only pass the parameters to the configs that are really input from the client. Think of them like templates that have parameters and you trigger them via the client. This approach is similar to what [EmailJS](https://www.emailjs.com/) does.

You can also define multiple message configs depending on the use cases. To define message configs, set the `configs` property in your module config. Then you can reference the configs via the first parameter of `this.$mail.send`. Here is an example:

```js
// Inside a component
this.$axios.$post('/mail/send', {
from: 'John Doe',
subject: 'Incredible',
text: 'This is an incredible test message',
})
// nuxt.config.js

export default {
modules: [
['nuxt-mail', {
configs: {
contact: {
from: '[email protected]',
to: '[email protected]',
},
issues: { /* ... */ },
},
smtp: { /* ... */ },
}],
],
}
```

Note that the data are passed to [nodemailer](https://nodemailer.com/message/). Refer to the documentation for available config options.
This message config defines a simple contact form email. Note how we set `from` and `to` because it cannot be done via `this.$mail.send`. When setting an object as a message config, `nuxt-mail` will auto filter out `to`, `cc` and `bcc` for you so you are on the safe side!

```js
<template>
<form @submit.prevent="submit">
<label for="email">Email</label>
<input type="email" id="email" name="email" v-model="email" />

<label for="email">Text</label>
<textarea id="text" name="text" v-model="text" />

<button type="submit" name="submit">Send</button>
</form>
</template>

## Multiple message configs
<script>
export default {
data: () => ({
email: '',
text: '',
}),
methods: {
submit() {
return this.$mail.send('contact', {
replyTo: this.email,
text: this.text,
})
},
},
}
</script>
```

It is also possible to provide multiple message configurations by changing the `message` config into an array.
Great, that already works!

## Functions as message configs

You can also set a function as a message config. It allows you to do more complex logic in the message config, but you also have to be very careful because `nuxt-mail` won't filter out `to`, `cc` and `bcc` anymore. You are responsible for what is happening inside the function. So be sure that you only return the fields that are necessary and that an attacker cannot inject variables. Better do not use a spread operator here. The following example does the same as the one above:

```js
export default {
modules: [
'@nuxtjs/axios',
['nuxt-mail', {
message: [
{ name: 'contact', to: '[email protected]' },
{ name: 'support', to: '[email protected]' },
],
...
configs: {
contact: ({ replyTo, text }) => ({
from: '[email protected]',
to: '[email protected]',
replyTo,
text,
}),
},
smtp: { /* ... */ },
}],
],
}
```

Then you can reference the config like this:
## Sending multiple emails

You can send multiple emails via a message config as well as `this.$mail.send` by specifying an array as parameter or return value. `nuxt-mail` will detect it and send them one after another:

```js
this.$axios.$post('/mail/send', {
config: 'support',
from: 'John Doe',
subject: 'Incredible',
text: 'This is an incredible test message',
})
export default {
modules: [
['nuxt-mail', {
configs: {
contact: [
{
from: '[email protected]',
to: '[email protected]',
replyTo,
text,
},
{
from: '[email protected]',
to: '[email protected]',
replyTo,
text,
},
],
},
smtp: { /* ... */ },
}],
],
}
```

The same applies to `this.$mail.send`. You can pass an array of messages:

```
this.$mail.send('contact', [
{
from: '[email protected]',
text: 'foo bar',
},
{
from: '[email protected]',
text: 'foo bar',
},
])
</script>
```

Or via index (in which case you do not need the `name` property):
You can even pass an array to both a message config and `this.$mail.send` and it will apply each email from `this.$mail.send` to each message returned from the message config 🥳.

## Server side

Since Nuxt is a Vue-based framework, a lot of the user interaction is going on client-side. If you do want to run `this.$mail.send` from the server, you can do that too. Server-side execution does not require a message config, you can also send emails directly. Be careful though, do not just expose an interface to the public. It can be used for spamming! There are not too many use cases to run `this.$mail.send` from the server, but here is an example that sends an email to an admin when loading a page via `asyncData`. Here `$mail.send` is accessed from the [application context](https://nuxtjs.org/docs/internals-glossary/context/).

```js
this.$axios.$post('/mail/send', {
config: 1, // Resolves to 'support'
from: 'John Doe',
subject: 'Incredible',
text: 'This is an incredible test message',
})
<template>
<div />
</template>

<script>
export default {
asyncData: ({ $mail, req }) => $mail.send({
from: '[email protected]',
replyTo: req.body.email,
text: req.body.text,
to: '[email protected]',
}),
}
</script>
```

## Note about production use
Note that you do not have to pass the first config parameter anymore but you can pass the message directly. Still, you can use configs on the server if you want:

```js
<template>
<div />
</template>

<script>
export default {
asyncData: ({ $mail, req }) => $mail.send('contact', {
from: '[email protected]',
replyTo: req.body.email,
text: req.body.text,
to: '[email protected]',
}),
}
</script>
```

When you use `nuxt-mail` in production and you configured a reverse proxy that hides your localhost behind a domain, you need to tell `@nuxt/axios` which base URL you are using. Otherwise `nuxt-mail` won't find the send route. Refer to [@nuxt/axios options](https://axios.nuxtjs.org/options) on how to do that. The easiest option is to set the `API_URL` environment variable, or set something else in your `nuxt.config.js`:
If you want to handle form submission server-side, you could add [body-parser](https://www.npmjs.com/package/body-parser) to your server middlewares and then access `context.req.body` in `asyncData`.

```js
// nuxt.config.js

import bodyParser from 'body-parser'

export default {
axios: {
baseURL: process.env.BASE_URL,
},
serverMiddleware: [
bodyParser.urlencoded({ extended: false }),
],
}
```

Also, the module does not work for static sites (via `nuxt generate`) because the module creates a server route.
```js
<template>
<form method="POST">
<label for="email">Email</label>
<input type="email" id="email" name="email" v-model="email" />

## Setting up popular email services
<label for="text">Text</label>
<textarea id="text" name="text" v-model="text" />

### Gmail
<button type="submit" name="submit">Send</button>
</form>
</template>

<script>
export default {
asyncData: ({ $mail, req }) => {
if (req.body.submit) {
return $mail.send('contact', {
from: '[email protected]',
replyTo: req.body.email,
text: req.body.text,
to: '[email protected]',
})
}
}
}
</script>
```

## FAQ

### What about server middlewares?

You cannot access the application context from server middlewares (as far as I know, otherwise let me know). So, if you want to send emails from your custom REST API, you should use `nodemailer` directly.

### Can I access the Nuxt application context in a message config?

You can't, and the reason is that `this.$mail.send` internally calls a serverMiddleware route and those routes cannot access the context. In fact it is not really needed because on client side you can access the context from the calling function via `this`, and on the server side you can access it via the `context` parameter passed to the respective functions. In both cases, run your application logic and then pass the result to `$mail.send`. In case there are issues that are not covered here, feel free to open up an [issue](https://github.com/dword-design/nuxt-mail/issues).

### How to setup Gmail?

You have to setup an [app-specific password](https://myaccount.google.com/apppasswords) to log into the SMTP server. Then, add the following config to your `nuxt-mail` config:

```js
export default {
modules: [
'@nuxtjs/axios',
['nuxt-mail', {
// ...
smtp: {
Expand All @@ -206,10 +354,6 @@ export default {

Missing something? Add your service here via a [pull request](https://github.com/dword-design/nuxt-mail/pulls).

## Debugging mail errors

If the mail doesn't get sent, you can debug the error using the browser developer tools. If a `400` error is thrown (check out the console output), you can find the error message in the Network tab. For Chrome users, open the Network tab, then find the "send" request. Open it and select the "Response" tab. There it should show the error message. In most cases, it is related to authentication with the SMTP server.

## Open questions

### "Self signed certificate in certificate chain" error
Expand Down
12 changes: 5 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nuxt-mail",
"version": "3.0.23",
"description": "Adds email sending capability to a Nuxt.js app. Adds a server route, an injected variable, and uses nodemailer to send emails.",
"description": "Adds email sending capabilities to a Nuxt.js app.",
"keywords": [
"email",
"express",
Expand Down Expand Up @@ -38,24 +38,22 @@
},
"dependencies": {
"@dword-design/functions": "^4.0.0",
"axios": "^0.25.0",
"express": "^4.17.1",
"nodemailer": "^6.4.11",
"nuxt-push-plugins": "^2.0.0"
},
"devDependencies": {
"@dword-design/base": "^8.0.0",
"@dword-design/proxyquire": "^2.0.0",
"@dword-design/puppeteer": "^5.0.0",
"@dword-design/tester": "^2.0.0",
"@dword-design/tester-plugin-nodemailer-mock": "^1.0.0",
"@dword-design/tester-plugin-puppeteer": "^2.0.0",
"@nuxtjs/axios": "^5.13.1",
"axios": "^0.25.0",
"@dword-design/tester-plugin-tmp-dir": "^2.1.5",
"depcheck-package-name": "^2.0.0",
"nodemailer-mock": "^1.5.4",
"mailparser": "^3.4.0",
"nuxt": "^2.15.3",
"output-files": "^2.0.0",
"with-local-tmp-dir": "^4.0.0"
"smtp-server": "^3.9.0"
},
"engines": {
"node": ">=12"
Expand Down
Loading