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