summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWerner Koch <wk@gnupg.org>2024-06-28 17:51:18 +0200
committerWerner Koch <wk@gnupg.org>2024-06-28 17:59:55 +0200
commit28a080bc9f9478f63a7edffa420512eaed3555ff (patch)
tree29951164e078052a6daf3c8adce78921cdbe83a1
parenttools: New support functions for the mail parser. (diff)
downloadgnupg2-28a080bc9f9478f63a7edffa420512eaed3555ff.tar.xz
gnupg2-28a080bc9f9478f63a7edffa420512eaed3555ff.zip
gpg-mail-tube: New utility.
* tools/gpg-mail-tube.c: new. * tools/Makefile.am: Add it.
-rw-r--r--doc/Makefile.am2
-rw-r--r--doc/tools.texi118
-rw-r--r--tools/Makefile.am14
-rw-r--r--tools/gpg-mail-tube.c819
4 files changed, 950 insertions, 3 deletions
diff --git a/doc/Makefile.am b/doc/Makefile.am
index 66a934cef..5be6009fa 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -81,7 +81,7 @@ myman_sources = gnupg7.texi gpg.texi gpgsm.texi gpg-agent.texi \
gpg-card.texi
myman_pages = gpgsm.1 gpg-agent.1 dirmngr.8 scdaemon.1 \
watchgnupg.1 gpgconf.1 addgnupghome.8 gpg-preset-passphrase.1 \
- gpg-connect-agent.1 gpgparsemail.1 gpgtar.1 \
+ gpg-connect-agent.1 gpgparsemail.1 gpgtar.1 gpg-mail-tube.1 \
applygnupgdefaults.8 gpg-wks-client.1 gpg-wks-server.1 \
dirmngr-client.1 gpg-card.1 gpg-check-pattern.1
if USE_GPG2_HACK
diff --git a/doc/tools.texi b/doc/tools.texi
index efd1c4902..c38dfbf68 100644
--- a/doc/tools.texi
+++ b/doc/tools.texi
@@ -20,6 +20,7 @@ GnuPG comes with a couple of smaller tools:
* dirmngr-client:: How to use the Dirmngr client tool.
* gpgparsemail:: Parse a mail message into an annotated format
* gpgtar:: Encrypt or sign files into an archive.
+* gpg-mail-tube:: Encrypt rfc822 formated mail in a pipeline.
* gpg-check-pattern:: Check a passphrase on stdin against the patternfile.
@end menu
@@ -2097,7 +2098,7 @@ This option is deprecated in favor of option @option{--directory}.
@item --no-compress
@opindex no-compress
This option tells gpg to disable compression (i.e., using option -z0).
-It is useful for archiving only large files which are are already
+It is useful for archiving only large files which are already
compressed (e.g., a set of videos).
@item --gpg @var{gpgcmd}
@@ -2166,6 +2167,121 @@ gpgtar --list-archive test1
@include see-also-note.texi
@c
+@c GPG-MAIL-TUBE
+@c
+@manpage gpg-mail-tube.1
+@node gpg-mail-tube
+@section Encrypt rfc822 formated mail in a pipeline
+@ifset manverb
+.B gpg-mail-tube
+\- Encrypt rfc822 formated mail in a pipeline
+@end ifset
+
+@mansect synopsis
+@ifset manverb
+.B gpg\-mail\-tube
+.RI [ options ]
+.I recipients
+@end ifset
+
+@mansect description
+@command{gpg-mail-tube} takes RFC-822 formatted mail on stdin and
+turns it into a PGP/MIME encrypted mail which is then written to
+stdout.
+
+The recipients must be plain mail addresses
+(e.g. @code{foo@@example.org}) and should in general list the To and
+Cc addresses contained in the mail.
+
+@mansect options
+@noindent
+@command{gpg-mail-tube} understands these options:
+
+@table @gnupgtabopt
+
+@item --verbose
+@itemx -v
+@opindex verbose
+Enable extra informational output.
+
+@item --quiet
+@itemx -q
+@opindex quiet
+Try to be as quiet as possible.
+
+@item --log-file @var{file}
+@opindex log-file
+Write log output to @var{file}. Use @file{socket://} to log to a
+socket.
+
+@item --no-stderr
+Suppresses all output to stderr. This is useful for callers which
+don't distinguish stdout and stderr. To get diagnostics the option
+@option{--log-file} can be used.
+
+@item --header @var{name}=@var{value}
+@opindex header
+Add the mail header "@var{name}: @var{value}" to the output.
+
+@item --setenv @var{name}=@var{value}
+@opindex setenv
+Put the given environment string into the environment of this process
+and of the called gpg. This option is required if there is no other
+way to set the environemt.
+
+@item --gpg @var{gpgcmd}
+@opindex gpg
+Use the specified command @var{gpgcmd} instead of @command{gpg}.
+
+@item --vsd
+@opindex vsd
+Use the gpg from a @emph{GnuPG VS-DesktopĀ®} AppImage. The AppImage is
+started if it is not running. A symlink named
+@file{~/.gnupg-vsd/gnupg-vs-desktop.AppImage} needs to link to the
+actually to be used AppImage.
+
+@item --version
+@opindex version
+Print version of the program and exit.
+
+@item --help
+@opindex help
+Display a brief help page and exit.
+
+@end table
+
+@mansect diagnostics
+@noindent
+The program returns 0 on a successful encryption or a non-zero value
+on error. Note that on error some output might have already been
+written to stdout.
+
+@mansect examples
+
+@noindent
+The following options can be used in a local transport rule of the
+Exim MTA which assumes that that @option{check_local_user} has been
+used in the router.
+
+@example
+transport_filter = /usr/local/bin/gpg-mail-tube --setenv HOME=$@{home@} \
+ --no-stderr -- $pipe_addresses
+@end example
+
+@noindent
+For a remote transport the use of @option{size_addition} and an
+explicit setting of the user and its home directory might be required.
+
+
+@mansect see also
+@ifset isman
+@command{gpg}(1),
+@end ifset
+@include see-also-note.texi
+
+
+
+@c
@c GPG-CHECK-PATTERN
@c
@manpage gpg-check-pattern.1
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 9321da9e3..3769d6606 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -65,7 +65,8 @@ endif
bin_PROGRAMS = gpgconf gpg-connect-agent gpg-card gpg-wks-client
if !HAVE_W32_SYSTEM
-bin_PROGRAMS += watchgnupg gpgparsemail ${gpg_wks_server} gpgsplit
+bin_PROGRAMS += watchgnupg gpgparsemail ${gpg_wks_server} gpgsplit \
+ gpg-mail-tube
else
bin_PROGRAMS += gpgconf-w32
endif
@@ -190,6 +191,17 @@ gpg_wks_client_LDADD = $(libcommon) \
$(LIBINTL) $(LIBICONV) $(NETLIBS) \
$(gpg_wks_client_rc_objs)
+gpg_mail_tube_SOURCES = \
+ gpg-mail-tube.c \
+ rfc822parse.c rfc822parse.h \
+ mime-maker.c mime-maker.h
+
+gpg_mail_tube_CFLAGS = $(LIBGCRYPT_CFLAGS) $(GPG_ERROR_CFLAGS) $(INCICONV)
+gpg_mail_tube_LDADD = $(libcommon) \
+ $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \
+ $(LIBINTL) $(LIBICONV)
+
+
gpg_pair_tool_SOURCES = \
gpg-pair-tool.c
diff --git a/tools/gpg-mail-tube.c b/tools/gpg-mail-tube.c
new file mode 100644
index 000000000..d039bf120
--- /dev/null
+++ b/tools/gpg-mail-tube.c
@@ -0,0 +1,819 @@
+/* gpg-mail-tube.c - A tool to encrypt mails in a pipeline
+ * Copyright (C) 2024 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This file 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#ifdef HAVE_W32_SYSTEM
+# error this program does not work for Windows
+#endif
+
+#define INCLUDED_BY_MAIN_MODULE 1
+#include "../common/util.h"
+#include "../common/init.h"
+#include "../common/sysutils.h"
+#include "../common/ccparray.h"
+#include "../common/exechelp.h"
+#include "../common/mbox-util.h"
+#include "../common/zb32.h"
+#include "rfc822parse.h"
+#include "mime-maker.h"
+
+
+
+/* Constants to identify the commands and options. */
+enum cmd_and_opt_values
+ {
+ aDefault = 0,
+
+ oQuiet = 'q',
+ oVerbose = 'v',
+
+ oDebug = 500,
+
+ oGpgProgram,
+ oHeader,
+ oVSD,
+ oLogFile,
+ oNoStderr,
+ oSetenv,
+
+ oDummy
+ };
+
+
+/* The list of commands and options. */
+static gpgrt_opt_t opts[] = {
+ ARGPARSE_group (301, ("@\nOptions:\n ")),
+
+ ARGPARSE_s_n (oVerbose, "verbose", ("verbose")),
+ ARGPARSE_s_n (oQuiet, "quiet", ("be somewhat more quiet")),
+ ARGPARSE_s_s (oDebug, "debug", "@"),
+ ARGPARSE_s_s (oGpgProgram, "gpg", "@"),
+ ARGPARSE_s_s (oHeader, "header" ,
+ "|NAME=VALUE|add \"NAME: VALUE\" as header to all mails"),
+ ARGPARSE_s_s (oSetenv, "setenv" , "|NAME=VALUE|set envvar NAME to VALUE"),
+ ARGPARSE_s_n (oVSD, "vsd", "run the vsd installation of gpg"),
+ ARGPARSE_s_s (oLogFile, "log-file", "|FILE|write diagnostics to FILE"),
+ ARGPARSE_s_n (oNoStderr, "no-stderr", "suppress all output to stderr"),
+
+ ARGPARSE_end ()
+};
+
+
+/* We keep all global options in the structure OPT. */
+static struct
+{
+ int verbose;
+ unsigned int debug;
+ int quiet;
+ const char *logfile; /* Name of a log file or NULL. */
+ char *gpg_program;
+ strlist_t extra_headers;
+
+ unsigned int vsd:1;
+ unsigned int no_stderr:1;/* Avoid any writes to stderr. */
+} opt;
+
+
+/* Debug values and macros. */
+#define DBG_MIME_VALUE 1 /* Debug the MIME structure. */
+#define DBG_PARSER_VALUE 2 /* Debug the Mail parser. */
+#define DBG_CRYPTO_VALUE 4 /* Debug low level crypto. */
+#define DBG_EXTPROG_VALUE 16384 /* debug external program calls */
+
+#define DBG_MIME (opt.debug & DBG_MIME_VALUE)
+#define DBG_PARSER (opt.debug & DBG_PARSER_VALUE)
+#define DBG_CRYPTO (opt.debug & DBG_CRYPTO_VALUE)
+#define DBG_EXTPROG (opt.debug & DBG_EXTPROG_VALUE)
+
+
+/* The list of supported debug flags. */
+static struct debug_flags_s debug_flags [] =
+ {
+ { DBG_MIME_VALUE , "mime" },
+ { DBG_PARSER_VALUE , "parser" },
+ { DBG_CRYPTO_VALUE , "crypto" },
+ { DBG_EXTPROG_VALUE, "extprog" },
+ { 0, NULL }
+ };
+
+
+/* Definition of the parser object. */
+struct parser_context_s
+{
+ /* The RFC822 parser context is stored here during callbacks. */
+ rfc822parse_t msg;
+
+ /* Helper to convey error codes from user callbacks. */
+ gpg_error_t err;
+
+ /* Flag is set if a MIME-Version header was found. */
+ unsigned int mime_version_seen:1;
+
+ /* Set when the body of a mail has been reached. */
+ unsigned int in_body:1;
+
+ /* Set as long as we are in a content-type or a continuation of
+ * it. */
+ unsigned int in_ct:1;
+
+ /* A buffer for reading a mail line. */
+ char line[5000];
+};
+
+
+
+
+/* Prototypes. */
+static gpg_error_t mail_tube_encrypt (estream_t fpin, strlist_t recipients);
+static void prepare_for_appimage (void);
+static gpg_error_t start_gpg_encrypt (estream_t *r_input,
+ gnupg_process_t *r_proc,
+ strlist_t recipients);
+
+
+
+/* Print usage information and provide strings for help. */
+static const char *
+my_strusage( int level )
+{
+ const char *p;
+
+ switch (level)
+ {
+ case 9: p = "LGPL-2.1-or-later"; break;
+ case 11: p = "gpg-mail-tube"; break;
+ case 12: p = "@GNUPG@"; break;
+ case 13: p = VERSION; break;
+ case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break;
+ case 17: p = PRINTABLE_OS_NAME; break;
+ case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break;
+
+ case 1:
+ case 40:
+ p = ("Usage: gpg-mail-tube [options] recipients (-h for help)");
+ break;
+ case 41:
+ p = ("Syntax: gpg-mail-tube [options]\n"
+ "Tool to encrypt rfc822 formatted mail in a pipeline\n");
+ break;
+
+ default: p = NULL; break;
+ }
+ return p;
+}
+
+
+/* static void */
+/* wrong_args (const char *text) */
+/* { */
+/* es_fprintf (es_stderr, "usage: %s [options] %s\n", gpgrt_strusage (11), text); */
+/* exit (2); */
+/* } */
+
+
+
+/* Command line parsing. */
+static enum cmd_and_opt_values
+parse_arguments (gpgrt_argparse_t *pargs, gpgrt_opt_t *popts)
+{
+ enum cmd_and_opt_values cmd = 0;
+ int no_more_options = 0;
+
+ while (!no_more_options && gpgrt_argparse (NULL, pargs, popts))
+ {
+ switch (pargs->r_opt)
+ {
+ case oQuiet: opt.quiet = 1; break;
+ case oVerbose: opt.verbose++; break;
+ case oDebug:
+ if (parse_debug_flag (pargs->r.ret_str, &opt.debug, debug_flags))
+ {
+ pargs->r_opt = ARGPARSE_INVALID_ARG;
+ pargs->err = ARGPARSE_PRINT_ERROR;
+ }
+ break;
+ case oLogFile: opt.logfile = pargs->r.ret_str; break;
+ case oNoStderr: opt.no_stderr = 1; break;
+
+ case oGpgProgram:
+ opt.gpg_program = pargs->r.ret_str;
+ break;
+ case oHeader:
+ append_to_strlist (&opt.extra_headers, pargs->r.ret_str);
+ break;
+ case oSetenv: putenv (pargs->r.ret_str); break;
+ case oVSD: opt.vsd = 1; break;
+
+ default: pargs->err = ARGPARSE_PRINT_ERROR; break;
+ }
+ }
+
+ return cmd;
+}
+
+
+
+/* gpg-mail-tube main. */
+int
+main (int argc, char **argv)
+{
+ gpg_error_t err;
+ gpgrt_argparse_t pargs;
+ enum cmd_and_opt_values cmd;
+ strlist_t recipients = NULL;
+ int i;
+
+ gnupg_reopen_std ("gpg-mail-tube");
+ gpgrt_set_strusage (my_strusage);
+ log_set_prefix ("gpg-mail-tube", GPGRT_LOG_WITH_PREFIX);
+
+ /* Make sure that our subsystems are ready. */
+ init_common_subsystems (&argc, &argv);
+
+ /* Parse the command line. */
+ pargs.argc = &argc;
+ pargs.argv = &argv;
+ pargs.flags = ARGPARSE_FLAG_KEEP;
+ cmd = parse_arguments (&pargs, opts);
+ gpgrt_argparse (NULL, &pargs, NULL);
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+ /* Some applications do not distinguish between stdout and stderr
+ * and would thus clutter the mail with diagnostics. This option
+ * can be used to inhibit this. */
+ if (opt.no_stderr)
+ {
+ es_fflush (es_stderr);
+ fflush (stderr);
+ i = open ("/dev/null", O_WRONLY);
+ if (i == -1)
+ log_fatal ("failed to open '/dev/null': %s\n",
+ gpg_strerror (gpg_err_code_from_syserror ()));
+ else if (dup2 (i, 2) == -1)
+ log_fatal ("directing stderr to '/dev/null' failed: %s\n",
+ gpg_strerror (gpg_err_code_from_syserror ()));
+ }
+
+ if (opt.logfile)
+ {
+ log_set_file (opt.logfile);
+ log_set_prefix (NULL, (GPGRT_LOG_WITH_PREFIX
+ | GPGRT_LOG_WITH_TIME
+ | GPGRT_LOG_WITH_PID));
+ }
+
+ /* Print a warning if an argument looks like an option. */
+ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN))
+ {
+ for (i=0; i < argc; i++)
+ if (argv[i][0] == '-' && argv[i][1] == '-')
+ log_info (("NOTE: '%s' is not considered an option\n"), argv[i]);
+ }
+
+ /* Set defaults for non given options. */
+ if (!opt.gpg_program)
+ opt.gpg_program = xstrdup (gnupg_module_name (GNUPG_MODULE_NAME_GPG));
+
+ /* Check for syntax errors in the --header option to avoid later
+ * error messages with a not easy to find cause */
+ if (opt.extra_headers)
+ {
+ strlist_t sl;
+
+ for (sl = opt.extra_headers; sl; sl = sl->next)
+ {
+ err = mime_maker_add_header (NULL, sl->d, NULL);
+ if (err)
+ log_error ("syntax error in \"--header %s\": %s\n",
+ sl->d, gpg_strerror (err));
+ }
+ }
+
+ /* The remaining argumenst are the recipients - put them into a list. */
+ /* TODO: Check that these are all valid mail addresses so that gpg
+ * will consider them as such. */
+ for (i=0; i < argc; i++)
+ add_to_strlist (&recipients, argv[i]);
+
+ if (opt.vsd)
+ prepare_for_appimage ();
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+
+ /* Run the selected command. */
+ switch (cmd)
+ {
+ case aDefault:
+ err = mail_tube_encrypt (es_stdin, recipients);
+ break;
+
+ default:
+ gpgrt_usage (1);
+ err = gpg_error (GPG_ERR_BUG);
+ break;
+ }
+
+ if (err)
+ log_error ("command failed: %s\n", gpg_strerror (err));
+ free_strlist (recipients);
+ return log_get_errorcount (0)? 1:0;
+}
+
+
+/* This function is called by the mail parser to communicate events.
+ * This callback communicates with the main function using a structure
+ * passed in OPAQUE. Should return 0 or set errno and return -1. */
+static int
+mail_tube_message_cb (void *opaque,
+ rfc822parse_event_t event, rfc822parse_t msg)
+{
+ struct parser_context_s *ctx = opaque;
+ const char *s;
+
+ if (event == RFC822PARSE_HEADER_SEEN)
+ {
+ /* We don't need this because we will output the header lines as
+ * collected by the parser and thus with canonicalized names.
+ * The original idea was to keep the lines as received so not to
+ * break DKIM. But DKIM would break anyway because DKIM should
+ * aleays cover the CT field which we have to replace. */
+ if (!(s = rfc822parse_last_header_line (msg)))
+ ;
+ else if (!rfc822_cmp_header_name (s, "Content-Type"))
+ ctx->in_ct = 1;
+ else if (*s)
+ ctx->in_ct = 0; /* Another header started. */
+
+ if (s && *s && !rfc822_cmp_header_name (s, "MIME-Version"))
+ ctx->mime_version_seen = 1;
+
+ }
+ else if (event == RFC822PARSE_T2BODY)
+ {
+ ctx->in_ct = 0;
+ ctx->in_body = 1;
+ }
+
+ return 0;
+}
+
+
+/* Receive a mail from FPIN and process to STDOUT. RECIPIENTS is a
+ * string list with the recipients of for this message. */
+static gpg_error_t
+mail_tube_encrypt (estream_t fpin, strlist_t recipients)
+{
+ static const char *ct_names[] =
+ { "Content-Type", "Content-Transfer-Encoding",
+ "Content-Description", "Content-Disposition" };
+ gpg_error_t err;
+ struct parser_context_s parser_context = { NULL };
+ struct parser_context_s *ctx = &parser_context;
+ unsigned int lineno = 0;
+ size_t length;
+ char *line;
+ strlist_t sl;
+ void *iterp;
+ const char *s;
+ char *boundary = NULL; /* Actually only the random part of it. */
+ estream_t gpginfp = NULL;
+ gnupg_process_t proc = NULL;
+ int exitcode;
+ int i, found;
+
+ ctx->msg = rfc822parse_open (mail_tube_message_cb, ctx);
+ if (!ctx->msg)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("can't open mail parser: %s", gpg_strerror (err));
+ goto leave;
+ }
+
+ /* Fixme: We should not use fgets because it can't cope with
+ embedded nul characters. */
+ while (!ctx->in_body && es_fgets (ctx->line, sizeof (ctx->line), fpin))
+ {
+ lineno++;
+ if (lineno == 1 && !strncmp (line, "From ", 5))
+ continue; /* We better ignore a leading From line. */
+
+ line = ctx->line;
+ length = strlen (line);
+ if (length && line[length - 1] == '\n')
+ line[--length] = 0;
+ else
+ log_error ("mail parser detected too long or"
+ " non terminated last line (lnr=%u)\n", lineno);
+ if (length && line[length - 1] == '\r')
+ line[--length] = 0;
+
+ ctx->err = 0;
+ if (rfc822parse_insert (ctx->msg, line, length))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("mail parser failed: %s", gpg_strerror (err));
+ goto leave;
+ }
+ if (ctx->err) /* Error from a callback detected. */
+ {
+ err = ctx->err;
+ goto leave;
+ }
+ }
+ if (!ctx->in_body)
+ {
+ log_error ("mail w/o a body\n");
+ err = gpg_error (GPG_ERR_NO_DATA);
+ goto leave;
+ }
+
+ /* Replace the content-type and output the collected headers. */
+ ctx->in_ct = 0;
+ for (iterp=NULL; (s = rfc822parse_enum_header_lines (ctx->msg, &iterp)); )
+ {
+ for (i=found=0; !found && i < DIM (ct_names); i++)
+ if (!rfc822_cmp_header_name (s, ct_names[i]))
+ found = 1;
+ if (found)
+ ctx->in_ct = 1;
+ else if (*s == ' ' || *s == '\t')
+ ; /* Continuation */
+ else
+ ctx->in_ct = 0;
+
+ if (!ctx->in_ct)
+ es_fprintf (es_stdout, "%s\r\n", s);
+ }
+ rfc822parse_enum_header_lines (NULL, &iterp); /* Close enumerator. */
+
+ /* Create a boundary. We use a pretty simple random string to avoid
+ * the Libgcrypt overhead. It could actually be a constant string
+ * because this is the outer container. */
+ {
+ uint32_t noncebuf = time (NULL);
+
+ boundary = zb32_encode (&noncebuf, 8 * sizeof noncebuf);
+ if (!boundary)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ }
+
+ if (!ctx->mime_version_seen)
+ es_fprintf (es_stdout, "MIME-Version: 1.0\r\n");
+ es_fprintf (es_stdout,
+ "Content-Type: multipart/encrypted;"
+ " protocol=\"application/pgp-encrypted\";\r\n"
+ "\tboundary=\"=-=mt-%s=-=\r\n", boundary);
+
+ /* Add the extra headers. */
+ for (sl = opt.extra_headers; sl; sl = sl->next)
+ {
+ s = strchr (sl->d, '=');
+ log_assert (s);
+ es_fprintf (es_stdout, "%.*s: %s\r\n", (int)(s - sl->d), sl->d, s + 1);
+ }
+
+ /* Output the PGP/MIME boilerplate. */
+ es_fprintf (es_stdout,
+ "\r\n"
+ "This is a PGP/MIME encrypted message.\r\n"
+ "\r\n"
+ "--=-=mt-%s=-=\r\n"
+ "Content-Type: application/pgp-encrypted\r\n"
+ "\r\n"
+ "Version: 1\r\n"
+ "\r\n"
+ "--=-=mt-%s=-=\r\n"
+ "Content-Type: application/octet-stream; name=\"encmsg.asc\"\r\n"
+ "Content-Description: PGP/MIME encrypted message\r\n"
+ "Content-Disposition: inline; filename=\"encmsg.asc\"\r\n"
+ "\r\n", boundary, boundary);
+
+ /* Start gpg and get a stream to fed data to gpg */
+ err = start_gpg_encrypt (&gpginfp, &proc, recipients);
+ if (err)
+ {
+ log_error ("failed to start gpg process: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ /* Write new mime headers using the old content-* values. */
+ for (i=0; i < DIM (ct_names); i++)
+ {
+ line = rfc822parse_get_field (ctx->msg, ct_names[i], -1, NULL);
+ if (line)
+ {
+ es_fprintf (gpginfp, "%s\r\n", line);
+ rfc822_free (line);
+ }
+ }
+ es_fprintf (gpginfp, "\r\n"); /* End of MIME header. */
+
+
+ /* Read the remaining input and feed it to gpg. */
+ while (es_fgets (ctx->line, sizeof (ctx->line), fpin))
+ {
+ lineno++;
+ line = ctx->line;
+ length = strlen (line);
+ if (length && line[length - 1] == '\n')
+ line[--length] = 0;
+ else
+ log_error ("mail parser detected too long or"
+ " non terminated last line (lnr=%u)\n", lineno);
+ if (length && line[length - 1] == '\r')
+ line[--length] = 0;
+ es_fprintf (gpginfp, "%s\r\n", line);
+ }
+
+ /* Wait for gpg to finish. */
+ err = es_fclose (gpginfp);
+ gpginfp = NULL;
+ if (err)
+ log_error ("error closing pipe: %s\n", gpg_strerror (err));
+
+ err = gnupg_process_wait (proc, 1 /* hang */);
+ if (err)
+ {
+ log_error ("waiting for process %s failed: %s\n",
+ opt.gpg_program, gpg_strerror (err));
+ goto leave;
+ }
+ gnupg_process_ctl (proc, GNUPG_PROCESS_GET_EXIT_ID, &exitcode);
+ if (exitcode)
+ {
+ log_error ("running %s failed: exitcode=%d\n",
+ opt.gpg_program, exitcode);
+ goto leave;
+ }
+
+ gnupg_process_release (proc);
+ proc = NULL;
+
+ /* Output the final boundary. */
+ es_fflush (es_stdout);
+ fflush (stdout);
+ es_fprintf (es_stdout,
+ "\r\n"
+ "--=-=mt-%s=-=--\r\n"
+ "\r\n",
+ boundary);
+
+
+ /* Success */
+ rfc822parse_close (ctx->msg);
+ ctx->msg = NULL;
+ err = 0;
+
+ leave:
+ gpgrt_fcancel (gpginfp);
+ gnupg_process_release (proc);
+ rfc822parse_cancel (ctx->msg);
+ xfree (boundary);
+ return err;
+}
+
+
+/* This function returns the name of the gpg binary unter the APPDIR
+ * of the VSD version as a malloced string. It also tests whether
+ * this binary is executable. Returns NULL if the APPDIR was not
+ * found or the gpg is not executable (i.e. not yet/anymore properly
+ * mounted) */
+static char *
+get_vsd_gpgbin (void)
+{
+ gpg_error_t err;
+ char *fname;
+ estream_t fp = NULL;
+ char *line = NULL;
+ size_t linelen;
+ char *gpgbin = NULL;
+ char *p, *pend;
+
+ fname = make_filename ("~/.gnupg-vsd/run-gpgconf", NULL);
+ /* Although we could simply run that script with -L bindir to get
+ * the bin directory we parse the script instead and look for the
+ * assignment of the APPDIR variable which should always be like
+ * APPDIR="/somepath"
+ * Doing this is much faster and avoids the overhead of running the
+ * script and the gpgconf tool. */
+
+ if (gnupg_access (fname, F_OK))
+ goto leave; /* File not available. */
+
+ fp = es_fopen (fname, "r");
+ if (!fp)
+ {
+ err = gpg_error_from_syserror ();
+ if (gpg_err_code (err) != GPG_ERR_ENOENT)
+ log_info ("error opening '%s': %s\n", fname, gpg_strerror (err));
+ goto leave;
+ }
+
+ line = NULL;
+ linelen = 0;
+ while (es_read_line (fp, &line, &linelen, NULL) > 0)
+ {
+ if (strncmp (line, "APPDIR=", 7))
+ continue;
+ p = line + 7;
+ if (*p != '\"' && *p != '\'')
+ continue;
+ pend = strchr (p+1, *p);
+ if (!pend || p+1 == pend)
+ continue;
+ *pend = 0;
+ gpgbin = xstrconcat (p+1, "/usr/bin/gpg", NULL);
+ break;
+ }
+ if (gpgbin && gnupg_access (gpgbin, X_OK))
+ {
+ xfree (gpgbin);
+ gpgbin = NULL;
+ }
+
+ leave:
+ es_fclose (fp);
+ xfree (line);
+ xfree (fname);
+ return gpgbin;
+}
+
+
+
+/* This function is used in VSD mode to override the opt.gpg_program
+ * by replacing it with the gpg from the "GnuPG VS-DesktopĀ®" AppImage.
+ * Start that AppImage if it has not yet been started. No error
+ * return but it bumps the error counter. */
+static void
+prepare_for_appimage (void)
+{
+ gpg_error_t err;
+ char *gpgbin;
+ char *fname = NULL;
+ int i;
+
+ gpgbin = get_vsd_gpgbin ();
+ if (!gpgbin)
+ {
+ /* Run the sleep program for 2^30 seconds (34 years). */
+ static const char *args[4] = { "-c", "sleep", "1073741824", NULL };
+ gnupg_spawn_actions_t act;
+
+ fname = make_filename ("~/.gnupg-vsd/gnupg-vs-desktop.AppImage", NULL);
+
+ err = gnupg_spawn_actions_new (&act);
+ if (!err)
+ {
+ err = gnupg_process_spawn (fname, args,
+ GNUPG_PROCESS_DETACHED, act, NULL);
+ gnupg_spawn_actions_release (act);
+ }
+ if (err)
+ {
+ log_error ("failed to start '%s': %s\n",
+ fname, gpg_strerror (err));
+ return;
+ }
+ for (i=0; i < 30 && !(gpgbin = get_vsd_gpgbin ()); i++)
+ {
+ if (opt.verbose)
+ log_info ("waiting until the AppImage has started ...\n");
+ gnupg_sleep (1);
+ }
+ if (opt.verbose && gpgbin)
+ log_info ("using AppImage gpg binary '%s'\n", gpgbin);
+ }
+
+ if (!gpgbin)
+ log_error ("AppImage did not start up properly\n");
+ else
+ {
+ xfree (opt.gpg_program);
+ opt.gpg_program = gpgbin;
+ }
+ xfree (fname);
+}
+
+
+/* Create a new gpg process for encryption. On success a new stream
+ * is stored at R_INPUT and the process objectat R_PROC. The gpg
+ * output is sent to stdout and is always armored. */
+static gpg_error_t
+start_gpg_encrypt (estream_t *r_input, gnupg_process_t *r_proc,
+ strlist_t recipients)
+{
+ gpg_error_t err;
+ strlist_t sl;
+ ccparray_t ccp;
+#ifdef HAVE_W32_SYSTEM
+ HANDLE except[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
+#else
+ int except[2] = { -1, -1 };
+#endif
+ const char **argv;
+ gnupg_spawn_actions_t act = NULL;
+ char *logfilebuf = NULL;
+
+ *r_input = NULL;
+ *r_proc = NULL;
+ es_fflush (es_stdout);
+
+ if (opt.verbose)
+ log_info ("starting gpg as %u:%u with HOME=%s\n",
+ (unsigned int)getuid (), (unsigned int)getgid (),
+ getenv ("HOME"));
+ ccparray_init (&ccp, 0);
+ ccparray_put (&ccp, "--batch");
+ if (opt.logfile)
+ {
+ logfilebuf = xasprintf ("--log-file=%s", opt.logfile);
+ ccparray_put (&ccp, logfilebuf);
+ }
+ if (DBG_EXTPROG)
+ ccparray_put (&ccp, "--debug=0");
+ ccparray_put (&ccp, "--armor");
+ ccparray_put (&ccp, "--output");
+ ccparray_put (&ccp, "-");
+ if (opt.vsd)
+ ccparray_put (&ccp, "--require-compliance");
+ ccparray_put (&ccp, "--auto-key-locate=clear,local,ldap");
+ ccparray_put (&ccp, "--encrypt");
+ for (sl = recipients; sl; sl = sl->next)
+ {
+ ccparray_put (&ccp, "-r");
+ ccparray_put (&ccp, sl->d);
+ if (opt.verbose)
+ log_info ("encrypting to '%s'\n", sl->d);
+ }
+ ccparray_put (&ccp, "--");
+
+ ccparray_put (&ccp, NULL);
+ argv = ccparray_get (&ccp, NULL);
+ if (!argv)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ err = gnupg_spawn_actions_new (&act);
+ if (err)
+ {
+ xfree (argv);
+ goto leave;
+ }
+
+#ifdef HAVE_W32_SYSTEM
+ gnupg_spawn_actions_set_inherit_handles (act, except);
+#else
+ gnupg_spawn_actions_set_inherit_fds (act, except);
+#endif
+ err = gnupg_process_spawn (opt.gpg_program, argv,
+ (GNUPG_PROCESS_STDIN_PIPE
+ | GNUPG_PROCESS_STDOUT_KEEP
+ | GNUPG_PROCESS_STDERR_KEEP), act, r_proc);
+ gnupg_spawn_actions_release (act);
+ xfree (argv);
+ if (err)
+ goto leave;
+ gnupg_process_get_streams (*r_proc, 0, r_input, NULL, NULL);
+
+ leave:
+ if (err)
+ {
+ gnupg_process_release (*r_proc);
+ *r_proc = NULL;
+ }
+ xfree (logfilebuf);
+ return err;
+}