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.

518 lines
12 KiB

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