summaryrefslogtreecommitdiff
path: root/daemons/lmail.c
blob: 33ccf66b8cca6e932fdb132e33c4acfbe4b5a9b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
/* Local mail delivery

   Copyright (C) 1996, 1997, 1998, 1999 Free Software Foundation, Inc.

   Written by Miles Bader <miles@gnu.ai.mit.edu>

   This file is part of the GNU Hurd.

   The GNU Hurd is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2, or (at
   your option) any later version.

   The GNU Hurd 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
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA. */

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <pwd.h>
#include <syslog.h>
#include <sysexits.h>
#include <paths.h>
#include <argp.h>
#include <hurd.h>
#include <hurd/fd.h>
#include <version.h>
#include <time.h>
#include <sys/time.h>
#include <sys/mman.h>

#define OPT_FILE -5
#define OPT_REMOVE -6

const char *argp_program_version = STANDARD_HURD_VERSION (mail.local);

static const struct argp_option
options[] =
{
  {"from",    'f',	"USER",	0, "Record sender as USER"},
  {0,         'd',	0,     	OPTION_ALIAS|OPTION_HIDDEN},
  {0,         'r',	0,     	OPTION_ALIAS|OPTION_HIDDEN},
  {"file",    OPT_FILE, "FILE",	0, "Deliver FILE instead of standard input"},
  {"remove",  OPT_REMOVE, 0,   	0, "Remove FILE after successful delivery"},
  {"mail-dir",'m',	"DIR", 	0, "Look for mailboxes in DIR"},
  {"use-lock-file",'l',	0,     	OPTION_HIDDEN,
   "Use a lock file instead of flock for mailboxes"},
  {0}
};
static const char args_doc[] = "USER...";
static const char doc[] = "Deliver mail to the local mailboxes of USER...";

#define HDR_PFX "From "		/* Header, at the beginning of a line,
				   starting each msg in a mailbox.  */
#define ESC_PFX ">"		/* Used to escape occurrences of HDR_PFX in
				   the msg body.  */

#define BMAX (64*1024)		/* Chunk size for I/O.  */

struct params
{
  char *from;			/* Who the mail is from.  */
  char *mail_dir;		/* Mailbox directory.  */
};

/* Convert the system error code ERR to an appropriate exit value.  This
   function currently only returns three sorts: success, temporary failure,
   or error, with exit codes 0, EX_TEMPFAIL, or EX_UNAVAILABLE.  The table of
   temporary failures is from the bsd original of this program.  */
static int
err_to_ex (error_t err)
{
  switch (err)
    {
    case 0:
      return 0;			/* Success */
    /* Temporary failures: */
#if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN)
    case EWOULDBLOCK:	/* Operation would block. */
#endif
    case EAGAIN:		/* Resource temporarily unavailable */
    case EDQUOT:		/* Disc quota exceeded */
    case EBUSY:			/* Device busy */
    case EPROCLIM:		/* Too many processes */
    case EUSERS:		/* Too many users */
    case ECONNABORTED:		/* Software caused connection abort */
    case ECONNREFUSED:		/* Connection refused */
    case ECONNRESET:		/* Connection reset by peer */
    case EDEADLK:		/* Resource deadlock avoided */
    case EFBIG:			/* File too large */
    case EHOSTDOWN:		/* Host is down */
    case EHOSTUNREACH:		/* No route to host */
    case EMFILE:		/* Too many open files */
    case ENETDOWN:		/* Network is down */
    case ENETRESET:		/* Network dropped connection on reset */
    case ENETUNREACH:		/* Network is unreachable */
    case ENFILE:		/* Too many open files in system */
    case ENOBUFS:		/* No buffer space available */
    case ENOMEM:		/* Cannot allocate memory */
    case ENOSPC:		/* No space left on device */
    case EROFS:			/* Read-only file system */
    case ESTALE:		/* Stale NFS file handle */
    case ETIMEDOUT:		/* Connection timed out */
      return EX_TEMPFAIL;
    default:
      return EX_UNAVAILABLE;
    }
}

/* Print and syslog the given error message, with the system error string for
   ERRNO appended.  Return an appropriate exit code for ERRNO.  */
#define SYSERR(fmt, args...) \
  ({ syslog (LOG_ERR, fmt ": %m" , ##args); err_to_ex (errno); })

/* Print and syslog the given error message.  Return the exit code
   EX_UNAVAILABLE.  */
#define ERR(fmt, args...) \
  ({ syslog (LOG_ERR, fmt , ##args); EX_UNAVAILABLE; })

/* Print and syslog the given error message, with the system error string for
   CODE appended.  Return an appropriate exit code for CODE.  */
#define SYSERRX(code, fmt, args...)					      \
  ({ error_t _code = (code);						      \
     syslog (LOG_ERR, fmt ": %s" , ##args , strerror (_code));		      \
     err_to_ex (_code); })

/* Block I/O functions.  These are structured to allow the use of a read
   method that avoids copying the data locally.  */

/* Free block allocated by bread.  */
static void
bfree (char *blk, size_t blk_len)
{
  if (blk_len > 0)
    munmap (blk, blk_len);
}

/* Read up to MAX chars from IN into BLK & BLK_LEN, which may be reused or
   freed.  */
static int
bread (int in, char *in_name, size_t max, char **blk, size_t *blk_len)
{
  char *orig_blk = *blk;
  size_t orig_blk_len = *blk_len;
  error_t err = HURD_DPORT_USE (in, io_read (port, blk, blk_len, -1, max));

  if (err)
    return SYSERRX (err, "%s", in_name);

  if (*blk != orig_blk)
    bfree (orig_blk, orig_blk_len);

  return 0;
}

/* Write BLK & BLK_LEN to OUT.  An exit status is returned.  */
static int
bwrite (int out, char *out_name, const char *blk, size_t blk_len)
{
  while (blk_len > 0)
    {
      ssize_t wr = write (out, blk, blk_len);
      if (wr < 0)
	return SYSERR ("%s", out_name);
      blk += wr;
      blk_len -= wr;
    }
  return 0;
}

/* Copy from file descriptor IN to OUT.  An exit status is returned.  */
static int
copy (int in, char *in_name, int out, char *out_name)
{
  int ex = 0;
  char *blk = 0;
  size_t blk_len = 0;

  do
    {
      ex = bread (in, in_name, BMAX, &blk, &blk_len);
      if (! ex)
	ex = bwrite (out, out_name, blk, blk_len);
    }
  while (blk_len > 0 && !ex);

  bfree (blk, blk_len);

  return ex;
}

static int
write_header (int out, char *out_name, struct params *params)
{
  char *hdr;
  size_t hdr_len;
  struct timeval tv;
  time_t time;
  int ex = 0;

  if (gettimeofday (&tv, 0) < 0)
    return SYSERR ("gettimeofday");

  /* Note that the string returned by ctime includes a terminating newline.  */
  time = tv.tv_sec;
  hdr_len = asprintf (&hdr, "From %s %s", params->from, ctime (&time));
  if (! hdr)
    return SYSERRX (ENOMEM, "%s", out_name);

  ex = bwrite (out, out_name, hdr, hdr_len);

  free (hdr);

  return ex;
}

/* Copy from file descriptor IN to OUT, making any changes needed to make the
   contents a valid mailbox entry.  These include:
    (1) Prepending a `From ...' line, and appending a blank line.
    (2) Replacing any occurrences of `From ' at the beginning of lines with
        `>From '.
   An exit status is returned.  */
static int
process (int in, char *in_name, int out, char *out_name, struct params *params)
{
  /* The block currently being processed.  */
  char *blk = 0;
  size_t blk_len = 0;
  /* MATCH is the string we're looking for to escape, NL_MATCH is the same
     string prefixed by a newline to ease searching (MATCH only matches at
     the beginning of lines).  */
  const char *const nl_match = "\n" HDR_PFX, *const match = nl_match + 1;
  /* The portion of MATCH that matched the end of the previous block.  0
     means that there was at least a newline, so initializing MATCHED to 0
     simulates a newline at the start of IN.  */
  ssize_t matched = 0;
  int ex = write_header (out, out_name, params);

#define match_len (sizeof HDR_PFX - 1)

  if (ex)
    return ex;

#define BWRITE(p, p_len)						      \
  ({ size_t _len = (p_len);						      \
     if (_len > 0 && (ex = bwrite (out, out_name, p, _len)))		      \
       break; })

  do
    {
      char *start, *end;

      ex = bread (in, in_name, BMAX, &blk, &blk_len);

      if (matched >= 0)
	/* The last block ended in a partial match, so see if we can complete
	   it in this block.  */
	{
	  if (blk_len >= match_len - matched
	      && memcmp (blk, match + matched, match_len - matched) == 0)
	    /* It matched; output the escape prefix before the match.  */
	    BWRITE (ESC_PFX, sizeof ESC_PFX - 1);
	  /* Now we have to output the part of the preceding block that we
	     didn't do before because it was a partial match.  */
	  BWRITE (match, matched);
	  /* Ok, we're all caught up.  The portion of the match that's in
	     this block will be output as normal text.  */
	  matched = -1;
	}

      /* Scan through the block looking for matches.  */
      for (start = end = blk; start < blk + blk_len; start = end)
	{
	  /* Look for our match, prefixed by a newline.  */
	  end = memmem (start, blk + blk_len - start, nl_match, match_len + 1);
	  if (end)
	    /* We found a match, output the escape prefix before it.  */
	    {
	      end++;		/* The newline should precede the escape.  */
	      BWRITE (start, end - start); /* part of block before match.  */
	      BWRITE (ESC_PFX, sizeof ESC_PFX - 1); /* escape prefix.  */
	    }
	  else
	    {
	      end = blk + blk_len;
	      break;
	    }
	}

      /* Now see if there are any partial matches at the end.  */
      for (matched =
	     end - start < match_len + 1 ? end - start - 1 : match_len;
	   matched >= 0;
	   matched--)
	if (memcmp (end - matched - 1, nl_match, matched + 1) == 0)
	  /* There's a partial match MATCHED characters long at the end of
	     this block, so delay outputting it until the next block can be
	     examined; we do output the preceding newline here, though.  */
	  {
	    end -= matched;
	    break;
	  }

      BWRITE (start, end - start);
    }
  while (blk_len > 0);

  if (! ex)
    ex = bwrite (out, out_name, "\n", 1); /* Append a blank line.  */

  bfree (blk, blk_len);

  return ex;
}

/* Deliver flags. */
#define D_PROCESS 0x1		/* Deliver */
#define D_REWIND  0x2		/* Rewind MSG before using it.  */

/* Deliver the text from the file descriptor MSG to the mailbox of the user
   RCPT in MAIL_DIR.  FLAGS is from the set D_* above.  An exit appropriate
   exit code is returned.  */
static int
deliver (int msg, char *msg_name, char *rcpt, int flags, struct params *params)
{
  char *mbox;			/* Name of mailbox */
  int fd;			/* Opened mailbox */
  struct stat stat;
  int ex = 0;			/* Exit status */
  struct passwd *pw = getpwnam (rcpt); /* Details of recipient */

  if (! pw)
    return ERR ("%s: Unknown user", rcpt);

  asprintf (&mbox, "%s/%s", params->mail_dir, rcpt);
  if (! mbox)
    return SYSERRX (ENOMEM, "%s", rcpt);

  do
    {
      /* First try to open an existing mailbox.  */
      fd = open (mbox, O_WRONLY|O_APPEND|O_NOLINK|O_EXLOCK);
      if (fd < 0 && errno == ENOENT)
	/* There is none; try to create it.  */
	{
	  fd = open (mbox, O_WRONLY|O_APPEND|O_CREAT|O_EXCL|O_NOLINK|O_EXLOCK,
		     S_IRUSR|S_IWUSR);
	  if (fd >= 0)
	    /* Made a new mailbox!  Set the owner and group appropriately.  */
	    {
	      if (fchown (fd, pw->pw_uid, pw->pw_gid) < 0)
		{
		  close (fd);
		  fd = -1;	/* Propagate error.  */
		}
	    }
	}
    }
  /* EEXIST can only be returned someone else created the mailbox between the
     two opens above, so if we try again, the first open should work.  */
  while (fd < 0 && errno == EEXIST);

  if (fd < 0 || fstat (fd, &stat) < 0)
    ex = SYSERR ("%s", mbox);
  else if (S_ISLNK (stat.st_mode) || stat.st_nlink != 1)
    ex = ERR ("%s: Is linked", mbox);
  else
    {
      if (flags & D_REWIND)
	{
	  if (lseek (msg, 0L, SEEK_SET) < 0)
	    ex = SYSERR ("%s", msg_name);
	}
      if (! ex)
	{
	  if (flags & D_PROCESS)
	    ex = process (msg, msg_name, fd, mbox, params);
	  else
	    ex = copy (msg, msg_name, fd, mbox);
	}
    }

  if (fd >= 0)
    {
      if (fsync (fd) < 0 && !ex)
	ex = SYSERR ("%s", mbox);
      if (close (fd) < 0 && !ex)
	ex = SYSERR ("%s", mbox);
    }
  free (mbox);

  return ex;
}

/* Process from the file descriptor IN into a temporary file, which return a
   descriptor to in CACHED; once *CACHED is closed, it will go away
   permanently.  The file pointer of *CACHED is at an undefined location.  An
   exit status is returned.  */
static int
cache (int in, char *in_name, struct params *params, int *cached)
{
  int ex;
  error_t err;
  file_t file;			/* Hurd port for temp file */
  int fd;			/* File descriptor for it */
  file_t tmp_dir = file_name_lookup (_PATH_TMP, O_RDONLY, 0);

  if (tmp_dir == MACH_PORT_NULL)
    return SYSERR ("%s", _PATH_TMP);

  /* Create FILE without actually putting it into TMP_DIR.  */
  err = dir_mkfile (tmp_dir, O_RDWR, 0600, &file);
  if (err)
    return SYSERRX (err, "%s", _PATH_TMP);

  fd = _hurd_intern_fd (file, O_RDWR, 1);
  if (fd < 0)
    return SYSERR ("%s", _PATH_TMP);

  ex = process (in, in_name, fd, _PATH_TMP, params);
  if (! ex)
    *cached = fd;
  else
    close (fd);

  return ex;
}

int
main (int argc, char **argv)
{
  int rcpt = 0;			/* Index in ARGV of next recipient.  */
  char *file = 0;		/* File containing message.  */
  int remove = 0;		/* Remove file after successful delivery.  */
  int in = 0;			/* Raw input file descriptor.  */
  int ex = 0;			/* Exit status.  */
  struct params params = { from: 0, mail_dir: _PATH_MAILDIR };

  error_t parse_opt (int key, char *arg, struct argp_state *state)
    {
      switch (key)
	{
	case 'd':
	  /* do nothing; compatibility */
	  break;
	case 'f':
	case 'r':
	  params.from = arg; break;
	case OPT_FILE:
	  file = arg; break;
	case OPT_REMOVE:
	  remove = 1; break;
	case 'm':
	  params.mail_dir = arg; break;
	case 'l':
	  argp_failure (state, EX_USAGE, EINVAL, "-l not supported");
	case ARGP_KEY_NO_ARGS:
	  argp_error (state, "No recipients");
	default:
	  return ARGP_ERR_UNKNOWN;
	}
      return 0;
    }
  const struct argp argp = { options, parse_opt, args_doc, doc };

  /* Parse arguments.  */
  argp_parse (&argp, argc, argv, 0, &rcpt, 0);

  /* Use syslog to log errors.  */
  openlog ("mail.local", LOG_PERROR, LOG_MAIL);

  if (! params.from)
    /* No from address specified, use the current user.  */
    {
      struct passwd *pw;
      int uid = getuid ();

      if (uid == -1)
	exit (ERR ("No user id"));

      pw = getpwuid (uid);
      if (! pw)
	exit (ERR ("%d: Unknown uid", uid));

      params.from = strdup (pw->pw_name);
    }

  if (file)
    /* Use FILE as the message contents.  */
    {
      in = open (file, O_RDONLY);
      if (in < 0)
	exit (SYSERR ("%s", file));
    }
  else
    /* Use standard input.  */
    in = 0;

  if (rcpt == argc - 1)
    /* Only a single recipient.  */
    ex = deliver (in, file ?: "-", argv[rcpt], D_PROCESS, &params);
  else
    /* Multiple recipients.  */
    {
      int cached = 0;		/* Temporary processed input file.  */

      ex = cache (in, file ?: "-", &params, &cached);
      if (! ex)
	while (rcpt < argc)
	  {
	    /* Deliver to one recipient.  */
	    int rex = deliver (cached, "message cache", argv[rcpt++],
			       D_REWIND, &params);

	    /* Merge the exit code for that recipient.  Temporary failures
	       take precedence over hard failures and success, as
	       subsequently delivering duplicates (of the successful
	       messages) is preferable to not delivering the temporary
	       failures.  */
	    if (ex != EX_TEMPFAIL)
	      {
		if (rex == EX_TEMPFAIL)
		  ex = EX_TEMPFAIL;
		else if (! ex)
		  ex = rex;
	      }
	  }
    }

  if (file && remove && !ex)
    unlink (file);

  exit (ex);
}