Skip to content

Commit 57aab38

Browse files
committed
Merge branch 'master' of https://github.com/haraka/Haraka
2 parents 0cad45d + a776ddf commit 57aab38

File tree

6 files changed

+193
-106
lines changed

6 files changed

+193
-106
lines changed

connection.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1344,7 +1344,7 @@ Connection.prototype.received_line = function() {
13441344
' cipher=' + this.notes.tls.cipher.name +
13451345
' verify=' + ((this.notes.tls.authorized) ? 'OK' :
13461346
((this.notes.tls.authorizationError &&
1347-
this.notes.tls.authorizationError.message === 'UNABLE_TO_GET_ISSUER_CERT') ? 'NO' : 'FAIL')) + ')';
1347+
this.notes.tls.authorizationError.code === 'UNABLE_TO_GET_ISSUER_CERT') ? 'NO' : 'FAIL')) + ')';
13481348
}
13491349
return [
13501350
'from ',

docs/plugins/bounce.md

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ for mail servers at domains with frequent spoofing and few or no human users.
3838
Valid bounces have a single recipient. Assure that the message really is a
3939
bounce by enforcing bounces to be addressed to a single recipient.
4040

41+
This check is skipped for relays or hosts with a private IP, this is because
42+
Microsoft Exchange distribution lists will send messages to list members with
43+
a null return-path when the 'Do not send delivery reports' option is enabled
44+
(yes, really...).
45+
4146
### empty\_return\_path
4247

4348
Valid bounces should have an empty return path. Test for the presence of the

mailheader.js

+10
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ Header.prototype._add_header_decode = function (key, value, method) {
231231
Header.prototype.add = function (key, value) {
232232
if (!key) key = 'X-Haraka-Blank';
233233
value = value.replace(/(\r?\n)*$/, '');
234+
if (/[^\x00-\x7f]/.test(value)) {
235+
// Need to QP encode this header value and assume UTF-8
236+
value = '=?UTF-8?q?' + utils.encode_qp(value) + '?=';
237+
value = value.replace(/\n/g, '\n '); // turn wraps into continuations
238+
}
234239
this._add_header(key.toLowerCase(), value, "unshift");
235240
this._add_header_decode(key.toLowerCase(), value, "unshift");
236241
this.header_list.unshift(key + ': ' + value + '\n');
@@ -239,6 +244,11 @@ Header.prototype.add = function (key, value) {
239244
Header.prototype.add_end = function (key, value) {
240245
if (!key) key = 'X-Haraka-Blank';
241246
value = value.replace(/(\r?\n)*$/, '');
247+
if (/[^\x00-\x7f]/.test(value)) {
248+
// Need to QP encode this header value and assume UTF-8
249+
value = '=?UTF-8?q?' + utils.encode_qp(value) + '?=';
250+
value = value.replace(/\n/g, ' \n'); // turn wraps into continuations
251+
}
242252
this._add_header(key.toLowerCase(), value, "push");
243253
this._add_header_decode(key.toLowerCase(), value, "push");
244254
this.header_list.push(key + ': ' + value + '\n');

plugins/auth/auth_proxy.js

+106-105
Original file line numberDiff line numberDiff line change
@@ -88,127 +88,128 @@ exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
8888
response = [];
8989
};
9090
socket.on('line', function (line) {
91-
var matches;
9291
connection.logprotocol(self, "S: " + line);
93-
if (matches = smtp_regexp.exec(line)) {
94-
var code = matches[1];
95-
var cont = matches[2];
96-
var rest = matches[3];
97-
response.push(rest);
98-
if (cont === ' ') {
99-
connection.logdebug(self, 'command state: ' + command);
100-
if (command === 'ehlo') {
101-
if (code.match(/^5/)) {
102-
// EHLO command rejected; we have to abort
103-
socket.send_command('QUIT');
92+
var matches = smtp_regexp.exec(line);
93+
if (!matches) return;
94+
95+
var code = matches[1];
96+
var cont = matches[2];
97+
var rest = matches[3];
98+
response.push(rest);
99+
100+
if (cont !== ' ') {
101+
// Unrecognised response.
102+
connection.logerror(self, "unrecognised response: " + line);
103+
socket.end();
104+
return;
105+
}
106+
107+
connection.logdebug(self, 'command state: ' + command);
108+
if (command === 'ehlo') {
109+
if (code[0] === '5') {
110+
// EHLO command rejected; abort
111+
socket.send_command('QUIT');
112+
return;
113+
}
114+
// Parse CAPABILITIES
115+
var i;
116+
for (i in response) {
117+
if (/^STARTTLS/.test(response[i])) {
118+
if (secure) continue; // silly remote, we've already upgraded
119+
var key = self.config.get('tls_key.pem', 'binary');
120+
var cert = self.config.get('tls_cert.pem', 'binary');
121+
// Use TLS opportunistically if we found the key and certificate
122+
if (key && cert) {
123+
this.on('secure', function () {
124+
secure = true;
125+
socket.send_command('EHLO', self.config.get('me'));
126+
});
127+
socket.send_command('STARTTLS');
104128
return;
105129
}
106-
// Parse CAPABILITIES
107-
var i;
108-
for (i in response) {
109-
if (!secure && response[i].match(/^STARTTLS/)) {
110-
var key = self.config.get('tls_key.pem', 'binary');
111-
var cert = self.config.get('tls_cert.pem', 'binary');
112-
// Use TLS opportunistically if we found the key and certificate
113-
if (key && cert) {
114-
this.on('secure', function () {
115-
secure = true;
116-
socket.send_command('EHLO', self.config.get('me'));
117-
});
118-
socket.send_command('STARTTLS');
119-
return;
120-
}
121-
}
122-
else if (response[i].match(/^AUTH /)) {
123-
// Parse supported AUTH methods
124-
var parse = /^AUTH (.+)$/.exec(response[i]);
125-
methods = parse[1].split(/\s+/);
126-
connection.logdebug(self, 'found supported AUTH methods: ' + methods);
127-
// Prefer PLAIN as it's easiest
128-
if (methods.indexOf('PLAIN') !== -1) {
129-
socket.send_command('AUTH','PLAIN ' + utils.base64("\0" + user + "\0" + passwd));
130-
return;
131-
}
132-
else if (methods.indexOf('LOGIN') !== -1) {
133-
socket.send_command('AUTH','LOGIN');
134-
return;
135-
}
136-
else {
137-
// No compatible methods; abort...
138-
connection.logdebug(self, 'no compatible AUTH methods');
139-
socket.send_command('QUIT');
140-
return;
141-
}
142-
}
143-
}
144130
}
145-
if (command === 'auth') {
146-
// Handle LOGIN
147-
if (code[0] === '3' && response[0] === 'VXNlcm5hbWU6') {
148-
// Write to the socket directly to keep the state at 'auth'
149-
this.write(utils.base64(user) + "\r\n");
150-
response = [];
131+
else if (/^AUTH /.test(response[i])) {
132+
// Parse supported AUTH methods
133+
var parse = /^AUTH (.+)$/.exec(response[i]);
134+
methods = parse[1].split(/\s+/);
135+
connection.logdebug(self, 'found supported AUTH methods: ' + methods);
136+
// Prefer PLAIN as it's easiest
137+
if (methods.indexOf('PLAIN') !== -1) {
138+
socket.send_command('AUTH','PLAIN ' + utils.base64("\0" + user + "\0" + passwd));
151139
return;
152140
}
153-
else if (code[0] === '3' && response[0] === 'UGFzc3dvcmQ6') {
154-
this.write(utils.base64(passwd) + "\r\n");
155-
response = [];
141+
else if (methods.indexOf('LOGIN') !== -1) {
142+
socket.send_command('AUTH','LOGIN');
156143
return;
157144
}
158-
if (code[0] === '5') {
159-
// Initial attempt failed; strip domain and retry.
160-
var u;
161-
if ((u = /^([^@]+)@.+$/.exec(user))) {
162-
user = u[1];
163-
if (methods.indexOf('PLAIN') !== -1) {
164-
socket.send_command('AUTH', 'PLAIN ' + utils.base64("\0" + user + "\0" + passwd));
165-
}
166-
else if (methods.indexOf('LOGIN') !== -1) {
167-
socket.send_command('AUTH', 'LOGIN');
168-
}
169-
return;
170-
}
171-
else {
172-
// Don't attempt any other hosts
173-
auth_complete = true;
174-
}
145+
else {
146+
// No compatible methods; abort...
147+
connection.logdebug(self, 'no compatible AUTH methods');
148+
socket.send_command('QUIT');
149+
return;
175150
}
176151
}
177-
if (/^[345]/.test(code)) {
178-
// Got an unhandled error
179-
connection.logdebug(self, 'error: ' + line);
180-
socket.send_command('QUIT');
152+
}
153+
}
154+
if (command === 'auth') {
155+
// Handle LOGIN
156+
if (code[0] === '3' && response[0] === 'VXNlcm5hbWU6') {
157+
// Write to the socket directly to keep the state at 'auth'
158+
this.write(utils.base64(user) + "\r\n");
159+
response = [];
160+
return;
161+
}
162+
else if (code[0] === '3' && response[0] === 'UGFzc3dvcmQ6') {
163+
this.write(utils.base64(passwd) + "\r\n");
164+
response = [];
165+
return;
166+
}
167+
if (code[0] === '5') {
168+
// Initial attempt failed; strip domain and retry.
169+
var u;
170+
if ((u = /^([^@]+)@.+$/.exec(user))) {
171+
user = u[1];
172+
if (methods.indexOf('PLAIN') !== -1) {
173+
socket.send_command('AUTH', 'PLAIN ' + utils.base64("\0" + user + "\0" + passwd));
174+
}
175+
else if (methods.indexOf('LOGIN') !== -1) {
176+
socket.send_command('AUTH', 'LOGIN');
177+
}
181178
return;
182179
}
183-
switch (command) {
184-
case 'starttls':
185-
var tls_options = { key: key, cert: cert };
186-
this.upgrade(tls_options);
187-
break;
188-
case 'connect':
189-
socket.send_command('EHLO', self.config.get('me'));
190-
break;
191-
case 'auth':
192-
// AUTH was successful
193-
auth_complete = true;
194-
auth_success = true;
195-
socket.send_command('QUIT');
196-
break;
197-
case 'ehlo':
198-
case 'helo':
199-
case 'quit':
200-
socket.end();
201-
break;
202-
default:
203-
throw new Error("[auth/auth_proxy] unknown command: " + command);
180+
else {
181+
// Don't attempt any other hosts
182+
auth_complete = true;
204183
}
205184
}
206185
}
207-
else {
208-
// Unrecognised response.
209-
connection.logerror(self, "unrecognised response: " + line);
210-
socket.end();
186+
if (/^[345]/.test(code)) {
187+
// Got an unhandled error
188+
connection.logdebug(self, 'error: ' + line);
189+
socket.send_command('QUIT');
211190
return;
212191
}
192+
switch (command) {
193+
case 'starttls':
194+
var tls_options = { key: key, cert: cert };
195+
this.upgrade(tls_options);
196+
break;
197+
case 'connect':
198+
socket.send_command('EHLO', self.config.get('me'));
199+
break;
200+
case 'auth':
201+
// AUTH was successful
202+
auth_complete = true;
203+
auth_success = true;
204+
socket.send_command('QUIT');
205+
break;
206+
case 'ehlo':
207+
case 'helo':
208+
case 'quit':
209+
socket.end();
210+
break;
211+
default:
212+
throw new Error("[auth/auth_proxy] unknown command: " + command);
213+
}
213214
});
214215
};

plugins/bounce.js

+16
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ exports.single_recipient = function(next, connection) {
100100
return next();
101101
}
102102

103+
// Skip this check for relays or private_ips
104+
// This is because Microsoft Exchange will send mail
105+
// to distribution groups using the null-sender if
106+
// the option 'Do not send delivery reports' is
107+
// checked (not sure if this is default or not)
108+
if (connection.relaying) {
109+
transaction.results.add(plugin,
110+
{skip: 'single_recipient(relay)', emit: true });
111+
return next();
112+
}
113+
if (net_utils.is_private_ip(connection.remote_ip)) {
114+
transaction.results.add(plugin,
115+
{skip: 'single_recipient(private_ip)', emit: true });
116+
return next();
117+
}
118+
103119
connection.loginfo(plugin, "bounce with too many recipients to: " +
104120
connection.transaction.rcpt_to.join(','));
105121

tests/mailheader.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
var Header = require('../mailheader').Header;
2+
3+
var lines = [
4+
'Return-Path: <helpme@gmail.com>',
5+
'Received: from [1.1.1.1] ([2.2.2.2])\
6+
by smtp.gmail.com with ESMTPSA id abcdef.28.2016.03.31.12.51.37\
7+
for <foo@bar.com>\
8+
(version=TLSv1/SSLv3 cipher=OTHER);\
9+
Thu, 31 Mar 2016 12:51:37 -0700 (PDT)',
10+
'From: Matt Sergeant <helpme@gmail.com>',
11+
'Content-Type: multipart/alternative;\
12+
boundary=Apple-Mail-F2C5DAD3-7EB3-409D-9FE0-135C9FD43B69',
13+
'Content-Transfer-Encoding: 7bit',
14+
'Mime-Version: 1.0 (1.0)',
15+
'Subject: Re: Haraka Rocks!',
16+
'Message-Id: <616DF75E-D799-4F3C-9901-1642B494C45D@gmail.com>',
17+
'Date: Thu, 31 Mar 2016 15:51:36 -0400',
18+
'To: The World <world@example.com>',
19+
'X-Mailer: iPhone Mail (13E233)',
20+
];
21+
22+
exports.basic = {
23+
parse_basic: function (test) {
24+
test.expect(2);
25+
var h = new Header();
26+
h.parse(lines);
27+
test.equal(h.lines().length, 11);
28+
test.ok(/multipart\/alternative;\s+boundary=Apple-Mail-F2C5DAD3-7EB3-409D-9FE0-135C9FD43B69/.test(h.get_decoded('content-type')));
29+
test.done();
30+
}
31+
}
32+
33+
exports.add_headers = {
34+
add_basic: function (test) {
35+
test.expect(2);
36+
var h = new Header();
37+
h.parse(lines);
38+
h.add('Foo', 'bar');
39+
test.equal(h.lines()[0], 'Foo: bar\n');
40+
h.add_end('Fizz', 'buzz');
41+
test.equal(h.lines()[12], 'Fizz: buzz\n');
42+
test.done();
43+
},
44+
add_utf8: function (test) {
45+
test.expect(2);
46+
var h = new Header();
47+
h.parse(lines);
48+
h.add('Foo', 'bøø');
49+
test.equal(h.lines()[0], 'Foo: =?UTF-8?q?b=F8=F8?=\n');
50+
// test wrapping
51+
h.add('Bar', 'bøø 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890');
52+
test.equal(h.lines()[0], 'Bar: =?UTF-8?q?b=F8=F8 1234567890123456789012345678901234567890123456789012345678901234567=\n 890123456789012345678901234567890?=\n');
53+
test.done();
54+
}
55+
}

0 commit comments

Comments
 (0)