A mail formatter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

661 lines
16 KiB

  1. /*
  2. * fmail - Mail formatter
  3. * Copyright (C) 2011,2018 Damien Goutte-Gattat
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. #ifdef HAVE_CONFIG_H
  19. #include <config.h>
  20. #endif
  21. #include <stdio.h>
  22. #include <stdlib.h>
  23. #include <getopt.h>
  24. #include <locale.h>
  25. #include <string.h>
  26. #include <ctype.h>
  27. #include <time.h>
  28. #include <unistd.h>
  29. #include <sys/stat.h>
  30. #include <err.h>
  31. #include <gpgme.h>
  32. #include <magic.h>
  33. #include <sbuffer.h>
  34. #include <xmem.h>
  35. /* Help and informations about the program. */
  36. static void
  37. usage(int status)
  38. {
  39. puts("\
  40. Usage: fmail [options]\n\
  41. Read a message from standard input and format it\n\
  42. for submission to a mail submission agent.\n");
  43. puts("Options:\n\
  44. -h, --help Display this help message.\n\
  45. -v, --version Display the version message.\n\
  46. ");
  47. puts("\
  48. -e, --edit Fire an editor to type mail body\n\
  49. instead of reading it from\n\
  50. standard input.\n\
  51. -f, --footer FILE Include FILE as the mail footer.\n\
  52. ");
  53. puts("\
  54. Headers options:\n\
  55. -H, --header \"NAME: TEXT\"\n\
  56. Add an arbitrary header.\n\
  57. -F, --from TEXT Set the From: header.\n\
  58. -T, --to TEXT Add a To: header.\n\
  59. -C, --cc TEXT Add a Cc: header.\n\
  60. -S, --subject TEXT Set the Subject: header.\n\
  61. -U, --user-agent Add a User-Agent: header.\n\
  62. -D, --date Add a Date: header with the current date.\n\
  63. ");
  64. puts("\
  65. Attachments options:\n\
  66. -a, --attach FILE Attach the specified file.\n\
  67. ");
  68. puts("\
  69. Cryptography options:\n\
  70. -s, --sign Sign the message.\n\
  71. ");
  72. printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
  73. exit(status);
  74. }
  75. static void
  76. info(void)
  77. {
  78. printf("\
  79. fmail %s\n\
  80. Copyright (C) 2011 Damien Goutte-Gattat\n\
  81. \n\
  82. This program is released under the GNU General Public License.\n\
  83. See the COPYING file or <http://www.gnu.org/licenses/gpl.html>.\n\
  84. ", VERSION);
  85. exit(EXIT_SUCCESS);
  86. }
  87. /* Helper functions. */
  88. const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  89. "abcdefghijklmnopqrstuvwxyz"
  90. "0123456789+/";
  91. typedef struct fmail_ctx
  92. {
  93. const char **attachments;
  94. size_t att_count;
  95. const char *footer;
  96. magic_t magic_ctx;
  97. gpgme_ctx_t signing_ctx;
  98. FILE *input;
  99. } fmail_ctx_t;
  100. static char *
  101. generate_boundary(char *buffer, size_t len)
  102. {
  103. unsigned i;
  104. for ( i = 0; i < len - 1; i++ )
  105. buffer[i] = base64_chars[rand() % (sizeof(base64_chars) - 1)];
  106. buffer[i] = '\0';
  107. return buffer;
  108. }
  109. static void
  110. qp_encode_stream(FILE *in, FILE *out)
  111. {
  112. int c;
  113. unsigned n;
  114. n = 0;
  115. while ( ! feof(in) ) {
  116. c = fgetc(in);
  117. if ( n >= 72 ) {
  118. fprintf(out, "=\r\n");
  119. n = 0;
  120. }
  121. if ( c == '\t' || c == ' ' ) {
  122. int next = fgetc(in);
  123. if ( next == '\n' ) {
  124. fprintf(out, "=%02X", c);
  125. n += 3;
  126. }
  127. else {
  128. fputc(c, out);
  129. n += 1;
  130. }
  131. ungetc(next, in);
  132. }
  133. else if ( c == '=' ) {
  134. fprintf(out, "=%02X", c);
  135. n += 3;
  136. }
  137. else if ( isprint(c) ) {
  138. fputc(c, out);
  139. n += 1;
  140. }
  141. else if ( c == '\n' ) {
  142. fprintf(out, "\r\n");
  143. n = 0;
  144. }
  145. else if ( c != EOF ) {
  146. fprintf(out, "=%02X", c);
  147. n += 3;
  148. }
  149. }
  150. }
  151. static void
  152. base64_encode_stream(FILE *in, FILE *out)
  153. {
  154. unsigned char buffer[3];
  155. int n, j;
  156. j = 0;
  157. while ( (n = fread(buffer, 1, 3, in)) > 0 ) {
  158. char quartet[4];
  159. if ( n < 3 )
  160. buffer[2] = 0;
  161. if ( n < 2 )
  162. buffer[1] = 0;
  163. quartet[0] = base64_chars[buffer[0] >> 2];
  164. quartet[1] = base64_chars[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xf0) >> 4)];
  165. quartet[2] = (unsigned char) (n > 1 ? base64_chars[((buffer[1] & 0x0f) << 2) | ((buffer[2] & 0x0c) >> 6)] : '=' );
  166. quartet[3] = (unsigned char) (n > 2 ? base64_chars[buffer[2] & 0x3f] : '=');
  167. j += 1;
  168. fwrite(quartet, 1, 4, out);
  169. if ( j == 16 ) {
  170. fprintf(out, "\r\n");
  171. j = 0;
  172. }
  173. }
  174. }
  175. static char *
  176. rfc2822_date(void)
  177. {
  178. time_t timestamp;
  179. struct tm *timestruct;
  180. const char *rfc2822_format = "%a, %d %b %Y %H:%M:%S %z";
  181. static char buffer[32];
  182. timestamp = time(NULL);
  183. timestruct = localtime(&timestamp);
  184. strftime(buffer, sizeof(buffer), rfc2822_format, timestruct);
  185. return buffer;
  186. }
  187. static void
  188. read_headers(FILE *in, string_buffer_t *headers)
  189. {
  190. int c, empty_line;
  191. size_t n;
  192. n = empty_line = 0;
  193. while ( ! empty_line ) {
  194. c = fgetc(in);
  195. if ( c == '\n' ) {
  196. if ( n == 0 )
  197. empty_line = 1;
  198. else {
  199. sb_add(headers, "\r\n");
  200. n = 0;
  201. }
  202. }
  203. else if ( c == EOF ) {
  204. /* EOF before reaching the end of headers, abort. */
  205. errx(EXIT_FAILURE, "cannot read mail headers");
  206. }
  207. else {
  208. sb_addc(headers, c);
  209. n += 1;
  210. }
  211. }
  212. }
  213. static void process_attachment(const char *, magic_t, FILE *);
  214. static void
  215. process_text_body(fmail_ctx_t *ctx, FILE *out)
  216. {
  217. char boundary[32];
  218. unsigned i;
  219. if ( ctx->att_count > 0 ) {
  220. generate_boundary(boundary, sizeof(boundary));
  221. fprintf(out, "Content-Type: multipart/mixed;\r\n"
  222. " boundary=\"%s\"\r\n"
  223. "\r\n"
  224. "--%s\r\n",
  225. boundary, boundary);
  226. }
  227. fprintf(out, "Content-Type: text/plain; charset=\"utf-8\"\r\n"
  228. "Content-Transfer-Encoding: quoted-printable\r\n"
  229. "Content-Disposition: inline\r\n\r\n");
  230. qp_encode_stream(ctx->input, out);
  231. fprintf(out, "\r\n");
  232. if ( ctx->footer ) {
  233. FILE *f;
  234. if ( ! (f = fopen(ctx->footer, "r")) )
  235. err(EXIT_FAILURE, "cannot open footer file '%s'", ctx->footer);
  236. fprintf(out, "-- \r\n");
  237. qp_encode_stream(f, out);
  238. fprintf(out, "\r\n");
  239. fclose(f);
  240. }
  241. for ( i = 0; i < ctx->att_count; i++ ) {
  242. fprintf(out, "--%s\r\n", boundary);
  243. process_attachment(ctx->attachments[i], ctx->magic_ctx, out);
  244. if ( i == ctx->att_count - 1 )
  245. fprintf(out, "--%s--\r\n", boundary);
  246. }
  247. }
  248. /* Signing stuff. */
  249. gpgme_ctx_t
  250. initialize_gpgme(void)
  251. {
  252. gpgme_ctx_t ctx;
  253. gpg_error_t gerr;
  254. gpgme_check_version(NULL);
  255. gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
  256. gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL));
  257. if ( (gerr = gpgme_new(&ctx)) != GPG_ERR_NO_ERROR )
  258. errx(EXIT_FAILURE, "cannot initialize GPGME: %s",
  259. gpgme_strerror(gerr));
  260. gpgme_set_armor(ctx, 1);
  261. return ctx;
  262. }
  263. static const char *
  264. hash_algo_to_string(gpgme_hash_algo_t algo)
  265. {
  266. switch ( algo ) {
  267. case GPGME_MD_MD5: return "pgp-md5";
  268. case GPGME_MD_SHA1: return "pgp-sha1";
  269. case GPGME_MD_RMD160: return "pgp-ripemd160";
  270. case GPGME_MD_MD2: return "pgp-md2";
  271. case GPGME_MD_TIGER: return "pgp-tiger192";
  272. case GPGME_MD_HAVAL: return "php-haval-5-160";
  273. case GPGME_MD_SHA256: return "pgp-sha256";
  274. case GPGME_MD_SHA384: return "pgp-sha384";
  275. case GPGME_MD_SHA512: return "pgp-sha512";
  276. case GPGME_MD_SHA224: return "pgp-sha224";
  277. case GPGME_MD_MD4: return "pgp-md4";
  278. case GPGME_MD_CRC32: return "pgp-crc32";
  279. case GPGME_MD_CRC32_RFC1510: return "pgp-crc32-rfc1510";
  280. case GPGME_MD_CRC24_RFC2440: return "pgp-crc24-rfc2440";
  281. case GPGME_MD_NONE: return "";
  282. }
  283. return ""; /* FIXME: What to do here? */
  284. }
  285. static void
  286. sign_stream(gpgme_ctx_t ctx, FILE *in, FILE *out)
  287. {
  288. gpgme_data_t gin, gout;
  289. gpgme_sign_result_t result;
  290. char boundary[32], buffer[512];
  291. int n;
  292. gpgme_data_new_from_stream(&gin, in);
  293. gpgme_data_new(&gout);
  294. gpgme_op_sign(ctx, gin, gout, GPGME_SIG_MODE_DETACH);
  295. result = gpgme_op_sign_result(ctx);
  296. generate_boundary(boundary, sizeof(boundary));
  297. fprintf(out, "Content-Type: multipart/signed;\r\n"
  298. " boundary=\"%s\";\r\n"
  299. " protocol=\"application/pgp-signature\";\r\n"
  300. " micalg=%s\r\n"
  301. "\r\n"
  302. "--%s\r\n",
  303. boundary,
  304. hash_algo_to_string(result->signatures->hash_algo),
  305. boundary);
  306. fseek(in, 0, SEEK_SET);
  307. while ( (n = fread(buffer, 1, sizeof(buffer), in)) > 0 )
  308. fwrite(buffer, 1, n, out);
  309. fprintf(out, "\r\n"
  310. "--%s\r\n"
  311. "Content-Type: application/pgp-signature; name=signature.asc\r\n"
  312. "\r\n",
  313. boundary);
  314. gpgme_data_seek(gout, 0, SEEK_SET);
  315. while ( (n = gpgme_data_read(gout, buffer, sizeof(buffer))) > 0 ) {
  316. int i = 0;
  317. while ( i < n ) {
  318. if ( buffer[i] == '\n' )
  319. fputc('\r', out);
  320. fputc(buffer[i], out);
  321. i += 1;
  322. }
  323. }
  324. fprintf(out, "\r\n--%s--\r\n", boundary);
  325. gpgme_data_release(gin);
  326. gpgme_data_release(gout);
  327. }
  328. /* Attachments stuff. */
  329. static magic_t
  330. initialize_magic(void)
  331. {
  332. magic_t ctx;
  333. if ( ! (ctx = magic_open(MAGIC_SYMLINK | MAGIC_MIME)) )
  334. err(EXIT_FAILURE, "cannot obtain libmagic cookie");
  335. if ( magic_load(ctx, NULL) == -1 )
  336. err(EXIT_FAILURE, "cannot load default magic database");
  337. return ctx;
  338. }
  339. static void
  340. process_attachment(const char *filename, magic_t ctx, FILE *out)
  341. {
  342. const char *mime;
  343. int binary;
  344. FILE *f;
  345. if ( (mime = magic_file(ctx, filename)) ) {
  346. const char *last_eq;
  347. last_eq = strrchr(mime, '=');
  348. binary = strcmp(last_eq, "=binary") ? 0 : 1;
  349. }
  350. else {
  351. mime = "application/octet-stream";
  352. binary = 1;
  353. }
  354. fprintf(out, "Content-Type: %s\r\n", mime);
  355. fprintf(out, "Content-Transfer-Encoding: %s\r\n", binary ? "base64" : "quoted-printable");
  356. fprintf(out, "Content-Disposition: attachment; filename=%s\r\n\r\n", filename);
  357. f = fopen(filename, "r");
  358. if ( binary )
  359. base64_encode_stream(f, out);
  360. else
  361. qp_encode_stream(f, out);
  362. fclose(f);
  363. fprintf(out, "\r\n");
  364. }
  365. /* Read mail from editor. */
  366. static const char *
  367. get_editor(void)
  368. {
  369. const char *editor;
  370. if ( ! (editor = getenv("EDITOR")) )
  371. if ( ! (editor = getenv("VISUAL")) )
  372. editor = "vi";
  373. return editor;
  374. }
  375. static int
  376. is_usable_as_tmp_dir(const char *dirname)
  377. {
  378. struct stat st_buf;
  379. if ( stat(dirname, &st_buf) != -1 )
  380. if ( S_ISDIR(st_buf.st_mode) )
  381. if ( access(dirname, R_OK | W_OK) != -1 )
  382. return 1;
  383. return 0;
  384. }
  385. static const char *
  386. get_tmp_dir(void)
  387. {
  388. const char *tmp;
  389. if ( ! ((tmp = getenv("TMPDIR")) && is_usable_as_tmp_dir(tmp)) )
  390. if ( ! ((tmp = getenv("TMP")) && is_usable_as_tmp_dir(tmp)) )
  391. tmp = is_usable_as_tmp_dir("/tmp") ? "/tmp" : ".";
  392. return tmp;
  393. }
  394. static FILE *
  395. read_input_from_editor(void)
  396. {
  397. char *command, *filename;
  398. int fd;
  399. FILE *f;
  400. xasprintf(&filename, "%s/fmailXXXXXX", get_tmp_dir());
  401. if ( (fd = mkstemp(filename)) == -1 )
  402. err(EXIT_FAILURE, "cannot create temporary file");
  403. xasprintf(&command, "%s %s", get_editor(), filename);
  404. if ( system(command) == -1 )
  405. err(EXIT_FAILURE, "cannot execute editor");
  406. if ( (f = fdopen(fd, "r")) == NULL )
  407. err(EXIT_FAILURE, "cannot open temporary file");
  408. unlink(filename);
  409. free(filename);
  410. free(command);
  411. return f;
  412. }
  413. /* Main function. */
  414. int
  415. main(int argc, char *argv[])
  416. {
  417. char c, with_useragent, with_date;
  418. string_buffer_t *headers;
  419. fmail_ctx_t ctx;
  420. struct option options[] = {
  421. { "help", 0, NULL, 'h' },
  422. { "version", 0, NULL, 'v' },
  423. { "sign", 0, NULL, 's' },
  424. { "attach", 1, NULL, 'a' },
  425. { "edit", 0, NULL, 'e' },
  426. { "footer", 1, NULL, 'f' },
  427. { "header", 1, NULL, 'H' },
  428. { "from", 1, NULL, 'F' },
  429. { "to", 1, NULL, 'T' },
  430. { "cc", 1, NULL, 'C' },
  431. { "subject", 1, NULL, 'S' },
  432. { "user-agent", 0, NULL, 'U' },
  433. { "date", 0, NULL, 'D' },
  434. { NULL, 0, NULL, 0 }
  435. };
  436. setprogname(argv[0]);
  437. setlocale(LC_ALL, "");
  438. srand(time(NULL));
  439. with_useragent = with_date = 0;
  440. headers = sb_new(0);
  441. ctx.signing_ctx = NULL;
  442. ctx.magic_ctx = initialize_magic();
  443. ctx.attachments = NULL;
  444. ctx.att_count = 0;
  445. ctx.footer = NULL;
  446. ctx.input = stdin;
  447. while ( (c = getopt_long(argc, argv, "hvsa:ef:H:F:T:C:S:",
  448. options, NULL)) != -1 ) {
  449. switch ( c ) {
  450. case 'h':
  451. usage(EXIT_SUCCESS);
  452. break;
  453. case '?':
  454. usage(EXIT_FAILURE);
  455. break;
  456. case 'v':
  457. info();
  458. break;
  459. case 's':
  460. if ( ! ctx.signing_ctx )
  461. ctx.signing_ctx = initialize_gpgme();
  462. break;
  463. case 'a':
  464. if ( ctx.att_count % 10 == 0 )
  465. ctx.attachments = xrealloc(ctx.attachments, ctx.att_count + 10);
  466. ctx.attachments[ctx.att_count++] = optarg;
  467. break;
  468. case 'e':
  469. ctx.input = NULL;
  470. break;
  471. case 'f':
  472. ctx.footer = optarg;
  473. break;
  474. case 'H':
  475. sb_addf(headers, "%s\r\n", optarg);
  476. break;
  477. case 'F':
  478. sb_addf(headers, "From: %s\r\n", optarg);
  479. break;
  480. case 'T':
  481. sb_addf(headers, "To: %s\r\n", optarg);
  482. break;
  483. case 'C':
  484. sb_addf(headers, "Cc: %s\r\n", optarg);
  485. break;
  486. case 'S':
  487. sb_addf(headers, "Subject: %s\r\n", optarg);
  488. break;
  489. case 'U':
  490. with_useragent = 1;
  491. break;
  492. case 'D':
  493. with_date = 1;
  494. break;
  495. }
  496. }
  497. /* Generate automatic headers */
  498. if ( with_date )
  499. sb_addf(headers, "Date: %s\r\n", rfc2822_date());
  500. if ( with_useragent )
  501. sb_addf(headers, "User-Agent: fmail %s\r\n", VERSION);
  502. /* Fire editor instead of reading from stdin? */
  503. if ( ! ctx.input )
  504. ctx.input = read_input_from_editor();
  505. /* Read user-provided headers. */
  506. read_headers(ctx.input, headers);
  507. /* Write all headers. */
  508. fprintf(stdout, "%s", sb_get(headers));
  509. if ( ctx.signing_ctx ) {
  510. FILE *tmp = tmpfile();
  511. process_text_body(&ctx, tmp);
  512. fseek(tmp, 0, SEEK_SET);
  513. sign_stream(ctx.signing_ctx, tmp, stdout);
  514. fclose(tmp);
  515. gpgme_release(ctx.signing_ctx);
  516. }
  517. else
  518. process_text_body(&ctx, stdout);
  519. fclose(ctx.input);
  520. if ( ctx.attachments )
  521. free(ctx.attachments);
  522. magic_close(ctx.magic_ctx);
  523. return EXIT_SUCCESS;
  524. }