/* * Copyright 2006 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. * * Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T * All Rights Reserved */ /* * Vacation * Copyright (c) 1983 Eric P. Allman * Berkeley, California * * Copyright (c) 1983 Regents of the University of California. * All rights reserved. The Berkeley software License Agreement * specifies the terms and conditions for redistribution. */ #pragma ident "%Z%%M% %I% %E% SMI" #ifndef lint static char SccsId[] = "%W% %E% SMI"; #endif /* not lint */ #include #include #include #include #include #include #include #include #include #include #include #include /* * VACATION -- return a message to the sender when on vacation. * * This program could be invoked as a message receiver * when someone is on vacation. It returns a message * specified by the user to whoever sent the mail, taking * care not to return a message too often to prevent * "I am on vacation" loops. * * For best operation, this program should run setuid to * root or uucp or someone else that sendmail will believe * a -f flag from. Otherwise, the user must be careful * to include a header on his .vacation.msg file. * * Positional Parameters: * the user to collect the vacation message from. * * Flag Parameters: * -I initialize the database. * -d turn on debugging. * -tT set the timeout to T. messages arriving more * often than T will be ignored to avoid loops. * * Side Effects: * A message is sent back to the sender. * * Author: * Eric Allman * UCB/INGRES */ #define MAXLINE 256 /* max size of a line */ #define ONEWEEK (60L*60L*24L*7L) #define MsgFile "/.vacation.msg" #define FilterFile "/.vacation.filter" #define DbFileBase "/.vacation" #define _PATH_TMP "/tmp/vacation.XXXXXX" typedef int bool; #define FALSE 0 #define TRUE 1 static time_t Timeout = ONEWEEK; /* timeout between notices per user */ static DBM *db; static bool Debug = FALSE; static bool AnswerAll = FALSE; /* default: answer if in To:/Cc: only */ static char *Subject = NULL; /* subject in message header */ static char *EncodedSubject = NULL; /* subject in message header */ static char Charset[MAXLINE]; /* for use in reply message */ static char *AliasList[MAXLINE]; /* list of aliases to allow */ static int AliasCount = 0; static char *myname; /* name of person "on vacation" */ static char *homedir; /* home directory of said person */ extern time_t convtime(char *, char); extern bool decode_rfc2047(char *, char *, char *); static bool ask(char *); static bool junkmail(char *); static bool filter_ok(char *, char *); static bool knows(char *); static bool sameword(char *, char *); static char *getfrom(char **); static char *newstr(char *); static void AutoInstall(); static void initialize(char *); static void sendmessage(char *, char *, char *); static void setknows(char *); void usrerr(const char *, ...); int main(argc, argv) int argc; char **argv; { char *from; char *p, *at, *c; struct passwd *pw; char *shortfrom; char buf[MAXLINE]; char *message_file = MsgFile; char *db_file_base = DbFileBase; char *filter_file = FilterFile; char *sender; bool sender_oob = FALSE; bool initialize_only = FALSE; /* process arguments */ while (--argc > 0 && (p = *++argv) != NULL && *p == '-') { switch (*++p) { case 'a': /* add this to list of acceptable aliases */ AliasList[AliasCount++] = argv[1]; if (argc > 0) { argc--; argv++; } break; case 'd': /* debug */ Debug = TRUE; break; case 'e': /* alternate filter file */ filter_file = argv[1]; if (argc > 0) { argc--; argv++; } break; case 'f': /* alternate database file name base */ db_file_base = argv[1]; if (argc > 0) { argc--; argv++; } break; case 'I': /* initialize */ initialize_only = TRUE; break; case 'j': /* answer all mail, even if not in To/Cc */ AnswerAll = TRUE; break; case 'm': /* alternate message file */ message_file = argv[1]; if (argc > 0) { argc--; argv++; } break; case 's': /* sender: use this instead of getfrom() */ sender = argv[1]; sender_oob = TRUE; if (argc > 0) { argc--; argv++; } break; case 't': /* set timeout */ Timeout = convtime(++p, 'w'); break; default: usrerr("Unknown flag -%s", p); exit(EX_USAGE); } } if (initialize_only) { initialize(db_file_base); exit(EX_OK); } /* verify recipient argument */ if (argc == 0) AutoInstall(); if (argc != 1) { usrerr("Usage: vacation username (or) vacation -I"); exit(EX_USAGE); } myname = p; Charset[0] = '\0'; /* find user's home directory */ pw = getpwnam(myname); if (pw == NULL) { usrerr("user %s look up failed, name services outage ?", myname); exit(EX_TEMPFAIL); } homedir = newstr(pw->pw_dir); (void) snprintf(buf, sizeof (buf), "%s%s%s", homedir, (db_file_base[0] == '/') ? "" : "/", db_file_base); if (!(db = dbm_open(buf, O_RDWR, 0))) { usrerr("%s: %s\n", buf, strerror(errno)); exit(EX_DATAERR); } if (sender_oob) { at = strchr(sender, '@'); if (at != NULL) for (c = at + 1; *c; c++) *c = (char)tolower((char)*c); from = sender; shortfrom = sender; } else /* read message from standard input (just from line) */ from = getfrom(&shortfrom); /* check if junk mail or this person is already informed */ if (!junkmail(shortfrom) && filter_ok(shortfrom, filter_file) && !knows(shortfrom)) { /* mark this person as knowing */ setknows(shortfrom); /* send the message back */ (void) strlcpy(buf, homedir, sizeof (buf)); if (message_file[0] != '/') (void) strlcat(buf, "/", sizeof (buf)); (void) strlcat(buf, message_file, sizeof (buf)); if (Debug) printf("Sending %s to %s\n", buf, from); else { sendmessage(buf, from, myname); /*NOTREACHED*/ } } while (fgets(buf, MAXLINE, stdin) != NULL) continue; /* drain input */ return (EX_OK); } /* * GETFROM -- read message from standard input and return sender * * Parameters: * none. * * Returns: * pointer to the sender address. * * Side Effects: * Reads first line from standard input. */ static char * getfrom(shortp) char **shortp; { static char line[MAXLINE]; char *p, *start, *at, *bang, *c; char saveat; /* read the from line */ if (fgets(line, sizeof (line), stdin) == NULL || strncmp(line, "From ", 5) != NULL) { usrerr("No initial From line"); exit(EX_PROTOCOL); } /* find the end of the sender address and terminate it */ start = &line[5]; p = strchr(start, ' '); if (p == NULL) { usrerr("Funny From line '%s'", line); exit(EX_PROTOCOL); } *p = '\0'; /* * Strip all but the rightmost UUCP host * to prevent loops due to forwarding. * Start searching leftward from the leftmost '@'. * a!b!c!d yields a short name of c!d * a!b!c!d@e yields a short name of c!d@e * e@a!b!c yields the same short name */ #ifdef VDEBUG printf("start='%s'\n", start); #endif /* VDEBUG */ *shortp = start; /* assume whole addr */ if ((at = strchr(start, '@')) == NULL) /* leftmost '@' */ at = p; /* if none, use end of addr */ saveat = *at; *at = '\0'; if ((bang = strrchr(start, '!')) != NULL) { /* rightmost '!' */ char *bang2; *bang = '\0'; /* 2nd rightmost '!' */ if ((bang2 = strrchr(start, '!')) != NULL) *shortp = bang2 + 1; /* move past ! */ *bang = '!'; } *at = saveat; #ifdef VDEBUG printf("place='%s'\n", *shortp); #endif /* VDEBUG */ for (c = at + 1; *c; c++) *c = (char)tolower((char)*c); /* return the sender address */ return (start); } /* * JUNKMAIL -- read the header and tell us if this is junk/bulk mail. * * Parameters: * from -- the Return-Path of the sender. We assume that * anything from "*-REQUEST@*" is bulk mail. * * Returns: * TRUE -- if this is junk or bulk mail (that is, if the * sender shouldn't receive a response). * FALSE -- if the sender deserves a response. * * Side Effects: * May read the header from standard input. When this * returns the position on stdin is undefined. */ static bool junkmail(from) char *from; { register char *p; char buf[MAXLINE+1]; bool inside, onlist; /* test for inhuman sender */ p = strrchr(from, '@'); if (p != NULL) { *p = '\0'; if (sameword(&p[-8], "-REQUEST") || sameword(&p[-10], "Postmaster") || sameword(&p[-13], "MAILER-DAEMON")) { *p = '@'; return (TRUE); } *p = '@'; } #define Delims " \n\t:,:;()<>@!" /* read the header looking for "interesting" lines */ inside = FALSE; onlist = FALSE; while (fgets(buf, MAXLINE, stdin) != NULL && buf[0] != '\n') { if (buf[0] != ' ' && buf[0] != '\t' && strchr(buf, ':') == NULL) return (FALSE); /* no header found */ p = strtok(buf, Delims); if (p == NULL) continue; if (sameword(p, "To") || sameword(p, "Cc")) { inside = TRUE; p = strtok((char *)NULL, Delims); if (p == NULL) continue; } else /* continuation line? */ if (inside) inside = (buf[0] == ' ' || buf[0] == '\t'); if (inside) { int i; do { if (sameword(p, myname)) onlist = TRUE; /* I am on the list */ for (i = 0; i < AliasCount; i++) if (sameword(p, AliasList[i])) onlist = TRUE; /* alias on list */ } while (p = strtok((char *)NULL, Delims)); continue; } if (sameword(p, "Precedence")) { /* find the value of this field */ p = strtok((char *)NULL, Delims); if (p == NULL) continue; /* see if it is "junk" or "bulk" */ p[4] = '\0'; if (sameword(p, "junk") || sameword(p, "bulk")) return (TRUE); } if (sameword(p, "Subject")) { char *decoded_subject; Subject = newstr(buf+9); if (p = strrchr(Subject, '\n')) *p = '\0'; EncodedSubject = newstr(Subject); decoded_subject = newstr(Subject); if (decode_rfc2047(Subject, decoded_subject, Charset)) Subject = decoded_subject; else Charset[0] = '\0'; if (Debug) printf("Subject=%s\n", Subject); } } if (AnswerAll) return (FALSE); else return (!onlist); } /* * FILTER_OK -- see if the Return-Path is in the filter file. * Note that a non-existent filter file means everything * is OK, but an empty file means nothing is OK. * * Parameters: * from -- the Return-Path of the sender. * * Returns: * TRUE -- if this is in the filter file * (sender should receive a response). * FALSE -- if the sender does not deserve a response. */ static bool filter_ok(from, filter_file) char *from; char *filter_file; { char file[MAXLINE]; char line[MAXLINE]; char *match_start; size_t line_len, from_len; bool result = FALSE; bool negated = FALSE; FILE *f; from_len = strlen(from); (void) strlcpy(file, homedir, sizeof (file)); if (filter_file[0] != '/') (void) strlcat(file, "/", sizeof (file)); (void) strlcat(file, filter_file, sizeof (file)); f = fopen(file, "r"); if (f == NULL) { /* * If the file does not exist, then there is no filter to * apply, so we simply return TRUE. */ if (Debug) (void) printf("%s does not exist, filter ok.\n", file); return (TRUE); } while (fgets(line, MAXLINE, f)) { line_len = strlen(line); /* zero out trailing newline */ if (line[line_len - 1] == '\n') line[--line_len] = '\0'; /* skip blank lines */ if (line_len == 0) continue; /* skip comment lines */ if (line[0] == '#') continue; if (line[0] == '!') { negated = TRUE; match_start = &line[1]; line_len--; } else { negated = FALSE; match_start = &line[0]; } if (strchr(line, '@') != NULL) { /* @ => full address */ if (strcasecmp(match_start, from) == 0) { result = TRUE; if (Debug) (void) printf("filter match on %s\n", line); break; } } else { /* no @ => domain */ if (from_len <= line_len) continue; /* * Make sure the last part of from is the domain line * and that the character immediately preceding is an * '@' or a '.', otherwise we could get false positives * from e.g. twinsun.com for sun.com . */ if (strncasecmp(&from[from_len - line_len], match_start, line_len) == 0 && (from[from_len - line_len -1] == '@' || from[from_len - line_len -1] == '.')) { result = TRUE; if (Debug) (void) printf("filter match on %s\n", line); break; } } } (void) fclose(f); if (Debug && !result) (void) printf("no filter match\n"); return (!negated && result); } /* * KNOWS -- predicate telling if user has already been informed. * * Parameters: * user -- the user who sent this message. * * Returns: * TRUE if 'user' has already been informed that the * recipient is on vacation. * FALSE otherwise. * * Side Effects: * none. */ static bool knows(user) char *user; { datum key, data; time_t now, then; (void) time(&now); key.dptr = user; key.dsize = strlen(user) + 1; data = dbm_fetch(db, key); if (data.dptr == NULL) return (FALSE); bcopy(data.dptr, (char *)&then, sizeof (then)); if (then + Timeout < now) return (FALSE); if (Debug) printf("User %s already knows\n", user); return (TRUE); } /* * SETKNOWS -- set that this user knows about the vacation. * * Parameters: * user -- the user who should be marked. * * Returns: * none. * * Side Effects: * The dbm file is updated as appropriate. */ static void setknows(user) char *user; { datum key, data; time_t now; key.dptr = user; key.dsize = strlen(user) + 1; (void) time(&now); data.dptr = (char *)&now; data.dsize = sizeof (now); dbm_store(db, key, data, DBM_REPLACE); } static bool any8bitchars(line) char *line; { char *c; for (c = line; *c; c++) if (*c & 0x80) return (TRUE); return (FALSE); } /* * SENDMESSAGE -- send a message to a particular user. * * Parameters: * msgf -- filename containing the message. * user -- user who should receive it. * * Returns: * none. * * Side Effects: * sends mail to 'user' using /usr/lib/sendmail. */ static void sendmessage(msgf, user, myname) char *msgf; char *user; char *myname; { FILE *f, *fpipe, *tmpf; char line[MAXLINE]; char *p, *tmpf_name; int i, pipefd[2], tmpfd; bool seen8bitchars = FALSE; bool in_header = TRUE; /* find the message to send */ f = fopen(msgf, "r"); if (f == NULL) { f = fopen("/etc/mail/vacation.def", "r"); if (f == NULL) usrerr("No message to send"); exit(EX_OSFILE); } if (pipe(pipefd) < 0) { usrerr("pipe() failed"); exit(EX_OSERR); } i = fork(); if (i < 0) { usrerr("fork() failed"); exit(EX_OSERR); } if (i == 0) { dup2(pipefd[0], 0); close(pipefd[0]); close(pipefd[1]); fclose(f); execl("/usr/lib/sendmail", "sendmail", "-eq", "-f", myname, "--", user, NULL); usrerr("can't exec /usr/lib/sendmail"); exit(EX_OSERR); } close(pipefd[0]); fpipe = fdopen(pipefd[1], "w"); if (fpipe == NULL) { usrerr("fdopen() failed"); exit(EX_OSERR); } fprintf(fpipe, "To: %s\n", user); fputs("Auto-Submitted: auto-replied\n", fpipe); fputs("X-Mailer: vacation %I%\n", fpipe); /* * We used to write directly to the pipe. But now we need to know * what character set to use, and we need to examine the entire * message to determine this. So write to a temp file first. */ tmpf_name = strdup(_PATH_TMP); if (tmpf_name == NULL) { usrerr("newstr: cannot alloc memory"); exit(EX_OSERR); } tmpfd = -1; tmpfd = mkstemp(tmpf_name); if (tmpfd == -1) { usrerr("can't open temp file %s", tmpf_name); exit(EX_OSERR); } tmpf = fdopen(tmpfd, "w"); if (tmpf == NULL) { usrerr("can't open temp file %s", tmpf_name); exit(EX_OSERR); } while (fgets(line, MAXLINE, f)) { /* * Check for a line with no ':' character. If it's just \n, * we're at the end of the headers and all is fine. Or if * it starts with white-space, then it's a continuation header. * Otherwise, it's the start of the body, which means the * header/body separator was skipped. So output it. */ if (in_header && line[0] != '\0' && strchr(line, ':') == NULL) { if (line[0] == '\n') in_header = FALSE; else if (!isspace(line[0])) { in_header = FALSE; fputs("\n", tmpf); } } p = strchr(line, '$'); if (p && strncmp(p, "$SUBJECT", 8) == 0) { *p = '\0'; seen8bitchars |= any8bitchars(line); fputs(line, tmpf); if (Subject) { if (in_header) fputs(EncodedSubject, tmpf); else { seen8bitchars |= any8bitchars(Subject); fputs(Subject, tmpf); } } seen8bitchars |= any8bitchars(p+8); fputs(p+8, tmpf); continue; } seen8bitchars |= any8bitchars(line); fputs(line, tmpf); } fclose(f); fclose(tmpf); /* * If we haven't seen a funky Subject with Charset, use the default. * If we have and it's us-ascii, 8-bit chars in the message file will * still result in iso-8859-1. */ if (Charset[0] == '\0') (void) strlcpy(Charset, (seen8bitchars) ? "iso-8859-1" : "us-ascii", sizeof (Charset)); else if ((strcasecmp(Charset, "us-ascii") == 0) && seen8bitchars) (void) strlcpy(Charset, "iso-8859-1", sizeof (Charset)); if (Debug) printf("Charset is %s\n", Charset); fprintf(fpipe, "Content-Type: text/plain; charset=%s\n", Charset); fputs("Mime-Version: 1.0\n", fpipe); /* * Now read back in from the temp file and write to the pipe. */ tmpf = fopen(tmpf_name, "r"); if (tmpf == NULL) { usrerr("can't open temp file %s", tmpf_name); exit(EX_OSERR); } while (fgets(line, MAXLINE, tmpf)) fputs(line, fpipe); fclose(fpipe); fclose(tmpf); (void) unlink(tmpf_name); free(tmpf_name); } /* * INITIALIZE -- initialize the database before leaving for vacation * * Parameters: * none. * * Returns: * none. * * Side Effects: * Initializes the files .vacation.{pag,dir} in the * caller's home directory. */ static void initialize(db_file_base) char *db_file_base; { char *homedir; char buf[MAXLINE]; DBM *db; setgid(getgid()); setuid(getuid()); homedir = getenv("HOME"); if (homedir == NULL) { usrerr("No home!"); exit(EX_NOUSER); } (void) snprintf(buf, sizeof (buf), "%s%s%s", homedir, (db_file_base[0] == '/') ? "" : "/", db_file_base); if (!(db = dbm_open(buf, O_WRONLY|O_CREAT|O_TRUNC, 0644))) { usrerr("%s: %s\n", buf, strerror(errno)); exit(EX_DATAERR); } dbm_close(db); } /* * USRERR -- print user error * * Parameters: * f -- format. * * Returns: * none. * * Side Effects: * none. */ /* PRINTFLIKE1 */ void usrerr(const char *f, ...) { va_list alist; va_start(alist, f); (void) fprintf(stderr, "vacation: "); (void) vfprintf(stderr, f, alist); (void) fprintf(stderr, "\n"); va_end(alist); } /* * NEWSTR -- copy a string * * Parameters: * s -- the string to copy. * * Returns: * A copy of the string. * * Side Effects: * none. */ static char * newstr(s) char *s; { char *p; size_t s_sz = strlen(s); p = malloc(s_sz + 1); if (p == NULL) { usrerr("newstr: cannot alloc memory"); exit(EX_OSERR); } (void) strlcpy(p, s, s_sz + 1); return (p); } /* * SAMEWORD -- return TRUE if the words are the same * * Ignores case. * * Parameters: * a, b -- the words to compare. * * Returns: * TRUE if a & b match exactly (modulo case) * FALSE otherwise. * * Side Effects: * none. */ static bool sameword(a, b) register char *a, *b; { char ca, cb; do { ca = *a++; cb = *b++; if (isascii(ca) && isupper(ca)) ca = ca - 'A' + 'a'; if (isascii(cb) && isupper(cb)) cb = cb - 'A' + 'a'; } while (ca != '\0' && ca == cb); return (ca == cb); } /* * When invoked with no arguments, we fall into an automatic installation * mode, stepping the user through a default installation. */ static void AutoInstall() { char file[MAXLINE]; char forward[MAXLINE]; char cmd[MAXLINE]; char line[MAXLINE]; char *editor; FILE *f; struct passwd *pw; extern mode_t umask(mode_t cmask); umask(022); pw = getpwuid(getuid()); if (pw == NULL) { usrerr("User ID unknown"); exit(EX_NOUSER); } myname = strdup(pw->pw_name); if (myname == NULL) { usrerr("Out of memory"); exit(EX_OSERR); } homedir = getenv("HOME"); if (homedir == NULL) { usrerr("Home directory unknown"); exit(EX_NOUSER); } printf("This program can be used to answer your mail automatically\n"); printf("when you go away on vacation.\n"); (void) strlcpy(file, homedir, sizeof (file)); (void) strlcat(file, MsgFile, sizeof (file)); do { f = fopen(file, "r"); if (f) { printf("You have a message file in %s.\n", file); if (ask("Would you like to see it")) { (void) snprintf(cmd, sizeof (cmd), "/usr/bin/more %s", file); system(cmd); } if (ask("Would you like to edit it")) f = NULL; } else { printf("You need to create a message file" " in %s first.\n", file); f = fopen(file, "w"); if (f == NULL) { usrerr("Cannot open %s", file); exit(EX_CANTCREAT); } fprintf(f, "Subject: away from my mail\n"); fprintf(f, "\nI will not be reading my mail" " for a while.\n"); fprintf(f, "Your mail regarding \"$SUBJECT\" will" " be read when I return.\n"); fclose(f); f = NULL; } if (f == NULL) { editor = getenv("VISUAL"); if (editor == NULL) editor = getenv("EDITOR"); if (editor == NULL) editor = "/usr/bin/vi"; (void) snprintf(cmd, sizeof (cmd), "%s %s", editor, file); printf("Please use your editor (%s)" " to edit this file.\n", editor); system(cmd); } } while (f == NULL); fclose(f); (void) strlcpy(forward, homedir, sizeof (forward)); (void) strlcat(forward, "/.forward", sizeof (forward)); f = fopen(forward, "r"); if (f) { printf("You have a .forward file" " in your home directory containing:\n"); while (fgets(line, MAXLINE, f)) printf(" %s", line); fclose(f); if (!ask("Would you like to remove it and" " disable the vacation feature")) exit(EX_OK); if (unlink(forward)) perror("Error removing .forward file:"); else printf("Back to normal reception of mail.\n"); exit(EX_OK); } printf("To enable the vacation feature" " a \".forward\" file is created.\n"); if (!ask("Would you like to enable the vacation feature")) { printf("OK, vacation feature NOT enabled.\n"); exit(EX_OK); } f = fopen(forward, "w"); if (f == NULL) { perror("Error opening .forward file"); exit(EX_CANTCREAT); } fprintf(f, "\\%s, \"|/usr/bin/vacation %s\"\n", myname, myname); fclose(f); printf("Vacation feature ENABLED." " Please remember to turn it off when\n"); printf("you get back from vacation. Bon voyage.\n"); initialize(DbFileBase); exit(EX_OK); } /* * Ask the user a question until we get a reasonable answer */ static bool ask(prompt) char *prompt; { char line[MAXLINE]; char *res; for (;;) { printf("%s? ", prompt); fflush(stdout); res = fgets(line, sizeof (line), stdin); if (res == NULL) return (FALSE); if (res[0] == 'y' || res[0] == 'Y') return (TRUE); if (res[0] == 'n' || res[0] == 'N') return (FALSE); printf("Please reply \"yes\" or \"no\" (\'y\' or \'n\')\n"); } }