From: Lyndon Nerenberg Date: Sun, 16 Sep 2012 21:39:27 +0000 (-0700) Subject: Merge branch 'master' of ssh://git.sv.gnu.org/srv/git/nmh X-Git-Url: http://git.marmaro.de/?a=commitdiff_plain;h=207d657cf4495be3eb44ccde94e807692665f5e2;hp=b471d7a3c81ac94c9880e47f8771d2aed9c66b7f;p=mmh Merge branch 'master' of ssh://git.sv.gnu.org/srv/git/nmh --- diff --git a/docs/README.developers b/docs/README.developers index bae6d9c..9038cd2 100644 --- a/docs/README.developers +++ b/docs/README.developers @@ -6,7 +6,19 @@ This file is intended to provide a few tips for anyone doing development on nmh. Developers who learn things "the hard way" about the nmh codebase (as opposed to local info best encoded in a comment) are encouraged to share their wisdom here. -The topics are organized alphabetically. +Following a commit checklist, the topics are organized alphabetically. + +---------------- +commit checklist +---------------- + +1. code updated? +2. test added? +3. man page and other documentation updated? +4. docs/pending-release-notes updated? +5. should commit message reference bug report? +6. update/close bug report (with commit id)? +7. notify nmh-users? ------------------------- diff --git a/docs/pending-release-notes b/docs/pending-release-notes index c286c3d..468da54 100644 --- a/docs/pending-release-notes +++ b/docs/pending-release-notes @@ -22,6 +22,8 @@ NEW FEATURES pid.time@localname, where time is in seconds, and matches previous behavior. The random style replaces the localname with some (pseudo)random bytes and uses microsecond-resolution time. +- Added -clobber switch to mhstore(1) to control overwriting of + existing files. ---------------------------- OBSOLETE/DEPRECATED FEATURES diff --git a/man/mhstore.man b/man/mhstore.man index 470714c..be99f0d 100644 --- a/man/mhstore.man +++ b/man/mhstore.man @@ -19,6 +19,8 @@ mhstore \- store contents of MIME messages into files .IR content ] \&... .RB [ \-auto " | " \-noauto ] +.RB [ \-clobber +.IR always " | " auto " | " suffix " | " ask " | " never ] .RB [ \-rcache .IR policy ] .RB [ \-wcache @@ -268,6 +270,72 @@ mhstore-store-application/PostScript: %m%P.ps .fi .RE .PP +.SS "Overwriting Existing Files" +The +.B \-clobber +switch controls whether +.B mhstore +should overwrite existing files. The allowed values for this switch +and corresponding behavior when +.B mhstore +encounters an existing file are: +.PP +.RS 5 +.nf +.ta \w'suffix 'u +always Overwrite existing file (default) +auto Create new file of form name-n.extension +suffix Create new file of form name.extension.n +ask Prompt the user to specify whether or not to overwrite + the existing file +never Do not overwrite existing file +.fi +.RE +.PP +With +.I auto +and +.IR suffix , +.I n +is the lowest unused number, starting from one, in the same form. If +a filename does not have an extension (following a '.'), then +.I auto +and +.I suffix +create a new file of the form +.I name-n +and +.IR name.n , +respectively. With +.I never +and +.IR ask , +the exit status of +.B mhstore +will be the number of files that were requested but not stored. +.PP +With +.IR ask , +if standard input is connected to a terminal, +the user is prompted to respond +.IR yes , +.IR no , +or +.I rename +to whether the file should be overwritten. The responses +can be abbreviated. If the user responds with +.IR rename , +then +.B mhstore +prompts the user for the name of the new file to be created. If it is +a relative path name (does not begin with '/'), then it is relative to +the current directory. If it is an absolute or relative path to a +directory that does not exist, the user will be prompted whether to +create the directory. If standard input is not connected to a +terminal, +.I ask +behaves the same as +.IR always . .SS "Reassembling Messages of Type message/partial" .B mhstore is also able to reassemble messages that have been @@ -462,6 +530,7 @@ mhbuild(1), mhlist(1), mhshow(1), sendfiles(1) .RB ` +folder "' defaults to the current folder" .RB ` msgs "' defaults to cur" .RB ` \-noauto ' +.RB ` \-clobber\ always ' .RB ` \-nocheck ' .RB ` \-rcache\ ask ' .RB ` \-wcache\ ask ' diff --git a/test/mhstore/test-mhstore b/test/mhstore/test-mhstore index 55cea9f..421580b 100755 --- a/test/mhstore/test-mhstore +++ b/test/mhstore/test-mhstore @@ -239,6 +239,45 @@ EOF run_test 'mhstore last' 'storing message 15 as file 15.txt' check $expected 15.txt +# check -clobber always +folder +inbox 7 > /dev/null +touch 7.txt +cat > $expected </dev/null 2>&1 +run_test "echo $?" 1 +set -e + +/bin/rm -f 7.txt 7-1.txt 7.txt.1 + # check with relative nmh-storage profile component storagedir=storagedir dir="$MH_TEST_DIR/Mail/inbox/$storagedir" diff --git a/test/post/test-post-basic b/test/post/test-post-basic index a2e4fac..74ef5e1 100755 --- a/test/post/test-post-basic +++ b/test/post/test-post-basic @@ -56,9 +56,50 @@ This is a blank test EOF run_test "send -draft -server 127.0.0.1 -port $localport" \ - "post: message has no From: header + "post: message has no From: header post: See default components files for examples post: re-format message and try again send: message not delivered to anyone" +# +# Make sure that empty Nmh-* header lines are ignored, and that post +# warns about non-empty ones. +# +cat > "${MH_TEST_DIR}/Mail/draft" < +To: Somebody Else +Nmh-Attachment: +Nmh-Unused: suppress this line +Subject: Test + +This is a test +. +EOF + +cat > "${testname}.expected" < +RCPT TO: +DATA +From: Mr Nobody +To: Somebody Else +Subject: Test +Date: + +This is a test +.. +. +QUIT +EOF + +cat > "${testname}.expected_send_output" <${testname}.send_output 2>&1 + +check "${testname}.send_output" "${testname}.expected_send_output" + + exit ${failed:-0} diff --git a/test/post/test-post-common.sh b/test/post/test-post-common.sh index bd37c7d..1118d9c 100755 --- a/test/post/test-post-common.sh +++ b/test/post/test-post-common.sh @@ -33,7 +33,7 @@ test_post () # retry a few times if it fails... status=1 for i in 0 1 2 3 4 5 6 7 8 9; do - if send -draft -server 127.0.0.1 -port $localport $3 >/dev/null 2>&1 + if send -draft -server 127.0.0.1 -port $localport $3 then status=0 break diff --git a/uip/mhstore.c b/uip/mhstore.c index e491dd9..fa30e03 100644 --- a/uip/mhstore.c +++ b/uip/mhstore.c @@ -47,16 +47,21 @@ static struct swit switches[] = { { "version", 0 }, #define HELPSW 12 { "help", 0 }, +#define CLOBBERSW 13 + { "clobber always|auto|suffix|ask|never", 0 }, /* * switches for debugging */ -#define DEBUGSW 13 +#define DEBUGSW 14 { "debug", -5 }, { NULL, 0 } }; +int save_clobber_policy (const char *); +extern int files_not_clobbered; + /* mhparse.c */ extern char *tmp; /* directory to place temp files */ @@ -213,6 +218,14 @@ do_cache: case NVERBSW: verbosw = 0; continue; + case CLOBBERSW: + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + if (save_clobber_policy (cp)) { + adios (NULL, "invalid argument, %s, to %s", argp[-1], + argp[-2]); + } + continue; case DEBUGSW: debugsw = 1; continue; @@ -375,7 +388,7 @@ do_cache: context_save (); /* save the context file */ } - done (0); + done (files_not_clobbered); return 1; } diff --git a/uip/mhstoresbr.c b/uip/mhstoresbr.c index a604471..ca03bda 100644 --- a/uip/mhstoresbr.c +++ b/uip/mhstoresbr.c @@ -75,7 +75,7 @@ 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 *); /* * Main entry point to store content @@ -589,7 +589,9 @@ store_content (CT ct, CT p) return show_content_aux (ct, 1, 0, buffer + 1, dir); /* record the filename */ - ct->c_storage = add (buffer, NULL); + if ((ct->c_storage = clobber_check (add (buffer, NULL))) == NULL) { + return NOTOK; + } got_filename: /* flush the output stream */ @@ -1076,3 +1078,230 @@ copy_some_headers (FILE *out, CT ct) return OK; } + +/******************************************************************************/ +/* -clobber support */ + +enum clobber_policy_t { + NMH_CLOBBER_ALWAYS, + NMH_CLOBBER_AUTO, + NMH_CLOBBER_SUFFIX, + NMH_CLOBBER_ASK, + NMH_CLOBBER_NEVER +}; + +static enum clobber_policy_t clobber_policy = NMH_CLOBBER_ALWAYS; + +int files_not_clobbered = 0; + +int +save_clobber_policy (const char *value) { + if (! mh_strcasecmp (value, "always")) { + clobber_policy = NMH_CLOBBER_ALWAYS; + } else if (! mh_strcasecmp (value, "auto")) { + clobber_policy = NMH_CLOBBER_AUTO; + } else if (! mh_strcasecmp (value, "suffix")) { + clobber_policy = NMH_CLOBBER_SUFFIX; + } else if (! mh_strcasecmp (value, "ask")) { + clobber_policy = NMH_CLOBBER_ASK; + } else if (! mh_strcasecmp (value, "never")) { + clobber_policy = NMH_CLOBBER_NEVER; + } else { + return 1; + } + + return 0; +} + + +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); + ++files_not_clobbered; + 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; + ++files_not_clobbered; + } + + return buffer; +} + + +static char * +clobber_check (char *original_file) { + /* clobber policy return value + * -------------- ------------ + * -always file + * -auto file-.extension + * -suffix file. + * -ask file, 0, or another filename/path + * -never 0 + */ + + char *file; + char *cwd = NULL; + int check_again; + + if (clobber_policy == NMH_CLOBBER_ASK) { + /* Save cwd for possible use in loop below. */ + char *slash; + + cwd = add (original_file, NULL); + slash = strrchr (cwd, '/'); + + if (slash) { + *slash = '\0'; + } else { + /* original_file wasn't a full path, which shouldn't happen. */ + cwd = NULL; + } + } + + do { + struct stat st; + + file = original_file; + check_again = 0; + + switch (clobber_policy) { + case NMH_CLOBBER_ALWAYS: + break; + + case NMH_CLOBBER_SUFFIX: + case NMH_CLOBBER_AUTO: + if (stat (file, &st) == OK) { + file = next_version (original_file, clobber_policy); + } + 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 }, { "no", 0 }, { "rename", 0 }, { NULL, 0 } }; + char **ans; + + if (isatty (fileno (stdin))) { + char *prompt = + concat ("Overwrite \"", file, "\" [y/n/rename]? ", NULL); + ans = getans (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; + ++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; + ++files_not_clobbered; + } else { + char *newline = strchr (buf, '\n'); + if (newline) { + *newline = '\0'; + } + } + + free (file); + + if (buf[0] == '/') { + /* Full path, use it. */ + file = add (buf, NULL); + } else { + /* Relative path. */ + file = cwd ? concat (cwd, "/", buf, NULL) : add (buf, NULL); + } + + 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; + ++files_not_clobbered; + } + break; + } + + original_file = file; + } while (check_again); + + if (cwd) { + free (cwd); + } + + return file; +} + +/* -clobber support */ +/******************************************************************************/ diff --git a/uip/post.c b/uip/post.c index 526b5e4..ff75197 100644 --- a/uip/post.c +++ b/uip/post.c @@ -709,7 +709,21 @@ putfmt (char *name, char *str, FILE *out) } if ((i = get_header (name, hdrtab)) == NOTOK) { - fprintf (out, "%s: %s", name, str); + if (strncasecmp (name, "nmh-", 4)) { + fprintf (out, "%s: %s", name, str); + } else { + /* Filter out all Nmh-* headers, because Norm asked. They + should never have reached this point. Warn about any + that are non-empty. */ + if (strcmp (str, "\n")) { + char *newline = strchr (str, '\n'); + if (newline) *newline = '\0'; + if (! whomsw) { + advise (NULL, "ignoring header line -- %s: %s", name, str); + } + } + } + return; } diff --git a/uip/sendsbr.c b/uip/sendsbr.c index 08dc766..dfd974f 100644 --- a/uip/sendsbr.c +++ b/uip/sendsbr.c @@ -209,19 +209,27 @@ attach(char *attachment_header_field_name, char *draft_file_name, field = (char *)mh_xmalloc(field_size = 256); /* - * Scan the draft file for a header field name that matches the -attach - * argument. The existence of one indicates that the draft has attachments. - * Bail out if there are no attachments because we're done. Read to the - * end of the headers even if we have no attachments. + * Scan the draft file for a header field name, with a non-empty + * body, that matches the -attach argument. The existence of one + * indicates that the draft has attachments. Bail out if there + * are no attachments because we're done. Read to the end of the + * headers even if we have no attachments. */ length = strlen(attachment_header_field_name); has_attachment = 0; - while (get_line() != EOF && *field != '\0' && *field != '-') - if (strncasecmp(field, attachment_header_field_name, length) == 0 && field[length] == ':') - has_attachment = 1; + while (get_line() != EOF && *field != '\0' && *field != '-') { + if (strncasecmp(field, attachment_header_field_name, length) == 0 && + field[length] == ':') { + for (p = field + length + 1; *p == ' ' || *p == '\t'; p++) + ; + if (strlen (p) > 0) { + has_attachment = 1; + } + } + } if (has_attachment == 0) return (DONE); @@ -267,14 +275,16 @@ attach(char *attachment_header_field_name, char *draft_file_name, } /* - * Start at the beginning of the draft file. Copy all non-attachment header fields - * to the temporary composition file. Then add the dashed line separator. + * Start at the beginning of the draft file. Copy all + * non-attachment header fields to the temporary composition + * file. Then add the dashed line separator. */ rewind(draft_file); while (get_line() != EOF && *field != '\0' && *field != '-') - if (strncasecmp(field, attachment_header_field_name, length) != 0 || field[length] != ':') + if (strncasecmp(field, attachment_header_field_name, length) != 0 || + field[length] != ':') (void)fprintf(composition_file, "%s\n", field); (void)fputs("--------\n", composition_file); @@ -301,8 +311,9 @@ attach(char *attachment_header_field_name, char *draft_file_name, "text/plain"); /* - * Now, go back to the beginning of the draft file and look for header fields - * that specify attachments. Add a mhbuild MIME composition file for each. + * Now, go back to the beginning of the draft file and look for + * header fields that specify attachments. Add a mhbuild MIME + * composition file for each. */ if ((fp = fopen (p = etcpath ("mhn.defaults"), "r"))) { @@ -313,22 +324,36 @@ attach(char *attachment_header_field_name, char *draft_file_name, rewind(draft_file); while (get_line() != EOF && *field != '\0' && *field != '-') { - if (strncasecmp(field, attachment_header_field_name, length) == 0 && field[length] == ':') { + if (strncasecmp(field, attachment_header_field_name, length) == 0 && + field[length] == ':') { for (p = field + length + 1; *p == ' ' || *p == '\t'; p++) ; - /* Don't set the default content type so take - make_mime_composition_file_entry() will try to infer it - from the file type. */ - make_mime_composition_file_entry(p, attachformat, 0); + /* Skip empty attachment_header_field_name lines. */ + if (strlen (p) > 0) { + struct stat st; + if (stat (p, &st) == OK) { + if (S_ISREG (st.st_mode)) { + /* Don't set the default content type so take + make_mime_composition_file_entry() will try + to infer it from the file type. */ + make_mime_composition_file_entry(p, attachformat, 0); + } else { + adios (NULL, "unable to attach %s, not a plain file", + p); + } + } else { + adios (NULL, "unable to access file \"%s\"", p); + } + } } } (void)fclose(composition_file); /* - * We're ready to roll! Run mhbuild on the composition file. Note that mhbuild - * is in the context as buildmimeproc. + * We're ready to roll! Run mhbuild on the composition file. + * Note that mhbuild is in the context as buildmimeproc. */ (void)sprintf(buf, "%s %s", buildmimeproc, composition_file_name); diff --git a/uip/whatnowsbr.c b/uip/whatnowsbr.c index 121a79b..d54877e 100644 --- a/uip/whatnowsbr.c +++ b/uip/whatnowsbr.c @@ -97,7 +97,7 @@ static struct swit aleqs[] = { #define PWDCMDSW 11 { "pwd", 0 }, #define LSCMDSW 12 - { "ls", 0 }, + { "ls", 2 }, #define ATTACHCMDSW 13 { "attach", 0 }, #define DETACHCMDSW 14 @@ -120,7 +120,7 @@ static int buildfile (char **, char *); static int check_draft (char *); static int whomfile (char **, char *); static int removefile (char *); -static void writelscmd(char *, int, char **); +static void writelscmd(char *, int, char *, char **); static void writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp); static FILE* popen_in_dir(const char *dir, const char *cmd, const char *type); static int system_in_dir(const char *dir, const char *cmd); @@ -336,7 +336,7 @@ WhatNow (int argc, char **argv) */ if (*(argp+1) == (char *)0) { - (void)sprintf(buf, "$SHELL -c \"cd;pwd\""); + (void)sprintf(buf, "$SHELL -c \"cd&&pwd\""); } else { writesomecmd(buf, BUFSIZ, "cd", "pwd", argp); @@ -366,7 +366,7 @@ WhatNow (int argc, char **argv) * Use the user's shell so that we can take advantage of any * syntax that the user is accustomed to. */ - writelscmd(buf, sizeof(buf), argp); + writelscmd(buf, sizeof(buf), "", argp); (void)system_in_dir(cwd, buf); break; @@ -431,7 +431,7 @@ WhatNow (int argc, char **argv) * Build a command line that causes the user's shell to list the file name * arguments. This handles and wildcard expansion, tilde expansion, etc. */ - writelscmd(buf, sizeof(buf), argp); + writelscmd(buf, sizeof(buf), "-d", argp); /* * Read back the response from the shell, which contains a number of lines @@ -520,7 +520,7 @@ WhatNow (int argc, char **argv) * We feed all the file names to the shell at once, otherwise you can't * provide a file name with a space in it. */ - writelscmd(buf, sizeof(buf), argp); + writelscmd(buf, sizeof(buf), "-d", argp); if ((f = popen_in_dir(cwd, buf, "r")) != (FILE *)0) { while (fgets(shell, sizeof (shell), f) != (char *)0) { *(strchr(shell, '\n')) = '\0'; @@ -561,9 +561,9 @@ writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp) * new C99 mandated 'number of chars that would have been written' */ /* length checks here and inside the loop allow for the - * trailing ';', trailcmd, '"' and NUL + * trailing "&&", trailcmd, '"' and NUL */ - int trailln = strlen(trailcmd) + 3; + int trailln = strlen(trailcmd) + 4; if (ln < 0 || ln + trailln > bufsz) adios((char *)0, "arguments too long"); @@ -579,9 +579,9 @@ writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp) cp += ln; } if (*trailcmd) { - *cp++ = ';'; + *cp++ = '&'; *cp++ = '&'; strcpy(cp, trailcmd); - cp += trailln - 3; + cp += trailln - 4; } *cp++ = '"'; *cp = 0; @@ -592,9 +592,11 @@ writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp) * arguments. This handles and wildcard expansion, tilde expansion, etc. */ static void -writelscmd(char *buf, int bufsz, char **argp) +writelscmd(char *buf, int bufsz, char *lsoptions, char **argp) { - writesomecmd(buf, bufsz, "ls", "", argp); + char *lscmd = concat ("ls ", lsoptions, " --", NULL); + writesomecmd(buf, bufsz, lscmd, "", argp); + free (lscmd); } /* Like system(), but run the command in directory dir.