toiljs 0.0.96 → 0.0.98

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.
@@ -80,16 +80,20 @@ export function resolveEmailConfig(
80
80
  if (!validFrom(from)) {
81
81
  return { config: null, warning: 'email `from` is not a valid address (CRLF or no `@`)' };
82
82
  }
83
- if (apiKey === undefined) {
84
- return {
85
- config: null,
86
- warning: 'email config present but TOIL_EMAIL_API_KEY is not set (in .env.secrets)',
87
- };
88
- }
89
-
83
+ // The API-key requirement is PER PROVIDER: Resend authenticates with a key, and
84
+ // Gmail SMTP needs an app password (also carried in the key), but a plain SMTP
85
+ // relay (a local dev catch-all like Mailpit/MailHog on 127.0.0.1, or an internal
86
+ // open relay) needs NO credential. Requiring a key for plain SMTP silently
87
+ // disabled a perfectly good local-SMTP dev setup.
90
88
  let provider: ResolvedProvider;
91
89
  let smtp: ResolvedSmtp | undefined;
92
90
  if (providerId === 'resend') {
91
+ if (apiKey === undefined) {
92
+ return {
93
+ config: null,
94
+ warning: 'provider `resend` requires TOIL_EMAIL_API_KEY (in .env.secrets)',
95
+ };
96
+ }
93
97
  provider = 'resend';
94
98
  } else if (providerId === 'gmail' || providerId === 'smtp') {
95
99
  provider = 'smtp';
@@ -101,6 +105,14 @@ export function resolveEmailConfig(
101
105
  if (!host) {
102
106
  return { config: null, warning: 'provider `smtp` requires TOIL_EMAIL_SMTP_HOST' };
103
107
  }
108
+ // Gmail always needs an app password; plain SMTP may be auth-less (the send
109
+ // path omits `auth` when the key is empty). See providers.ts sendSmtp.
110
+ if (isGmail && apiKey === undefined) {
111
+ return {
112
+ config: null,
113
+ warning: 'provider `gmail` requires TOIL_EMAIL_API_KEY (a Gmail app password)',
114
+ };
115
+ }
104
116
  const port = parseInt0(envOf(reserved, 'SMTP_PORT'), c.smtp?.port ?? 0) || 587;
105
117
  const user = envOf(reserved, 'SMTP_USER') ?? c.smtp?.user?.trim() ?? from;
106
118
  smtp = { host, port, user };
@@ -115,7 +127,7 @@ export function resolveEmailConfig(
115
127
  config: {
116
128
  provider,
117
129
  from,
118
- apiKey,
130
+ apiKey: apiKey ?? '',
119
131
  maxPerMin: parseInt0(envOf(reserved, 'MAX_PER_MIN'), c.maxPerMin ?? 60),
120
132
  maxPerDay: parseInt0(envOf(reserved, 'MAX_PER_DAY'), c.maxPerDay ?? 0),
121
133
  maxPerRecipientPerHour: parseInt0(
@@ -91,7 +91,8 @@ export async function sendSmtp(
91
91
  host: smtp.host,
92
92
  port: smtp.port,
93
93
  secure: smtp.port === 465, // 465 = implicit TLS; else STARTTLS
94
- auth: { user: smtp.user, pass: cfg.apiKey },
94
+ // Auth only when there is a credential; a local/auth-less relay takes none.
95
+ auth: cfg.apiKey.length > 0 ? { user: smtp.user, pass: cfg.apiKey } : undefined,
95
96
  connectionTimeout: POST_TIMEOUT_MS,
96
97
  greetingTimeout: POST_TIMEOUT_MS,
97
98
  });
@@ -228,6 +228,11 @@ function devEmailSend(ref: MemoryRef, reqPtr: number, reqLen: number): number {
228
228
  const { status, parsed } = svc.prepare(raw);
229
229
  if (parsed === null) {
230
230
  process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
231
+ // The real send was skipped (bad recipient / deduped / budget / cap), but in
232
+ // DEV still draw the email so its link/code stays testable. Not recorded to
233
+ // the test seam (that tracks accepted sends only).
234
+ const skipped = parseEmailBlob(raw);
235
+ if (skipped !== null) previewDevEmail(skipped, EmailStatus[status]);
231
236
  return status;
232
237
  }
233
238
  recordSentEmail(parsed); // dev test seam
@@ -154,6 +154,24 @@ describe('config resolution', () => {
154
154
  resolveEmailConfig({ from: 'a@b.com' }, reserved({ TOIL_EMAIL_API_KEY: 'k', TOIL_EMAIL_PROVIDER: 'mailgun' }))
155
155
  .warning,
156
156
  ).toMatch(/unknown/);
157
+ // gmail still needs an app password
158
+ expect(
159
+ resolveEmailConfig({ provider: 'gmail', from: 'me@gmail.com' }, reserved({})).warning,
160
+ ).toMatch(/gmail/);
161
+ });
162
+
163
+ it('plain SMTP needs NO api key (a local / auth-less relay)', () => {
164
+ const { config, warning } = resolveEmailConfig(
165
+ { provider: 'smtp', from: 'dev@dacely.local' },
166
+ reserved({ TOIL_EMAIL_SMTP_HOST: '127.0.0.1' }),
167
+ );
168
+ expect(warning).toBeNull();
169
+ expect(config).toMatchObject({
170
+ provider: 'smtp',
171
+ from: 'dev@dacely.local',
172
+ apiKey: '', // no credential -> the send path omits SMTP auth
173
+ smtp: { host: '127.0.0.1', port: 587 },
174
+ });
157
175
  });
158
176
 
159
177
  it('TOIL_EMAIL_ENABLED=false disables silently', () => {