forked from smfreegard/Haraka
-
Notifications
You must be signed in to change notification settings - Fork 6
/
data.headers.js
351 lines (297 loc) · 12.4 KB
/
data.headers.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
// validate message headers and some fields
var net_utils = require('./net_utils');
exports.register = function () {
var plugin = this;
var load_config = function () {
plugin.cfg = plugin.config.get('data.headers.ini', {
booleans: [
'+check.duplicate_singular',
'+check.missing_required',
'+check.invalid_return_path',
'+check.invalid_date',
'+check.user_agent',
'+check.direct_to_mx',
'+check.from_match',
'+check.mailing_list',
'-reject.duplicate_singular',
'-reject.missing_required',
'-reject.invalid_return_path',
'-reject.invalid_date',
],
}, load_config);
};
load_config();
try {
plugin.addrparser = require('address-rfc2822');
}
catch (e) {
plugin.logerror("unable to load address-rfc2822, try\n\n\t'npm install -g address-rfc2822'\n\n");
}
this.register_hook('data_post', 'duplicate_singular');
this.register_hook('data_post', 'missing_required');
this.register_hook('data_post', 'invalid_date');
this.register_hook('data_post', 'invalid_return_path');
this.register_hook('data_post', 'user_agent');
this.register_hook('data_post', 'direct_to_mx');
if (plugin.addrparser) {
this.register_hook('data_post', 'from_match');
}
this.register_hook('data_post', 'mailing_list');
};
exports.duplicate_singular = function(next, connection) {
var plugin = this;
if (!plugin.cfg.check.duplicate_singular) { return next(); }
// RFC 5322 Section 3.6, Headers that MUST be unique if present
var singular = plugin.cfg.main.singular !== undefined ?
plugin.cfg.main.singular.split(',') :
['Date', 'From', 'Sender', 'Reply-To', 'To', 'Cc',
'Bcc', 'Message-Id', 'In-Reply-To', 'References',
'Subject'];
var failures = [];
for (var i=0, l=singular.length; i < l; i++) {
if (connection.transaction.header.get_all(singular[i]).length > 1) {
var name = singular[i];
connection.transaction.results.add(plugin, {fail: 'duplicate:'+name});
failures.push(name);
}
}
if (failures.length) {
if (plugin.cfg.reject.duplicate_singular) {
return next(DENY, "Only one " + failures[0] +
" header allowed. See RFC 5322, Section 3.6");
}
return next();
}
connection.transaction.results.add(plugin, {pass: 'duplicate'});
return next();
};
exports.missing_required = function(next, connection) {
var plugin = this;
if (!plugin.cfg.check.missing_required) return next();
// Enforce RFC 5322 Section 3.6, Headers that MUST be present
var required = plugin.cfg.main.required !== undefined ?
plugin.cfg.main.required.split(',') :
['Date', 'From'];
var failures = [];
for (var i=0; i < required.length; i++) {
var h = required[i];
if (connection.transaction.header.get_all(h).length === 0) {
connection.transaction.results.add(plugin, {fail: 'missing:'+h});
failures.push(h);
}
}
if (failures.length) {
if (plugin.cfg.reject.missing_required) {
return next(DENY, "Required header '" + failures[0] + "' missing");
}
return next();
}
connection.transaction.results.add(plugin, {pass: 'missing'});
return next();
};
exports.invalid_return_path = function(next, connection) {
var plugin = this;
if (!plugin.cfg.check.invalid_return_path) return next();
// Tests for Return-Path headers that shouldn't be present
// RFC 5321#section-4.4 Trace Information
// A message-originating SMTP system SHOULD NOT send a message that
// already contains a Return-path header field.
// Return-Path, aka Reverse-PATH, Envelope FROM, RFC5321.MailFrom
var rp = connection.transaction.header.get('Return-Path');
if (rp) {
if (connection.relaying) { // On messages we originate
connection.transaction.results.add(plugin, {fail: 'Return-Path', emit: true});
if (plugin.cfg.reject.invalid_return_path) {
return next(DENY, "outgoing mail must not have a Return-Path header (RFC 5321)");
}
return next();
}
else {
// generally, messages from the internet shouldn't have a
// Return-Path, except for when they can. Read RFC 5321, it's
// complicated. In most cases, The Right Thing to do here is to
// strip the Return-Path header.
connection.transaction.remove_header('Return-Path');
// unless it was added by Haraka. Which at present, doesn't.
}
}
connection.transaction.results.add(plugin, {pass: 'Return-Path'});
return next();
};
exports.invalid_date = function (next, connection) {
var plugin = this;
if (!plugin.cfg.check.invalid_date) return next();
// Assure Date header value is [somewhat] sane
var msg_date = connection.transaction.header.get_all('Date');
if (!msg_date || msg_date.length === 0) return next();
connection.logdebug(plugin, "message date: " + msg_date);
msg_date = Date.parse(msg_date);
var date_future_days = plugin.cfg.main.date_future_days !== undefined ?
plugin.cfg.main.date_future_days :
2;
if (date_future_days > 0) {
var too_future = new Date;
too_future.setHours(too_future.getHours() + 24 * date_future_days);
// connection.logdebug(plugin, "too future: " + too_future);
if (msg_date > too_future) {
connection.transaction.results.add(plugin, {fail: 'invalid_date(future)'});
if (plugin.cfg.reject.invalid_date) {
return next(DENY, "The Date header is too far in the future");
}
return next();
}
}
var date_past_days = plugin.cfg.main.date_past_days !== undefined ?
plugin.cfg.main.date_past_days :
15;
if (date_past_days > 0) {
var too_old = new Date;
too_old.setHours(too_old.getHours() - 24 * date_past_days);
// connection.logdebug(plugin, "too old: " + too_old);
if (msg_date < too_old) {
connection.loginfo(plugin, "date is older than: " + too_old);
connection.transaction.results.add(plugin, {fail: 'invalid_date(past)'});
if (plugin.cfg.reject.invalid_date) {
return next(DENY, "The Date header is too old");
}
return next();
}
}
connection.transaction.results.add(plugin, {pass: 'invalid_date'});
return next();
};
exports.user_agent = function (next, connection) {
var plugin = this;
if (!plugin.cfg.check.user_agent) return next();
if (!connection.transaction) return next();
var h = connection.transaction.header;
var found_ua = 0;
// User-Agent: Thunderbird, Squirrelmail, Roundcube, Mutt, MacOutlook, Kmail, IMP
// X-Mailer: Apple Mail, swaks, Outlook (12-14), Yahoo Webmail, Cold Fusion, Zimbra, Evolution
// Check for User-Agent
var headers = ['user-agent','x-mailer','x-mua'];
for (var i=0; i < headers.length; i++) {
var name = headers[i];
var header = connection.transaction.header.get(name);
if (!header) continue; // header not present
found_ua++;
connection.transaction.results.add(plugin, {pass: 'UA('+header.substring(0,12)+')'});
}
if (found_ua) return next();
connection.transaction.results.add(plugin, {fail: 'UA'});
return next();
};
exports.direct_to_mx = function (next, connection) {
var plugin = this;
if (!plugin.cfg.check.direct_to_mx) return next();
if (!connection.transaction) return next();
// Legit messages normally have at least 2 hops (Received headers)
// MUA -> sending MTA -> Receiving MTA (Haraka?)
if (connection.notes.auth_user) {
// User authenticated, so we're likely the first MTA
connection.transaction.results.add(plugin, {skip: 'direct-to-mx(auth)'});
return next();
}
// TODO: what about connection.relaying? (...collecting data...)
var received = connection.transaction.header.get_all('received');
if (!received) {
connection.transaction.results.add(plugin, {fail: 'direct-to-mx(none)'});
return next();
}
var c = received.length;
if (c < 2) {
connection.transaction.results.add(plugin, {fail: 'direct-to-mx(too few Received('+c+'))'});
return next();
}
connection.transaction.results.add(plugin, {pass: 'direct-to-mx('+c+')'});
return next();
};
exports.from_match = function (next, connection) {
var plugin = this;
if (!plugin.cfg.check.from_match) return next();
// see if the header From matches the envelope FROM. There are valid
// cases to not match (~10% of ham) but a not matching is much more
// likely to be spam than ham. This test is useful for heuristics.
if (!connection.transaction) return next();
var env_addr = connection.transaction.mail_from;
var hdr_from = connection.transaction.header.get('From');
if (!hdr_from) {
connection.transaction.results.add(plugin, {fail: 'from_match(missing)'});
return next();
}
var hdr_addr = (plugin.addrparser.parse(hdr_from))[0];
if (env_addr.address().toLowerCase() == hdr_addr.address.toLowerCase()) {
connection.transaction.results.add(plugin, {pass: 'from_match'});
return next();
}
var env_dom = net_utils.get_organizational_domain(env_addr.host);
var msg_dom = net_utils.get_organizational_domain(hdr_addr.host());
if (env_dom && msg_dom && env_dom.toLowerCase() === msg_dom.toLowerCase()) {
connection.transaction.results.add(plugin, {pass: 'from_match(domain)'});
return next();
}
connection.transaction.results.add(plugin, {emit: true,
fail: 'from_match(' + env_dom + ' / ' + msg_dom + ')'
});
return next();
};
exports.mailing_list = function (next, connection) {
var plugin = this;
if (!plugin.cfg.check.mailing_list) return next();
if (!connection.transaction) return next();
var h = connection.transaction.header;
var found_mlm = 0;
var mlms = {
'Mailing-List' : [
{ mlm: 'ezmlm', match: 'ezmlm' },
{ mlm: 'yahoogroups', match: 'yahoogroups' },
],
'Sender' : [
{ mlm: 'majordomo', start: 'owner-' },
],
'X-Mailman-Version' : [ { mlm: 'mailman' }, ],
'X-Majordomo-Version': [ { mlm: 'majordomo' }, ],
'X-Google-Loop' : [ { mlm: 'googlegroups' } ],
};
for (var name in mlms) {
var header = connection.transaction.header.get(name);
if (!header) continue; // header not present
for (var i=0; i < mlms[name].length; i++) {
var j = mlms[name][i];
if (j.start) {
if (header.substring(0,j.start.length) === j.start) {
connection.transaction.results.add(plugin, {pass: 'MLM('+j.mlm+')'});
found_mlm++;
continue;
}
connection.logerror(plugin, "mlm start miss: " + name + ': ' + header);
}
if (j.match) {
if (header.match(new RegExp(j.match,'i'))) {
connection.transaction.results.add(plugin, {pass: 'MLM('+j.mlm+')'});
found_mlm++;
continue;
}
connection.logerror(plugin, "mlm match miss: " + name + ': ' + header);
}
if (name === 'X-Mailman-Version') {
connection.transaction.results.add(plugin, {pass: 'MLM('+j.mlm+')'});
found_mlm++;
continue;
}
if (name === 'X-Majordomo-Version') {
connection.transaction.results.add(plugin, {pass: 'MLM('+j.mlm+')'});
found_mlm++;
continue;
}
if (name === 'X-Google-Loop') {
connection.transaction.results.add(plugin, {pass: 'MLM('+j.mlm+')'});
found_mlm++;
continue;
}
}
}
if (found_mlm) return next();
connection.transaction.results.add(plugin, {fail: 'MLM'});
return next();
};