blob: 9efc12e0861637ab2aa9c4d2c3670ac6b9da5b9d [file]
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MAX_LINE 4096
#define MAX_STACK 10
int do_clean_hdr = 0;
int do_quote = 0; /* -q: prefix each body line with '> ' */
char *clen_file = NULL; /* if set, write the produced body length here */
long tot_body_bytes = 0; /* number of body bytes emitted for the current message */
int at_bol = 1; /* next body byte starts a new line (for quoting) */
/* emit a single byte as body content, counting it for the Content-Length. With
* -q each body line is prefixed with '> ', or a bare '>' when the line is empty
* (so no trailing space is produced). The prefix bytes are counted too, so the
* reported length stays exact. A CR or LF at the start of a line marks an empty
* line; the prefix is emitted lazily on the first byte of each line.
*/
void put_body_ch(int c)
{
if (do_quote && at_bol) {
if (c == '\n' || c == '\r') {
putchar('>');
tot_body_bytes++;
} else {
fputs("> ", stdout);
tot_body_bytes += 2;
}
at_bol = 0;
}
putchar(c);
tot_body_bytes++;
if (c == '\n')
at_bol = 1;
}
/* emit a string as body content (see put_body_ch() for the accounting) */
void put_body_str(const char *s)
{
while (*s)
put_body_ch((unsigned char)*s++);
}
/* extract the boundary from <in> which must start at "boundary=" into <store>
* of size <size>. It takes care of quoted strings.
*/
void store_boundary(const char *in, char *store, size_t size)
{
const char *end = in;
in += strlen("boundary=");
if (*in == '"')
in++;
for (end = in; *end && *end != '"' && *end > ' '; end++)
;
if (end - in >= size)
end = in + size - 1;
memcpy(store, in, end - in);
store[end - in] = '\0';
}
/* returns true if CR or LF */
int is_crlf(int ch)
{
return ch == '\r' || ch == '\n';
}
/* returns true if SP or HT */
int is_spht(int ch)
{
return ch == ' ' || ch == '\t';
}
/* Complete word <w> by decoding char <c>. Returns the number of bits emitted
* (either 0 or 6), or an error (-1) if the char is not a base64 one. If <bits>
* is not NULL, it's incremented by the number of emitted bits. If <ofs> is
* not NULL, it's incremented by one for valid chars.
*/
int b64dec(unsigned *word, int *bits, int *ofs, char c)
{
int nbbits = 6;
switch (c) {
case 'A'...'Z':
c += 0 - 'A';
break;
case 'a'...'z':
c += 26 - 'a';
break;
case '0'...'9':
c += 52 - '0';
break;
case '+':
c = 62;
break;
case '/':
c = 63;
break;
case '=':
/* end of block, no more byte to emit */
nbbits = 0;
c = 0;
break;
default:
return -1;
}
*word = (*word << 6) + (unsigned char)c;
if (bits)
*bits += nbbits;
if (ofs)
(*ofs)++;
return nbbits;
}
/* returns the hex char value or -1 if not a hex char */
int h2i(char c)
{
return (c >= '0' && c <= '9') ? c - '0' :
(c >= 'a' && c <= 'f') ? c - 'a' + 10 :
(c >= 'A' && c <= 'F') ? c - 'A' + 10 :
-1;
}
/* Decode a single line of quoted-printable text and emits it on stdout. Note
* that the spec says the input cannot be longer than 76 chars. Accepted line
* endings are CR, LF or NUL.
*/
void decode_qp_line(const char *line)
{
int h, l, i;
for (i = 0; line[i] != '\r' && line[i] != '\n' && line[i] != '\0'; i++) {
if (line[i] != '=') {
put_body_ch(line[i]);
continue;
}
if ((h = h2i(line[i + 1])) >= 0 && (l = h2i(line[i + 2])) >= 0) {
put_body_ch((h << 4) + l);
i += 2;
}
else if (line[i + 1] == '\r' || line[i + 1] == '\n' || line[i + 1] == '\0') {
/* a lone '=' at the end of a line is a soft break: the
* line continues on the next one.
*/
return;
}
else {
/* probably an encoding issue, let's dump the '='. */
put_body_ch('=');
}
}
/* input is one line at a time, and soft break has already been handled */
put_body_ch('\n');
}
/* returns true if <hdr> starts with <start>, ignoring case */
int hdr_starts_with(const char *hdr, const char *start)
{
do {
if (!*start)
return 1;
} while (tolower(*hdr++) == tolower(*start++));
return 0;
}
/* reads a possibly multi-line header from <in> and returns it, or NULL if end
* reached. It requires 2 buffers, one for the currently assembled line, and
* one for the next one, both of size <size>. The caller is responsible for
* making sure that <next> is empty on the first call. It will return an empty
* string once the input is depleted.
*/
const char *read_hdr(FILE *in, char *curr, char *next, int size)
{
int ofs = 0;
int ret;
/* both are the same size, this fits */
ofs = snprintf(curr, size, "%s", next);
for (*next = 0;
fgets(next, size, in) && (!ofs || is_spht(*next)) && ofs < size;
*next = 0, ofs += ret) {
ret = snprintf(curr + ofs, size - ofs, "%s", next);
if (ret <= 0 || ret > (size - ofs))
break;
}
//printf("### returning curr=<%s> next=<%s>\n", curr, next);
return curr;
}
/* Tells whether the message body is over. <clen> is the number of body bytes
* still to consume according to the Content-Length header, or negative if the
* header was absent. With a Content-Length, the body ends exactly when the
* count is exhausted, which is robust against "From " lines inlined in the body
* (e.g. quoted mails or pasted patches). Without one, we fall back to the
* historical heuristic: the body ends at the next mbox "From " line.
*/
int body_done(long clen, const char *line)
{
if (clen >= 0)
return clen <= 0;
return strncmp(line, "From ", 5) == 0;
}
/* Account <line> as consumed body input against the Content-Length budget
* <*clen>. No-op when no Content-Length was present (<*clen> negative).
*/
void consume(long *clen, const char *line)
{
if (*clen >= 0)
*clen -= strlen(line);
}
/* If <line> is a MIME boundary delimiter, i.e. "--" followed by one of the
* boundaries currently on the stack (levels 0..<stack_ptr>), return its level,
* else -1. The deepest matching level is returned so that an outer boundary
* can close the inner levels it encloses.
*/
int boundary_level(const char *line, char boundaries[][MAX_LINE], int stack_ptr)
{
int lvl;
if (line[0] != '-' || line[1] != '-')
return -1;
for (lvl = stack_ptr; lvl >= 0; lvl--)
if (strncmp(line + 2, boundaries[lvl], strlen(boundaries[lvl])) == 0)
return lvl;
return -1;
}
/* True if <line> is the closing delimiter "--<boundary>--" rather than a plain
* part separator "--<boundary>". This does NOT verify that <line> matches
* <boundary> at all: it only inspects the two bytes right after the boundary.
* The caller must have already established, via boundary_level(), that <line>
* begins with "--" followed by <boundary>; otherwise the result is meaningless.
*/
int is_close_delim(const char *line, const char *boundary)
{
int n = strlen(boundary);
return line[2 + n] == '-' && line[2 + n + 1] == '-';
}
/* Emit the body of a leaf (non-multipart) MIME part, decoding base64 or
* quoted-printable as requested, and counting the produced bytes. Stops at a
* known boundary delimiter (levels 0..<level>), when the Content-Length budget
* <*clen> is exhausted, or at end of input.
*/
void dump_leaf(FILE *in, char *next, long *clen, char boundaries[][MAX_LINE],
int level, int is_base64, int is_qp)
{
unsigned base64_word = 0;
int base64_ofs = 0;
int bits = 0;
char *c;
while (*next) {
if (boundary_level(next, boundaries, level) >= 0)
break;
if (*clen >= 0 && *clen <= 0)
break;
consume(clen, next);
if (is_base64) {
/* decode and dump accumulated base64 bytes */
for (c = next; *c; c++) {
if (b64dec(&base64_word, &bits, &base64_ofs, *c) < 0)
continue;
if (base64_ofs == 4) {
if (bits >= 8)
put_body_ch((unsigned char)(base64_word >> 16));
if (bits >= 16)
put_body_ch((unsigned char)(base64_word >> 8));
if (bits >= 24)
put_body_ch((unsigned char)base64_word);
base64_ofs = 0;
bits = 0;
}
}
}
else if (is_qp) {
/* decode quoted printable */
decode_qp_line(next);
}
else
put_body_str(next);
if (!fgets(next, MAX_LINE, in)) {
*next = 0;
break;
}
}
}
/* Read one MIME part's headers from <in> into <hdrs> (of size <hdrs_size>),
* stopping after the blank line that ends them. Each header line is charged to
* the Content-Length budget <*clen>. The Content-Transfer-Encoding and a nested
* "Content-Type: multipart" line are interpreted but not stored, everything else
* (including the terminating blank line) is appended to <hdrs>. On return:
* *is_text_plain is set if the part is text/plain;
* *is_base64 / *is_qp reflect its transfer encoding;
* *is_nested is set if the part is itself multipart and there was stack room,
* in which case its boundary has been stored into boundaries[depth + 1].
*/
void read_part_hdrs(FILE *in, char *line, char *next, char *hdrs, int hdrs_size,
char boundaries[][MAX_LINE], int depth, long *clen,
int *is_text_plain, int *is_base64, int *is_qp, int *is_nested)
{
const char *boundary;
int len = 0;
hdrs[0] = 0;
*is_text_plain = *is_base64 = *is_qp = *is_nested = 0;
while (*read_hdr(in, line, next, MAX_LINE)) {
int ret;
consume(clen, line);
if (!*is_nested &&
hdr_starts_with(line, "Content-Type: multipart") &&
depth + 1 < MAX_STACK) {
boundary = strstr(line, "boundary=");
if (boundary) {
store_boundary(boundary, boundaries[depth + 1], MAX_LINE);
*is_nested = 1;
}
continue;
}
if (hdr_starts_with(line, "Content-Transfer-Encoding: base64")) {
*is_base64 = 1;
continue;
}
else if (hdr_starts_with(line, "Content-Transfer-Encoding: quoted-printable")) {
*is_qp = 1;
continue;
}
/* the rest is always appended */
ret = snprintf(hdrs + len, hdrs_size - len, "%s", line);
if (ret >= 0 && ret < hdrs_size - len)
len += ret;
if (is_crlf(line[0]))
break;
if (hdr_starts_with(line, "Content-Type: text/plain"))
*is_text_plain = 1;
}
}
/* Process the MIME parts at one multipart level, where boundaries[depth] is this
* level's boundary (already stored by the caller). Reads from the shared
* look-ahead, emits the headers and decoded body of the first text/plain leaf
* found anywhere in the subtree (setting *found), and drops every other part,
* recursing into nested multipart wrappers. Each consumed line is charged to
* <*clen>. Returns the level of the boundary delimiter that ended this level:
* <depth> - 1 for our own closing delimiter, or a smaller value for an ancestor
* delimiter that must keep unwinding; returns -1 once a part was emitted or the
* input/body is exhausted. <part_hdrs> is a caller-provided scratch buffer.
*/
int walk_level(FILE *in, char *line, char *next, char boundaries[][MAX_LINE],
int depth, char *part_hdrs, long *clen, int *found)
{
for (;;) {
int lvl, is_text_plain, is_base64, is_qp, is_nested, r;
if (*found || body_done(*clen, next))
return -1;
if (!*read_hdr(in, line, next, MAX_LINE))
return -1;
consume(clen, line);
have_line:
/* Anything that is not one of our (or an ancestor's) boundary
* delimiters is preamble or a skipped part's body: drop it.
*/
lvl = boundary_level(line, boundaries, depth);
if (lvl < 0)
continue;
if (lvl < depth)
return lvl; /* ancestor boundary: keep unwinding */
/* boundary_level() has already proven <line> is
* "--<boundaries[depth]>...". This is what tells us we hit a
* boundary at all. is_close_delim() does not re-check the
* boundary; it only looks at the bytes after it to tell a
* closing "--<boundary>--" from a separator "--<boundary>".
*/
if (is_close_delim(line, boundaries[depth]))
return depth - 1; /* our closing delimiter */
/* a separator "--<boundary>" at our level: a new part begins */
read_part_hdrs(in, line, next, part_hdrs, 4 * MAX_LINE, boundaries,
depth, clen, &is_text_plain, &is_base64, &is_qp, &is_nested);
if (is_nested) {
r = walk_level(in, line, next, boundaries, depth + 1,
part_hdrs, clen, found);
if (*found)
return -1;
if (r < depth)
return r; /* an ancestor's delimiter bubbled up */
/* <line> holds the delimiter that stopped the child;
* re-examine it at our level (our separator, our close,
* or a now-irrelevant inner close to drop past).
*/
goto have_line;
}
if (is_text_plain && !*found) {
/* dump its headers (so we keep the content-type and the
* transfer-encoding) followed by its decoded body.
*/
printf("%s", part_hdrs);
dump_leaf(in, next, clen, boundaries, depth, is_base64, is_qp);
*found = 1;
return -1;
}
/* a non-text leaf: its body is dropped by the loop above */
}
}
void process_mbox(FILE *in)
{
char line[MAX_LINE], next[MAX_LINE];
char boundaries[MAX_STACK][MAX_LINE];
int stack_ptr = -1;
int is_multipart = 0;
int found_and_dumped = 0;
const char *boundary;
char part_hdrs[4*MAX_LINE]; // should be sufficient for a few headers
int is_qp = 0;
long clen = -1; /* body bytes left to consume per Content-Length, or -1 */
while (*read_hdr(in, line, next, sizeof(line))) {
if (strncmp(line, "From ", 5) == 0) {
/* new mail */
printf("%s", line);
stack_ptr = -1;
is_multipart = 0;
is_qp = 0;
found_and_dumped = 0;
clen = -1;
tot_body_bytes = 0;
at_bol = 1;
/* 1. HEADER: capture and drop content-length, drop lines,
* and look for content-type. If multipart, we'll inspect
* attachments. We stop before the empty line. Note that
* CR/LF are part of the line here.
*/
while (*read_hdr(in, line, next, sizeof(line)) && !is_crlf(line[0])) {
if (hdr_starts_with(line, "Content-Length:")) {
/* keep the value to delimit the body, but
* don't emit it: the body we produce will
* have a different size.
*/
clen = atol(line + strlen("Content-Length:"));
continue;
}
if (hdr_starts_with(line, "Lines:"))
continue;
if (hdr_starts_with(line, "Content-Type: multipart")) {
/* the boundary is on this line */
is_multipart = 1;
boundary = strstr(line, "boundary=");
if (boundary && stack_ptr < MAX_STACK - 1) {
stack_ptr++;
store_boundary(boundary, boundaries[stack_ptr], sizeof(boundaries[stack_ptr]));
//printf("### boundaries[%d]=%s\n", stack_ptr, boundaries[stack_ptr]);
}
continue;
}
if (hdr_starts_with(line, "Content-Transfer-Encoding: quoted-printable")) {
is_qp = 1;
continue;
}
if (do_clean_hdr) {
if (hdr_starts_with(line, "Received"))
continue;
if (hdr_starts_with(line, "X-"))
continue;
if (hdr_starts_with(line, "Authentication-"))
continue;
if (hdr_starts_with(line, "ARC-"))
continue;
if (hdr_starts_with(line, "DKIM-"))
continue;
if (hdr_starts_with(line, "DMARC-"))
continue;
}
printf("%s", line);
}
/* We've reached the empty line, we're now inspecting
* the body. If not multipart, we dump everything and
* we're done till the next message. We have the next
* line in <next>. The empty line is not part of the body
* counted by Content-Length.
*/
if (!is_multipart) {
printf("%s", line);
while (*next && !body_done(clen, next)) {
consume(&clen, next);
if (is_qp) {
/* decode quoted printable */
decode_qp_line(next);
}
else
put_body_str(next);
if (!fgets(next, sizeof(next), in)) {
*next = 0;
break;
}
}
} else {
/* Multipart: descend through the parts, recursing into
* any nested multipart wrappers, and emit the first
* text/plain leaf found. We don't emit the empty line
* that ended the headers; if no text part is found we
* emit a lone blank line below. boundaries[0] holds the
* top-level boundary, stored while reading the headers.
*/
walk_level(in, line, next, boundaries, 0, part_hdrs,
&clen, &found_and_dumped);
/* If we found nothing, ensure a blank line exists */
if (!found_and_dumped)
printf("\n");
/* skip to end or next mail */
while (*next && !body_done(clen, next)) {
read_hdr(in, line, next, sizeof(line));
consume(&clen, line);
}
}
/* report the number of body bytes we produced, so the
* next stage of the pipeline can compute an accurate
* Content-Length for the rewritten message.
*/
if (clen_file) {
FILE *cf = fopen(clen_file, "w");
if (cf) {
fprintf(cf, "%ld\n", tot_body_bytes);
fclose(cf);
}
}
}
}
}
void print_usage(const char *prog, int rc)
{
fprintf(rc ? stderr : stdout,
"Usage: %s [-c] [-q] [-w FILE] [FILE]\n"
" -c drop noise headers (Received, X-*, DKIM/DMARC/ARC, ...)\n"
" -q quote the kept body with '> ' (e.g. for replies)\n"
" -w FILE write the produced body length, in bytes, to FILE\n"
" -h show this help\n"
"Reads an mbox from FILE or stdin, keeps text only, writes to stdout.\n",
prog);
exit(rc);
}
int main(int argc, char *argv[])
{
const char *prog_name = argv[0];
while (argc > 1 && argv[1][0] == '-') {
if (strcmp(argv[1], "-c") == 0) {
/* clean useless headers */
do_clean_hdr = 1;
argv++;
argc--;
} else if (strcmp(argv[1], "-q") == 0) {
/* quote the body with '> ' */
do_quote = 1;
argv++;
argc--;
} else if (strcmp(argv[1], "-w") == 0 && argc > 2) {
/* write the produced body length to this file */
clen_file = argv[2];
argv += 2;
argc -= 2;
} else if (strcmp(argv[1], "-h") == 0) {
print_usage(prog_name, 0);
} else {
break;
}
}
if (argc < 2)
process_mbox(stdin);
else {
FILE *f = fopen(argv[1], "r");
if (!f)
return 1;
process_mbox(f);
fclose(f);
}
return 0;
}