Echo Writes Code

email-tls-and-me-backflipping-into-a-faceplant-part-1.md

E-mail, TLS, and Me: Backflipping into a Faceplant (Part 1)

So. I decided to set up a contact form a few days ago. I wrote all the HTML, figured out how to make it look nice on mobile and desktop, rigged up the route in web-pylon, and... hm.

How... how do you send an email??

Scouting other implementations

I've worked on a few projects that send emails before. One of my first jobs had a heavily modified WordPress backend, so we had access to PHP's mail() function. Let's take a look at what that... does... oh.

/* {{{ php_mail */
PHPAPI bool php_mail(const char *to, const char *subject, const char *message, const char *headers, const char *extra_cmd)
{
	/* ... snip ... */

#ifdef PHP_WIN32
	sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL);
#else
	errno = 0;
	sendmail = popen(sendmail_cmd, "w");
#endif

	/* ... snip ... */
}

Okay, that's a little disappointing, but all that means is we have to go find the source for the sendmail command. From what I've read, that command can actually be one of a few different sendmail implementations, but on my machine, it's the Postfix version. I read this version for my research, but just know there are other sendmails running around out there.

So, let's check out sendmail.c in the Postfix source tree.

int main(int argc, char **argv)
{
	/* ... snip ... */

	case SM_MODE_ENQUEUE:
		if (site_to_flush) {
			if (argv[OPTIND])
				msg_fatal_status(EX_USAGE, "flush site requires no recipient");
			ext_argv = argv_alloc(2);
			argv_add(ext_argv, "postqueue", "-s", site_to_flush, (char *) 0);
			for (n = 0; n < msg_verbose; n++)
				argv_add(ext_argv, "-v", (char *) 0);
			argv_terminate(ext_argv);
			mail_run_replace(var_command_dir, ext_argv->argv);
			/* NOTREACHED */
		} else if (id_to_flush) {
			if (argv[OPTIND])
				msg_fatal_status(EX_USAGE, "flush queue_id requires no recipient");
			ext_argv = argv_alloc(2);
			argv_add(ext_argv, "postqueue", "-i", id_to_flush, (char *) 0);
			for (n = 0; n < msg_verbose; n++)
				argv_add(ext_argv, "-v", (char *) 0);
			argv_terminate(ext_argv);
			mail_run_replace(var_command_dir, ext_argv->argv);
			/* NOTREACHED */
		} else {
			enqueue(flags, encoding, dsn_envid, dsn_ret, dsn_notify,
					rewrite_context, sender, full_name, argv + OPTIND);
			exit(0);
			/* NOTREACHED */
		}
		break;

	/* ... snip ... */
}

We could follow the enqueue() function, which looks like the only one that does anything, but I have a more interesting idea. Let's just run it with strace and see what kind of system calls it makes.

Digging through system calls

# echo test | strace -s 1024 -o sendmail.log --follow-forks --output-separately sendmail nobody@example.com

I'll spare you the full traces (you can obtain them easily if you run the same command as above), but I discovered the following interesting things:

  • The initial sendmail process does a decent amount of work, but never actually sends the email anywhere; instead, it opens a socket, then clone()s itself. It then performs a bit of cleanup and finally wait4()s on the other process it spawned.
  • The child process very quickly execve()s itself into /usr/sbin/postdrop. This definitely sounds like a mail program! It then chdir()s into /var/spool/postfix.
  • If we search for test or nobody@example.com, we can see that postdrop reads the message, recipient, and some other information from standard input, then immediately writes it to a file called maildrop/nnnnnn.nnnnnnn (n is just some digits). It then connects to a Unix socket called public/pickup, writes the sequence "W\0" to it, and then closes it.

So. All that, and it turns out all that happens is we write some stuff to a file, ping a control socket, and let somebody else take care of it.