smtp_client_misc.c
Go to the documentation of this file.
1 /**
2  * @file smtp_client_misc.c
3  * @brief Helper functions for SMTP client
4  *
5  * @section License
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  *
9  * Copyright (C) 2010-2019 Oryx Embedded SARL. All rights reserved.
10  *
11  * This file is part of CycloneTCP Open.
12  *
13  * This program is free software; you can redistribute it and/or
14  * modify it under the terms of the GNU General Public License
15  * as published by the Free Software Foundation; either version 2
16  * of the License, or (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program; if not, write to the Free Software Foundation,
25  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26  *
27  * @author Oryx Embedded SARL (www.oryx-embedded.com)
28  * @version 1.9.4
29  **/
30 
31 //Switch to the appropriate trace level
32 #define TRACE_LEVEL SMTP_TRACE_LEVEL
33 
34 //Dependencies
35 #include <stdlib.h>
36 #include <ctype.h>
37 #include "core/net.h"
38 #include "smtp/smtp_client.h"
40 #include "smtp/smtp_client_misc.h"
41 #include "str.h"
42 #include "debug.h"
43 
44 //Check TCP/IP stack configuration
45 #if (SMTP_CLIENT_SUPPORT == ENABLED)
46 
47 
48 /**
49  * @brief Update SMTP client state
50  * @param[in] context Pointer to the SMTP client context
51  * @param[in] newState New state to switch to
52  **/
53 
55  SmtpClientState newState)
56 {
57  //Switch to the new state
58  context->state = newState;
59 
60  //Save current time
61  context->timestamp = osGetSystemTime();
62 }
63 
64 
65 /**
66  * @brief Send SMTP command and wait for a reply
67  * @param[in] context Pointer to the SMTP client context
68  * @param[in] callback Optional callback to parse each line of the reply
69  * @return Error code
70  **/
71 
73  SmtpClientReplyCallback callback)
74 {
75  error_t error;
76  size_t n;
77  bool_t more;
78  char_t *reply;
79 
80  //Initialize status code
81  error = NO_ERROR;
82 
83  //Point to the server's response
84  reply = context->buffer;
85 
86  //Send SMTP command and wait for the SMTP reply to be received
87  while(!error)
88  {
89  //Send SMTP command
90  if(context->bufferPos < context->commandLen)
91  {
92  //Send more data
93  error = smtpClientSendData(context,
94  context->buffer + context->bufferPos,
95  context->commandLen - context->bufferPos, &n, 0);
96 
97  //Check status code
98  if(error == NO_ERROR || error == ERROR_TIMEOUT)
99  {
100  //Advance data pointer
101  context->bufferPos += n;
102  }
103  }
104  else
105  {
106  //Determine whether more data should be collected
107  if(context->replyLen != 0 && reply[context->replyLen - 1] == '\n')
108  more = FALSE;
109  else if(context->replyLen == (SMTP_CLIENT_BUFFER_SIZE - 1))
110  more = FALSE;
111  else
112  more = TRUE;
113 
114  //Receive SMTP response
115  if(more)
116  {
117  //Receive more data
118  error = smtpClientReceiveData(context,
119  context->buffer + context->replyLen,
120  SMTP_CLIENT_BUFFER_SIZE - 1 - context->replyLen,
122 
123  //Check status code
124  if(error == NO_ERROR)
125  {
126  //Advance data pointer
127  context->replyLen += n;
128  }
129  }
130  else
131  {
132  //Properly terminate the response with a NULL character
133  reply[context->replyLen] = '\0';
134 
135  //Remove trailing whitespace from the response
136  strRemoveTrailingSpace(reply);
137 
138  //Debug message
139  TRACE_DEBUG("SMTP server: %s\r\n", reply);
140 
141  //All replies begin with a three digit numeric code
142  if(isdigit((uint8_t) reply[0]) &&
143  isdigit((uint8_t) reply[1]) &&
144  isdigit((uint8_t) reply[2]))
145  {
146  //A space character follows the response code for the last line
147  if(reply[3] == ' ' || reply[3] == '\0')
148  {
149  //Retrieve SMTP reply code
150  context->replyCode = strtoul(reply, NULL, 10);
151 
152  //A valid SMTP response has been received
153  break;
154  }
155  else
156  {
157  //Any callback function defined?
158  if(callback)
159  {
160  //Parse intermediary line
161  error = callback(context, reply);
162  }
163 
164  //Flush receive buffer
165  context->replyLen = 0;
166  }
167  }
168  else
169  {
170  //Ignore incorrectly formatted lines
171  context->replyLen = 0;
172  }
173  }
174  }
175  }
176 
177  //Return status code
178  return error;
179 }
180 
181 
182 /**
183  * @brief Format SMTP command
184  * @param[in] context Pointer to the SMTP client context
185  * @param[in] command NULL-terminated string containing the SMTP command
186  * @param[in] argument NULL-terminated string containing the argument
187  * @return Error code
188  **/
189 
191  const char_t *command, const char_t *argument)
192 {
193  //Check SMTP command name
194  if(!strcasecmp(command, "MAIL FROM") ||
195  !strcasecmp(command, "RCPT TO"))
196  {
197  //Check whether the address is valid
198  if(argument != NULL)
199  {
200  //Format MAIL FROM or RCPT TO command
201  sprintf(context->buffer, "%s: <%s>\r\n", command, argument);
202  }
203  else
204  {
205  //A null return path is accepted
206  sprintf(context->buffer, "%s: <>\r\n", command);
207  }
208 
209  //Debug message
210  TRACE_DEBUG("SMTP client: %s", context->buffer);
211  }
212  else if(!strcasecmp(command, "."))
213  {
214  //SMTP indicates the end of the mail data by sending a line containing
215  //only a "." (refer to RFC 5321, section 3.3)
216  sprintf(context->buffer, "\r\n.\r\n");
217 
218  //Debug message
219  TRACE_DEBUG("%s", context->buffer);
220  }
221  else
222  {
223  //The argument is optional
224  if(argument != NULL)
225  {
226  //Format SMTP command
227  sprintf(context->buffer, "%s %s\r\n", command, argument);
228  }
229  else
230  {
231  //Format SMTP command
232  sprintf(context->buffer, "%s\r\n", command);
233  }
234 
235  //Debug message
236  TRACE_DEBUG("SMTP client: %s", context->buffer);
237  }
238 
239  //Calculate the length of the SMTP command
240  context->commandLen = strlen(context->buffer);
241 
242  //Flush receive buffer
243  context->bufferPos = 0;
244  context->replyLen = 0;
245 
246  //Successful processing
247  return NO_ERROR;
248 }
249 
250 
251 /**
252  * @brief Parse EHLO response
253  * @param[in] context SMTP client context
254  * @param[in] replyLine Response line
255  * @return Error code
256  **/
257 
259  char_t *replyLine)
260 {
261  char_t *p;
262  char_t *token;
263 
264  //The line must be at least 4 characters long
265  if(strlen(replyLine) < 4)
266  return ERROR_INVALID_SYNTAX;
267 
268  //Skip the response code and the separator
269  replyLine += 4;
270 
271  //Get the first keyword
272  token = strtok_r(replyLine, " ", &p);
273  //Check whether the response line is empty
274  if(token == NULL)
275  return ERROR_INVALID_SYNTAX;
276 
277  //The response to EHLO is a multiline reply. Each line of the response
278  //contains a keyword
279  if(!strcasecmp(token, "STARTTLS"))
280  {
281  //The STARTTLS keyword is used to tell the SMTP client that the
282  //SMTP server allows use of TLS
283  context->startTlsSupported = TRUE;
284  }
285  else if(!strcasecmp(token, "AUTH"))
286  {
287  //The AUTH keyword contains a space-separated list of names of
288  //available authentication mechanisms
289  token = strtok_r(NULL, " ", &p);
290 
291  //Parse the list of keywords
292  while(token != NULL)
293  {
294  //Check the name of the authentication mechanism
295  if(!strcasecmp(token, "LOGIN"))
296  {
297  //LOGIN authentication mechanism is supported
298  context->authLoginSupported = TRUE;
299  }
300  else if(!strcasecmp(token, "PLAIN"))
301  {
302  //PLAIN authentication mechanism is supported
303  context->authPlainSupported = TRUE;
304  }
305  else if(!strcasecmp(token, "CRAM-MD5"))
306  {
307  //CRAM-MD5 authentication mechanism is supported
308  context->authCramMd5Supported = TRUE;
309  }
310  else
311  {
312  //Unknown authentication mechanism
313  }
314 
315  //Get the next keyword
316  token = strtok_r(NULL, " ", &p);
317  }
318  }
319  else
320  {
321  //Discard unknown keywords
322  }
323 
324  //Successful processing
325  return NO_ERROR;
326 }
327 
328 
329 /**
330  * @brief Format email header
331  * @param[in] context Pointer to the SMTP client context
332  * @param[in] from Email address of the sender
333  * @param[in] recipients Email addresses of the recipients
334  * @param[in] numRecipients Number of email addresses in the list
335  * @param[in] subject NULL-terminsated string containing the email subject
336  * @return Error code
337  **/
338 
340  const SmtpMailAddr *from, const SmtpMailAddr *recipients,
341  uint_t numRecipients, const char_t *subject)
342 {
343  char_t *p;
344  uint_t i;
345  uint_t type;
346  bool_t first;
347 
348  //Point to the buffer
349  p = context->buffer;
350 
351  //Valid sender address?
352  if(from->addr != NULL)
353  {
354  //Valid friendly name?
355  if(from->name && from->name[0] != '\0')
356  {
357  //A friendly name may be associated with the sender address
358  p += sprintf(p, "From: \"%s\" <%s>\r\n", from->name, from->addr);
359  }
360  else
361  {
362  //Format sender address
363  p += sprintf(p, "From: %s\r\n", from->addr);
364  }
365  }
366 
367  //Process TO, CC and BCC recipients
369  {
370  //Loop through the list of recipients
371  for(first = TRUE, i = 0; i < numRecipients; i++)
372  {
373  //Ensure the current email address is valid
374  if(recipients[i].addr != NULL)
375  {
376  //Check recipient type
377  if(recipients[i].type == type)
378  {
379  //The first item of the list requires special handling
380  if(first)
381  {
382  //Check recipient type
383  if(type == SMTP_ADDR_TYPE_TO)
384  {
385  //List of recipients
386  p += sprintf(p, "To: ");
387  }
388  else if(type == SMTP_ADDR_TYPE_CC)
389  {
390  //List of recipients which should get a carbon copy (CC)
391  //of the message
392  p += sprintf(p, "Cc: ");
393  }
394  else if(type == SMTP_ADDR_TYPE_BCC)
395  {
396  //List of recipients which should get a blind carbon copy
397  //(BCC) of the message
398  p += sprintf(p, "Bcc: ");
399  }
400  else
401  {
402  //Invalid recipient type
404  }
405  }
406  else
407  {
408  //The addresses are comma-separated
409  p += sprintf(p, ", ");
410  }
411 
412  //Valid friendly name?
413  if(recipients[i].name && recipients[i].name[0] != '\0')
414  {
415  //A friendly name may be associated with the address
416  p += sprintf(p, "\"%s\" <%s>", recipients[i].name,
417  recipients[i].addr);
418  }
419  else
420  {
421  //Add the email address to the list of recipients
422  p += sprintf(p, "%s", recipients[i].addr);
423  }
424 
425  //The current recipient is valid
426  first = FALSE;
427  }
428  }
429  }
430 
431  //Any recipients found?
432  if(!first)
433  {
434  //Terminate the line with a CRLF sequence
435  p += sprintf(p, "\r\n");
436  }
437  }
438 
439  //Valid subject?
440  if(subject != NULL && subject[0] != '\0')
441  {
442  //Format email subject
443  p += sprintf(p, "Subject: %s\r\n", subject);
444  }
445 
446 #if (SMTP_CLIENT_MIME_SUPPORT == ENABLED)
447  //Valid content type?
448  if(context->contentType[0] != '\0')
449  {
450  //The presence of this header field indicates the message is
451  //MIME-formatted
452  p += sprintf(p, "MIME-Version: 1.0\r\n");
453 
454  //Check whether multipart encoding is being used
455  if(!strncasecmp(context->contentType, "multipart/", 10))
456  {
457  //This Content-Type header field defines the boundary string
458  p += sprintf(p, "Content-Type: %s; boundary=%s\r\n",
459  context->contentType, context->boundary);
460  }
461  else
462  {
463  //This Content-Type header field indicates the media type of the
464  //message content, consisting of a type and subtype
465  p += sprintf(p, "Content-Type: %s\r\n", context->contentType);
466  }
467  }
468 #endif
469 
470  //The header and the body are separated by an empty line
471  sprintf(p, "\r\n");
472 
473  //Debug message
474  TRACE_DEBUG("%s", context->buffer);
475 
476  //Save the length of the header
477  context->bufferLen = strlen(context->buffer);
478  context->bufferPos = 0;
479 
480  //Successful processing
481  return NO_ERROR;
482 }
483 
484 
485 /**
486  * @brief Format multipart header
487  * @param[in] context Pointer to the SMTP client context
488  * @param[in] filename NULL-terminated string that holds the file name
489  * (optional parameter)
490  * @param[in] contentType NULL-terminated string that holds the content type
491  * (optional parameter)
492  * @param[in] contentTransferEncoding NULL-terminated string that holds the
493  * content transfer encoding (optional parameter)
494  * @param[in] last This flag indicates whether the multipart header is the
495  * final one
496  * @return Error code
497  **/
498 
500  const char_t *filename, const char_t *contentType,
501  const char_t *contentTransferEncoding, bool_t last)
502 {
503 #if (SMTP_CLIENT_MIME_SUPPORT == ENABLED)
504  char_t *p;
505 
506  //Point to the buffer
507  p = context->buffer;
508 
509  //Check whether the multipart header is the final one
510  if(!last)
511  {
512  //The encapsulation boundary is defined as a line consisting entirely
513  //of two hyphen characters followed by the boundary parameter value
514  //from the Content-Type header field. The encapsulation boundary must
515  //occur at the beginning of a line
516  p += sprintf(p, "\r\n--%s\r\n", context->boundary);
517 
518  //Valid file name?
519  if(filename != NULL && filename[0] != '\0')
520  {
521  //The Content-Disposition header field specifies the presentation style
522  p += sprintf(p, "Content-Disposition: attachment; filename=\"%s\"\r\n",
523  filename);
524  }
525 
526  //Valid content type?
527  if(contentType != NULL && contentType[0] != '\0')
528  {
529  //This Content-Type header field indicates the media type of the
530  //message content, consisting of a type and subtype
531  p += sprintf(p, "Content-Type: %s\r\n", contentType);
532  }
533 
534  //Valid content transfer encoding?
535  if(contentTransferEncoding != NULL && contentTransferEncoding[0] != '\0')
536  {
537  //The Content-Transfer-Encoding header can be used for representing
538  //binary data in formats other than ASCII text format
539  p += sprintf(p, "Content-Transfer-Encoding: %s\r\n",
540  contentTransferEncoding);
541 
542  //Base64 encoding?
543  if(!strcasecmp(contentTransferEncoding, "base64"))
544  {
545  context->base64Encoding = TRUE;
546  }
547  }
548  }
549  else
550  {
551  //The encapsulation boundary following the last body part is a
552  //distinguished delimiter that indicates that no further body parts
553  //will follow. Such a delimiter is identical to the previous
554  //delimiters, with the addition of two more hyphens at the end of
555  //the line
556  p += sprintf(p, "\r\n--%s--\r\n", context->boundary);
557  }
558 
559  //Terminate the multipart header with an empty line
560  sprintf(p, "\r\n");
561 
562  //Debug message
563  TRACE_DEBUG("%s", context->buffer);
564 
565  //Save the length of the header
566  context->bufferLen = strlen(context->buffer);
567  context->bufferPos = 0;
568 
569  //Successful processing
570  return NO_ERROR;
571 #else
572  //MIME extension is not implemented
573  return ERROR_NOT_IMPLEMENTED;
574 #endif
575 }
576 
577 
578 /**
579  * @brief Determine whether a timeout error has occurred
580  * @param[in] context Pointer to the SMTP client context
581  * @return Error code
582  **/
583 
585 {
586 #if (NET_RTOS_SUPPORT == DISABLED)
587  error_t error;
588  systime_t time;
589 
590  //Get current time
591  time = osGetSystemTime();
592 
593  //Check whether the timeout has elapsed
594  if(timeCompare(time, context->timestamp + context->timeout) >= 0)
595  {
596  //Report a timeout error
597  error = ERROR_TIMEOUT;
598  }
599  else
600  {
601  //The operation would block
602  error = ERROR_WOULD_BLOCK;
603  }
604 
605  //Return status code
606  return error;
607 #else
608  //Report a timeout error
609  return ERROR_TIMEOUT;
610 #endif
611 }
612 
613 #endif
SmtpClientState
SMTP client states.
Definition: smtp_client.h:193
char_t filename[]
Definition: tftp_common.h:91
uint32_t systime_t
Definition: compiler_port.h:46
#define timeCompare(t1, t2)
Definition: os_port.h:42
char char_t
Definition: compiler_port.h:43
error_t smtpClientFormatMailHeader(SmtpClientContext *context, const SmtpMailAddr *from, const SmtpMailAddr *recipients, uint_t numRecipients, const char_t *subject)
Format email header.
Helper functions for SMTP client.
systime_t osGetSystemTime(void)
Retrieve system time.
uint32_t time
TCP/IP stack core.
Debugging facilities.
uint8_t p
Definition: ndp.h:298
char_t * name
Definition: smtp_client.h:237
Invalid parameter.
Definition: error.h:47
Transport protocol abstraction layer.
String manipulation helper functions.
char_t type
#define strtok_r(str, delim, p)
error_t smtpClientSendCommand(SmtpClientContext *context, SmtpClientReplyCallback callback)
Send SMTP command and wait for a reply.
error_t smtpClientCheckTimeout(SmtpClientContext *context)
Determine whether a timeout error has occurred.
#define TRUE
Definition: os_port.h:50
uint16_t first
Definition: ipv4_frag.h:95
#define strcasecmp
error_t(* SmtpClientReplyCallback)(SmtpClientContext *context, char_t *replyLine)
Multiline reply parsing callback function.
Definition: smtp_client.h:214
char_t * addr
Definition: smtp_client.h:238
char_t name[]
uint16_t last
Definition: ipv4_frag.h:96
error_t smtpClientReceiveData(SmtpClientContext *context, void *data, size_t size, size_t *received, uint_t flags)
Receive data using the relevant transport protocol.
SMTP client (Simple Mail Transfer Protocol)
error_t smtpClientFormatCommand(SmtpClientContext *context, const char_t *command, const char_t *argument)
Format SMTP command.
Email address.
Definition: smtp_client.h:235
#define SmtpClientContext
Definition: smtp_client.h:156
Success.
Definition: error.h:44
error_t smtpClientFormatMultipartHeader(SmtpClientContext *context, const char_t *filename, const char_t *contentType, const char_t *contentTransferEncoding, bool_t last)
Format multipart header.
error_t smtpClientParseEhloReply(SmtpClientContext *context, char_t *replyLine)
Parse EHLO response.
error_t
Error codes.
Definition: error.h:42
unsigned int uint_t
Definition: compiler_port.h:45
uint8_t token[]
Definition: coap_common.h:183
#define strncasecmp
error_t smtpClientSendData(SmtpClientContext *context, const void *data, size_t length, size_t *written, uint_t flags)
Send data using the relevant transport protocol.
#define SMTP_CLIENT_BUFFER_SIZE
Definition: smtp_client.h:88
uint8_t n
#define FALSE
Definition: os_port.h:46
int bool_t
Definition: compiler_port.h:49
void strRemoveTrailingSpace(char_t *s)
Removes all trailing whitespace from a string.
Definition: str.c:117
Ipv4Addr addr
Definition: nbns_common.h:121
void smtpClientChangeState(SmtpClientContext *context, SmtpClientState newState)
Update SMTP client state.
#define TRACE_DEBUG(...)
Definition: debug.h:106