/* * mhstoresbr.c -- routines to save/store the contents of MIME messages * * This code is Copyright (c) 2002, by the authors of nmh. See the * COPYRIGHT file in the root directory of the nmh distribution for * complete copyright information. */ #include #include #include #include #include #include #include #include enum clobber_policy_t { NMH_CLOBBER_ALWAYS = 0, NMH_CLOBBER_AUTO, NMH_CLOBBER_SUFFIX, NMH_CLOBBER_ASK, NMH_CLOBBER_NEVER }; static enum clobber_policy_t clobber_policy (const char *); struct mhstoreinfo { CT *cts; /* Top-level list of contents to store. */ char *cwd; /* cached current directory */ int autosw; /* -auto enabled */ int verbosw; /* -verbose enabled */ int files_not_clobbered; /* output flag indicating that store failed in order to not clobber an existing file */ /* The following must never be touched by a caller: they are for internal use by the mhstoresbr functions. */ char *dir; /* directory in which to store contents */ enum clobber_policy_t clobber_policy; /* -clobber selection */ }; static bool use_param_as_filename(const char *p); mhstoreinfo_t mhstoreinfo_create (CT *ct, char *pwd, const char *csw, int asw, int vsw) { mhstoreinfo_t info; NEW(info); info->cts = ct; info->cwd = pwd; info->autosw = asw; info->verbosw = vsw; info->files_not_clobbered = 0; info->dir = NULL; info->clobber_policy = clobber_policy (csw); return info; } void mhstoreinfo_free (mhstoreinfo_t info) { free (info->cwd); free (info->dir); free (info); } int mhstoreinfo_files_not_clobbered (const mhstoreinfo_t info) { return info->files_not_clobbered; } /* * Type for a compare function for qsort. This keeps * the compiler happy. */ typedef int (*qsort_comp) (const void *, const void *); /* mhmisc.c */ int part_ok (CT); int type_ok (CT, int); void flush_errors (void); /* * static prototypes */ static void store_single_message (CT, mhstoreinfo_t); static int store_switch (CT, mhstoreinfo_t); static int store_generic (CT, mhstoreinfo_t); static int store_application (CT, mhstoreinfo_t); static int store_multi (CT, mhstoreinfo_t); static int store_partial (CT, mhstoreinfo_t); static int store_external (CT, mhstoreinfo_t); static int ct_compar (CT *, CT *); static int store_content (CT, CT, mhstoreinfo_t); static int output_content_file (CT, int); static int output_content_folder (char *, char *); static int parse_format_string (CT, char *, char *, int, char *); static void get_storeproc (CT); static int copy_some_headers (FILE *, CT); static char *clobber_check (char *, mhstoreinfo_t); /* * Main entry point to store content * from a collection of messages. */ void store_all_messages (mhstoreinfo_t info) { CT ct, *ctp; char *cp; /* * Check for the directory in which to * store any contents. */ if ((cp = context_find (nmhstorage)) && *cp) info->dir = mh_xstrdup(cp); else info->dir = getcpy (info->cwd); for (ctp = info->cts; *ctp; ctp++) { ct = *ctp; store_single_message (ct, info); } flush_errors (); } /* * Entry point to store the content * in a (single) message */ static void store_single_message (CT ct, mhstoreinfo_t info) { if (type_ok (ct, 1)) { umask (ct->c_umask); store_switch (ct, info); if (ct->c_fp) { fclose (ct->c_fp); ct->c_fp = NULL; } if (ct->c_ceclosefnx) (*ct->c_ceclosefnx) (ct); } } /* * Switching routine to store different content types */ static int store_switch (CT ct, mhstoreinfo_t info) { switch (ct->c_type) { case CT_MULTIPART: return store_multi (ct, info); case CT_MESSAGE: switch (ct->c_subtype) { case MESSAGE_PARTIAL: return store_partial (ct, info); case MESSAGE_EXTERNAL: return store_external (ct, info); case MESSAGE_RFC822: default: return store_generic (ct, info); } case CT_APPLICATION: default: return store_application (ct, info); case CT_TEXT: case CT_AUDIO: case CT_IMAGE: case CT_VIDEO: return store_generic (ct, info); } return OK; /* NOT REACHED */ } /* * Generic routine to store a MIME content. * (audio, video, image, text, message/rfc822) */ static int store_generic (CT ct, mhstoreinfo_t info) { /* * Check if the content specifies a filename. * Don't bother with this for type "message" * (only "message/rfc822" will use store_generic). */ if (info->autosw && ct->c_type != CT_MESSAGE) get_storeproc (ct); return store_content (ct, NULL, info); } /* * Store content of type "application" */ static int store_application (CT ct, mhstoreinfo_t info) { CI ci = &ct->c_ctinfo; /* Check if the content specifies a filename */ if (info->autosw) get_storeproc (ct); /* * If storeproc is not defined, and the content is type * "application/octet-stream", we also check for various * attribute/value pairs which specify if this a tar file. */ if (!ct->c_storeproc && ct->c_subtype == APPLICATION_OCTETS) { int tarP = 0, zP = 0, gzP = 0; char *cp; if ((cp = get_param(ci->ci_first_pm, "type", ' ', 1))) { if (strcasecmp (cp, "tar") == 0) tarP = 1; } /* check for "conversions=compress" attribute */ if ((cp = get_param(ci->ci_first_pm, "conversions", ' ', 1)) || (cp = get_param(ci->ci_first_pm, "x-conversions", ' ', 1))) { if (strcasecmp (cp, "compress") == 0 || strcasecmp (cp, "x-compress") == 0) { zP = 1; } if (strcasecmp (cp, "gzip") == 0 || strcasecmp (cp, "x-gzip") == 0) { gzP = 1; } } if (tarP) { ct->c_showproc = add (zP ? "%euncompress | tar tvf -" : (gzP ? "%egzip -dc | tar tvf -" : "%etar tvf -"), NULL); if (!ct->c_storeproc) { if (info->autosw) { ct->c_storeproc = add (zP ? "| uncompress | tar xvpf -" : (gzP ? "| gzip -dc | tar xvpf -" : "| tar xvpf -"), NULL); ct->c_umask = 0022; } else { ct->c_storeproc= add (zP ? "%m%P.tar.Z" : (gzP ? "%m%P.tar.gz" : "%m%P.tar"), NULL); } } } } return store_content (ct, NULL, info); } /* * Store the content of a multipart message */ static int store_multi (CT ct, mhstoreinfo_t info) { int result; struct multipart *m = (struct multipart *) ct->c_ctparams; struct part *part; result = NOTOK; for (part = m->mp_parts; part; part = part->mp_next) { CT p = part->mp_part; if (part_ok (p) && type_ok (p, 1)) { if (ct->c_storage) { /* Support mhstore -outfile. The MIME parser doesn't load c_storage, so we know that p->c_storage is NULL here. */ p->c_storage = mh_xstrdup(ct->c_storage); } result = store_switch (p, info); if (result == OK && ct->c_subtype == MULTI_ALTERNATE) break; } } return result; } /* * Reassemble and store the contents of a collection * of messages of type "message/partial". */ static int store_partial (CT ct, mhstoreinfo_t info) { int cur, hi, i; CT p, *ctp, *ctq; CT *base; struct partial *pm, *qm; qm = (struct partial *) ct->c_ctparams; if (qm->pm_stored) return OK; hi = i = 0; for (ctp = info->cts; *ctp; ctp++) { p = *ctp; if (p->c_type == CT_MESSAGE && p->c_subtype == ct->c_subtype) { pm = (struct partial *) p->c_ctparams; if (!pm->pm_stored && strcmp (qm->pm_partid, pm->pm_partid) == 0) { pm->pm_marked = pm->pm_partno; if (pm->pm_maxno) hi = pm->pm_maxno; pm->pm_stored = 1; i++; } else pm->pm_marked = 0; } } if (hi == 0) { advise (NULL, "missing (at least) last part of multipart message"); return NOTOK; } base = mh_xcalloc(i + 1, sizeof *base); ctq = base; for (ctp = info->cts; *ctp; ctp++) { p = *ctp; if (p->c_type == CT_MESSAGE && p->c_subtype == ct->c_subtype) { pm = (struct partial *) p->c_ctparams; if (pm->pm_marked) *ctq++ = p; } } *ctq = NULL; if (i > 1) qsort ((char *) base, i, sizeof(*base), (qsort_comp) ct_compar); cur = 1; for (ctq = base; *ctq; ctq++) { p = *ctq; pm = (struct partial *) p->c_ctparams; if (pm->pm_marked != cur) { if (pm->pm_marked == cur - 1) { admonish (NULL, "duplicate part %d of %d part multipart message", pm->pm_marked, hi); continue; } missing_part: advise (NULL, "missing %spart %d of %d part multipart message", cur != hi ? "(at least) " : "", cur, hi); goto losing; } else cur++; } if (hi != --cur) { cur = hi; goto missing_part; } /* * Now cycle through the sorted list of messages of type * "message/partial" and save/append them to a file. */ ctq = base; ct = *ctq++; if (store_content (ct, NULL, info) == NOTOK) { losing: free(base); return NOTOK; } for (; *ctq; ctq++) { p = *ctq; if (store_content (p, ct, info) == NOTOK) goto losing; } free(base); return OK; } /* * Store content from a message of type "message/external". */ static int store_external (CT ct, mhstoreinfo_t info) { int result = NOTOK; struct exbody *e = (struct exbody *) ct->c_ctparams; CT p = e->eb_content; if (!type_ok (p, 1)) return OK; /* * Check if the parameters for the external body * specified a filename. */ if (info->autosw) { char *cp; if ((cp = e->eb_name) && use_param_as_filename(cp)) { if (!ct->c_storeproc) ct->c_storeproc = mh_xstrdup(cp); if (!p->c_storeproc) p->c_storeproc = mh_xstrdup(cp); } } /* * Since we will let the Content structure for the * external body substitute for the current content, * we temporarily change its partno (number inside * multipart), so everything looks right. */ p->c_partno = ct->c_partno; /* we probably need to check if content is really there */ if (ct->c_storage) { /* Support mhstore -outfile. The MIME parser doesn't load c_storage, so we know that p->c_storage is NULL here. */ p->c_storage = mh_xstrdup(ct->c_storage); } result = store_switch (p, info); p->c_partno = NULL; return result; } /* * Compare the numbering from two different * message/partials (needed for sorting). */ static int ct_compar (CT *a, CT *b) { struct partial *am = (struct partial *) ((*a)->c_ctparams); struct partial *bm = (struct partial *) ((*b)->c_ctparams); return (am->pm_marked - bm->pm_marked); } /* * Store contents of a message or message part to * a folder, a file, the standard output, or pass * the contents to a command. * * If the current content to be saved is a followup part * to a collection of messages of type "message/partial", * then field "p" is a pointer to the Content structure * to the first message/partial in the group. */ static int store_content (CT ct, CT p, mhstoreinfo_t info) { int appending = 0, msgnum = 0; int is_partial = 0, first_partial = 0; int last_partial = 0; char *cp, buffer[BUFSIZ]; /* * Do special processing for messages of * type "message/partial". * * We first check if this content is of type * "message/partial". If it is, then we need to check * whether it is the first and/or last in the group. * * Then if "p" is a valid pointer, it points to the Content * structure of the first partial in the group. So we copy * the file name and/or folder name from that message. In * this case, we also note that we will be appending. */ if (ct->c_type == CT_MESSAGE && ct->c_subtype == MESSAGE_PARTIAL) { struct partial *pm = (struct partial *) ct->c_ctparams; /* Yep, it's a message/partial */ is_partial = 1; /* But is it the first and/or last in the collection? */ if (pm->pm_partno == 1) first_partial = 1; if (pm->pm_maxno && pm->pm_partno == pm->pm_maxno) last_partial = 1; /* * If "p" is a valid pointer, then it points to the * Content structure for the first message in the group. * So we just copy the filename or foldername information * from the previous iteration of this function. */ if (p) { appending = 1; if (! ct->c_storage) { ct->c_storage = add (p->c_storage, NULL); /* record the folder name */ if (p->c_folder) { ct->c_folder = mh_xstrdup(p->c_folder); } } goto got_filename; } } /* * Get storage formatting string. * * 1) If we have storeproc defined, then use that * 2) Else check for a mhn-store-/ entry * 3) Else check for a mhn-store- entry * 4) Else if content is "message", use "+" (current folder) * 5) Else use string "%m%P.%s". */ if ((cp = ct->c_storeproc) == NULL || *cp == '\0') { CI ci = &ct->c_ctinfo; cp = context_find_by_type ("store", ci->ci_type, ci->ci_subtype); if (cp == NULL) { cp = ct->c_type == CT_MESSAGE ? "+" : "%m%P.%s"; } } if (! ct->c_storage) { /* * Check the beginning of storage formatting string * to see if we are saving content to a folder. */ if (*cp == '+' || *cp == '@') { char *tmpfilenam, *folder; /* Store content in temporary file for now */ if ((tmpfilenam = m_mktemp(invo_name, NULL, NULL)) == NULL) { adios(NULL, "unable to create temporary file in %s", get_temp_dir()); } ct->c_storage = mh_xstrdup(tmpfilenam); /* Get the folder name */ if (cp[1]) folder = pluspath (cp); else folder = getfolder (1); /* Check if folder exists */ create_folder(m_mailpath(folder), 0, exit); /* Record the folder name */ ct->c_folder = add (folder, NULL); if (cp[1]) free (folder); goto got_filename; } /* * Parse and expand the storage formatting string * in `cp' into `buffer'. */ parse_format_string (ct, cp, buffer, sizeof(buffer), info->dir); /* * If formatting begins with '|' or '!', then pass * content to standard input of a command and return. */ if (buffer[0] == '|' || buffer[0] == '!') return show_content_aux (ct, 0, buffer + 1, info->dir, NULL); /* record the filename */ if ((ct->c_storage = clobber_check (mh_xstrdup(buffer), info)) == NULL) { return NOTOK; } } else { /* The output filename was explicitly specified, so use it. */ if ((ct->c_storage = clobber_check (ct->c_storage, info)) == NULL) { return NOTOK; } } got_filename: /* flush the output stream */ fflush (stdout); /* Now save or append the content to a file */ if (output_content_file (ct, appending) == NOTOK) return NOTOK; /* * If necessary, link the file into a folder and remove * the temporary file. If this message is a partial, * then only do this if it is the last one in the group. */ if (ct->c_folder && (!is_partial || last_partial)) { msgnum = output_content_folder (ct->c_folder, ct->c_storage); (void) m_unlink (ct->c_storage); if (msgnum == NOTOK) return NOTOK; } if (info->verbosw) { /* * Now print out the name/number of the message * that we are storing. */ if (is_partial) { if (first_partial) fprintf (stderr, "reassembling partials "); if (last_partial) fputs(ct->c_file, stderr); else fprintf (stderr, "%s,", ct->c_file); } else { fprintf (stderr, "storing message %s", ct->c_file); if (ct->c_partno) fprintf (stderr, " part %s", ct->c_partno); } /* * Unless we are in the "middle" of group of message/partials, * we now print the name of the file, folder, and/or message * to which we are storing the content. */ if (!is_partial || last_partial) { if (ct->c_folder) { fprintf (stderr, " to folder %s as message %d\n", ct->c_folder, msgnum); } else if (!strcmp(ct->c_storage, "-")) { fprintf (stderr, " to stdout\n"); } else { int cwdlen = strlen (info->cwd); fprintf (stderr, " as file %s\n", !has_prefix(ct->c_storage, info->cwd) || ct->c_storage[cwdlen] != '/' ? ct->c_storage : ct->c_storage + cwdlen + 1); } } } return OK; } /* * Output content to a file */ static int output_content_file (CT ct, int appending) { int filterstate; char *file, buffer[BUFSIZ]; long pos, last; FILE *fp; /* * If the pathname is absolute, make sure * all the relevant directories exist. */ if (strchr(ct->c_storage, '/') && make_intermediates (ct->c_storage) == NOTOK) return NOTOK; if (ct->c_encoding != CE_7BIT) { int cc, fd; if (!ct->c_ceopenfnx) { advise (NULL, "don't know how to decode part %s of message %s", ct->c_partno, ct->c_file); return NOTOK; } file = appending || !strcmp (ct->c_storage, "-") ? NULL : ct->c_storage; if ((fd = (*ct->c_ceopenfnx) (ct, &file)) == NOTOK) return NOTOK; if (!strcmp (file, ct->c_storage)) { (*ct->c_ceclosefnx) (ct); return OK; } /* * Send to standard output */ if (!strcmp (ct->c_storage, "-")) { int gd; if ((gd = dup (fileno (stdout))) == NOTOK) { advise ("stdout", "unable to dup"); losing: (*ct->c_ceclosefnx) (ct); return NOTOK; } if ((fp = fdopen (gd, appending ? "a" : "w")) == NULL) { advise ("stdout", "unable to fdopen (%d, \"%s\") from", gd, appending ? "a" : "w"); close (gd); goto losing; } } else { /* * Open output file */ if ((fp = fopen (ct->c_storage, appending ? "a" : "w")) == NULL) { advise (ct->c_storage, "unable to fopen for %s", appending ? "appending" : "writing"); goto losing; } } /* * Filter the header fields of the initial enclosing * message/partial into the file. */ if (ct->c_type == CT_MESSAGE && ct->c_subtype == MESSAGE_PARTIAL) { struct partial *pm = (struct partial *) ct->c_ctparams; if (pm->pm_partno == 1) copy_some_headers (fp, ct); } for (;;) { switch (cc = read (fd, buffer, sizeof(buffer))) { case NOTOK: advise (file, "error reading content from"); break; case OK: break; default: if ((int) fwrite (buffer, sizeof(*buffer), cc, fp) < cc) { advise ("output_content_file", "fwrite"); } continue; } break; } (*ct->c_ceclosefnx) (ct); if (cc != NOTOK && fflush (fp)) advise (ct->c_storage, "error writing to"); fclose (fp); return (cc != NOTOK ? OK : NOTOK); } if (!ct->c_fp && (ct->c_fp = fopen (ct->c_file, "r")) == NULL) { advise (ct->c_file, "unable to open for reading"); return NOTOK; } pos = ct->c_begin; last = ct->c_end; fseek (ct->c_fp, pos, SEEK_SET); if (!strcmp (ct->c_storage, "-")) { int gd; if ((gd = dup (fileno (stdout))) == NOTOK) { advise ("stdout", "unable to dup"); return NOTOK; } if ((fp = fdopen (gd, appending ? "a" : "w")) == NULL) { advise ("stdout", "unable to fdopen (%d, \"%s\") from", gd, appending ? "a" : "w"); close (gd); return NOTOK; } } else { if ((fp = fopen (ct->c_storage, appending ? "a" : "w")) == NULL) { advise (ct->c_storage, "unable to fopen for %s", appending ? "appending" : "writing"); return NOTOK; } } /* * Copy a few of the header fields of the initial * enclosing message/partial into the file. */ filterstate = 0; if (ct->c_type == CT_MESSAGE && ct->c_subtype == MESSAGE_PARTIAL) { struct partial *pm = (struct partial *) ct->c_ctparams; if (pm->pm_partno == 1) { copy_some_headers (fp, ct); filterstate = 1; } } while (fgets (buffer, sizeof buffer, ct->c_fp)) { if ((pos += strlen (buffer)) > last) { int diff; diff = strlen (buffer) - (pos - last); if (diff >= 0) buffer[diff] = '\0'; } /* * If this is the first content of a group of * message/partial contents, then we only copy a few * of the header fields of the enclosed message. */ if (filterstate) { switch (buffer[0]) { case ' ': case '\t': if (filterstate < 0) buffer[0] = 0; break; case '\n': filterstate = 0; break; default: if (!uprf (buffer, XXX_FIELD_PRF) && !uprf (buffer, VRSN_FIELD) && !uprf (buffer, "Subject:") && !uprf (buffer, "Encrypted:") && !uprf (buffer, "Message-ID:")) { filterstate = -1; buffer[0] = 0; break; } filterstate = 1; break; } } fputs (buffer, fp); if (pos >= last) break; } if (fflush (fp)) advise (ct->c_storage, "error writing to"); fclose (fp); fclose (ct->c_fp); ct->c_fp = NULL; return OK; } /* * Add a file to a folder. * * Return the new message number of the file * when added to the folder. Return -1, if * there is an error. */ static int output_content_folder (char *folder, char *filename) { int msgnum; struct msgs *mp; /* Read the folder. */ if ((mp = folder_read (folder, 0))) { /* Link file into folder */ msgnum = folder_addmsg (&mp, filename, 0, 0, 0, 0, NULL); } else { advise (NULL, "unable to read folder %s", folder); return NOTOK; } /* free folder structure */ folder_free (mp); /* * Return msgnum. We are relying on the fact that * msgnum will be -1, if folder_addmsg() had an error. */ return msgnum; } /* * Parse and expand the storage formatting string * pointed to by "cp" into "buffer". */ static int parse_format_string (CT ct, char *cp, char *buffer, int buflen, char *dir) { int len; char *bp; CI ci = &ct->c_ctinfo; /* * If storage string is "-", just copy it, and * return (send content to standard output). */ if (cp[0] == '-' && cp[1] == '\0') { strncpy (buffer, cp, buflen); return 0; } bp = buffer; bp[0] = '\0'; /* * If formatting string is a pathname that doesn't * begin with '/', then preface the path with the * appropriate directory. */ if (*cp != '/' && *cp != '|' && *cp != '!') { snprintf (bp, buflen, "%s/", dir[1] ? dir : ""); len = strlen (bp); bp += len; buflen -= len; } for (; *cp; cp++) { /* We are processing a storage escape */ if (*cp == '%') { switch (*++cp) { case 'a': /* * Insert parameters from Content-Type. * This is only valid for '|' commands. */ if (buffer[0] != '|' && buffer[0] != '!') { *bp++ = *--cp; *bp = '\0'; buflen--; continue; } else { PM pm; char *s = ""; for (pm = ci->ci_first_pm; pm; pm = pm->pm_next) { snprintf (bp, buflen, "%s%s=\"%s\"", s, pm->pm_name, get_param_value(pm, '?')); len = strlen (bp); bp += len; buflen -= len; s = " "; } } break; case 'm': /* insert message number */ snprintf (bp, buflen, "%s", r1bindex (ct->c_file, '/')); break; case 'P': /* insert part number with leading dot */ if (ct->c_partno) snprintf (bp, buflen, ".%s", ct->c_partno); break; case 'p': /* insert part number withouth leading dot */ if (ct->c_partno) strncpy (bp, ct->c_partno, buflen); break; case 't': /* insert content type */ strncpy (bp, ci->ci_type, buflen); break; case 's': /* insert content subtype */ strncpy (bp, ci->ci_subtype, buflen); break; case '%': /* insert the character % */ goto raw; default: *bp++ = *--cp; *bp = '\0'; buflen--; continue; } /* Advance bp and decrement buflen */ len = strlen (bp); bp += len; buflen -= len; } else { raw: *bp++ = *cp; *bp = '\0'; buflen--; } } return 0; } /* * Check if the content specifies a filename * in its MIME parameters. */ static void get_storeproc (CT ct) { char *cp; CI ci; /* * If the storeproc has already been defined, * we just return (for instance, if this content * is part of a "message/external". */ if (ct->c_storeproc) return; /* * If there's a Content-Disposition header and it has a filename, * use that (RFC-2183). */ if (ct->c_dispo) { if ((cp = get_param(ct->c_dispo_first, "filename", '_', 0)) && use_param_as_filename(cp)) { ct->c_storeproc = mh_xstrdup(cp); free(cp); return; } mh_xfree(cp); } /* * Check the attribute/value pairs, for the attribute "name". * If found, do a few sanity checks and copy the value into * the storeproc. */ ci = &ct->c_ctinfo; if ((cp = get_param(ci->ci_first_pm, "name", '_', 0)) && use_param_as_filename(cp)) { ct->c_storeproc = mh_xstrdup(cp); } mh_xfree(cp); } /* * Copy some of the header fields of the initial message/partial * message into the header of the reassembled message. */ static int copy_some_headers (FILE *out, CT ct) { HF hp; hp = ct->c_first_hf; /* start at first header field */ while (hp) { /* * A few of the header fields of the enclosing * messages are not copied. */ if (!uprf (hp->name, XXX_FIELD_PRF) && strcasecmp (hp->name, VRSN_FIELD) && strcasecmp (hp->name, "Subject") && strcasecmp (hp->name, "Encrypted") && strcasecmp (hp->name, "Message-ID")) fprintf (out, "%s:%s", hp->name, hp->value); hp = hp->next; /* next header field */ } return OK; } /******************************************************************************/ /* -clobber support */ static enum clobber_policy_t clobber_policy (const char *value) { if (value == NULL || ! strcasecmp (value, "always")) { return NMH_CLOBBER_ALWAYS; } if (! strcasecmp (value, "auto")) { return NMH_CLOBBER_AUTO; } if (! strcasecmp (value, "suffix")) { return NMH_CLOBBER_SUFFIX; } if (! strcasecmp (value, "ask")) { return NMH_CLOBBER_ASK; } if (! strcasecmp (value, "never")) { return NMH_CLOBBER_NEVER; } adios (NULL, "invalid argument, %s, to clobber", value); } static char * next_version (char *file, enum clobber_policy_t clobber_policy) { const size_t max_versions = 1000000; /* 8 = log max_versions + one for - or . + one for null terminator */ const size_t buflen = strlen (file) + 8; char *buffer = mh_xmalloc (buflen); size_t version; char *extension = NULL; if (clobber_policy == NMH_CLOBBER_AUTO && ((extension = strrchr (file, '.')) != NULL)) { *extension++ = '\0'; } for (version = 1; version < max_versions; ++version) { int fd; switch (clobber_policy) { case NMH_CLOBBER_AUTO: { snprintf (buffer, buflen, "%s-%ld%s%s", file, (long) version, extension == NULL ? "" : ".", extension == NULL ? "" : extension); break; } case NMH_CLOBBER_SUFFIX: snprintf (buffer, buflen, "%s.%ld", file, (long) version); break; default: /* Should never get here. */ advise (NULL, "will not overwrite %s, invalid clobber policy", buffer); free (buffer); return NULL; } /* Actually (try to) create the file here to avoid a race condition on file naming + creation. This won't solve the problem with old NFS that doesn't support O_EXCL, though. Let the umask strip off permissions from 0666 as desired. That's what fopen () would do if it was creating the file. */ if ((fd = open (buffer, O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) >= 0) { close (fd); break; } } free (file); if (version >= max_versions) { advise (NULL, "will not overwrite %s, too many versions", buffer); free (buffer); buffer = NULL; } return buffer; } static char * clobber_check (char *original_file, mhstoreinfo_t info) { /* clobber policy return value * -------------- ------------ * -always original_file * -auto original_file-.extension * -suffix original_file. * -ask original_file, 0, or another filename/path * -never 0 */ char *file; char *cwd = NULL; int check_again; if (! strcmp (original_file, "-")) { return original_file; } if (info->clobber_policy == NMH_CLOBBER_ASK) { /* Save cwd for possible use in loop below. */ char *slash; cwd = mh_xstrdup(original_file); slash = strrchr (cwd, '/'); if (slash) { *slash = '\0'; } else { /* original_file isn't a full path, which should only happen if it is -. */ free (cwd); cwd = NULL; } } do { struct stat st; file = original_file; check_again = 0; switch (info->clobber_policy) { case NMH_CLOBBER_ALWAYS: break; case NMH_CLOBBER_SUFFIX: case NMH_CLOBBER_AUTO: if (stat (file, &st) == OK) { if ((file = next_version (original_file, info->clobber_policy)) == NULL) { ++info->files_not_clobbered; } } break; case NMH_CLOBBER_ASK: if (stat (file, &st) == OK) { enum answers { NMH_YES, NMH_NO, NMH_RENAME }; static struct swit answer[4] = { { "yes", 0, NMH_YES }, { "no", 0, NMH_NO }, { "rename", 0, NMH_RENAME }, { NULL, 0, 0 } }; char **ans; if (isatty (fileno (stdin))) { char *prompt = concat ("Overwrite \"", file, "\" [y/n/rename]? ", NULL); ans = read_switch_multiword (prompt, answer); free (prompt); } else { /* Overwrite, that's what nmh used to do. And warn. */ advise (NULL, "-clobber ask but no tty, so overwrite %s", file); break; } switch ((enum answers) smatch (*ans, answer)) { case NMH_YES: break; case NMH_NO: free (file); file = NULL; ++info->files_not_clobbered; break; case NMH_RENAME: { char buf[PATH_MAX]; printf ("Enter filename or full path of the new file: "); if (fgets (buf, sizeof buf, stdin) == NULL || buf[0] == '\0') { file = NULL; ++info->files_not_clobbered; } else { trim_suffix_c(buf, '\n'); } free (file); if (buf[0] == '/') { /* Full path, use it. */ file = mh_xstrdup(buf); } else { /* Relative path. */ file = cwd ? concat (cwd, "/", buf, NULL) : mh_xstrdup(buf); } check_again = 1; break; } } } break; case NMH_CLOBBER_NEVER: if (stat (file, &st) == OK) { /* Keep count of files that would have been clobbered, and return that as process exit status. */ advise (NULL, "will not overwrite %s with -clobber never", file); free (file); file = NULL; ++info->files_not_clobbered; } break; } original_file = file; } while (check_again); free (cwd); return file; } static bool use_param_as_filename(const char *p) { /* Preserve result of original test that considered an empty string * OK. */ return !*p || (!strchr("/.|!", *p) && !strchr(p, '%')); } /* -clobber support */ /******************************************************************************/