Skip to content

Instantly share code, notes, and snippets.

@jessekelly881
Created February 11, 2025 18:46
Show Gist options
  • Select an option

  • Save jessekelly881/e070ff55d23e8219bf58f3c1974b8273 to your computer and use it in GitHub Desktop.

Select an option

Save jessekelly881/e070ff55d23e8219bf58f3c1974b8273 to your computer and use it in GitHub Desktop.
Email service
import { Config, Context, Effect, Option, Schedule, Schema } from "effect";
import { Resend } from "resend";
import { EmailRepo } from "../../modules/email/repo";
import { HashId } from "../hashId";
import { EmailAddress, Event, OneTimeCode, User } from "@eventmind/domain";
import type { Mjml2HtmlOptions } from "mjml-react";
import { EventRepo } from "../../modules/event/repo";
import { UserRepo } from "../../modules/user/repo";
import * as EventCancelledEmail from "./emails/eventCancelled";
import * as EventReminderEmail from "./emails/eventReminder";
import * as OneTimePasswordEmail from "./emails/oneTimePassword";
import * as WelcomeEmail from "./emails/welcome";
interface EmailSendOptions {
to: string;
subject: string;
text: string;
html?: string;
}
export class EmailSendError extends Schema.TaggedError<EmailSendError>()(
"EmailSendError",
{
info: Schema.Struct({
to: Schema.String,
subject: Schema.String,
}),
message: Schema.String,
},
) {}
/**
* Handles the actual sending of emails and records the email in the database.
*/
export class EmailProvider extends Effect.Service<EmailProvider>()(
"app/EmailProvider",
{
scoped: Effect.gen(function* () {
const from = yield* EmailAddress.config("EMAIL_FROM");
const apiKey = yield* Config.string("RESEND_API_KEY");
const resend = new Resend(apiKey);
return {
/**
* Sends the email and optionally returns an identifier.
*/
send: (options: EmailSendOptions) =>
Effect.promise(() => resend.emails.send({ ...options, from })).pipe(
Effect.flatMap((res) =>
res.error
? Effect.fail(res.error)
: Effect.succeed(Option.fromNullable(res.data?.id)),
),
// Retry when the server is down.
Effect.retry({
times: 3,
until: (res) => res.name !== "internal_server_error",
schedule: Schedule.exponential("1 second"),
}),
Effect.mapError(
(res) =>
new EmailSendError({
message: res.message,
info: { to: options.to, subject: options.subject },
}),
),
Effect.withSpan("EmailProvider.send", {
attributes: { to: options.to, subject: options.subject },
}),
),
};
}),
},
) {}
/**
* Writes the email to the database after sending.
* @category aspect
*/
export const withDatabasePersistence = <A, E, R>(
self: Effect.Effect<A, E, R>,
) =>
Effect.gen(function* () {
const hashId = yield* HashId;
const emailRepo = yield* EmailRepo;
Effect.updateService(
EmailProvider,
(service) =>
new EmailProvider({
...service,
send: (options: EmailSendOptions) =>
Effect.gen(function* () {
const providerIdentifier = yield* service.send(options);
yield* emailRepo
.insert({
...options,
recipient: options.to,
providerIdentifier,
})
.pipe(Effect.provideService(HashId, hashId));
return providerIdentifier;
}),
}),
)(self);
});
export class MjmlHtmlOptions extends Context.Reference<MjmlHtmlOptions>()(
"@/MjmlHtmlOptions",
{
defaultValue: () =>
({
validationLevel: "soft",
minify: true,
}) as Mjml2HtmlOptions,
},
) {}
export class Emailer extends Effect.Service<Emailer>()("app/Emailer", {
accessors: true,
dependencies: [EmailProvider.Default, UserRepo.Default, EventRepo.Default],
scoped: Effect.gen(function* () {
const emailProvider = yield* EmailProvider;
const scope = yield* Effect.scope;
const send = (options: EmailSendOptions) =>
emailProvider.send(options).pipe(
Effect.tapBoth({
onFailure: (err) => Effect.logError(err.message),
onSuccess: () =>
Effect.logInfo(
`Emailer: Sent - Subject: ${options.subject} | Recipient: ${options.to}`,
),
}),
Effect.forkIn(scope),
Effect.asVoid,
);
return {
// event confirmation
// follow up after event
// event suggestions
sendWelcomeEmail: Effect.functionWithSpan({
options: ({ id: userId }) => ({
name: "Emailer.sendWelcomeEmail",
attributes: { userId },
}),
body: (user: User.User) =>
Effect.gen(function* () {
yield* send({
to: user.email,
subject: "Welcome to EventMind",
text: yield* WelcomeEmail.text({ user }),
html: yield* WelcomeEmail.html({ user }),
});
}),
}),
sendOtcEmail: Effect.functionWithSpan({
options: (email) => ({
name: "Emailer.sendOtcEmail",
attributes: { email },
}),
body: (email: EmailAddress.EmailAddress, otc: OneTimeCode.OneTimeCode) =>
Effect.gen(function* () {
yield* send({
to: email,
subject: "Your one time code",
text: yield* OneTimePasswordEmail.text({ otc }),
html: yield* OneTimePasswordEmail.html({ otc }),
});
}),
}),
sendEventReminderEmail: Effect.functionWithSpan({
options: ({ id: userId }, { id: eventId }) => ({
name: "Emailer.sendEventReminderEmail",
attributes: { userId, eventId },
}),
body: (user: User.User, event: Event.Event) =>
Effect.gen(function* () {
yield* send({
to: user.email,
subject: event.name,
text: yield* EventReminderEmail.text({ user, event }),
html: yield* EventReminderEmail.html({ user, event }),
});
}),
}),
sendEventCancelledEmail: Effect.functionWithSpan({
options: ({ id: userId }, { id: eventId }) => ({
name: "Emailer.sendEventCancelledEmail",
attributes: { userId, eventId },
}),
body: (user: User.User, event: Event.Event) =>
Effect.gen(function* () {
yield* send({
to: user.email,
subject: event.name,
text: yield* EventCancelledEmail.text({ user, event }),
html: yield* EventCancelledEmail.html({ user, event }),
});
}),
}),
};
}),
}) {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment