agentmail-clone-v1 0.1.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 (227) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/docs-deploy.yml +37 -0
  3. package/.github/workflows/landing-preview.yml +43 -0
  4. package/.github/workflows/openapi-lint.yml +31 -0
  5. package/.github/workflows/sdk-generate-check.yml +66 -0
  6. package/.github/workflows/sdk-release.yml +62 -0
  7. package/CHANGELOG.md +10 -0
  8. package/README.md +208 -0
  9. package/RELEASING.md +43 -0
  10. package/docs/api-reference/api-keys.mdx +11 -0
  11. package/docs/api-reference/domains.mdx +13 -0
  12. package/docs/api-reference/emails.mdx +26 -0
  13. package/docs/api-reference/inboxes.mdx +13 -0
  14. package/docs/api-reference/metrics.mdx +10 -0
  15. package/docs/authentication.mdx +25 -0
  16. package/docs/docs.json +35 -0
  17. package/docs/errors.mdx +34 -0
  18. package/docs/favicon.svg +5 -0
  19. package/docs/idempotency.mdx +18 -0
  20. package/docs/index.mdx +24 -0
  21. package/docs/quickstart.mdx +134 -0
  22. package/landing/DEPLOYING.md +33 -0
  23. package/landing/favicon.svg +5 -0
  24. package/landing/index.html +129 -0
  25. package/landing/main.js +45 -0
  26. package/landing/privacy.html +29 -0
  27. package/landing/styles.css +356 -0
  28. package/landing/terms.html +29 -0
  29. package/netlify.toml +15 -0
  30. package/openapi/openapi.yaml +1016 -0
  31. package/package.json +34 -0
  32. package/render.yaml +48 -0
  33. package/scripts/generate-sdk-py.sh +16 -0
  34. package/scripts/generate-sdk-ts.sh +16 -0
  35. package/scripts/migrate.js +66 -0
  36. package/scripts/validate-docs.js +56 -0
  37. package/scripts/validate-landing.js +39 -0
  38. package/sdks/python/README.md +40 -0
  39. package/sdks/python/emailagent_sdk/__init__.py +157 -0
  40. package/sdks/python/generated/.openapi-generator/FILES +101 -0
  41. package/sdks/python/generated/.openapi-generator/VERSION +1 -0
  42. package/sdks/python/generated/.openapi-generator-ignore +23 -0
  43. package/sdks/python/generated/emailagent_sdk_generated/__init__.py +105 -0
  44. package/sdks/python/generated/emailagent_sdk_generated/api/__init__.py +9 -0
  45. package/sdks/python/generated/emailagent_sdk_generated/api/api_keys_api.py +1162 -0
  46. package/sdks/python/generated/emailagent_sdk_generated/api/domains_api.py +1168 -0
  47. package/sdks/python/generated/emailagent_sdk_generated/api/emails_api.py +1232 -0
  48. package/sdks/python/generated/emailagent_sdk_generated/api/inboxes_api.py +1191 -0
  49. package/sdks/python/generated/emailagent_sdk_generated/api/metrics_api.py +285 -0
  50. package/sdks/python/generated/emailagent_sdk_generated/api_client.py +801 -0
  51. package/sdks/python/generated/emailagent_sdk_generated/api_response.py +21 -0
  52. package/sdks/python/generated/emailagent_sdk_generated/configuration.py +586 -0
  53. package/sdks/python/generated/emailagent_sdk_generated/docs/APIKeysApi.md +334 -0
  54. package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyCreated.md +35 -0
  55. package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyCreatedResponse.md +29 -0
  56. package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyListItem.md +35 -0
  57. package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyListResponse.md +29 -0
  58. package/sdks/python/generated/emailagent_sdk_generated/docs/CreateApiKeyRequest.md +30 -0
  59. package/sdks/python/generated/emailagent_sdk_generated/docs/CreateApiKeyRequestScopes.md +29 -0
  60. package/sdks/python/generated/emailagent_sdk_generated/docs/CreateDomainRequest.md +29 -0
  61. package/sdks/python/generated/emailagent_sdk_generated/docs/CreateInboxRequest.md +31 -0
  62. package/sdks/python/generated/emailagent_sdk_generated/docs/Domain.md +37 -0
  63. package/sdks/python/generated/emailagent_sdk_generated/docs/DomainListResponse.md +29 -0
  64. package/sdks/python/generated/emailagent_sdk_generated/docs/DomainResponse.md +29 -0
  65. package/sdks/python/generated/emailagent_sdk_generated/docs/DomainsApi.md +336 -0
  66. package/sdks/python/generated/emailagent_sdk_generated/docs/Email.md +43 -0
  67. package/sdks/python/generated/emailagent_sdk_generated/docs/EmailListResponse.md +29 -0
  68. package/sdks/python/generated/emailagent_sdk_generated/docs/EmailResponse.md +29 -0
  69. package/sdks/python/generated/emailagent_sdk_generated/docs/EmailsApi.md +353 -0
  70. package/sdks/python/generated/emailagent_sdk_generated/docs/ErrorResponse.md +29 -0
  71. package/sdks/python/generated/emailagent_sdk_generated/docs/Inbox.md +38 -0
  72. package/sdks/python/generated/emailagent_sdk_generated/docs/InboxListResponse.md +29 -0
  73. package/sdks/python/generated/emailagent_sdk_generated/docs/InboxResponse.md +29 -0
  74. package/sdks/python/generated/emailagent_sdk_generated/docs/InboxesApi.md +337 -0
  75. package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsApi.md +83 -0
  76. package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsData.md +35 -0
  77. package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsResponse.md +29 -0
  78. package/sdks/python/generated/emailagent_sdk_generated/docs/OkResponse.md +29 -0
  79. package/sdks/python/generated/emailagent_sdk_generated/docs/PlanLimits.md +32 -0
  80. package/sdks/python/generated/emailagent_sdk_generated/docs/SendEmailRequest.md +32 -0
  81. package/sdks/python/generated/emailagent_sdk_generated/docs/UpdateEmailReadRequest.md +29 -0
  82. package/sdks/python/generated/emailagent_sdk_generated/docs/UpdateInboxRequest.md +29 -0
  83. package/sdks/python/generated/emailagent_sdk_generated/exceptions.py +216 -0
  84. package/sdks/python/generated/emailagent_sdk_generated/models/__init__.py +41 -0
  85. package/sdks/python/generated/emailagent_sdk_generated/models/api_key_created.py +113 -0
  86. package/sdks/python/generated/emailagent_sdk_generated/models/api_key_created_response.py +91 -0
  87. package/sdks/python/generated/emailagent_sdk_generated/models/api_key_list_item.py +123 -0
  88. package/sdks/python/generated/emailagent_sdk_generated/models/api_key_list_response.py +95 -0
  89. package/sdks/python/generated/emailagent_sdk_generated/models/create_api_key_request.py +93 -0
  90. package/sdks/python/generated/emailagent_sdk_generated/models/create_api_key_request_scopes.py +143 -0
  91. package/sdks/python/generated/emailagent_sdk_generated/models/create_domain_request.py +87 -0
  92. package/sdks/python/generated/emailagent_sdk_generated/models/create_inbox_request.py +91 -0
  93. package/sdks/python/generated/emailagent_sdk_generated/models/domain.py +134 -0
  94. package/sdks/python/generated/emailagent_sdk_generated/models/domain_list_response.py +95 -0
  95. package/sdks/python/generated/emailagent_sdk_generated/models/domain_response.py +91 -0
  96. package/sdks/python/generated/emailagent_sdk_generated/models/email.py +175 -0
  97. package/sdks/python/generated/emailagent_sdk_generated/models/email_list_response.py +95 -0
  98. package/sdks/python/generated/emailagent_sdk_generated/models/email_response.py +91 -0
  99. package/sdks/python/generated/emailagent_sdk_generated/models/error_response.py +87 -0
  100. package/sdks/python/generated/emailagent_sdk_generated/models/inbox.py +136 -0
  101. package/sdks/python/generated/emailagent_sdk_generated/models/inbox_list_response.py +95 -0
  102. package/sdks/python/generated/emailagent_sdk_generated/models/inbox_response.py +91 -0
  103. package/sdks/python/generated/emailagent_sdk_generated/models/metrics_data.py +110 -0
  104. package/sdks/python/generated/emailagent_sdk_generated/models/metrics_response.py +91 -0
  105. package/sdks/python/generated/emailagent_sdk_generated/models/ok_response.py +87 -0
  106. package/sdks/python/generated/emailagent_sdk_generated/models/plan_limits.py +93 -0
  107. package/sdks/python/generated/emailagent_sdk_generated/models/send_email_request.py +93 -0
  108. package/sdks/python/generated/emailagent_sdk_generated/models/update_email_read_request.py +87 -0
  109. package/sdks/python/generated/emailagent_sdk_generated/models/update_inbox_request.py +92 -0
  110. package/sdks/python/generated/emailagent_sdk_generated/rest.py +258 -0
  111. package/sdks/python/generated/emailagent_sdk_generated/test/__init__.py +0 -0
  112. package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_created.py +68 -0
  113. package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_created_response.py +52 -0
  114. package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_list_item.py +66 -0
  115. package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_list_response.py +56 -0
  116. package/sdks/python/generated/emailagent_sdk_generated/test/test_api_keys_api.py +59 -0
  117. package/sdks/python/generated/emailagent_sdk_generated/test/test_create_api_key_request.py +53 -0
  118. package/sdks/python/generated/emailagent_sdk_generated/test/test_create_api_key_request_scopes.py +50 -0
  119. package/sdks/python/generated/emailagent_sdk_generated/test/test_create_domain_request.py +52 -0
  120. package/sdks/python/generated/emailagent_sdk_generated/test/test_create_inbox_request.py +54 -0
  121. package/sdks/python/generated/emailagent_sdk_generated/test/test_domain.py +70 -0
  122. package/sdks/python/generated/emailagent_sdk_generated/test/test_domain_list_response.py +56 -0
  123. package/sdks/python/generated/emailagent_sdk_generated/test/test_domain_response.py +52 -0
  124. package/sdks/python/generated/emailagent_sdk_generated/test/test_domains_api.py +59 -0
  125. package/sdks/python/generated/emailagent_sdk_generated/test/test_email.py +79 -0
  126. package/sdks/python/generated/emailagent_sdk_generated/test/test_email_list_response.py +56 -0
  127. package/sdks/python/generated/emailagent_sdk_generated/test/test_email_response.py +52 -0
  128. package/sdks/python/generated/emailagent_sdk_generated/test/test_emails_api.py +59 -0
  129. package/sdks/python/generated/emailagent_sdk_generated/test/test_error_response.py +52 -0
  130. package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox.py +68 -0
  131. package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox_list_response.py +56 -0
  132. package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox_response.py +52 -0
  133. package/sdks/python/generated/emailagent_sdk_generated/test/test_inboxes_api.py +59 -0
  134. package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_api.py +38 -0
  135. package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_data.py +72 -0
  136. package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_response.py +74 -0
  137. package/sdks/python/generated/emailagent_sdk_generated/test/test_ok_response.py +52 -0
  138. package/sdks/python/generated/emailagent_sdk_generated/test/test_plan_limits.py +58 -0
  139. package/sdks/python/generated/emailagent_sdk_generated/test/test_send_email_request.py +56 -0
  140. package/sdks/python/generated/emailagent_sdk_generated/test/test_update_email_read_request.py +52 -0
  141. package/sdks/python/generated/emailagent_sdk_generated/test/test_update_inbox_request.py +52 -0
  142. package/sdks/python/generated/emailagent_sdk_generated_README.md +140 -0
  143. package/sdks/python/openapitools.json +7 -0
  144. package/sdks/python/pyproject.toml +19 -0
  145. package/sdks/typescript/README.md +41 -0
  146. package/sdks/typescript/generated/.openapi-generator/FILES +41 -0
  147. package/sdks/typescript/generated/.openapi-generator/VERSION +1 -0
  148. package/sdks/typescript/generated/.openapi-generator-ignore +23 -0
  149. package/sdks/typescript/generated/package.json +21 -0
  150. package/sdks/typescript/generated/src/apis/APIKeysApi.ts +314 -0
  151. package/sdks/typescript/generated/src/apis/DomainsApi.ts +314 -0
  152. package/sdks/typescript/generated/src/apis/EmailsApi.ts +350 -0
  153. package/sdks/typescript/generated/src/apis/InboxesApi.ts +329 -0
  154. package/sdks/typescript/generated/src/apis/MetricsApi.ts +93 -0
  155. package/sdks/typescript/generated/src/apis/index.ts +7 -0
  156. package/sdks/typescript/generated/src/index.ts +5 -0
  157. package/sdks/typescript/generated/src/models/ApiKeyCreated.ts +123 -0
  158. package/sdks/typescript/generated/src/models/ApiKeyCreatedResponse.ts +74 -0
  159. package/sdks/typescript/generated/src/models/ApiKeyListItem.ts +121 -0
  160. package/sdks/typescript/generated/src/models/ApiKeyListResponse.ts +74 -0
  161. package/sdks/typescript/generated/src/models/CreateApiKeyRequest.ts +82 -0
  162. package/sdks/typescript/generated/src/models/CreateApiKeyRequestScopes.ts +45 -0
  163. package/sdks/typescript/generated/src/models/CreateDomainRequest.ts +66 -0
  164. package/sdks/typescript/generated/src/models/CreateInboxRequest.ts +82 -0
  165. package/sdks/typescript/generated/src/models/Domain.ts +152 -0
  166. package/sdks/typescript/generated/src/models/DomainListResponse.ts +74 -0
  167. package/sdks/typescript/generated/src/models/DomainResponse.ts +74 -0
  168. package/sdks/typescript/generated/src/models/Email.ts +222 -0
  169. package/sdks/typescript/generated/src/models/EmailListResponse.ts +74 -0
  170. package/sdks/typescript/generated/src/models/EmailResponse.ts +74 -0
  171. package/sdks/typescript/generated/src/models/ErrorResponse.ts +66 -0
  172. package/sdks/typescript/generated/src/models/Inbox.ts +159 -0
  173. package/sdks/typescript/generated/src/models/InboxListResponse.ts +74 -0
  174. package/sdks/typescript/generated/src/models/InboxResponse.ts +74 -0
  175. package/sdks/typescript/generated/src/models/MetricsData.ts +139 -0
  176. package/sdks/typescript/generated/src/models/MetricsResponse.ts +74 -0
  177. package/sdks/typescript/generated/src/models/OkResponse.ts +66 -0
  178. package/sdks/typescript/generated/src/models/PlanLimits.ts +93 -0
  179. package/sdks/typescript/generated/src/models/SendEmailRequest.ts +91 -0
  180. package/sdks/typescript/generated/src/models/UpdateEmailReadRequest.ts +66 -0
  181. package/sdks/typescript/generated/src/models/UpdateInboxRequest.ts +66 -0
  182. package/sdks/typescript/generated/src/models/index.ts +27 -0
  183. package/sdks/typescript/generated/src/runtime.ts +432 -0
  184. package/sdks/typescript/generated/tsconfig.esm.json +7 -0
  185. package/sdks/typescript/generated/tsconfig.json +16 -0
  186. package/sdks/typescript/openapitools.json +8 -0
  187. package/sdks/typescript/package.json +27 -0
  188. package/sdks/typescript/src/index.ts +138 -0
  189. package/sdks/typescript/tsconfig.json +14 -0
  190. package/sql/001_init.sql +143 -0
  191. package/sql/002_local_auth.sql +38 -0
  192. package/sql/003_domain_routes.sql +2 -0
  193. package/sql/004_reliability_primitives.sql +75 -0
  194. package/sql/005_auth_email_flows.sql +22 -0
  195. package/src/config.js +30 -0
  196. package/src/db.js +25 -0
  197. package/src/lib/api-auth.js +55 -0
  198. package/src/lib/auth.js +71 -0
  199. package/src/lib/csrf.js +46 -0
  200. package/src/lib/dodo.js +67 -0
  201. package/src/lib/email-templates.js +67 -0
  202. package/src/lib/idempotency.js +85 -0
  203. package/src/lib/mailgun.js +188 -0
  204. package/src/lib/plan.js +24 -0
  205. package/src/lib/rate-limit.js +43 -0
  206. package/src/lib/security.js +62 -0
  207. package/src/lib/session.js +21 -0
  208. package/src/lib/store.js +638 -0
  209. package/src/lib/transactional-mailer.js +54 -0
  210. package/src/lib/validation.js +30 -0
  211. package/src/routes/api.js +485 -0
  212. package/src/routes/app.js +699 -0
  213. package/src/routes/auth.js +404 -0
  214. package/src/routes/webhooks.js +257 -0
  215. package/src/server.js +79 -0
  216. package/src/views/pages/admin.ejs +58 -0
  217. package/src/views/pages/api-keys.ejs +56 -0
  218. package/src/views/pages/billing.ejs +71 -0
  219. package/src/views/pages/domains.ejs +106 -0
  220. package/src/views/pages/inboxes.ejs +127 -0
  221. package/src/views/pages/login.ejs +57 -0
  222. package/src/views/pages/metrics.ejs +34 -0
  223. package/src/views/pages/reset-password.ejs +19 -0
  224. package/src/views/partials/bottom.ejs +3 -0
  225. package/src/views/partials/csrf-field.ejs +3 -0
  226. package/src/views/partials/flash.ejs +3 -0
  227. package/src/views/partials/top.ejs +130 -0
package/src/server.js ADDED
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import Fastify from 'fastify';
4
+ import fastifyFormbody from '@fastify/formbody';
5
+ import fastifyMultipart from '@fastify/multipart';
6
+ import fastifyView from '@fastify/view';
7
+ import fastifyRawBody from 'fastify-raw-body';
8
+ import ejs from 'ejs';
9
+
10
+ import { config } from './config.js';
11
+ import { pool } from './db.js';
12
+ import { authPlugin } from './lib/auth.js';
13
+ import { apiAuthPlugin } from './lib/api-auth.js';
14
+ import { authRoutes } from './routes/auth.js';
15
+ import { appRoutes } from './routes/app.js';
16
+ import { apiRoutes } from './routes/api.js';
17
+ import { webhookRoutes } from './routes/webhooks.js';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ const fastify = Fastify({
23
+ logger: true,
24
+ trustProxy: config.nodeEnv === 'production'
25
+ });
26
+
27
+ await fastify.register(fastifyFormbody);
28
+ await fastify.register(fastifyMultipart, {
29
+ attachFieldsToBody: true
30
+ });
31
+ await fastify.register(fastifyRawBody, {
32
+ field: 'rawBody',
33
+ global: false,
34
+ encoding: 'utf8',
35
+ runFirst: true
36
+ });
37
+ await fastify.register(fastifyView, {
38
+ engine: { ejs },
39
+ root: path.join(__dirname, 'views'),
40
+ includeViewExtension: true
41
+ });
42
+
43
+ await fastify.register(authPlugin);
44
+ await fastify.register(apiAuthPlugin);
45
+ await fastify.register(authRoutes);
46
+ await fastify.register(appRoutes);
47
+ await fastify.register(apiRoutes);
48
+ await fastify.register(webhookRoutes);
49
+
50
+ fastify.get('/healthz', async () => ({ ok: true }));
51
+
52
+ fastify.setErrorHandler((err, req, reply) => {
53
+ req.log.error({ err }, 'Unhandled error');
54
+ if (!reply.sent) {
55
+ reply.code(500).send('Internal Server Error');
56
+ }
57
+ });
58
+
59
+ async function start() {
60
+ try {
61
+ await fastify.listen({ port: config.port, host: '0.0.0.0' });
62
+ fastify.log.info(`Server running at ${config.appBaseUrl}`);
63
+ } catch (err) {
64
+ fastify.log.error(err);
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ async function shutdown(signal) {
70
+ fastify.log.info({ signal }, 'Shutting down');
71
+ await fastify.close();
72
+ await pool.end();
73
+ process.exit(0);
74
+ }
75
+
76
+ process.on('SIGINT', () => shutdown('SIGINT'));
77
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
78
+
79
+ start();
@@ -0,0 +1,58 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="grid three-col">
5
+ <div class="card">
6
+ <h3 style="margin-top:0;">Total Inboxes Created</h3>
7
+ <p><strong><%= globalMetrics.totalInboxesCreated %></strong></p>
8
+ </div>
9
+ <div class="card">
10
+ <h3 style="margin-top:0;">Total Domains Created</h3>
11
+ <p><strong><%= globalMetrics.totalDomainsCreated %></strong></p>
12
+ </div>
13
+ <div class="card">
14
+ <h3 style="margin-top:0;">Total API Keys Created</h3>
15
+ <p><strong><%= globalMetrics.totalApiKeysCreated %></strong></p>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="card stack" style="margin-top:16px;">
20
+ <h2 style="margin:0;">Plan Limits</h2>
21
+ <p class="muted">Use this to experiment with free and paid limits.</p>
22
+
23
+ <% ['free', 'paid'].forEach((planName) => { const limits = planLimits[planName]; %>
24
+ <div class="card stack" style="padding:12px;">
25
+ <h3 style="margin:0;text-transform:capitalize;"><%= planName %> plan</h3>
26
+ <form method="post" action="/app/admin/plan-limits" class="stack" hx-post="/app/admin/plan-limits">
27
+ <%- include('../partials/csrf-field') %>
28
+ <input type="hidden" name="plan" value="<%= planName %>" />
29
+
30
+ <div class="row">
31
+ <div>
32
+ <label>Max inboxes</label>
33
+ <input name="maxInboxes" type="number" min="0" value="<%= limits.maxInboxes %>" required />
34
+ </div>
35
+ <div>
36
+ <label>Max custom domains</label>
37
+ <input name="maxCustomDomains" type="number" min="0" value="<%= limits.maxCustomDomains %>" required />
38
+ </div>
39
+ </div>
40
+
41
+ <div class="row">
42
+ <div>
43
+ <label>Max API keys</label>
44
+ <input name="maxApiKeys" type="number" min="0" value="<%= limits.maxApiKeys %>" required />
45
+ </div>
46
+ <div>
47
+ <label>Monthly outbound email limit</label>
48
+ <input name="monthlyEmails" type="number" min="0" value="<%= limits.monthlyEmails %>" required />
49
+ </div>
50
+ </div>
51
+
52
+ <button type="submit" style="max-width:220px;">Save <%= planName %> limits</button>
53
+ </form>
54
+ </div>
55
+ <% }) %>
56
+ </div>
57
+
58
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,56 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="grid two-col">
5
+ <div class="card stack">
6
+ <h2 style="margin:0;">Create API Key</h2>
7
+ <form method="post" action="/app/api-keys" hx-post="/app/api-keys" class="stack">
8
+ <%- include('../partials/csrf-field') %>
9
+ <label>Name</label>
10
+ <input name="name" placeholder="openclaw-agent" required />
11
+ <label>Scopes (comma-separated)</label>
12
+ <input name="scopes" placeholder="inboxes:read,inboxes:write,emails:send,domains:read,domains:write,metrics:read" />
13
+ <button type="submit">Create Key</button>
14
+ </form>
15
+ <p class="muted">Use <code>*</code> for full access.</p>
16
+ <p class="muted">Active keys: <%= activeKeyCount %> / <%= limits.maxApiKeys %></p>
17
+ </div>
18
+
19
+ <div class="card stack">
20
+ <h2 style="margin:0;">Existing Keys</h2>
21
+ <% if (!apiKeys.length) { %>
22
+ <p class="muted">No API keys yet.</p>
23
+ <% } else { %>
24
+ <table>
25
+ <thead><tr><th>Name</th><th>Prefix</th><th>Scopes</th><th>Created</th><th>Status</th><th>Actions</th></tr></thead>
26
+ <tbody>
27
+ <% apiKeys.forEach((key) => { %>
28
+ <tr>
29
+ <td><%= key.name %></td>
30
+ <td><code><%= key.key_prefix %>...</code></td>
31
+ <td><%= (key.scopes || []).join(', ') %></td>
32
+ <td><%= new Date(key.created_at).toLocaleString() %></td>
33
+ <td><%= key.revoked_at ? 'revoked' : 'active' %></td>
34
+ <td>
35
+ <% if (!key.revoked_at) { %>
36
+ <div class="inline">
37
+ <form method="post" action="/app/api-keys/<%= key.id %>/rotate" hx-post="/app/api-keys/<%= key.id %>/rotate" onsubmit="return confirm('Rotate this API key?')">
38
+ <%- include('../partials/csrf-field') %>
39
+ <button class="secondary" type="submit">Rotate</button>
40
+ </form>
41
+ <form method="post" action="/app/api-keys/<%= key.id %>/revoke" hx-post="/app/api-keys/<%= key.id %>/revoke" onsubmit="return confirm('Revoke this API key?')">
42
+ <%- include('../partials/csrf-field') %>
43
+ <button class="danger" type="submit">Revoke</button>
44
+ </form>
45
+ </div>
46
+ <% } %>
47
+ </td>
48
+ </tr>
49
+ <% }) %>
50
+ </tbody>
51
+ </table>
52
+ <% } %>
53
+ </div>
54
+ </div>
55
+
56
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,71 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="card stack">
5
+ <h2 style="margin:0;">Plans</h2>
6
+ <p class="muted">Simple pricing: free for testing, paid for production usage.</p>
7
+
8
+ <table>
9
+ <thead>
10
+ <tr>
11
+ <th>Feature</th>
12
+ <th>Free</th>
13
+ <th>Paid</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <tr>
18
+ <td>Price</td>
19
+ <td>$0 / month</td>
20
+ <td><strong>$10 / month</strong></td>
21
+ </tr>
22
+ <tr>
23
+ <td>Inboxes</td>
24
+ <td><%= freeLimits.maxInboxes %></td>
25
+ <td><%= paidLimits.maxInboxes %></td>
26
+ </tr>
27
+ <tr>
28
+ <td>Custom domains</td>
29
+ <td><%= freeLimits.maxCustomDomains %></td>
30
+ <td><%= paidLimits.maxCustomDomains %></td>
31
+ </tr>
32
+ <tr>
33
+ <td>API keys</td>
34
+ <td><%= freeLimits.maxApiKeys %></td>
35
+ <td><%= paidLimits.maxApiKeys %></td>
36
+ </tr>
37
+ <tr>
38
+ <td>Monthly outbound emails</td>
39
+ <td><%= freeLimits.monthlyEmails %></td>
40
+ <td><%= paidLimits.monthlyEmails %></td>
41
+ </tr>
42
+ <tr>
43
+ <td>Inbox UI, Domains UI, API Keys, Metrics</td>
44
+ <td>Included</td>
45
+ <td>Included</td>
46
+ </tr>
47
+ <tr>
48
+ <td>Agent API access</td>
49
+ <td>Included</td>
50
+ <td>Included</td>
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+
56
+ <div class="card stack" style="margin-top:16px;">
57
+ <h3 style="margin:0;">Upgrade</h3>
58
+ <% if (org.plan === 'paid') { %>
59
+ <div class="flash success" style="margin:0;">Your organization is already on the paid plan.</div>
60
+ <% } else if (!dodoConfigured) { %>
61
+ <div class="flash error" style="margin:0;">Dodo Payments is not configured. Set server env vars to enable checkout.</div>
62
+ <% } else { %>
63
+ <p class="muted">Upgrade now to unlock custom domains and higher limits.</p>
64
+ <form method="post" action="/app/billing/checkout" hx-post="/app/billing/checkout" style="max-width:280px;">
65
+ <%- include('../partials/csrf-field') %>
66
+ <button type="submit">Upgrade to Paid ($10/mo)</button>
67
+ </form>
68
+ <% } %>
69
+ </div>
70
+
71
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,106 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="grid two-col">
5
+ <div class="card stack">
6
+ <h2 style="margin:0;">Add Domain</h2>
7
+ <% if (limits.maxCustomDomains < 1) { %>
8
+ <div class="flash error">Free plan cannot create custom domains.</div>
9
+ <% } %>
10
+ <form method="post" action="/app/domains" hx-post="/app/domains" class="stack">
11
+ <%- include('../partials/csrf-field') %>
12
+ <label>Domain Name</label>
13
+ <input name="name" placeholder="mail.yourcompany.com" required <%= limits.maxCustomDomains < 1 ? 'disabled' : '' %> />
14
+ <button type="submit" <%= limits.maxCustomDomains < 1 ? 'disabled' : '' %>>Create Domain</button>
15
+ </form>
16
+ <div class="muted">Limit: <%= limits.maxCustomDomains %> custom domain(s).</div>
17
+ </div>
18
+
19
+ <div class="card stack">
20
+ <h2 style="margin:0;">Inbound Forwarding</h2>
21
+ <p class="muted">After DNS verification, Mailgun routes inbound mail to this endpoint automatically.</p>
22
+ <div class="inline" style="align-items:flex-start;">
23
+ <code style="word-break:break-all;"><%= inboundForwardUrl %></code>
24
+ <button type="button" class="secondary copy-btn" style="width:auto;" data-copy="<%= inboundForwardUrl %>">Copy</button>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="card stack" style="margin-top:16px;">
30
+ <h2 style="margin:0;">Your Domains</h2>
31
+ <% if (!domains.length) { %>
32
+ <p class="muted">No custom domains configured.</p>
33
+ <% } else { %>
34
+ <% domains.forEach((domain) => { %>
35
+ <div class="card stack" style="padding:12px;">
36
+ <div class="inline">
37
+ <strong><%= domain.name %></strong>
38
+ <span class="pill">Status: <%= domain.status %></span>
39
+ <span class="pill">Inbound route: <%= domain.provider_route_id ? 'configured' : 'pending' %></span>
40
+ </div>
41
+
42
+ <div>
43
+ <strong>DNS records</strong>
44
+ <% if (!(domain.dns_records_json || []).length) { %>
45
+ <p class="muted">No DNS records available yet. Click Verify DNS to refresh records.</p>
46
+ <% } else { %>
47
+ <table>
48
+ <thead>
49
+ <tr><th>Type</th><th>Host</th><th>Value</th><th>Priority</th><th>Copy</th></tr>
50
+ </thead>
51
+ <tbody>
52
+ <% (domain.dns_records_json || []).forEach((record) => {
53
+ const type = record.record_type || record.type || '';
54
+ const host = record.name || '';
55
+ const value = record.value || record.valid || '';
56
+ const priority = record.priority || '';
57
+ %>
58
+ <tr>
59
+ <td><%= type %></td>
60
+ <td><code><%= host %></code></td>
61
+ <td><code style="word-break:break-all;"><%= value %></code></td>
62
+ <td><%= priority %></td>
63
+ <td>
64
+ <button type="button" class="secondary copy-btn" style="width:auto;" data-copy="<%= value %>">Copy</button>
65
+ </td>
66
+ </tr>
67
+ <% }) %>
68
+ </tbody>
69
+ </table>
70
+ <% } %>
71
+ </div>
72
+
73
+ <div class="inline">
74
+ <form method="post" action="/app/domains/<%= domain.id %>/verify" hx-post="/app/domains/<%= domain.id %>/verify">
75
+ <%- include('../partials/csrf-field') %>
76
+ <button class="secondary" type="submit">Verify DNS</button>
77
+ </form>
78
+ <form method="post" action="/app/domains/<%= domain.id %>/delete" hx-post="/app/domains/<%= domain.id %>/delete" onsubmit="return confirm('Delete this domain?')">
79
+ <%- include('../partials/csrf-field') %>
80
+ <button class="danger" type="submit">Delete</button>
81
+ </form>
82
+ </div>
83
+ </div>
84
+ <% }) %>
85
+ <% } %>
86
+ </div>
87
+
88
+ <script>
89
+ document.querySelectorAll('.copy-btn').forEach((btn) => {
90
+ btn.addEventListener('click', async () => {
91
+ const value = btn.dataset.copy || '';
92
+ try {
93
+ await navigator.clipboard.writeText(value);
94
+ const old = btn.textContent;
95
+ btn.textContent = 'Copied';
96
+ setTimeout(() => {
97
+ btn.textContent = old;
98
+ }, 1200);
99
+ } catch (_) {
100
+ alert('Copy failed. Please copy manually.');
101
+ }
102
+ });
103
+ });
104
+ </script>
105
+
106
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,127 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="grid two-col">
5
+ <div class="card stack">
6
+ <h2 style="margin:0;">Create Inbox</h2>
7
+ <form method="post" action="/app/inboxes" hx-post="/app/inboxes">
8
+ <%- include('../partials/csrf-field') %>
9
+ <div class="stack">
10
+ <label>Local Part</label>
11
+ <input name="localPart" placeholder="agent-1" required />
12
+ <label>Display Name</label>
13
+ <input name="displayName" placeholder="Sales Agent" />
14
+ <label>Domain</label>
15
+ <select name="domainName">
16
+ <option value="<%= sharedDomain %>"><%= sharedDomain %> (shared)</option>
17
+ <% customDomains.forEach((domain) => { %>
18
+ <option value="<%= domain.name %>"><%= domain.name %> (custom)</option>
19
+ <% }) %>
20
+ </select>
21
+ <button type="submit">Create Inbox</button>
22
+ </div>
23
+ </form>
24
+ <div class="muted">Limit: <%= limits.maxInboxes %> inbox(es). Current: <%= inboxes.length %>.</div>
25
+ </div>
26
+
27
+ <div class="card stack">
28
+ <h2 style="margin:0;">Inboxes</h2>
29
+ <% if (!inboxes.length) { %>
30
+ <p class="muted">No inboxes yet.</p>
31
+ <% } else { %>
32
+ <table>
33
+ <thead>
34
+ <tr><th>Email</th><th>Display Name</th><th>Actions</th></tr>
35
+ </thead>
36
+ <tbody>
37
+ <% inboxes.forEach((inbox) => { %>
38
+ <tr>
39
+ <td>
40
+ <a href="/app/inboxes?inboxId=<%= inbox.id %>"><%= inbox.email_address %></a>
41
+ </td>
42
+ <td>
43
+ <form method="post" action="/app/inboxes/<%= inbox.id %>/update" hx-post="/app/inboxes/<%= inbox.id %>/update" class="row">
44
+ <%- include('../partials/csrf-field') %>
45
+ <input name="displayName" value="<%= inbox.display_name || '' %>" placeholder="Optional" />
46
+ <button class="secondary" type="submit">Save</button>
47
+ </form>
48
+ </td>
49
+ <td>
50
+ <form method="post" action="/app/inboxes/<%= inbox.id %>/delete" hx-post="/app/inboxes/<%= inbox.id %>/delete" onsubmit="return confirm('Delete this inbox?')">
51
+ <%- include('../partials/csrf-field') %>
52
+ <button class="danger" type="submit">Delete</button>
53
+ </form>
54
+ </td>
55
+ </tr>
56
+ <% }) %>
57
+ </tbody>
58
+ </table>
59
+ <% } %>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="grid two-col" style="margin-top:16px;">
64
+ <div class="card stack">
65
+ <h2 style="margin:0;">Compose</h2>
66
+ <% if (!selectedInbox) { %>
67
+ <p class="muted">Select or create an inbox to compose.</p>
68
+ <% } else { %>
69
+ <p class="muted">From: <strong><%= selectedInbox.email_address %></strong></p>
70
+ <form method="post" action="/app/emails/send" hx-post="/app/emails/send" class="stack">
71
+ <%- include('../partials/csrf-field') %>
72
+ <input type="hidden" name="inboxId" value="<%= selectedInbox.id %>" />
73
+ <label>To (comma-separated)</label>
74
+ <input name="to" placeholder="a@example.com,b@example.com" required />
75
+ <label>Subject</label>
76
+ <input name="subject" />
77
+ <label>Body</label>
78
+ <textarea name="text" rows="6"></textarea>
79
+ <button type="submit">Send Email</button>
80
+ </form>
81
+ <div class="muted">Monthly sends: <%= usage.sent_count %> / <%= limits.monthlyEmails %></div>
82
+ <% } %>
83
+ </div>
84
+
85
+ <div class="card stack">
86
+ <h2 style="margin:0;">Messages <%= selectedInbox ? `for ${selectedInbox.email_address}` : '' %></h2>
87
+ <% if (!selectedInbox) { %>
88
+ <p class="muted">Choose an inbox to view messages.</p>
89
+ <% } else if (!emails.length) { %>
90
+ <p class="muted">No messages yet.</p>
91
+ <% } else { %>
92
+ <table>
93
+ <thead>
94
+ <tr><th>Dir</th><th>Subject</th><th>From</th><th>To</th><th>Time</th><th>Actions</th></tr>
95
+ </thead>
96
+ <tbody>
97
+ <% emails.forEach((email) => { %>
98
+ <tr style="opacity:<%= email.is_read ? 0.7 : 1 %>">
99
+ <td><%= email.direction %></td>
100
+ <td><%= email.subject || '(no subject)' %></td>
101
+ <td><%= email.from_address %></td>
102
+ <td><%= (email.to_addresses || []).join(', ') %></td>
103
+ <td><%= new Date(email.created_at).toLocaleString() %></td>
104
+ <td>
105
+ <div class="inline">
106
+ <form method="post" action="/app/emails/<%= email.id %>/read" hx-post="/app/emails/<%= email.id %>/read">
107
+ <%- include('../partials/csrf-field') %>
108
+ <input type="hidden" name="inboxId" value="<%= selectedInbox.id %>" />
109
+ <input type="hidden" name="mark" value="<%= email.is_read ? 'unread' : 'read' %>" />
110
+ <button class="secondary" type="submit"><%= email.is_read ? 'Unread' : 'Read' %></button>
111
+ </form>
112
+ <form method="post" action="/app/emails/<%= email.id %>/delete" hx-post="/app/emails/<%= email.id %>/delete" onsubmit="return confirm('Delete this message?')">
113
+ <%- include('../partials/csrf-field') %>
114
+ <input type="hidden" name="inboxId" value="<%= selectedInbox.id %>" />
115
+ <button class="danger" type="submit">Delete</button>
116
+ </form>
117
+ </div>
118
+ </td>
119
+ </tr>
120
+ <% }) %>
121
+ </tbody>
122
+ </table>
123
+ <% } %>
124
+ </div>
125
+ </div>
126
+
127
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,57 @@
1
+ <%- include('../partials/top', { pageTitle: 'Login', user: null, org: null }) %>
2
+ <div class="card" style="max-width:900px;margin:40px auto;">
3
+ <h1 style="margin-top:0;">Agent Inbox SaaS</h1>
4
+ <p class="muted">Create inboxes, custom domains, API keys, and usage metrics for agent workflows.</p>
5
+ <%- include('../partials/flash', { flash }) %>
6
+
7
+ <div class="grid two-col">
8
+ <div class="card stack">
9
+ <h2 style="margin:0;">Sign Up</h2>
10
+ <form method="post" action="/auth/register" class="stack">
11
+ <%- include('../partials/csrf-field') %>
12
+ <label>Name</label>
13
+ <input name="fullName" placeholder="Jane Doe" required />
14
+ <label>Email</label>
15
+ <input name="email" type="email" placeholder="jane@example.com" value="<%= prefillEmail %>" required />
16
+ <label>Password</label>
17
+ <input name="password" type="password" placeholder="At least 8 characters" minlength="8" required />
18
+ <button type="submit">Create Account</button>
19
+ </form>
20
+ </div>
21
+
22
+ <div class="card stack">
23
+ <h2 style="margin:0;">Log In</h2>
24
+ <form method="post" action="/auth/login" class="stack">
25
+ <%- include('../partials/csrf-field') %>
26
+ <label>Email</label>
27
+ <input name="email" type="email" placeholder="jane@example.com" value="<%= prefillEmail %>" required />
28
+ <label>Password</label>
29
+ <input name="password" type="password" required />
30
+ <button type="submit">Log In</button>
31
+ </form>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="grid two-col" style="margin-top:16px;">
36
+ <div class="card stack">
37
+ <h3 style="margin:0;">Resend Verification Email</h3>
38
+ <form method="post" action="/auth/resend-verification" class="stack">
39
+ <%- include('../partials/csrf-field') %>
40
+ <label>Email</label>
41
+ <input name="email" type="email" placeholder="jane@example.com" value="<%= prefillEmail %>" required />
42
+ <button class="secondary" type="submit">Resend Verification</button>
43
+ </form>
44
+ </div>
45
+
46
+ <div class="card stack">
47
+ <h3 style="margin:0;">Forgot Password</h3>
48
+ <form method="post" action="/auth/forgot-password" class="stack">
49
+ <%- include('../partials/csrf-field') %>
50
+ <label>Email</label>
51
+ <input name="email" type="email" placeholder="jane@example.com" value="<%= prefillEmail %>" required />
52
+ <button class="secondary" type="submit">Send Reset Link</button>
53
+ </form>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,34 @@
1
+ <%- include('../partials/top', { pageTitle, user, org }) %>
2
+ <%- include('../partials/flash', { flash }) %>
3
+
4
+ <div class="grid three-col">
5
+ <div class="card">
6
+ <h3 style="margin-top:0;">Plan</h3>
7
+ <p><strong><%= org.plan %></strong></p>
8
+ </div>
9
+ <div class="card">
10
+ <h3 style="margin-top:0;">Monthly Outbound</h3>
11
+ <p><strong><%= snapshot.sentCount %></strong> / <%= limits.monthlyEmails %></p>
12
+ </div>
13
+ <div class="card">
14
+ <h3 style="margin-top:0;">Monthly Inbound</h3>
15
+ <p><strong><%= snapshot.inboundCount %></strong></p>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="grid three-col" style="margin-top:16px;">
20
+ <div class="card">
21
+ <h3 style="margin-top:0;">Inboxes</h3>
22
+ <p><strong><%= snapshot.inboxCount %></strong> / <%= limits.maxInboxes %></p>
23
+ </div>
24
+ <div class="card">
25
+ <h3 style="margin-top:0;">Custom Domains</h3>
26
+ <p><strong><%= snapshot.customDomainCount %></strong> / <%= limits.maxCustomDomains %></p>
27
+ </div>
28
+ <div class="card">
29
+ <h3 style="margin-top:0;">API Keys</h3>
30
+ <p><strong><%= snapshot.apiKeyCount %></strong> / <%= limits.maxApiKeys %></p>
31
+ </div>
32
+ </div>
33
+
34
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,19 @@
1
+ <%- include('../partials/top', { pageTitle: 'Reset Password', user: null, org: null }) %>
2
+ <div class="card" style="max-width:520px;margin:40px auto;">
3
+ <h1 style="margin-top:0;">Reset Password</h1>
4
+ <p class="muted">Set a new password for your EmailAgent account.</p>
5
+ <%- include('../partials/flash', { flash }) %>
6
+
7
+ <form method="post" action="/auth/reset-password" class="stack">
8
+ <%- include('../partials/csrf-field') %>
9
+ <input type="hidden" name="token" value="<%= token %>" />
10
+ <label>New Password</label>
11
+ <input name="password" type="password" placeholder="At least 8 characters" minlength="8" required />
12
+ <button type="submit">Update Password</button>
13
+ </form>
14
+
15
+ <p style="margin-top:12px;">
16
+ <a class="muted" href="/">Back to login</a>
17
+ </p>
18
+ </div>
19
+ <%- include('../partials/bottom') %>
@@ -0,0 +1,3 @@
1
+ </div>
2
+ </body>
3
+ </html>
@@ -0,0 +1,3 @@
1
+ <% if (typeof csrfToken === 'string' && csrfToken) { %>
2
+ <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
3
+ <% } %>
@@ -0,0 +1,3 @@
1
+ <% if (flash) { %>
2
+ <div class="flash <%= flash.type %>"><%= flash.message %></div>
3
+ <% } %>