Skip to content
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
129 changes: 83 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# 📨 MailForm

> The lightweight email service for contact forms and more!

This is basically a minimal self-hosted open source alternative to [Formspree](https://formspree.io/) and [SendGrid](https://sendgrid.com/).

Unlike other mail services (that often gives you an API key for backends), this self-hosted mail service is designed to be accessed directly from a frontend, but also offers you the option to use it as a mail service with configurable API keys.

### Features

- Access via API or HTML form with redirects
- Configurable CORS and Origin restriction
- ReCaptcha and hCaptcha support
- Custom rate limits for every target
- Optional API keys

### Planned features

- [ ] Email Templates
- [x] File Uploads for attachments
- [x] ReCaptcha and hCaptcha support

### Used frameworks & libraries

- [Express](https://expressjs.com/)
- [Typescript](https://www.typescriptlang.org/)
- [Nodemailer](https://nodemailer.com/about/)
Expand All @@ -26,7 +30,9 @@ Unlike other mail services (that often gives you an API key for backends), this
- [axios](https://github.com/axios/axios)

## 💽 Installation

### Docker

```shell
git clone https://github.com/Feuerhamster/mailform.git
cd mailform
Expand All @@ -38,7 +44,8 @@ docker run Feuerhamster/mailform
```

### Manually
*Requires NodeJS 16 or higher*

_Requires NodeJS 16 or higher_

```shell
git clone https://github.com/Feuerhamster/mailform.git
Expand All @@ -49,74 +56,96 @@ npm run start
```

## ⚙️Configuration

### Application

MailForm can be configured using environment variables.

**Environment variables:**

- `PORT` The port on which the application starts. If not provided, a random port will be selected.
- `TARGETS_DIR` Path to the directory with your target files. Default is `/targets` of the project root.
- `PROXY` A boolean that enables the "trust proxy" option of Express. **Enable this if you're using MailForm behind a reverse proxy like NGINX!** Default value is false.

### Targets

Targets are your different endpoints each with its own rate limits and smtp provider.
They are JSON files placed in the `/targets` directory.
They are JSON files placed in the `/targets` directory.

**Example target:**

```json
{
"smtp": "smtps://username:password@smtp.example.com",
"origin": "my-website.com",
"recipients": ["example@example.com"],
"rateLimit": {
"timespan": 300,
"requests": 1
},
"captcha": {
"provider": "hcaptcha",
"secret": "xxx"
}
"smtp": "smtps://username:password@smtp.example.com",
"origin": "my-website.com",
"recipients": ["example@example.com"],
"from": "no-reply@example.com",
"rateLimit": {
"timespan": 300,
"requests": 1
},
"captcha": {
"provider": "hcaptcha",
"secret": "xxx"
}
}
```

**Available fields:**
- `smtp` *required* | A valid SMTP(S) url.
- `origin` *optional* | A HTTP origin that is used for CORS and to restrict access. Default is * if not set.
- `recipients` *required* | An array of email addresses which should receive the email.
- `from` *optional* | The "from" field of an email. This is used as fallback if no "from" is provided in the request.
- `subjectPrefix` *optional* | A target-wide prefix for the email subject.
- `key` *optional* | A string used as API key if you want to restrict access to this target.
- `redirect` *optional*:
- `success` *optional*: A valid relative or absolute URL to redirect the user if the mail was sent successful.
- `error` *optional*: A valid relative or absolute URL to redirect the user if the mail can't be sent due to an error.
- `rateLimit` *required*:
- `timespan` *required* | Timespan (in seconds) for the rate limiter to reset.
- `requests` *required* | Allowed amount of requests in the given timespan.
- `captcha` *optional*:
- `provider` *required if captcha* | The captcha provider ("recaptcha" or "hcaptcha").
- `secret` *required if captcha* | Secret key for your captcha.

**Required**

- `smtp` | A valid SMTP(S) url.
- `recipients` | An array of email addresses which should receive the email.
- `from` | The default "from" address used as fallback when no `from` is provided in the request. A `from` supplied in the request overrides this value.
- `rateLimit.timespan` | Timespan (in seconds) for the rate limiter to reset.
- `rateLimit.requests` | Allowed amount of requests in the given timespan.

**Optional**

- `origin` | A HTTP origin that is used for CORS and to restrict access. Default is \* if not set.
- `replyTo` | The "replyTo" field of an email. If not provided in the request, it defaults to the effective `from` that is used for the email.
- `subjectPrefix` | A target-wide prefix for the email subject.
- `key` | A string used as API key if you want to restrict access to this target.
- `redirect.success` | A valid relative or absolute URL to redirect the user if the mail was sent successful.
- `redirect.error` | A valid relative or absolute URL to redirect the user if the mail can't be sent due to an error.
- `captcha` | Enables captcha verification for the target.
- `captcha.provider` _required if captcha_ | The captcha provider ("recaptcha" or "hcaptcha").
- `captcha.secret` _required if captcha_ | Secret key for your captcha.

For the exact validations of the fields please see here: [target.ts](/src/models/target.ts)

## 📫 Usage

### Fields

Whether as formular data or json, the fields are the same.

- `from` *optional* | The email address of the sender. If this filed is not set, the "from" field of your target will be used.
- `firstName` *optional* | A classic first name filed which will be attached to the "from" field of the email.
- `lastName` *optional* | A classic last name filed which will be attached to the "from" field of the email.
- `subjectPrefix` *optional* | A Prefix for the email subject.
- `subject` *required* | The email subject.
- `body` *required* | The email body (supports HTML).

- `g-recaptcha-response` *only required if target use captcha* | Field for ReCaptcha response.
- `h-captcha-response` *only required if target use captcha* | Field for hCaptcha response.
**Required**

- `subject` | The email subject.
- `body` | The email body (supports HTML).

**Optional**

- `from` | The email address of the sender. If this field is not set, the target's `from` will be used. If set, it overrides the target's `from`.
- `replyTo` | Reply-to address. If not set, it defaults to the effective `from`.
- `firstName` | A classic first name filed which will be attached to the "from" field of the email.
- `lastName` | A classic last name filed which will be attached to the "from" field of the email.
- `subjectPrefix` | A Prefix for the email subject.

**Only required if the target uses captcha**

- `g-recaptcha-response` | Field for ReCaptcha response.
- `h-captcha-response` | Field for hCaptcha response.

For the exact validations of the fields please see here: [posts.ts](/src/models/post.ts)

**Important info:** If a redirect is configured for your target, it will always return the redirect, even if you make an API call.
If no redirect is set, http status codes will be returned.

### Captchas

MailForm supports both [ReCaptcha](https://www.google.com/recaptcha/) and [hCaptcha](https://www.hcaptcha.com/).

To use captchas, you have to configure it in your target.
Expand All @@ -128,28 +157,34 @@ If you use an API request, you have to fill it manually.
### HTML Form

**Example html form:**

```html
<form method="post" action="https://mailform.yourserver.com/your-target-file-name">
<input type="email" name="from" placeholder="Sender's email address"/>
<input type="text" name="firstName" placeholder="First name" />
<input type="text" name="lastName" placeholder="Last name" />
<input type="hidden" name="subjectPrefix" value="[App-Question] " />
<input type="text" name="subject" placeholder="Subject" />
<div class="g-recaptcha" data-sitekey="your_site_key"></div>
<textarea name="body" placeholder="Your message"></textarea>
<form
method="post"
action="https://mailform.yourserver.com/your-target-file-name"
>
<input type="email" name="from" placeholder="Sender's email address" />
<input type="text" name="firstName" placeholder="First name" />
<input type="text" name="lastName" placeholder="Last name" />
<input type="hidden" name="subjectPrefix" value="[App-Question] " />
<input type="text" name="subject" placeholder="Subject" />
<div class="g-recaptcha" data-sitekey="your_site_key"></div>
<textarea name="body" placeholder="Your message"></textarea>
</form>
```

To work properly, you may want to configure a redirect in the target.

### API

Simply make a request to `/:target` (replace with your target's file name).
If you have set an API key, add the HTTP Authorization header with type `Bearer` and then the key.
Make sure to also use the right origin (if not set automatically because the request is from a backend).

> ⚠ Since the file upload feature got added, there is an Issue with `application/json`. Please use multipart form or form urlencoded for API requests. I am working on a rewrite where this gets fixed.

**Example request:**
**Example request:**

```http request
POST https://mailform.yourserver.com/your-target-file-name
Origin: your-configured-origin.com
Expand All @@ -164,13 +199,15 @@ Authorization: Bearer your-optional-api-key
```

**Possible status codes:**

- `200` Email was successfully sent.
- `401` Authentication failed: API key not present or wrong.
- `403` Forbidden because of wrong origin header.
- `404` Target not found.
- `500` Sending the email failed.

## 👋 Contribution

Feel free to create issues and pull requests if you want!

Please keep up with the code style and discuss new features beforehand with the project owner.
37 changes: 19 additions & 18 deletions src/@types/target.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
export interface Target {
smtp: string;
origin: string;
recipients: string[];
from?: string;
subjectPrefix?: string;
redirect?: Redirects;
key?: string;
rateLimit?: TargetRateLimit;
captcha?: TargetCaptchaOptions
smtp: string;
origin: string;
recipients: string[];
replyTo?: string;
from: string;
subjectPrefix?: string;
redirect?: Redirects;
key?: string;
rateLimit?: TargetRateLimit;
captcha?: TargetCaptchaOptions;
}

export interface Redirects {
success?: string;
error?: string
success?: string;
error?: string;
}

export interface TargetRateLimit {
timespan: number;
requests: number;
timespan: number;
requests: number;
}

export interface TargetCaptchaOptions {
provider: CaptchaProvider;
secret: string;
provider: CaptchaProvider;
secret: string;
}

export enum CaptchaProvider {
RECAPTCHA = "recaptcha",
HCAPTCHA = "hcaptcha"
}
RECAPTCHA = "recaptcha",
HCAPTCHA = "hcaptcha",
}
Loading