@rapidmx/autodiscover-plugin 1.0.0-beta.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/lib/AutodiscoverXml.js +108 -11
- package/dist/lib/AutodiscoverXml.js.map +1 -1
- package/dist/lib/BaseAutodiscoverRoute.js +77 -16
- package/dist/lib/BaseAutodiscoverRoute.js.map +1 -1
- package/dist/lib/index.js +9 -12
- package/dist/lib/index.js.map +1 -1
- package/dist/types/AutodiscoverXml.d.ts +3 -4
- package/dist/types/BaseAutodiscoverRoute.d.ts +24 -4
- package/dist/types/index.d.ts +9 -12
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/RapidMX/autodiscover/actions/workflows/build.yml)
|
|
4
4
|
[](https://coveralls.io/github/RapidMX/autodiscover?branch=main)
|
|
5
|
-
[](https://www.npmjs.com/package/@rapidmx/autodiscover)
|
|
5
|
+
[](https://www.npmjs.com/package/@rapidmx/autodiscover-plugin)
|
|
6
6
|
|
|
7
7
|
Autodiscover support for a [RapidMX](https://rapidmx.io) mail server. Lets a real mail client find this
|
|
8
|
-
deployment's [`@rapidmx/activesync`](https://github.com/RapidMX/activesync) (EAS) and
|
|
9
|
-
[`@rapidmx/mapi`](https://github.com/RapidMX/mapi) server URLs from just an email address: classic
|
|
8
|
+
deployment's [`@rapidmx/activesync-plugin`](https://github.com/RapidMX/activesync) (EAS) and
|
|
9
|
+
[`@rapidmx/mapi-plugin`](https://github.com/RapidMX/mapi) server URLs from just an email address: classic
|
|
10
10
|
POX (`POST /autodiscover/autodiscover.xml`, serving either the EAS-only MobileSync response or, when a real
|
|
11
11
|
Outlook desktop client requests it via `AcceptableResponseSchema`, an Outlook/EXCH response pointing at the
|
|
12
12
|
MAPI/HTTP endpoint) and the modern JSON variant Microsoft calls "Autodiscover v2"
|
|
@@ -32,20 +32,20 @@ server is mounted — an ops/deployment task, not something this package configu
|
|
|
32
32
|
* Mailbox resolution against both `Mailbox.primarySmtpAddress` and `Mailbox.aliasAddresses`
|
|
33
33
|
* Deliberately unauthenticated by design — reveals only deployment-wide server URLs, never a per-mailbox
|
|
34
34
|
secret
|
|
35
|
-
* MongoDB and SQL persistence backends (`@rapidmx/autodiscover/mongo`, `@rapidmx/autodiscover/sql`)
|
|
35
|
+
* MongoDB and SQL persistence backends (`@rapidmx/autodiscover-plugin/mongo`, `@rapidmx/autodiscover-plugin/sql`)
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
39
39
|
### NPM
|
|
40
40
|
|
|
41
41
|
```
|
|
42
|
-
npm i @rapidmx/autodiscover
|
|
42
|
+
npm i @rapidmx/autodiscover-plugin
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### Yarn
|
|
46
46
|
|
|
47
47
|
```
|
|
48
|
-
yarn add @rapidmx/autodiscover
|
|
48
|
+
yarn add @rapidmx/autodiscover-plugin
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
## Requirements
|
|
@@ -62,11 +62,11 @@ The following peer dependencies are required:
|
|
|
62
62
|
|
|
63
63
|
## Usage
|
|
64
64
|
|
|
65
|
-
Mount `AutodiscoverRouteMongo`/`AutodiscoverRouteSQL` (from `@rapidmx/autodiscover/mongo` or `/sql`) with a
|
|
65
|
+
Mount `AutodiscoverRouteMongo`/`AutodiscoverRouteSQL` (from `@rapidmx/autodiscover-plugin/mongo` or `/sql`) with a
|
|
66
66
|
one-line subclass supplying your deployment's EAS and MAPI URLs:
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
|
-
import { AutodiscoverRouteMongo } from "@rapidmx/autodiscover/mongo";
|
|
69
|
+
import { AutodiscoverRouteMongo } from "@rapidmx/autodiscover-plugin/mongo";
|
|
70
70
|
import { RouteDecorators } from "@rapidrest/service-core";
|
|
71
71
|
const { Route } = RouteDecorators;
|
|
72
72
|
|
|
@@ -11,15 +11,115 @@
|
|
|
11
11
|
* This follows the same "hand-build the wire format precisely, no library" approach Phase 2's WBXML codec
|
|
12
12
|
* already took, just for text XML instead of binary.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/** Reads an XML name (`[\w:.-]*`) starting at `start`, returning it lower-cased plus the index just past it. */
|
|
15
|
+
function readTagName(xml, start) {
|
|
16
|
+
let end = start;
|
|
17
|
+
while (end < xml.length && /[\w:.-]/.test(xml[end])) {
|
|
18
|
+
end++;
|
|
19
|
+
}
|
|
20
|
+
return { name: xml.slice(start, end).toLowerCase(), end };
|
|
21
|
+
}
|
|
22
|
+
/** Whether a (lower-cased) tag name is `localName`, optionally namespace-prefixed (`ns:localName`). */
|
|
23
|
+
function isElement(name, localName) {
|
|
24
|
+
return name === localName || name.endsWith(`:${localName}`);
|
|
25
|
+
}
|
|
26
|
+
const COMMENT_OPEN = "<!--";
|
|
27
|
+
const COMMENT_CLOSE = "-->";
|
|
28
|
+
const CDATA_OPEN = "<![CDATA[";
|
|
29
|
+
const CDATA_CLOSE = "]]>";
|
|
30
|
+
/**
|
|
31
|
+
* If a `<!-- -->` comment or `<![CDATA[...]]>` section starts at `lt`, returns the index just past its end
|
|
32
|
+
* (`-1` when it's unterminated) plus the CDATA section's verbatim content; `undefined` for anything else.
|
|
33
|
+
*/
|
|
34
|
+
function skipCommentOrCdata(xml, lt) {
|
|
35
|
+
const comment = xml.startsWith(COMMENT_OPEN, lt);
|
|
36
|
+
if (!comment && !xml.startsWith(CDATA_OPEN, lt)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const [open, close] = comment ? [COMMENT_OPEN, COMMENT_CLOSE] : [CDATA_OPEN, CDATA_CLOSE];
|
|
40
|
+
const closeAt = xml.indexOf(close, lt + open.length);
|
|
41
|
+
if (closeAt === -1) {
|
|
42
|
+
return { end: -1 };
|
|
43
|
+
}
|
|
44
|
+
return { end: closeAt + close.length, cdata: comment ? undefined : xml.slice(lt + open.length, closeAt) };
|
|
45
|
+
}
|
|
46
|
+
/** Returns the index of the first non-whitespace character at or after `start`. */
|
|
47
|
+
function skipWhitespace(xml, start) {
|
|
48
|
+
let end = start;
|
|
49
|
+
while (end < xml.length && /\s/.test(xml[end])) {
|
|
50
|
+
end++;
|
|
51
|
+
}
|
|
52
|
+
return end;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns the text content of the first `<localName>text</localName>` element (namespace prefix and attributes
|
|
56
|
+
* allowed, case-insensitive, whitespace allowed before the closing tag's `>`) whose content is plain text:
|
|
57
|
+
* entities are decoded, `<![CDATA[...]]>` sections are unwrapped verbatim and `<!-- -->` comments are dropped.
|
|
58
|
+
* Comments/CDATA outside such an element are skipped whole, so a commented-out element never matches. A single
|
|
59
|
+
* forward, linear-time scan - deliberately NOT a regex: this runs on an unauthenticated request body, and the
|
|
60
|
+
* previous `\s*([^<]*?)\s*<\/...>` pattern backtracked cubically on an unterminated element padded with
|
|
61
|
+
* whitespace. Every `indexOf` below starts past the previous one, so no character is rescanned more than a
|
|
62
|
+
* constant number of times.
|
|
63
|
+
*/
|
|
64
|
+
function extractElementText(xml, localName) {
|
|
65
|
+
const target = localName.toLowerCase();
|
|
66
|
+
let pos = 0;
|
|
67
|
+
while ((pos = xml.indexOf("<", pos)) !== -1) {
|
|
68
|
+
const special = skipCommentOrCdata(xml, pos);
|
|
69
|
+
if (special) {
|
|
70
|
+
if (special.end === -1) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
pos = special.end;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const open = readTagName(xml, pos + 1);
|
|
77
|
+
pos = open.end;
|
|
78
|
+
if (!isElement(open.name, target)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const gt = xml.indexOf(">", pos);
|
|
82
|
+
if (gt === -1) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
pos = gt + 1;
|
|
86
|
+
if (xml[gt - 1] === "/") {
|
|
87
|
+
continue; // Self-closing, so no text content.
|
|
88
|
+
}
|
|
89
|
+
const parts = [];
|
|
90
|
+
let lt;
|
|
91
|
+
while ((lt = xml.indexOf("<", pos)) !== -1) {
|
|
92
|
+
parts.push(decodeXmlEntities(xml.slice(pos, lt)));
|
|
93
|
+
const special = skipCommentOrCdata(xml, lt);
|
|
94
|
+
if (!special) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (special.end === -1) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
parts.push(special.cdata ?? "");
|
|
101
|
+
pos = special.end;
|
|
102
|
+
}
|
|
103
|
+
if (lt === -1) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (xml[lt + 1] === "/") {
|
|
107
|
+
const close = readTagName(xml, lt + 2);
|
|
108
|
+
if (isElement(close.name, target) && xml[skipWhitespace(xml, close.end)] === ">") {
|
|
109
|
+
return parts.join("");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// A child element or a mismatched close tag, so not plain text content: resume the outer scan here.
|
|
113
|
+
pos = lt;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
15
117
|
/**
|
|
16
|
-
* Extracts the request's `<EMailAddress>` text content via a small,
|
|
118
|
+
* Extracts the request's `<EMailAddress>` text content via a small, linear-time tag scan rather than a DOM
|
|
17
119
|
* parse. Never resolves entities/DTDs - only the literal text between the open/close tags is ever inspected.
|
|
18
120
|
*/
|
|
19
121
|
export function extractEmailAddress(xml) {
|
|
20
|
-
|
|
21
|
-
const value = match?.[1]?.trim();
|
|
22
|
-
return value ? decodeXmlEntities(value) : undefined;
|
|
122
|
+
return extractElementText(xml, "EMailAddress")?.trim() || undefined;
|
|
23
123
|
}
|
|
24
124
|
/** Escapes the 5 XML-predefined-entity characters for safe inclusion as element text content. */
|
|
25
125
|
export function escapeXml(value) {
|
|
@@ -38,19 +138,16 @@ function decodeXmlEntities(value) {
|
|
|
38
138
|
.replace(/'/g, "'")
|
|
39
139
|
.replace(/&/g, "&");
|
|
40
140
|
}
|
|
41
|
-
const ACCEPTABLE_RESPONSE_SCHEMA_PATTERN = /<[\w:]*AcceptableResponseSchema[^>]*>\s*([^<]*?)\s*<\/[\w:]*AcceptableResponseSchema>/i;
|
|
42
141
|
/** The `AcceptableResponseSchema` value a real Outlook desktop client sends to request the Outlook/EXCH
|
|
43
142
|
* response shape (`buildOutlookSuccessXml`) instead of the EAS-only MobileSync one - confirmed against
|
|
44
143
|
* `[MS-OXDSCLI]`'s own Autodiscover Response XSD, whose target namespace is exactly this value. */
|
|
45
144
|
export const OUTLOOK_RESPONSE_SCHEMA = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
|
|
46
145
|
/**
|
|
47
|
-
* Extracts the request's `<AcceptableResponseSchema>` text content via the same
|
|
48
|
-
*
|
|
49
|
-
* endpoint).
|
|
146
|
+
* Extracts the request's `<AcceptableResponseSchema>` text content via the same linear-time tag scan as
|
|
147
|
+
* `extractEmailAddress` - never a DOM parse, same rationale (unauthenticated, internet-facing endpoint).
|
|
50
148
|
*/
|
|
51
149
|
export function extractAcceptableResponseSchema(xml) {
|
|
52
|
-
|
|
53
|
-
return match?.[1]?.trim() || undefined;
|
|
150
|
+
return extractElementText(xml, "AcceptableResponseSchema")?.trim() || undefined;
|
|
54
151
|
}
|
|
55
152
|
/**
|
|
56
153
|
* Builds a real `mobilesync:Response` success document per `[MS-ASCMD]`'s `AutodiscoverMobileSync.xsd` -
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutodiscoverXml.js","sourceRoot":"","sources":["../../src/AutodiscoverXml.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAE/E;;;;;;;;GAQG;AAEH,MAAM,
|
|
1
|
+
{"version":3,"file":"AutodiscoverXml.js","sourceRoot":"","sources":["../../src/AutodiscoverXml.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,14 +15,20 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
15
15
|
// SPDX-License-Identifier: MPL-2.0
|
|
16
16
|
///////////////////////////////////////////////////////////////////////////////
|
|
17
17
|
import { ObjectDecorators } from "@rapidrest/core";
|
|
18
|
-
import { RepoUtils, RouteDecorators } from "@rapidrest/service-core";
|
|
18
|
+
import { ModelUtils, RepoUtils, RouteDecorators } from "@rapidrest/service-core";
|
|
19
19
|
import { buildOutlookSuccessXml, buildPoxSuccessXml, extractAcceptableResponseSchema, extractEmailAddress, OUTLOOK_RESPONSE_SCHEMA, } from "./AutodiscoverXml.js";
|
|
20
20
|
import { PluginRegistry } from "@rapidmx/restapi";
|
|
21
21
|
const { Config, Init, Logger } = ObjectDecorators;
|
|
22
22
|
/** The plugin packages whose endpoints Autodiscover advertises. */
|
|
23
|
-
export const ACTIVESYNC_PLUGIN = "@rapidmx/activesync";
|
|
24
|
-
export const MAPI_PLUGIN = "@rapidmx/mapi";
|
|
23
|
+
export const ACTIVESYNC_PLUGIN = "@rapidmx/activesync-plugin";
|
|
24
|
+
export const MAPI_PLUGIN = "@rapidmx/mapi-plugin";
|
|
25
25
|
const { Get, Param, Post, Query, Request, Response } = RouteDecorators;
|
|
26
|
+
/** The largest POX request body (in bytes) `pox()` will scan; anything bigger gets a `413`. */
|
|
27
|
+
export const MAX_POX_BODY_BYTES = 16 * 1024;
|
|
28
|
+
/** The longest email address looked up (RFC 5321's 254-character forward-path limit). */
|
|
29
|
+
export const MAX_ADDRESS_LENGTH = 254;
|
|
30
|
+
/** A single plain `local@domain` address: nothing the search query parser could read as an operator or a list. */
|
|
31
|
+
const PLAIN_ADDRESS_PATTERN = /^[^\s@(),"\\]+@[^\s@(),"\\]+$/;
|
|
26
32
|
/**
|
|
27
33
|
* Abstract base for the two Autodiscover endpoints a real mail client uses to find this deployment's EAS
|
|
28
34
|
* server URL from just an email address - classic POX (`POST /autodiscover/autodiscover.xml`, per
|
|
@@ -51,7 +57,7 @@ const { Get, Param, Post, Query, Request, Response } = RouteDecorators;
|
|
|
51
57
|
*
|
|
52
58
|
* `mailboxClass` is supplied by the Mongo/SQL concrete subclasses following the exact one-line-per-backend
|
|
53
59
|
* pattern used throughout this library. The advertised URLs are built from the `mail:autodiscover:public_url`
|
|
54
|
-
* setting, and each protocol is only advertised while its plugin (`@rapidmx/activesync`, `@rapidmx/mapi`) is
|
|
60
|
+
* setting, and each protocol is only advertised while its plugin (`@rapidmx/activesync-plugin`, `@rapidmx/mapi-plugin`) is
|
|
55
61
|
* loaded, so a client is never pointed at an endpoint this deployment doesn't serve.
|
|
56
62
|
*
|
|
57
63
|
* @author Jean-Philippe Steinmetz
|
|
@@ -74,31 +80,75 @@ export class BaseAutodiscoverRoute {
|
|
|
74
80
|
* Builds the query value used to match `Mailbox.aliasAddresses` against the given address. See
|
|
75
81
|
* `BaseMailIngestRoute.aliasQueryValue()`'s identical doc comment - same Mongo-array vs.
|
|
76
82
|
* SQL-`simple-json`-column backend split, same override point (`AutodiscoverRouteSQL` overrides this
|
|
77
|
-
* identically to `MailIngestRouteSQL`).
|
|
83
|
+
* identically to `MailIngestRouteSQL`). The base form is a `ModelUtils.literal()` exact match.
|
|
78
84
|
*/
|
|
79
85
|
aliasQueryValue(address) {
|
|
80
|
-
return address;
|
|
86
|
+
return ModelUtils.literal(address);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalizes `publicUrl` into a base URL (origin plus path, no trailing slash), or `undefined` when it's
|
|
90
|
+
* unset or unsafe to advertise: it must parse, use `https:` (plain `http:` is only tolerated for a loopback
|
|
91
|
+
* host, for local development), and carry no credentials, query string or fragment - any of which would
|
|
92
|
+
* produce a broken or insecure endpoint URL once a path is appended.
|
|
93
|
+
*/
|
|
94
|
+
get baseUrl() {
|
|
95
|
+
const value = this.publicUrl.trim();
|
|
96
|
+
if (!value) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
let url;
|
|
100
|
+
try {
|
|
101
|
+
url = new URL(value);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
107
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
// Checked on the raw string: `URL.search`/`hash` are empty for a bare trailing `?`/`#`.
|
|
111
|
+
if (value.includes("?") || value.includes("#") || url.username || url.password) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
81
115
|
}
|
|
82
116
|
endpointUrl(plugin, path) {
|
|
83
|
-
|
|
117
|
+
const base = this.baseUrl;
|
|
118
|
+
if (!base || !PluginRegistry.isActive(plugin)) {
|
|
84
119
|
return undefined;
|
|
85
120
|
}
|
|
86
|
-
return `${
|
|
121
|
+
return `${base}${path}`;
|
|
87
122
|
}
|
|
88
123
|
async init() {
|
|
89
|
-
if (!this.publicUrl) {
|
|
124
|
+
if (!this.publicUrl.trim()) {
|
|
90
125
|
this.logger?.warn("Autodiscover has no mail:autodiscover:public_url set, so it can't point clients at any protocol.");
|
|
91
126
|
}
|
|
127
|
+
else if (!this.baseUrl) {
|
|
128
|
+
this.logger?.warn("Autodiscover's mail:autodiscover:public_url is not a valid https:// URL without a query string or " +
|
|
129
|
+
"fragment, so it can't point clients at any protocol.");
|
|
130
|
+
}
|
|
92
131
|
this.mailboxRepo = await this._objectFactory.newInstance(RepoUtils, {
|
|
93
132
|
name: this.mailboxClass.name,
|
|
94
133
|
args: [this.mailboxClass],
|
|
95
134
|
});
|
|
96
135
|
}
|
|
97
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Normalizes an unauthenticated caller's email value into a single plain address, or `undefined` when it isn't
|
|
138
|
+
* one. service-core's search query parser interprets `op(value)` strings (`like(*)`, `regex(^a)`, `in(a,b)`,
|
|
139
|
+
* `ne(x)`, ...), so an arbitrary value reaching `find()` would let an anonymous caller enumerate the mailbox
|
|
140
|
+
* directory; rejecting whitespace, parentheses, commas, quotes and backslashes (plus RFC 5321's 254-character
|
|
141
|
+
* length cap) up front means no query is ever issued for such a value.
|
|
142
|
+
*/
|
|
143
|
+
normalizeAddress(email) {
|
|
98
144
|
const address = email.trim().toLowerCase();
|
|
145
|
+
return address.length <= MAX_ADDRESS_LENGTH && PLAIN_ADDRESS_PATTERN.test(address) ? address : undefined;
|
|
146
|
+
}
|
|
147
|
+
/** Looks up a mailbox by an already-`normalizeAddress()`ed address, always as a `ModelUtils.literal()` value. */
|
|
148
|
+
async resolveMailbox(address) {
|
|
99
149
|
const [byPrimary, byAlias] = await Promise.all([
|
|
100
|
-
this.mailboxRepo.find({ primarySmtpAddress: address }, { ignoreACL: true, limit: 1 }),
|
|
101
|
-
this.mailboxRepo.find({ aliasAddresses: this.aliasQueryValue(address) }, { ignoreACL: true, limit: 1 }),
|
|
150
|
+
this.mailboxRepo.find({ primarySmtpAddress: ModelUtils.literal(address), limit: 1 }, { ignoreACL: true, limit: 1 }),
|
|
151
|
+
this.mailboxRepo.find({ aliasAddresses: this.aliasQueryValue(address), limit: 1 }, { ignoreACL: true, limit: 1 }),
|
|
102
152
|
]);
|
|
103
153
|
return byPrimary[0] ?? byAlias[0];
|
|
104
154
|
}
|
|
@@ -118,8 +168,15 @@ export class BaseAutodiscoverRoute {
|
|
|
118
168
|
res.status(500).send();
|
|
119
169
|
return;
|
|
120
170
|
}
|
|
171
|
+
// A real Autodiscover request is well under 1KB; refuse to even scan anything larger on this
|
|
172
|
+
// unauthenticated endpoint.
|
|
173
|
+
if (req.rawBody && req.rawBody.length > MAX_POX_BODY_BYTES) {
|
|
174
|
+
res.status(413).send();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
121
177
|
const body = req.rawBody ? req.rawBody.toString("utf-8") : "";
|
|
122
|
-
const
|
|
178
|
+
const extracted = extractEmailAddress(body);
|
|
179
|
+
const email = extracted ? this.normalizeAddress(extracted) : undefined;
|
|
123
180
|
if (!email) {
|
|
124
181
|
res.status(400).send();
|
|
125
182
|
return;
|
|
@@ -136,9 +193,11 @@ export class BaseAutodiscoverRoute {
|
|
|
136
193
|
res.status(404).send();
|
|
137
194
|
return;
|
|
138
195
|
}
|
|
196
|
+
// The display name is never disclosed to this anonymous caller (see the class doc): the address they
|
|
197
|
+
// already supplied stands in for it, which keeps the Outlook schema's required `DisplayName` populated.
|
|
139
198
|
const xml = outlook
|
|
140
|
-
? buildOutlookSuccessXml({ emailAddress: email, displayName:
|
|
141
|
-
: buildPoxSuccessXml({ emailAddress: email, displayName:
|
|
199
|
+
? buildOutlookSuccessXml({ emailAddress: email, displayName: email, mapiUrl: url })
|
|
200
|
+
: buildPoxSuccessXml({ emailAddress: email, displayName: email, easUrl: url });
|
|
142
201
|
res.setHeader("Content-Type", "application/xml; charset=utf-8").status(200).send(xml);
|
|
143
202
|
}
|
|
144
203
|
/**
|
|
@@ -160,7 +219,9 @@ export class BaseAutodiscoverRoute {
|
|
|
160
219
|
});
|
|
161
220
|
return;
|
|
162
221
|
}
|
|
163
|
-
|
|
222
|
+
// The router has already percent-decoded path params; decoding again would throw (-> 500) on a literal `%`.
|
|
223
|
+
const address = this.normalizeAddress(email);
|
|
224
|
+
const mailbox = address ? await this.resolveMailbox(address) : undefined;
|
|
164
225
|
if (!mailbox) {
|
|
165
226
|
res.status(404).json({
|
|
166
227
|
ErrorCode: "UserNotFound",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseAutodiscoverRoute.js","sourceRoot":"","sources":["../../src/BaseAutodiscoverRoute.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,
|
|
1
|
+
{"version":3,"file":"BaseAutodiscoverRoute.js","sourceRoot":"","sources":["../../src/BaseAutodiscoverRoute.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/index.js
CHANGED
|
@@ -10,18 +10,15 @@
|
|
|
10
10
|
* exported from this package's `./mongo`/`./sql` subpaths instead, alongside every other entity/route/job this
|
|
11
11
|
* library defines - see `src/eas/index.ts`'s identical doc comment for the same convention.
|
|
12
12
|
*
|
|
13
|
-
* A deployment mounts Autodiscover
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* protected readonly mapiUrl = "https://mail.example.com/mapi/emsmdb";
|
|
23
|
-
* }
|
|
24
|
-
* ```
|
|
13
|
+
* A deployment mounts Autodiscover by loading the ready-to-mount `AutodiscoverRouteMongo`/`AutodiscoverRouteSQL`
|
|
14
|
+
* class from this plugin's `./mongo`/`./sql` entry point directly - no subclass or URLs to supply, since
|
|
15
|
+
* `BaseAutodiscoverRoute` builds the EAS (`easUrl`) and MAPI (`mapiUrl`) endpoint URLs itself from the
|
|
16
|
+
* `mail:autodiscover:public_url` config setting (also exposed as this plugin's own "Public server URL"
|
|
17
|
+
* admin-console setting - see `package.json`'s `rapidmx.plugin.settings`), and only advertises each protocol
|
|
18
|
+
* while its own plugin (`@rapidmx/activesync-plugin`, `@rapidmx/mapi-plugin`) is loaded. Set
|
|
19
|
+
* `mail:autodiscover:public_url` to this deployment's externally-reachable `https://` base URL (e.g.
|
|
20
|
+
* `https://mail.example.com`) for either endpoint to be advertised at all - see `BaseAutodiscoverRoute`'s own
|
|
21
|
+
* doc comment for the exact validation rules.
|
|
25
22
|
*/
|
|
26
23
|
export * from "./AutodiscoverXml.js";
|
|
27
24
|
export * from "./BaseAutodiscoverRoute.js";
|
package/dist/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAE/E
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;GAiBG;AACH,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extracts the request's `<EMailAddress>` text content via a small,
|
|
2
|
+
* Extracts the request's `<EMailAddress>` text content via a small, linear-time tag scan rather than a DOM
|
|
3
3
|
* parse. Never resolves entities/DTDs - only the literal text between the open/close tags is ever inspected.
|
|
4
4
|
*/
|
|
5
5
|
export declare function extractEmailAddress(xml: string): string | undefined;
|
|
@@ -15,9 +15,8 @@ export interface AutodiscoverPoxSuccess {
|
|
|
15
15
|
* `[MS-OXDSCLI]`'s own Autodiscover Response XSD, whose target namespace is exactly this value. */
|
|
16
16
|
export declare const OUTLOOK_RESPONSE_SCHEMA = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
|
|
17
17
|
/**
|
|
18
|
-
* Extracts the request's `<AcceptableResponseSchema>` text content via the same
|
|
19
|
-
*
|
|
20
|
-
* endpoint).
|
|
18
|
+
* Extracts the request's `<AcceptableResponseSchema>` text content via the same linear-time tag scan as
|
|
19
|
+
* `extractEmailAddress` - never a DOM parse, same rationale (unauthenticated, internet-facing endpoint).
|
|
21
20
|
*/
|
|
22
21
|
export declare function extractAcceptableResponseSchema(xml: string): string | undefined;
|
|
23
22
|
/**
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { HttpRequest, HttpResponse } from "@rapidrest/service-core";
|
|
2
2
|
import { Mailbox } from "@rapidmx/restapi";
|
|
3
3
|
/** The plugin packages whose endpoints Autodiscover advertises. */
|
|
4
|
-
export declare const ACTIVESYNC_PLUGIN = "@rapidmx/activesync";
|
|
5
|
-
export declare const MAPI_PLUGIN = "@rapidmx/mapi";
|
|
4
|
+
export declare const ACTIVESYNC_PLUGIN = "@rapidmx/activesync-plugin";
|
|
5
|
+
export declare const MAPI_PLUGIN = "@rapidmx/mapi-plugin";
|
|
6
|
+
/** The largest POX request body (in bytes) `pox()` will scan; anything bigger gets a `413`. */
|
|
7
|
+
export declare const MAX_POX_BODY_BYTES: number;
|
|
8
|
+
/** The longest email address looked up (RFC 5321's 254-character forward-path limit). */
|
|
9
|
+
export declare const MAX_ADDRESS_LENGTH = 254;
|
|
6
10
|
/**
|
|
7
11
|
* Abstract base for the two Autodiscover endpoints a real mail client uses to find this deployment's EAS
|
|
8
12
|
* server URL from just an email address - classic POX (`POST /autodiscover/autodiscover.xml`, per
|
|
@@ -31,7 +35,7 @@ export declare const MAPI_PLUGIN = "@rapidmx/mapi";
|
|
|
31
35
|
*
|
|
32
36
|
* `mailboxClass` is supplied by the Mongo/SQL concrete subclasses following the exact one-line-per-backend
|
|
33
37
|
* pattern used throughout this library. The advertised URLs are built from the `mail:autodiscover:public_url`
|
|
34
|
-
* setting, and each protocol is only advertised while its plugin (`@rapidmx/activesync`, `@rapidmx/mapi`) is
|
|
38
|
+
* setting, and each protocol is only advertised while its plugin (`@rapidmx/activesync-plugin`, `@rapidmx/mapi-plugin`) is
|
|
35
39
|
* loaded, so a client is never pointed at an endpoint this deployment doesn't serve.
|
|
36
40
|
*
|
|
37
41
|
* @author Jean-Philippe Steinmetz
|
|
@@ -52,11 +56,27 @@ export declare abstract class BaseAutodiscoverRoute<M extends Mailbox> {
|
|
|
52
56
|
* Builds the query value used to match `Mailbox.aliasAddresses` against the given address. See
|
|
53
57
|
* `BaseMailIngestRoute.aliasQueryValue()`'s identical doc comment - same Mongo-array vs.
|
|
54
58
|
* SQL-`simple-json`-column backend split, same override point (`AutodiscoverRouteSQL` overrides this
|
|
55
|
-
* identically to `MailIngestRouteSQL`).
|
|
59
|
+
* identically to `MailIngestRouteSQL`). The base form is a `ModelUtils.literal()` exact match.
|
|
56
60
|
*/
|
|
57
61
|
protected aliasQueryValue(address: string): any;
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes `publicUrl` into a base URL (origin plus path, no trailing slash), or `undefined` when it's
|
|
64
|
+
* unset or unsafe to advertise: it must parse, use `https:` (plain `http:` is only tolerated for a loopback
|
|
65
|
+
* host, for local development), and carry no credentials, query string or fragment - any of which would
|
|
66
|
+
* produce a broken or insecure endpoint URL once a path is appended.
|
|
67
|
+
*/
|
|
68
|
+
private get baseUrl();
|
|
58
69
|
private endpointUrl;
|
|
59
70
|
init(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Normalizes an unauthenticated caller's email value into a single plain address, or `undefined` when it isn't
|
|
73
|
+
* one. service-core's search query parser interprets `op(value)` strings (`like(*)`, `regex(^a)`, `in(a,b)`,
|
|
74
|
+
* `ne(x)`, ...), so an arbitrary value reaching `find()` would let an anonymous caller enumerate the mailbox
|
|
75
|
+
* directory; rejecting whitespace, parentheses, commas, quotes and backslashes (plus RFC 5321's 254-character
|
|
76
|
+
* length cap) up front means no query is ever issued for such a value.
|
|
77
|
+
*/
|
|
78
|
+
private normalizeAddress;
|
|
79
|
+
/** Looks up a mailbox by an already-`normalizeAddress()`ed address, always as a `ModelUtils.literal()` value. */
|
|
60
80
|
private resolveMailbox;
|
|
61
81
|
/**
|
|
62
82
|
* Classic POX Autodiscover. Per Microsoft's own client-behavior documentation, a permanent failure is just
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,18 +6,15 @@
|
|
|
6
6
|
* exported from this package's `./mongo`/`./sql` subpaths instead, alongside every other entity/route/job this
|
|
7
7
|
* library defines - see `src/eas/index.ts`'s identical doc comment for the same convention.
|
|
8
8
|
*
|
|
9
|
-
* A deployment mounts Autodiscover
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* protected readonly mapiUrl = "https://mail.example.com/mapi/emsmdb";
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
9
|
+
* A deployment mounts Autodiscover by loading the ready-to-mount `AutodiscoverRouteMongo`/`AutodiscoverRouteSQL`
|
|
10
|
+
* class from this plugin's `./mongo`/`./sql` entry point directly - no subclass or URLs to supply, since
|
|
11
|
+
* `BaseAutodiscoverRoute` builds the EAS (`easUrl`) and MAPI (`mapiUrl`) endpoint URLs itself from the
|
|
12
|
+
* `mail:autodiscover:public_url` config setting (also exposed as this plugin's own "Public server URL"
|
|
13
|
+
* admin-console setting - see `package.json`'s `rapidmx.plugin.settings`), and only advertises each protocol
|
|
14
|
+
* while its own plugin (`@rapidmx/activesync-plugin`, `@rapidmx/mapi-plugin`) is loaded. Set
|
|
15
|
+
* `mail:autodiscover:public_url` to this deployment's externally-reachable `https://` base URL (e.g.
|
|
16
|
+
* `https://mail.example.com`) for either endpoint to be advertised at all - see `BaseAutodiscoverRoute`'s own
|
|
17
|
+
* doc comment for the exact validation rules.
|
|
21
18
|
*/
|
|
22
19
|
export * from "./AutodiscoverXml.js";
|
|
23
20
|
export * from "./BaseAutodiscoverRoute.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/autodiscover-plugin",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A RapidMX plugin that provides Autodiscover protocol support.",
|
|
5
5
|
"repository": "https://github.com/RapidMX/autodiscover.git",
|
|
6
6
|
"author": "RapidREST <rapidrests@gmail.com>",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@rapidmx/restapi": "0.x",
|
|
26
26
|
"@rapidrest/core": "5.x",
|
|
27
|
-
"@rapidrest/service-core": "2.x"
|
|
27
|
+
"@rapidrest/service-core": "^2.x"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@rapidmx/restapi": "^0.
|
|
30
|
+
"@rapidmx/restapi": "^0.10.0",
|
|
31
31
|
"@rapidrest/core": "^5.2.2",
|
|
32
|
-
"@rapidrest/service-core": "^2.
|
|
32
|
+
"@rapidrest/service-core": "^2.1.0",
|
|
33
33
|
"@swc/core": "^1.16.2",
|
|
34
34
|
"@types/nconf": "^0.10.7",
|
|
35
35
|
"@types/node": "^26.5.0",
|
|
@@ -97,9 +97,14 @@
|
|
|
97
97
|
"key": "mail:autodiscover:public_url",
|
|
98
98
|
"label": "Public server URL",
|
|
99
99
|
"type": "string",
|
|
100
|
-
"
|
|
100
|
+
"default": "",
|
|
101
|
+
"help": "The address clients reach this server at, for example https://mail.example.com - the same host your DNS/MX setup already points at. Nothing is advertised until this is set. Also add a CNAME (autodiscover.<domain> -> this host) or, to avoid needing a second TLS certificate, a DNS SRV record (_autodiscover._tcp.<domain> -> this host) so mail clients can find this server automatically - see this deployment's Domain DNS setup checklist."
|
|
101
102
|
}
|
|
102
|
-
]
|
|
103
|
+
],
|
|
104
|
+
"requires": {
|
|
105
|
+
"@rapidmx/activesync-plugin": "^1.0.0-beta.2",
|
|
106
|
+
"@rapidmx/mapi-plugin": "^1.0.0-beta.3"
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
}
|