|
|
/*
* fmail - Mail formatter * Copyright (C) 2011 Damien Goutte-Gattat * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <locale.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <gpgme.h>
#include <magic.h>
#include <sbuffer.h>
#include <xmem.h>
/* Help and informations about the program. */
static void usage(int status) { puts("\
Usage: fmail [options]\n\ Read a mail from standard input and send it.\n");
puts("Options:\n\
-h, --help Display this help message.\n\ -v, --version Display the version message.\n\ ");
puts("\
-f, --footer FILE Include FILE as the mail footer.\n\ ");
puts("\
Headers options:\n\ -H, --header \"NAME: TEXT\"\n\
Add an arbitrary header.\n\ -F, --from TEXT Set the From: header.\n\ -T, --to TEXT Add a To: header.\n\ -C, --cc TEXT Add a Cc: header.\n\ -S, --subject TEXT Set the Subject: header.\n\ ");
puts("\
Attachments options:\n\ -a, --attach FILE Attach the specified file.\n\ ");
puts("\
Cryptography options:\n\ -s, --sign Sign the message.\n\ ");
printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
exit(status); }
static void info(void) { printf("\
fmail %s\n\ Copyright (C) 2011 Damien Goutte-Gattat\n\ \n\ This program is released under the GNU General Public License.\n\ See the COPYING file or <http://www.gnu.org/licenses/gpl.html>.\n\ ", VERSION);
exit(EXIT_SUCCESS); }
/* Helper functions. */
const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/";
typedef struct fmail_ctx { const char **attachments; size_t att_count; const char *footer; magic_t magic_ctx; gpgme_ctx_t signing_ctx; } fmail_ctx_t;
static char * generate_boundary(char *buffer, size_t len) { int i;
for ( i = 0; i < len - 1; i++ ) buffer[i] = base64_chars[rand() % (sizeof(base64_chars) - 1)]; buffer[i] = '\0';
return buffer; }
static void qp_encode_stream(FILE *in, FILE *out) { int c; unsigned n;
n = 0; while ( ! feof(in) ) {
c = fgetc(in);
if ( n >= 72 ) { fprintf(out, "=\r\n"); n = 0; }
if ( c == '\t' || c == ' ' ) { int next = fgetc(in);
if ( next == '\n' ) { fprintf(out, "=%02X", c); n += 3; } else { fputc(c, out); n += 1; } ungetc(next, in); } else if ( c == '=' ) { fprintf(out, "=%02X", c); n += 3; } else if ( isprint(c) ) { fputc(c, out); n += 1; } else if ( c == '\n' ) { fprintf(out, "\r\n"); n = 0; } else if ( c != EOF ) { fprintf(out, "=%02X", c); n += 3; } } }
static void base64_encode_stream(FILE *in, FILE *out) { unsigned char buffer[3]; int n, j;
j = 0; while ( (n = fread(buffer, 1, 3, in)) > 0 ) { char quartet[4];
if ( n < 3 ) buffer[2] = 0; if ( n < 2 ) buffer[1] = 0;
quartet[0] = base64_chars[buffer[0] >> 2]; quartet[1] = base64_chars[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xf0) >> 4)]; quartet[2] = (unsigned char) (n > 1 ? base64_chars[((buffer[1] & 0x0f) << 2) | ((buffer[2] & 0x0c) >> 6)] : '=' ); quartet[3] = (unsigned char) (n > 2 ? base64_chars[buffer[2] & 0x3f] : '=');
j += 1; fwrite(quartet, 1, 4, out); if ( j == 16 ) { fprintf(out, "\r\n"); j = 0; } } }
static char * rfc2822_date(void) { time_t timestamp; struct tm *timestruct; const char *rfc2822_format = "%a, %d %b %Y %H:%M:%S %z"; static char buffer[32];
timestamp = time(NULL); timestruct = localtime(×tamp); strftime(buffer, sizeof(buffer), rfc2822_format, timestruct);
return buffer; }
static void read_headers(FILE *in, string_buffer_t *headers) { int c, empty_line; size_t n;
n = empty_line = 0;
while ( ! empty_line ) { c = fgetc(in);
if ( c == '\n' ) { if ( n == 0 ) empty_line = 1; else { sb_add(headers, "\r\n"); n = 0; } } else { sb_addc(headers, c); n += 1; } } }
static void process_attachment(const char *, magic_t, FILE *);
static void process_text_body(fmail_ctx_t *ctx, FILE *in, FILE *out) { char boundary[32]; int i;
if ( ctx->att_count > 0 ) { generate_boundary(boundary, sizeof(boundary)); fprintf(out, "Content-Type: multipart/mixed;\r\n" " boundary=\"%s\"\r\n" "\r\n" "--%s\r\n", boundary, boundary); }
fprintf(out, "Content-Type: text/plain; charset=\"utf-8\"\r\n" "Content-Transfer-Encoding: quoted-printable\r\n" "Content-Disposition: inline\r\n\r\n"); qp_encode_stream(in, out); fprintf(out, "\r\n");
if ( ctx->footer ) { FILE *f;
if ( ! (f = fopen(ctx->footer, "r")) ) err(EXIT_FAILURE, "cannot open footer file '%s'", ctx->footer);
fprintf(out, "-- \r\n"); qp_encode_stream(f, out); fprintf(out, "\r\n"); fclose(f); }
for ( i = 0; i < ctx->att_count; i++ ) { fprintf(out, "--%s\r\n", boundary); process_attachment(ctx->attachments[i], ctx->magic_ctx, out);
if ( i == ctx->att_count - 1 ) fprintf(out, "--%s--\r\n", boundary); } }
/* Signing stuff. */
gpgme_ctx_t initialize_gpgme(void) { gpgme_ctx_t ctx; gpg_error_t gerr;
gpgme_check_version(NULL); gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL)); gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL));
if ( (gerr = gpgme_new(&ctx)) != GPG_ERR_NO_ERROR ) errx(EXIT_FAILURE, "cannot initialize GPGME: %s", gpgme_strerror(gerr));
gpgme_set_armor(ctx, 1);
return ctx; }
static void sign_stream(gpgme_ctx_t ctx, FILE *in, FILE *out) { gpgme_data_t gin, gout; char boundary[32], buffer[512]; int n;
gpgme_data_new_from_stream(&gin, in); gpgme_data_new(&gout);
gpgme_op_sign(ctx, gin, gout, GPGME_SIG_MODE_DETACH);
generate_boundary(boundary, sizeof(boundary)); fprintf(out, "Content-Type: multipart/signed;\r\n" " boundary=\"%s\";\r\n" " protocol=\"application/pgp-signature\";\r\n" " micalg=pgp-sha1\r\n" "\r\n" "--%s\r\n", boundary, boundary);
fseek(in, 0, SEEK_SET); while ( (n = fread(buffer, 1, sizeof(buffer), in)) > 0 ) fwrite(buffer, 1, n, out);
fprintf(out, "\r\n" "--%s\r\n" "Content-Type: application/pgp-signature; name=signature.asc\r\n" "\r\n", boundary);
gpgme_data_seek(gout, 0, SEEK_SET); while ( (n = gpgme_data_read(gout, buffer, sizeof(buffer))) > 0 ) { int i = 0;
while ( i < n ) { if ( buffer[i] == '\n' ) fputc('\r', out); fputc(buffer[i], out);
i += 1; } }
fprintf(out, "\r\n--%s--\r\n", boundary);
gpgme_data_release(gin); gpgme_data_release(gout); }
/* Attachments stuff. */
static magic_t initialize_magic(void) { magic_t ctx;
if ( ! (ctx = magic_open(MAGIC_SYMLINK | MAGIC_MIME)) ) err(EXIT_FAILURE, "cannot obtain libmagic cookie");
if ( magic_load(ctx, NULL) == -1 ) err(EXIT_FAILURE, "cannot load default magic database");
return ctx; }
static void process_attachment(const char *filename, magic_t ctx, FILE *out) { const char *mime, *last_eq; int binary; FILE *f;
mime = magic_file(ctx, filename); last_eq = strrchr(mime, '='); binary = strcmp(last_eq, "=binary") ? 0 : 1;
fprintf(out, "Content-Type: %s\r\n", mime); fprintf(out, "Content-Transfer-Encoding: %s\r\n", binary ? "base64" : "quoted-printable"); fprintf(out, "Content-Disposition: attachment; filename=%s\r\n\r\n", filename);
f = fopen(filename, "r"); if ( binary ) base64_encode_stream(f, out); else qp_encode_stream(f, out); fclose(f);
fprintf(out, "\r\n"); }
/* Main function. */
int main(int argc, char *argv[]) { char c; string_buffer_t *headers; fmail_ctx_t ctx;
struct option options[] = { { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'v' }, { "sign", 0, NULL, 's' }, { "attach", 1, NULL, 'a' }, { "footer", 1, NULL, 'f' }, { "header", 1, NULL, 'H' }, { "from", 1, NULL, 'F' }, { "to", 1, NULL, 'T' }, { "cc", 1, NULL, 'C' }, { "subject", 1, NULL, 'S' }, { NULL, 0, NULL, 0 } };
setprogname(argv[0]); setlocale(LC_ALL, ""); srand(time(NULL));
headers = sb_new(0); ctx.signing_ctx = NULL; ctx.magic_ctx = initialize_magic(); ctx.attachments = NULL; ctx.att_count = 0; ctx.footer = NULL;
while ( (c = getopt_long(argc, argv, "hvsa:f:H:F:T:C:S:", options, NULL)) != -1 ) { switch ( c ) { case 'h': usage(EXIT_SUCCESS); break;
case '?': usage(EXIT_FAILURE); break;
case 'v': info(); break;
case 's': ctx.signing_ctx = initialize_gpgme(); break;
case 'a': if ( ctx.att_count % 10 == 0 ) ctx.attachments = xrealloc(ctx.attachments, ctx.att_count + 10); ctx.attachments[ctx.att_count++] = optarg; break;
case 'f': ctx.footer = optarg; break;
case 'H': sb_addf(headers, "%s\r\n", optarg); break;
case 'F': sb_addf(headers, "From: %s\r\n", optarg); break;
case 'T': sb_addf(headers, "To: %s\r\n", optarg); break;
case 'C': sb_addf(headers, "Cc: %s\r\n", optarg); break;
case 'S': sb_addf(headers, "Subject: %s\r\n", optarg); break; } }
/* Generate automatic headers */ sb_addf(headers, "Date: %s\r\n", rfc2822_date()); sb_addf(headers, "User-Agent: fmail %s\r\n", VERSION);
/* Read user-provided headers. */ read_headers(stdin, headers);
/* Write all headers. */ fprintf(stdout, "%s", sb_get(headers));
if ( ctx.signing_ctx ) { FILE *tmp = tmpfile();
process_text_body(&ctx, stdin, tmp); fseek(tmp, 0, SEEK_SET); sign_stream(ctx.signing_ctx, tmp, stdout);
fclose(tmp); gpgme_release(ctx.signing_ctx); } else process_text_body(&ctx, stdin, stdout);
if ( ctx.attachments ) free(ctx.attachments);
magic_close(ctx.magic_ctx);
return EXIT_SUCCESS; }
|