mcp-twake-mail 0.1.0 → 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 +77 -48
- package/build/cli/commands/setup.js +54 -12
- package/build/cli/commands/setup.js.map +1 -1
- package/build/cli/prompts/setup-wizard.d.ts +33 -1
- package/build/cli/prompts/setup-wizard.js +134 -2
- package/build/cli/prompts/setup-wizard.js.map +1 -1
- package/build/cli/prompts/setup-wizard.test.d.ts +1 -0
- package/build/cli/prompts/setup-wizard.test.js +161 -0
- package/build/cli/prompts/setup-wizard.test.js.map +1 -0
- package/build/config/schema.d.ts +2 -0
- package/build/config/schema.js +3 -0
- package/build/config/schema.js.map +1 -1
- package/build/discovery/dns-srv.d.ts +18 -0
- package/build/discovery/dns-srv.js +60 -0
- package/build/discovery/dns-srv.js.map +1 -0
- package/build/discovery/dns-srv.test.d.ts +4 -0
- package/build/discovery/dns-srv.test.js +79 -0
- package/build/discovery/dns-srv.test.js.map +1 -0
- package/build/discovery/index.d.ts +9 -0
- package/build/discovery/index.js +13 -0
- package/build/discovery/index.js.map +1 -0
- package/build/discovery/oauth-discovery.d.ts +34 -0
- package/build/discovery/oauth-discovery.js +160 -0
- package/build/discovery/oauth-discovery.js.map +1 -0
- package/build/discovery/oauth-discovery.test.d.ts +1 -0
- package/build/discovery/oauth-discovery.test.js +198 -0
- package/build/discovery/oauth-discovery.test.js.map +1 -0
- package/build/discovery/orchestrator.d.ts +31 -0
- package/build/discovery/orchestrator.js +87 -0
- package/build/discovery/orchestrator.js.map +1 -0
- package/build/discovery/orchestrator.test.d.ts +4 -0
- package/build/discovery/orchestrator.test.js +242 -0
- package/build/discovery/orchestrator.test.js.map +1 -0
- package/build/discovery/types.d.ts +18 -0
- package/build/discovery/types.js +15 -0
- package/build/discovery/types.js.map +1 -0
- package/build/discovery/well-known.d.ts +21 -0
- package/build/discovery/well-known.js +52 -0
- package/build/discovery/well-known.js.map +1 -0
- package/build/discovery/well-known.test.d.ts +4 -0
- package/build/discovery/well-known.test.js +120 -0
- package/build/discovery/well-known.test.js.map +1 -0
- package/build/mcp/server.d.ts +5 -3
- package/build/mcp/server.js +11 -5
- package/build/mcp/server.js.map +1 -1
- package/build/mcp/tools/email-sending.d.ts +10 -1
- package/build/mcp/tools/email-sending.js +60 -15
- package/build/mcp/tools/email-sending.js.map +1 -1
- package/build/mcp/tools/index.d.ts +10 -1
- package/build/mcp/tools/index.js +4 -3
- package/build/mcp/tools/index.js.map +1 -1
- package/build/signature/converter.d.ts +2 -0
- package/build/signature/converter.js +23 -0
- package/build/signature/converter.js.map +1 -0
- package/build/signature/converter.test.d.ts +1 -0
- package/build/signature/converter.test.js +84 -0
- package/build/signature/converter.test.js.map +1 -0
- package/build/signature/index.d.ts +2 -0
- package/build/signature/index.js +3 -0
- package/build/signature/index.js.map +1 -0
- package/build/signature/loader.d.ts +6 -0
- package/build/signature/loader.js +31 -0
- package/build/signature/loader.js.map +1 -0
- package/build/signature/loader.test.d.ts +1 -0
- package/build/signature/loader.test.js +85 -0
- package/build/signature/loader.test.js.map +1 -0
- package/docs/auto-discovery.md +210 -0
- package/docs/oidc-configuration.md +261 -0
- package/package.json +3 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { markdownToHtml, markdownToPlainText } from './converter.js';
|
|
3
|
+
describe('markdownToHtml', () => {
|
|
4
|
+
it('converts simple paragraph to HTML', () => {
|
|
5
|
+
const result = markdownToHtml('Hello world');
|
|
6
|
+
expect(result).toContain('<p>Hello world</p>');
|
|
7
|
+
});
|
|
8
|
+
it('converts bold **text** to <strong>', () => {
|
|
9
|
+
const result = markdownToHtml('This is **bold** text');
|
|
10
|
+
expect(result).toContain('<strong>bold</strong>');
|
|
11
|
+
});
|
|
12
|
+
it('converts italic *text* to <em>', () => {
|
|
13
|
+
const result = markdownToHtml('This is *italic* text');
|
|
14
|
+
expect(result).toContain('<em>italic</em>');
|
|
15
|
+
});
|
|
16
|
+
it('converts links [text](url) to <a> tags', () => {
|
|
17
|
+
const result = markdownToHtml('[Click here](https://example.com)');
|
|
18
|
+
expect(result).toContain('<a href="https://example.com">Click here</a>');
|
|
19
|
+
});
|
|
20
|
+
it('preserves paragraphs on double newlines', () => {
|
|
21
|
+
const result = markdownToHtml('Line one\n\nLine two');
|
|
22
|
+
expect(result).toContain('<p>Line one</p>');
|
|
23
|
+
expect(result).toContain('<p>Line two</p>');
|
|
24
|
+
});
|
|
25
|
+
it('converts mixed formatting correctly', () => {
|
|
26
|
+
const markdown = 'Hello **world**!\n\nThis is [a link](https://example.com)';
|
|
27
|
+
const result = markdownToHtml(markdown);
|
|
28
|
+
expect(result).toContain('<strong>world</strong>');
|
|
29
|
+
expect(result).toContain('<a href="https://example.com">a link</a>');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('markdownToPlainText', () => {
|
|
33
|
+
it('strips heading markers', () => {
|
|
34
|
+
expect(markdownToPlainText('# Heading 1')).toBe('Heading 1');
|
|
35
|
+
expect(markdownToPlainText('## Heading 2')).toBe('Heading 2');
|
|
36
|
+
expect(markdownToPlainText('### Heading 3')).toBe('Heading 3');
|
|
37
|
+
});
|
|
38
|
+
it('strips bold markers **text**', () => {
|
|
39
|
+
const result = markdownToPlainText('This is **bold** text');
|
|
40
|
+
expect(result).toBe('This is bold text');
|
|
41
|
+
});
|
|
42
|
+
it('strips bold markers __text__', () => {
|
|
43
|
+
const result = markdownToPlainText('This is __bold__ text');
|
|
44
|
+
expect(result).toBe('This is bold text');
|
|
45
|
+
});
|
|
46
|
+
it('strips italic markers *text*', () => {
|
|
47
|
+
const result = markdownToPlainText('This is *italic* text');
|
|
48
|
+
expect(result).toBe('This is italic text');
|
|
49
|
+
});
|
|
50
|
+
it('strips italic markers _text_', () => {
|
|
51
|
+
const result = markdownToPlainText('This is _italic_ text');
|
|
52
|
+
expect(result).toBe('This is italic text');
|
|
53
|
+
});
|
|
54
|
+
it('converts links to "text (url)" format', () => {
|
|
55
|
+
const result = markdownToPlainText('[Click here](https://example.com)');
|
|
56
|
+
expect(result).toBe('Click here (https://example.com)');
|
|
57
|
+
});
|
|
58
|
+
it('strips inline code backticks', () => {
|
|
59
|
+
const result = markdownToPlainText('Use `const` instead of `var`');
|
|
60
|
+
expect(result).toBe('Use const instead of var');
|
|
61
|
+
});
|
|
62
|
+
it('converts list markers to bullets', () => {
|
|
63
|
+
const markdown = '* Item 1\n- Item 2\n+ Item 3';
|
|
64
|
+
const result = markdownToPlainText(markdown);
|
|
65
|
+
expect(result).toContain('• Item 1');
|
|
66
|
+
expect(result).toContain('• Item 2');
|
|
67
|
+
expect(result).toContain('• Item 3');
|
|
68
|
+
});
|
|
69
|
+
it('handles mixed formatting', () => {
|
|
70
|
+
const markdown = '## Welcome\n\nThis is **bold** and *italic*.\n\n[Link](https://example.com)';
|
|
71
|
+
const result = markdownToPlainText(markdown);
|
|
72
|
+
expect(result).toContain('Welcome');
|
|
73
|
+
expect(result).toContain('This is bold and italic.');
|
|
74
|
+
expect(result).toContain('Link (https://example.com)');
|
|
75
|
+
expect(result).not.toContain('##');
|
|
76
|
+
expect(result).not.toContain('**');
|
|
77
|
+
expect(result).not.toContain('*');
|
|
78
|
+
});
|
|
79
|
+
it('trims whitespace', () => {
|
|
80
|
+
const result = markdownToPlainText(' \n\nHello \n\n ');
|
|
81
|
+
expect(result).toBe('Hello');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=converter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.test.js","sourceRoot":"","sources":["../../src/signature/converter.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signature/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAyB,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile, access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { markdownToHtml, markdownToPlainText } from './converter.js';
|
|
5
|
+
export async function loadSignature(signaturePath, logger) {
|
|
6
|
+
if (!signaturePath) {
|
|
7
|
+
logger.debug('No signature path configured');
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
// Expand ~ to home directory
|
|
11
|
+
const expandedPath = signaturePath.replace(/^~/, homedir());
|
|
12
|
+
try {
|
|
13
|
+
// Check file exists and is readable
|
|
14
|
+
await access(expandedPath, constants.R_OK);
|
|
15
|
+
// Read file content
|
|
16
|
+
const markdown = await readFile(expandedPath, 'utf-8');
|
|
17
|
+
// Convert to both formats
|
|
18
|
+
const result = {
|
|
19
|
+
text: markdownToPlainText(markdown),
|
|
20
|
+
html: markdownToHtml(markdown),
|
|
21
|
+
};
|
|
22
|
+
logger.info({ path: expandedPath }, 'Signature loaded successfully');
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Don't crash if file missing - just log warning
|
|
27
|
+
logger.warn({ error, path: expandedPath }, 'Failed to load signature file - emails will be sent without signature');
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/signature/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAOrE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,aAAiC,EACjC,MAAc;IAEd,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC7C,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,6BAA6B;IAC7B,MAAM,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAE5D,IAAI,CAAC;QACH,oCAAoC;QACpC,MAAM,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAE3C,oBAAoB;QACpB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAEvD,0BAA0B;QAC1B,MAAM,MAAM,GAAqB;YAC/B,IAAI,EAAE,mBAAmB,CAAC,QAAQ,CAAC;YACnC,IAAI,EAAE,cAAc,CAAC,QAAQ,CAAC;SAC/B,CAAC;QAEF,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,+BAA+B,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,iDAAiD;QACjD,MAAM,CAAC,IAAI,CACT,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,EAC7B,uEAAuE,CACxE,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { loadSignature } from './loader.js';
|
|
4
|
+
// Mock fs/promises
|
|
5
|
+
vi.mock('node:fs/promises', () => ({
|
|
6
|
+
readFile: vi.fn(),
|
|
7
|
+
access: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
// Import after mocking
|
|
10
|
+
const { readFile, access } = await import('node:fs/promises');
|
|
11
|
+
describe('loadSignature', () => {
|
|
12
|
+
let mockLogger;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
mockLogger = {
|
|
16
|
+
debug: vi.fn(),
|
|
17
|
+
info: vi.fn(),
|
|
18
|
+
warn: vi.fn(),
|
|
19
|
+
error: vi.fn(),
|
|
20
|
+
fatal: vi.fn(),
|
|
21
|
+
trace: vi.fn(),
|
|
22
|
+
child: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
it('returns undefined when signaturePath is undefined', async () => {
|
|
26
|
+
const result = await loadSignature(undefined, mockLogger);
|
|
27
|
+
expect(result).toBeUndefined();
|
|
28
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('No signature path configured');
|
|
29
|
+
expect(access).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
it('returns undefined when signaturePath is empty string', async () => {
|
|
32
|
+
const result = await loadSignature('', mockLogger);
|
|
33
|
+
expect(result).toBeUndefined();
|
|
34
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('No signature path configured');
|
|
35
|
+
expect(access).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it('loads and converts file successfully', async () => {
|
|
38
|
+
const markdown = '**John Doe**\nSoftware Engineer\n[email@example.com](mailto:email@example.com)';
|
|
39
|
+
vi.mocked(access).mockResolvedValue(undefined);
|
|
40
|
+
vi.mocked(readFile).mockResolvedValue(markdown);
|
|
41
|
+
const result = await loadSignature('/path/to/signature.md', mockLogger);
|
|
42
|
+
expect(result).toBeDefined();
|
|
43
|
+
expect(result?.text).toContain('John Doe');
|
|
44
|
+
expect(result?.text).toContain('Software Engineer');
|
|
45
|
+
expect(result?.text).toContain('email@example.com (mailto:email@example.com)');
|
|
46
|
+
expect(result?.html).toContain('<strong>John Doe</strong>');
|
|
47
|
+
expect(result?.html).toContain('<a href="mailto:email@example.com">email@example.com</a>');
|
|
48
|
+
expect(mockLogger.info).toHaveBeenCalledWith({ path: '/path/to/signature.md' }, 'Signature loaded successfully');
|
|
49
|
+
});
|
|
50
|
+
it('expands ~ to home directory', async () => {
|
|
51
|
+
const markdown = 'Test signature';
|
|
52
|
+
const expectedPath = `${homedir()}/signature.md`;
|
|
53
|
+
vi.mocked(access).mockResolvedValue(undefined);
|
|
54
|
+
vi.mocked(readFile).mockResolvedValue(markdown);
|
|
55
|
+
await loadSignature('~/signature.md', mockLogger);
|
|
56
|
+
expect(access).toHaveBeenCalledWith(expectedPath, expect.any(Number));
|
|
57
|
+
expect(readFile).toHaveBeenCalledWith(expectedPath, 'utf-8');
|
|
58
|
+
expect(mockLogger.info).toHaveBeenCalledWith({ path: expectedPath }, 'Signature loaded successfully');
|
|
59
|
+
});
|
|
60
|
+
it('returns undefined and logs warning when file does not exist', async () => {
|
|
61
|
+
const error = new Error('ENOENT: no such file or directory');
|
|
62
|
+
vi.mocked(access).mockRejectedValue(error);
|
|
63
|
+
const result = await loadSignature('/nonexistent/signature.md', mockLogger);
|
|
64
|
+
expect(result).toBeUndefined();
|
|
65
|
+
expect(mockLogger.warn).toHaveBeenCalledWith({ error, path: '/nonexistent/signature.md' }, 'Failed to load signature file - emails will be sent without signature');
|
|
66
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('returns undefined and logs warning when file is not readable', async () => {
|
|
69
|
+
const error = new Error('EACCES: permission denied');
|
|
70
|
+
vi.mocked(access).mockRejectedValue(error);
|
|
71
|
+
const result = await loadSignature('/restricted/signature.md', mockLogger);
|
|
72
|
+
expect(result).toBeUndefined();
|
|
73
|
+
expect(mockLogger.warn).toHaveBeenCalledWith({ error, path: '/restricted/signature.md' }, 'Failed to load signature file - emails will be sent without signature');
|
|
74
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('returns undefined and logs warning when readFile fails', async () => {
|
|
77
|
+
const error = new Error('Read error');
|
|
78
|
+
vi.mocked(access).mockResolvedValue(undefined);
|
|
79
|
+
vi.mocked(readFile).mockRejectedValue(error);
|
|
80
|
+
const result = await loadSignature('/path/to/signature.md', mockLogger);
|
|
81
|
+
expect(result).toBeUndefined();
|
|
82
|
+
expect(mockLogger.warn).toHaveBeenCalledWith({ error, path: '/path/to/signature.md' }, 'Failed to load signature file - emails will be sent without signature');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=loader.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.test.js","sourceRoot":"","sources":["../../src/signature/loader.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Auto-Discovery
|
|
2
|
+
|
|
3
|
+
mcp-twake-mail supports automatic discovery of JMAP server and OIDC configuration from just an email address. This simplifies setup by eliminating the need to manually find server URLs.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
When you provide an email address (e.g., `user@example.com`), the auto-discovery system performs the following steps:
|
|
8
|
+
|
|
9
|
+
### 1. DNS SRV Lookup
|
|
10
|
+
|
|
11
|
+
First, the system queries DNS for a SRV record:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
_jmap._tcp.example.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If found, this returns the JMAP server hostname and port. For example:
|
|
18
|
+
```
|
|
19
|
+
_jmap._tcp.example.com. 3600 IN SRV 0 1 443 jmap.example.com.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This indicates the JMAP server is at `jmap.example.com` on port 443.
|
|
23
|
+
|
|
24
|
+
### 2. Well-Known JMAP Endpoint
|
|
25
|
+
|
|
26
|
+
If DNS SRV fails or as a verification step, the system tries the `.well-known/jmap` endpoint:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
https://example.com/.well-known/jmap
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This endpoint should redirect (302/307) or return the JMAP session URL directly. Per RFC 8620, the response provides the full session URL like:
|
|
33
|
+
```
|
|
34
|
+
https://jmap.example.com/jmap/session
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. OAuth/OIDC Discovery
|
|
38
|
+
|
|
39
|
+
Once the JMAP server is found, the system attempts to discover OAuth/OIDC configuration:
|
|
40
|
+
|
|
41
|
+
#### Protected Resource Metadata (RFC 9728)
|
|
42
|
+
|
|
43
|
+
The system checks for OAuth metadata at the JMAP URL:
|
|
44
|
+
```
|
|
45
|
+
https://jmap.example.com/.well-known/oauth-protected-resource
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This may return the authorization server (OIDC issuer) URL.
|
|
49
|
+
|
|
50
|
+
#### WWW-Authenticate Header
|
|
51
|
+
|
|
52
|
+
If the above fails, the system makes an unauthenticated request to the JMAP session URL and parses the `WWW-Authenticate` header:
|
|
53
|
+
|
|
54
|
+
```http
|
|
55
|
+
WWW-Authenticate: Bearer realm="example.com", authorization_uri="https://sso.example.com"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Common SSO Patterns
|
|
59
|
+
|
|
60
|
+
As a fallback, the system tries common SSO subdomain patterns:
|
|
61
|
+
- `https://sso.example.com`
|
|
62
|
+
- `https://auth.example.com`
|
|
63
|
+
- `https://login.example.com`
|
|
64
|
+
- `https://id.example.com`
|
|
65
|
+
- `https://accounts.example.com`
|
|
66
|
+
|
|
67
|
+
For each, it checks if `/.well-known/openid-configuration` exists.
|
|
68
|
+
|
|
69
|
+
## Using Auto-Discovery
|
|
70
|
+
|
|
71
|
+
### Setup Wizard
|
|
72
|
+
|
|
73
|
+
Run the setup wizard and choose auto-discovery mode:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx mcp-twake-mail setup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
=== MCP Twake Mail Setup Wizard ===
|
|
81
|
+
|
|
82
|
+
Setup mode:
|
|
83
|
+
1. Auto-discover from email address (Recommended)
|
|
84
|
+
2. Manual configuration
|
|
85
|
+
Choose [1-2]: 1
|
|
86
|
+
|
|
87
|
+
Email address: user@example.com
|
|
88
|
+
|
|
89
|
+
Discovering JMAP server...
|
|
90
|
+
✓ Found JMAP server: https://jmap.example.com/jmap/session
|
|
91
|
+
✓ Found OIDC issuer: https://sso.example.com
|
|
92
|
+
|
|
93
|
+
Use discovered settings? [Y/n]: y
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Programmatic Usage
|
|
97
|
+
|
|
98
|
+
The discovery module can be used programmatically:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { discoverFromEmail } from 'mcp-twake-mail/discovery';
|
|
102
|
+
|
|
103
|
+
const result = await discoverFromEmail('user@example.com');
|
|
104
|
+
|
|
105
|
+
console.log(result.jmapUrl); // https://jmap.example.com/jmap/session
|
|
106
|
+
console.log(result.jmapMethod); // 'dns-srv' | 'well-known'
|
|
107
|
+
|
|
108
|
+
if (result.oidc) {
|
|
109
|
+
console.log(result.oidc.issuer); // https://sso.example.com
|
|
110
|
+
console.log(result.oidc.method); // 'protected-resource' | 'www-authenticate' | 'sso-pattern'
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Discovery Methods
|
|
115
|
+
|
|
116
|
+
### JMAP Discovery
|
|
117
|
+
|
|
118
|
+
| Method | Priority | Description |
|
|
119
|
+
|--------|----------|-------------|
|
|
120
|
+
| DNS SRV | 1 (first) | `_jmap._tcp.{domain}` lookup |
|
|
121
|
+
| .well-known/jmap | 2 (fallback) | HTTPS endpoint at domain root |
|
|
122
|
+
|
|
123
|
+
### OIDC Discovery
|
|
124
|
+
|
|
125
|
+
| Method | Priority | Description |
|
|
126
|
+
|--------|----------|-------------|
|
|
127
|
+
| Protected Resource Metadata | 1 | RFC 9728 OAuth metadata |
|
|
128
|
+
| WWW-Authenticate | 2 | Parse Bearer challenge header |
|
|
129
|
+
| SSO Patterns | 3 | Try common subdomain patterns |
|
|
130
|
+
|
|
131
|
+
## Configuration After Discovery
|
|
132
|
+
|
|
133
|
+
After discovery, you'll have:
|
|
134
|
+
|
|
135
|
+
- **JMAP Session URL** — Required for all operations
|
|
136
|
+
- **OIDC Issuer** — Optional, for OAuth authentication
|
|
137
|
+
|
|
138
|
+
You can then choose your authentication method:
|
|
139
|
+
|
|
140
|
+
1. **Basic** — Username/password (no OIDC needed)
|
|
141
|
+
2. **Bearer** — Pre-existing JWT token (no OIDC needed)
|
|
142
|
+
3. **OIDC** — Full OAuth flow using discovered issuer
|
|
143
|
+
|
|
144
|
+
## Timeout Handling
|
|
145
|
+
|
|
146
|
+
All discovery operations have built-in timeouts:
|
|
147
|
+
|
|
148
|
+
| Operation | Timeout |
|
|
149
|
+
|-----------|---------|
|
|
150
|
+
| DNS SRV lookup | 3 seconds |
|
|
151
|
+
| HTTP requests | 10 seconds |
|
|
152
|
+
|
|
153
|
+
If discovery fails or times out, the setup wizard automatically falls back to manual configuration.
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
### DNS SRV Not Found
|
|
158
|
+
|
|
159
|
+
This is normal for many domains. The system will automatically try `.well-known/jmap`.
|
|
160
|
+
|
|
161
|
+
### .well-known/jmap Returns 404
|
|
162
|
+
|
|
163
|
+
The domain doesn't support JMAP auto-discovery. You'll need to manually enter the JMAP session URL.
|
|
164
|
+
|
|
165
|
+
### OIDC Discovery Fails
|
|
166
|
+
|
|
167
|
+
OIDC discovery is optional. If it fails:
|
|
168
|
+
- You can still use Basic or Bearer authentication
|
|
169
|
+
- You can manually enter the OIDC issuer URL
|
|
170
|
+
|
|
171
|
+
### Firewall Blocking DNS
|
|
172
|
+
|
|
173
|
+
Corporate firewalls may block DNS SRV queries. The system will fall back to HTTPS-based discovery.
|
|
174
|
+
|
|
175
|
+
## Server Requirements
|
|
176
|
+
|
|
177
|
+
For auto-discovery to work, your JMAP server should implement:
|
|
178
|
+
|
|
179
|
+
### RFC 8620 Service Discovery
|
|
180
|
+
|
|
181
|
+
1. **DNS SRV record** (recommended):
|
|
182
|
+
```
|
|
183
|
+
_jmap._tcp.example.com. 3600 IN SRV 0 1 443 jmap.example.com.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
2. **.well-known/jmap endpoint**:
|
|
187
|
+
```
|
|
188
|
+
https://example.com/.well-known/jmap
|
|
189
|
+
```
|
|
190
|
+
Should redirect to or return the JMAP session URL.
|
|
191
|
+
|
|
192
|
+
### OAuth Discovery (Optional)
|
|
193
|
+
|
|
194
|
+
For OIDC auto-discovery:
|
|
195
|
+
|
|
196
|
+
1. **Protected Resource Metadata** (RFC 9728):
|
|
197
|
+
```
|
|
198
|
+
https://jmap.example.com/.well-known/oauth-protected-resource
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
2. **WWW-Authenticate header** on 401 responses:
|
|
202
|
+
```
|
|
203
|
+
WWW-Authenticate: Bearer realm="example.com", authorization_uri="https://sso.example.com"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## References
|
|
207
|
+
|
|
208
|
+
- [RFC 8620](https://datatracker.ietf.org/doc/html/rfc8620) — JMAP Core (Section 2.2: Service Autodiscovery)
|
|
209
|
+
- [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) — OAuth 2.0 Protected Resource Metadata
|
|
210
|
+
- [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) — OIDC Discovery specification
|