Minimalist C SMTP Mail Server
For one of my projects, I thought it would be really useful for my home file server to be able to receive emails from public email addresses. This way I would be able to send emails from any contemporary email address to some magic “something@kwf.dyndns.org” email address, and have it be received on my server. This is different than how most people are used to interacting with their email, because most people expect to have something like example@gmail.com, and then expect Google to deal with keeping a server on all the time to handle incoming mail (via SMTP), so that the user only needs to use a mail agent (such as Outlook or Gmail’s web interface) to interact with email when they feel like it.
The problem is that, while digging through all of the documentation for Postfix and Sendmail, which are the programs one would typically use to set up these email backends, is that they do way more than I need and are likewise much more complicated than I need (as they should be - email is a hard thing). My project really only needs the most basic of mail processing; receive an email, figure out which account it is addressed to, and extract the subject line and actual body of the email. What I plan on doing with it once I have these things becomes quite a bit easier, so I really only want an email server which can handle the network exchange and then intelligently punt received emails to different programs to do with as they please.
Many moons ago, I happened upon the very interesting Mailinator blog. Mailinator is a rather useful service providing you with disposable email addresses, but what I most enjoy about it is Paul’s very interesting technical articles on the trials of writing your own high-performance custom-application mail server. In one of his early articles, he really impressed on the fact that SMTP (Simple Mail Transfer Protocol) is actually a really simple protocol, so I figured that if I think Postfix is too complicated for what I need, why don’t I just write my very own mail server from scratch?
SMTP, like the vast majority of protocols used throughout the Internet, has been freely documented in a series of documents called RFCs, or “Request for Comments.” Granted, just because something exists as an RFC does not mean that it is actually used, but it’s a good place to start looking for how the Internet works. The basics of SMTP happen to be documented in RFC821, which is a somewhat intimidating 67 pages long. It is a running joke that actually trying to read RFCs is a nightmare, and the jokes are mostly correct; reading 67 pages on the gripping subject of how mail is transferred between mail servers is simply not an exciting exercise. On the other hand, once I managed to get 400 lines of C to handle enough of SMTP to receive mail and print it on the screen, that was pretty cool.
How SMTP Works
SMTP really is a very simple protocol. Given an email that needs to be delivered to another server, your mail server connects to port 25 on the remote server, and then trades four letter verbs for three digit return codes. For example, when an exchange first starts, the connecting server sends a “HELO” verb, and the remote server typically responds with a “250” code indicating the completion of the verb. Once a connection is established, moving emails is nothing more than a long series of these back and forth until there’s no more work to be done.
The Wikipedia article and the RFC both give simple exchange examples, and I will supplement those with one of my own, captured with my very own SMTP server:
smtp_exchange.txt
./ccsmtpd[3519]: Starting thread for socket #5
220 kwf.dyndns.org SMTP CCSMTP
C5: EHLO mail-ey0-f180.google.com
S5: 502 Command not implemented
C5: HELO mail-ey0-f180.google.com
S5: 250 Ok
C5: MAIL FROM:<kennethfinnegan2007@gmail.com>
S5: 250 Ok
C5: RCPT TO:<anothermailbox@kwf.dyndns.org>
S5: 250 Ok
C5: RCPT TO:<anymailbox@kwf.dyndns.org>
S5: 250 Ok
C5: DATA
S5: 354 Continue
C5: Received: by eaai12 with SMTP id i12so1751180eaa.25
C5: for <multiple recipients>; Mon, 11 Jun 2012 14:01:01 -0700 (PDT)
C5: DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
C5: d=gmail.com; s=20120113;
C5: h=mime-version:date:message-id:subject:from:to:content-type;
C5: bh=HADX3ZJBWSSmr7lEkeRdPC7BDfJJc+HvRZksFUNNwEw=;
C5: b=OOQN2io8TBNSZhdQL6EyL9F5JVeASiiGwdUAcuwWyOvNyxHxfvUTAzFN9X/tYDeYpj
C5: HrVLiprOaidWTWU3m4E2gAEPdx7IFcKVFWca+Vx1m07GkhgRtUQ6QU3BGEGNx0McvVX3
C5: NcxP9CSAO38G7cqJXjpA7LFXUAyKmgEkOtQmDzI5hFpt84dZ5Bj/5x5HfnOGZrs/jcSZ
C5: WmfMGwClUDVx1jPI/EAfn/noA2POoV4VudVqokgNMif+yIndsblpAHTRsdlIPvqlQcRX
C5: oWKr+z5b8FVcPYbJKGJPQ+UmLAtrrBzcuU50/F3VEeGsQHBgHN1eGR2Xs5dBt5OuNuju
C5: Q2Ww==
C5: MIME-Version: 1.0
C5: Received: by 10.14.188.129 with SMTP id a1mr6710045een.10.1339448461633; Mon,
C5: 11 Jun 2012 14:01:01 -0700 (PDT)
C5: Received: by 10.14.98.66 with HTTP; Mon, 11 Jun 2012 14:01:01 -0700 (PDT)
C5: Date: Mon, 11 Jun 2012 14:01:01 -0700
C5: Message-ID: <CAFS5k-hX3c3YcKhxmg=6rM-VDHY4fkqhju4uEeGA9gu=FEdymw@mail.gmail.com>
C5: Subject: Subject Line
C5: From: Kenneth Finnegan <kennethfinnegan2007@gmail.com>
C5: To: anymailbox@kwf.dyndns.org, anothermailbox@kwf.dyndns.org
C5: Content-Type: text/plain; charset=ISO-8859-1
C5:
C5: Contents of the email.
C5:
C5: Kenneth Finnegan
C5: .
S5: 250 Ok
C5: QUIT
S5: 221 Ok
The Source Code
Now let us be very clear here; I am not posting this 400 line SMTP server in the hopes that you find it useful as a mail server. I very much hope that I do not ever actually see a copy of this running in the wild, and here is why: this is not a good SMTP server.
It just isn’t. It is a rather neat little toy to play with, and I think it is a very educational example of the basis for how SMTP works, Posix threads, and network programming. That is all I think it is good for, at this point. It isn’t done yet, because I have built in almost no safe-guards with respect to remote servers doing anything naughty or unexpected. There is no limits on bandwidth usage, RAM usage, number of connections opened at once, nothing. That being said, all of those things would make this quite a bit longer, and muddle the basics of exactly how it works. I thought you may find this academic example interesting, but dear God don’t treat it as anything except an academic example.
That all being said, here is 409 lines of C which can manage to receive basic SMTP transfers from other mail hosts. It does crap out on emails with attachments for some reason that I don’t quite understand, but this project is not even close to finished, but simply at a good point for me to take pause for your benefit. Source code:
ccsmtp.c
// Command and Control Simple Mail Transport Protocol Server [CCSMTP]
// Kenneth Finnegan, GPLv2 - 2012
// kennethfinnegan.blogspot.com
//
// This is a VERY simple smtp daemon which implements the bare minimum
// required of it to receive email messages directed at it from other
// mail servers.
//
// It currently does no processing to the received email short of simply
// printing it to the terminal one line at a time.
// Attachments seem to blow its mind...
//
// WARNING: THIS IS NOTHING MORE THAN A TOY DAEMON!
//
// For sake of simplicity, many standard counter-measures were not
// implemented to protect this server from a variety of attacks.
// This means that for an attacker to disable this server or your
// entire computer is a trivial affair.
//
// If you're reading this looking for a useful SMTP server to deploy
// on your network, you are looking at the wrong thing!
// You should probably be looking at Sendmail or Postfix.
//
// This code should be seen as nothing more than an educational toy.
// Implementing all of the required checks for a robust network service
// are left as an educational excersize for the reader. No attempt has
// been made to sandbox the server or prevent it from using excessive
// system resources in the name of responding to client requests.
//
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <time.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
// Port 25 redirected to 2525 through firewall
// or change this define to instead be "25"
#define PORT "2525"
// Specify the domain being served by this server
// Ideally this is a config argument and not a const
#define DOMAIN "kwf.dyndns.org"
#define BACKLOG_MAX (10)
#define BUF_SIZE 4096
#define STREQU(a,b) (strcmp(a, b) == 0)
// Linked list of ints container
struct int_ll {
int d;
struct int_ll *next;
};
// Overall server state
struct {
struct int_ll *sockfds;
int sockfd_max;
char *domain;
pthread_t thread; // Latest spawned thread
} state;
// Function prototypes
void init_socket(void);
void *handle_smtp (void *thread_arg);
void *get_in_addr(struct sockaddr *sa);
// M M A IIIIIII N N
// MM MM A A I NN N
// M M M M A A I N N N
// M M M A A I N N N
// M M AAAAAAA I N N N
// M M A A I N N N
// M M A A I N N N
// M M A A I N NN
// M M A A IIIIIII N N
int main (int argc, char *argv[]) {
int rc, i, j;
char strbuf[INET6_ADDRSTRLEN];
// Init syslog with program prefix
char *syslog_buf = (char*) malloc(1024);
sprintf(syslog_buf, "%s", argv[0]);
openlog(syslog_buf, LOG_PERROR | LOG_PID, LOG_USER);
// This would be more useful as an argument
state.domain = DOMAIN;
// Open sockets to listen on for client connections
init_socket();
// Loop forever listening for connections and spawning
// threads to handle each exchange via handle_smtp()
while (1) {
fd_set sockets;
FD_ZERO(&sockets);
struct int_ll *p;
for (p = state.sockfds; p != NULL; p = p->next) {
FD_SET(p->d, &sockets);
}
// Wait forever for a connection on any of the bound sockets
select (state.sockfd_max+1, &sockets, NULL, NULL, NULL);
// Iterate through the sockets looking for one with a new connection
for (p = state.sockfds; p != NULL; p = p->next) {
if (FD_ISSET(p->d, &sockets)) {
struct sockaddr_storage client_addr;
socklen_t sin_size = sizeof(client_addr);
int new_sock = accept (p->d, \
(struct sockaddr*) &client_addr, &sin_size);
if (new_sock == -1) {
syslog(LOG_ERR, "Accepting client connection failed");
continue;
}
// Convert client IP to human-readable
void *client_ip = get_in_addr(\
(struct sockaddr *)&client_addr);
inet_ntop(client_addr.ss_family, \
client_ip, strbuf, sizeof(strbuf));
syslog(LOG_DEBUG, "Connection from %s", strbuf);
// Pack the socket file descriptor into dynamic mem
// to be passed to thread; it will free this when done.
int * thread_arg = (int*) malloc(sizeof(int));
*thread_arg = new_sock;
// Spawn new thread to handle SMTP exchange
pthread_create(&(state.thread), NULL, \
handle_smtp, thread_arg);
}
}
} // end forever loop
return 0;
}
// SSS OOO CCC K K EEEEEEE TTTTTTT
// SS SS O O CC CC K K E T
// S O O CC C K K E T
// SS O O C K K E T
// SSS O O C KK EEEE T
// SS O O C K K E T
// S O O CC C K K E T
// SS SS O O CC CC K K E T
// SSS OOO CCC K K EEEEEEE T
//
// Try to bind to as many local sockets as available.
// Typically this would just be one IPv4 and one IPv6 socket
void init_socket(void) {
int rc, i, j, yes = 1;
int sockfd;
struct addrinfo hints, *hostinfo, *p;
// Set up the hints indicating all of localhost's sockets
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
state.sockfds = NULL;
state.sockfd_max = 0;
rc = getaddrinfo(NULL, PORT, &hints, &hostinfo);
if (rc != 0) {
syslog(LOG_ERR, "Failed to get host addr info");
exit(EXIT_FAILURE);
}
for (p=hostinfo; p != NULL; p = p->ai_next) {
void *addr;
char ipstr[INET6_ADDRSTRLEN];
if (p->ai_family == AF_INET) {
addr = &((struct sockaddr_in*)p->ai_addr)->sin_addr;
} else {
addr = &((struct sockaddr_in6*)p->ai_addr)->sin6_addr;
}
inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) {
syslog(LOG_NOTICE, "Failed to create IPv%d socket", \
(p->ai_family == AF_INET) ? 4 : 6 );
continue;
}
setsockopt(sockfd, SOL_SOCKET, \
SO_REUSEADDR, &yes, sizeof(int));
rc = bind(sockfd, p->ai_addr, p->ai_addrlen);
if (rc == -1) {
close (sockfd);
syslog(LOG_NOTICE, "Failed to bind to IPv%d socket", \
(p->ai_family == AF_INET) ? 4 : 6 );
continue;
}
rc = listen(sockfd, BACKLOG_MAX);
if (rc == -1) {
syslog(LOG_NOTICE, "Failed to listen to IPv%d socket", \
(p->ai_family == AF_INET) ? 4 : 6 );
exit(EXIT_FAILURE);
}
// Update highest fd value for select()
(sockfd > state.sockfd_max) ? (state.sockfd_max = sockfd) : 1;
// Add new socket to linked list of sockets to listen to
struct int_ll *new_sockfd = malloc(sizeof(struct int_ll));
new_sockfd->d = sockfd;
new_sockfd->next = state.sockfds;
state.sockfds = new_sockfd;
}
if (state.sockfds == NULL) {
syslog(LOG_ERR, "Completely failed to bind to any sockets");
exit(EXIT_FAILURE);
}
freeaddrinfo(hostinfo);
return;
}
// SSS M M TTTTTTT PPPP
// SS SS MM MM T P PP
// S M M M M T P PP
// SS M M M T P PP
// SSS M M T PPPP
// SS M M T P
// S M M T P
// SS SS M M T P
// SSS M M T P
//
// This is typically spawned as a new thread for each exchange
// to handle the actual SMTP conversation with each client.
void *handle_smtp (void *thread_arg) {
syslog(LOG_DEBUG, "Starting thread for socket #%d", *(int*)thread_arg);
int rc, i, j;
char buffer[BUF_SIZE], bufferout[BUF_SIZE];
int buffer_offset = 0;
buffer[BUF_SIZE-1] = '\0';
// Unpack dynamic mem argument from main()
int sockfd = *(int*)thread_arg;
free(thread_arg);
// Flag for being inside of DATA verb
int inmessage = 0;
sprintf(bufferout, "220 %s SMTP CCSMTP\r\n", state.domain);
printf("%s", bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
while (1) {
fd_set sockset;
struct timeval tv;
FD_ZERO(&sockset);
FD_SET(sockfd, &sockset);
tv.tv_sec = 120; // Some SMTP servers pause for ~15s per message
tv.tv_usec = 0;
// Wait tv timeout for the server to send anything.
select(sockfd+1, &sockset, NULL, NULL, &tv);
if (!FD_ISSET(sockfd, &sockset)) {
syslog(LOG_DEBUG, "%d: Socket timed out", sockfd);
break;
}
int buffer_left = BUF_SIZE - buffer_offset - 1;
if (buffer_left == 0) {
syslog(LOG_DEBUG, "%d: Command line too long", sockfd);
sprintf(bufferout, "500 Too long\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
buffer_offset = 0;
continue;
}
rc = recv(sockfd, buffer + buffer_offset, buffer_left, 0);
if (rc == 0) {
syslog(LOG_DEBUG, "%d: Remote host closed socket", sockfd);
break;
}
if (rc == -1) {
syslog(LOG_DEBUG, "%d: Error on socket", sockfd);
break;
}
buffer_offset += rc;
char *eol;
// Only process one line of the received buffer at a time
// If multiple lines were received in a single recv(), goto
// back to here for each line
//
processline:
eol = strstr(buffer, "\r\n");
if (eol == NULL) {
syslog(LOG_DEBUG, "%d: Haven't found EOL yet", sockfd);
continue;
}
// Null terminate each line to be processed individually
eol[0] = '\0';
if (!inmessage) { // Handle system verbs
printf("C%d: %s\n", sockfd, buffer);
// Replace all lower case letters so verbs are all caps
for (i=0; i<4; i++) {
if (islower(buffer[i])) {
buffer[i] += 'A' - 'a';
}
}
// Null-terminate the verb for strcmp
buffer[4] = '\0';
// Respond to each verb accordingly.
// You should replace these with more meaningful
// actions than simply printing everything.
//
if (STREQU(buffer, "HELO")) { // Initial greeting
sprintf(bufferout, "250 Ok\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
} else if (STREQU(buffer, "MAIL")) { // New mail from...
sprintf(bufferout, "250 Ok\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
} else if (STREQU(buffer, "RCPT")) { // Mail addressed to...
sprintf(bufferout, "250 Ok recipient\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
} else if (STREQU(buffer, "DATA")) { // Message contents...
sprintf(bufferout, "354 Continue\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
inmessage = 1;
} else if (STREQU(buffer, "RSET")) { // Reset the connection
sprintf(bufferout, "250 Ok reset\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
} else if (STREQU(buffer, "NOOP")) { // Do nothing.
sprintf(bufferout, "250 Ok noop\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
} else if (STREQU(buffer, "QUIT")) { // Close the connection
sprintf(bufferout, "221 Ok\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
break;
} else { // The verb used hasn't been implemented.
sprintf(bufferout, "502 Command Not Implemented\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
}
} else { // We are inside the message after a DATA verb.
printf("C%d: %s\n", sockfd, buffer);
if (STREQU(buffer, ".")) { // A single "." signifies the end
sprintf(bufferout, "250 Ok\r\n");
printf("S%d: %s", sockfd, bufferout);
send(sockfd, bufferout, strlen(bufferout), 0);
inmessage = 0;
}
}
// Shift the rest of the buffer to the front
memmove(buffer, eol+2, BUF_SIZE - (eol + 2 - buffer));
buffer_offset -= (eol - buffer) + 2;
// Do we already have additional lines to process? If so,
// commit a horrid sin and goto the line processing section again.
if (strstr(buffer, "\r\n"))
goto processline;
}
// All done. Clean up everything and exit.
close(sockfd);
pthread_exit(NULL);
}
// Extract the address from sockaddr depending on which family of socket it is
void * get_in_addr(struct sockaddr *sa) {
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}