smtpc.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. /*
  2. * Copyright 2010 by egnite GmbH
  3. *
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without
  7. * modification, are permitted provided that the following conditions
  8. * are met:
  9. *
  10. * 1. Redistributions of source code must retain the above copyright
  11. * notice, this list of conditions and the following disclaimer.
  12. * 2. Redistributions in binary form must reproduce the above copyright
  13. * notice, this list of conditions and the following disclaimer in the
  14. * documentation and/or other materials provided with the distribution.
  15. * 3. Neither the name of the copyright holders nor the names of
  16. * contributors may be used to endorse or promote products derived
  17. * from this software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  20. * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  21. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  22. * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  23. * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  24. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  25. * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  26. * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
  27. * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  28. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
  29. * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  30. * SUCH DAMAGE.
  31. *
  32. * For additional information see http://www.ethernut.de/
  33. */
  34. /*
  35. * \file pro/smtpc.c
  36. * \brief Simple mail transfer protocol client.
  37. *
  38. * \verbatim
  39. * $Id$
  40. * \endverbatim
  41. */
  42. #include <sys/nutdebug.h>
  43. #include <sys/confnet.h>
  44. #include <sys/socket.h>
  45. #include <arpa/inet.h>
  46. #include <pro/rfctime.h>
  47. #include <memdebug.h>
  48. #include <string.h>
  49. #include <pro/smtpc.h>
  50. /*!
  51. * \addtogroup xgSMTPC
  52. */
  53. /*@{*/
  54. #ifndef SMTP_TIMEOUT
  55. #define SMTP_TIMEOUT 600000
  56. #endif
  57. /*!
  58. * \brief BASE64 encoder.
  59. *
  60. * The encoder that is available in the gorp library can't be used for
  61. * sending SMTP authentication strings containing ASCII 0 characters.
  62. * This local implementation fixes this and takes a user supplied buffer
  63. * for the encoded string, avoiding costly heap allocation.
  64. *
  65. * In opposite to the original gorp routine, this function will not
  66. * insert any line breaks. Note, that the encoder loop will not release
  67. * the CPU. Thus, the caller should split large buffers into smaller
  68. * chunks and add any required line breaks.
  69. *
  70. * \param sptr Pointer to the source buffer that contains the data to
  71. * encode.
  72. * \param slen Number of bytes available in the source buffer.
  73. * \param dptr Pointer to the destination buffer that receives the
  74. * encoded data. A terminating zero byte will be appended.
  75. * The caller must make sure, that the size of this buffer
  76. * is large enough to hold the resulting string. A save
  77. * calculation is (slen / 3) * 4 + 5.
  78. */
  79. static void NutBase64Encode(const uint8_t * sptr, size_t slen, char *dptr)
  80. {
  81. static const char base64set[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  82. uint_fast8_t i;
  83. uint32_t blk;
  84. uint_fast8_t pad = 0;
  85. /* Normally we do not check for NULL pointers. */
  86. NUTASSERT(sptr != NULL);
  87. NUTASSERT(dptr != NULL);
  88. while (slen) {
  89. blk = (uint32_t) (*sptr++) << 16;
  90. if (slen > 1) {
  91. blk |= (uint32_t) (*sptr++) << 8;
  92. if (slen > 2) {
  93. blk |= *sptr++;
  94. } else {
  95. pad = 1;
  96. }
  97. } else {
  98. pad = 2;
  99. }
  100. for (i = pad; i < 4; i++) {
  101. *dptr++ = base64set[(blk >> 18) & 63];
  102. blk <<= 6;
  103. }
  104. for (i = 0; i < pad; i++) {
  105. *dptr++ = '=';
  106. }
  107. slen -= 3 - pad;
  108. }
  109. *dptr = '\0';
  110. }
  111. /*!
  112. * \brief Read a response line from the server.
  113. *
  114. * \return Pointer to a buffer containing the response. In case of a
  115. * broken connection or a line overflow, a NULL pointer is
  116. * returned.
  117. */
  118. const char *NutSmtpReceiveResponse(SMTPCLIENTSESSION * si)
  119. {
  120. char *cp;
  121. /* Normally we do not check for NULL pointers. */
  122. NUTASSERT(si != NULL);
  123. if (fgets(si->smtp_buff, sizeof(si->smtp_buff), si->smtp_stream)) {
  124. cp = strchr(si->smtp_buff, '\r');
  125. if (cp == NULL) {
  126. cp = strchr(si->smtp_buff, '\n');
  127. }
  128. if (cp) {
  129. *cp = '\0';
  130. return si->smtp_buff;
  131. }
  132. /* Line overflow. */
  133. }
  134. return NULL;
  135. }
  136. /*!
  137. * \brief Send command to the server and return the first response line.
  138. *
  139. * If a multi-line response is expected, the caller may use
  140. * NutSmtpReceiveResponse() to receive additional response lines.
  141. *
  142. * \param fmt Format string containing conversion specifications like
  143. * printf.
  144. */
  145. const char *NutSmtpSendCommand(SMTPCLIENTSESSION * si, const char *fmt, ...)
  146. {
  147. va_list ap;
  148. /* Normally we do not check for NULL pointers. */
  149. NUTASSERT(si != NULL);
  150. NUTASSERT(fmt != NULL);
  151. va_start(ap, fmt);
  152. vfprintf(si->smtp_stream, (char *) fmt, ap);
  153. va_end(ap);
  154. fputs("\r\n", si->smtp_stream);
  155. fflush(si->smtp_stream);
  156. return NutSmtpReceiveResponse(si);
  157. }
  158. /*!
  159. * \brief Terminate an SMTP session.
  160. *
  161. * Gracefully closes the SMTP connection.
  162. *
  163. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  164. * from a previous call to NutSmtpConnect().
  165. */
  166. void NutSmtpDisconnect(SMTPCLIENTSESSION * si)
  167. {
  168. /* Normally we do not check for NULL pointers. */
  169. NUTASSERT(si != NULL);
  170. if (si->smtp_sock) {
  171. if (si->smtp_stream) {
  172. NutSmtpSendCommand(si, "QUIT");
  173. fclose(si->smtp_stream);
  174. }
  175. NutTcpCloseSocket(si->smtp_sock);
  176. }
  177. free(si);
  178. }
  179. /*!
  180. * \brief Start an SMTP session.
  181. *
  182. * \param ip IP address of the host to connect.
  183. * \param port Port number to connect. Typically port 25 is used by SMTP.
  184. *
  185. * \return A pointer to a newly create \ref SMTPCLIENTSESSION structure,
  186. * if the server is connected and ready to accept commands.
  187. * Otherwise a NULL pointer is returned.
  188. */
  189. SMTPCLIENTSESSION *NutSmtpConnect(uint32_t ip, uint16_t port)
  190. {
  191. SMTPCLIENTSESSION *si;
  192. si = calloc(1, sizeof(SMTPCLIENTSESSION));
  193. if (si) {
  194. si->smtp_sock = NutTcpCreateSocket();
  195. if (si->smtp_sock && NutTcpConnect(si->smtp_sock, ip, port) == 0) {
  196. uint32_t tmo = SMTP_TIMEOUT;
  197. NutTcpSetSockOpt(si->smtp_sock, SO_RCVTIMEO, &tmo, sizeof(tmo));
  198. si->smtp_stream = _fdopen((int) ((intptr_t) si->smtp_sock), "r+b");
  199. if (si->smtp_stream) {
  200. for (;;) {
  201. const char *rsp = NutSmtpReceiveResponse(si);
  202. if (*rsp != '2') {
  203. break;
  204. }
  205. if (*(rsp + 3) != '-') {
  206. return si;
  207. }
  208. }
  209. }
  210. }
  211. NutSmtpDisconnect(si);
  212. }
  213. return NULL;
  214. }
  215. /*!
  216. * \brief Send EHLO/HELO command to the SMTP server.
  217. *
  218. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  219. * from a previous call to NutSmtpConnect().
  220. * \param cmd Either EHLO or HELO.
  221. * \param host Primary host name, or NULL if the local host has no name.
  222. *
  223. * \return
  224. */
  225. static const char *SayHello(SMTPCLIENTSESSION * si, char *cmd, char *host)
  226. {
  227. if (host) {
  228. return NutSmtpSendCommand(si, "%s %s", cmd, host);
  229. }
  230. return NutSmtpSendCommand(si, "%s [%s]", cmd, inet_ntoa(confnet.cdn_ip_addr));
  231. }
  232. /*!
  233. * \brief Identify the client SMTP to the server.
  234. *
  235. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  236. * from a previous call to NutSmtpConnect().
  237. * \param host Primary host name, or NULL if the local host has no name.
  238. * \param user Login name. Set to NULL if authorization is not required.
  239. * \param pass Login password. Ignored if login name is set to NULL.
  240. *
  241. * \return 0 on success, -1 otherwise.
  242. */
  243. int NutSmtpLogin(SMTPCLIENTSESSION * si, char *host, char *user, char *pass)
  244. {
  245. const char *rsp;
  246. /* Normally we do not check for NULL pointers. */
  247. NUTASSERT(si != NULL);
  248. /* We start an SMTP session by issuing the EHLO command. */
  249. rsp = SayHello(si, "EHLO", host);
  250. if (rsp && *rsp == '5') {
  251. /* If EHLO is not supported, we fall back to HELO. */
  252. si->smtp_feat |= SMTPFEAT_VINTAGE;
  253. rsp = SayHello(si, "HELO", host);
  254. }
  255. if (rsp && *rsp == '2') {
  256. if ((si->smtp_feat & SMTPFEAT_VINTAGE) != SMTPFEAT_VINTAGE) {
  257. for (;;) {
  258. if (strncmp(rsp + 4, "AUTH ", 5) == 0) {
  259. if (strstr(rsp + 9, "LOGIN")) {
  260. si->smtp_feat |= SMTPFEAT_AUTH_LOGIN;
  261. }
  262. if (strstr(rsp + 9, "PLAIN")) {
  263. si->smtp_feat |= SMTPFEAT_AUTH_PLAIN;
  264. }
  265. }
  266. if (*(rsp + 3) != '-') {
  267. break;
  268. }
  269. rsp = NutSmtpReceiveResponse(si);
  270. if (rsp == NULL) {
  271. break;
  272. }
  273. }
  274. }
  275. if (user == NULL) {
  276. /* Return with success, if authorization not required. */
  277. return 0;
  278. }
  279. if (si->smtp_feat & SMTPFEAT_AUTH_PLAIN) {
  280. int lu = strlen(user);
  281. int lp = strlen(pass);
  282. uint8_t *auth = malloc(lu + lp + 3);
  283. *auth = '\0';
  284. memcpy(auth + 1, user, lu + 1);
  285. memcpy(auth + 1 + lu + 1, pass, lp);
  286. NutBase64Encode(auth, lu + lp + 2, si->smtp_buff);
  287. rsp = NutSmtpSendCommand(si, "AUTH PLAIN %s", si->smtp_buff);
  288. if (rsp && *rsp == '2') {
  289. return 0;
  290. }
  291. } else if (si->smtp_feat & SMTPFEAT_AUTH_LOGIN) {
  292. }
  293. }
  294. return -1;
  295. }
  296. /*!
  297. * \brief Send mail request.
  298. *
  299. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  300. * from a previous call to NutSmtpConnect().
  301. * \param me Pointer to the \ref MAILENVELOPE structure.
  302. *
  303. * \return Number of recipients accepted by the server. If 0, no mail
  304. * should be sent. In any case the caller should inspect the
  305. * status of each recipient address in the mail envelope
  306. * structure.
  307. */
  308. int NutSmtpSendMailRequest(SMTPCLIENTSESSION * si, MAILENVELOPE * me)
  309. {
  310. int rc = 0;
  311. const char *rsp;
  312. /* Normally we do not check for NULL pointers. */
  313. NUTASSERT(me != NULL);
  314. rsp = NutSmtpSendCommand(si, "MAIL FROM:%s", me->mail_from);
  315. if (rsp && *rsp == '2') {
  316. int i;
  317. for (i = 0; i < MAX_MAIL_RCPTS; i++) {
  318. /* Request all unprocessed recipients. */
  319. if (me->mail_rcpt[i] && (me->mail_rcpt_stat[i] & MAIL_RCPT_DONE) == 0) {
  320. rsp = NutSmtpSendCommand(si, "RCPT TO:%s", me->mail_rcpt[i]);
  321. if (rsp) {
  322. if (*rsp == '2') {
  323. /* Recipient accepted. */
  324. me->mail_rcpt_stat[i] |= MAIL_RCPT_ACPT;
  325. rc++;
  326. } else {
  327. /* Recipient not accepted. */
  328. me->mail_rcpt_stat[i] &= ~MAIL_RCPT_ACPT;
  329. if (*rsp == '5') {
  330. /* Recipient refused. */
  331. me->mail_rcpt_stat[i] |= MAIL_RCPT_FAIL;
  332. }
  333. }
  334. }
  335. }
  336. }
  337. }
  338. return rc;
  339. }
  340. /*!
  341. * \brief Send header line with all recipients of a specified type.
  342. *
  343. * \param stream Stream to send to.
  344. * \param me Pointer to the \ref MAILENVELOPE structure.
  345. * \param type Tpye of recipients, either MAIL_RCPT_TO or MAIL_RCPT_CC.
  346. */
  347. static int SendMailHeaderRecipient(FILE *stream, MAILENVELOPE * me, uint8_t type)
  348. {
  349. uint_fast8_t i;
  350. int cnt;
  351. /* Process the list of recipients given by this envelope. */
  352. for (i = 0, cnt = 0; i < MAX_MAIL_RCPTS; i++) {
  353. /* Check for the recipient's type. */
  354. if ((me->mail_rcpt_stat[i] & MAIL_RCPT_TYPE) == type) {
  355. if (cnt) {
  356. /* Additional recipient. */
  357. fputs(",\r\n ", stream);
  358. } else {
  359. /* First one found. */
  360. fputs(type == MAIL_RCPT_TO ? "To: " : "CC: ", stream);
  361. }
  362. fputs(me->mail_rcpt_header[i], stream);
  363. cnt++;
  364. }
  365. }
  366. if (cnt) {
  367. fputs("\r\n", stream);
  368. }
  369. return 0;
  370. }
  371. /*!
  372. * \brief Send major mail header lines.
  373. *
  374. * This function will send the following header lines:
  375. *
  376. * - Date
  377. * - From
  378. * - Subject
  379. * - To
  380. * - CC
  381. *
  382. * The header information must be supplied by the caller in the mail
  383. * envelope structure.
  384. *
  385. * Note, that BCC recipients are not included.
  386. *
  387. * The caller may add additional headers using stdio functions with the
  388. * stream handle available in the \ref SMTPCLIENTSESSION structure.
  389. *
  390. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  391. * from a previous call to NutSmtpConnect().
  392. * \param me Pointer to the \ref MAILENVELOPE structure.
  393. *
  394. * \return Always 0 right now. Later versions may return -1 in case of
  395. * an error.
  396. */
  397. int NutSmtpSendMailHeader(SMTPCLIENTSESSION * si, MAILENVELOPE * me)
  398. {
  399. /* Normally we do not check for NULL pointers. */
  400. NUTASSERT(si != NULL);
  401. NUTASSERT(si->smtp_stream != NULL);
  402. NUTASSERT(me != NULL);
  403. if (me->mail_date) {
  404. fprintf(si->smtp_stream, "Date: %s\r\n", Rfc1123TimeString(gmtime(&me->mail_date)));
  405. }
  406. fprintf(si->smtp_stream, "From: %s\r\n", me->mail_from_header);
  407. fprintf(si->smtp_stream, "Subject: %s\r\n", me->mail_subj);
  408. SendMailHeaderRecipient(si->smtp_stream, me, MAIL_RCPT_TO);
  409. SendMailHeaderRecipient(si->smtp_stream, me, MAIL_RCPT_CC);
  410. return 0;
  411. }
  412. /*!
  413. * \brief Send encoded email text lines.
  414. *
  415. * Each line will be terminated by a carriage return / linefeed pair.
  416. * Lines larger than \ref SMTP_BUFSIZ are split. This may occure in
  417. * the middle of a word. A dot will be put in front of lines that start
  418. * with a dot.
  419. *
  420. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  421. * from a previous call to NutSmtpConnect().
  422. * \param text Pointer to a string containing the text lines to send.
  423. * Lines should be separated by linefeeds and may be
  424. * optionally prepended by a carriage return. If the pointer
  425. * points to an empty string, nothing will be sent.
  426. *
  427. * \return 0 on success, -1 otherwise. An error typically indicates a
  428. * broken connection.
  429. */
  430. int NutSmtpSendEncodedLines(SMTPCLIENTSESSION * si, const char *text)
  431. {
  432. /* Sanity checks. */
  433. NUTASSERT(si != NULL);
  434. NUTASSERT(text != NULL);
  435. while (*text) {
  436. char *bufp = si->smtp_buff;
  437. int i;
  438. /* Collect a line or a complete buffer. */
  439. for (i = 0; *text && i < SMTP_BUFSIZ; text++) {
  440. /* Ignore carriage returns. */
  441. if (*text != '\r') {
  442. /* Stop at linefeeds or at the end of the message. */
  443. if (*text == '\n' || *text == '\0') {
  444. /* Also send newline. */
  445. i++;
  446. text++;
  447. break;
  448. } else {
  449. /* Send this character unchanged. */
  450. *bufp++ = *text;
  451. i++;
  452. }
  453. }
  454. }
  455. if (i) {
  456. *bufp = '\0';
  457. /* Insert a dot in front of any line that starts with a dot. */
  458. if (si->smtp_buff[0] == '.') {
  459. fputc('.', si->smtp_stream);
  460. }
  461. /* Send the buffer with CR/LF appended. */
  462. fputs(si->smtp_buff, si->smtp_stream);
  463. if (fputs("\r\n", si->smtp_stream) == EOF) {
  464. /* Connection broken. */
  465. return -1;
  466. }
  467. }
  468. }
  469. return 0;
  470. }
  471. /*!
  472. * \brief Send an email via an active SMTP session.
  473. *
  474. * Applications may use the following basic sequence to send an email:
  475. *
  476. * \code
  477. * #include <pro/smtpc.h>
  478. *
  479. * MAILENVELOPE email = {
  480. * 0, "<me@ethernut.de>", "Problem",
  481. * "Be warned.",
  482. * { "<admin@ethernut.de>", NULL, NULL, NULL },
  483. * { MAIL_RCPT_TO, 0, 0, 0 }
  484. * };
  485. * SMTPCLIENTSESSION *smtp;
  486. *
  487. * smtp = NutSmtpConnect(daemon_ip, 25);
  488. * NutSmtpLogin(smtp, NULL, NULL, NULL);
  489. * NutSmtpSendMail(smtp, &email);
  490. * NutSmtpDisconnect(smtp);
  491. * \endcode
  492. *
  493. * More advanced mail transfers may be implemented by using the
  494. * other routines of this API for sending commands or parts of
  495. * an email individually. Even the stream in the session structure
  496. * may be used with stdio calls.
  497. *
  498. * \param si Pointer to the \ref SMTPCLIENTSESSION structure, obtained
  499. * from a previous call to NutSmtpConnect().
  500. * \param me Pointer to the \ref MAILENVELOPE structure. On return,
  501. * the caller should inspect the status of each recipient.
  502. *
  503. * \return Pointer to a buffer containing the last response. The status
  504. * of the recipients in the envelope will have been updated. In
  505. * case of a fatal error or if all recipients had been rejected,
  506. * a NULL pointer is returned.
  507. */
  508. const char *NutSmtpSendMail(SMTPCLIENTSESSION * si, MAILENVELOPE * me)
  509. {
  510. const char *rsp = NULL;
  511. /* Normally we do not check for NULL pointers. */
  512. NUTASSERT(si != NULL);
  513. NUTASSERT(me != NULL);
  514. /* Send a request for sending mail. The function returns the number
  515. of accepted recipients. */
  516. if (NutSmtpSendMailRequest(si, me) > 0) {
  517. /* Start sending the email. */
  518. rsp = NutSmtpSendCommand(si, "DATA");
  519. /* Server wants us to go ahead? */
  520. if (rsp && *rsp == '3') {
  521. /* Send the major header lines. */
  522. NutSmtpSendMailHeader(si, me);
  523. fputs("\r\n", si->smtp_stream);
  524. /* If available, send the mail body's text. */
  525. if (me->mail_body) {
  526. NutSmtpSendEncodedLines(si, me->mail_body);
  527. }
  528. fputs(".\r\n", si->smtp_stream);
  529. fflush(si->smtp_stream);
  530. /* Check the result of the data transfer. */
  531. rsp = NutSmtpReceiveResponse(si);
  532. /* On success, update the status of the previously accepted
  533. recipients. */
  534. if (rsp && *rsp == '2') {
  535. uint_fast8_t i;
  536. for (i = 0; i < MAX_MAIL_RCPTS; i++) {
  537. if (me->mail_rcpt[i]) {
  538. if ((me->mail_rcpt_stat[i] & MAIL_RCPT_ACPT) == MAIL_RCPT_ACPT) {
  539. me->mail_rcpt_stat[i] |= MAIL_RCPT_SENT;
  540. }
  541. }
  542. }
  543. }
  544. }
  545. }
  546. return rsp;
  547. }
  548. /*@}*/