File: [local] / src / usr.sbin / smtpd / enqueue.c (download)
Revision 1.122, Sat Jan 20 09:01:03 2024 UTC (4 months, 2 weeks ago) by claudio
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, HEAD Changes since 1.121: +2 -2 lines
Use imsg_get_fd() to access the fd passed via imsgs.
Most of the conversion is simple there is just log_imsg() that can
no longer display the fd since imsg_get_fd() can only be called once.
OK op@
|
/* $OpenBSD: enqueue.c,v 1.122 2024/01/20 09:01:03 claudio Exp $ */
/*
* Copyright (c) 2005 Henning Brauer <henning@bulabula.org>
* Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
* Copyright (c) 2012 Gilles Chehade <gilles@poolp.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "smtpd.h"
extern struct imsgbuf *ibuf;
void usage(void);
static void build_from(char *, struct passwd *);
static int parse_message(FILE *, int, int, FILE *);
static void parse_addr(char *, size_t, int);
static void parse_addr_terminal(int);
static char *qualify_addr(char *);
static void rcpt_add(char *);
static int open_connection(void);
static int get_responses(FILE *, int);
static int send_line(FILE *, int, char *, ...)
__attribute__((__format__ (printf, 3, 4)));
static int enqueue_offline(int, char *[], FILE *, FILE *);
static int savedeadletter(struct passwd *, FILE *);
extern int srv_connected(void);
enum headerfields {
HDR_NONE,
HDR_FROM,
HDR_TO,
HDR_CC,
HDR_BCC,
HDR_SUBJECT,
HDR_DATE,
HDR_MSGID,
HDR_MIME_VERSION,
HDR_CONTENT_TYPE,
HDR_CONTENT_DISPOSITION,
HDR_CONTENT_TRANSFER_ENCODING,
HDR_USER_AGENT
};
struct {
char *word;
enum headerfields type;
} keywords[] = {
{ "From:", HDR_FROM },
{ "To:", HDR_TO },
{ "Cc:", HDR_CC },
{ "Bcc:", HDR_BCC },
{ "Subject:", HDR_SUBJECT },
{ "Date:", HDR_DATE },
{ "Message-Id:", HDR_MSGID },
{ "MIME-Version:", HDR_MIME_VERSION },
{ "Content-Type:", HDR_CONTENT_TYPE },
{ "Content-Disposition:", HDR_CONTENT_DISPOSITION },
{ "Content-Transfer-Encoding:", HDR_CONTENT_TRANSFER_ENCODING },
{ "User-Agent:", HDR_USER_AGENT },
};
#define LINESPLIT 990
#define SMTP_LINELEN 1000
#define TIMEOUTMSG "Timeout\n"
#define WSP(c) (c == ' ' || c == '\t')
int verbose = 0;
static char host[HOST_NAME_MAX+1];
char *user = NULL;
time_t timestamp;
struct {
int fd;
char *from;
char *fromname;
char **rcpts;
char *dsn_notify;
char *dsn_ret;
char *dsn_envid;
int rcpt_cnt;
int need_linesplit;
int saw_date;
int saw_msgid;
int saw_from;
int saw_mime_version;
int saw_content_type;
int saw_content_disposition;
int saw_content_transfer_encoding;
int saw_user_agent;
int noheader;
} msg;
struct {
uint quote;
uint comment;
uint esc;
uint brackets;
size_t wpos;
char buf[SMTP_LINELEN];
} pstate;
#define QP_TEST_WRAP(fp, buf, linelen, size) do { \
if (((linelen) += (size)) + 1 > 76) { \
fprintf((fp), "=\r\n"); \
if (buf[0] == '.') \
fprintf((fp), "."); \
(linelen) = (size); \
} \
} while (0)
/* RFC 2045 section 6.7 */
static void
qp_encoded_write(FILE *fp, char *buf)
{
size_t linelen = 0;
for (;buf[0] != '\0' && buf[0] != '\n'; buf++) {
/*
* Point 3: Any TAB (HT) or SPACE characters on an encoded line
* MUST thus be followed on that line by a printable character.
*
* Ergo, only encode if the next character is EOL.
*/
if (buf[0] == ' ' || buf[0] == '\t') {
if (buf[1] == '\n') {
QP_TEST_WRAP(fp, buf, linelen, 3);
fprintf(fp, "=%2X", *buf & 0xff);
} else {
QP_TEST_WRAP(fp, buf, linelen, 1);
fprintf(fp, "%c", *buf & 0xff);
}
/*
* Point 1, with exclusion of point 2, skip EBCDIC NOTE.
* Do this after whitespace check, else they would match here.
*/
} else if (!((buf[0] >= 33 && buf[0] <= 60) ||
(buf[0] >= 62 && buf[0] <= 126))) {
QP_TEST_WRAP(fp, buf, linelen, 3);
fprintf(fp, "=%2X", *buf & 0xff);
/* Point 2: 33 through 60 inclusive, and 62 through 126 */
} else {
QP_TEST_WRAP(fp, buf, linelen, 1);
fprintf(fp, "%c", *buf);
}
}
fprintf(fp, "\r\n");
}
int
enqueue(int argc, char *argv[], FILE *ofp)
{
int i, ch, tflag = 0;
char *fake_from = NULL, *buf = NULL;
struct passwd *pw;
FILE *fp = NULL, *fout;
size_t sz = 0, envid_sz = 0;
ssize_t len;
char *line;
int inheaders = 1;
int save_argc;
char **save_argv;
int no_getlogin = 0;
memset(&msg, 0, sizeof(msg));
time(×tamp);
save_argc = argc;
save_argv = argv;
while ((ch = getopt(argc, argv,
"A:B:b:E::e:F:f:iJ::L:mN:o:p:qr:R:StvV:x")) != -1) {
switch (ch) {
case 'f':
fake_from = optarg;
break;
case 'F':
msg.fromname = optarg;
break;
case 'N':
msg.dsn_notify = optarg;
break;
case 'r':
fake_from = optarg;
break;
case 'R':
msg.dsn_ret = optarg;
break;
case 'S':
no_getlogin = 1;
break;
case 't':
tflag = 1;
break;
case 'v':
verbose = 1;
break;
case 'V':
msg.dsn_envid = optarg;
break;
/* all remaining: ignored, sendmail compat */
case 'A':
case 'B':
case 'b':
case 'E':
case 'e':
case 'i':
case 'L':
case 'm':
case 'o':
case 'p':
case 'x':
break;
case 'q':
/* XXX: implement "process all now" */
return (EX_SOFTWARE);
default:
usage();
}
}
argc -= optind;
argv += optind;
if (getmailname(host, sizeof(host)) == -1)
errx(EX_NOHOST, "getmailname");
if (no_getlogin) {
if ((pw = getpwuid(getuid())) == NULL)
user = "anonymous";
if (pw != NULL)
user = xstrdup(pw->pw_name);
}
else {
uid_t ruid = getuid();
if ((user = getlogin()) != NULL && *user != '\0') {
if ((pw = getpwnam(user)) == NULL ||
(ruid != 0 && ruid != pw->pw_uid))
pw = getpwuid(ruid);
} else if ((pw = getpwuid(ruid)) == NULL) {
user = "anonymous";
}
user = xstrdup(pw ? pw->pw_name : user);
}
build_from(fake_from, pw);
while (argc > 0) {
rcpt_add(argv[0]);
argv++;
argc--;
}
if ((fp = tmpfile()) == NULL)
err(EX_UNAVAILABLE, "tmpfile");
msg.noheader = parse_message(stdin, fake_from == NULL, tflag, fp);
if (msg.rcpt_cnt == 0)
errx(EX_SOFTWARE, "no recipients");
/* init session */
rewind(fp);
/* check if working in offline mode */
/* If the server is not running, enqueue the message offline */
if (!srv_connected()) {
if (pledge("stdio", NULL) == -1)
err(1, "pledge");
return (enqueue_offline(save_argc, save_argv, fp, ofp));
}
if ((msg.fd = open_connection()) == -1)
errx(EX_UNAVAILABLE, "server too busy");
if (pledge("stdio wpath cpath", NULL) == -1)
err(1, "pledge");
fout = fdopen(msg.fd, "a+");
if (fout == NULL)
err(EX_UNAVAILABLE, "fdopen");
/*
* We need to call get_responses after every command because we don't
* support PIPELINING on the server-side yet.
*/
/* banner */
if (!get_responses(fout, 1))
goto fail;
if (!send_line(fout, verbose, "EHLO localhost\r\n"))
goto fail;
if (!get_responses(fout, 1))
goto fail;
if (msg.dsn_envid != NULL)
envid_sz = strlen(msg.dsn_envid);
if (!send_line(fout, verbose, "MAIL FROM:<%s> %s%s %s%s\r\n",
msg.from,
msg.dsn_ret ? "RET=" : "",
msg.dsn_ret ? msg.dsn_ret : "",
envid_sz ? "ENVID=" : "",
envid_sz ? msg.dsn_envid : ""))
goto fail;
if (!get_responses(fout, 1))
goto fail;
for (i = 0; i < msg.rcpt_cnt; i++) {
if (!send_line(fout, verbose, "RCPT TO:<%s> %s%s\r\n",
msg.rcpts[i],
msg.dsn_notify ? "NOTIFY=" : "",
msg.dsn_notify ? msg.dsn_notify : ""))
goto fail;
if (!get_responses(fout, 1))
goto fail;
}
if (!send_line(fout, verbose, "DATA\r\n"))
goto fail;
if (!get_responses(fout, 1))
goto fail;
/* add From */
if (!msg.saw_from && !send_line(fout, 0, "From: %s%s<%s>\r\n",
msg.fromname ? msg.fromname : "", msg.fromname ? " " : "",
msg.from))
goto fail;
/* add Date */
if (!msg.saw_date && !send_line(fout, 0, "Date: %s\r\n",
time_to_text(timestamp)))
goto fail;
if (msg.need_linesplit) {
/* we will always need to mime encode for long lines */
if (!msg.saw_mime_version && !send_line(fout, 0,
"MIME-Version: 1.0\r\n"))
goto fail;
if (!msg.saw_content_type && !send_line(fout, 0,
"Content-Type: text/plain; charset=unknown-8bit\r\n"))
goto fail;
if (!msg.saw_content_disposition && !send_line(fout, 0,
"Content-Disposition: inline\r\n"))
goto fail;
if (!msg.saw_content_transfer_encoding && !send_line(fout, 0,
"Content-Transfer-Encoding: quoted-printable\r\n"))
goto fail;
}
/* add separating newline */
if (msg.noheader) {
if (!send_line(fout, 0, "\r\n"))
goto fail;
inheaders = 0;
}
for (;;) {
if ((len = getline(&buf, &sz, fp)) == -1) {
if (feof(fp))
break;
else
err(EX_UNAVAILABLE, "getline");
}
/* newlines have been normalized on first parsing */
if (buf[len-1] != '\n')
errx(EX_SOFTWARE, "expect EOL");
len--;
if (buf[0] == '.') {
if (fputc('.', fout) == EOF)
goto fail;
}
line = buf;
if (inheaders) {
if (strncasecmp("from ", line, 5) == 0)
continue;
if (strncasecmp("return-path: ", line, 13) == 0)
continue;
}
if (msg.saw_content_transfer_encoding || msg.noheader ||
inheaders || !msg.need_linesplit) {
if (!send_line(fout, 0, "%.*s\r\n", (int)len, line))
goto fail;
if (inheaders && buf[0] == '\n')
inheaders = 0;
continue;
}
/* we don't have a content transfer encoding, use our default */
qp_encoded_write(fout, line);
}
free(buf);
if (!send_line(fout, verbose, ".\r\n"))
goto fail;
if (!get_responses(fout, 1))
goto fail;
if (!send_line(fout, verbose, "QUIT\r\n"))
goto fail;
if (!get_responses(fout, 1))
goto fail;
fclose(fp);
fclose(fout);
exit(EX_OK);
fail:
if (pw)
savedeadletter(pw, fp);
exit(EX_SOFTWARE);
}
static int
get_responses(FILE *fin, int n)
{
char *buf = NULL;
size_t sz = 0;
ssize_t len;
int e, ret = 0;
fflush(fin);
if ((e = ferror(fin))) {
warnx("ferror: %d", e);
goto err;
}
while (n) {
if ((len = getline(&buf, &sz, fin)) == -1) {
if (ferror(fin)) {
warn("getline");
goto err;
} else if (feof(fin))
break;
else
err(EX_UNAVAILABLE, "getline");
}
/* account for \r\n linebreaks */
if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n')
buf[--len - 1] = '\n';
if (len < 4) {
warnx("bad response");
goto err;
}
if (verbose)
printf("<<< %.*s", (int)len, buf);
if (buf[3] == '-')
continue;
if (buf[0] != '2' && buf[0] != '3') {
warnx("command failed: %.*s", (int)len, buf);
goto err;
}
n--;
}
ret = 1;
err:
free(buf);
return ret;
}
static int
send_line(FILE *fp, int v, char *fmt, ...)
{
int ret = 0;
va_list ap;
va_start(ap, fmt);
if (vfprintf(fp, fmt, ap) >= 0)
ret = 1;
va_end(ap);
if (ret && v) {
printf(">>> ");
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
return (ret);
}
static void
build_from(char *fake_from, struct passwd *pw)
{
char *p;
if (fake_from == NULL)
msg.from = qualify_addr(user);
else {
if (fake_from[0] == '<') {
if (fake_from[strlen(fake_from) - 1] != '>')
errx(1, "leading < but no trailing >");
fake_from[strlen(fake_from) - 1] = 0;
p = xstrdup(fake_from + 1);
msg.from = qualify_addr(p);
free(p);
} else
msg.from = qualify_addr(fake_from);
}
if (msg.fromname == NULL && fake_from == NULL && pw != NULL) {
int len, apos;
len = strcspn(pw->pw_gecos, ",");
if ((p = memchr(pw->pw_gecos, '&', len))) {
apos = p - pw->pw_gecos;
if (asprintf(&msg.fromname, "%.*s%s%.*s",
apos, pw->pw_gecos,
pw->pw_name,
len - apos - 1, p + 1) == -1)
err(1, NULL);
msg.fromname[apos] = toupper((unsigned char)msg.fromname[apos]);
} else {
if (asprintf(&msg.fromname, "%.*s", len,
pw->pw_gecos) == -1)
err(1, NULL);
}
}
}
static int
parse_message(FILE *fin, int get_from, int tflag, FILE *fout)
{
char *buf = NULL;
size_t sz = 0;
ssize_t len;
uint i, cur = HDR_NONE;
uint header_seen = 0, header_done = 0;
memset(&pstate, 0, sizeof(pstate));
for (;;) {
if ((len = getline(&buf, &sz, fin)) == -1) {
if (feof(fin))
break;
else
err(EX_UNAVAILABLE, "getline");
}
/* account for \r\n linebreaks */
if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n')
buf[--len - 1] = '\n';
if (len == 1 && buf[0] == '\n') /* end of header */
header_done = 1;
if (!WSP(buf[0])) { /* whitespace -> continuation */
if (cur == HDR_FROM)
parse_addr_terminal(1);
if (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC)
parse_addr_terminal(0);
cur = HDR_NONE;
}
/* not really exact, if we are still in headers */
if (len + (buf[len - 1] == '\n' ? 0 : 1) >= LINESPLIT)
msg.need_linesplit = 1;
for (i = 0; !header_done && cur == HDR_NONE &&
i < nitems(keywords); i++)
if ((size_t)len > strlen(keywords[i].word) &&
!strncasecmp(buf, keywords[i].word,
strlen(keywords[i].word)))
cur = keywords[i].type;
if (cur != HDR_NONE)
header_seen = 1;
if (cur != HDR_BCC) {
if (!send_line(fout, 0, "%.*s", (int)len, buf))
err(1, "write error");
if (buf[len - 1] != '\n') {
if (fputc('\n', fout) == EOF)
err(1, "write error");
}
}
/*
* using From: as envelope sender is not sendmail compatible,
* but I really want it that way - maybe needs a knob
*/
if (cur == HDR_FROM) {
msg.saw_from++;
if (get_from)
parse_addr(buf, len, 1);
}
if (tflag && (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC))
parse_addr(buf, len, 0);
if (cur == HDR_DATE)
msg.saw_date++;
if (cur == HDR_MSGID)
msg.saw_msgid++;
if (cur == HDR_MIME_VERSION)
msg.saw_mime_version = 1;
if (cur == HDR_CONTENT_TYPE)
msg.saw_content_type = 1;
if (cur == HDR_CONTENT_DISPOSITION)
msg.saw_content_disposition = 1;
if (cur == HDR_CONTENT_TRANSFER_ENCODING)
msg.saw_content_transfer_encoding = 1;
if (cur == HDR_USER_AGENT)
msg.saw_user_agent = 1;
}
free(buf);
return (!header_seen);
}
static void
parse_addr(char *s, size_t len, int is_from)
{
size_t pos = 0;
int terminal = 0;
/* unless this is a continuation... */
if (!WSP(s[pos]) && s[pos] != ',' && s[pos] != ';') {
/* ... skip over everything before the ':' */
for (; pos < len && s[pos] != ':'; pos++)
; /* nothing */
/* ... and check & reset parser state */
parse_addr_terminal(is_from);
}
/* skip over ':' ',' ';' and whitespace */
for (; pos < len && !pstate.quote && (WSP(s[pos]) || s[pos] == ':' ||
s[pos] == ',' || s[pos] == ';'); pos++)
; /* nothing */
for (; pos < len; pos++) {
if (!pstate.esc && !pstate.quote && s[pos] == '(')
pstate.comment++;
if (!pstate.comment && !pstate.esc && s[pos] == '"')
pstate.quote = !pstate.quote;
if (!pstate.comment && !pstate.quote && !pstate.esc) {
if (s[pos] == ':') { /* group */
for (pos++; pos < len && WSP(s[pos]); pos++)
; /* nothing */
pstate.wpos = 0;
}
if (s[pos] == '\n' || s[pos] == '\r')
break;
if (s[pos] == ',' || s[pos] == ';') {
terminal = 1;
break;
}
if (s[pos] == '<') {
pstate.brackets = 1;
pstate.wpos = 0;
}
if (pstate.brackets && s[pos] == '>')
terminal = 1;
}
if (!pstate.comment && !terminal && (!(!(pstate.quote ||
pstate.esc) && (s[pos] == '<' || WSP(s[pos]))))) {
if (pstate.wpos >= sizeof(pstate.buf))
errx(1, "address exceeds buffer size");
pstate.buf[pstate.wpos++] = s[pos];
}
if (!pstate.quote && pstate.comment && s[pos] == ')')
pstate.comment--;
if (!pstate.esc && !pstate.comment && s[pos] == '\\')
pstate.esc = 1;
else
pstate.esc = 0;
}
if (terminal)
parse_addr_terminal(is_from);
for (; pos < len && (s[pos] == '\r' || s[pos] == '\n'); pos++)
; /* nothing */
if (pos < len)
parse_addr(s + pos, len - pos, is_from);
}
static void
parse_addr_terminal(int is_from)
{
if (pstate.comment || pstate.quote || pstate.esc)
errx(1, "syntax error in address");
if (pstate.wpos) {
if (pstate.wpos >= sizeof(pstate.buf))
errx(1, "address exceeds buffer size");
pstate.buf[pstate.wpos] = '\0';
if (is_from)
msg.from = qualify_addr(pstate.buf);
else
rcpt_add(pstate.buf);
pstate.wpos = 0;
}
}
static char *
qualify_addr(char *in)
{
char *out;
if (strlen(in) > 0 && strchr(in, '@') == NULL) {
if (asprintf(&out, "%s@%s", in, host) == -1)
err(1, "qualify asprintf");
} else
out = xstrdup(in);
return (out);
}
static void
rcpt_add(char *addr)
{
void *nrcpts;
char *p;
int n;
n = 1;
p = addr;
while ((p = strchr(p, ',')) != NULL) {
n++;
p++;
}
if ((nrcpts = reallocarray(msg.rcpts,
msg.rcpt_cnt + n, sizeof(char *))) == NULL)
err(1, "rcpt_add realloc");
msg.rcpts = nrcpts;
while (n--) {
if ((p = strchr(addr, ',')) != NULL)
*p++ = '\0';
msg.rcpts[msg.rcpt_cnt++] = qualify_addr(addr);
if (p == NULL)
break;
addr = p;
}
}
static int
open_connection(void)
{
struct imsg imsg;
int fd;
int n;
imsg_compose(ibuf, IMSG_CTL_SMTP_SESSION, IMSG_VERSION, 0, -1, NULL, 0);
while (ibuf->w.queued)
if (msgbuf_write(&ibuf->w) <= 0 && errno != EAGAIN)
err(1, "write error");
while (1) {
if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
errx(1, "imsg_read error");
if (n == 0)
errx(1, "pipe closed");
if ((n = imsg_get(ibuf, &imsg)) == -1)
errx(1, "imsg_get error");
if (n == 0)
continue;
switch (imsg.hdr.type) {
case IMSG_CTL_OK:
break;
case IMSG_CTL_FAIL:
errx(1, "server disallowed submission request");
default:
errx(1, "unexpected imsg reply type");
}
fd = imsg_get_fd(&imsg);
imsg_free(&imsg);
break;
}
return fd;
}
static int
enqueue_offline(int argc, char *argv[], FILE *ifile, FILE *ofile)
{
int i, ch;
for (i = 1; i < argc; i++) {
if (strchr(argv[i], '|') != NULL) {
warnx("%s contains illegal character", argv[i]);
ftruncate(fileno(ofile), 0);
exit(EX_SOFTWARE);
}
if (fprintf(ofile, "%s%s", i == 1 ? "" : "|", argv[i]) < 0)
goto write_error;
}
if (fputc('\n', ofile) == EOF)
goto write_error;
while ((ch = fgetc(ifile)) != EOF) {
if (fputc(ch, ofile) == EOF)
goto write_error;
}
if (ferror(ifile)) {
warn("read error");
ftruncate(fileno(ofile), 0);
exit(EX_UNAVAILABLE);
}
if (fclose(ofile) == EOF)
goto write_error;
return (EX_TEMPFAIL);
write_error:
warn("write error");
ftruncate(fileno(ofile), 0);
exit(EX_UNAVAILABLE);
}
static int
savedeadletter(struct passwd *pw, FILE *in)
{
char buffer[PATH_MAX];
FILE *fp;
char *buf = NULL;
size_t sz = 0;
ssize_t len;
(void)snprintf(buffer, sizeof buffer, "%s/dead.letter", pw->pw_dir);
if (fseek(in, 0, SEEK_SET) != 0)
return 0;
if ((fp = fopen(buffer, "w")) == NULL)
return 0;
/* add From */
if (!msg.saw_from)
fprintf(fp, "From: %s%s<%s>\n",
msg.fromname ? msg.fromname : "",
msg.fromname ? " " : "",
msg.from);
/* add Date */
if (!msg.saw_date)
fprintf(fp, "Date: %s\n", time_to_text(timestamp));
if (msg.need_linesplit) {
/* we will always need to mime encode for long lines */
if (!msg.saw_mime_version)
fprintf(fp, "MIME-Version: 1.0\n");
if (!msg.saw_content_type)
fprintf(fp, "Content-Type: text/plain; "
"charset=unknown-8bit\n");
if (!msg.saw_content_disposition)
fprintf(fp, "Content-Disposition: inline\n");
if (!msg.saw_content_transfer_encoding)
fprintf(fp, "Content-Transfer-Encoding: "
"quoted-printable\n");
}
/* add separating newline */
if (msg.noheader)
fprintf(fp, "\n");
while ((len = getline(&buf, &sz, in)) != -1) {
if (buf[len - 1] == '\n')
buf[len - 1] = '\0';
fprintf(fp, "%s\n", buf);
}
free(buf);
fprintf(fp, "\n");
fclose(fp);
return 1;
}