Skip to content

Commit 1d0f1c7

Browse files
committed
feat: allow basic auth for outbound SMTP API
1 parent 92a6d18 commit 1d0f1c7

File tree

3 files changed

+96
-4
lines changed

3 files changed

+96
-4
lines changed

app/views/api/index.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,36 @@ The current HTTP base URI path is: `BASE_URI`.
9898

9999
## Authentication
100100

101-
All endpoints require your [API key](https://forwardemail.net/my-account/security) to be set as the "username" value of the request's [Basic Authorization](https://en.wikipedia.org/wiki/Basic_access_authentication) header (with the exception of [Alias Contacts](#alias-contacts), [Alias Calendars](#alias-calendars), and [Alias Mailboxes](#alias-mailboxes) which use a [generated alias username and password](/faq#do-you-support-receiving-email-with-imap))..
101+
All endpoints require authentication using [Basic Authorization](https://en.wikipedia.org/wiki/Basic_access_authentication). We support two authentication methods:
102+
103+
### API Token Authentication (Recommended for most endpoints)
104+
105+
Set your [API key](https://forwardemail.net/my-account/security) as the "username" value with an empty password:
106+
107+
```sh
108+
curl BASE_URI/v1/account \
109+
-u API_TOKEN:
110+
```
111+
112+
Note the colon (`:`) after the API token – this indicates an empty password in Basic Auth format.
113+
114+
### Alias Credentials Authentication (For outbound email)
115+
116+
The [Create outbound SMTP email](#create-outbound-smtp-email) endpoint also supports authentication using your alias email address and a [generated alias password](/faq#do-you-support-receiving-email-with-imap):
117+
118+
```sh
119+
curl -X POST BASE_URI/v1/emails \
120+
-u "[email protected]:your_generated_password" \
121+
122+
-d "subject=Hello" \
123+
-d "text=Test email"
124+
```
125+
126+
This method is useful when sending emails from applications that already use SMTP credentials and makes migration from SMTP to our API seamless.
127+
128+
### Alias-Only Endpoints
129+
130+
[Alias Contacts](#alias-contacts-carddav), [Alias Calendars](#alias-calendars-caldav), [Alias Messages](#alias-messages-imappop3), and [Alias Folders](#alias-folders-imappop3) endpoints require alias credentials and do not support API token authentication.
102131

103132
Don't worry – examples are provided below for you if you're not sure what this is.
104133

@@ -484,6 +513,8 @@ You should either pass the single option of `raw` with your raw full email inclu
484513

485514
This API endpoint will automatically encode emojis for you if they are found in the headers (e.g. a subject line of `Subject: 🤓 Hello` gets converted to `Subject: =?UTF-8?Q?=F0=9F=A4=93?= Hello` automatically). Our goal was to make an extremely developer-friendly and dummy-proof email API.
486515

516+
**Authentication:** This endpoint supports both [API token authentication](#api-token-authentication-recommended-for-most-endpoints) and [alias credentials authentication](#alias-credentials-authentication-for-outbound-email). See the [Authentication](#authentication) section above for details.
517+
487518
> `POST /v1/emails`
488519
489520
| Body Parameter | Required | Type | Description |
@@ -514,7 +545,7 @@ This API endpoint will automatically encode emojis for you if they are found in
514545
| `date` | No | String or Date | An optional Date value that will be used if the Date header is missing after parsing, otherwise the current UTC string will be used if not set. The date header cannot be more than 30 days in advance of the current time. |
515546
| `list` | No | Object | An optional Object of `List-*` headers (see [Nodemailer's list headers](https://nodemailer.com/message/list-headers/)). |
516547

517-
> Example Request:
548+
> Example Request (API Token):
518549
519550
```sh
520551
curl -X POST BASE_URI/v1/emails \
@@ -525,7 +556,18 @@ curl -X POST BASE_URI/v1/emails \
525556
-d "text=test"
526557
```
527558

528-
> Example Request:
559+
> Example Request (Alias Credentials):
560+
561+
```sh
562+
curl -X POST BASE_URI/v1/emails \
563+
-u "alias@DOMAIN_NAME:GENERATED_PASSWORD" \
564+
-d "from=alias@DOMAIN_NAME" \
565+
-d "to=EMAIL" \
566+
-d "subject=test" \
567+
-d "text=test"
568+
```
569+
570+
> Example Request (Raw Email):
529571
530572
```sh
531573
curl -X POST BASE_URI/v1/emails \
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) Forward Email LLC
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
6+
const Boom = require('@hapi/boom');
7+
const basicAuth = require('basic-auth');
8+
9+
const policies = require('#helpers/policies');
10+
const aliasAuth = require('#controllers/api/v1/alias-auth');
11+
12+
/**
13+
* Dual authentication middleware that supports both API token and alias credentials
14+
* - If password is empty/not provided: treats username as API token
15+
* - If password is provided: treats username as alias email and validates with alias password
16+
*
17+
* @param {Object} ctx - Koa context
18+
* @param {Function} next - Next middleware function
19+
* @returns {Promise<void>}
20+
*/
21+
async function ensureApiTokenOrAliasAuth(ctx, next) {
22+
const creds = basicAuth(ctx.req);
23+
24+
if (!creds || !creds.name) {
25+
return ctx.throw(
26+
Boom.unauthorized(
27+
ctx.translate
28+
? ctx.translate('AUTHENTICATION_REQUIRED')
29+
: 'Authentication required. Use either API token or alias credentials.'
30+
)
31+
);
32+
}
33+
34+
// If password is empty/not provided, treat as API token
35+
// (this maintains backward compatibility with existing API token auth)
36+
if (!creds.pass || creds.pass === '') {
37+
ctx.logger.debug('Using API token authentication');
38+
return policies.ensureApiToken(ctx, next);
39+
}
40+
41+
// If password is provided, treat as alias credentials
42+
// (username should be in [email protected] format)
43+
ctx.logger.debug('Using alias authentication', {
44+
username: creds.name
45+
});
46+
return aliasAuth(ctx, next);
47+
}
48+
49+
module.exports = ensureApiTokenOrAliasAuth;

routes/api/v1/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const { boolean } = require('boolean');
1616
const _ = require('#helpers/lodash');
1717
const api = require('#controllers/api');
1818
const config = require('#config');
19+
const ensureApiTokenOrAliasAuth = require('#helpers/ensure-api-token-or-alias-auth');
1920
const policies = require('#helpers/policies');
2021
const rateLimit = require('#helpers/rate-limit');
2122
const web = require('#controllers/web');
@@ -59,7 +60,7 @@ const router = new Router({
5960

6061
router.post(
6162
'/emails',
62-
policies.ensureApiToken,
63+
ensureApiTokenOrAliasAuth,
6364
policies.checkVerifiedEmail,
6465
web.myAccount.ensureNotBanned,
6566
api.v1.enforcePaidPlan,

0 commit comments

Comments
 (0)