With a basic understanding of both SMTP and the email format, we are ready to program a simple email client. Our client takes as inputs: the destination email server, the recipient's address, the sender's address, the email subject line, and the email body text.
Our program begins by including the necessary headers with the following statements:
/*smtp_send.c*/
#include "chap08.h"
#include <ctype.h>
#include <stdarg.h>
We also define the following two constants to make buffer allocation and checking easier:
/*smtp_send.c continued*/
#define MAXINPUT 512
#define MAXRESPONSE 1024
Our program needs to prompt the user for input several times. This is required to get the email server's hostname, the recipient's address, and so on. C provides the gets() function for this purpose but gets() is deprecated in the latest C standard. Therefore, we implement our own function.
The following function, get_input(), prompts for user input:
/*smtp_send.c continued*/
void get_input(const char *prompt, char *buffer)
{
printf("%s", prompt);
buffer[0] = 0;
fgets(buffer, MAXINPUT, stdin);
const int read = strlen(buffer);
if (read > 0)
buffer[read-1] = 0;
}
The get_input() function uses fgets() to read from stdin. The buffer passed to get_input() is assumed to be MAXINPUT bytes, which we defined at the top of the file.
The fgets() function does not remove a newline character from the received input; therefore, we overwrite the last character inputted with a terminating null character.
It is also very helpful to have a function that can send formatted strings directly over the network. We implement a function called send_format() for this purpose. It takes a socket, a formatting string, and the additional arguments to send. You can think of send_format() as being very similar to printf(). The difference is that send_format() delivers the formatted text over the network instead of printing to the screen.
The code for send_format() is as follows:
/*smtp_send.c continued*/
void send_format(SOCKET server, const char *text, ...) {
char buffer[1024];
va_list args;
va_start(args, text);
vsprintf(buffer, text, args);
va_end(args);
send(server, buffer, strlen(buffer), 0);
printf("C: %s", buffer);
}
The preceding code works by first reserving a buffer. vsprintf() is then used to format the text into that buffer. It is up to the caller to ensure that the formatted output doesn't exceed the reserved buffer space. We are assuming for this program that the user is trusted, but in a production program, you would want to add checks to prevent a buffer overflow here.
After the output text is formatted into buffer, it is sent using send(). We also print the sent text to the screen. A C: is printed preceding it to indicate that the text was sent by us, the client.
One of the trickier parts of our SMTP client is parsing the SMTP server responses. This is important because the SMTP client must not issue a second command until a response is received for the first command. If the SMTP client sends a new command before the server is ready, then the server will likely terminate the connection.
Recall that each SMTP response starts with a three-digit code. We want to parse out this code to check for errors. Each SMTP response is usually followed by text that we ignore. SMTP responses are typically only one line long, but they can sometimes span multiple lines. In this case, each line up to the penultimate line contains a dash character, -, directly following the three-digit response code.
To illustrate how multiline responses work, consider the following two responses as equivalent:
/*response 1*/
250 Message received!
/*response 2*/
250-Message
250 received!
It is important that our program recognizes multiline responses; it must not mistakenly treat a single multiline response as separate responses.
We implement a function called parse_response() for this purpose. It takes in a null-terminated response string and returns the parsed response code. If no code is found or the response isn't complete, then 0 is returned instead. The code for this function is as follows:
/*smtp_send.c continued*/
int parse_response(const char *response) {
const char *k = response;
if (!k[0] || !k[1] || !k[2]) return 0;
for (; k[3]; ++k) {
if (k == response || k[-1] == '\n') {
if (isdigit(k[0]) && isdigit(k[1]) && isdigit(k[2])) {
if (k[3] != '-') {
if (strstr(k, "\r\n")) {
return strtol(k, 0, 10);
}
}
}
}
}
return 0;
}
The parse_response() function begins by checking for a null terminator in the first three characters of the response. If a null is found there, then the function can return immediately because response isn't long enough to constitute a valid SMTP response.
It then loops through the response input string. The loop goes until a null-terminating character is found three characters out. Each loop, isdigit() is used to see whether the current character and the next two characters are all digits. If so, the fourth character, k[3], is checked. If k[3] is a dash, then the response continues onto the next line. However, if k[3] isn't a dash, then k[0] represents the beginning of the last line of the SMTP response. In this case, the code checks if the line ending has been received; strstr() is used for this purpose. It the line ending was received, the code uses strtol() to convert the response code to an integer.
If the code loops through response() without returning, then 0 is returned, and the client needs to wait for more input from the SMTP server.
With parse_response() out of the way, it is useful to have a function that waits until a particular response code is received over the network. We implement a function called wait_on_response() for this purpose, which begins as follows:
/*smtp_send.c continued*/
void wait_on_response(SOCKET server, int expecting) {
char response[MAXRESPONSE+1];
char *p = response;
char *end = response + MAXRESPONSE;
int code = 0;
In the preceding code, a response buffer variable is reserved for storing the SMTP server's response. A pointer, p, is set to the beginning of this buffer; p will be incremented to point to the end of the received data, but it starts at response since no data has been received yet. An end pointer variable is set to the end of the buffer, which is useful to ensure we do not attempt to write past the buffer end.
Finally, we set code = 0 to indicate that no response code has been received yet.
The wait_on_response() function then continues with a loop as follows:
/*smtp_send.c continued*/
do {
int bytes_received = recv(server, p, end - p, 0);
if (bytes_received < 1) {
fprintf(stderr, "Connection dropped.\n");
exit(1);
}
p += bytes_received;
*p = 0;
if (p == end) {
fprintf(stderr, "Server response too large:\n");
fprintf(stderr, "%s", response);
exit(1);
}
code = parse_response(response);
} while (code == 0);
The beginning of the preceding loop uses recv() to receive data from the SMTP server. The received data is written at point p in the response array. We are careful to use end to make sure received data isn't written past the end of response.
After recv() returns, p is incremented to the end of the received data, and a null terminating character is set. A check for p == end ensures that we haven't written to the end of the response buffer.
Our function from earlier, parse_response(), is used to check whether a full SMTP response has been received. If so, then code is set to that response. If not, then code is equal to 0, and the loop continues to receive additional data.
After the loop terminates, the wait_on_response() function checks that the received SMTP response code is as expected. If so, the received data is printed to the screen, and the function returns. The code for this is as follows:
/*smtp_send.c continued*/
if (code != expecting) {
fprintf(stderr, "Error from server:\n");
fprintf(stderr, "%s", response);
exit(1);
}
printf("S: %s", response);
}
That concludes the wait_on_response() function. This function proves very useful, and it is needed after every command sent to the SMTP server.
We also define a function called connect_to_host(), which attempts to open a TCP connection to a given hostname and port number. This function is extremely similar to the code we've used in the previous chapters.
First getaddrinfo() is used to resolve the hostname and getnameinfo() is then used to print the server IP address. The following code achieves those two purposes:
/*smtp_send.c continued*/
SOCKET connect_to_host(const char *hostname, const char *port) {
printf("Configuring remote address...\n");
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *peer_address;
if (getaddrinfo(hostname, port, &hints, &peer_address)) {
fprintf(stderr, "getaddrinfo() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
printf("Remote address is: ");
char address_buffer[100];
char service_buffer[100];
getnameinfo(peer_address->ai_addr, peer_address->ai_addrlen,
address_buffer, sizeof(address_buffer),
service_buffer, sizeof(service_buffer),
NI_NUMERICHOST);
printf("%s %s\n", address_buffer, service_buffer);
A socket is then created with socket(), as shown in the following code:
/*smtp_send.c continued*/
printf("Creating socket...\n");
SOCKET server;
server = socket(peer_address->ai_family,
peer_address->ai_socktype, peer_address->ai_protocol);
if (!ISVALIDSOCKET(server)) {
fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
Once the socket has been created, connect() is used to establish the connection. The following code shows the use of connect() and the end of the connect_to_host() function:
/*smtp_send.c continued*/
printf("Connecting...\n");
if (connect(server,
peer_address->ai_addr, peer_address->ai_addrlen)) {
fprintf(stderr, "connect() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
freeaddrinfo(peer_address);
printf("Connected.\n\n");
return server;
}
Don't forget to call freeaddrinfo() to free the memory allocated for the server address, as shown by the preceding code.
Finally, with those helper functions out of the way, we can begin on main(). The following code defines main() and initializes Winsock if required:
/*smtp_send.c continued*/
int main() {
#if defined(_WIN32)
WSADATA d;
if (WSAStartup(MAKEWORD(2, 2), &d)) {
fprintf(stderr, "Failed to initialize.\n");
return 1;
}
#endif
See Chapter 2, Getting to Grips with Socket APIs, for more information about initializing Winsock and establishing connections.
Our program can proceed by prompting the user for an SMTP hostname. This hostname is stored in hostname, and our connect_to_host() function is used to open a connection. The following code shows this:
/*smtp_send.c continued*/
char hostname[MAXINPUT];
get_input("mail server: ", hostname);
printf("Connecting to host: %s:25\n", hostname);
SOCKET server = connect_to_host(hostname, "25");
After the connection is established, our SMTP client must not issue any commands until the server responds with a 220 code. We use wait_on_response() to wait for this with the following code:
/*smtp_send.c continued*/
wait_on_response(server, 220);
Once the server is ready to receive commands, we must issue the HELO command. The following code sends the HELO command and waits for a 250 response code:
/*smtp_send.c continued*/
send_format(server, "HELO HONPWC\r\n");
wait_on_response(server, 250);
HELO should be followed by the SMTP client's hostname; however, since we are probably running this client from a development machine, it's likely we don't have a hostname setup. For this reason, we simply send HONPWC, although any arbitrary string can be used. If you are running this client from a server, then you should change the HONPWC string to a domain that points to your server.
Also, note the line ending used in the preceding code. The line ending used by SMTP is a carriage return character followed by a line feed character. In C, this is represented by "\r\n".
Our program then prompts the user for the sending and receiving addresses and issues the appropriate SMTP commands. This is done using get_input() to prompt the user, send_format() to issue the SMTP commands, and wait_on_response() to receive the SMTP server's response:
/*smtp_send.c continued*/
char sender[MAXINPUT];
get_input("from: ", sender);
send_format(server, "MAIL FROM:<%s>\r\n", sender);
wait_on_response(server, 250);
char recipient[MAXINPUT];
get_input("to: ", recipient);
send_format(server, "RCPT TO:<%s>\r\n", recipient);
wait_on_response(server, 250);
After the sender and receiver are specified, the next step in the SMTP is to issue the DATA command. The DATA command instructs the server to listen for the actual email. It is issued by the following code:
/*smtp_send.c continued*/
send_format(server, "DATA\r\n");
wait_on_response(server, 354);
Our client program then prompts the user for an email subject line. After the subject line is specified, it can send the email headers: From, To, and Subject. The following code does this:
/*smtp_send.c continued*/
char subject[MAXINPUT];
get_input("subject: ", subject);
send_format(server, "From:<%s>\r\n", sender);
send_format(server, "To:<%s>\r\n", recipient);
send_format(server, "Subject:%s\r\n", subject);
It is also useful to add a date header. Emails use a special format for dates. We can make use of the strftime() function to format the date properly. The following code formats the date into the proper email header:
/*smtp_send.c continued*/
time_t timer;
time(&timer);
struct tm *timeinfo;
timeinfo = gmtime(&timer);
char date[128];
strftime(date, 128, "%a, %d %b %Y %H:%M:%S +0000", timeinfo);
send_format(server, "Date:%s\r\n", date);
In the preceding code, the time() function is used to get the current date and time, and gmtime() is used to convert it into a timeinfo struct. Then, strftime() is called to format the data and time into a temporary buffer, date. This formatted string is then transmitted to the SMTP server as an email header.
After the email headers are sent, the email body is delineated by a blank line. The following code sends this blank line:
/*smtp_send.c continued*/
send_format(server, "\r\n");
We can then prompt the user for the body of the email using get_input(). The body is transmitted one line at a time. When the user finishes their email, they should enter a single period on a line by itself. This indicates both to our client and the SMTP server that the email is finished.
The following code sends user input to the server until a single period is inputted:
/*smtp_send.c continued*/
printf("Enter your email text, end with \".\" on a line by itself.\n");
while (1) {
char body[MAXINPUT];
get_input("> ", body);
send_format(server, "%s\r\n", body);
if (strcmp(body, ".") == 0) {
break;
}
}
If the mail was accepted by the SMTP server, it sends a 250 response code. Our client then issues the QUIT command and checks for a 221 response code. The 221 response code indicates that the connection is terminating as shown in the following code:
/*smtp_send.c continued*/
wait_on_response(server, 250);
send_format(server, "QUIT\r\n");
wait_on_response(server, 221);
Our SMTP client concludes by closing the socket, cleaning up Winsock (if required), and exiting as shown here:
/*smtp_send.c continued*/
printf("\nClosing socket...\n");
CLOSESOCKET(server);
#if defined(_WIN32)
WSACleanup();
#endif
printf("Finished.\n");
return 0;
}
That concludes smtp_send.c.
You can compile and run smtp_send.c on Windows with MinGW using the following:
gcc smtp_send.c -o smtp_send.exe -lws2_32
smtp_send.exe
On Linux or macOS, compiling and running smtp_send.c is done by the following:
gcc smtp_send.c -o smtp_send
./smtp_send
The following screenshot shows the sending of a simple email using smtp_send.c:
If you're doing a lot of testing, you may find it tedious to type in the email each time. In that case, you can automate it by putting your text in a file and using the cat utility to read it into smtp_send. For example, you may have the email.txt file as follows:
/*email.txt*/
mail-server.example.net
bob@example.com
alice@example.net
Re: The Cake
Hi Alice,
What about the cake then?
Bob
.
With the program input stored in email.txt, you can send an email using the following command:
cat email.txt | ./smtp_send
Hopefully, you can send some test emails with smtp_send. There are, however, a few obstacles you may run into. Your ISP may block outgoing emails from your connection, and many email servers do not accept mail from residential IP address blocks. See the Spam-blocking pitfalls section later in the chapter for more information.
Although smtp_send is useful for sending simple text-based messages, you may be wondering how to add formatting to your email. Perhaps you want to send files as attachments. The next section addresses these issues.