This commit is contained in:
Martin
2024-09-19 21:51:50 +08:00
commit 39fc7df763
10 changed files with 2788 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
MAX_FILE_SIZE=5242880
AWS_REGION=your-aws-region
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
PORT=25
S3_BUCKET_NAME=your-s3-bucket-name
WEBHOOK_URL=https://your-webhook-url.com
SMTP_SECURE=false
WEBHOOK_CONCURRENCY=5
+18
View File
@@ -0,0 +1,18 @@
node_modules/
npm-debug.log
yarn-error.log
.env
logs
*.log
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
.npm
*.tgz
.yarn-integrity
.env.test
tmp/
temp/
+19
View File
@@ -0,0 +1,19 @@
const dotenv = require('dotenv');
const AWS = require('aws-sdk');
dotenv.config();
module.exports = {
WEBHOOK_URL: process.env.WEBHOOK_URL || 'https://enkhprqr4n2t.x.pipedream.net/',
PORT: process.env.PORT || 25,
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE || 5 * 1024 * 1024,
BUCKET_NAME: process.env.S3_BUCKET_NAME,
SMTP_SECURE: process.env.SMTP_SECURE === 'true',
WEBHOOK_CONCURRENCY: process.env.WEBHOOK_CONCURRENCY || 5,
s3: new AWS.S3({
region: process.env.AWS_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
})
};
+2500
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.x.x",
"@aws-sdk/lib-storage": "^3.x.x",
"aws-sdk": "^2.x",
"axios": "^1.7.7",
"better-queue": "^3.8.12",
"dotenv": "^16.4.5",
"mailparser": "^3.7.1",
"smtp-server": "^3.13.5"
}
}
+87
View File
@@ -0,0 +1,87 @@
# Inbound Email (SMTP) to Webhook
Author: [Martin Alexander](https://martinalexander.me/) - [LinkedIn](https://www.linkedin.com/in/martin-alexander)
A simple, efficient script that provides an SMTP server to receive emails, parse content (including headers), store attachments in Amazon S3, and forward email content to a webhook. Graceful handling of multiple concurrent SMTP sessions and webhook requests.
## Features
- SMTP server to receive emails concurrently
- Parses incoming emails using `mailparser`
- Uploads attachments to Amazon S3
- Forwards parsed email content to a specified webhook
- Configurable via environment variables
- Handles large attachments gracefully
- Robust queue system for processing multiple emails and webhook requests simultaneously
## Prerequisites
- Node.js (v18 or later recommended)
- If saving attachments, an Amazon Web Services (AWS) account with S3 access or a compatible system
- A HTTP(s) webhook endpoint to receive the processed emails
## Installation
1. Clone this repository:
```
git clone https://github.com/sendbetter/inbound-email.git
cd inbound-email
```
2. Install dependencies:
```
npm install
```
3. Copy the `.env.example` file to `.env` and set the required configuration: (eg. `mv .env.example .env`)
- `MAX_FILE_SIZE`: Maximum size of attachments to process, in bytes (default: 5MB)
- `AWS_REGION`: Your AWS region
- `AWS_ACCESS_KEY_ID`: Your AWS access key ID
- `AWS_SECRET_ACCESS_KEY`: Your AWS secret access key
- `S3_BUCKET_NAME`: The name of your S3 bucket for storing attachments
- `PORT`: The port for the SMTP server to listen on (default: 25)
- `WEBHOOK_URL`: The URL where parsed emails will be sent (required)
- `SMTP_SECURE`: Set to 'true' for TLS support (default: false)
- `WEBHOOK_CONCURRENCY`: Number of concurrent webhook requests (default: 5)
## Usage
Start the server:
```
npm start
```
The SMTP server will start and listen on the specified port (default: 25) on all network interfaces.
You can use pm2 or supervisor to keep the server running after restart. Example: `pm2 start server.js`
## Sample Use Cases
1. **Email to Ticket System**: Use this bridge to receive support emails and automatically create tickets in your helpdesk system via the webhook.
2. **Document Processing**: Receive emails with document attachments, store them in S3, and trigger a document processing pipeline through the webhook.
3. **Email Marketing Analysis**: Collect incoming emails from a campaign, store any images or attachments, and send the content to an analytics system for processing.
4. **Automated Reporting**: Set up an email address that receives automated reports, stores them in S3, and notifies your team via the webhook.
5. **DMARC Reporting**: Receive DMARC reports via email and store them in S3.
Using inbound parse for something interesting? Please let me know, I'd love to hear about it.
## Todo
- Rate limiting
- Log Storage
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License. This means you are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, including for commercial purposes.
## Disclaimer
Please ensure you have the necessary permissions and security measures in place when deploying an SMTP server. Depending on your firewall configuration, you may be exposing this service to the internet.
+78
View File
@@ -0,0 +1,78 @@
const SMTPServer = require('smtp-server').SMTPServer;
const config = require('./config');
const { parseEmail } = require('./services/emailParser');
const { sendToWebhook } = require('./services/webhookService');
require('aws-sdk/lib/maintenance_mode_message').suppress = true;
// Use a more robust queue implementation
const Queue = require('better-queue');
// Create a queue for webhook sending
const webhookQueue = new Queue(async function (parsed, cb) {
try {
await sendToWebhook(parsed);
console.log('Successfully sent to webhook');
cb(null);
} catch (error) {
console.error('Webhook error:', error.message);
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', error.response.data);
}
cb(error);
}
}, { concurrent: config.WEBHOOK_CONCURRENCY || 5 });
const server = new SMTPServer({
onData(stream, session, callback) {
parseEmail(stream)
.then(parsed => {
webhookQueue.push(parsed);
console.log('Email added to queue. Queue size:', webhookQueue.getStats().total);
callback();
})
.catch(error => {
console.error('Parsing error:', error);
callback(new Error('Failed to parse email'));
});
},
onError(error) {
console.error('SMTP server error:', error);
},
disabledCommands: ['AUTH'],
secure: config.SMTP_SECURE
});
server.listen(config.PORT, '0.0.0.0', err => {
if (err) {
console.error('Failed to start SMTP server:', err);
process.exit(1);
}
console.log(`SMTP server listening on port ${config.PORT} on all interfaces`);
});
function gracefulShutdown(reason) {
console.log(`Shutting down: ${reason}`);
server.close(() => {
console.log('Server closed. Exiting process.');
process.exit(0);
});
}
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
gracefulShutdown('Uncaught exception');
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
gracefulShutdown('Unhandled rejection');
});
process.on('SIGTERM', () => {
gracefulShutdown('SIGTERM signal received');
});
process.on('SIGINT', () => {
gracefulShutdown('SIGINT signal received');
});
+29
View File
@@ -0,0 +1,29 @@
const simpleParser = require('mailparser').simpleParser;
const { uploadToS3 } = require('./s3Service');
async function parseEmail(stream) {
const parsed = await simpleParser(stream);
const attachments = parsed.attachments || [];
delete parsed.attachments;
const processedAttachments = await Promise.all(attachments.map(async att => {
const s3Url = await uploadToS3(att);
return {
filename: att.filename,
contentType: att.contentType,
size: att.size,
s3Url: s3Url,
skipped: s3Url === null
};
}));
parsed.attachmentInfo = processedAttachments.filter(att => !att.skipped);
parsed.skippedAttachments = processedAttachments.filter(att => att.skipped).map(att => ({
filename: att.filename,
size: att.size
}));
return parsed;
}
module.exports = { parseEmail };
+25
View File
@@ -0,0 +1,25 @@
const config = require('../config');
async function uploadToS3(attachment) {
if (attachment.size > config.MAX_FILE_SIZE) {
console.log(`Skipping large attachment: ${attachment.filename} (${attachment.size} bytes)`);
return null;
}
const params = {
Bucket: config.BUCKET_NAME,
Key: `${Date.now()}-${attachment.filename}`,
Body: attachment.content,
ContentType: attachment.contentType
};
try {
const result = await config.s3.upload(params).promise();
return result.Location;
} catch (error) {
console.error('S3 upload error:', error);
throw error;
}
}
module.exports = { uploadToS3 };
+8
View File
@@ -0,0 +1,8 @@
const axios = require('axios');
const config = require('../config');
function sendToWebhook(data) {
return axios.post(config.WEBHOOK_URL, data, { timeout: 5000 });
}
module.exports = { sendToWebhook };