@flink-app/inbound-email-plugin
v2.0.0-alpha.61
Published
Flink plugin for SMTP inbound email with auto-discovered EmailHandler files
Readme
@flink-app/inbound-email-plugin
Inbound SMTP email for Flink apps. Starts an SMTP server and auto-discovers EmailHandler files in src/inbound-email-handlers/, routing each incoming email to the first matching handler.
Install
pnpm add @flink-app/inbound-email-pluginSetup
1. Register the compiler plugin
// flink.config.js
const { compilerPlugin } = require("@flink-app/inbound-email-plugin/compiler");
module.exports = {
compilerPlugins: [compilerPlugin()],
};2. Add the runtime plugin
// src/index.ts
import { inboundEmailPlugin } from "@flink-app/inbound-email-plugin";
new FlinkApp<Ctx>({
// ...
plugins: [
inboundEmailPlugin({
port: 2525,
allowedDomains: ["myapp.com"],
resolveUser: async (email, ctx) =>
ctx.repos.userRepo.getOne({ email: email.from }),
resolvePermissions: async (user, ctx) =>
ctx.repos.permRepo.getForUser(user.id),
onUnhandled: async (email) =>
console.warn("No handler matched", email.from),
}),
],
});3. Create an email handler
// src/inbound-email-handlers/HandleSupportEmail.ts
import { EmailHandler, EmailRouteProps } from "@flink-app/inbound-email-plugin";
import { Ctx } from "../Ctx";
export const Route: EmailRouteProps = {
from: /.*@customers\.com/,
};
const handler: EmailHandler<Ctx> = async ({ ctx, email, user }) => {
await ctx.repos.ticketRepo.create({
subject: email.subject,
body: email.text,
from: email.from,
userId: user?.id,
});
};
export default handler;Routing
EmailRouteProps fields are all optional — omit any to match everything:
| Field | Type | Matches when |
|-------|------|--------------|
| from | string \| RegExp \| (email) => boolean | sender address |
| to | string \| RegExp | any recipient address |
| subject | string \| RegExp | subject line |
Handlers are evaluated in discovery order. The first match wins.
SPF/DKIM verification
Pass verify: true to enable both checks with defaults, or an object for fine-grained control:
inboundEmailPlugin({
verify: {
spf: true, // default: true
dkim: true, // default: true
rejectOnFail: true, // bounce with SMTP 550 on failure — default: false
mta: "mx.myapp.com", // your MTA hostname for SPF — default: os.hostname()
},
})When rejectOnFail is false (the default), the result is passed to every handler as auth and your code decides what to do:
const handler: EmailHandler<Ctx> = async ({ email, auth }) => {
if (!auth?.passed) {
console.warn("Unauthenticated email from", email.from);
return;
}
// ...
};auth.passed is true only when every enabled check returned "pass". The individual results are available on auth.spf.result and auth.dkim.result (with per-signature detail on auth.dkim.signatures).
Verification is powered by mailauth and loaded lazily so startup time is not affected when verify is not configured.
Plugin options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| port | number | 2525 | SMTP listening port |
| allowedDomains | string[] | — | Reject mail to other domains |
| maxMessageSize | number | 10MB | Max message size in bytes |
| tls | { key, cert } | — | Paths to PEM files for TLS |
| verify | boolean \| VerifyOptions | — | SPF/DKIM verification (see above) |
| resolveUser | (email, ctx) => Promise<any> | — | Look up the sending user |
| resolvePermissions | (user, ctx) => Promise<string[]> | — | Load permissions for the user |
| onUnhandled | (email, ctx) => Promise<void> | — | Called when no handler matched |
Request context
When resolveUser and resolvePermissions are configured, the user and permissions are available inside handlers via the standard Flink helpers:
import { getRequestUser, getRequestPermissions } from "@flink-app/flink";
const handler: EmailHandler<Ctx> = async ({ ctx, email }) => {
const user = getRequestUser(); // same as the `user` arg
const perms = getRequestPermissions();
};This means Flink tools and agents called from within a handler also receive the correct user context automatically.
CLI — test delivery
Deliver a raw .eml file to a running local SMTP server without needing an external tool like swaks:
# Built-in samples — quickest way to verify your handler is working
npx flink-email deliver --sample plain
npx flink-email deliver --sample attachment
# Your own .eml file
npx flink-email deliver ./my-email.eml
# Override port or envelope addresses
npx flink-email deliver --sample plain --port 2525 --from [email protected] --to [email protected]
# Pipe from stdin (e.g. paste from Gmail "Show original")
pbpaste | npx flink-email deliver -The tool speaks real SMTP — headers are parsed for From/To envelope addresses automatically, and SPF/DKIM checks run on the raw bytes exactly as they would from a real relay.
Custom scan directory
// flink.config.js
compilerPlugins: [compilerPlugin({ scanDir: "src/mail-handlers" })]