smaller timeout on SMTP connect
[public/netxms.git] / src / server / core / email.cpp
1 /*
2 ** NetXMS - Network Management System
3 ** Copyright (C) 2003-2016 Victor Kirhenshtein
4 **
5 ** This program is free software; you can redistribute it and/or modify
6 ** it under the terms of the GNU General Public License as published by
7 ** the Free Software Foundation; either version 2 of the License, or
8 ** (at your option) any later version.
9 **
10 ** This program is distributed in the hope that it will be useful,
11 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ** GNU General Public License for more details.
14 **
15 ** You should have received a copy of the GNU General Public License
16 ** along with this program; if not, write to the Free Software
17 ** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 **
19 ** File: email.cpp
20 **
21 **/
22
23 #include "nxcore.h"
24
25 /**
26 * Receive buffer size
27 */
28 #define SMTP_BUFFER_SIZE 1024
29
30 /**
31 * Sender errors
32 */
33 #define SMTP_ERR_SUCCESS 0
34 #define SMTP_ERR_BAD_SERVER_NAME 1
35 #define SMTP_ERR_COMM_FAILURE 2
36 #define SMTP_ERR_PROTOCOL_FAILURE 3
37
38 /**
39 * Mail sender states
40 */
41 #define STATE_INITIAL 0
42 #define STATE_HELLO 1
43 #define STATE_FROM 2
44 #define STATE_RCPT 3
45 #define STATE_DATA 4
46 #define STATE_MAIL_BODY 5
47 #define STATE_QUIT 6
48 #define STATE_FINISHED 7
49 #define STATE_ERROR 8
50
51 /**
52 * Mail envelope structure
53 */
54 struct MAIL_ENVELOPE
55 {
56 char rcptAddr[MAX_RCPT_ADDR_LEN];
57 char subject[MAX_EMAIL_SUBJECT_LEN];
58 char *text;
59 char encoding[64];
60 int retryCount;
61 };
62
63 /**
64 * Static data
65 */
66 static TCHAR m_szSmtpServer[MAX_PATH] = _T("localhost");
67 static WORD m_wSmtpPort = 25;
68 static char m_szFromAddr[MAX_PATH] = "netxms@localhost";
69 static char m_szFromName[MAX_PATH] = "NetXMS Server";
70 static Queue *m_pMailerQueue = NULL;
71 static THREAD m_hThread = INVALID_THREAD_HANDLE;
72
73 /**
74 * Find end-of-line character
75 */
76 static char *FindEOL(char *pszBuffer, int nLen)
77 {
78 int i;
79
80 for(i = 0; i < nLen; i++)
81 if (pszBuffer[i] == '\n')
82 return &pszBuffer[i];
83 return NULL;
84 }
85
86 /**
87 * Read line from socket
88 */
89 static BOOL ReadLineFromSocket(SOCKET hSocket, char *pszBuffer, int *pnBufPos, char *pszLine)
90 {
91 char *ptr;
92 int nRet;
93
94 do
95 {
96 ptr = FindEOL(pszBuffer, *pnBufPos);
97 if (ptr == NULL)
98 {
99 nRet = RecvEx(hSocket, &pszBuffer[*pnBufPos], SMTP_BUFFER_SIZE - *pnBufPos, 0, 30000);
100 if (nRet <= 0)
101 return FALSE;
102 *pnBufPos += nRet;
103 }
104 } while(ptr == NULL);
105 *ptr = 0;
106 strcpy(pszLine, pszBuffer);
107 *pnBufPos -= (int)(ptr - pszBuffer + 1);
108 memmove(pszBuffer, ptr + 1, *pnBufPos);
109 return TRUE;
110 }
111
112 /**
113 * Read SMTP response code from socket
114 */
115 static int GetSMTPResponse(SOCKET hSocket, char *pszBuffer, int *pnBufPos)
116 {
117 char szLine[SMTP_BUFFER_SIZE];
118
119 while(1)
120 {
121 if (!ReadLineFromSocket(hSocket, pszBuffer, pnBufPos, szLine))
122 return -1;
123 if (strlen(szLine) < 4)
124 return -1;
125 if (szLine[3] == ' ')
126 {
127 szLine[3] = 0;
128 break;
129 }
130 }
131 return atoi(szLine);
132 }
133
134 /**
135 * Send e-mail
136 */
137 static UINT32 SendMail(const char *pszRcpt, const char *pszSubject, const char *pszText, const char *encoding)
138 {
139 SOCKET hSocket;
140 char szBuffer[SMTP_BUFFER_SIZE];
141 int iResp, iState = STATE_INITIAL, nBufPos = 0;
142 UINT32 dwRetCode;
143
144 // Resolve hostname
145 InetAddress addr = InetAddress::resolveHostName(m_szSmtpServer);
146 if (!addr.isValidUnicast())
147 return SMTP_ERR_BAD_SERVER_NAME;
148
149 // Create socket
150 hSocket = socket(addr.getFamily(), SOCK_STREAM, 0);
151 if (hSocket == INVALID_SOCKET)
152 return SMTP_ERR_COMM_FAILURE;
153
154 // Connect to server
155 SockAddrBuffer sa;
156 addr.fillSockAddr(&sa, m_wSmtpPort);
157 if (ConnectEx(hSocket, (struct sockaddr *)&sa, SA_LEN((struct sockaddr *)&sa), 3000) == 0)
158 {
159 while((iState != STATE_FINISHED) && (iState != STATE_ERROR))
160 {
161 iResp = GetSMTPResponse(hSocket, szBuffer, &nBufPos);
162 DbgPrintf(8, _T("SMTP RESPONSE: %03d (state=%d)"), iResp, iState);
163 if (iResp > 0)
164 {
165 switch(iState)
166 {
167 case STATE_INITIAL:
168 // Server should send 220 text after connect
169 if (iResp == 220)
170 {
171 iState = STATE_HELLO;
172 SendEx(hSocket, "HELO netxms\r\n", 13, 0, NULL);
173 }
174 else
175 {
176 iState = STATE_ERROR;
177 }
178 break;
179 case STATE_HELLO:
180 // Server should respond with 250 text to our HELO command
181 if (iResp == 250)
182 {
183 iState = STATE_FROM;
184 snprintf(szBuffer, SMTP_BUFFER_SIZE, "MAIL FROM: <%s>\r\n", m_szFromAddr);
185 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
186 }
187 else
188 {
189 iState = STATE_ERROR;
190 }
191 break;
192 case STATE_FROM:
193 // Server should respond with 250 text to our MAIL FROM command
194 if (iResp == 250)
195 {
196 iState = STATE_RCPT;
197 snprintf(szBuffer, SMTP_BUFFER_SIZE, "RCPT TO: <%s>\r\n", pszRcpt);
198 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
199 }
200 else
201 {
202 iState = STATE_ERROR;
203 }
204 break;
205 case STATE_RCPT:
206 // Server should respond with 250 text to our RCPT TO command
207 if (iResp == 250)
208 {
209 iState = STATE_DATA;
210 SendEx(hSocket, "DATA\r\n", 6, 0, NULL);
211 }
212 else
213 {
214 iState = STATE_ERROR;
215 }
216 break;
217 case STATE_DATA:
218 // Server should respond with 354 text to our DATA command
219 if (iResp == 354)
220 {
221 iState = STATE_MAIL_BODY;
222
223 // Mail headers
224 // from
225 snprintf(szBuffer, SMTP_BUFFER_SIZE, "From: %s <%s>\r\n", m_szFromName, m_szFromAddr);
226 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
227 // to
228 snprintf(szBuffer, SMTP_BUFFER_SIZE, "To: <%s>\r\n", pszRcpt);
229 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
230 // subject
231 bool encodeSubject = false;
232 for(const char *p = pszSubject; *p != 0; p++)
233 if (*p & 0x80)
234 {
235 encodeSubject = true;
236 break;
237 }
238 if (encodeSubject)
239 {
240 char *encodedSubject = NULL;
241 base64_encode_alloc(pszSubject, strlen(pszSubject), &encodedSubject);
242 if (encodedSubject != NULL)
243 {
244 snprintf(szBuffer, SMTP_BUFFER_SIZE, "Subject: =?%s?B?%s?=\r\n", encoding, encodedSubject);
245 free(encodedSubject);
246 }
247 else
248 {
249 // fallback
250 snprintf(szBuffer, SMTP_BUFFER_SIZE, "Subject: %s\r\n", pszSubject);
251 }
252 }
253 else
254 {
255 snprintf(szBuffer, SMTP_BUFFER_SIZE, "Subject: %s\r\n", pszSubject);
256 }
257 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
258
259 // date
260 time_t currentTime;
261 struct tm *pCurrentTM;
262 time(&currentTime);
263 #ifdef HAVE_LOCALTIME_R
264 struct tm currentTM;
265 localtime_r(&currentTime, &currentTM);
266 pCurrentTM = &currentTM;
267 #else
268 pCurrentTM = localtime(&currentTime);
269 #endif
270 #ifdef _WIN32
271 strftime(szBuffer, sizeof(szBuffer), "Date: %a, %d %b %Y %H:%M:%S ", pCurrentTM);
272
273 TIME_ZONE_INFORMATION tzi;
274 UINT32 tzType = GetTimeZoneInformation(&tzi);
275 LONG effectiveBias;
276 switch(tzType)
277 {
278 case TIME_ZONE_ID_STANDARD:
279 effectiveBias = tzi.Bias + tzi.StandardBias;
280 break;
281 case TIME_ZONE_ID_DAYLIGHT:
282 effectiveBias = tzi.Bias + tzi.DaylightBias;
283 break;
284 case TIME_ZONE_ID_UNKNOWN:
285 effectiveBias = tzi.Bias;
286 break;
287 default: // error
288 effectiveBias = 0;
289 DbgPrintf(4, _T("GetTimeZoneInformation() call failed"));
290 break;
291 }
292 int offset = abs(effectiveBias);
293 sprintf(&szBuffer[strlen(szBuffer)], "%c%02d%02d\r\n", effectiveBias <= 0 ? '+' : '-', offset / 60, offset % 60);
294 #else
295 strftime(szBuffer, sizeof(szBuffer), "Date: %a, %d %b %Y %H:%M:%S %z\r\n", pCurrentTM);
296 #endif
297
298 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
299 // content-type
300 snprintf(szBuffer, SMTP_BUFFER_SIZE,
301 "Content-Type: text/plain; charset=%s\r\n"
302 "Content-Transfer-Encoding: 8bit\r\n\r\n", encoding);
303 SendEx(hSocket, szBuffer, strlen(szBuffer), 0, NULL);
304
305 // Mail body
306 SendEx(hSocket, pszText, strlen(pszText), 0, NULL);
307 SendEx(hSocket, "\r\n.\r\n", 5, 0, NULL);
308 }
309 else
310 {
311 iState = STATE_ERROR;
312 }
313 break;
314 case STATE_MAIL_BODY:
315 // Server should respond with 250 to our mail body
316 if (iResp == 250)
317 {
318 iState = STATE_QUIT;
319 SendEx(hSocket, "QUIT\r\n", 6, 0, NULL);
320 }
321 else
322 {
323 iState = STATE_ERROR;
324 }
325 break;
326 case STATE_QUIT:
327 // Server should respond with 221 text to our QUIT command
328 if (iResp == 221)
329 {
330 iState = STATE_FINISHED;
331 }
332 else
333 {
334 iState = STATE_ERROR;
335 }
336 break;
337 default:
338 iState = STATE_ERROR;
339 break;
340 }
341 }
342 else
343 {
344 iState = STATE_ERROR;
345 }
346 }
347 // Shutdown communication channel
348 shutdown(hSocket, SHUT_RDWR);
349
350 dwRetCode = (iState == STATE_FINISHED) ? SMTP_ERR_SUCCESS : SMTP_ERR_PROTOCOL_FAILURE;
351 }
352 else
353 {
354 dwRetCode = SMTP_ERR_COMM_FAILURE;
355 }
356
357 if (hSocket != -1)
358 {
359 closesocket(hSocket);
360 }
361
362 return dwRetCode;
363 }
364
365 /**
366 * Mailer thread
367 */
368 static THREAD_RESULT THREAD_CALL MailerThread(void *pArg)
369 {
370 static const TCHAR *m_szErrorText[] =
371 {
372 _T("Sent successfully"),
373 _T("Unable to resolve SMTP server name"),
374 _T("Communication failure"),
375 _T("SMTP conversation failure")
376 };
377
378 DbgPrintf(1, _T("SMTP mailer thread started"));
379 while(1)
380 {
381 MAIL_ENVELOPE *pEnvelope = (MAIL_ENVELOPE *)m_pMailerQueue->getOrBlock();
382 if (pEnvelope == INVALID_POINTER_VALUE)
383 break;
384
385 DbgPrintf(6, _T("SMTP(%p): new envelope, rcpt=%hs"), pEnvelope, pEnvelope->rcptAddr);
386
387 ConfigReadStr(_T("SMTPServer"), m_szSmtpServer, MAX_PATH, _T("localhost"));
388 ConfigReadStrA(_T("SMTPFromAddr"), m_szFromAddr, MAX_PATH, "netxms@localhost");
389 ConfigReadStrA(_T("SMTPFromName"), m_szFromName, MAX_PATH, "NetXMS Server");
390 m_wSmtpPort = (WORD)ConfigReadInt(_T("SMTPPort"), 25);
391
392 UINT32 dwResult = SendMail(pEnvelope->rcptAddr, pEnvelope->subject, pEnvelope->text, pEnvelope->encoding);
393 if (dwResult != SMTP_ERR_SUCCESS)
394 {
395 pEnvelope->retryCount--;
396 DbgPrintf(6, _T("SMTP(%p): Failed to send e-mail, remaining retries: %d"), pEnvelope, pEnvelope->retryCount);
397 if (pEnvelope->retryCount > 0)
398 {
399 // Try posting again
400 m_pMailerQueue->put(pEnvelope);
401 }
402 else
403 {
404 PostEvent(EVENT_SMTP_FAILURE, g_dwMgmtNode, "dsmm", dwResult,
405 m_szErrorText[dwResult], pEnvelope->rcptAddr, pEnvelope->subject);
406 free(pEnvelope->text);
407 free(pEnvelope);
408 }
409 }
410 else
411 {
412 DbgPrintf(6, _T("SMTP(%p): mail sent successfully"), pEnvelope);
413 free(pEnvelope->text);
414 free(pEnvelope);
415 }
416 }
417 return THREAD_OK;
418 }
419
420 /**
421 * Initialize mailer subsystem
422 */
423 void InitMailer()
424 {
425 m_pMailerQueue = new Queue;
426 m_hThread = ThreadCreateEx(MailerThread, 0, NULL);
427 }
428
429 /**
430 * Shutdown mailer
431 */
432 void ShutdownMailer()
433 {
434 m_pMailerQueue->clear();
435 m_pMailerQueue->put(INVALID_POINTER_VALUE);
436 if (m_hThread != INVALID_THREAD_HANDLE)
437 ThreadJoin(m_hThread);
438 delete m_pMailerQueue;
439 }
440
441 /**
442 * Post e-mail to queue
443 */
444 void NXCORE_EXPORTABLE PostMail(const TCHAR *pszRcpt, const TCHAR *pszSubject, const TCHAR *pszText)
445 {
446 MAIL_ENVELOPE *envelope = (MAIL_ENVELOPE *)malloc(sizeof(MAIL_ENVELOPE));
447 ConfigReadStrA(_T("MailEncoding"), envelope->encoding, 64, "utf8");
448 bool isUtf8 = (!stricmp(envelope->encoding, "utf-8") || !stricmp(envelope->encoding, "utf8"));
449
450 #ifdef UNICODE
451 WideCharToMultiByte(isUtf8 ? CP_UTF8 : CP_ACP, isUtf8 ? 0 : WC_DEFAULTCHAR | WC_COMPOSITECHECK, pszRcpt, -1, envelope->rcptAddr, MAX_RCPT_ADDR_LEN, NULL, NULL);
452 envelope->rcptAddr[MAX_RCPT_ADDR_LEN - 1] = 0;
453 WideCharToMultiByte(isUtf8 ? CP_UTF8 : CP_ACP, isUtf8 ? 0 : WC_DEFAULTCHAR | WC_COMPOSITECHECK, pszSubject, -1, envelope->subject, MAX_EMAIL_SUBJECT_LEN, NULL, NULL);
454 envelope->subject[MAX_EMAIL_SUBJECT_LEN - 1] = 0;
455 envelope->text = isUtf8 ? UTF8StringFromWideString(pszText) : MBStringFromWideString(pszText);
456 #else
457 if (isUtf8)
458 {
459 utf8_to_mb(pszRcpt, -1, envelope->rcptAddr, MAX_RCPT_ADDR_LEN);
460 envelope->rcptAddr[MAX_RCPT_ADDR_LEN - 1] = 0;
461 utf8_to_mb(pszSubject, -1, envelope->subject, MAX_EMAIL_SUBJECT_LEN);
462 envelope->subject[MAX_EMAIL_SUBJECT_LEN - 1] = 0;
463 envelope->text = UTF8StringFromMBString(pszText);
464 }
465 else
466 {
467 nx_strncpy(envelope->rcptAddr, pszRcpt, MAX_RCPT_ADDR_LEN);
468 nx_strncpy(envelope->subject, pszSubject, MAX_EMAIL_SUBJECT_LEN);
469 envelope->text = strdup(pszText);
470 }
471 #endif
472 envelope->retryCount = ConfigReadInt(_T("SMTPRetryCount"), 1);
473 m_pMailerQueue->put(envelope);
474 }