universal-mailer-lib 2.0.2

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.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # universal-mailer-lib
2
+
3
+ > Universal email client. Supports SMTP, Gmail API, and IMAP/POP3.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install universal-mailer-lib
9
+ ```
10
+
11
+ Requires peer dependencies: `googleapis`, `nodemailer`, `handlebars`, `imapflow`, `mailparser`
12
+
13
+ ```bash
14
+ npm install universal-mailer-lib googleapis nodemailer handlebars imapflow mailparser
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { UniversalMailer } from 'universal-mailer-lib'
21
+
22
+ const mailer = new UniversalMailer({
23
+ transport: 'smtp',
24
+ smtp: {
25
+ host: 'smtp.gmail.com',
26
+ port: 465,
27
+ secure: true,
28
+ auth: {
29
+ user: 'you@gmail.com',
30
+ pass: 'your-app-password',
31
+ },
32
+ },
33
+ })
34
+
35
+ const result = await mailer.sendEmail({
36
+ from: 'you@gmail.com',
37
+ to: 'recipient@example.com',
38
+ subject: 'Hello from gmail-mailer',
39
+ html: '<h1>Hello World</h1><p>This is a test email.</p>',
40
+ })
41
+
42
+ console.log(result.messageId) // Gmail message ID
43
+ ```
44
+
45
+ ## OAuth2 Setup
46
+
47
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
48
+ 2. Create a project and enable the **Gmail API**
49
+ 3. Create **OAuth 2.0 credentials** (Desktop app type)
50
+ 4. Get your `clientId` and `clientSecret`
51
+ 5. Obtain a `refreshToken` using [OAuth 2.0 Playground](https://developers.google.com/oauthplayground/)
52
+ - Select scope: `https://www.googleapis.com/auth/gmail.send`
53
+ - Authorize and exchange for tokens
54
+
55
+ ## Features
56
+
57
+ ### HTML Email
58
+
59
+ ```typescript
60
+ await mailer.sendEmail({
61
+ from: 'you@gmail.com',
62
+ to: 'user@example.com',
63
+ subject: 'HTML Email',
64
+ html: `
65
+ <html>
66
+ <body>
67
+ <h1>Welcome!</h1>
68
+ <p>This is an <strong>HTML</strong> email.</p>
69
+ </body>
70
+ </html>
71
+ `,
72
+ })
73
+ ```
74
+
75
+ ### Plain Text Email
76
+
77
+ ```typescript
78
+ await mailer.sendEmail({
79
+ from: 'you@gmail.com',
80
+ to: 'user@example.com',
81
+ subject: 'Text Email',
82
+ text: 'This is a plain text email.',
83
+ })
84
+ ```
85
+
86
+ ### Template Email (Handlebars)
87
+
88
+ ```typescript
89
+ import { GmailMailer } from 'gmail-mailer'
90
+
91
+ const mailer = new GmailMailer({ clientId, clientSecret, refreshToken })
92
+
93
+ const result = await mailer.sendEmail({
94
+ from: 'noreply@yourapp.com',
95
+ to: 'user@example.com',
96
+ subject: 'Welcome, {{name}}!',
97
+ template: `
98
+ <h1>Hello {{name}}</h1>
99
+ <p>Your account has been created.</p>
100
+ <p>Verification code: <strong>{{code}}</strong></p>
101
+ `,
102
+ context: {
103
+ name: '王小明',
104
+ code: 'ABC123',
105
+ },
106
+ })
107
+ ```
108
+
109
+ ### Email with Attachments
110
+
111
+ ```typescript
112
+ import { GmailMailer } from 'gmail-mailer'
113
+ import { readFileSync } from 'node:fs'
114
+
115
+ const mailer = new GmailMailer({ clientId, clientSecret, refreshToken })
116
+
117
+ await mailer.sendEmail({
118
+ from: 'you@gmail.com',
119
+ to: 'user@example.com',
120
+ subject: 'Report Attached',
121
+ html: '<p>Please find the attached report.</p>',
122
+ attachments: [
123
+ {
124
+ filename: 'report.pdf',
125
+ content: readFileSync('./report.pdf'),
126
+ },
127
+ {
128
+ filename: 'data.json',
129
+ content: JSON.stringify({ key: 'value' }),
130
+ contentType: 'application/json',
131
+ },
132
+ ],
133
+ })
134
+ ```
135
+
136
+ ### CC, BCC, Reply-To
137
+
138
+ ```typescript
139
+ await mailer.sendEmail({
140
+ from: 'you@gmail.com',
141
+ to: 'primary@example.com',
142
+ cc: 'manager@example.com',
143
+ bcc: 'archive@example.com',
144
+ replyTo: 'support@example.com',
145
+ subject: 'Important Update',
146
+ html: '<p>Please review the attached document.</p>',
147
+ })
148
+ ```
149
+
150
+ ### Verify Connection
151
+
152
+ ```typescript
153
+ const email = await mailer.verifyConnection()
154
+ console.log(`Connected as: ${email}`) // your@gmail.com
155
+ ```
156
+
157
+ ## API Reference
158
+
159
+ ### `GmailMailer`
160
+
161
+ #### Constructor
162
+
163
+ ```typescript
164
+ new GmailMailer(config: OAuth2Config)
165
+ ```
166
+
167
+ | Option | Type | Required | Description |
168
+ |--------|------|----------|-------------|
169
+ | `clientId` | `string` | ✅ | Google OAuth2 client ID |
170
+ | `clientSecret` | `string` | ✅ | Google OAuth2 client secret |
171
+ | `refreshToken` | `string` | ✅ | OAuth2 refresh token |
172
+ | `accessToken` | `string` | ❌ | Optional access token |
173
+
174
+ #### `sendEmail(options)` → `Promise<SendResult>`
175
+
176
+ | Option | Type | Required | Description |
177
+ |--------|------|----------|-------------|
178
+ | `from` | `string` | ✅ | Sender email |
179
+ | `to` | `string` | ✅ | Recipient email |
180
+ | `cc` | `string` | ❌ | CC recipient |
181
+ | `bcc` | `string` | ❌ | BCC recipient |
182
+ | `replyTo` | `string` | ❌ | Reply-to address |
183
+ | `subject` | `string` | ✅ | Email subject |
184
+ | `text` | `string` | ❌ | Plain text body |
185
+ | `html` | `string` | ❌ | HTML body |
186
+ | `template` | `string` | ❌ | Handlebars template string |
187
+ | `context` | `object` | ❌ | Template variables |
188
+ | `attachments` | `Attachment[]` | ❌ | File attachments |
189
+
190
+ **Returns:**
191
+ ```typescript
192
+ {
193
+ messageId: string
194
+ threadId?: string
195
+ labelIds?: string[]
196
+ }
197
+ ```
198
+
199
+ #### `verifyConnection()` → `Promise<string>`
200
+
201
+ Returns the authenticated user's email address.
202
+
203
+ #### `getAccessToken()` → `Promise<string>`
204
+
205
+ Returns the current OAuth2 access token (auto-refreshes if expired).
206
+
207
+ ### Utility Functions
208
+
209
+ ```typescript
210
+ import { renderTemplate, registerTemplate, registerHelper } from 'gmail-mailer'
211
+
212
+ // Render a Handlebars template
213
+ const html = renderTemplate('<h1>Hello {{name}}</h1>', { name: 'World' })
214
+
215
+ // Register a reusable partial
216
+ registerTemplate('footer', '<footer>© 2026</footer>')
217
+
218
+ // Register a custom helper
219
+ registerHelper('upper', (text: string) => text.toUpperCase())
220
+ ```
221
+
222
+ ### MIME Builder
223
+
224
+ ```typescript
225
+ import { buildRawMessage, buildMimeMessage } from 'gmail-mailer'
226
+
227
+ // Build base64url encoded message (for Gmail API)
228
+ const raw = await buildRawMessage({ from, to, subject, html })
229
+
230
+ // Build raw Buffer (for SMTP or other transports)
231
+ const buffer = await buildMimeMessage({ from, to, subject, html })
232
+ ```
233
+
234
+ ## Design
235
+
236
+ | Feature | Why |
237
+ |---------|-----|
238
+ | Gmail API only | No SMTP needed, better security with OAuth2 |
239
+ | OAuth2 auto-refresh | `google-auth-library` handles token refresh automatically |
240
+ | Handlebars templates | Industry standard for email templating |
241
+ | nodemailer MailComposer | Battle-tested MIME generation, no reinventing the wheel |
242
+ | Zero SMTP | Bypasses SMTP entirely, uses Gmail REST API directly |
243
+
244
+ ## License
245
+
246
+ MIT
@@ -0,0 +1,60 @@
1
+ /**
2
+ * GmailMailer — Gmail API email sender with OAuth2 auto-refresh.
3
+ *
4
+ * Usage:
5
+ * const mailer = new GmailMailer({ clientId, clientSecret, refreshToken })
6
+ * const result = await mailer.sendEmail({ from, to, subject, html })
7
+ */
8
+ import { type Attachment } from './mime-builder.js';
9
+ export interface OAuth2Config {
10
+ clientId: string;
11
+ clientSecret: string;
12
+ refreshToken: string;
13
+ accessToken?: string;
14
+ redirectUri?: string;
15
+ }
16
+ export interface SendOptions {
17
+ from: string;
18
+ to: string;
19
+ cc?: string;
20
+ bcc?: string;
21
+ replyTo?: string;
22
+ subject: string;
23
+ text?: string;
24
+ html?: string;
25
+ template?: string;
26
+ context?: Record<string, unknown>;
27
+ attachments?: Attachment[];
28
+ }
29
+ export interface SendResult {
30
+ messageId: string;
31
+ threadId?: string;
32
+ labelIds?: string[];
33
+ }
34
+ export declare const GMAIL_SCOPES: string[];
35
+ export declare class GmailMailer {
36
+ private oauth2Client;
37
+ constructor(config: OAuth2Config);
38
+ /**
39
+ * Ensure a valid access token is available (refreshes if needed).
40
+ */
41
+ private ensureAccessToken;
42
+ /**
43
+ * Manually refresh the access token using the refresh token.
44
+ * Use this if automatic refresh fails.
45
+ */
46
+ forceRefreshToken(): Promise<void>;
47
+ /**
48
+ * Send an email via Gmail API.
49
+ * Supports raw HTML, plain text, or Handlebars templates.
50
+ */
51
+ sendEmail(options: SendOptions): Promise<SendResult>;
52
+ /**
53
+ * Get the current OAuth2 access token (triggers refresh if needed).
54
+ */
55
+ getAccessToken(): Promise<string>;
56
+ /**
57
+ * Verify OAuth2 credentials by fetching the Gmail user's profile.
58
+ */
59
+ verifyConnection(): Promise<string>;
60
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * GmailMailer — Gmail API email sender with OAuth2 auto-refresh.
3
+ *
4
+ * Usage:
5
+ * const mailer = new GmailMailer({ clientId, clientSecret, refreshToken })
6
+ * const result = await mailer.sendEmail({ from, to, subject, html })
7
+ */
8
+ import { google } from 'googleapis';
9
+ import { renderTemplate } from './template.js';
10
+ import { buildRawMessage } from './mime-builder.js';
11
+ /**
12
+ * Redact sensitive information from error messages.
13
+ * Prevents accidental leakage of tokens, secrets, and emails.
14
+ */
15
+ function redactSensitiveInfo(message) {
16
+ return message
17
+ .replace(/ya29\.[A-Za-z0-9_-]+/g, '[REDACTED_TOKEN]')
18
+ .replace(/1\/\/[A-Za-z0-9_-]+/g, '[REDACTED_REFRESH_TOKEN]')
19
+ .replace(/GOCSPX-[A-Za-z0-9_-]+/g, '[REDACTED_SECRET]')
20
+ .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]');
21
+ }
22
+ export const GMAIL_SCOPES = ['https://www.googleapis.com/auth/gmail.send'];
23
+ export class GmailMailer {
24
+ oauth2Client;
25
+ constructor(config) {
26
+ if (!config.clientId || !config.clientSecret || !config.refreshToken) {
27
+ throw new Error('OAuth2 config requires clientId, clientSecret, and refreshToken');
28
+ }
29
+ this.oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, config.redirectUri);
30
+ this.oauth2Client.setCredentials({
31
+ refresh_token: config.refreshToken,
32
+ access_token: config.accessToken,
33
+ });
34
+ }
35
+ /**
36
+ * Ensure a valid access token is available (refreshes if needed).
37
+ */
38
+ async ensureAccessToken() {
39
+ // Force a token refresh by calling getRequestHeaders which triggers the refresh flow
40
+ await this.oauth2Client.getRequestHeaders();
41
+ }
42
+ /**
43
+ * Manually refresh the access token using the refresh token.
44
+ * Use this if automatic refresh fails.
45
+ */
46
+ async forceRefreshToken() {
47
+ const credentials = this.oauth2Client.credentials;
48
+ const refreshToken = credentials.refresh_token;
49
+ if (!refreshToken) {
50
+ throw new Error('No refresh token available');
51
+ }
52
+ // Direct token refresh via Google's token endpoint
53
+ const tokenUrl = 'https://oauth2.googleapis.com/token';
54
+ const params = new URLSearchParams({
55
+ client_id: this.oauth2Client._clientId ?? '',
56
+ client_secret: this.oauth2Client._clientSecret ?? '',
57
+ refresh_token: refreshToken,
58
+ grant_type: 'refresh_token',
59
+ });
60
+ const response = await fetch(tokenUrl, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
63
+ body: params.toString(),
64
+ });
65
+ if (!response.ok) {
66
+ const error = await response.text();
67
+ throw new Error(`Token refresh failed: ${redactSensitiveInfo(error)}`);
68
+ }
69
+ const data = await response.json();
70
+ this.oauth2Client.setCredentials({
71
+ access_token: data.access_token,
72
+ refresh_token: data.refresh_token ?? refreshToken,
73
+ });
74
+ }
75
+ /**
76
+ * Send an email via Gmail API.
77
+ * Supports raw HTML, plain text, or Handlebars templates.
78
+ */
79
+ async sendEmail(options) {
80
+ // Ensure we have a valid access token before sending
81
+ await this.ensureAccessToken();
82
+ const token = await this.getAccessToken();
83
+ let html = options.html;
84
+ let text = options.text;
85
+ // Render template if provided
86
+ if (options.template && options.context) {
87
+ html = renderTemplate(options.template, options.context);
88
+ }
89
+ if (!html && !text) {
90
+ throw new Error('Email requires html, text, or template');
91
+ }
92
+ // Build MIME message
93
+ const raw = await buildRawMessage({
94
+ from: options.from,
95
+ to: options.to,
96
+ cc: options.cc,
97
+ bcc: options.bcc,
98
+ replyTo: options.replyTo,
99
+ subject: options.subject,
100
+ text,
101
+ html,
102
+ attachments: options.attachments,
103
+ });
104
+ // Send via Gmail API using fetch directly
105
+ const response = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Authorization': `Bearer ${token}`,
109
+ 'Content-Type': 'application/json',
110
+ },
111
+ body: JSON.stringify({ raw }),
112
+ });
113
+ if (!response.ok) {
114
+ const errorText = await response.text();
115
+ throw new Error(`Gmail API error (${response.status}): ${redactSensitiveInfo(errorText)}`);
116
+ }
117
+ const data = await response.json();
118
+ return {
119
+ messageId: data.id,
120
+ threadId: data.threadId,
121
+ labelIds: data.labelIds,
122
+ };
123
+ }
124
+ /**
125
+ * Get the current OAuth2 access token (triggers refresh if needed).
126
+ */
127
+ async getAccessToken() {
128
+ const token = await this.oauth2Client.getAccessToken();
129
+ if (!token.token) {
130
+ throw new Error('Failed to obtain access token');
131
+ }
132
+ return token.token;
133
+ }
134
+ /**
135
+ * Verify OAuth2 credentials by fetching the Gmail user's profile.
136
+ */
137
+ async verifyConnection() {
138
+ // Ensure we have a valid access token before verifying
139
+ await this.ensureAccessToken();
140
+ const token = await this.getAccessToken();
141
+ const response = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/profile', {
142
+ headers: { Authorization: `Bearer ${token}` },
143
+ });
144
+ if (!response.ok) {
145
+ const errorText = await response.text();
146
+ throw new Error(`Gmail profile error (${response.status}): ${redactSensitiveInfo(errorText)}`);
147
+ }
148
+ const data = await response.json();
149
+ if (!data.emailAddress) {
150
+ throw new Error('Could not retrieve user email');
151
+ }
152
+ return data.emailAddress;
153
+ }
154
+ }
@@ -0,0 +1,5 @@
1
+ export { UniversalMailer } from './universal-mailer.js';
2
+ export type { MailerConfig, SendOptions, SendResult, FetchOptions, EmailMessage } from './universal-mailer.js';
3
+ export { renderTemplate, registerTemplate, registerHelper } from './template.js';
4
+ export { buildMimeMessage, buildRawMessage } from './mime-builder.js';
5
+ export type { MimeOptions, Attachment } from './mime-builder.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { UniversalMailer } from './universal-mailer.js';
2
+ export { renderTemplate, registerTemplate, registerHelper } from './template.js';
3
+ export { buildMimeMessage, buildRawMessage } from './mime-builder.js';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MIME message builder using nodemailer's internal MailComposer.
3
+ * Generates RFC 2822 compliant email messages.
4
+ */
5
+ export interface Attachment {
6
+ filename: string;
7
+ content: string | Buffer;
8
+ contentType?: string;
9
+ }
10
+ export interface MimeOptions {
11
+ from: string;
12
+ to: string;
13
+ cc?: string;
14
+ bcc?: string;
15
+ replyTo?: string;
16
+ subject: string;
17
+ text?: string;
18
+ html?: string;
19
+ attachments?: Attachment[];
20
+ }
21
+ /**
22
+ * Build a raw RFC 2822 email message.
23
+ * @param options - Email message options
24
+ * @returns Raw email buffer
25
+ */
26
+ export declare function buildMimeMessage(options: MimeOptions): Promise<Buffer>;
27
+ /**
28
+ * Build and encode a raw email message as base64url string.
29
+ * Required for Gmail API messages.send endpoint.
30
+ * @param options - Email message options
31
+ * @returns Base64url encoded string
32
+ */
33
+ export declare function buildRawMessage(options: MimeOptions): Promise<string>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * MIME message builder using nodemailer's internal MailComposer.
3
+ * Generates RFC 2822 compliant email messages.
4
+ */
5
+ import MailComposer from 'nodemailer/lib/mail-composer/index.js';
6
+ /**
7
+ * Build a raw RFC 2822 email message.
8
+ * @param options - Email message options
9
+ * @returns Raw email buffer
10
+ */
11
+ export async function buildMimeMessage(options) {
12
+ const mailOptions = {
13
+ from: options.from,
14
+ to: options.to,
15
+ cc: options.cc,
16
+ bcc: options.bcc,
17
+ replyTo: options.replyTo,
18
+ subject: options.subject,
19
+ text: options.text,
20
+ html: options.html,
21
+ attachments: options.attachments?.map(a => ({
22
+ filename: a.filename,
23
+ content: a.content,
24
+ contentType: a.contentType,
25
+ })),
26
+ };
27
+ const composer = new MailComposer(mailOptions);
28
+ return composer.compile().build();
29
+ }
30
+ /**
31
+ * Build and encode a raw email message as base64url string.
32
+ * Required for Gmail API messages.send endpoint.
33
+ * @param options - Email message options
34
+ * @returns Base64url encoded string
35
+ */
36
+ export async function buildRawMessage(options) {
37
+ const buffer = await buildMimeMessage(options);
38
+ return buffer
39
+ .toString('base64')
40
+ .replace(/\+/g, '-')
41
+ .replace(/\//g, '_')
42
+ .replace(/=+$/, '');
43
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Handlebars template engine for email content.
3
+ * Pure function, no side effects.
4
+ */
5
+ import Handlebars from 'handlebars';
6
+ /**
7
+ * Register a Handlebars template by name.
8
+ * @param name - Template identifier
9
+ * @param source - Handlebars template string
10
+ */
11
+ export declare function registerTemplate(name: string, source: string): void;
12
+ /**
13
+ * Render a Handlebars template with context data.
14
+ * @param template - Handlebars template string
15
+ * @param context - Data object for template variables
16
+ * @returns Rendered HTML string
17
+ */
18
+ export declare function renderTemplate(template: string, context: Record<string, unknown>): string;
19
+ /**
20
+ * Register a custom Handlebars helper.
21
+ * @param name - Helper name
22
+ * @param fn - Helper function
23
+ */
24
+ export declare function registerHelper(name: string, fn: Handlebars.HelperDelegate): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Handlebars template engine for email content.
3
+ * Pure function, no side effects.
4
+ */
5
+ import Handlebars from 'handlebars';
6
+ /**
7
+ * Register a Handlebars template by name.
8
+ * @param name - Template identifier
9
+ * @param source - Handlebars template string
10
+ */
11
+ export function registerTemplate(name, source) {
12
+ Handlebars.registerPartial(name, source);
13
+ }
14
+ /**
15
+ * Render a Handlebars template with context data.
16
+ * @param template - Handlebars template string
17
+ * @param context - Data object for template variables
18
+ * @returns Rendered HTML string
19
+ */
20
+ export function renderTemplate(template, context) {
21
+ const compiled = Handlebars.compile(template);
22
+ return compiled(context);
23
+ }
24
+ /**
25
+ * Register a custom Handlebars helper.
26
+ * @param name - Helper name
27
+ * @param fn - Helper function
28
+ */
29
+ export function registerHelper(name, fn) {
30
+ Handlebars.registerHelper(name, fn);
31
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Universal Mailer — Universal email client.
3
+ * Supports SMTP, Gmail API, and IMAP/POP3.
4
+ *
5
+ * Architecture: Strategy Pattern
6
+ * - SendTransport: SMTP or Gmail API
7
+ * - FetchTransport: IMAP or POP3
8
+ */
9
+ import { type Attachment } from './mime-builder.js';
10
+ export type TransportType = 'smtp' | 'gmail-api';
11
+ export interface SmtpConfig {
12
+ host: string;
13
+ port: number;
14
+ secure?: boolean;
15
+ auth: {
16
+ user: string;
17
+ pass: string;
18
+ };
19
+ }
20
+ export interface GmailApiConfig {
21
+ clientId: string;
22
+ clientSecret: string;
23
+ refreshToken: string;
24
+ accessToken?: string;
25
+ redirectUri?: string;
26
+ }
27
+ export interface ImapConfig {
28
+ host: string;
29
+ port: number;
30
+ secure?: boolean;
31
+ auth: {
32
+ user: string;
33
+ pass: string;
34
+ };
35
+ }
36
+ export interface Pop3Config {
37
+ host: string;
38
+ port: number;
39
+ secure?: boolean;
40
+ auth: {
41
+ user: string;
42
+ pass: string;
43
+ };
44
+ }
45
+ export interface MailerConfig {
46
+ transport: TransportType;
47
+ smtp?: SmtpConfig;
48
+ gmailApi?: GmailApiConfig;
49
+ fetchTransport?: 'imap' | 'pop3';
50
+ imap?: ImapConfig;
51
+ pop3?: Pop3Config;
52
+ }
53
+ export interface SendOptions {
54
+ from: string;
55
+ to: string;
56
+ cc?: string;
57
+ bcc?: string;
58
+ replyTo?: string;
59
+ subject: string;
60
+ text?: string;
61
+ html?: string;
62
+ template?: string;
63
+ context?: Record<string, unknown>;
64
+ attachments?: Attachment[];
65
+ }
66
+ export interface SendResult {
67
+ messageId: string;
68
+ threadId?: string;
69
+ labelIds?: string[];
70
+ }
71
+ export interface FetchOptions {
72
+ limit?: number;
73
+ seen?: boolean;
74
+ mailbox?: string;
75
+ }
76
+ export interface EmailMessage {
77
+ id: string;
78
+ from: string;
79
+ to: string;
80
+ subject: string;
81
+ date: Date;
82
+ text?: string;
83
+ html?: string;
84
+ seen: boolean;
85
+ attachments: Array<{
86
+ filename: string;
87
+ contentType: string;
88
+ }>;
89
+ }
90
+ export declare class UniversalMailer {
91
+ private sender;
92
+ private fetcher?;
93
+ constructor(config: MailerConfig);
94
+ /**
95
+ * Send an email via the configured transport.
96
+ */
97
+ sendEmail(options: SendOptions): Promise<SendResult>;
98
+ /**
99
+ * Verify the send transport connection.
100
+ */
101
+ verifyConnection(): Promise<string>;
102
+ /**
103
+ * Fetch emails via IMAP (if configured).
104
+ */
105
+ fetchEmails(options?: FetchOptions): Promise<EmailMessage[]>;
106
+ /**
107
+ * Get OAuth2 access token (Gmail API only).
108
+ */
109
+ getAccessToken(): Promise<string>;
110
+ /**
111
+ * Force refresh OAuth2 token (Gmail API only).
112
+ */
113
+ forceRefreshToken(): Promise<void>;
114
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Universal Mailer — Universal email client.
3
+ * Supports SMTP, Gmail API, and IMAP/POP3.
4
+ *
5
+ * Architecture: Strategy Pattern
6
+ * - SendTransport: SMTP or Gmail API
7
+ * - FetchTransport: IMAP or POP3
8
+ */
9
+ import { createTransport } from 'nodemailer';
10
+ import { google } from 'googleapis';
11
+ import { ImapFlow } from 'imapflow';
12
+ import { simpleParser } from 'mailparser';
13
+ import { renderTemplate } from './template.js';
14
+ import { buildRawMessage } from './mime-builder.js';
15
+ // ============================================================
16
+ // Security
17
+ // ============================================================
18
+ function redactSensitiveInfo(message) {
19
+ return message
20
+ .replace(/ya29\.[A-Za-z0-9_-]+/g, '[REDACTED_TOKEN]')
21
+ .replace(/1\/\/[A-Za-z0-9_-]+/g, '[REDACTED_REFRESH_TOKEN]')
22
+ .replace(/GOCSPX-[A-Za-z0-9_-]+/g, '[REDACTED_SECRET]')
23
+ .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]');
24
+ }
25
+ // ============================================================
26
+ // Gmail API Transport
27
+ // ============================================================
28
+ class GmailApiSender {
29
+ oauth2Client;
30
+ constructor(config) {
31
+ if (!config.clientId || !config.clientSecret || !config.refreshToken) {
32
+ throw new Error('Gmail API config requires clientId, clientSecret, and refreshToken');
33
+ }
34
+ this.oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, config.redirectUri);
35
+ this.oauth2Client.setCredentials({
36
+ refresh_token: config.refreshToken,
37
+ access_token: config.accessToken,
38
+ });
39
+ }
40
+ async ensureAccessToken() {
41
+ await this.oauth2Client.getRequestHeaders();
42
+ }
43
+ async getAccessToken() {
44
+ await this.ensureAccessToken();
45
+ const token = await this.oauth2Client.getAccessToken();
46
+ if (!token.token) {
47
+ throw new Error('Failed to obtain access token');
48
+ }
49
+ return token.token;
50
+ }
51
+ async forceRefreshToken() {
52
+ const credentials = this.oauth2Client.credentials;
53
+ const refreshToken = credentials.refresh_token;
54
+ if (!refreshToken)
55
+ throw new Error('No refresh token available');
56
+ const tokenUrl = 'https://oauth2.googleapis.com/token';
57
+ const params = new URLSearchParams({
58
+ client_id: this.oauth2Client._clientId ?? '',
59
+ client_secret: this.oauth2Client._clientSecret ?? '',
60
+ refresh_token: refreshToken,
61
+ grant_type: 'refresh_token',
62
+ });
63
+ const response = await fetch(tokenUrl, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
66
+ body: params.toString(),
67
+ });
68
+ if (!response.ok) {
69
+ const error = await response.text();
70
+ throw new Error(`Token refresh failed: ${redactSensitiveInfo(error)}`);
71
+ }
72
+ const data = await response.json();
73
+ this.oauth2Client.setCredentials({
74
+ access_token: data.access_token,
75
+ refresh_token: data.refresh_token ?? refreshToken,
76
+ });
77
+ }
78
+ async sendEmail(options) {
79
+ await this.ensureAccessToken();
80
+ const token = await this.getAccessToken();
81
+ let html = options.html;
82
+ let text = options.text;
83
+ if (options.template && options.context) {
84
+ html = renderTemplate(options.template, options.context);
85
+ }
86
+ if (!html && !text) {
87
+ throw new Error('Email requires html, text, or template');
88
+ }
89
+ const raw = await buildRawMessage({
90
+ from: options.from,
91
+ to: options.to,
92
+ cc: options.cc,
93
+ bcc: options.bcc,
94
+ replyTo: options.replyTo,
95
+ subject: options.subject,
96
+ text,
97
+ html,
98
+ attachments: options.attachments,
99
+ });
100
+ const response = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Authorization': `Bearer ${token}`,
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ body: JSON.stringify({ raw }),
107
+ });
108
+ if (!response.ok) {
109
+ const errorText = await response.text();
110
+ throw new Error(`Gmail API error (${response.status}): ${redactSensitiveInfo(errorText)}`);
111
+ }
112
+ const data = await response.json();
113
+ return {
114
+ messageId: data.id,
115
+ threadId: data.threadId,
116
+ labelIds: data.labelIds,
117
+ };
118
+ }
119
+ async verifyConnection() {
120
+ await this.ensureAccessToken();
121
+ const token = await this.getAccessToken();
122
+ const response = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/profile', {
123
+ headers: { Authorization: `Bearer ${token}` },
124
+ });
125
+ if (!response.ok) {
126
+ const errorText = await response.text();
127
+ throw new Error(`Gmail profile error (${response.status}): ${redactSensitiveInfo(errorText)}`);
128
+ }
129
+ const data = await response.json();
130
+ if (!data.emailAddress)
131
+ throw new Error('Could not retrieve user email');
132
+ return data.emailAddress;
133
+ }
134
+ }
135
+ // ============================================================
136
+ // SMTP Transport
137
+ // ============================================================
138
+ class SmtpSender {
139
+ transporter;
140
+ constructor(config) {
141
+ if (!config.host || !config.port || !config.auth?.user || !config.auth?.pass) {
142
+ throw new Error('SMTP config requires host, port, user, and pass');
143
+ }
144
+ this.transporter = createTransport({
145
+ host: config.host,
146
+ port: config.port,
147
+ secure: config.secure ?? config.port === 465,
148
+ auth: config.auth,
149
+ });
150
+ }
151
+ async sendEmail(options) {
152
+ let html = options.html;
153
+ let text = options.text;
154
+ if (options.template && options.context) {
155
+ html = renderTemplate(options.template, options.context);
156
+ }
157
+ if (!html && !text) {
158
+ throw new Error('Email requires html, text, or template');
159
+ }
160
+ const info = await this.transporter.sendMail({
161
+ from: options.from,
162
+ to: options.to,
163
+ cc: options.cc,
164
+ bcc: options.bcc,
165
+ replyTo: options.replyTo,
166
+ subject: options.subject,
167
+ text,
168
+ html,
169
+ attachments: options.attachments?.map(a => ({
170
+ filename: a.filename,
171
+ content: a.content,
172
+ contentType: a.contentType,
173
+ })),
174
+ });
175
+ return {
176
+ messageId: info.messageId,
177
+ };
178
+ }
179
+ async verifyConnection() {
180
+ await this.transporter.verify();
181
+ return 'SMTP connection verified';
182
+ }
183
+ }
184
+ // ============================================================
185
+ // IMAP Fetcher
186
+ // ============================================================
187
+ class ImapFetcher {
188
+ config;
189
+ constructor(config) {
190
+ if (!config.host || !config.port || !config.auth?.user || !config.auth?.pass) {
191
+ throw new Error('IMAP config requires host, port, user, and pass');
192
+ }
193
+ this.config = config;
194
+ }
195
+ async fetchEmails(options = {}) {
196
+ const { limit = 10, seen, mailbox = 'INBOX' } = options;
197
+ const client = new ImapFlow({
198
+ host: this.config.host,
199
+ port: this.config.port,
200
+ secure: this.config.secure ?? this.config.port === 993,
201
+ auth: this.config.auth,
202
+ });
203
+ try {
204
+ await client.connect();
205
+ const lock = await client.getMailboxLock(mailbox);
206
+ try {
207
+ const messages = [];
208
+ const search = {};
209
+ if (seen !== undefined)
210
+ search.seen = seen;
211
+ let count = 0;
212
+ for await (const message of client.fetch(search, {
213
+ envelope: true,
214
+ source: true,
215
+ })) {
216
+ if (count >= limit)
217
+ break;
218
+ count++;
219
+ const parsed = await simpleParser(message.source ?? '');
220
+ messages.push({
221
+ id: message.uid?.toString() ?? '',
222
+ from: parsed.from?.text ?? '',
223
+ to: parsed.to?.text ?? '',
224
+ subject: parsed.subject ?? '',
225
+ date: parsed.date ?? new Date(),
226
+ text: parsed.text ?? undefined,
227
+ html: parsed.html ?? undefined,
228
+ seen: message.flags?.has('\\Seen') ?? false,
229
+ attachments: (parsed.attachments ?? []).map((a) => ({
230
+ filename: a.filename ?? 'unknown',
231
+ contentType: a.contentType ?? 'application/octet-stream',
232
+ })),
233
+ });
234
+ }
235
+ return messages;
236
+ }
237
+ finally {
238
+ lock.release();
239
+ }
240
+ }
241
+ finally {
242
+ client.close();
243
+ }
244
+ }
245
+ }
246
+ // ============================================================
247
+ // Universal Mailer (Facade)
248
+ // ============================================================
249
+ export class UniversalMailer {
250
+ sender;
251
+ fetcher;
252
+ constructor(config) {
253
+ // Setup send transport
254
+ if (config.transport === 'gmail-api') {
255
+ if (!config.gmailApi) {
256
+ throw new Error('Gmail API transport requires gmailApi config');
257
+ }
258
+ this.sender = new GmailApiSender(config.gmailApi);
259
+ }
260
+ else if (config.transport === 'smtp') {
261
+ if (!config.smtp) {
262
+ throw new Error('SMTP transport requires smtp config');
263
+ }
264
+ this.sender = new SmtpSender(config.smtp);
265
+ }
266
+ else {
267
+ throw new Error(`Unknown transport: ${config.transport}`);
268
+ }
269
+ // Setup fetch transport
270
+ if (config.fetchTransport === 'imap') {
271
+ if (!config.imap) {
272
+ throw new Error('IMAP fetch transport requires imap config');
273
+ }
274
+ this.fetcher = new ImapFetcher(config.imap);
275
+ }
276
+ // POP3: not implemented yet (future)
277
+ }
278
+ /**
279
+ * Send an email via the configured transport.
280
+ */
281
+ async sendEmail(options) {
282
+ return this.sender.sendEmail(options);
283
+ }
284
+ /**
285
+ * Verify the send transport connection.
286
+ */
287
+ async verifyConnection() {
288
+ return this.sender.verifyConnection();
289
+ }
290
+ /**
291
+ * Fetch emails via IMAP (if configured).
292
+ */
293
+ async fetchEmails(options = {}) {
294
+ if (!this.fetcher) {
295
+ throw new Error('Fetch transport not configured. Set fetchTransport to "imap" in config.');
296
+ }
297
+ return this.fetcher.fetchEmails(options);
298
+ }
299
+ /**
300
+ * Get OAuth2 access token (Gmail API only).
301
+ */
302
+ async getAccessToken() {
303
+ if (this.sender instanceof GmailApiSender) {
304
+ return this.sender.getAccessToken();
305
+ }
306
+ throw new Error('getAccessToken is only available for Gmail API transport');
307
+ }
308
+ /**
309
+ * Force refresh OAuth2 token (Gmail API only).
310
+ */
311
+ async forceRefreshToken() {
312
+ if (this.sender instanceof GmailApiSender) {
313
+ return this.sender.forceRefreshToken();
314
+ }
315
+ throw new Error('forceRefreshToken is only available for Gmail API transport');
316
+ }
317
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "universal-mailer-lib",
3
+ "version": "2.0.2",
4
+ "description": "Universal email client. Supports SMTP, Gmail API, and IMAP/POP3.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/index.js",
16
+ "dist/index.d.ts",
17
+ "dist/gmail-mailer.js",
18
+ "dist/gmail-mailer.d.ts",
19
+ "dist/universal-mailer.js",
20
+ "dist/universal-mailer.d.ts",
21
+ "dist/mime-builder.js",
22
+ "dist/mime-builder.d.ts",
23
+ "dist/template.js",
24
+ "dist/template.d.ts",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "npm run build && node dist/test-runner.js",
31
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
32
+ "dev": "npm run build && node dist/index.js",
33
+ "client": "npm run build && node dist/test-send.js",
34
+ "security:audit": "node scripts/security-audit.mjs",
35
+ "release:dry": "node scripts/release.mjs --dry-run",
36
+ "release": "node scripts/release.mjs",
37
+ "release:patch": "node scripts/release.mjs patch",
38
+ "release:minor": "node scripts/release.mjs minor",
39
+ "prepublishOnly": "npm run typecheck && npm run build"
40
+ },
41
+ "keywords": [
42
+ "email",
43
+ "mailer",
44
+ "smtp",
45
+ "imap",
46
+ "pop3",
47
+ "gmail-api",
48
+ "oauth2",
49
+ "handlebars",
50
+ "template",
51
+ "attachments",
52
+ "universal-mailer"
53
+ ],
54
+ "author": "",
55
+ "license": "MIT",
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ },
59
+ "peerDependencies": {
60
+ "googleapis": ">=140.0.0",
61
+ "nodemailer": ">=7.0.0",
62
+ "handlebars": ">=4.7.0",
63
+ "imapflow": ">=1.0.0",
64
+ "mailparser": ">=3.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^22.0.0",
68
+ "googleapis": "^171.0.0",
69
+ "nodemailer": "^8.0.0",
70
+ "handlebars": "^4.7.9",
71
+ "imapflow": "^1.0.179",
72
+ "mailparser": "^3.7.2",
73
+ "typescript": "^5.7.2"
74
+ }
75
+ }