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 +246 -0
- package/dist/gmail-mailer.d.ts +60 -0
- package/dist/gmail-mailer.js +154 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/mime-builder.d.ts +33 -0
- package/dist/mime-builder.js +43 -0
- package/dist/template.d.ts +24 -0
- package/dist/template.js +31 -0
- package/dist/universal-mailer.d.ts +114 -0
- package/dist/universal-mailer.js +317 -0
- package/package.json +75 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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;
|
package/dist/template.js
ADDED
|
@@ -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
|
+
}
|