fluxmail 0.2.0 → 0.3.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 (55) hide show
  1. package/LICENSE.md +111 -0
  2. package/README.md +20 -4
  3. package/dist/accounts/gmailConnection.d.ts +17 -0
  4. package/dist/accounts/gmailConnection.d.ts.map +1 -0
  5. package/dist/accounts/gmailConnection.js +32 -0
  6. package/dist/accounts/gmailConnection.js.map +1 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +175 -8
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +8 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +22 -1
  14. package/dist/config.js.map +1 -1
  15. package/dist/context.d.ts +2 -0
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +3 -1
  18. package/dist/context.js.map +1 -1
  19. package/dist/http/app.d.ts +2 -0
  20. package/dist/http/app.d.ts.map +1 -1
  21. package/dist/http/app.js +96 -19
  22. package/dist/http/app.js.map +1 -1
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp/buildServer.d.ts +11 -3
  28. package/dist/mcp/buildServer.d.ts.map +1 -1
  29. package/dist/mcp/buildServer.js +333 -229
  30. package/dist/mcp/buildServer.js.map +1 -1
  31. package/dist/permissions.d.ts +19 -0
  32. package/dist/permissions.d.ts.map +1 -0
  33. package/dist/permissions.js +70 -0
  34. package/dist/permissions.js.map +1 -0
  35. package/dist/service/emailService.d.ts +1 -1
  36. package/dist/service/emailService.d.ts.map +1 -1
  37. package/dist/service/emailService.js +2 -2
  38. package/dist/service/emailService.js.map +1 -1
  39. package/dist/storage/apiKeys.d.ts +6 -1
  40. package/dist/storage/apiKeys.d.ts.map +1 -1
  41. package/dist/storage/apiKeys.js +56 -5
  42. package/dist/storage/apiKeys.js.map +1 -1
  43. package/dist/storage/db.d.ts +172 -0
  44. package/dist/storage/db.d.ts.map +1 -1
  45. package/dist/storage/db.js +35 -1
  46. package/dist/storage/db.js.map +1 -1
  47. package/dist/storage/gmailConnectionGrants.d.ts +28 -0
  48. package/dist/storage/gmailConnectionGrants.d.ts.map +1 -0
  49. package/dist/storage/gmailConnectionGrants.js +70 -0
  50. package/dist/storage/gmailConnectionGrants.js.map +1 -0
  51. package/dist/telemetry.d.ts +35 -0
  52. package/dist/telemetry.d.ts.map +1 -0
  53. package/dist/telemetry.js +283 -0
  54. package/dist/telemetry.js.map +1 -0
  55. package/package.json +11 -7
package/LICENSE.md ADDED
@@ -0,0 +1,111 @@
1
+ # Fluxmail Proprietary License Agreement
2
+
3
+ Version 1.0, July 12, 2026
4
+
5
+ Copyright © 2026 Fluxmail. All rights reserved.
6
+
7
+ This Fluxmail Proprietary License Agreement (the “Agreement”) is between Fluxmail and the individual or entity accepting this Agreement (“you”). If you use the Software for an organization, you represent that you have authority to bind that organization, and “you” includes that organization.
8
+
9
+ By downloading, copying, installing, modifying, or using the Software, you accept this Agreement. If you do not accept it, do not use the Software.
10
+
11
+ ## 1. Definitions
12
+
13
+ “Software” means the Fluxmail software distributed with this Agreement, including its source code, object code, documentation, and any Fluxmail updates or modifications provided under this Agreement. Software does not include Third-Party Software.
14
+
15
+ “License Entitlement” means the users, mailboxes, instances, features, usage limits, term, and other rights authorized by a license key, built-in plan, order form, subscription record, or other written authorization issued by Fluxmail.
16
+
17
+ “Production Use” means using the Software for live personal, business, organizational, customer-facing, or revenue-generating operations. It includes processing live mailbox data or making the Software available to users other than solely for development, testing, security review, or evaluation.
18
+
19
+ “Non-Production Use” means private development, testing, security review, or evaluation that does not process live operations or provide a service to end users.
20
+
21
+ “Third-Party Software” means software identified as third-party or open-source software, including dependencies distributed with or used by the Software.
22
+
23
+ “Competing Product” means a product or service whose primary purpose is to provide third parties with a self-hosted or hosted email integration server, email-agent interface, or email API offering substantially similar core functionality to the Software. It does not include software developed and used solely for your own internal operations.
24
+
25
+ ## 2. License grant
26
+
27
+ Subject to this Agreement, Fluxmail grants you a limited, worldwide, non-exclusive, non-transferable, non-sublicensable license to:
28
+
29
+ 1. view, audit, download, copy, compile, and run the Software for Non-Production Use;
30
+ 2. make private modifications to the Software for your own use;
31
+ 3. make reasonable backup and archival copies;
32
+ 4. use GitHub’s standard public-repository features to view and fork the Software solely as needed to exercise the rights in items 1 through 3; and
33
+ 5. use the Software in Production Use only within a valid License Entitlement.
34
+
35
+ A License Entitlement may be offered without charge. A zero-dollar or built-in entitlement remains subject to this Agreement and to its stated limits.
36
+
37
+ All rights not expressly granted in this Agreement are reserved by Fluxmail.
38
+
39
+ ## 3. License Entitlements and technical controls
40
+
41
+ Your Production Use must stay within your License Entitlement. A License Entitlement applies only to the person, organization, and instances it covers. You may not share a license key outside that person or organization or use it on more instances than authorized.
42
+
43
+ You may not modify, remove, disable, circumvent, bypass, or interfere with any license-key validation, entitlement enforcement, usage metering, plan limitation, or other technical restriction in the Software. You may not enable or expose functionality protected by any of those controls except as authorized by your License Entitlement.
44
+
45
+ Modified versions and derivative works containing any part of the Software remain subject to this Agreement. Production Use of them requires a valid License Entitlement covering that use.
46
+
47
+ ## 4. Restrictions
48
+
49
+ Unless Fluxmail gives you separate written permission, you may not:
50
+
51
+ 1. distribute, publish, sell, sublicense, rent, lease, or otherwise provide the Software or a modified version to a third party;
52
+ 2. publish or maintain a fork for distributing the Software, providing production-ready builds to others, avoiding License Entitlements, or operating a Competing Product;
53
+ 3. offer the Software or its substantial features to third parties as a hosted, managed, time-sharing, or service-bureau product;
54
+ 4. white-label the Software or represent it as your own product;
55
+ 5. embed or incorporate the Software into a product or service distributed or provided to third parties;
56
+ 6. use the Software or its source code to develop, train, support, or operate a Competing Product;
57
+ 7. exceed or help another person exceed a License Entitlement;
58
+ 8. remove, alter, or obscure copyright, license, attribution, or trademark notices; or
59
+ 9. use Fluxmail names, logos, or trademarks except as allowed by applicable law or separate written permission.
60
+
61
+ These restrictions do not limit rights that applicable law does not allow Fluxmail to restrict.
62
+
63
+ ## 5. Ownership and modifications
64
+
65
+ Fluxmail and its licensors retain all right, title, and interest in the Software, including all intellectual property rights. This Agreement does not transfer ownership of the Software to you.
66
+
67
+ As between you and Fluxmail, you retain ownership of code you write independently. Your ownership of a modification does not give you rights in the underlying Software or permission to distribute or commercially exploit a combined or derivative work.
68
+
69
+ ## 6. Third-Party Software
70
+
71
+ Third-Party Software is licensed under its own terms, not this Agreement. If a third-party license conflicts with this Agreement for that Third-Party Software, the third-party license controls for that component. You are responsible for complying with applicable third-party terms.
72
+
73
+ ## 7. Orders, subscriptions, and other agreements
74
+
75
+ Pricing, payment, renewal, cancellation, support, and service terms may be stated in an order form, subscription terms, or another agreement with Fluxmail. If that agreement conflicts with this Agreement, it controls for the specific subject and parties it covers.
76
+
77
+ Each release of the Software is governed by the license version distributed with that release. Fluxmail may distribute later releases under updated terms, but updated terms do not change the license for a release you already received unless you agree otherwise.
78
+
79
+ ## 8. Term and termination
80
+
81
+ This Agreement continues until terminated. It terminates automatically if you materially breach it and do not cure the breach within 30 days after receiving notice from Fluxmail. A breach involving circumvention of technical controls, unauthorized distribution, or misuse of a license key may be terminated immediately if it cannot reasonably be cured.
82
+
83
+ When this Agreement terminates, you must stop using and delete all copies of the Software in your possession or control, except copies you must retain by law. Sections 4 through 12 survive termination. Termination does not affect rights or remedies that accrued before termination.
84
+
85
+ Expiration, cancellation, or downgrade of a License Entitlement may reduce or end your authorized Production Use without terminating this Agreement. Any grace period or restricted mode is determined by the applicable License Entitlement and the Software release you use.
86
+
87
+ ## 9. Disclaimer of warranties
88
+
89
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. FLUXMAIL DISCLAIMS ALL EXPRESS, IMPLIED, AND STATUTORY WARRANTIES, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND THAT THE SOFTWARE WILL BE SECURE, UNINTERRUPTED, OR ERROR-FREE.
90
+
91
+ ## 10. Limitation of liability
92
+
93
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, FLUXMAIL WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF DATA, REVENUE, PROFITS, GOODWILL, OR BUSINESS OPPORTUNITY, ARISING OUT OF OR RELATED TO THE SOFTWARE OR THIS AGREEMENT, EVEN IF FLUXMAIL WAS ADVISED THAT SUCH DAMAGES WERE POSSIBLE.
94
+
95
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, FLUXMAIL’S TOTAL LIABILITY ARISING OUT OF OR RELATED TO THE SOFTWARE OR THIS AGREEMENT WILL NOT EXCEED THE GREATER OF (A) THE AMOUNT YOU PAID FLUXMAIL FOR THE SOFTWARE DURING THE 12 MONTHS BEFORE THE EVENT GIVING RISE TO THE CLAIM OR (B) US$100.
96
+
97
+ Some jurisdictions do not allow certain warranty disclaimers or liability limitations. In those jurisdictions, these provisions apply only to the extent permitted by law.
98
+
99
+ ## 11. Notices
100
+
101
+ Fluxmail may give you notice under this Agreement by email to any address you provided in connection with a License Entitlement, order, subscription, support request, or other communication with Fluxmail. An email notice is deemed received on the first business day after it is sent unless Fluxmail receives a delivery-failure message. You are responsible for keeping your contact information current.
102
+
103
+ ## 12. General terms
104
+
105
+ You may not assign or transfer this Agreement or your License Entitlement without Fluxmail’s prior written consent. Fluxmail may assign this Agreement in connection with a merger, acquisition, reorganization, or sale of substantially all related assets.
106
+
107
+ If any provision of this Agreement is unenforceable, it will be enforced to the maximum extent permitted, and the remaining provisions will remain in effect. A waiver must be in writing and applies only to the specific instance stated. Headings are for convenience and do not affect interpretation.
108
+
109
+ This Agreement is governed by the laws of the State of New Jersey, excluding its conflict-of-law rules. Any dispute arising out of or relating to the Software or this Agreement must be brought exclusively in a state or federal court of competent jurisdiction located in New Jersey. You and Fluxmail consent to the personal jurisdiction of those courts. This paragraph does not limit rights that applicable law does not allow a party to waive.
110
+
111
+ This Agreement, together with any applicable License Entitlement and agreement described in Section 7, is the entire agreement between you and Fluxmail about the Software and replaces prior discussions or understandings about its licensing.
package/README.md CHANGED
@@ -1,15 +1,21 @@
1
1
  # Fluxmail
2
2
 
3
- Fluxmail is a self-hosted MCP server that connects AI agents to Gmail and IMAP/SMTP mailboxes. It gives MCP clients a consistent set of tools to read, search, draft, send, and organize email.
3
+ [Fluxmail](https://github.com/churichard/fluxmail-mcp) is a self-hosted MCP server that connects AI agents to Gmail and IMAP/SMTP mailboxes. It gives MCP clients a consistent set of tools to read, search, draft, send, and organize email.
4
4
 
5
5
  ## Install
6
6
 
7
- Fluxmail requires Node.js 20 or later.
7
+ Fluxmail requires Node.js 20.20.x, or Node.js 22.22 or later.
8
8
 
9
9
  ```bash
10
10
  npm install -g fluxmail
11
11
  ```
12
12
 
13
+ To run Fluxmail without installing it globally:
14
+
15
+ ```bash
16
+ npx fluxmail
17
+ ```
18
+
13
19
  ## Connect Gmail
14
20
 
15
21
  Create a Google Cloud OAuth client with the Gmail API enabled, then save its credentials:
@@ -20,7 +26,15 @@ fluxmail config set GOOGLE_CLIENT_SECRET <your-client-secret>
20
26
  fluxmail accounts add gmail
21
27
  ```
22
28
 
23
- The last command opens Google's consent flow in your browser.
29
+ For a local installation, the last command starts Google's consent flow in your browser.
30
+
31
+ For Docker or a remote server, set `FLUXMAIL_PUBLIC_URL` to the server's public HTTPS address and register `<FLUXMAIL_PUBLIC_URL>/auth/google/callback` as an authorized redirect URI in Google Cloud. Run the command on the server, then open the printed URL in your local browser:
32
+
33
+ ```bash
34
+ docker compose exec fluxmail fluxmail accounts add gmail
35
+ ```
36
+
37
+ Open the printed link, then select Continue with Google. The link expires after 10 minutes and works once. Fluxmail selects the flow from `FLUXMAIL_PUBLIC_URL`; `--local` and `--hosted` override that selection when needed.
24
38
 
25
39
  ## Connect IMAP
26
40
 
@@ -35,7 +49,7 @@ Fluxmail prompts for the password and stores it encrypted. Use `--imap-password-
35
49
 
36
50
  ## Connect an MCP client
37
51
 
38
- Configure your client to launch `fluxmail stdio`. For example, with Claude Code:
52
+ For example, with Claude Code:
39
53
 
40
54
  ```bash
41
55
  claude mcp add fluxmail -- fluxmail stdio
@@ -47,6 +61,8 @@ Or with Codex CLI:
47
61
  codex mcp add fluxmail -- fluxmail stdio
48
62
  ```
49
63
 
64
+ To have your MCP client run Fluxmail without a global installation, replace `fluxmail stdio` with `npx -y fluxmail@latest stdio`. Replace `@latest` with an exact version to keep the client on that version.
65
+
50
66
  Fluxmail also supports Streamable HTTP for Docker and remote deployments.
51
67
 
52
68
  See the [setup guide](https://fluxmail.ai/docs/quickstart) for Google OAuth configuration, Docker deployment, supported MCP clients, environment variables, and the complete tool list.
@@ -0,0 +1,17 @@
1
+ import type { FluxmailConfig } from '../config.js';
2
+ import type { FluxmailDb } from '../storage/db.js';
3
+ import { type GmailConnectionIntent } from '../storage/gmailConnectionGrants.js';
4
+ export type GmailConnectionMode = 'local' | 'hosted';
5
+ export declare function validateAccountConnectionFlags(provider: 'gmail' | 'imap', options: {
6
+ local?: boolean;
7
+ hosted?: boolean;
8
+ }): void;
9
+ export declare function selectGmailConnectionMode(config: FluxmailConfig, options: {
10
+ local?: boolean;
11
+ hosted?: boolean;
12
+ }): GmailConnectionMode;
13
+ export declare function prepareHostedGmailConnection(db: FluxmailDb, config: FluxmailConfig, intent: GmailConnectionIntent): {
14
+ connectionUrl: string;
15
+ expiresAt: number;
16
+ };
17
+ //# sourceMappingURL=gmailConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gmailConnection.d.ts","sourceRoot":"","sources":["../../src/accounts/gmailConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAA8B,KAAK,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAG7G,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAOrD,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,OAAO,GAAG,MAAM,EAC1B,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7C,IAAI,CAIN;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7C,mBAAmB,CAUrB;AAED,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,qBAAqB,GAC5B;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAO9C"}
@@ -0,0 +1,32 @@
1
+ import { EmailError } from '@fluxmail/core';
2
+ import { createGmailConnectionGrant } from '../storage/gmailConnectionGrants.js';
3
+ import { requireGoogleConfig } from './googleAuth.js';
4
+ function isLoopbackPublicUrl(publicUrl) {
5
+ const hostname = new URL(publicUrl).hostname;
6
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
7
+ }
8
+ export function validateAccountConnectionFlags(provider, options) {
9
+ if (provider === 'imap' && (options.local || options.hosted)) {
10
+ throw new EmailError('invalid_request', '--local and --hosted are only available for Gmail accounts.');
11
+ }
12
+ }
13
+ export function selectGmailConnectionMode(config, options) {
14
+ if (options.local && options.hosted) {
15
+ throw new EmailError('invalid_request', '--local and --hosted cannot be used together.');
16
+ }
17
+ if (options.hosted && !config.publicUrlConfigured) {
18
+ throw new EmailError('invalid_request', '--hosted requires FLUXMAIL_PUBLIC_URL to be set.');
19
+ }
20
+ return options.hosted || (config.publicUrlConfigured && !options.local && !isLoopbackPublicUrl(config.publicUrl))
21
+ ? 'hosted'
22
+ : 'local';
23
+ }
24
+ export function prepareHostedGmailConnection(db, config, intent) {
25
+ requireGoogleConfig(config);
26
+ const { token, expiresAt } = createGmailConnectionGrant(db, intent);
27
+ return {
28
+ connectionUrl: `${config.publicUrl}/auth/google/connect?token=${encodeURIComponent(token)}`,
29
+ expiresAt,
30
+ };
31
+ }
32
+ //# sourceMappingURL=gmailConnection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gmailConnection.js","sourceRoot":"","sources":["../../src/accounts/gmailConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,EAAE,0BAA0B,EAA8B,MAAM,qCAAqC,CAAC;AAC7G,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAItD,SAAS,mBAAmB,CAAC,SAAiB;IAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC;IAC7C,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,OAAO,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,QAA0B,EAC1B,OAA8C;IAE9C,IAAI,QAAQ,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,6DAA6D,CAAC,CAAC;IACzG,CAAC;AACH,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,MAAsB,EACtB,OAA8C;IAE9C,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACpC,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,+CAA+C,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAClD,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,kDAAkD,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/G,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,OAAO,CAAC;AACd,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,EAAc,EACd,MAAsB,EACtB,MAA6B;IAE7B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,0BAA0B,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACpE,OAAO;QACL,aAAa,EAAE,GAAG,MAAM,CAAC,SAAS,8BAA8B,kBAAkB,CAAC,KAAK,CAAC,EAAE;QAC3F,SAAS;KACV,CAAC;AACJ,CAAC"}
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { type PermissionPolicy } from './permissions.js';
3
+ interface PermissionOptions {
4
+ profile?: string;
5
+ allow: string[];
6
+ }
7
+ export declare function permissionPolicyFromOptions(opts: PermissionOptions, requireSelection?: boolean): PermissionPolicy;
2
8
  export {};
3
9
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAwCA,OAAO,EAOL,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAkC1B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAMD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,iBAAiB,EAAE,gBAAgB,UAAQ,GAAG,gBAAgB,CAa/G"}
package/dist/cli.js CHANGED
@@ -9,7 +9,8 @@ import { configFilePath, maskStoredConfigValue, readStoredConfig, resolveDataDir
9
9
  import { createApp } from './http/app.js';
10
10
  import { buildMcpServer } from './mcp/buildServer.js';
11
11
  import { runLoopbackFlow } from './accounts/googleAuth.js';
12
- import { createApiKey, listApiKeys, revokeApiKey } from './storage/apiKeys.js';
12
+ import { prepareHostedGmailConnection, selectGmailConnectionMode, validateAccountConnectionFlags, } from './accounts/gmailConnection.js';
13
+ import { createApiKey, listApiKeys, revokeApiKey, updateApiKeyPermissions } from './storage/apiKeys.js';
13
14
  import { addMember, findMember, listMembers, removeMember } from './storage/members.js';
14
15
  import { activateLicense } from './licensing/activation.js';
15
16
  import { LICENSE_KEY_PATTERN, releaseLicense } from './licensing/client.js';
@@ -18,6 +19,27 @@ import { checkLicenseState, clearLease, getEntitlements, readLeaseRow, } from '.
18
19
  import { loadInstanceId, startLicenseRefresher } from './licensing/refresher.js';
19
20
  import { VERSION } from './version.js';
20
21
  import { countPending } from './storage/scheduledSends.js';
22
+ import { customPermissionPolicy, FULL_PERMISSION_POLICY, isNamedPermissionProfile, MCP_CAPABILITIES, NAMED_PERMISSION_PROFILES, permissionPolicyForProfile, } from './permissions.js';
23
+ import { getTelemetry, installTelemetryStreamEndHandler, isTelemetryEnabled, setTelemetryEnabled, shutdownTelemetry, } from './telemetry.js';
24
+ function collectOption(value, previous) {
25
+ return [...previous, value];
26
+ }
27
+ export function permissionPolicyFromOptions(opts, requireSelection = false) {
28
+ if (opts.profile && opts.allow.length) {
29
+ throw new Error('--profile cannot be combined with --allow.');
30
+ }
31
+ if (opts.profile) {
32
+ if (!isNamedPermissionProfile(opts.profile)) {
33
+ throw new Error(`Unknown profile "${opts.profile}". Expected one of: ${NAMED_PERMISSION_PROFILES.join(', ')}.`);
34
+ }
35
+ return permissionPolicyForProfile(opts.profile);
36
+ }
37
+ if (opts.allow.length)
38
+ return customPermissionPolicy(opts.allow);
39
+ if (requireSelection)
40
+ throw new Error('Choose --profile or at least one --allow capability.');
41
+ return FULL_PERMISSION_POLICY;
42
+ }
21
43
  function connectionPort(value, option) {
22
44
  const port = Number(value);
23
45
  if (!Number.isInteger(port) || port < 1 || port > 65_535) {
@@ -93,15 +115,72 @@ function warnLicense(db, log = console.error) {
93
115
  log(`Warning: ${warning}`);
94
116
  }
95
117
  const program = new Command();
96
- program.name('fluxmail').description('Fluxmail, a self-hosted MCP server for your email').version(VERSION);
118
+ program
119
+ .name('fluxmail')
120
+ .description('Fluxmail, a self-hosted MCP server for your email')
121
+ .version(VERSION, '-v, --version');
122
+ function commandPath(command) {
123
+ const names = [];
124
+ for (let current = command; current?.parent; current = current.parent)
125
+ names.unshift(current.name());
126
+ return names.join(' ');
127
+ }
128
+ let telemetrySignalHandlersInstalled = false;
129
+ const TELEMETRY_SIGNAL_SHUTDOWN_TIMEOUT_MS = 1_000;
130
+ /** Flush queued telemetry before restoring Node's default signal behavior. */
131
+ function installTelemetrySignalHandlers() {
132
+ if (telemetrySignalHandlersInstalled)
133
+ return;
134
+ telemetrySignalHandlersInstalled = true;
135
+ const shutdownAndResignal = (signal) => {
136
+ process.off('SIGINT', onSigint);
137
+ process.off('SIGTERM', onSigterm);
138
+ process.off('SIGHUP', onSighup);
139
+ let resignaled = false;
140
+ const resignal = () => {
141
+ if (resignaled)
142
+ return;
143
+ resignaled = true;
144
+ process.kill(process.pid, signal);
145
+ };
146
+ const timeout = setTimeout(resignal, TELEMETRY_SIGNAL_SHUTDOWN_TIMEOUT_MS);
147
+ void shutdownTelemetry().finally(() => {
148
+ clearTimeout(timeout);
149
+ resignal();
150
+ });
151
+ };
152
+ const onSigint = () => shutdownAndResignal('SIGINT');
153
+ const onSigterm = () => shutdownAndResignal('SIGTERM');
154
+ const onSighup = () => shutdownAndResignal('SIGHUP');
155
+ process.once('SIGINT', onSigint);
156
+ process.once('SIGTERM', onSigterm);
157
+ process.once('SIGHUP', onSighup);
158
+ }
159
+ program.hook('preAction', (_command, actionCommand) => {
160
+ const command = commandPath(actionCommand);
161
+ // Respect the opt-out before creating a telemetry client or recording this command.
162
+ if (command === 'telemetry disable')
163
+ return;
164
+ const dataDir = resolveDataDir();
165
+ getTelemetry(dataDir).capture('cli command used', {
166
+ product_surface: 'cli',
167
+ command,
168
+ });
169
+ });
170
+ program.hook('postAction', async (_command, actionCommand) => {
171
+ if (actionCommand.name() !== 'serve' && actionCommand.name() !== 'stdio')
172
+ await shutdownTelemetry();
173
+ });
97
174
  program
98
175
  .command('serve')
99
176
  .description('Run the HTTP server (Streamable HTTP MCP at /mcp)')
100
177
  .action(() => {
178
+ installTelemetrySignalHandlers();
101
179
  const ctx = createContext();
102
180
  const app = createApp(ctx);
103
181
  ctx.scheduler.start();
104
182
  serve({ fetch: app.fetch, port: ctx.config.port }, () => {
183
+ ctx.telemetry.capture('mcp server started', { product_surface: 'mcp', transport: 'http' });
105
184
  console.log(`Fluxmail listening on ${ctx.config.publicUrl}`);
106
185
  console.log(` MCP endpoint: ${ctx.config.publicUrl}/mcp`);
107
186
  console.log(` Auth mode: ${ctx.config.authMode}`);
@@ -125,11 +204,22 @@ program
125
204
  program
126
205
  .command('stdio')
127
206
  .description('Run as a stdio MCP server (for Claude Desktop / Claude Code local config)')
128
- .action(async () => {
207
+ .option('--profile <profile>', `Tool profile: ${NAMED_PERMISSION_PROFILES.join(', ')}`)
208
+ .option('--allow <capability>', 'Allow one MCP capability; repeat as needed', collectOption, [])
209
+ .action(async (opts) => {
210
+ installTelemetrySignalHandlers();
211
+ installTelemetryStreamEndHandler(process.stdin);
129
212
  const ctx = createContext();
130
- const server = buildMcpServer(ctx.service);
213
+ const permissions = permissionPolicyFromOptions(opts);
214
+ const server = buildMcpServer(ctx.service, {
215
+ permissions,
216
+ maxAttachmentBytes: ctx.config.maxAttachmentBytes,
217
+ telemetry: ctx.telemetry,
218
+ transport: 'stdio',
219
+ });
131
220
  ctx.scheduler.start();
132
221
  await server.connect(new StdioServerTransport());
222
+ ctx.telemetry.capture('mcp server started', { product_surface: 'mcp', transport: 'stdio' });
133
223
  // stdout belongs to the MCP protocol; log to stderr only.
134
224
  startLicenseRefresher({
135
225
  db: ctx.db,
@@ -149,6 +239,8 @@ accounts
149
239
  .argument('<provider>', 'Email provider: gmail or imap')
150
240
  .option('--reauthorize <account-id>', 'Reconnect an existing account')
151
241
  .option('--member <member>', 'Member (id or email) who owns the new mailbox; omit for a shared mailbox')
242
+ .option('--local', 'Use the local browser callback for Gmail')
243
+ .option('--hosted', 'Use FLUXMAIL_PUBLIC_URL for the Gmail callback')
152
244
  .option('--email <address>', 'Mailbox address (required for IMAP)')
153
245
  .option('--display-name <name>', 'Sender name for IMAP messages')
154
246
  .option('--imap-host <host>', 'IMAP server hostname')
@@ -177,6 +269,7 @@ accounts
177
269
  const ctx = createContext();
178
270
  warnLicense(ctx.db);
179
271
  try {
272
+ validateAccountConnectionFlags(provider, opts);
180
273
  if (opts.reauthorize && opts.member) {
181
274
  throw new EmailError('invalid_request', '--member cannot be combined with --reauthorize; use "fluxmail accounts assign" to change ownership.');
182
275
  }
@@ -240,6 +333,17 @@ accounts
240
333
  console.log(`Warning: ${warning.message}.`);
241
334
  return;
242
335
  }
336
+ const gmailConnectionMode = selectGmailConnectionMode(ctx.config, opts);
337
+ if (gmailConnectionMode === 'hosted') {
338
+ const { connectionUrl } = prepareHostedGmailConnection(ctx.db, ctx.config, {
339
+ ...(member ? { memberId: member.id } : {}),
340
+ ...(existing ? { reauthorizeAccountId: existing.id } : {}),
341
+ });
342
+ console.log('\nOpen this URL in your browser to connect Gmail:\n');
343
+ console.log(` ${connectionUrl}\n`);
344
+ console.log('This link expires in 10 minutes and can only be used once.');
345
+ return;
346
+ }
243
347
  const account = await runLoopbackFlow(ctx.config, (url) => {
244
348
  console.log('\nOpen this URL in your browser to authorize Gmail access:\n');
245
349
  console.log(` ${url}\n`);
@@ -425,18 +529,30 @@ membersCmd
425
529
  }
426
530
  });
427
531
  const apikey = program.command('apikey').description('Manage API keys for the HTTP MCP endpoint');
532
+ apikey
533
+ .command('capabilities')
534
+ .description('List MCP capabilities for custom permission policies')
535
+ .action(() => {
536
+ for (const capability of MCP_CAPABILITIES)
537
+ console.log(capability);
538
+ });
428
539
  apikey
429
540
  .command('create')
430
541
  .requiredOption('--name <name>', 'Human-readable key name')
431
542
  .option('--member <member>', 'Member (id or email) the key is issued to')
543
+ .option('--profile <profile>', `Tool profile: ${NAMED_PERMISSION_PROFILES.join(', ')}`)
544
+ .option('--allow <capability>', 'Allow one MCP capability; repeat as needed', collectOption, [])
432
545
  .description('Create an API key (shown once)')
433
546
  .action((opts) => {
434
547
  const ctx = createContext();
435
548
  warnLicense(ctx.db);
436
549
  try {
437
550
  const member = opts.member ? findMember(ctx.db, opts.member) : undefined;
438
- const { key, info } = createApiKey(ctx.db, opts.name, member?.id);
439
- console.log(`Created API key "${info.name}" (id: ${info.id})` + (member ? ` for member ${member.name}` : '') + '\n');
551
+ const permissions = permissionPolicyFromOptions(opts);
552
+ const { key, info } = createApiKey(ctx.db, opts.name, member?.id, permissions);
553
+ console.log(`Created API key "${info.name}" (id: ${info.id}, profile: ${info.permissionProfile})` +
554
+ (member ? ` for member ${member.name}` : '') +
555
+ '\n');
440
556
  console.log(` ${key}\n`);
441
557
  console.log('Store it now; it cannot be shown again.');
442
558
  }
@@ -460,7 +576,32 @@ apikey
460
576
  for (const k of keys) {
461
577
  const lastUsed = k.lastUsedAt ? new Date(k.lastUsedAt).toISOString() : 'never';
462
578
  const member = k.memberId ? (memberNames.get(k.memberId) ?? k.memberId) : '-';
463
- console.log(`${k.id} ${k.name} member=${member} created=${new Date(k.createdAt).toISOString()} lastUsed=${lastUsed}`);
579
+ console.log(`${k.id} ${k.name} member=${member} profile=${k.permissionProfile}` +
580
+ (k.permissionProfile === 'custom' ? ` allows=${k.capabilities.join(',')}` : '') +
581
+ ` created=${new Date(k.createdAt).toISOString()} lastUsed=${lastUsed}`);
582
+ }
583
+ });
584
+ apikey
585
+ .command('permissions')
586
+ .argument('<keyId>')
587
+ .option('--profile <profile>', `Tool profile: ${NAMED_PERMISSION_PROFILES.join(', ')}`)
588
+ .option('--allow <capability>', 'Allow one MCP capability; repeat as needed', collectOption, [])
589
+ .description('Change the MCP permissions for an API key')
590
+ .action((keyId, opts) => {
591
+ const ctx = createContext();
592
+ warnLicense(ctx.db);
593
+ try {
594
+ const permissions = permissionPolicyFromOptions(opts, true);
595
+ if (!updateApiKeyPermissions(ctx.db, keyId, permissions)) {
596
+ console.error(`No key with id ${keyId}`);
597
+ process.exitCode = 1;
598
+ return;
599
+ }
600
+ console.log(`Updated ${keyId} to profile ${permissions.profile}`);
601
+ }
602
+ catch (err) {
603
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
604
+ process.exitCode = 1;
464
605
  }
465
606
  });
466
607
  apikey
@@ -614,6 +755,31 @@ configCmd
614
755
  console.log(`${key}=${maskStoredConfigValue(key, value)}`);
615
756
  }
616
757
  });
758
+ const telemetryCmd = program.command('telemetry').description('Manage anonymous usage telemetry');
759
+ telemetryCmd
760
+ .command('disable')
761
+ .description('Stop sending anonymous usage telemetry')
762
+ .action(() => {
763
+ const dataDir = resolveDataDir();
764
+ setTelemetryEnabled(dataDir, false);
765
+ console.log('Telemetry disabled.');
766
+ });
767
+ telemetryCmd
768
+ .command('enable')
769
+ .description('Allow anonymous usage telemetry')
770
+ .action(() => {
771
+ const dataDir = resolveDataDir();
772
+ setTelemetryEnabled(dataDir, true);
773
+ unsetStoredConfig(dataDir, 'FLUXMAIL_TELEMETRY');
774
+ console.log('Telemetry enabled. FLUXMAIL_TELEMETRY=0 or DO_NOT_TRACK=1 can still turn it off.');
775
+ });
776
+ telemetryCmd
777
+ .command('status')
778
+ .description('Show whether anonymous usage telemetry is enabled')
779
+ .action(() => {
780
+ const dataDir = resolveDataDir();
781
+ console.log(`Telemetry is ${isTelemetryEnabled(dataDir) ? 'enabled' : 'disabled'}.`);
782
+ });
617
783
  program
618
784
  .command('status')
619
785
  .description('Show accounts, members, entitlements, and provider availability')
@@ -622,8 +788,9 @@ program
622
788
  warnLicense(ctx.db);
623
789
  console.log(JSON.stringify(await ctx.service.status(), null, 2));
624
790
  });
625
- program.parseAsync(process.argv).catch((err) => {
791
+ program.parseAsync(process.argv).catch(async (err) => {
626
792
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
627
793
  process.exitCode = 1;
794
+ await shutdownTelemetry();
628
795
  });
629
796
  //# sourceMappingURL=cli.js.map