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?
-------------------------
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
.IR content ]
\&...
.RB [ \-auto " | " \-noauto ]
+.RB [ \-clobber
+.IR always " | " auto " | " suffix " | " ask " | " never ]
.RB [ \-rcache
.IR policy ]
.RB [ \-wcache
.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
.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 '
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 <<EOF
+This is message number 7
+EOF
+run_test 'mhstore' 'storing message 7 as file 7.txt'
+check $expected 7.txt 'keep first'
+run_test 'mhstore -clobber always' 'storing message 7 as file 7.txt'
+check $expected 7.txt 'keep first'
+
+# check -clobber auto
+touch 7.txt
+run_test 'mhstore -clobber auto' 'storing message 7 as file 7-1.txt'
+check $expected 7-1.txt 'keep first'
+touch 7-1.txt
+run_test 'mhstore -clobber auto' 'storing message 7 as file 7-2.txt'
+check $expected 7-2.txt 'keep first'
+
+# check -clobber suffix
+run_test 'mhstore -clobber suffix' 'storing message 7 as file 7.txt.1'
+check $expected 7.txt.1 'keep first'
+touch 7.txt.1
+run_test 'mhstore -clobber suffix' 'storing message 7 as file 7.txt.2'
+check $expected 7.txt.2 'keep first'
+
+# Don't check -clobber ask because it requires connection to a
+# terminal, and this test won't always be run with one.
+
+# check -clobber never. Its exit status is the number of files not overwritten.
+run_test 'mhstore -clobber never' \
+ "mhstore: will not overwrite `pwd`/7.txt with -clobber never"
+set +e
+mhstore -clobber never >/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"
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" <<EOF
+From: Mr Nobody <nobody@example.com>
+To: Somebody Else <somebody@example.com>
+Nmh-Attachment:
+Nmh-Unused: suppress this line
+Subject: Test
+
+This is a test
+.
+EOF
+
+cat > "${testname}.expected" <<EOF
+EHLO nosuchhost.example.com
+MAIL FROM:<nobody@example.com>
+RCPT TO:<somebody@example.com>
+DATA
+From: Mr Nobody <nobody@example.com>
+To: Somebody Else <somebody@example.com>
+Subject: Test
+Date:
+
+This is a test
+..
+.
+QUIT
+EOF
+
+cat > "${testname}.expected_send_output" <<EOF
+post: ignoring header line -- Nmh-Unused: suppress this line
+EOF
+
+test_post "${testname}.actual" "${testname}.expected" \
+ >${testname}.send_output 2>&1
+
+check "${testname}.send_output" "${testname}.expected_send_output"
+
+
exit ${failed:-0}
# 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
{ "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 */
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;
context_save (); /* save the context file */
}
- done (0);
+ done (files_not_clobbered);
return 1;
}
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
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 */
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-<digits>.extension
+ * -suffix file.<digits>
+ * -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 */
+/******************************************************************************/
}
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;
}
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);
}
/*
- * 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);
"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"))) {
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);
#define PWDCMDSW 11
{ "pwd", 0 },
#define LSCMDSW 12
- { "ls", 0 },
+ { "ls", 2 },
#define ATTACHCMDSW 13
{ "attach", 0 },
#define DETACHCMDSW 14
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);
*/
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);
* 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;
* 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
* 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';
* 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");
cp += ln;
}
if (*trailcmd) {
- *cp++ = ';';
+ *cp++ = '&'; *cp++ = '&';
strcpy(cp, trailcmd);
- cp += trailln - 3;
+ cp += trailln - 4;
}
*cp++ = '"';
*cp = 0;
* 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.