mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 06:58:21 +00:00
feat: email api wip (#3391)
This commit is contained in:
Generated
+11
@@ -6482,6 +6482,16 @@
|
||||
"form-data": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/oracledb": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz",
|
||||
@@ -18202,6 +18212,7 @@
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/pg": "^8.6.1",
|
||||
"@types/validator": "^13.15.10",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -24,12 +24,20 @@ import type { IConfig } from '../../types';
|
||||
import { PuterClient } from '../types';
|
||||
import { EMAIL_TEMPLATES, type EmailTemplateName } from './templates';
|
||||
|
||||
// -- Types ------------------------------------------------------------
|
||||
/** Attachment shape passed through to the underlying transport. */
|
||||
export interface EmailAttachment {
|
||||
filename: string;
|
||||
content: Buffer | string;
|
||||
contentType?: string;
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
// nodemailer doesn't ship TS types, so declare the subset we use.
|
||||
interface NodemailerTransport {
|
||||
sendMail: (options: SendMailOptions) => Promise<unknown>;
|
||||
close?: () => void;
|
||||
/** Subset of the transport's send result callers may care about. */
|
||||
export interface SentMessageInfo {
|
||||
messageId?: string;
|
||||
accepted?: string[];
|
||||
rejected?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SendMailOptions {
|
||||
@@ -41,6 +49,7 @@ export interface SendMailOptions {
|
||||
html?: string;
|
||||
text?: string;
|
||||
replyTo?: string;
|
||||
attachments?: EmailAttachment[];
|
||||
}
|
||||
|
||||
export type EmailValidator = (email: string) => Promise<boolean> | boolean;
|
||||
@@ -100,7 +109,8 @@ const DOMAIN_ALIASES: Record<string, string> = {
|
||||
* - Policy + extensible validation (via `validate`)
|
||||
*/
|
||||
export class EmailClient extends PuterClient {
|
||||
private transport: NodemailerTransport | null = null;
|
||||
private transport: ReturnType<typeof nodemailer.createTransport> | null =
|
||||
null;
|
||||
private compiledTemplates: Partial<
|
||||
Record<EmailTemplateName, CompiledTemplate>
|
||||
> = {};
|
||||
@@ -123,8 +133,7 @@ export class EmailClient extends PuterClient {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.transport = nodemailer.createTransport(emailConf as any);
|
||||
this.transport = nodemailer.createTransport(emailConf);
|
||||
console.log('[email] transport configured');
|
||||
}
|
||||
|
||||
@@ -159,21 +168,30 @@ export class EmailClient extends PuterClient {
|
||||
/**
|
||||
* Raw send — bypasses the template system. Useful for one-off
|
||||
* admin emails that don't warrant a named template.
|
||||
*
|
||||
* Returns the transport's send result, or `null` when no transport
|
||||
* is configured (the send is a no-op in that case — callers that
|
||||
* must not silently drop mail should check `isConfigured` first).
|
||||
*/
|
||||
async sendRaw(options: SendMailOptions): Promise<void> {
|
||||
async sendRaw(options: SendMailOptions) {
|
||||
if (!this.transport) {
|
||||
console.warn(
|
||||
'[email] attempted to send email without transport. If you need to send email, configure an SMTP transport in your config file (see docs for details). Email content:',
|
||||
options,
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
await this.transport.sendMail({
|
||||
from: options.from ?? this.defaultFrom(),
|
||||
return await this.transport.sendMail({
|
||||
...options,
|
||||
from: options.from ?? this.defaultFrom(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether an SMTP transport is configured (sends are no-ops otherwise). */
|
||||
get isConfigured(): boolean {
|
||||
return this.transport !== null;
|
||||
}
|
||||
|
||||
// -- Public API: clean / validate ---------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,9 +54,11 @@ export interface Actor {
|
||||
* Session reference when authenticated via a session token (user actors)
|
||||
* or an app-under-user token that carries a session. Absent for system,
|
||||
* raw-app, and pure access-token actors. Used for session introspection
|
||||
* and targeted logout.
|
||||
* and targeted logout. `kind` mirrors the session row's kind (e.g.
|
||||
* 'web', 'app', 'worker') so callers can gate on how the credential
|
||||
* was minted without an extra session lookup.
|
||||
*/
|
||||
session?: { uid: string } | null;
|
||||
session?: { uid: string; kind?: string | null } | null;
|
||||
}
|
||||
|
||||
/** UUID of the baked-in system user (see 0025 seed migration). */
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/pg": "^8.6.1",
|
||||
"@types/validator": "^13.15.10",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -1967,7 +1967,9 @@ export class AuthService extends PuterService {
|
||||
#buildUserActor(user: UserRow, session: SessionRow | null): Actor {
|
||||
return {
|
||||
user: this.#actorUserFromRow(user),
|
||||
session: session ? { uid: session.uuid } : null,
|
||||
session: session
|
||||
? { uid: session.uuid, kind: session.kind ?? null }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1982,7 +1984,9 @@ export class AuthService extends PuterService {
|
||||
uid: app.uid,
|
||||
id: app.id,
|
||||
},
|
||||
session: session ? { uid: session.uuid } : null,
|
||||
session: session
|
||||
? { uid: session.uuid, kind: session.kind ?? null }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -4,6 +4,7 @@ import type { Apps, AppListOptions, AppRecord, CreateAppOptions, UpdateAppAttrib
|
||||
import type { Auth, APIUsage, AllowanceInfo, AppUsage, AuthUser, DetailedAppUsage, MonthlyUsage } from './types/modules/auth.d.ts';
|
||||
import type { Debug } from './types/modules/debug.d.ts';
|
||||
import type { Driver, DriverDescriptor, Drivers } from './types/modules/drivers.d.ts';
|
||||
import type { Email, EmailAttachment, EmailSendOptions, EmailSendResult } from './types/modules/email.d.ts';
|
||||
import type { FS, CopyOptions, DeleteOptions, MkdirOptions, MoveOptions, ReadOptions, ReaddirOptions, SignResult, SpaceInfo, UploadOptions, WriteOptions } from './types/modules/filesystem.d.ts';
|
||||
import type { FSItem, FileSignatureInfo, InternalFSProperties } from './types/modules/fs-item.d.ts';
|
||||
import type { Hosting, Subdomain } from './types/modules/hosting.d.ts';
|
||||
@@ -59,6 +60,10 @@ export type {
|
||||
Driver,
|
||||
DriverDescriptor,
|
||||
Drivers,
|
||||
Email,
|
||||
EmailAttachment,
|
||||
EmailSendOptions,
|
||||
EmailSendResult,
|
||||
FSItem,
|
||||
FilePickerOptions,
|
||||
FileSignatureInfo,
|
||||
|
||||
@@ -10,6 +10,7 @@ import Apps from './modules/Apps.js';
|
||||
import Auth from './modules/Auth.js';
|
||||
import { Debug } from './modules/Debug.js';
|
||||
import Drivers from './modules/Drivers.js';
|
||||
import Email from './modules/Email.js';
|
||||
import { PuterJSFileSystemModule } from './modules/FileSystem/index.js';
|
||||
import FSItem from './modules/FSItem.js';
|
||||
import Hosting from './modules/Hosting.js';
|
||||
@@ -202,6 +203,7 @@ const puterInit = function () {
|
||||
this.registerModule('apps', Apps);
|
||||
this.registerModule('ai', AI);
|
||||
this.registerModule('kv', KV);
|
||||
this.registerModule('email', Email);
|
||||
this.registerModule('perms', Perms);
|
||||
this.registerModule('drivers', Drivers);
|
||||
this.registerModule('debug', Debug);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as utils from '../lib/utils.js';
|
||||
|
||||
/**
|
||||
* Restricted outbound email (the `puter-email` driver interface).
|
||||
*
|
||||
* Every send must be authorized by a worker: either the worker calls
|
||||
* directly (`me.puter.email.send(...)`), or a user calls with their own
|
||||
* token and passes the worker's token as `emailAccessToken` — the caller is
|
||||
* the one billed and rate-limited. In a worker handler:
|
||||
*
|
||||
* router.post('/notify', async ({ request, user }) => {
|
||||
* const { to, subject, text } = await request.json();
|
||||
* return await user.puter.email.send({
|
||||
* to, subject, text,
|
||||
* emailAccessToken: me.puter.authToken,
|
||||
* // Inline or Puter-FS attachments:
|
||||
* attachments: [
|
||||
* { filename, content, contentType }, // content = base64
|
||||
* { path: '~/Documents/report.pdf' }, // read server-side
|
||||
* ],
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* Positional form: `await puter.email.send(to, subject, body)`.
|
||||
*/
|
||||
class Email {
|
||||
/**
|
||||
* @class
|
||||
* @param {object} puter - The parent puter instance.
|
||||
*/
|
||||
constructor(puter) {
|
||||
this.puter = puter;
|
||||
this.authToken = puter.authToken;
|
||||
this.APIOrigin = puter.APIOrigin;
|
||||
this.appID = puter.appID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new authentication token.
|
||||
*
|
||||
* @param {string} authToken - The new authentication token.
|
||||
* @returns {void}
|
||||
*/
|
||||
setAuthToken(authToken) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API origin.
|
||||
*
|
||||
* @param {string} APIOrigin - The new API origin.
|
||||
* @returns {void}
|
||||
*/
|
||||
setAPIOrigin(APIOrigin) {
|
||||
this.APIOrigin = APIOrigin;
|
||||
}
|
||||
|
||||
send = utils.make_driver_method(
|
||||
['to', 'subject', 'body'],
|
||||
'puter-email',
|
||||
undefined,
|
||||
'send',
|
||||
{
|
||||
preprocess: (args) => {
|
||||
// `body` is positional-call sugar for `text`.
|
||||
if (
|
||||
args.body !== undefined &&
|
||||
args.text === undefined &&
|
||||
args.html === undefined
|
||||
) {
|
||||
args.text = args.body;
|
||||
}
|
||||
delete args.body;
|
||||
return args;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default Email;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* One attachment: either inline base64 `content`, or a Puter FS reference
|
||||
* (`path`/`uid`) read server-side with the caller's — falling back to the
|
||||
* authorizing worker's — file permissions.
|
||||
*/
|
||||
export interface EmailAttachment {
|
||||
/** Required with `content`; defaults to the file's name for FS refs. */
|
||||
filename?: string;
|
||||
/** Base64 file body. Mutually exclusive with `path`/`uid`. */
|
||||
content?: string;
|
||||
/** Puter FS path (supports `~/`). Mutually exclusive with `content`. */
|
||||
path?: string;
|
||||
/** Puter FS entry uid. Mutually exclusive with `content`. */
|
||||
uid?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
export interface EmailSendOptions {
|
||||
/** Recipient address(es). */
|
||||
to: string | string[];
|
||||
subject: string;
|
||||
/** Plain-text body. At least one of `text` / `html` is required. */
|
||||
text?: string;
|
||||
/** HTML body. */
|
||||
html?: string;
|
||||
cc?: string | string[];
|
||||
bcc?: string | string[];
|
||||
replyTo?: string;
|
||||
/**
|
||||
* A worker's auth token authorizing the send when the caller is not
|
||||
* itself a worker (inside a worker: `me.puter.authToken`). The caller
|
||||
* stays the billed and rate-limited identity.
|
||||
*/
|
||||
emailAccessToken?: string;
|
||||
attachments?: EmailAttachment[];
|
||||
}
|
||||
|
||||
export interface EmailSendResult {
|
||||
/** Transport message id, when the mail server reports one. */
|
||||
messageId: string | null;
|
||||
/** Total charge for this send, in microcents. */
|
||||
cost: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricted outbound email. Sending is limited server-side to trusted
|
||||
* callers (Puter workers owned by allowlisted or permitted users).
|
||||
*/
|
||||
export class Email {
|
||||
/** Sends an email with a plain-text body. */
|
||||
send (to: string | string[], subject: string, body: string): Promise<EmailSendResult>;
|
||||
send (options: EmailSendOptions): Promise<EmailSendResult>;
|
||||
}
|
||||
Vendored
+2
@@ -3,6 +3,7 @@ import type { Apps } from './modules/apps.d.ts';
|
||||
import type { Auth } from './modules/auth.d.ts';
|
||||
import type { Debug } from './modules/debug.d.ts';
|
||||
import type { Drivers } from './modules/drivers.d.ts';
|
||||
import type { Email } from './modules/email.d.ts';
|
||||
import type { FS } from './modules/filesystem.d.ts';
|
||||
import type { FSItem } from './modules/fs-item.d.ts';
|
||||
import type { Hosting } from './modules/hosting.d.ts';
|
||||
@@ -53,6 +54,7 @@ export class Puter {
|
||||
ui: UI;
|
||||
hosting: Hosting;
|
||||
kv: KV;
|
||||
email: Email;
|
||||
perms: Perms;
|
||||
drivers: Drivers;
|
||||
debug: Debug;
|
||||
|
||||
Reference in New Issue
Block a user