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