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