Skip to content

Commit 73f11be

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 73f11be

File tree

4 files changed

+118
-8
lines changed

4 files changed

+118
-8
lines changed

.github/workflows/testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ jobs:
194194
#
195195
# v3 is to re-verify all Mac OS builds after fix for the
196196
# gh-6076 problem (see below).
197-
key: ${{ matrix.runs-on }}-${{ matrix.tarantool }}-v3
197+
key: ${{ matrix.runs-on }}-${{ matrix.tarantool }}-v3-
198198
if: matrix.tarantool != 'brew' && matrix.tarantool != 'master'
199199

200200
- name: Install tarantool build dependencies

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: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ 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+
s:write('Breaking connection\r\n')
22+
return -1
23+
else
24+
s:write('250 OK\r\n')
25+
end
26+
return 1
27+
end
28+
1329
local function smtp_h(s)
1430
s:write('220 localhost ESMTP Tarantool\r\n')
1531
local l
@@ -26,10 +42,14 @@ local function smtp_h(s)
2642
s:write('250 HELP\r\n')
2743
elseif l:find('MAIL FROM:') then
2844
mail.from = l:sub(11):sub(1, -3)
29-
s:write('250 OK\r\n')
45+
if write_reply_code(s, l) == -1 then
46+
return
47+
end
3048
elseif l:find('RCPT TO:') then
3149
mail.rcpt[#mail.rcpt + 1] = l:sub(9):sub(1, -3)
32-
s:write('250 OK\r\n')
50+
if write_reply_code(s, l) == -1 then
51+
return
52+
end
3353
elseif l == 'DATA\r\n' then
3454
s:write('354 Enter message, ending with "." on a line by itself\r\n')
3555
while true do
@@ -56,7 +76,7 @@ local server = socket.tcp_server('127.0.0.1', 0, smtp_h)
5676
local addr = 'smtp://127.0.0.1:' .. server:name().port
5777

5878
test:test("smtp.client", function(test)
59-
test:plan(10)
79+
test:plan(26)
6080
local r
6181
local m
6282

@@ -146,6 +166,54 @@ test:test("smtp.client", function(test)
146166
m.text,
147167
"Subject: =%?utf%-8%?b%?YWJjZGVmZ2hpamvRj2xtbm9wcXJzdHV2d3h5eg==%?=", ""))
148168
test:is(subj, 1, 'subject codes >127')
149-
end)
150169

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

0 commit comments

Comments
 (0)