/
quarantine.js
135 lines (111 loc) · 4.52 KB
/
quarantine.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// quarantine
const fs = require('node:fs');
const path = require('node:path');
exports.register = function () {
this.load_quarantine_ini();
this.register_hook('queue', 'quarantine');
this.register_hook('queue_outbound', 'quarantine');
}
exports.hook_init_master = function (next, server) {
this.init_quarantine_dir(() => {
this.clean_tmp_directory(next);
});
}
exports.load_quarantine_ini = function () {
this.cfg = this.config.get('quarantine.ini', () => {
this.load_quarantine_ini();
})
}
const zeroPad = exports.zeroPad = (n, digits) => {
n = n.toString();
while (n.length < digits) {
n = `0${n}`;
}
return n;
}
exports.clean_tmp_directory = function (next) {
const tmp_dir = path.join(this.get_base_dir(), 'tmp');
if (fs.existsSync(tmp_dir)) {
const dirent = fs.readdirSync(tmp_dir);
this.loginfo(`Removing temporary files from: ${tmp_dir}`);
for (const element of dirent) {
fs.unlinkSync(path.join(tmp_dir, element));
}
}
next();
}
function wants_quarantine (connection) {
const { notes, transaction } = connection ?? {}
if (notes.quarantine) return notes.quarantine;
if (transaction.notes.quarantine) return transaction.notes.quarantine;
return transaction.notes.get('queue.wants') === 'quarantine';
}
exports.get_base_dir = function () {
if (this.cfg.main.quarantine_path) return this.cfg.main.quarantine_path;
return '/var/spool/haraka/quarantine';
}
exports.init_quarantine_dir = function (done) {
const tmp_dir = path.join(this.get_base_dir(), 'tmp');
fs.promises.mkdir(tmp_dir, { recursive: true })
.then(made => this.loginfo(`created ${tmp_dir}`))
.catch(err => this.logerror(`Unable to create ${tmp_dir}`))
.finally(done);
}
exports.quarantine = function (next, connection) {
const quarantine = wants_quarantine(connection);
this.logdebug(`quarantine: ${quarantine}`);
if (!quarantine) return next();
// Calculate date in YYYYMMDD format
const d = new Date();
const yyyymmdd = d.getFullYear() + zeroPad(d.getMonth()+1, 2)
+ this.zeroPad(d.getDate(), 2);
let subdir = yyyymmdd;
// Allow either boolean or a sub-directory to be specified
if (typeof quarantine !== 'boolean' && quarantine !== 1) {
subdir = path.join(quarantine, yyyymmdd);
}
const txn = connection?.transaction;
if (!txn) return next();
const base_dir = this.get_base_dir();
const msg_dir = path.join(base_dir, subdir);
const tmp_path = path.join(base_dir, 'tmp', txn.uuid);
const msg_path = path.join(msg_dir, txn.uuid);
// Create all the directories recursively if they do not exist.
// Then write the file to a temporary directory first, once this is
// successful we hardlink the file to the final destination and then
// remove the temporary file to guarantee a complete file in the
// final destination.
fs.promises.mkdir(msg_dir, { recursive: true })
.catch(err => {
connection.logerror(this, `Error creating directory: ${msg_dir}`);
next();
})
.then(ok => {
const ws = fs.createWriteStream(tmp_path);
ws.on('error', err => {
connection.logerror(this, `Error writing quarantine file: ${err.message}`);
return next();
});
ws.on('close', () => {
fs.link(tmp_path, msg_path, err => {
if (err) {
connection.logerror(this, `Error writing quarantine file: ${err}`);
}
else {
// Add a note to where we stored the message
txn.notes.quarantined = msg_path;
txn.results.add(this, { pass: msg_path, emit: true });
// Now delete the temporary file
fs.unlink(tmp_path, () => {});
}
// Using notes.quarantine_action to decide what to do after the message is quarantined.
// Format can be either action = [ code, msg ] or action = code
const action = (connection.notes.quarantine_action || txn.notes.quarantine_action);
if (!action) return next();
if (Array.isArray(action)) return next(action[0], action[1]);
return next(action);
});
});
txn.message_stream.pipe(ws, { line_endings: '\n' });
});
}