Added lots of stuff
This commit is contained in:
parent
432b3311b9
commit
af06ad78c9
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
database.js
|
||||||
|
haraka.js
|
||||||
49
knexfile.js
Executable file
49
knexfile.js
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
const config = require('./src/config/database');
|
||||||
|
/**
|
||||||
|
* @type { Object.<string, import("knex").Knex.Config> }
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
development: {
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
host: config.development.connection.host,
|
||||||
|
user: config.development.connection.user,
|
||||||
|
password: config.development.connection.password,
|
||||||
|
database: config.development.connection.database
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: './src/db/migrations',
|
||||||
|
tableName: 'knex_migrations'
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: './src/db/seeds'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
production: {
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
host: config.development.connection.host,
|
||||||
|
user: config.development.connection.user,
|
||||||
|
password: config.development.connection.password,
|
||||||
|
database: config.development.connection.database
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: config.development.migrations.directory,
|
||||||
|
tableName: 'knex_migrations'
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: './src/db/seeds'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
5
nodemon.json
Normal file
5
nodemon.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"ignore": ["test.js", "README"],
|
||||||
|
"watch": ["views", "*.js"],
|
||||||
|
"exec": "node app.js"
|
||||||
|
}
|
||||||
@ -3,7 +3,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node app.js",
|
||||||
|
"dev": "nodemon app.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
21
setup.sh
Executable file
21
setup.sh
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
CURRENT_DIR=$(pwd)
|
||||||
|
|
||||||
|
# Create group and user
|
||||||
|
sudo groupadd smtp
|
||||||
|
sudo useradd -r -g smtp smtp
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
sudo mkdir -p /var/spool/haraka /var/log/haraka
|
||||||
|
sudo chown -R smtp:smtp /var/spool/haraka /var/log/haraka
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
sudo chmod 755 /var/spool/haraka
|
||||||
|
sudo chmod 644 /var/log/haraka
|
||||||
|
|
||||||
|
# Install Haraka
|
||||||
|
npm install -g haraka
|
||||||
|
|
||||||
|
# Install Haraka plugins
|
||||||
|
mkdir -p $CURRENT_DIR/src/email_server/plugins/queue
|
||||||
|
ln -s $CURRENT_DIR/src/haraka-plugins/queue/store_message.js $CURRENT_DIR/src/email_server/plugins/queue/store_message.js
|
||||||
15
src/config/database-example.js
Normal file
15
src/config/database-example.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
development: {
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
database: 'email_api',
|
||||||
|
user: 'email_api',
|
||||||
|
password: '',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3306
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: '../db/migrations'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/config/haraka-example.js
Normal file
13
src/config/haraka-example.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
smtp: {
|
||||||
|
port: 2525,
|
||||||
|
host: '0.0.0.0'
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
retention: 14 // days
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
jwtSecret: '',
|
||||||
|
tokenExpiry: '24h'
|
||||||
|
}
|
||||||
|
};
|
||||||
51
src/db/migrations/20250127075144_initial.js
Normal file
51
src/db/migrations/20250127075144_initial.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable("domains", (table) => {
|
||||||
|
table.increments("id");
|
||||||
|
table.string("name").notNullable().unique();
|
||||||
|
table.boolean("active").defaultTo(true);
|
||||||
|
table.timestamps(true, true);
|
||||||
|
})
|
||||||
|
.createTable("users", (table) => {
|
||||||
|
table.increments("id");
|
||||||
|
table.string("email").notNullable().unique();
|
||||||
|
table.string("password").notNullable();
|
||||||
|
table.boolean("is_admin").defaultTo(false);
|
||||||
|
table.text("api_key", "longtext").nullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
})
|
||||||
|
.createTable("temp_emails", (table) => {
|
||||||
|
table.increments("id");
|
||||||
|
table.string("email").notNullable().unique();
|
||||||
|
table.integer("user_id").unsigned().references("users.id");
|
||||||
|
table.timestamp("expires_at").notNullable();
|
||||||
|
table.timestamps(true, true);
|
||||||
|
})
|
||||||
|
.createTable("messages", (table) => {
|
||||||
|
table.increments("id");
|
||||||
|
table.integer("temp_email_id").unsigned().references("temp_emails.id");
|
||||||
|
table.string("from").notNullable();
|
||||||
|
table.string("to").notNullable();
|
||||||
|
table.text("subject");
|
||||||
|
table.text("body");
|
||||||
|
table.jsonb("headers");
|
||||||
|
table.timestamp("received_at").defaultTo(knex.fn.now());
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.dropTable("messages")
|
||||||
|
.dropTable("users")
|
||||||
|
.dropTable("domains")
|
||||||
|
.dropTable("temp_emails");
|
||||||
|
};
|
||||||
18
src/db/models/Base.js
Normal file
18
src/db/models/Base.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const { Model } = require("objection");
|
||||||
|
|
||||||
|
class BaseModel extends Model {
|
||||||
|
static get modelPaths() {
|
||||||
|
return [__dirname];
|
||||||
|
}
|
||||||
|
|
||||||
|
$beforeInsert() {
|
||||||
|
this.created_at = new Date().toISOString();
|
||||||
|
this.updated_at = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$beforeUpdate() {
|
||||||
|
this.updated_at = new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseModel;
|
||||||
13
src/db/models/Domain.js
Normal file
13
src/db/models/Domain.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const BaseModel = require('./Base');
|
||||||
|
|
||||||
|
class Domain extends BaseModel {
|
||||||
|
static get tableName() {
|
||||||
|
return 'domains';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Domain;
|
||||||
23
src/db/models/Message.js
Normal file
23
src/db/models/Message.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const BaseModel = require('./Base');
|
||||||
|
|
||||||
|
class Message extends BaseModel {
|
||||||
|
static get tableName() {
|
||||||
|
return 'messages';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
const TempEmail = require('./TempEmail');
|
||||||
|
return {
|
||||||
|
domain: {
|
||||||
|
relation: BaseModel.BelongsToOneRelation,
|
||||||
|
modelClass: TempEmail,
|
||||||
|
join: {
|
||||||
|
from: 'messages.temp_email_id',
|
||||||
|
to: 'temp_emails.id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Message;
|
||||||
23
src/db/models/TempEmail.js
Normal file
23
src/db/models/TempEmail.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const BaseModel = require('./Base');
|
||||||
|
|
||||||
|
class TempEmail extends BaseModel {
|
||||||
|
static get tableName() {
|
||||||
|
return 'temp_emails';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings() {
|
||||||
|
const Message = require('./Message');
|
||||||
|
return {
|
||||||
|
messages: {
|
||||||
|
relation: BaseModel.HasManyRelation,
|
||||||
|
modelClass: Message,
|
||||||
|
join: {
|
||||||
|
from: 'temp_emails.id',
|
||||||
|
to: 'messages.temp_email_id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TempEmail;
|
||||||
19
src/db/models/User.js
Normal file
19
src/db/models/User.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const BaseModel = require('./Base');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
|
class User extends BaseModel {
|
||||||
|
static get tableName() {
|
||||||
|
return 'users';
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeInsert() {
|
||||||
|
await super.$beforeInsert();
|
||||||
|
this.password = await bcrypt.hash(this.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPassword(password) {
|
||||||
|
return bcrypt.compare(password, this.password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = User;
|
||||||
32
src/db/seeds/admin_user.js
Normal file
32
src/db/seeds/admin_user.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const config = require('../../config/haraka');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.seed = async function(knex) {
|
||||||
|
await knex('users').del();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash('admin', 10);
|
||||||
|
const [userId] = await knex('users').insert([
|
||||||
|
{
|
||||||
|
password: hashedPassword,
|
||||||
|
email: 'admin@admin.com',
|
||||||
|
is_admin: true,
|
||||||
|
api_key: crypto.randomBytes(16).toString('hex')
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = await knex('users').where('id', userId).first();
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user.id, email: user.email, is_admin: user.is_admin },
|
||||||
|
config.auth.jwtSecret,
|
||||||
|
{ expiresIn: config.auth.tokenExpiry }
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex('users').where('id', user.id).update({ api_key: token });
|
||||||
|
};
|
||||||
18
src/db/seeds/domains.js
Normal file
18
src/db/seeds/domains.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.seed = async function(knex) {
|
||||||
|
await knex('domains').del()
|
||||||
|
await knex('domains').insert([
|
||||||
|
{ name: 'icantreadpls.top', active: true},
|
||||||
|
{ name: 'icantreadpls.fyi', active: true},
|
||||||
|
{ name: '20is20butimnotgay.top', active: true},
|
||||||
|
{ name: '20is20butimnotgay.fyi', active: true},
|
||||||
|
{ name: 'bigwhitevanfbi.top', active: true},
|
||||||
|
{ name: 'bigwhitevanfbi.fyi', active: true},
|
||||||
|
{ name: 'idonthaveabig.wang,wang', active: true},
|
||||||
|
{ name: '2weekmail.fyi', active: true},
|
||||||
|
{ name: '2weekmail.test', active: true}
|
||||||
|
]);
|
||||||
|
};
|
||||||
27
src/email_server/README
Normal file
27
src/email_server/README
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Haraka
|
||||||
|
|
||||||
|
Congratulations on creating a new installation of Haraka.
|
||||||
|
|
||||||
|
This directory contains two key directories for how Haraka will function:
|
||||||
|
|
||||||
|
- config
|
||||||
|
|
||||||
|
This directory contains configuration files for Haraka. The
|
||||||
|
directory contains the default configuration. You probably want
|
||||||
|
to modify some files in here, particularly 'smtp.ini'.
|
||||||
|
|
||||||
|
- plugins
|
||||||
|
|
||||||
|
This directory contains custom plugins which you write to run in
|
||||||
|
Haraka. The plugins which ship with Haraka are still available
|
||||||
|
to use.
|
||||||
|
|
||||||
|
- docs/plugins
|
||||||
|
|
||||||
|
This directory contains documentation for your plugins.
|
||||||
|
|
||||||
|
Documentation for Haraka is available via 'haraka -h <name>' where the name
|
||||||
|
is either the name of a plugin (without the .js extension) or the name of
|
||||||
|
a core Haraka module, such as 'Connection' or 'Transaction'.
|
||||||
|
|
||||||
|
To get documentation on writing a plugin type 'haraka -h Plugins'.
|
||||||
9
src/email_server/config/host_list
Normal file
9
src/email_server/config/host_list
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
icantreadpls.top
|
||||||
|
icantreadpls.fyi
|
||||||
|
20is20butimnotgay.top
|
||||||
|
20is20butimnotgay.fyi
|
||||||
|
bigwhitevanfbi.top
|
||||||
|
bigwhitevanfbi.fyi
|
||||||
|
idonthaveabig.wang,wang
|
||||||
|
2weekmail.fyi
|
||||||
|
2weekmail.test
|
||||||
1
src/email_server/config/internalcmd_key
Normal file
1
src/email_server/config/internalcmd_key
Normal file
@ -0,0 +1 @@
|
|||||||
|
952c69a398e308057dd7ff5836bb34def13fbe37f0147c4dbf24f94daba60971
|
||||||
2
src/email_server/config/limit.ini
Normal file
2
src/email_server/config/limit.ini
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[values]
|
||||||
|
max_unrecognized_commands=10
|
||||||
11
src/email_server/config/log.ini
Normal file
11
src/email_server/config/log.ini
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[main]
|
||||||
|
|
||||||
|
; level=data, protocol, debug, info, notice, warn, error, crit, alert, emerg
|
||||||
|
level=info
|
||||||
|
|
||||||
|
; prepend timestamps to log entries? This setting does NOT affect logs emitted
|
||||||
|
; by logging plugins (like syslog).
|
||||||
|
timestamps=false
|
||||||
|
|
||||||
|
; format=default, logfmt, json
|
||||||
|
format=default
|
||||||
1
src/email_server/config/me
Normal file
1
src/email_server/config/me
Normal file
@ -0,0 +1 @@
|
|||||||
|
ryahn-SER
|
||||||
72
src/email_server/config/plugins
Normal file
72
src/email_server/config/plugins
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# This file lists plugins that Haraka will run
|
||||||
|
#
|
||||||
|
# Plugin ordering often matters, run 'haraka -o -c /path/to/haraka/config'
|
||||||
|
# to see the order plugins (and their hooks) will run.
|
||||||
|
#
|
||||||
|
# To see a list of installed plugins, run 'haraka -l'
|
||||||
|
#
|
||||||
|
# The plugin registry: https://github.com/haraka/Haraka/blob/master/Plugins.md
|
||||||
|
#
|
||||||
|
# To see the docs for a plugin, run 'haraka -h plugin.name'
|
||||||
|
|
||||||
|
# status
|
||||||
|
# process_title
|
||||||
|
# syslog
|
||||||
|
# watch
|
||||||
|
|
||||||
|
# CONNECT
|
||||||
|
# ----------
|
||||||
|
# toobusy
|
||||||
|
# karma
|
||||||
|
# relay
|
||||||
|
# access
|
||||||
|
# p0f
|
||||||
|
# geoip
|
||||||
|
# asn
|
||||||
|
# fcrdns
|
||||||
|
# dns-list
|
||||||
|
|
||||||
|
# HELO
|
||||||
|
# ----------
|
||||||
|
# early_talker
|
||||||
|
# helo.checks
|
||||||
|
# see 'haraka -h tls' before enabling!
|
||||||
|
# tls
|
||||||
|
#
|
||||||
|
# AUTH plugins require TLS before AUTH is advertised, see
|
||||||
|
# https://github.com/haraka/Haraka/wiki/Require-SSL-TLS
|
||||||
|
# ----------
|
||||||
|
# auth/flat_file
|
||||||
|
# auth/auth_proxy
|
||||||
|
|
||||||
|
# MAIL FROM
|
||||||
|
# ----------
|
||||||
|
mail_from.is_resolvable
|
||||||
|
# spf
|
||||||
|
|
||||||
|
# RCPT TO
|
||||||
|
# ----------
|
||||||
|
# At least one rcpt_to plugin is REQUIRED for inbound email.
|
||||||
|
rcpt_to.in_host_list
|
||||||
|
# qmail-deliverable
|
||||||
|
# rcpt_to.routes
|
||||||
|
|
||||||
|
# DATA
|
||||||
|
# ----------
|
||||||
|
# attachment
|
||||||
|
# bounce
|
||||||
|
# clamd
|
||||||
|
# dkim
|
||||||
|
# headers
|
||||||
|
# limit
|
||||||
|
# rspamd
|
||||||
|
# spamassassin
|
||||||
|
# uribl
|
||||||
|
|
||||||
|
# QUEUE
|
||||||
|
# ----------
|
||||||
|
# queues: discard qmail-queue quarantine smtp_forward smtp_proxy
|
||||||
|
# Queue mail via smtp - see config/smtp_forward.ini for where your mail goes
|
||||||
|
queue/smtp_forward
|
||||||
|
queue/store_message
|
||||||
|
limit
|
||||||
72
src/email_server/config/smtp.ini
Normal file
72
src/email_server/config/smtp.ini
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
; address to listen on (default: all IPv6 and IPv4 addresses, port 25)
|
||||||
|
; use "[::0]:25" to listen on IPv6 and IPv4 (not all OSes)
|
||||||
|
listen=[::0]:2525
|
||||||
|
|
||||||
|
; Note you can listen on multiple IPs/ports using commas:
|
||||||
|
;listen=127.0.0.1:2529,127.0.0.2:2529,127.0.0.3:2530
|
||||||
|
|
||||||
|
; public IP address (default: none)
|
||||||
|
; If your machine is behind a NAT, some plugins (SPF, GeoIP) gain features
|
||||||
|
; if they know the servers public IP. If 'stun' is installed, Haraka will
|
||||||
|
; try to figure it out. If that doesn't work, set it here.
|
||||||
|
public_ip=0.0.0.0
|
||||||
|
outbound=false
|
||||||
|
|
||||||
|
|
||||||
|
; Time in seconds to let sockets be idle with no activity
|
||||||
|
;inactivity_timeout=300
|
||||||
|
|
||||||
|
; Drop privileges to this user/group
|
||||||
|
; user=smtp
|
||||||
|
; group=smtp
|
||||||
|
|
||||||
|
; Don't stop Haraka if plugins fail to compile
|
||||||
|
;ignore_bad_plugins=0
|
||||||
|
|
||||||
|
; Run using cluster to fork multiple backend processes
|
||||||
|
; Ref: https://github.com/haraka/Haraka/wiki/Performance-Tuning
|
||||||
|
;nodes=cpus
|
||||||
|
|
||||||
|
; Daemonize
|
||||||
|
;daemonize=true
|
||||||
|
;daemon_log_file=/var/log/haraka.log
|
||||||
|
;daemon_pid_file=/var/run/haraka.pid
|
||||||
|
|
||||||
|
; Spooling
|
||||||
|
; Save memory by spooling large messages to disk
|
||||||
|
;spool_dir=/var/spool/haraka
|
||||||
|
; Specify -1 to never spool to disk
|
||||||
|
; Specify 0 to always spool to disk
|
||||||
|
; Otherwise specify a size in bytes, once reached the
|
||||||
|
; message will be spooled to disk to save memory.
|
||||||
|
;spool_after=
|
||||||
|
|
||||||
|
; Force Shutdown Timeout
|
||||||
|
; - Haraka tries to close down gracefully, but if everything is shut down
|
||||||
|
; after this time it will hard close. 30s is usually long enough to
|
||||||
|
; wait for outbound connections to finish.
|
||||||
|
;force_shutdown_timeout=30
|
||||||
|
|
||||||
|
; SMTP service extensions: https://tools.ietf.org/html/rfc1869
|
||||||
|
; strict_rfc1869 = false
|
||||||
|
|
||||||
|
; Advertise support for SMTPUTF8 (RFC-6531)
|
||||||
|
;smtputf8=true
|
||||||
|
|
||||||
|
; Security & stability
|
||||||
|
daemonize=true
|
||||||
|
daemon_log_file=/var/log/haraka.log
|
||||||
|
daemon_pid_file=/var/run/haraka.pid
|
||||||
|
user=smtp
|
||||||
|
group=smtp
|
||||||
|
|
||||||
|
; Performance
|
||||||
|
nodes=cpus
|
||||||
|
spool_dir=/var/spool/haraka
|
||||||
|
spool_after=5242880 ; 5MB
|
||||||
|
|
||||||
|
; Headers
|
||||||
|
[headers]
|
||||||
|
add_received=true
|
||||||
|
max_lines=1000
|
||||||
|
max_received=100
|
||||||
3
src/email_server/config/smtp_forward.ini
Normal file
3
src/email_server/config/smtp_forward.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
host=127.0.0.1
|
||||||
|
port=2525
|
||||||
|
enable_outbound=false
|
||||||
398
src/email_server/docs/Plugins.md
Normal file
398
src/email_server/docs/Plugins.md
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
# Plugins
|
||||||
|
|
||||||
|
Most aspects of receiving an email in Haraka are controlled by plugins. Mail cannot even be received unless at least a 'rcpt' and 'queue' plugin are
|
||||||
|
enabled.
|
||||||
|
|
||||||
|
Recipient (*rcpt*) plugins determine if a particular recipient is allowed to be relayed or received for. A *queue* plugin queues the message somewhere - normally to disk or to an another SMTP server.
|
||||||
|
|
||||||
|
## Plugin Lists
|
||||||
|
|
||||||
|
Get a list of installed plugins by running `haraka -l`. To include locally installed plugins, add the `-c /path/to/config` option.
|
||||||
|
|
||||||
|
We also have a [registry of known plugins](https://github.com/haraka/Haraka/blob/master/Plugins.md).
|
||||||
|
|
||||||
|
Display the help text for a plugin by running:
|
||||||
|
|
||||||
|
`haraka -h <name> -c /path/to/config`
|
||||||
|
|
||||||
|
# Writing Haraka Plugins
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
|
||||||
|
## Anatomy of a Plugin
|
||||||
|
|
||||||
|
Plugins in Haraka are JS files in the `plugins` directory (legacy) and npm
|
||||||
|
modules in the node\_modules directory. See "Plugins as Modules" below.
|
||||||
|
|
||||||
|
Plugins can be installed in the Haraka global directory (default:
|
||||||
|
/$os/$specific/lib/node\_modules/Haraka) or in the Haraka install directory
|
||||||
|
(whatever you chose when you typed `haraka -i`. Example: `haraka -i /etc/haraka`
|
||||||
|
|
||||||
|
To enable a plugin, add its name to `config/plugins`. For npm packaged plugins, the name does not include the `haraka-plugin` prefix.
|
||||||
|
|
||||||
|
### Register
|
||||||
|
|
||||||
|
Register is the only plugin function that is syncronous and receives no arguments. Its primary purpose is enabling your plugin to register SMTP hooks. It is also used for syncronous initialization tasks such as [loading a config file](https://github.com/haraka/haraka-config). For heavier initialization tasks such as establishing database connections, look to `init_master` and `init_child` instead.
|
||||||
|
|
||||||
|
### Register a Hook
|
||||||
|
|
||||||
|
There are two ways for plugins to register hooks. Both examples register a function on the *rcpt* hook:
|
||||||
|
|
||||||
|
1. The `register_hook` function in register():
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.register = function () {
|
||||||
|
this.register_hook('rcpt', 'my_rcpt_validate')
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.my_rcpt_validate = function (next, connection, params) {
|
||||||
|
// do processing
|
||||||
|
next()
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The hook_[$name] syntax:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.hook_rcpt = function (next, connection, params) {
|
||||||
|
// do processing
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The register_hook function within `register()` offers a few advantages:
|
||||||
|
|
||||||
|
1. register a hook multiple times (see below)
|
||||||
|
2. a unique function name in stack traces
|
||||||
|
3. [a better function name](https://google.com/search?q=programming%20good%20function%20names)
|
||||||
|
4. hooks can be registered conditionally (ie, based on a config setting)
|
||||||
|
|
||||||
|
### Register a Hook Multiple Times
|
||||||
|
|
||||||
|
To register the same hook more than once, call `register_hook()` multiple times with the same hook name:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.register = function () {
|
||||||
|
this.register_hook('queue', 'try_queue_my_way')
|
||||||
|
this.register_hook('queue', 'try_queue_highway')
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
When `try_queue_my_way()` calls `next()`, the next function registered on hook *queue* will be called, in this case, `try_queue_highway()`.
|
||||||
|
|
||||||
|
#### Determine hook name
|
||||||
|
|
||||||
|
When a single function runs on multiple hooks, the function can check the
|
||||||
|
*hook* property of the *connection* or *hmail* argument to determine which hook it is running on:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.register = function () {
|
||||||
|
this.register_hook('rcpt', 'my_rcpt')
|
||||||
|
this.register_hook('rcpt_ok', 'my_rcpt')
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.my_rcpt = function (next, connection, params) {
|
||||||
|
const hook_name = connection.hook; // rcpt or rcpt_ok
|
||||||
|
// email address is in params[0]
|
||||||
|
// do processing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next()
|
||||||
|
|
||||||
|
After registering a hook, functions are called with that hooks arguments (see **Available Hooks** below. The first argument is a callback function, conventionally named `next`. When the function is completed, it calls `next()` and the connection continues. Failing to call `next()` will result in the connection hanging until that plugin's timer expires.
|
||||||
|
|
||||||
|
`next([code, msg])` accepts two optional parameters:
|
||||||
|
|
||||||
|
1. `code` is one of the listed return codes.
|
||||||
|
2. `msg` is a string to send to the client in case of a failure. Use an array to send a multi-line message. `msg` should NOT contain the code number - that is handled by Haraka.
|
||||||
|
|
||||||
|
#### Next() Return Codes
|
||||||
|
|
||||||
|
These constants are in your plugin when it is loaded, you do not
|
||||||
|
need to define them:
|
||||||
|
|
||||||
|
* CONT
|
||||||
|
|
||||||
|
Continue and let other plugins handle this particular hook. This is the
|
||||||
|
default. These are identical: `next()` and `next(CONT)`;
|
||||||
|
|
||||||
|
* DENY - Reject with a 5xx error.
|
||||||
|
|
||||||
|
* DENYSOFT - Reject with a 4xx error.
|
||||||
|
|
||||||
|
* DENYDISCONNECT - Reject with a 5xx error and immediately disconnect.
|
||||||
|
|
||||||
|
* DISCONNECT - Immediately disconnect
|
||||||
|
|
||||||
|
* OK
|
||||||
|
|
||||||
|
Required by `rcpt` plugins to accept a recipient and `queue` plugins when the queue was successful.
|
||||||
|
|
||||||
|
After a plugin calls `next(OK)`, no further plugins on that hook will run.
|
||||||
|
|
||||||
|
Exceptions to next(OK):
|
||||||
|
|
||||||
|
* connect_init and disconnect hooks are **always called**.
|
||||||
|
|
||||||
|
* On the deny hook, `next(OK)` overrides the default CONT.
|
||||||
|
|
||||||
|
* HOOK\_NEXT
|
||||||
|
|
||||||
|
HOOK_NEXT is only available on the `unrecognized_command` hook. It instructs Haraka to run a different plugin hook. The `msg` argument must be set to the name of the hook to be run. Ex: `next(HOOK_NEXT, 'rcpt_ok');`
|
||||||
|
|
||||||
|
## Available Hooks
|
||||||
|
|
||||||
|
These are the hook and their parameters (next excluded):
|
||||||
|
|
||||||
|
* init\_master - called when the main (master) process is started
|
||||||
|
* init\_child - in cluster, called when a child process is started
|
||||||
|
* init\_http - called when Haraka is started.
|
||||||
|
* init_wss - called after init_http
|
||||||
|
* connect\_init - used to init data structures, called for *every* connection
|
||||||
|
* lookup\_rdns - called to look up the rDNS - return the rDNS via `next(OK, rdns)`
|
||||||
|
* connect - called after we got rDNS
|
||||||
|
* capabilities - called to get the ESMTP capabilities (such as STARTTLS)
|
||||||
|
* unrecognized\_command - called when the remote end sends a command we don't recognise
|
||||||
|
* disconnect - called upon disconnect
|
||||||
|
* helo (hostname)
|
||||||
|
* ehlo (hostname)
|
||||||
|
* quit
|
||||||
|
* vrfy
|
||||||
|
* noop
|
||||||
|
* rset
|
||||||
|
* mail ([from, esmtp\_params])
|
||||||
|
* rcpt ([to, esmtp\_params])
|
||||||
|
* rcpt\_ok (to)
|
||||||
|
* data - called at the DATA command
|
||||||
|
* data\_post - called at the end-of-data marker
|
||||||
|
* max\_data\_exceeded - called when the message exceeds connection.max\_bytes
|
||||||
|
* queue - called to queue the mail
|
||||||
|
* queue\_outbound - called to queue the mail when connection.relaying is set
|
||||||
|
* queue\_ok - called when a mail has been queued successfully
|
||||||
|
* reset\_transaction - called before the transaction is reset (via RSET, or MAIL)
|
||||||
|
* deny - called when a plugin returns DENY, DENYSOFT or DENYDISCONNECT
|
||||||
|
* get\_mx (hmail, domain) - called by outbound to resolve the MX record
|
||||||
|
* deferred (hmail, params) - called when an outbound message is deferred
|
||||||
|
* bounce (hmail, err) - called when an outbound message bounces
|
||||||
|
* delivered (hmail, [host, ip, response, delay, port, mode, ok_recips, secured, authenticated]) - called when outbound mail is delivered
|
||||||
|
* send\_email (hmail) - called when outbound is about to be sent
|
||||||
|
* pre\_send\_trans\_email (fake_connection) - called just before an email is queued to disk with a faked connection object
|
||||||
|
|
||||||
|
### rcpt
|
||||||
|
|
||||||
|
The *rcpt* hook is slightly special.
|
||||||
|
|
||||||
|
When **connection.relaying == false** (the default, to avoid being an open relay), a rcpt plugin MUST return `next(OK)` or the sender will receive the error message "I cannot deliver for that user". The default *rcpt* plugin is **rcpt_to.in_host_list**, which lists the domains for which to accept email.
|
||||||
|
|
||||||
|
After a *rcpt* plugin calls `next(OK)`, the *rcpt_ok* hook is run.
|
||||||
|
|
||||||
|
If a plugin prior to the *rcpt* hook sets **connection.relaying = true**, then it is not necessary for a rcpt plugin to call `next(OK)`.
|
||||||
|
|
||||||
|
### connect_init
|
||||||
|
|
||||||
|
The `connect_init` hook is unique in that all return codes are ignored. This is so that plugins that need to do initialization for every connection can be assured they will run. Return values are ignored.
|
||||||
|
|
||||||
|
### hook_init_http (next, Server)
|
||||||
|
|
||||||
|
If http listeners are are enabled in http.ini and the express module loaded, the express library will be located at Server.http.express. More importantly, the express [app / instance](http://expressjs.com/4x/api.html#app) will be located at Server.http.app. Plugins can register routes on the app just as they would with any [Express.js](http://expressjs.com/) app.
|
||||||
|
|
||||||
|
### hook_init_wss (next, Server)
|
||||||
|
|
||||||
|
If express loaded, an attempt is made to load [ws](https://www.npmjs.com/package/ws), the websocket server. If it succeeds, the wss server will be located at Server.http.wss. Because of how websockets work, only one websocket plugin will work at a time. One plugin using wss is [watch](https://github.com/haraka/Haraka/tree/master/plugins/watch).
|
||||||
|
|
||||||
|
### pre\_send\_trans\_email (next, fake_connection)
|
||||||
|
|
||||||
|
The `fake` connection here is a holder for a new transaction object. It only has the log methods and a `transaction` property
|
||||||
|
so don't expect it to behave like a a real connection object. This hook is designed so you can add headers and modify mails
|
||||||
|
sent via `outbound.send_email()`, see the dkim_sign plugin for an example.
|
||||||
|
|
||||||
|
## Hook Order
|
||||||
|
|
||||||
|
The ordering of hooks is determined by the SMTP protocol. Knowledge of [RFC 5321](http://tools.ietf.org/html/rfc5321) is beneficial.
|
||||||
|
|
||||||
|
##### Typical Inbound Connection
|
||||||
|
|
||||||
|
- hook_connect_init
|
||||||
|
- hook_lookup_rdns
|
||||||
|
- hook_connect
|
||||||
|
- hook_helo **OR** hook_ehlo (EHLO is sent when ESMTP is desired which allows extensions
|
||||||
|
such as STARTTLS, AUTH, SIZE etc.)
|
||||||
|
- hook_helo
|
||||||
|
- hook_ehlo
|
||||||
|
- hook_capabilities
|
||||||
|
- *hook_unrecognized_command* is run for each ESMTP extension the client requests
|
||||||
|
e.g. STARTTLS, AUTH etc.)
|
||||||
|
- hook_mail
|
||||||
|
- hook_rcpt (once per-recipient)
|
||||||
|
- hook_rcpt_ok (for every recipient that hook_rcpt returned `next(OK)` for)
|
||||||
|
- hook_data
|
||||||
|
- *[attachment hooks]*
|
||||||
|
- hook_data_post
|
||||||
|
- hook_queue **OR** hook_queue_outbound
|
||||||
|
- hook_queue_ok (called if hook_queue or hook_queue_outbound returns `next(OK)`)
|
||||||
|
- hook_quit **OR** hook_rset **OR** hook_helo **OR** hook_ehlo (after a message has been sent or rejected, the client can disconnect or start a new transaction with RSET, EHLO or HELO)
|
||||||
|
- hook_reset_transaction
|
||||||
|
- hook_disconnect
|
||||||
|
|
||||||
|
##### Typical Outbound mail
|
||||||
|
|
||||||
|
By 'outbound' we mean messages using Haraka's built-in queue and delivery
|
||||||
|
mechanism. The Outbound queue is used when `connection.relaying = true` is set during the transaction and `hook_queue_outbound` is called to queue the message.
|
||||||
|
|
||||||
|
The Outbound hook ordering mirrors the Inbound hook order above until after `hook_queue_outbound`, which is followed by:
|
||||||
|
|
||||||
|
- hook_send_email
|
||||||
|
- hook_get_mx
|
||||||
|
- at least one of:
|
||||||
|
- hook_delivered (once per delivery domain with at least one successful recipient)
|
||||||
|
- hook_deferred (once per delivery domain where at least one recipient or connection was deferred)
|
||||||
|
- hook_bounce (once per delivery domain where the recipient(s) or message was rejected by the destination)
|
||||||
|
|
||||||
|
## Plugin Run Order
|
||||||
|
|
||||||
|
Plugins are run on each hook in the order that they are specified in `config/plugins`. When a plugin returns anything other than `next()` on a hook, all subsequent plugins due to run on that hook are skipped (exceptions: connect_init, disconnect).
|
||||||
|
|
||||||
|
This is important as some plugins might rely on `results` or `notes` that have been set by plugins that need to run before them. This should be noted in the plugins documentation. Make sure to read it.
|
||||||
|
|
||||||
|
If you are writing a complex plugin, you may have to split it into multiple plugins to run in a specific order e.g. you want hook_deny to run last after all other plugins and hook_lookup_rdns to run first, then you can explicitly register your hooks and provide a `priority` value which is an integer between -100 (highest priority) to 100 (lowest priority) which defaults to 0 (zero) if not supplied. You can apply a priority to your hook in the following way:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.register = function () {
|
||||||
|
this.register_hook('connect', 'do_connect_stuff', -100);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would ensure that your `do_connect_stuff` function will run before any other
|
||||||
|
plugins registered on the `connect` hook, regardless of the order it was
|
||||||
|
specified in `config/plugins`.
|
||||||
|
|
||||||
|
Check the order that the plugins will run on each hook by running:
|
||||||
|
|
||||||
|
`haraka -o -c /path/to/config`
|
||||||
|
|
||||||
|
## Skipping Plugins
|
||||||
|
|
||||||
|
Plugins can be skipped at runtime by pushing the name of the plugin into the `skip_plugins` array in `transaction.notes`. This array is reset for every transaction and once a plugin is added to the list, it will not run any hooks in that plugin for the remainder of the transaction. For example, one could create a whitelist plugin that skipped `spamassassin` if the sender was in a whitelist.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Plugins inherit all the logging methods of `logger.js`, which are:
|
||||||
|
|
||||||
|
* logprotocol
|
||||||
|
* logdebug
|
||||||
|
* loginfo
|
||||||
|
* lognotice
|
||||||
|
* logwarn
|
||||||
|
* logerror
|
||||||
|
* logcrit
|
||||||
|
* logalert
|
||||||
|
* logemerg
|
||||||
|
|
||||||
|
If plugins throw an exception when in a hook, the exception will be caught
|
||||||
|
and generate a logcrit level error. However, exceptions will not be caught
|
||||||
|
as gracefully when plugins are running async code. Use error codes for that,
|
||||||
|
log the error, and run your next() function appropriately.
|
||||||
|
|
||||||
|
## Sharing State
|
||||||
|
|
||||||
|
There are several cases where you might need to share information between
|
||||||
|
plugins. This is done using `notes` - there are three types available:
|
||||||
|
|
||||||
|
* server.notes
|
||||||
|
|
||||||
|
Available in all plugins. This is created at PID start-up and is shared
|
||||||
|
amongst all plugins on the same PID and listener.
|
||||||
|
Typical uses for notes at this level would be to share database
|
||||||
|
connections between multiple plugins or connection pools etc.
|
||||||
|
|
||||||
|
* connection.notes
|
||||||
|
|
||||||
|
Available on any hook that passes 'connection' as a function parameter.
|
||||||
|
This is shared amongst all plugins for a single connection and is
|
||||||
|
destroyed after the client disconnects.
|
||||||
|
Typical uses for notes at this level would be to store information
|
||||||
|
about the connected client e.g. rDNS names, HELO/EHLO, white/black
|
||||||
|
list status etc.
|
||||||
|
|
||||||
|
* connection.transaction.notes
|
||||||
|
|
||||||
|
Available on any hook that passes 'connection' as a function parameter
|
||||||
|
between hook\_mail and hook\_data\_post.
|
||||||
|
This is shared amongst all plugins for this transaction (e.g. MAIL FROM
|
||||||
|
through until a message is received or the connection is reset).
|
||||||
|
Typical uses for notes at this level would be to store information
|
||||||
|
on things like greylisting which uses client, sender and recipient
|
||||||
|
information etc.
|
||||||
|
|
||||||
|
* hmail.todo.notes
|
||||||
|
|
||||||
|
Available on any outbound hook that passes `hmail` as a function parameter.
|
||||||
|
This is the same object as 'connection.transaction.notes', so anything
|
||||||
|
you store in the transaction notes is automatically available in the
|
||||||
|
outbound functions here.
|
||||||
|
|
||||||
|
All of these notes are JS objects - use them as simple key/value store e.g.
|
||||||
|
|
||||||
|
connection.transaction.notes.test = 'testing';
|
||||||
|
|
||||||
|
## Plugins as Modules
|
||||||
|
|
||||||
|
Plugins as NPM modules are named with the `haraka-plugin` prefix. Therefore, a
|
||||||
|
plugin that frobnobricates might be called `haraka-plugin-frobnobricate` and
|
||||||
|
published to NPM with that name. The prefix is not required in the
|
||||||
|
`config/plugins` file.
|
||||||
|
|
||||||
|
Plugins loaded as NPM modules behave slightly different than plugins loaded
|
||||||
|
as plain JS files.
|
||||||
|
|
||||||
|
Plain JS plugins have a custom `require()` which allows loading core Haraka
|
||||||
|
modules via specifying `require('./name')` (note the `./` prefix). Although
|
||||||
|
the core modules aren't in the same folder, the custom `require` intercepts
|
||||||
|
this and look for core modules. Note that if there is a module in your plugins
|
||||||
|
folder of the same name that will not take preference, so avoid using names
|
||||||
|
similar to core modules.
|
||||||
|
|
||||||
|
Plugins loaded as modules do not have the special `require()`. To load
|
||||||
|
a core Haraka module you must use `this.haraka_require('name')`.
|
||||||
|
This should also be preferred for plain JS plugins, as the
|
||||||
|
`./` hack is likely to be removed in the future.
|
||||||
|
|
||||||
|
Plugins loaded as modules are not compiled in the Haraka plugin sandbox,
|
||||||
|
which blocks access to certain globals and provides a global `server` object.
|
||||||
|
To access the `server` object, use `connection.server` instead.
|
||||||
|
|
||||||
|
Module plugins support default config in their local `config` directory. See the
|
||||||
|
"Default Config and Overrides" section in [Config](Config.md).
|
||||||
|
|
||||||
|
## Shutdown
|
||||||
|
|
||||||
|
On graceful reload, Haraka will call a plugin's `shutdown` method.
|
||||||
|
|
||||||
|
This is so you can clear any timers or intervals, or shut down any connections
|
||||||
|
to remote servers. See [Issue 2024](https://github.com/haraka/Haraka/issues/2024).
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.shutdown = function () {
|
||||||
|
clearInterval(this._interval);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't implement this in your plugin and have a connection open or a
|
||||||
|
timer running then Haraka will take 30 seconds to shut down and have to
|
||||||
|
forcibly kill your process.
|
||||||
|
|
||||||
|
Note: This only applies when running with a `nodes=...` value in smtp.ini.
|
||||||
|
|
||||||
|
## See also, [Results](Results.md)
|
||||||
|
|
||||||
|
|
||||||
|
Further Reading
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Read about the [Connection](Connection.md) object.
|
||||||
|
|
||||||
|
Outbound hooks are [also documented](Outbound.md).
|
||||||
8
src/email_server/package.json
Normal file
8
src/email_server/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "haraka_local",
|
||||||
|
"description": "An SMTP Server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {},
|
||||||
|
"repository": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
1
src/email_server/plugins/queue/store_message.js
Symbolic link
1
src/email_server/plugins/queue/store_message.js
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/root/temp_mail/src/haraka-plugins/queue/store_message.js
|
||||||
76
src/email_server/services/MessageService.js
Normal file
76
src/email_server/services/MessageService.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { Message } = require("../../db/models/Message");
|
||||||
|
const { Domain } = require("../../db/models/Domain");
|
||||||
|
const { TempEmail } = require("../../db/models/TempEmail");
|
||||||
|
const config = require("../../config/haraka");
|
||||||
|
|
||||||
|
class MessageService {
|
||||||
|
static async isValidDomain(email) {
|
||||||
|
const domain = email.split("@")[1];
|
||||||
|
const tempDomain = await Domain.query()
|
||||||
|
.where("name", domain)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return !tempDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async isValidEmail(email) {
|
||||||
|
const tempEmail = await TempEmail.query()
|
||||||
|
.where("email", email)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return !tempEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async store(messageData) {
|
||||||
|
const tempEmail = await TempEmail.query()
|
||||||
|
.where("email", messageData.to)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!tempEmail) {
|
||||||
|
throw new Error("Temporary email not authorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isToValid = await Promise.all(
|
||||||
|
messageData.to.split(",").map((email) => this.isValidEmail(email.trim()))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isToValid.includes(false)) {
|
||||||
|
throw new Error("Receiving to temporary email addresses is not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Message.query().insert({
|
||||||
|
...messageData,
|
||||||
|
temp_email_id: tempEmail.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async cleanup() {
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - config.storage.retention);
|
||||||
|
|
||||||
|
return Message.query().where("created_at", "<", cutoffDate).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async search(params) {
|
||||||
|
let query = Message.query()
|
||||||
|
.joinRelated("temp_email")
|
||||||
|
.select("messages.*", "temp_email.email as temp_email");
|
||||||
|
|
||||||
|
if (params.temp_email) {
|
||||||
|
query = query.where("temp_email.email", params.temp_email);
|
||||||
|
}
|
||||||
|
if (params.from) {
|
||||||
|
query = query.where("from", "like", `%${params.from}%`);
|
||||||
|
}
|
||||||
|
if (params.to) {
|
||||||
|
query = query.where("to", "like", `%${params.to}%`);
|
||||||
|
}
|
||||||
|
if (params.subject) {
|
||||||
|
query = query.where("subject", "like", `%${params.subject}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.orderBy("received_at", "desc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MessageService;
|
||||||
28
src/haraka-plugins/queue/store_message.js
Normal file
28
src/haraka-plugins/queue/store_message.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
exports.register = function () {
|
||||||
|
const plugin = this;
|
||||||
|
const MessageService = require("../../services/MessageService");
|
||||||
|
|
||||||
|
plugin.store_message = async function (next, connection) {
|
||||||
|
const transaction = connection.transaction;
|
||||||
|
if (!transaction) return next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageData = {
|
||||||
|
from: transaction.mail_from.address(),
|
||||||
|
to: transaction.rcpt_to.map((addr) => addr.address()).join(", "),
|
||||||
|
subject: transaction.header.get("subject"),
|
||||||
|
body: transaction.body.bodytext,
|
||||||
|
headers: transaction.header.headers_decoded,
|
||||||
|
domain: transaction.rcpt_to[0].host,
|
||||||
|
};
|
||||||
|
|
||||||
|
await MessageService.store(messageData);
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
connection.logerror(plugin, `Failed to store message: ${error.message}`);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin.register_hook("queue", "store_message");
|
||||||
|
};
|
||||||
28
src/middleware/auth.js
Normal file
28
src/middleware/auth.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const config = require('../config/haraka');
|
||||||
|
|
||||||
|
function authenticateToken(req, res, next) {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, config.auth.jwtSecret, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(403).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAdmin(req, res, next) {
|
||||||
|
if (!req.user.is_admin) {
|
||||||
|
return res.status(403).json({ error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { authenticateToken, requireAdmin };
|
||||||
30
src/routes/auth.js
Normal file
30
src/routes/auth.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { User } = require('../db/models/User');
|
||||||
|
const config = require('../config/haraka');
|
||||||
|
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
const user = await User.query().where('email', email).first();
|
||||||
|
|
||||||
|
if (!user || !(await user.verifyPassword(password))) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user.id, email: user.email, is_admin: user.is_admin },
|
||||||
|
config.auth.jwtSecret,
|
||||||
|
{ expiresIn: config.auth.tokenExpiry }
|
||||||
|
);
|
||||||
|
|
||||||
|
await User.query().where('id', user.id).update({ api_key: token });
|
||||||
|
|
||||||
|
res.json({ token });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Loading…
x
Reference in New Issue
Block a user