/* * fmail - Mail formatter * Copyright (C) 2011,2018 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 . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Help and informations about the program. */ static void usage(int status) { puts("\ Usage: fmail [options]\n\ Read a message from standard input and format it\n\ for submission to a mail submission agent.\n"); puts("Options:\n\ -h, --help Display this help message.\n\ -v, --version Display the version message.\n\ "); puts("\ -e, --edit Fire an editor to type mail body\n\ instead of reading it from\n\ standard input.\n\ -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\ -U, --user-agent Add a User-Agent: header.\n\ -D, --date Add a Date: header with the current date.\n\ "); puts("\ Attachments options:\n\ -a, --attach FILE Attach the specified file.\n\ "); puts("\ Cryptography options:\n\ -s, --sign Sign the message.\n\ -E, --encrypt RECP Encrypt for the specified recipient.\n\ "); printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT); exit(status); } static void info(void) { printf("\ fmail %s\n\ Copyright (C) 2018 Damien Goutte-Gattat\n\ \n\ This program is released under the GNU General Public License.\n\ See the COPYING file or .\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 **recipients; size_t rcp_count; const char *footer; magic_t magic_ctx; gpgme_ctx_t crypto_ctx; FILE *input; } fmail_ctx_t; static char * generate_boundary(char *buffer, size_t len) { unsigned 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 if ( c == EOF ) { /* EOF before reaching the end of headers, abort. */ errx(EXIT_FAILURE, "cannot read mail headers"); } 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 *out) { char boundary[32]; unsigned 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(ctx->input, 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); } } /* Crypto 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 const char * hash_algo_to_string(gpgme_hash_algo_t algo) { switch ( algo ) { case GPGME_MD_MD5: return "pgp-md5"; case GPGME_MD_SHA1: return "pgp-sha1"; case GPGME_MD_RMD160: return "pgp-ripemd160"; case GPGME_MD_MD2: return "pgp-md2"; case GPGME_MD_TIGER: return "pgp-tiger192"; case GPGME_MD_HAVAL: return "pgp-haval-5-160"; case GPGME_MD_SHA256: return "pgp-sha256"; case GPGME_MD_SHA384: return "pgp-sha384"; case GPGME_MD_SHA512: return "pgp-sha512"; case GPGME_MD_SHA224: return "pgp-sha224"; case GPGME_MD_MD4: return "pgp-md4"; case GPGME_MD_CRC32: return "pgp-crc32"; case GPGME_MD_CRC32_RFC1510: return "pgp-crc32-rfc1510"; case GPGME_MD_CRC24_RFC2440: return "pgp-crc24-rfc2440"; case GPGME_MD_NONE: return ""; } return ""; /* FIXME: What to do here? */ } static void sign_stream(gpgme_ctx_t ctx, FILE *in, FILE *out) { gpgme_data_t gin, gout; gpgme_sign_result_t result; gpgme_error_t gerr; char boundary[32], buffer[512]; int n; gpgme_data_new_from_stream(&gin, in); gpgme_data_new(&gout); gerr = gpgme_op_sign(ctx, gin, gout, GPGME_SIG_MODE_DETACH); if ( gerr != GPG_ERR_NO_ERROR ) errx(EXIT_FAILURE, "signing failed: %s", gpgme_strerror(gerr)); result = gpgme_op_sign_result(ctx); generate_boundary(boundary, sizeof(boundary)); fprintf(out, "Content-Type: multipart/signed;\r\n" " boundary=\"%s\";\r\n" " protocol=\"application/pgp-signature\";\r\n" " micalg=%s\r\n" "\r\n" "--%s\r\n", boundary, hash_algo_to_string(result->signatures->hash_algo), 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); } static gpgme_key_t * get_recipient_keys(gpgme_ctx_t ctx, const char **recipients, size_t nr) { gpgme_key_t *keys, key; gpgme_error_t gerr; int i, j; keys = NULL; for ( i = j = gerr = 0; i < nr && ! gerr ; i++ ) { gerr = gpgme_op_keylist_start (ctx, recipients[i], 0); while ( ! gerr ) { gerr = gpgme_op_keylist_next (ctx, &key); if ( gerr ) break; if ( j % 10 == 0 ) keys = xrealloc(keys, j + 10); keys[j++] = key; } if ( gpgme_err_code(gerr) == GPG_ERR_EOF ) gerr = 0; } if ( gerr != 0 && gpgme_err_code(gerr) != GPG_ERR_EOF) errx(EXIT_FAILURE, "cannot get recipient keys: %s", gpgme_strerror(gerr)); /* GpgME expects a NULL-terminated array of keys. */ if ( j % 10 == 0 ) keys = xrealloc(keys, j + 1); keys[j++] = NULL; return keys; } static void encrypt_stream(gpgme_ctx_t ctx, FILE *in, FILE *out, const char **recipients, size_t nr) { gpgme_data_t gin, gout; gpgme_key_t *keys, *key; gpgme_error_t gerr; char boundary[32], buffer[512]; int n; keys = get_recipient_keys(ctx, recipients, nr); gpgme_data_new_from_stream(&gin, in); gpgme_data_new(&gout); gerr = gpgme_op_encrypt(ctx, keys, 0, gin, gout); if ( gerr != GPG_ERR_NO_ERROR ) errx(EXIT_FAILURE, "encrypting failed: %s", gpgme_strerror(gerr)); generate_boundary(boundary, sizeof(boundary)); fprintf(out, "Content-Type: multipart/encrypted;\r\n" " boundary=\"%s\";\r\n" " protocol=\"application/pgp-encrypted\r\n" "\r\n" "--%s\r\n" "Content-Type: application/pgp-encrypted\r\n" "\r\n" "Version: 1\r\n" "\r\n" "--%s\r\n" "Content-Type: application/octet-stream\r\n" "\r\n", boundary, boundary, 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); for ( key = keys; *key != NULL; key++ ) gpgme_key_release(*key); free(keys); } /* 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 const char * get_basename(const char *filename) { char *last_slash = strrchr(filename, '/'); return last_slash ? last_slash + 1 : filename; } static void process_attachment(const char *filename, magic_t ctx, FILE *out) { const char *mime, basename; int binary; FILE *f; if ( (mime = magic_file(ctx, filename)) ) { const char *last_eq; last_eq = strrchr(mime, '='); binary = strcmp(last_eq, "=binary") ? 0 : 1; } else { mime = "application/octet-stream"; binary = 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", get_basename(filename)); f = fopen(filename, "r"); if ( binary ) base64_encode_stream(f, out); else qp_encode_stream(f, out); fclose(f); fprintf(out, "\r\n"); } /* Read mail from editor. */ static const char * get_editor(void) { const char *editor; if ( ! (editor = getenv("EDITOR")) ) if ( ! (editor = getenv("VISUAL")) ) editor = "vi"; return editor; } static int is_usable_as_tmp_dir(const char *dirname) { struct stat st_buf; if ( stat(dirname, &st_buf) != -1 ) if ( S_ISDIR(st_buf.st_mode) ) if ( access(dirname, R_OK | W_OK) != -1 ) return 1; return 0; } static const char * get_tmp_dir(void) { const char *tmp; if ( ! ((tmp = getenv("TMPDIR")) && is_usable_as_tmp_dir(tmp)) ) if ( ! ((tmp = getenv("TMP")) && is_usable_as_tmp_dir(tmp)) ) tmp = is_usable_as_tmp_dir("/tmp") ? "/tmp" : "."; return tmp; } static FILE * read_input_from_editor(void) { char *command, *filename; int fd; FILE *f; xasprintf(&filename, "%s/fmailXXXXXX", get_tmp_dir()); if ( (fd = mkstemp(filename)) == -1 ) err(EXIT_FAILURE, "cannot create temporary file"); xasprintf(&command, "%s %s", get_editor(), filename); if ( system(command) == -1 ) err(EXIT_FAILURE, "cannot execute editor"); if ( (f = fdopen(fd, "r")) == NULL ) err(EXIT_FAILURE, "cannot open temporary file"); unlink(filename); free(filename); free(command); return f; } /* Main function. */ int main(int argc, char *argv[]) { char c, with_useragent, with_date; string_buffer_t *headers; fmail_ctx_t ctx; int do_sign; struct option options[] = { { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'v' }, { "sign", 0, NULL, 's' }, { "encrypt", 1, NULL, 'E' }, { "attach", 1, NULL, 'a' }, { "edit", 0, NULL, 'e' }, { "footer", 1, NULL, 'f' }, { "header", 1, NULL, 'H' }, { "from", 1, NULL, 'F' }, { "to", 1, NULL, 'T' }, { "cc", 1, NULL, 'C' }, { "subject", 1, NULL, 'S' }, { "user-agent", 0, NULL, 'U' }, { "date", 0, NULL, 'D' }, { NULL, 0, NULL, 0 } }; setprogname(argv[0]); setlocale(LC_ALL, ""); srand(time(NULL)); with_useragent = with_date = do_sign = 0; headers = sb_new(0); ctx.crypto_ctx = NULL; ctx.magic_ctx = initialize_magic(); ctx.attachments = NULL; ctx.att_count = 0; ctx.recipients = NULL; ctx.rcp_count = 0; ctx.footer = NULL; ctx.input = stdin; while ( (c = getopt_long(argc, argv, "hvsE:a:ef:H:F:T:C:S:UD", options, NULL)) != -1 ) { switch ( c ) { case 'h': usage(EXIT_SUCCESS); break; case '?': usage(EXIT_FAILURE); break; case 'v': info(); break; case 's': do_sign = 1; break; case 'E': if ( ctx.rcp_count % 10 == 0 ) ctx.recipients = xrealloc(ctx.recipients, ctx.rcp_count + 10); ctx.recipients[ctx.rcp_count++] = optarg; 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 'e': ctx.input = NULL; 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; case 'U': with_useragent = 1; break; case 'D': with_date = 1; break; } } if ( do_sign && ctx.rcp_count ) errx(EXIT_FAILURE, "encrypt and sign is not currently supported"); if ( do_sign || ctx.rcp_count ) ctx.crypto_ctx = initialize_gpgme(); /* Generate automatic headers */ if ( with_date ) sb_addf(headers, "Date: %s\r\n", rfc2822_date()); if ( with_useragent ) sb_addf(headers, "User-Agent: fmail %s\r\n", VERSION); /* Fire editor instead of reading from stdin? */ if ( ! ctx.input ) ctx.input = read_input_from_editor(); /* Read user-provided headers. */ read_headers(ctx.input, headers); /* Write all headers. */ fprintf(stdout, "%s", sb_get(headers)); if ( do_sign || ctx.rcp_count ) { FILE *tmp = tmpfile(); process_text_body(&ctx, tmp); fseek(tmp, 0, SEEK_SET); if ( do_sign ) sign_stream(ctx.crypto_ctx, tmp, stdout); else encrypt_stream(ctx.crypto_ctx, tmp, stdout, ctx.recipients, ctx.rcp_count); fclose(tmp); gpgme_release(ctx.crypto_ctx); } else process_text_body(&ctx, stdout); fclose(ctx.input); if ( ctx.attachments ) free(ctx.attachments); if ( ctx.recipients ) free(ctx.recipients); magic_close(ctx.magic_ctx); return EXIT_SUCCESS; }