Skip to content

Commit f1e5b60

Browse files
committed
client: handling SMTP errors from server
In case server has some settings, which don't let some types of SMTP request to be handled, SMTP client may receive 5xx, 4xx, 3xx codes. This patch fixes the problem, when on the client side this SMTP codes are not handled normally. Instead of runtime error and unusual message that there is some connection error. Fixes #13
1 parent c771f3f commit f1e5b60

File tree

3 files changed

+116
-7
lines changed

3 files changed

+116
-7
lines changed

smtp/smtpc.c

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ smtpc_request_new(struct smtpc_env *env, const char *url, const char *from)
241241
return NULL;
242242
}
243243
memset(req, 0, sizeof(*req));
244+
245+
req->error_buf = calloc(CURL_ERROR_SIZE, sizeof(char));
246+
if (req->error_buf == NULL) {
247+
free(req);
248+
box_error_set(__FILE__, __LINE__, ER_MEMORY_ISSUE,
249+
"Can't alloc error message buffer");
250+
return NULL;
251+
}
244252
req->env = env;
245253

246254
req->easy = curl_easy_init();
@@ -252,6 +260,7 @@ smtpc_request_new(struct smtpc_env *env, const char *url, const char *from)
252260
}
253261
curl_easy_setopt(req->easy, CURLOPT_URL, url);
254262
curl_easy_setopt(req->easy, CURLOPT_MAIL_FROM, from);
263+
curl_easy_setopt(req->easy, CURLOPT_ERRORBUFFER, req->error_buf);
255264

256265
return req;
257266
}
@@ -270,6 +279,7 @@ smtpc_request_delete(struct smtpc_request *req)
270279
if (req->easy != NULL)
271280
coio_call(smtpc_task_delete, req);
272281
free(req->body);
282+
free(req->error_buf);
273283
if (req->recipients)
274284
curl_slist_free_all(req->recipients);
275285

@@ -440,10 +450,25 @@ smtpc_execute(struct smtpc_request *req, double timeout)
440450
++env->stat.failed_requests;
441451
smtpc_request_delete(req);
442452
return -1;
453+
case CURLE_SEND_ERROR:
454+
case CURLE_RECV_ERROR:
455+
curl_easy_getinfo(req->easy, CURLINFO_RESPONSE_CODE, &longval);
456+
req->status = (int) longval;
457+
/*
458+
* When client can not read bytes from another side that
459+
* leads to a message about failed response reading but
460+
* response code stays 250, that is not correct.
461+
*/
462+
if (strcmp(req->error_buf, "response reading failed") == 0) {
463+
req->status = -1;
464+
}
465+
req->reason = req->error_buf;
466+
++env->stat.failed_requests;
467+
break;
443468
default: {
444469
char error_msg[256];
445470
curl_easy_getinfo(req->easy, CURLINFO_OS_ERRNO, &longval);
446-
snprintf(error_msg, sizeof(error_msg), "SMTP error %i (os errno %li)", req->code, longval);
471+
snprintf(error_msg, sizeof(error_msg), "CURL error %i (os errno %li)", req->code, longval);
447472
box_error_set(__FILE__, __LINE__, ER_UNKNOWN, error_msg);
448473
++env->stat.failed_requests;
449474
smtpc_request_delete(req);

smtp/smtpc.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,27 @@ struct smtpc_request {
112112
int body_size;
113113
/** Buffer read position. */
114114
char *body_rpos;
115-
/** SMTP status code. */
115+
/**
116+
* SMTP status code.
117+
* It takes the value of -1 if there is some problem,
118+
* which is not related to SMTP, like connection error.
119+
*/
116120
int status;
117-
/** Error message. */
121+
/**
122+
* Error message.
123+
* It is a string to report details of an error to a user.
124+
* Does not require freeing.
125+
* It is never NULL if smtpc_execute() returns zero.
126+
*/
118127
const char *reason;
128+
/**
129+
* Error buffer for receiving messages.
130+
* It should exist during a request lifetime and
131+
* must be freed at freeing the request structure.
132+
* This field is not for reading directly, cause
133+
* reason field points to it, when appropriate.
134+
*/
135+
char *error_buf;
119136
};
120137

121138
/**

test/smtp.test.lua

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ local os = require('os')
1010
test:plan(1)
1111
mails = fiber.channel(100)
1212

13+
function write_reply_code(s, l)
14+
if l:find('3xx') then
15+
s:write('354 Start mail input\r\n')
16+
elseif l:find('4xx') then
17+
s:write('421 Service not available, closing transmission channel\r\n')
18+
elseif l:find('5xx') then
19+
s:write('510 Bad email address\r\n')
20+
elseif l:find('breakconnect') then
21+
return -1
22+
else
23+
s:write('250 OK\r\n')
24+
end
25+
return 1
26+
end
27+
1328
local function smtp_h(s)
1429
s:write('220 localhost ESMTP Tarantool\r\n')
1530
local l
@@ -26,10 +41,14 @@ local function smtp_h(s)
2641
s:write('250 HELP\r\n')
2742
elseif l:find('MAIL FROM:') then
2843
mail.from = l:sub(11):sub(1, -3)
29-
s:write('250 OK\r\n')
44+
if write_reply_code(s, l) == -1 then
45+
return
46+
end
3047
elseif l:find('RCPT TO:') then
3148
mail.rcpt[#mail.rcpt + 1] = l:sub(9):sub(1, -3)
32-
s:write('250 OK\r\n')
49+
if write_reply_code(s, l) == -1 then
50+
return
51+
end
3352
elseif l == 'DATA\r\n' then
3453
s:write('354 Enter message, ending with "." on a line by itself\r\n')
3554
while true do
@@ -56,7 +75,7 @@ local server = socket.tcp_server('127.0.0.1', 0, smtp_h)
5675
local addr = 'smtp://127.0.0.1:' .. server:name().port
5776

5877
test:test("smtp.client", function(test)
59-
test:plan(10)
78+
test:plan(26)
6079
local r
6180
local m
6281

@@ -146,6 +165,54 @@ test:test("smtp.client", function(test)
146165
m.text,
147166
"Subject: =%?utf%-8%?b%?YWJjZGVmZ2hpamvRj2xtbm9wcXJzdHV2d3h5eg==%?=", ""))
148167
test:is(subj, 1, 'subject codes >127')
149-
end)
150168

169+
r = client:request(addr, '[email protected]',
170+
171+
'mail.body')
172+
test:is(r.reason, 'MAIL failed: 354', 'errors 3xx')
173+
test:is(r.status, 354, 'expected code')
174+
175+
r = client:request(addr, '[email protected]',
176+
177+
'mail.body')
178+
test:is(r.reason, 'MAIL failed: 421', 'service unavailable')
179+
test:is(r.status, 421, 'expected code')
180+
181+
r = client:request(addr, '[email protected]',
182+
183+
'mail.body')
184+
test:is(r.reason, 'MAIL failed: 510', 'unexisting recipient')
185+
test:is(r.status, 510, 'expected code')
186+
187+
r = client:request(addr, '[email protected]',
188+
189+
'mail.body')
190+
test:is(r.reason, 'response reading failed', 'unexpected response code')
191+
test:is(r.status, -1, 'expected code')
192+
193+
r = client:request(addr, '[email protected]',
194+
195+
'mail.body')
196+
test:is(r.reason, 'RCPT failed: 354', 'errors 3xx')
197+
test:is(r.status, 354, 'expected code')
198+
199+
r = client:request(addr, '[email protected]',
200+
201+
'mail.body')
202+
test:is(r.reason, 'RCPT failed: 421', 'service unavailable')
203+
test:is(r.status, 421, 'expected code')
204+
205+
r = client:request(addr, '[email protected]',
206+
207+
'mail.body')
208+
test:is(r.reason, 'RCPT failed: 510', 'unexisting recipient')
209+
test:is(r.status, 510, 'expected code')
210+
211+
r = client:request(addr, '[email protected]',
212+
213+
'mail.body')
214+
test:is(r.reason, 'response reading failed', 'unexpected response code')
215+
test:is(r.status, -1, 'expected code')
216+
217+
end)
151218
os.exit(test:check() == true and 0 or -1)

0 commit comments

Comments
 (0)