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.
Files changed (69) hide show
  1. package/README.md +77 -48
  2. package/build/cli/commands/setup.js +54 -12
  3. package/build/cli/commands/setup.js.map +1 -1
  4. package/build/cli/prompts/setup-wizard.d.ts +33 -1
  5. package/build/cli/prompts/setup-wizard.js +134 -2
  6. package/build/cli/prompts/setup-wizard.js.map +1 -1
  7. package/build/cli/prompts/setup-wizard.test.d.ts +1 -0
  8. package/build/cli/prompts/setup-wizard.test.js +161 -0
  9. package/build/cli/prompts/setup-wizard.test.js.map +1 -0
  10. package/build/config/schema.d.ts +2 -0
  11. package/build/config/schema.js +3 -0
  12. package/build/config/schema.js.map +1 -1
  13. package/build/discovery/dns-srv.d.ts +18 -0
  14. package/build/discovery/dns-srv.js +60 -0
  15. package/build/discovery/dns-srv.js.map +1 -0
  16. package/build/discovery/dns-srv.test.d.ts +4 -0
  17. package/build/discovery/dns-srv.test.js +79 -0
  18. package/build/discovery/dns-srv.test.js.map +1 -0
  19. package/build/discovery/index.d.ts +9 -0
  20. package/build/discovery/index.js +13 -0
  21. package/build/discovery/index.js.map +1 -0
  22. package/build/discovery/oauth-discovery.d.ts +34 -0
  23. package/build/discovery/oauth-discovery.js +160 -0
  24. package/build/discovery/oauth-discovery.js.map +1 -0
  25. package/build/discovery/oauth-discovery.test.d.ts +1 -0
  26. package/build/discovery/oauth-discovery.test.js +198 -0
  27. package/build/discovery/oauth-discovery.test.js.map +1 -0
  28. package/build/discovery/orchestrator.d.ts +31 -0
  29. package/build/discovery/orchestrator.js +87 -0
  30. package/build/discovery/orchestrator.js.map +1 -0
  31. package/build/discovery/orchestrator.test.d.ts +4 -0
  32. package/build/discovery/orchestrator.test.js +242 -0
  33. package/build/discovery/orchestrator.test.js.map +1 -0
  34. package/build/discovery/types.d.ts +18 -0
  35. package/build/discovery/types.js +15 -0
  36. package/build/discovery/types.js.map +1 -0
  37. package/build/discovery/well-known.d.ts +21 -0
  38. package/build/discovery/well-known.js +52 -0
  39. package/build/discovery/well-known.js.map +1 -0
  40. package/build/discovery/well-known.test.d.ts +4 -0
  41. package/build/discovery/well-known.test.js +120 -0
  42. package/build/discovery/well-known.test.js.map +1 -0
  43. package/build/mcp/server.d.ts +5 -3
  44. package/build/mcp/server.js +11 -5
  45. package/build/mcp/server.js.map +1 -1
  46. package/build/mcp/tools/email-sending.d.ts +10 -1
  47. package/build/mcp/tools/email-sending.js +60 -15
  48. package/build/mcp/tools/email-sending.js.map +1 -1
  49. package/build/mcp/tools/index.d.ts +10 -1
  50. package/build/mcp/tools/index.js +4 -3
  51. package/build/mcp/tools/index.js.map +1 -1
  52. package/build/signature/converter.d.ts +2 -0
  53. package/build/signature/converter.js +23 -0
  54. package/build/signature/converter.js.map +1 -0
  55. package/build/signature/converter.test.d.ts +1 -0
  56. package/build/signature/converter.test.js +84 -0
  57. package/build/signature/converter.test.js.map +1 -0
  58. package/build/signature/index.d.ts +2 -0
  59. package/build/signature/index.js +3 -0
  60. package/build/signature/index.js.map +1 -0
  61. package/build/signature/loader.d.ts +6 -0
  62. package/build/signature/loader.js +31 -0
  63. package/build/signature/loader.js.map +1 -0
  64. package/build/signature/loader.test.d.ts +1 -0
  65. package/build/signature/loader.test.js +85 -0
  66. package/build/signature/loader.test.js.map +1 -0
  67. package/docs/auto-discovery.md +210 -0
  68. package/docs/oidc-configuration.md +261 -0
  69. 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,2 @@
1
+ export { loadSignature, type SignatureContent } from './loader.js';
2
+ export { markdownToHtml, markdownToPlainText } from './converter.js';
@@ -0,0 +1,3 @@
1
+ export { loadSignature } from './loader.js';
2
+ export { markdownToHtml, markdownToPlainText } from './converter.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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,6 @@
1
+ import type { Logger } from '../config/logger.js';
2
+ export interface SignatureContent {
3
+ text: string;
4
+ html: string;
5
+ }
6
+ export declare function loadSignature(signaturePath: string | undefined, logger: Logger): Promise<SignatureContent | undefined>;
@@ -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