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.
 
 
 

585 lines
13 KiB

  1. /*
  2. * fmail - Mail formatter
  3. * Copyright (C) 2011 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 <gpgme.h>
  29. #include <magic.h>
  30. #include <sbuffer.h>
  31. #include <xmem.h>
  32. /* Help and informations about the program. */
  33. static void
  34. usage(int status)
  35. {
  36. puts("\
  37. Usage: fmail [options]\n\
  38. Read a message from standard input and format it\n\
  39. for submission to a mail submission agent.\n");
  40. puts("Options:\n\
  41. -h, --help Display this help message.\n\
  42. -v, --version Display the version message.\n\
  43. ");
  44. puts("\
  45. -e, --edit Fire an editor to type mail body\n\
  46. instead of reading it from\n\
  47. standard input.\n\
  48. -f, --footer FILE Include FILE as the mail footer.\n\
  49. ");
  50. puts("\
  51. Headers options:\n\
  52. -H, --header \"NAME: TEXT\"\n\
  53. Add an arbitrary header.\n\
  54. -F, --from TEXT Set the From: header.\n\
  55. -T, --to TEXT Add a To: header.\n\
  56. -C, --cc TEXT Add a Cc: header.\n\
  57. -S, --subject TEXT Set the Subject: header.\n\
  58. ");
  59. puts("\
  60. Attachments options:\n\
  61. -a, --attach FILE Attach the specified file.\n\
  62. ");
  63. puts("\
  64. Cryptography options:\n\
  65. -s, --sign Sign the message.\n\
  66. ");
  67. printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
  68. exit(status);
  69. }
  70. static void
  71. info(void)
  72. {
  73. printf("\
  74. fmail %s\n\
  75. Copyright (C) 2011 Damien Goutte-Gattat\n\
  76. \n\
  77. This program is released under the GNU General Public License.\n\
  78. See the COPYING file or <http://www.gnu.org/licenses/gpl.html>.\n\
  79. ", VERSION);
  80. exit(EXIT_SUCCESS);
  81. }
  82. /* Helper functions. */
  83. const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  84. "abcdefghijklmnopqrstuvwxyz"
  85. "0123456789+/";
  86. typedef struct fmail_ctx
  87. {
  88. const char **attachments;
  89. size_t att_count;
  90. const char *footer;
  91. magic_t magic_ctx;
  92. gpgme_ctx_t signing_ctx;
  93. FILE *input;
  94. } fmail_ctx_t;
  95. static char *
  96. generate_boundary(char *buffer, size_t len)
  97. {
  98. int i;
  99. for ( i = 0; i < len - 1; i++ )
  100. buffer[i] = base64_chars[rand() % (sizeof(base64_chars) - 1)];
  101. buffer[i] = '\0';
  102. return buffer;
  103. }
  104. static void
  105. qp_encode_stream(FILE *in, FILE *out)
  106. {
  107. int c;
  108. unsigned n;
  109. n = 0;
  110. while ( ! feof(in) ) {
  111. c = fgetc(in);
  112. if ( n >= 72 ) {
  113. fprintf(out, "=\r\n");
  114. n = 0;
  115. }
  116. if ( c == '\t' || c == ' ' ) {
  117. int next = fgetc(in);
  118. if ( next == '\n' ) {
  119. fprintf(out, "=%02X", c);
  120. n += 3;
  121. }
  122. else {
  123. fputc(c, out);
  124. n += 1;
  125. }
  126. ungetc(next, in);
  127. }
  128. else if ( c == '=' ) {
  129. fprintf(out, "=%02X", c);
  130. n += 3;
  131. }
  132. else if ( isprint(c) ) {
  133. fputc(c, out);
  134. n += 1;
  135. }
  136. else if ( c == '\n' ) {
  137. fprintf(out, "\r\n");
  138. n = 0;
  139. }
  140. else if ( c != EOF ) {
  141. fprintf(out, "=%02X", c);
  142. n += 3;
  143. }
  144. }
  145. }
  146. static void
  147. base64_encode_stream(FILE *in, FILE *out)
  148. {
  149. unsigned char buffer[3];
  150. int n, j;
  151. j = 0;
  152. while ( (n = fread(buffer, 1, 3, in)) > 0 ) {
  153. char quartet[4];
  154. if ( n < 3 )
  155. buffer[2] = 0;
  156. if ( n < 2 )
  157. buffer[1] = 0;
  158. quartet[0] = base64_chars[buffer[0] >> 2];
  159. quartet[1] = base64_chars[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xf0) >> 4)];
  160. quartet[2] = (unsigned char) (n > 1 ? base64_chars[((buffer[1] & 0x0f) << 2) | ((buffer[2] & 0x0c) >> 6)] : '=' );
  161. quartet[3] = (unsigned char) (n > 2 ? base64_chars[buffer[2] & 0x3f] : '=');
  162. j += 1;
  163. fwrite(quartet, 1, 4, out);
  164. if ( j == 16 ) {
  165. fprintf(out, "\r\n");
  166. j = 0;
  167. }
  168. }
  169. }
  170. static char *
  171. rfc2822_date(void)
  172. {
  173. time_t timestamp;
  174. struct tm *timestruct;
  175. const char *rfc2822_format = "%a, %d %b %Y %H:%M:%S %z";
  176. static char buffer[32];
  177. timestamp = time(NULL);
  178. timestruct = localtime(&timestamp);
  179. strftime(buffer, sizeof(buffer), rfc2822_format, timestruct);
  180. return buffer;
  181. }
  182. static void
  183. read_headers(FILE *in, string_buffer_t *headers)
  184. {
  185. int c, empty_line;
  186. size_t n;
  187. n = empty_line = 0;
  188. while ( ! empty_line ) {
  189. c = fgetc(in);
  190. if ( c == '\n' ) {
  191. if ( n == 0 )
  192. empty_line = 1;
  193. else {
  194. sb_add(headers, "\r\n");
  195. n = 0;
  196. }
  197. }
  198. else if ( c == EOF ) {
  199. /* EOF before reaching the end of headers, abort. */
  200. errx(EXIT_FAILURE, "cannot read mail headers");
  201. }
  202. else {
  203. sb_addc(headers, c);
  204. n += 1;
  205. }
  206. }
  207. }
  208. static void process_attachment(const char *, magic_t, FILE *);
  209. static void
  210. process_text_body(fmail_ctx_t *ctx, FILE *out)
  211. {
  212. char boundary[32];
  213. int i;
  214. if ( ctx->att_count > 0 ) {
  215. generate_boundary(boundary, sizeof(boundary));
  216. fprintf(out, "Content-Type: multipart/mixed;\r\n"
  217. " boundary=\"%s\"\r\n"
  218. "\r\n"
  219. "--%s\r\n",
  220. boundary, boundary);
  221. }
  222. fprintf(out, "Content-Type: text/plain; charset=\"utf-8\"\r\n"
  223. "Content-Transfer-Encoding: quoted-printable\r\n"
  224. "Content-Disposition: inline\r\n\r\n");
  225. qp_encode_stream(ctx->input, out);
  226. fprintf(out, "\r\n");
  227. if ( ctx->footer ) {
  228. FILE *f;
  229. if ( ! (f = fopen(ctx->footer, "r")) )
  230. err(EXIT_FAILURE, "cannot open footer file '%s'", ctx->footer);
  231. fprintf(out, "-- \r\n");
  232. qp_encode_stream(f, out);
  233. fprintf(out, "\r\n");
  234. fclose(f);
  235. }
  236. for ( i = 0; i < ctx->att_count; i++ ) {
  237. fprintf(out, "--%s\r\n", boundary);
  238. process_attachment(ctx->attachments[i], ctx->magic_ctx, out);
  239. if ( i == ctx->att_count - 1 )
  240. fprintf(out, "--%s--\r\n", boundary);
  241. }
  242. }
  243. /* Signing stuff. */
  244. gpgme_ctx_t
  245. initialize_gpgme(void)
  246. {
  247. gpgme_ctx_t ctx;
  248. gpg_error_t gerr;
  249. gpgme_check_version(NULL);
  250. gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
  251. gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL));
  252. if ( (gerr = gpgme_new(&ctx)) != GPG_ERR_NO_ERROR )
  253. errx(EXIT_FAILURE, "cannot initialize GPGME: %s",
  254. gpgme_strerror(gerr));
  255. gpgme_set_armor(ctx, 1);
  256. return ctx;
  257. }
  258. static void
  259. sign_stream(gpgme_ctx_t ctx, FILE *in, FILE *out)
  260. {
  261. gpgme_data_t gin, gout;
  262. char boundary[32], buffer[512];
  263. int n;
  264. gpgme_data_new_from_stream(&gin, in);
  265. gpgme_data_new(&gout);
  266. gpgme_op_sign(ctx, gin, gout, GPGME_SIG_MODE_DETACH);
  267. generate_boundary(boundary, sizeof(boundary));
  268. fprintf(out, "Content-Type: multipart/signed;\r\n"
  269. " boundary=\"%s\";\r\n"
  270. " protocol=\"application/pgp-signature\";\r\n"
  271. " micalg=pgp-sha1\r\n"
  272. "\r\n"
  273. "--%s\r\n",
  274. boundary, boundary);
  275. fseek(in, 0, SEEK_SET);
  276. while ( (n = fread(buffer, 1, sizeof(buffer), in)) > 0 )
  277. fwrite(buffer, 1, n, out);
  278. fprintf(out, "\r\n"
  279. "--%s\r\n"
  280. "Content-Type: application/pgp-signature; name=signature.asc\r\n"
  281. "\r\n",
  282. boundary);
  283. gpgme_data_seek(gout, 0, SEEK_SET);
  284. while ( (n = gpgme_data_read(gout, buffer, sizeof(buffer))) > 0 ) {
  285. int i = 0;
  286. while ( i < n ) {
  287. if ( buffer[i] == '\n' )
  288. fputc('\r', out);
  289. fputc(buffer[i], out);
  290. i += 1;
  291. }
  292. }
  293. fprintf(out, "\r\n--%s--\r\n", boundary);
  294. gpgme_data_release(gin);
  295. gpgme_data_release(gout);
  296. }
  297. /* Attachments stuff. */
  298. static magic_t
  299. initialize_magic(void)
  300. {
  301. magic_t ctx;
  302. if ( ! (ctx = magic_open(MAGIC_SYMLINK | MAGIC_MIME)) )
  303. err(EXIT_FAILURE, "cannot obtain libmagic cookie");
  304. if ( magic_load(ctx, NULL) == -1 )
  305. err(EXIT_FAILURE, "cannot load default magic database");
  306. return ctx;
  307. }
  308. static void
  309. process_attachment(const char *filename, magic_t ctx, FILE *out)
  310. {
  311. const char *mime, *last_eq;
  312. int binary;
  313. FILE *f;
  314. mime = magic_file(ctx, filename);
  315. last_eq = strrchr(mime, '=');
  316. binary = strcmp(last_eq, "=binary") ? 0 : 1;
  317. fprintf(out, "Content-Type: %s\r\n", mime);
  318. fprintf(out, "Content-Transfer-Encoding: %s\r\n", binary ? "base64" : "quoted-printable");
  319. fprintf(out, "Content-Disposition: attachment; filename=%s\r\n\r\n", filename);
  320. f = fopen(filename, "r");
  321. if ( binary )
  322. base64_encode_stream(f, out);
  323. else
  324. qp_encode_stream(f, out);
  325. fclose(f);
  326. fprintf(out, "\r\n");
  327. }
  328. /* Read mail from editor. */
  329. static const char *
  330. get_editor(void)
  331. {
  332. const char *editor;
  333. if ( ! (editor = getenv("EDITOR")) )
  334. if ( ! (editor = getenv("VISUAL")) )
  335. editor = "vi";
  336. return editor;
  337. }
  338. static FILE *
  339. read_input_from_editor(void)
  340. {
  341. char tmp_filename[] = "/tmp/fmailXXXXXX";
  342. const char *editor;
  343. char *command;
  344. int tmp_fd;
  345. FILE *f;
  346. editor = get_editor();
  347. if ( (tmp_fd = mkstemp(tmp_filename)) == -1 )
  348. err(EXIT_FAILURE, "cannot create temporary file");
  349. command = xmalloc(strlen(editor) + sizeof(tmp_filename) + 1);
  350. sprintf(command, "%s %s", editor, tmp_filename);
  351. if ( system(command) == -1 )
  352. err(EXIT_FAILURE, "cannot execute editor '%s'", editor);
  353. if ( (f = fdopen(tmp_fd, "r")) == NULL )
  354. err(EXIT_FAILURE, "cannot open temporary file");
  355. unlink(tmp_filename);
  356. free(command);
  357. return f;
  358. }
  359. /* Main function. */
  360. int
  361. main(int argc, char *argv[])
  362. {
  363. char c;
  364. string_buffer_t *headers;
  365. fmail_ctx_t ctx;
  366. struct option options[] = {
  367. { "help", 0, NULL, 'h' },
  368. { "version", 0, NULL, 'v' },
  369. { "sign", 0, NULL, 's' },
  370. { "attach", 1, NULL, 'a' },
  371. { "edit", 0, NULL, 'e' },
  372. { "footer", 1, NULL, 'f' },
  373. { "header", 1, NULL, 'H' },
  374. { "from", 1, NULL, 'F' },
  375. { "to", 1, NULL, 'T' },
  376. { "cc", 1, NULL, 'C' },
  377. { "subject", 1, NULL, 'S' },
  378. { NULL, 0, NULL, 0 }
  379. };
  380. setprogname(argv[0]);
  381. setlocale(LC_ALL, "");
  382. srand(time(NULL));
  383. headers = sb_new(0);
  384. ctx.signing_ctx = NULL;
  385. ctx.magic_ctx = initialize_magic();
  386. ctx.attachments = NULL;
  387. ctx.att_count = 0;
  388. ctx.footer = NULL;
  389. ctx.input = stdin;
  390. while ( (c = getopt_long(argc, argv, "hvsa:ef:H:F:T:C:S:",
  391. options, NULL)) != -1 ) {
  392. switch ( c ) {
  393. case 'h':
  394. usage(EXIT_SUCCESS);
  395. break;
  396. case '?':
  397. usage(EXIT_FAILURE);
  398. break;
  399. case 'v':
  400. info();
  401. break;
  402. case 's':
  403. ctx.signing_ctx = initialize_gpgme();
  404. break;
  405. case 'a':
  406. if ( ctx.att_count % 10 == 0 )
  407. ctx.attachments = xrealloc(ctx.attachments, ctx.att_count + 10);
  408. ctx.attachments[ctx.att_count++] = optarg;
  409. break;
  410. case 'e':
  411. ctx.input = NULL;
  412. break;
  413. case 'f':
  414. ctx.footer = optarg;
  415. break;
  416. case 'H':
  417. sb_addf(headers, "%s\r\n", optarg);
  418. break;
  419. case 'F':
  420. sb_addf(headers, "From: %s\r\n", optarg);
  421. break;
  422. case 'T':
  423. sb_addf(headers, "To: %s\r\n", optarg);
  424. break;
  425. case 'C':
  426. sb_addf(headers, "Cc: %s\r\n", optarg);
  427. break;
  428. case 'S':
  429. sb_addf(headers, "Subject: %s\r\n", optarg);
  430. break;
  431. }
  432. }
  433. /* Generate automatic headers */
  434. sb_addf(headers, "Date: %s\r\n", rfc2822_date());
  435. sb_addf(headers, "User-Agent: fmail %s\r\n", VERSION);
  436. /* Fire editor instead of reading from stdin? */
  437. if ( ! ctx.input )
  438. ctx.input = read_input_from_editor();
  439. /* Read user-provided headers. */
  440. read_headers(ctx.input, headers);
  441. /* Write all headers. */
  442. fprintf(stdout, "%s", sb_get(headers));
  443. if ( ctx.signing_ctx ) {
  444. FILE *tmp = tmpfile();
  445. process_text_body(&ctx, tmp);
  446. fseek(tmp, 0, SEEK_SET);
  447. sign_stream(ctx.signing_ctx, tmp, stdout);
  448. fclose(tmp);
  449. gpgme_release(ctx.signing_ctx);
  450. }
  451. else
  452. process_text_body(&ctx, stdout);
  453. fclose(ctx.input);
  454. if ( ctx.attachments )
  455. free(ctx.attachments);
  456. magic_close(ctx.magic_ctx);
  457. return EXIT_SUCCESS;
  458. }