From: Jon Steinhart Date: Mon, 19 Aug 2002 20:50:41 +0000 (+0000) Subject: Added an improved user interface for sending messages with attachments. X-Git-Tag: RELEASE_1_2~117 X-Git-Url: http://git.marmaro.de/?a=commitdiff_plain;h=7480dbc14bc90f2d872d434205c0784704213252;p=mmh Added an improved user interface for sending messages with attachments. --- diff --git a/README-ATTACHMENTS b/README-ATTACHMENTS new file mode 100644 index 0000000..ff9db2a --- /dev/null +++ b/README-ATTACHMENTS @@ -0,0 +1,281 @@ +Jon Steinhart's (jon@fourwinds.com) Attachment Handling Mods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A bunch of changes have been made to improve the nmh user interface for +handling MIME attachments. + +Why Did I Do This? +~~~~~~~~~~~~~~~~~~ + +Although nmh contains the necessary functionality for MIME message handing, +the interface to this functionality is pretty obtuse. There's no way that +I'm ever going to convince my partner to write mhbuild composition files! +And even though I know how to write them, I often screw up when sending a +message that contains a shell script because I forget that I have to double +any # at the start of a line, which shell scripts have galore. + +These changes simplify the task of managing attachments on draft files. +They allow attachments to be added, listed, and deleted. MIME messages are +automatically created when drafts with attachments are sent. + +The Simple Setup +~~~~~~~~~~~~~~~~ + +The gory details are all given below. Here's how to set things up simply. This +only works if you're using the "standard" interface, i.e., whatnow. Life is more +complicated if you run mh-e. + +Add the following to your .mh_profile: + + send: -attach X-MH-Attachment + whatnow: -attach X-MH-Attachment + +You may already have send and whatnow lines in your .mh_profile; if you do, just add +the new stuff to the end of the line. For example, mine looks like: + + send: -alias aliases -attach X-MH-Attachment + whatnow: -attach X-MH-Attachment + +That's it. Next time you want to send an attachment, say "attach filename" at the +whatnow prompt and you're done. You can check your attachments by saying "alist". + +Did I Do This Correctly? +~~~~~~~~~~~~~~~~~~~~~~~~ + +Hard to say. Despite lots of time looking at the nmh code, I can't say that +I get the philosophy behind its structure. + +I am aware of two deviations from what I saw in the nmh code. + + 1. I commented my changes. + + 2. It's been years since I've used a VT-100, so I don't try to make code + fit into 80 columns anymore. Seems silly to me. + +What Did I Do? +~~~~~~~~~~~~~~ + +I made changes to the following files: + + h/ + prototypes.h + man/ + anno.man + send.man + whatnow.man + uip/ + Makefile.in + anno.c + annosbr.c + send.c + sendsbr.c + viamail.c (needed change for new sendsbr argument) + whatnowsbr.c + +Attachments are associated with messages using header fields. For example, a +draft that looks like this + + To: jon + Subject: test of attachments + X-MH-Attachment: /export/home/jon/foo + X-MH-Attachment: /export/home/jon/bar + X-MH-Attachment: /export/home/jon/test/foo + -------- + +has the files "foo", "bar", and foo as attachments. + +Although I use the header field name "X-MH-Attachment" to indicate +attachments, the implementation allows any header field name. + +The advantage of using header fields is that the list of attachments +travels with the draft so it remains valid across editing sessions. + +Note that the header fields for attachments are removed from the message +before it is sent. + +Since I was adding header fields to messages, it seemed sensible to use the +existing anno program to do this for me. This required several changes to +generalize anno: + + o I added a -draft option that permits annotations (header fields) to + be added to the draft instead of a message sequence. + + o I added a -delete option that allows annotations to be deleted. + + o I added a -list option that allows annotations to be listed. + + o I added a -number option that modifies the behavior of -delete and -list. + + o I added a -append option so that annotations are appended to the headers + instead of the default prepend. Without this, attachments come out in + reverse order. + +Using the modified anno, the example above could be created (assuming that the +draft exists) by + + prompt% anno -draft -comp X-MH-Attachment -text /export/home/jon/foo -nodate -append + prompt% anno -draft -comp X-MH-Attachment -text /export/home/jon/bar -nodate -append + prompt% anno -draft -comp X-MH-Attachment -text /export/home/jon/test/foo -nodate -append + +One can quite easily make an "attach" command using shell scripts, aliases or functions. +For example, here's a bash function that does the job: + + function attach() { for i in $*; do anno -append -nodate -draft -comp X-MH-Attachment -text "$i"; done; } + +The following examples show the different ways in which attachments can be +listed. + + prompt% anno -list -draft -comp X-MH-Attachment + foo + bar + foo + + prompt% anno -list -draft -comp X-MH-Attachment -text / + /export/home/jon/foo + /export/home/jon/bar + /export/home/jon/test/foo + + prompt% anno -list -draft -comp X-MH-Attachment -number + 1 foo + 2 bar + 3 foo + + prompt% anno -list -draft -comp X-MH-Attachment -text / -number + 1 /export/home/jon/foo + 2 /export/home/jon/bar + 3 /export/home/jon/test/foo + + prompt% + +Why all these listing options? + +I feel that the listing as produced by the first example is what most people +would want most of the time. + +The listing as produced by the second example seemed necessary for situations +where there were several attachments with the same file name in different +directories. + +I included the numbering option so that attachments could be deleted by number +which might be easier in situations where there were several attachments with +the same file name in different directories, as in the above example. + +Attachments are deleted using the -delete option. + + prompt% anno -delete -draft -comp X-MH-Attachment -text foo + +deletes the first attachment since the foo matches the basename of the attachment +name. + + prompt% anno -delete -draft -comp X-MH-Attachment -text /export/home/jon/test/foo + +deletes the third attachment since the text is a full path name and matches. + + prompt% anno -delete -draft -comp X-MH-Attachment -number 2 + +deletes the second attachment. + +The attachment annotations are converted to a MIME message by send. I'm not +completely sure that this is the right place to do it, as opposed to doing +it in post, but both would work. It seemed to me to make more sense to do +it in send so that all of the various post options would apply to the MIME +message instead of the original draft file. + +I added an -attach option to send that specifies the header field name used +for attachments. Send performs the following additional steps if this option +is set: + + o It scans the header of the draft for attachments. Normal behavior applies + if none exist. + + o A temporary mhbuild composition file is created if there are attachments. + + o All non-attachment headers are copied from the draft file to the + composition file. + + o The body of the draft is copied to a temporary body file if it contains at + least one non-whitespace character. A mhbuild directive for this file is + appended to the composition file. Note that this eliminates the problem + of lines beginning with the # character in the message body. + + o A mhbuild directive is appended to the composition file for each attachment + header. + + o mhbuild is run on the composition file, converting it to a MIME message. + + o The converted composition file is substituted for the original draft file + and run through the rest of send. + + o The original draft file is renamed instead of the converted composition + file. This preserves the original message instead of the composition file + which is really what a user would want. + + o The ,file.orig file created by mhbuild is removed as it's a nuisance. + +The mhbuild directives appended to the composition file are constructed as +follows: + + o The content-type a file with a dot-suffix is obtained from the list of + mhshow-suffix- entries in the profile. + + o A file with no dot-suffix or no entry in the profile is assigned a + content-type of application/octet-stream if it contains any non-ASCII + characters. + + o A file with no dot-suffix or no entry in the profile is assigned a + content-type of text/plain if it contains only ASCII characters. + + o The directive is of the form: + + #content-type; name="basename"; x-unix-mode=mode [ description ] filename + + The content type is derived as discussed above. The basename is the + last component of the pathname given in the body of the attachment header + field. The mode is file permissions. The description is obtained by + running the file command on the attachment file. The filename is the + field body from the attachment header field. + +I added an -attach option to whatnow that specifies the header field name for +attachments. + +I added to the commands available at the whatnow prompt to provide an interface +to the attachment mechanism. + +I'm not completely happy with some of these additions because they duplicate +shell functionality. I'm not sure that there's a good way around it other than +to not use whatnow. + +The first three additions (the ones I'm not happy with) are cd, ls, and pwd. +These do the same thing as their system counterparts. As a matter of fact, +these are implemented by running the commands in a subshell. I did this because +I wanted proper interpretation of shell-specific things like ~ and wildcard +expansion. + +The next command is attach. This takes a list of files and adds them to the draft +as attachments using the same code as the modified anno. The list of files is +run through ls using the user's shell, so wildcard expansion and all that works. + +The alist command lists the attachments on the current draft using listing function +that I added to anno. It takes two optional options, -l for a long listing meaning +full path names, and -n for a numbered listing. + +The detach command removes attachments from the current draft, again using the +modified anno. The arguments are interpreted as numbers if the -n option is used, +otherwise they're interpreted as file names. File names are shoveled through ls +using the user's shell in the directory containing the file for wildcard expansion +and such. File names are matched against the last pathname component unless they +begin with a / in which case they're matched against the entire name. + +What's Left To Do? +~~~~~~~~~~~~~~~~~~ + +Nothing on this stuff. When I get time I'd like to improve the interface +for reading messages with attachments. It's my opinion that, because of the +command line nature of nmh, it makes the most sense to treat attachments as +separate messages. In other words one should be able to read the next +attachment using next, and the previous one using prev. One should be able +to show and scan attachments. This would probably involve a major change +in the message numbering scheme to allow something like 123.4 to indicate +attachment 4 on message 123. + + Jon Steinhart diff --git a/h/prototypes.h b/h/prototypes.h index d252ead..63ea87e 100644 --- a/h/prototypes.h +++ b/h/prototypes.h @@ -154,11 +154,12 @@ char *OfficialName(char *); /* * prototypes for some routines in uip */ -int annotate (char *, char *, char *, int, int); +int annotate (char *, char *, char *, int, int, int, int); +void annolist(char *, char *, char *, int); int distout (char *, char *, char *); void replout (FILE *, char *, char *, struct msgs *, int, int, char *, char *, char *); -int sendsbr (char **, int, char *, struct stat *, int); +int sendsbr (char **, int, char *, struct stat *, int, char *); int what_now (char *, int, int, char *, char *, int, struct msgs *, char *, int, char *); diff --git a/man/anno.man b/man/anno.man index 7a735bc..b626e2a 100644 --- a/man/anno.man +++ b/man/anno.man @@ -15,6 +15,12 @@ anno \- annotate messages .IR field ] .RB [ \-inplace " | " \-noinplace ] .RB [ \-date " | " \-nodate ] +.RB [ \-draft ] +.RB [ \-append ] +.RB [ \-list ] +.RB [ \-delete ] +.RB [ \-number +.IR [ num ]] .RB [ \-version ] .RB [ \-help ] .RB [ \-text @@ -22,8 +28,22 @@ anno \- annotate messages .ad .SH DESCRIPTION .B Anno -annotates the specified messages in the named folder using -the field and body. +manipulates header fields or +.I annotations +in messages. +Header fields consist of a field name and an optional field body +as defined by RFC-2822. +The +.B -component +option specifies the field name, and the +.B -text +option specifies the field body. +.PP +The messages are either the +.I msgs +in the named folder, or the draft if invoked with the +.B -draft +option. .PP Usually, annotation is performed by the commands .BR dist , @@ -48,6 +68,13 @@ The switch inhibits the date annotation, leaving only the body annotation. .PP +By default, +.B anno +prepends the annotations to the message. +Annotations are instead appended if the +.B -append +option is specified. +.PP If a .B \-component .I field @@ -57,8 +84,8 @@ is invoked, .B anno will prompt the user for the name of field for the annotation. .PP -The field specified should be a valid 822-style message field name, -which means that it should consist of alphanumerics (or dashes) only. +The field specified must be a valid 2822-style message field name, +which means that it may only consist of alphanumerics and dashes, The body specified is arbitrary text. .PP Normally @@ -67,7 +94,45 @@ does the annotation inplace in order to preserve any links to the message. You may change this by using the .B \-noinplace switch. - +.PP +The +.B -list +option produces a listing of the field bodies for header fields with +names matching the specified component, one per line. +The listing is numbered, starting at 1, if the +.B -number +option is also used. +A tab character separates the number and the field body. +The field body is treated as if it is a file name, and only the final +path name component is listed. +The complete field body is listed if the +.B -text +option is used, the contents of the text are ignored. +.PP +The +.B -delete +option removes header fields from messages. +The first header field whose name matches the component is deleted if +no other options are specified. +If the +.B -text +option is used in conjunction with the +.B -delete +option, the first header field whose name matches the component and +whose body matches the text is deleted. +The text is treated as if it was a file name; if it begins with a +slash, the entire field body must match the text, otherwise just the +last path name component of the field body must match. +If the +.B -number +option is used in conjuction with the +.B -delete +option, header field +.I num +whose name matches the component is deleted. +The number matches that which is produced by the +.B -list +option. .SH FILES .fc ^ ~ .nf diff --git a/man/send.man b/man/send.man index 5a2f897..85b3164 100644 --- a/man/send.man +++ b/man/send.man @@ -40,6 +40,8 @@ send \- send a message \&...] .RB [ \-version ] .RB [ \-help ] +.RB [ \-attach +.IR header-field-name ] .ad .SH DESCRIPTION .B Send @@ -63,6 +65,41 @@ profile component. Most of the features attributed to .B send are actually performed by .BR post . + +.PP +If a +.I header-field-name +is supplied using the +.B -attach +option, the draft is scanned for a header whose field name matches the +supplied +.IR header-field-name . +The draft is converted to a MIME message if one or more matches are found. +This conversion occurs before all other processing. +.PP +The first part of the MIME message is the draft body if that body contains +any non-blank characters. +The body of each header field whose name matches the +.I header-field-name +is interpreted as a file name, and each file named is included as a separate +part in the MIME message. +.PP +For file names with dot suffixes, the context is scanned for a +.I mhshow-suffix- +entry for that suffix. +The content-type for the part is taken from that context entry if a match is +found. +If no match is found or the file does not have a dot suffix, the content-type +is text/plain if the file contains only ASCII characters or application/octet-stream +if it contains characters outside of the ASCII range. +.PP +Each part contains a name attribute that is the last component of the path name. +A +.I x-unix-mode +attribute containing the file mode accompanies each part. +Finally, a description attribute is generated by running the +.I file +command on the file. .PP If .B \-push diff --git a/man/whatnow.man b/man/whatnow.man index 5bc5da7..cc2e989 100644 --- a/man/whatnow.man +++ b/man/whatnow.man @@ -22,6 +22,8 @@ whatnow \- prompting front-end for sending messages .RI [ file ] .RB [ \-version ] .RB [ \-help ] +.RB [ \-attach +.IR header-field-name ] .ad .SH DESCRIPTION .B Whatnow @@ -45,18 +47,18 @@ repetitively prompts the user with \*(lqWhat now?\*(rq and awaits a response. The valid responses are: .PP .RS 5 -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B edit re\-edit using the same editor that was used on the preceding round unless a profile entry \*(lq\-next: \*(rq names an alternate editor -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B edit invoke for further editing -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B refile +folder refile the draft into the given folder -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B mime process the draft as MIME composition file using the @@ -64,38 +66,56 @@ the command .RB ( mhbuild by default) -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B display list the message being distributed/replied\-to on the terminal -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B list list the draft on the terminal -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B send send the message -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B send \-watch send the message and monitor the delivery process -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B push send the message in the background -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B whom list the addresses that the message will go to -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B whom \-check list the addresses and verify that they are acceptable to the transport service -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B quit preserve the draft and exit -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B quit \-delete delete the draft and exit -.TP \w'refilezzfolderz'u +.TP \w'refilezzzzfolderz'u .B delete delete the draft and exit +.TP \w'refilezzzzfolderz'u +.B cd directory +use the directory when interpreting attachment file names +.TP \w'refilezzzzfolderz'u +.B pwd +print the working directory for attachment files +.TP \w'refilezzzzfolderz'u +.B ls [ls-options] +list files in the attachment working directory using the ls command +.TP \w'refilezzzzfolderz'u +.B attach files +add the named files to the draft as MIME attachments +.TP \w'refilezzzzfolderz'u +.B alist [-ln] +list the MIME attachments, either short, long [-l] or numbered [-n] +.TP \w'refilezzzzfolderz'u +.B detach [-n] files-or-numbers +remove MIME attachments, either by file name or by number with -n .RE .PP When entering your response, you need only type enough characters diff --git a/uip/anno.c b/uip/anno.c index ea60e6d..b92abdc 100644 --- a/uip/anno.c +++ b/uip/anno.c @@ -7,6 +7,42 @@ * 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. + * + * Four new options have been added: delete, list, number, and draft. + * Message header fields are used by the new MIME attachment code in + * the send command. Adding features to generalize the anno command + * seemed to be a better approach than the creation of a new command + * whose features would overlap with those of the anno command. + * + * The -draft option causes anno to operate on the current draft file + * instead of on a message sequence. + * + * The -delete option deletes header elements that match the -component + * field name. If -delete is used without the -text option, the first + * header field whose field name matches the component name is deleted. + * If the -delete is used with the -text option, and the -text argument + * begins with a /, the first header field whose field name matches the + * component name and whose field body matches the text is deleted. If + * the -text argument does not begin with a /, then the text is assumed + * to be the last component of a path name, and the first header field + * whose field name matches the component name and a field body whose + * last path name component matches the text is deleted. If the -delete + * option is used with the new -number option described below, the nth + * header field whose field name matches the component name is deleted. + * No header fields are deleted if none of the above conditions are met. + * + * The -list option outputs the field bodies from each header field whose + * field name matches the component name, one per line. If no -text + * option is specified, only the last path name component of each field + * body is output. The entire field body is output if the -text option + * is used; the contents of the -text argument are ignored. If the -list + * option is used in conjuction with the new -number option described + * below, each line is numbered starting with 1. A tab separates the + * number from the field body. + * + * The -number option works with both the -delete and -list options as + * described above. The -number option takes an optional argument. A + * value of 1 is assumed if this argument is absent. */ #include @@ -35,6 +71,16 @@ static struct swit switches[] = { { "version", 0 }, #define HELPSW 7 { "help", 0 }, +#define DRFTSW 8 + { "draft", 2 }, +#define LISTSW 9 + { "list", 1 }, +#define DELETESW 10 + { "delete", 2 }, +#define NUMBERSW 11 + { "number", 2 }, +#define APPENDSW 12 + { "append", 1 }, { NULL, 0 } }; @@ -53,6 +99,12 @@ main (int argc, char **argv) char *text = NULL, *folder = NULL, buf[BUFSIZ]; char **argp, **arguments, **msgs; struct msgs *mp; + int append = 0; /* append annotations instead of default prepend */ + int delete = -1; /* delete header element if set */ + char *draft = (char *)0; /* draft file name */ + int isdf = 0; /* return needed for m_draft() */ + int list = 0; /* list header elements if set */ + int number = 0; /* delete specific number of like elements if set */ #ifdef LOCALE setlocale(LC_ALL, ""); @@ -77,13 +129,13 @@ main (int argc, char **argv) while ((cp = *argp++)) { if (*cp == '-') { switch (smatch (++cp, switches)) { - case AMBIGSW: + case AMBIGSW: ambigsw (cp, switches); done (1); - case UNKWNSW: + case UNKWNSW: adios (NULL, "-%s unknown", cp); - case HELPSW: + case HELPSW: snprintf (buf, sizeof(buf), "%s [+folder] [msgs] [switches]", invo_name); print_help (buf, switches, 1); @@ -92,33 +144,62 @@ main (int argc, char **argv) print_version(invo_name); done (1); - case COMPSW: + case COMPSW: if (comp) adios (NULL, "only one component at a time!"); if (!(comp = *argp++) || *comp == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; - case DATESW: + case DATESW: datesw++; continue; - case NDATESW: + case NDATESW: datesw = 0; continue; - case INPLSW: + case INPLSW: inplace++; continue; - case NINPLSW: + case NINPLSW: inplace = 0; continue; - case TEXTSW: + case TEXTSW: if (text) adios (NULL, "only one body at a time!"); if (!(text = *argp++) || *text == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; + + case DELETESW: /* delete annotations */ + delete = 0; + continue; + + case DRFTSW: /* draft message specified */ + draft = ""; + continue; + + case LISTSW: /* produce a listing */ + list = 1; + continue; + + case NUMBERSW: /* number listing or delete by number */ + if (number != 0) + adios (NULL, "only one number at a time!"); + + if (argp - arguments == argc - 1 || **argp == '-') + number = 1; + + else if (!(number = atoi(*argp++))) + adios (NULL, "missing argument to %s", argp[-2]); + + delete = number; + continue; + + case APPENDSW: /* append annotations instead of default prepend */ + append = 1; + continue; } } if (*cp == '+' || *cp == '@') { @@ -141,6 +222,28 @@ main (int argc, char **argv) } } + /* + * We're dealing with the draft message instead of message numbers. + * Get the name of the draft and deal with it just as we do with + * message numbers below. + */ + + if (draft != (char *)0) { + if (nummsgs != 0) + adios(NULL, "can only have message numbers or -draft."); + + draft = getcpy(m_draft(folder, (char *)0, 1, &isdf)); + + make_comp(&comp); + + if (list) + annolist(draft, comp, text, number); + else + annotate (draft, comp, text, inplace, datesw, delete, append); + + return (done(0)); + } + #ifdef UCI if (strcmp(invo_name, "fanno") == 0) /* ugh! */ datesw = 0; @@ -173,9 +276,14 @@ main (int argc, char **argv) make_comp (&comp); /* annotate all the SELECTED messages */ - for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) - if (is_selected(mp, msgnum)) - annotate (m_name (msgnum), comp, text, inplace, datesw); + for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) { + if (is_selected(mp, msgnum)) { + if (list) + annolist(m_name(msgnum), comp, text, number); + else + annotate (m_name (msgnum), comp, text, inplace, datesw, delete, append); + } + } context_replace (pfolder, folder); /* update current folder */ seq_setcur (mp, mp->lowsel); /* update current message */ @@ -185,7 +293,6 @@ main (int argc, char **argv) return done (0); } - static void make_comp (char **ap) { @@ -214,4 +321,3 @@ make_comp (char **ap) if (!isalnum (*cp) && *cp != '-') adios (NULL, "invalid component name %s", *ap); } - diff --git a/uip/annosbr.c b/uip/annosbr.c index e250c1a..41b0cc2 100644 --- a/uip/annosbr.c +++ b/uip/annosbr.c @@ -19,41 +19,142 @@ extern int errno; /* * static prototypes */ -static int annosbr (int, char *, char *, char *, int, int); +static int annosbr (int, char *, char *, char *, int, int, int, int); int -annotate (char *file, char *comp, char *text, int inplace, int datesw) +annotate (char *file, char *comp, char *text, int inplace, int datesw, int delete, int append) { int i, fd; /* open and lock the file to be annotated */ if ((fd = lkopen (file, O_RDWR, 0)) == NOTOK) { switch (errno) { - case ENOENT: + case ENOENT: break; - default: + default: admonish (file, "unable to lock and open"); break; } return 1; } - i = annosbr (fd, file, comp, text, inplace, datesw); + i = annosbr (fd, file, comp, text, inplace, datesw, delete, append); lkclose (fd, file); return i; } +/* + * Produce a listing of all header fields (annotations) whose field name matches + * comp. Number the listing if number is set. Treate the field bodies as path + * names and just output the last component unless text is non-NULL. We don't + * care what text is set to. + */ + +void +annolist(char *file, char *comp, char *text, int number) +{ + int c; /* current character */ + int count; /* header field (annotation) counter */ + char *cp; /* miscellaneous character pointer */ + char *field; /* buffer for header field */ + int field_size; /* size of field buffer */ + FILE *fp; /* file pointer made from locked file descriptor */ + int length; /* length of field name */ + int n; /* number of bytes written */ + char *sp; /* another miscellaneous character pointer */ + + if ((fp = fopen(file, "r")) == (FILE *)0) + adios(file, "unable to open"); + + /* + * Allocate a buffer to hold the header components as they're read in. + * This buffer might need to be quite large, so we grow it as needed. + */ + + if ((field = (char *)malloc(field_size = 256)) == (char *)0) + adios(NULL, "can't allocate field buffer."); + + /* + * Get the length of the field name since we use it often. + */ + + length = strlen(comp); + + count = 0; + + do { + /* + * Get a line from the input file, growing the field buffer as needed. We do this + * so that we can fit an entire line in the buffer making it easy to do a string + * comparison on both the field name and the field body which might be a long path + * name. + */ + + for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) { + if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') { + (void)ungetc(c, fp); + c = '\n'; + break; + } + + if (++n >= field_size - 1) { + if ((field = (char *)realloc((void *)field, field_size += 256)) == (char *)0) + adios(NULL, "can't grow field buffer."); + + cp = field + n - 1; + } + } + + /* + * NUL-terminate the field.. + */ + + *cp = '\0'; + + if (strncasecmp(field, comp, length) == 0 && field[length] == ':') { + for (cp = field + length + 1; *cp == ' ' || *cp == '\t'; cp++) + ; + + if (number) + (void)printf("%d\t", ++count); + + if (text == (char *)0 && (sp = strrchr(cp, '/')) != (char *)0) + cp = sp + 1; + + (void)printf("%s\n", cp); + } + + } while (*field != '\0' && *field != '-'); + + /* + * Clean up. + */ + + free(field); + + (void)fclose(fp); + + return; +} + static int -annosbr (int fd, char *file, char *comp, char *text, int inplace, int datesw) +annosbr (int fd, char *file, char *comp, char *text, int inplace, int datesw, int delete, int append) { int mode, tmpfd; char *cp, *sp; char buffer[BUFSIZ], tmpfil[BUFSIZ]; struct stat st; - FILE *tmp; + FILE *tmp; + int c; /* current character */ + int count; /* header field (annotation) counter */ + char *field; /* buffer for header field */ + int field_size; /* size of field buffer */ + FILE *fp; /* file pointer made from locked file descriptor */ + int length; /* length of field name */ + int n; /* number of bytes written */ mode = fstat (fd, &st) != NOTOK ? (st.st_mode & 0777) : m_gmprot (); @@ -65,29 +166,217 @@ annosbr (int fd, char *file, char *comp, char *text, int inplace, int datesw) } chmod (tmpfil, mode); - if (datesw) - fprintf (tmp, "%s: %s\n", comp, dtimenow (0)); - if ((cp = text)) { + /* + * We're going to need to copy some of the message file to the temporary + * file while examining the contents. Convert the message file descriptor + * to a file pointer since it's a lot easier and more efficient to use + * stdio for this. Also allocate a buffer to hold the header components + * as they're read in. This buffer is grown as needed later. + */ + + if (delete >= 0 || append != 0) { + if ((fp = fdopen(fd, "r")) == (FILE *)0) + adios(NULL, "unable to fdopen file."); + + if ((field = (char *)malloc(field_size = 256)) == (char *)0) + adios(NULL, "can't allocate field buffer."); + } + + /* + * We're trying to delete a header field (annotation )if the delete flag is + * non-negative. A value greater than zero means that we're deleting the + * nth header field that matches the field (component) name. A value of + * zero means that we're deleting the first field in which both the field + * name matches the component name and the field body matches the text. + * The text is matched in its entirety if it begins with a slash; otherwise + * the text is matched against whatever portion of the field body follows + * the last slash. This allows matching of both absolute and relative path + * names. This is because this functionality was added to support attachments. + * It might be worth having a separate flag to indicate path name matching to + * make it more general. + */ + + if (delete >= 0) { + /* + * Get the length of the field name since we use it often. + */ + + length = strlen(comp); + + /* + * Initialize the field counter. This is only used if we're deleting by + * number. + */ + + count = 0; + + /* + * Copy lines from the input file to the temporary file until we either find the one + * that we're looking for (which we don't copy) or we reach the end of the headers. + * Both a blank line and a line beginning with a - terminate the headers so that we + * can handle both drafts and RFC-2822 format messages. + */ + do { - while (*cp == ' ' || *cp == '\t') - cp++; - sp = cp; - while (*cp && *cp++ != '\n') - continue; - if (cp - sp) - fprintf (tmp, "%s: %*.*s", comp, cp - sp, cp - sp, sp); - } while (*cp); - if (cp[-1] != '\n' && cp != text) - putc ('\n', tmp); + /* + * Get a line from the input file, growing the field buffer as needed. We do this + * so that we can fit an entire line in the buffer making it easy to do a string + * comparison on both the field name and the field body which might be a long path + * name. + */ + + for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) { + if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') { + (void)ungetc(c, fp); + c = '\n'; + break; + } + + if (++n >= field_size - 1) { + if ((field = (char *)realloc((void *)field, field_size *= 2)) == (char *)0) + adios(NULL, "can't grow field buffer."); + + cp = field + n - 1; + } + } + + /* + * NUL-terminate the field.. + */ + + *cp = '\0'; + + /* + * Check for a match on the field name. We delete the line by not copying it to the + * temporary file if + * + * o The delete flag is 0, meaning that we're going to delete the first matching + * field, and the text is NULL meaning that we don't care about the field body. + * + * o The delete flag is 0, meaning that we're going to delete the first matching + * field, and the text begins with a / meaning that we're looking for a full + * path name, and the text matches the field body. + * + * o The delete flag is 0, meaning that we're going to delete the first matching + * field, the text does not begin with a / meaning that we're looking for the + * last path name component, and the last path name component matches the text. + * + * o The delete flag is non-zero meaning that we're going to delete the nth field + * with a matching field name, and this is the nth matching field name. + */ + + if (strncasecmp(field, comp, length) == 0 && field[length] == ':') { + if (delete == 0) { + if (text == (char *)0) + break; + + for (cp = field + length + 1; *cp == ' ' || *cp == '\t'; cp++) + ; + + if (*text == '/') { + if (strcmp(cp, text) == 0) + break; + } + else { + if ((sp = strrchr(cp, '/')) != (char *)0) + cp = sp + 1; + + if (strcmp(cp, text) == 0) + break; + } + } + + else if (++count == delete) + break; + } + + /* + * This line wasn't a match so copy it to the temporary file. + */ + + if ((n = fputs(field, tmp)) == EOF || (c == '\n' && fputc('\n', tmp) == EOF)) + adios(NULL, "unable to write temporary file."); + + } while (*field != '\0' && *field != '-'); + + /* + * Get rid of the field buffer because we're done with it. + */ + + free((void *)field); + } + + else { + /* + * Find the end of the headers before adding the annotations if we're + * appending instead of the default prepending. + */ + + if (append) { + /* + * Copy lines from the input file to the temporary file until we + * reach the end of the headers. + */ + + while ((c = getc(fp)) != EOF) { + (void)putc(c, tmp); + + if (c == '\n') { + (void)ungetc(c = getc(fp), fp); + + if (c == '\n' || c == '-') + break; + } + } + } + + if (datesw) + fprintf (tmp, "%s: %s\n", comp, dtimenow (0)); + if ((cp = text)) { + do { + while (*cp == ' ' || *cp == '\t') + cp++; + sp = cp; + while (*cp && *cp++ != '\n') + continue; + if (cp - sp) + fprintf (tmp, "%s: %*.*s", comp, cp - sp, cp - sp, sp); + } while (*cp); + if (cp[-1] != '\n' && cp != text) + putc ('\n', tmp); + } } + fflush (tmp); + + /* + * We've been messing with the input file position. Move the input file + * descriptor to the current place in the file because the stock data + * copying routine uses the descriptor, not the pointer. + */ + + if (append || delete >= 0) { + if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) + adios(NULL, "can't seek."); + } + cpydata (fd, fileno (tmp), file, tmpfil); fclose (tmp); if (inplace) { if ((tmpfd = open (tmpfil, O_RDONLY)) == NOTOK) adios (tmpfil, "unable to open for re-reading"); + lseek (fd, (off_t) 0, SEEK_SET); + + /* + * We're making the file shorter if we're deleting a header field + * so the file has to be truncated or it will contain garbage. + */ + + if (delete >= 0 && ftruncate(fd, 0) == -1) + adios(tmpfil, "unable to truncate."); + cpydata (tmpfd, fd, tmpfil, file); close (tmpfd); unlink (tmpfil); @@ -111,5 +400,14 @@ annosbr (int fd, char *file, char *comp, char *text, int inplace, int datesw) } } + /* + * Close the delete file so that we don't run out of file pointers if + * we're doing piles of files. Note that this will make the close() in + * lkclose() fail, but that failure is ignored so it's not a problem. + */ + + if (delete >= 0) + (void)fclose(fp); + return 0; } diff --git a/uip/send.c b/uip/send.c index ec81aaa..1da9440 100644 --- a/uip/send.c +++ b/uip/send.c @@ -102,6 +102,8 @@ static struct swit switches[] = { { "saslmech", SASLminc(-5) }, #define USERSW 39 { "user", SASLminc(-4) }, +#define ATTACHSW 40 + { "attach", 6 }, { NULL, 0 } }; @@ -140,6 +142,7 @@ main (int argc, char **argv) char *msgs[MAXARGS], *vec[MAXARGS]; struct msgs *mp; struct stat st; + char *attach = (char *)0; /* header field name for attachments */ #ifdef UCI FILE *fp; #endif /* UCI */ @@ -273,6 +276,11 @@ main (int argc, char **argv) adios (NULL, "missing argument to %s", argp[-2]); vec[vecp++] = cp; continue; + + case ATTACHSW: + if (!(attach = *argp++) || *attach == '-') + adios (NULL, "missing argument to %s", argp[-2]); + continue; } } else { msgs[msgp++] = cp; @@ -436,7 +444,7 @@ go_to_it: closefds (3); for (msgnum = 0; msgnum < msgp; msgnum++) { - switch (sendsbr (vec, vecp, msgs[msgnum], &st, 1)) { + switch (sendsbr (vec, vecp, msgs[msgnum], &st, 1, attach)) { case DONE: done (++status); case NOTOK: diff --git a/uip/sendsbr.c b/uip/sendsbr.c index b055bb0..7576889 100644 --- a/uip/sendsbr.c +++ b/uip/sendsbr.c @@ -42,10 +42,18 @@ char *distfile = NULL; static int armed = 0; static jmp_buf env; +static char body_file_name[MAXPATHLEN + 1]; /* name of temporary file for body content */ +static char composition_file_name[MAXPATHLEN + 1]; /* name of mhbuild composition temporary file */ +static int field_size; /* size of header field buffer */ +static char *field; /* header field buffer */ +static FILE *draft_file; /* draft file pointer */ +static FILE *body_file; /* body file pointer */ +static FILE *composition_file; /* composition file pointer */ + /* * external prototypes */ -int sendsbr (char **, int, char *, struct stat *, int); +int sendsbr (char **, int, char *, struct stat *, int, char *); int done (int); char *getusername (void); @@ -59,17 +67,54 @@ static void annoaux (int); static int splitmsg (char **, int, char *, struct stat *, int); static int sendaux (char **, int, char *, struct stat *); +static int attach(char *, char *); +static void clean_up_temporary_files(void); +static int get_line(void); +static void make_mime_composition_file_entry(char *); + /* * Entry point into (back-end) routines to send message. */ int -sendsbr (char **vec, int vecp, char *drft, struct stat *st, int rename_drft) +sendsbr (char **vec, int vecp, char *drft, struct stat *st, int rename_drft, char *attachment_header_field_name) { int status; char buffer[BUFSIZ], file[BUFSIZ]; struct stat sts; + char *original_draft; /* name of original draft file */ + char *p; /* string pointer for building file name */ + + /* + * Save the original name of the draft file. The name of the draft file is changed + * to a temporary file containing the built MIME message if there are attachments. + * We need the original name so that it can be renamed after the message is sent. + */ + + original_draft = drft; + + /* + * There might be attachments if a header field name for attachments is supplied. + * Convert the draft to a MIME message. Use the mhbuild composition file for the + * draft if there was a successful conversion because that now contains the MIME + * message. A nice side effect of this is that it leaves the original draft file + * untouched so that it can be retrieved and modified if desired. + */ + + if (attachment_header_field_name != (char *)0) { + switch (attach(attachment_header_field_name, drft)) { + case OK: + drft = composition_file_name; + break; + + case NOTOK: + return (NOTOK); + + case DONE: + break; + } + } armed++; switch (setjmp (env)) { @@ -98,7 +143,7 @@ sendsbr (char **vec, int vecp, char *drft, struct stat *st, int rename_drft) /* rename the original draft */ if (rename_drft && status == OK && - rename (drft, strncpy (buffer, m_backup (drft), sizeof(buffer))) == NOTOK) + rename (original_draft, strncpy (buffer, m_backup (original_draft), sizeof(buffer))) == NOTOK) advise (buffer, "unable to rename %s to", drft); break; @@ -111,9 +156,340 @@ sendsbr (char **vec, int vecp, char *drft, struct stat *st, int rename_drft) if (distfile) unlink (distfile); + /* + * Get rid of any temporary files that we created for attachments. Also get rid of + * the renamed composition file that mhbuild leaves as a turd. It looks confusing, + * but we use the body file name to help build the renamed composition file name. + */ + + if (drft == composition_file_name) { + clean_up_temporary_files(); + + if (strlen(composition_file_name) >= sizeof (composition_file_name) - 6) + advise((char *)0, "unable to remove original composition file."); + + else { + if ((p = strrchr(composition_file_name, '/')) == (char *)0) + p = composition_file_name; + else + p++; + + (void)strcpy(body_file_name, p); + *p++ = ','; + (void)strcpy(p, body_file_name); + (void)strcat(p, ".orig"); + + (void)unlink(composition_file_name); + } + } + return status; } +static int +attach(char *attachment_header_field_name, char *draft_file_name) +{ + char buf[MAXPATHLEN + 6]; /* miscellaneous buffer */ + int c; /* current character for body copy */ + int has_attachment; /* draft has at least one attachment */ + int has_body; /* draft has a message body */ + int length; /* length of attachment header field name */ + char *p; /* miscellaneous string pointer */ + + /* + * Open up the draft file. + */ + + if ((draft_file = fopen(draft_file_name, "r")) == (FILE *)0) + adios((char *)0, "can't open draft file `%s'.", draft_file_name); + + /* + * Allocate a buffer to hold the header components as they're read in. + * This buffer might need to be quite large, so we grow it as needed. + */ + + if ((field = (char *)malloc(field_size = 256)) == (char *)0) + adios(NULL, "can't allocate field buffer."); + + /* + * 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. + */ + + 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; + + if (has_attachment == 0) + return (DONE); + + /* + * We have at least one attachment. Look for at least one non-blank line + * in the body of the message which indicates content in the body. + */ + + has_body = 0; + + while (get_line() != EOF) { + for (p = field; *p != '\0'; p++) { + if (*p != ' ' && *p != '\t') { + has_body = 1; + break; + } + } + + if (has_body) + break; + } + + /* + * Make names for the temporary files. + */ + + (void)strncpy(body_file_name, m_scratch("", m_maildir(invo_name)), sizeof (body_file_name)); + (void)strncpy(composition_file_name, m_scratch("", m_maildir(invo_name)), sizeof (composition_file_name)); + + if (has_body) + body_file = fopen(body_file_name, "w"); + + composition_file = fopen(composition_file_name, "w"); + + if ((has_body && body_file == (FILE *)0) || composition_file == (FILE *)0) { + clean_up_temporary_files(); + adios((char *)0, "unable to open all of the temporary files."); + } + + /* + * 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] != ':') + (void)fprintf(composition_file, "%s\n", field); + + (void)fputs("--------\n", composition_file); + + /* + * Copy the message body to a temporary file. + */ + + if (has_body) { + while ((c = getc(draft_file)) != EOF) + putc(c, body_file); + + (void)fclose(body_file); + } + + /* + * Add a mhbuild MIME composition file line for the body if there was one. + */ + + if (has_body) + make_mime_composition_file_entry(body_file_name); + + /* + * 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. + */ + + rewind(draft_file); + + 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++) + ; + + make_mime_composition_file_entry(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. + */ + + (void)sprintf(buf, "%s %s", buildmimeproc, composition_file_name); + + if (system(buf) != 0) { + clean_up_temporary_files(); + return (NOTOK); + } + + return (OK); +} + +static void +clean_up_temporary_files(void) +{ + (void)unlink(body_file_name); + (void)unlink(composition_file_name); + + return; +} + +static int +get_line(void) +{ + int c; /* current character */ + int n; /* number of bytes in buffer */ + char *p; /* buffer pointer */ + + /* + * Get a line from the input file, growing the field buffer as needed. We do this + * so that we can fit an entire line in the buffer making it easy to do a string + * comparison on both the field name and the field body which might be a long path + * name. + */ + + for (n = 0, p = field; (c = getc(draft_file)) != EOF; *p++ = c) { + if (c == '\n' && (c = getc(draft_file)) != ' ' && c != '\t') { + (void)ungetc(c, draft_file); + c = '\n'; + break; + } + + if (++n >= field_size - 1) { + if ((field = (char *)realloc((void *)field, field_size += 256)) == (char *)0) + adios(NULL, "can't grow field buffer."); + + p = field + n - 1; + } + } + + /* + * NUL-terminate the field.. + */ + + *p = '\0'; + + return (c); +} + +static void +make_mime_composition_file_entry(char *file_name) +{ + int binary; /* binary character found flag */ + int c; /* current character */ + char cmd[MAXPATHLEN + 6]; /* file command buffer */ + char *content_type; /* mime content type */ + FILE *fp; /* content and pipe file pointer */ + struct node *np; /* context scan node pointer */ + char *p; /* miscellaneous string pointer */ + struct stat st; /* file status buffer */ + + content_type = (char *)0; + + /* + * Check the file name for a suffix. Scan the context for that suffix on a + * mhshow-suffix- entry. We use these entries to be compatible with mhnshow, + * and there's no reason to make the user specify each suffix twice. Context + * entries of the form "mhshow-suffix-contenttype" in the name have the suffix + * in the field, including the dot. + */ + + if ((p = strrchr(file_name, '.')) != (char *)0) { + for (np = m_defs; np; np = np->n_next) { + if (strncasecmp(np->n_name, "mhshow-suffix-", 14) == 0 && strcasecmp(p, np->n_field) == 0) { + content_type = np->n_name + 14; + break; + } + } + } + + /* + * No content type was found, either because there was no matching entry in the + * context or because the file name has no suffix. Open the file and check for + * non-ASCII characters. Choose the content type based on this check. + */ + + if (content_type == (char *)0) { + if ((fp = fopen(file_name, "r")) == (FILE *)0) { + clean_up_temporary_files(); + adios((char *)0, "unable to access file \"%s\"", file_name); + } + + binary = 0; + + while ((c = getc(fp)) != EOF) { + if (c > 127 || c < 0) { + binary = 1; + break; + } + } + + (void)fclose(fp); + + content_type = binary ? "application/octet-stream" : "text/plain"; + } + + /* + * Make sure that the attachment file exists and is readable. Append a mhbuild + * directive to the draft file. This starts with the content type. Append a + * file name attribute and a private x-unix-mode attribute. Also append a + * description obtained (if possible) by running the "file" command on the file. + */ + + if (stat(file_name, &st) == -1 || access(file_name, R_OK) != 0) { + clean_up_temporary_files(); + adios((char *)0, "unable to access file \"%s\"", file_name); + } + + (void)fprintf(composition_file, "#%s; name=\"%s\"; x-unix-mode=0%.3ho", + content_type, ((p = strrchr(file_name, '/')) == (char *)0) ? file_name : p + 1, (unsigned short)(st.st_mode & 0777)); + + if (strlen(file_name) > MAXPATHLEN) { + clean_up_temporary_files(); + adios((char *)0, "attachment file name `%s' too long.", file_name); + } + + (void)sprintf(cmd, "file '%s'", file_name); + + if ((fp = popen(cmd, "r")) != (FILE *)0 && fgets(cmd, sizeof (cmd), fp) != (char *)0) { + *strchr(cmd, '\n') = '\0'; + + /* + * The output of the "file" command is of the form + * + * file: description + * + * Strip off the "file:" and subsequent white space. + */ + + for (p = cmd; *p != '\0'; p++) { + if (*p == ':') { + for (p++; *p != '\0'; p++) { + if (*p != '\t') + break; + } + break; + } + } + + if (*p != '\0') + (void)fprintf(composition_file, " [ %s ]", p); + + (void)pclose(fp); + } + + /* + * Finish up with the file name. + */ + + (void)fprintf(composition_file, " %s\n", file_name); + + return; +} /* * Split large message into several messages of @@ -644,7 +1020,7 @@ annoaux (int fd) if (is_selected(mp, msgnum)) { if (debugsw) advise (NULL, "annotate message %d", msgnum); - annotate (m_name (msgnum), annotext, cp, inplace, 1); + annotate (m_name (msgnum), annotext, cp, inplace, 1, 0, 0); } } diff --git a/uip/viamail.c b/uip/viamail.c index de0e950..b4cd5e2 100644 --- a/uip/viamail.c +++ b/uip/viamail.c @@ -237,7 +237,7 @@ via_mail (char *mailsw, char *subjsw, char *parmsw, char *descsw, if (verbsw) vec[vecp++] = "-verbose"; - switch (sendsbr (vec, vecp, tmpfil, &st, 0)) { + switch (sendsbr (vec, vecp, tmpfil, &st, 0, (char *)0)) { case DONE: case NOTOK: status++; diff --git a/uip/whatnowsbr.c b/uip/whatnowsbr.c index ab98074..53320d3 100644 --- a/uip/whatnowsbr.c +++ b/uip/whatnowsbr.c @@ -7,6 +7,37 @@ * 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. + * + * Several options have been added to ease the inclusion of attachments + * using the header field name mechanism added to anno and send. The + * -attach option is used to specify the header field name for attachments. + * + * Several commands have been added at the whatnow prompt: + * + * cd [ directory ] This option works just like the shell's + * cd command and lets the user change the + * directory from which attachments are + * taken so that long path names are not + * needed with every file. + * + * ls [ ls-options ] This option works just like the normal + * ls command and exists to allow the user + * to verify file names in the directory. + * + * pwd This option works just like the normal + * pwd command and exists to allow the user + * to verify the directory. + * + * attach files This option attaches the named files to + * the draft. + * + * alist [-ln] This option lists the attachments on the + * draft. -l gets long listings, -n gets + * numbered listings. + * + * detach files This option removes attachments from the + * detach -n numbers draft. This can be done by file name or + * by attachment number. */ #include @@ -31,6 +62,8 @@ static struct swit whatnowswitches[] = { { "version", 0 }, #define HELPSW 7 { "help", 0 }, +#define ATTACHSW 8 + { "attach header-field-name", 0 }, { NULL, 0 } }; @@ -58,6 +91,18 @@ static struct swit aleqs[] = { { "quit [-delete]", 0 }, #define DELETESW 9 { "delete", 0 }, +#define CDCMDSW 10 + { "cd [directory]", 0 }, +#define PWDCMDSW 11 + { "pwd", 0 }, +#define LSCMDSW 12 + { "ls", 0 }, +#define ATTACHCMDSW 13 + { "attach", 0 }, +#define DETACHCMDSW 14 + { "detach [-n]", 2 }, +#define ALISTCMDSW 15 + { "alist [-ln] ", 2 }, { NULL, 0 } }; @@ -89,6 +134,13 @@ WhatNow (int argc, char **argv) char buf[BUFSIZ], prompt[BUFSIZ]; char **argp, **arguments; struct stat st; + char *attach = (char *)0; /* attachment header field name */ + char cwd[MAXPATHLEN + 1]; /* current working directory */ + char file[MAXPATHLEN + 1]; /* file name buffer */ + char shell[MAXPATHLEN + 1]; /* shell response buffer */ + FILE *f; /* read pointer for bgnd proc */ + char *l; /* set on -l to alist command */ + int n; /* set on -n to alist command */ invo_name = r1bindex (argv[0], '/'); @@ -98,16 +150,24 @@ WhatNow (int argc, char **argv) arguments = getarguments (invo_name, argc, argv, 1); argp = arguments; + /* + * Get the initial current working directory. + */ + + if (getcwd(cwd, sizeof (cwd)) == (char *)0) { + adios("getcwd", "could not get working directory"); + } + while ((cp = *argp++)) { if (*cp == '-') { switch (smatch (++cp, whatnowswitches)) { - case AMBIGSW: + case AMBIGSW: ambigsw (cp, whatnowswitches); done (1); - case UNKWNSW: + case UNKWNSW: adios (NULL, "-%s unknown", cp); - case HELPSW: + case HELPSW: snprintf (buf, sizeof(buf), "%s [switches] [file]", invo_name); print_help (buf, whatnowswitches, 1); done (1); @@ -115,7 +175,7 @@ WhatNow (int argc, char **argv) print_version(invo_name); done (1); - case DFOLDSW: + case DFOLDSW: if (dfolder) adios (NULL, "only one draft folder at a time!"); if (!(cp = *argp++) || *cp == '-') @@ -123,23 +183,23 @@ WhatNow (int argc, char **argv) dfolder = path (*cp == '+' || *cp == '@' ? cp + 1 : cp, *cp != '@' ? TFOLDER : TSUBCWF); continue; - case DMSGSW: + case DMSGSW: if (dmsg) adios (NULL, "only one draft message at a time!"); if (!(dmsg = *argp++) || *dmsg == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; - case NDFLDSW: + case NDFLDSW: dfolder = NULL; isdf = NOTOK; continue; - case EDITRSW: + case EDITRSW: if (!(ed = *argp++) || *ed == '-') adios (NULL, "missing argument to %s", argp[-2]); nedit = 0; continue; - case NEDITSW: + case NEDITSW: nedit++; continue; @@ -147,6 +207,13 @@ WhatNow (int argc, char **argv) if (!(myprompt = *argp++) || *myprompt == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; + + case ATTACHSW: + if (attach != (char *)0) + adios(NULL, "only one attachment header field name at a time!"); + if (!(attach = *argp++) || *attach == '-') + adios (NULL, "missing argument to %s", argp[-2]); + continue; } } if (drft) @@ -200,7 +267,7 @@ WhatNow (int argc, char **argv) done (1); break; - case LISTSW: + case LISTSW: /* display the draft file */ showfile (++argp, drft); break; @@ -232,18 +299,258 @@ WhatNow (int argc, char **argv) done (1); break; - case SENDSW: + case SENDSW: /* Send draft */ sendfile (++argp, drft, 0); break; - case REFILEOPT: + case REFILEOPT: /* Refile the draft */ if (refile (++argp, drft) == 0) done (0); break; - default: + case CDCMDSW: + /* Change the working directory for attachments + * + * Run the directory through the user's shell so that + * we can take advantage of any syntax that the user + * is accustomed to. Read back the absolute path. + */ + + if (*++argp == (char *)0) { + (void)sprintf(buf, "$SHELL -c \"cd;pwd\""); + } + else if (strlen(*argp) >= BUFSIZ) { + adios((char *)0, "arguments too long"); + } + else { + (void)sprintf(buf, "$SHELL -c \"cd %s;cd %s;pwd\"", cwd, *argp); + } + if ((f = popen(buf, "r")) != (FILE *)0) { + fgets(cwd, sizeof (cwd), f); + + if (strchr(cwd, '\n') != (char *)0) + *strchr(cwd, '\n') = '\0'; + + pclose(f); + } + else { + advise("popen", "could not get directory"); + } + + break; + + case PWDCMDSW: + /* Print the working directory for attachments */ + printf("%s\n", cwd); + break; + + case LSCMDSW: + /* List files in the current attachment working directory + * + * Use the user's shell so that we can take advantage of any + * syntax that the user is accustomed to. + */ + + cp = buf + sprintf(buf, "$SHELL -c \" cd %s;ls", cwd); + + while (*++argp != (char *)0) { + if (cp + strlen(*argp) + 2 >= buf + BUFSIZ) + adios((char *)0, "arguments too long"); + + cp += sprintf(cp, " %s", *argp); + } + + (void)strcat(cp, "\""); + (void)system(buf); + break; + + case ALISTCMDSW: + /* + * List attachments on current draft. Options are: + * + * -l long listing (full path names) + * -n numbers listing + */ + + if (attach == (char *)0) { + advise((char *)0, "can't list because no header field name was given."); + break; + } + + l = (char *)0; + n = 0; + + while (*++argp != (char *)0) { + if (strcmp(*argp, "-l") == 0) + l = "/"; + + else if (strcmp(*argp, "-n") == 0) + n = 1; + + else if (strcmp(*argp, "-ln") == 0 || strcmp(*argp, "-nl") == 0) { + l = "/"; + n = 1; + } + + else { + n = -1; + break; + } + } + + if (n == -1) + advise((char *)0, "usage is alist [-ln]."); + + else + annolist(drft, attach, l, n); + + break; + + case ATTACHCMDSW: + /* + * Attach files to current draft. + */ + + if (attach == (char *)0) { + advise((char *)0, "can't attach because no header field name was given."); + 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. + */ + + cp = buf + sprintf(buf, "$SHELL -c \" cd %s;ls", cwd); + + while (*++argp != (char *)0) { + if (cp + strlen(*argp) + 3 >= buf + BUFSIZ) + adios((char *)0, "arguments too long"); + + cp += sprintf(cp, " %s", *argp); + } + + (void)strcat(cp, "\""); + + /* + * Read back the response from the shell, which contains a number of lines + * with one file name per line. Remove off the newline. Determine whether + * we have an absolute or relative path name. Prepend the current working + * directory to relative path names. Add the attachment annotation to the + * draft. + */ + + if ((f = popen(buf, "r")) != (FILE *)0) { + while (fgets(shell, sizeof (shell), f) != (char *)0) { + *(strchr(shell, '\n')) = '\0'; + + if (*shell == '/') + (void)annotate(drft, attach, shell, 1, 0, -1, 1); + else { + (void)sprintf(file, "%s/%s", cwd, shell); + (void)annotate(drft, attach, file, 1, 0, -1, 1); + } + } + + pclose(f); + } + else { + advise("popen", "could not get file from shell"); + } + + break; + + case DETACHCMDSW: + /* + * Detach files from current draft. + */ + + if (attach == (char *)0) { + advise((char *)0, "can't detach because no header field name was given."); + break; + } + + /* + * Scan the arguments for a -n. Mixed file names and numbers aren't allowed, + * so this catches a -n anywhere in the argument list. + */ + + for (n = 0, arguments = argp + 1; *arguments != (char *)0; arguments++) { + if (strcmp(*arguments, "-n") == 0) { + n = 1; + break; + } + } + + /* + * A -n was found so interpret the arguments as attachment numbers. + * Decrement any remaining argument number that is greater than the one + * just processed after processing each one so that the numbering stays + * correct. + */ + + if (n == 1) { + for (arguments = argp + 1; *arguments != (char *)0; arguments++) { + if (strcmp(*arguments, "-n") == 0) + continue; + + if (**arguments != '\0') { + n = atoi(*arguments); + (void)annotate(drft, attach, (char *)0, 1, 0, n, 1); + + for (argp = arguments + 1; *argp != (char *)0; argp++) { + if (atoi(*argp) > n) { + if (atoi(*argp) == 1) + *argp = ""; + else + (void)sprintf(*argp, "%d", atoi(*argp) - 1); + } + } + } + } + } + + /* + * The arguments are interpreted as file names. Run them through the + * user's shell for wildcard expansion and other goodies. Do this from + * the current working directory if the argument is not an absolute path + * name (does not begin with a /). + */ + + else { + for (arguments = argp + 1; *arguments != (char *)0; arguments++) { + if (**arguments == '/') { + if (strlen(*arguments) + sizeof ("$SHELL -c \"ls \"") >= sizeof (buf)) + adios((char *)0, "arguments too long"); + + (void)sprintf(buf, "$SHELL -c \"ls %s\"", *arguments); + } + else { + if (strlen(cwd) + strlen(*arguments) + sizeof ("$SHELL -c \" cd ; ls \"") >= sizeof (buf)) + adios((char *)0, "arguments too long"); + + (void)sprintf(buf, "$SHELL -c \" cd %s;ls %s\"", cwd, *arguments); + } + + if ((f = popen(buf, "r")) != (FILE *)0) { + while (fgets(shell, sizeof (cwd), f) != (char *)0) { + *(strchr(shell, '\n')) = '\0'; + (void)annotate(drft, attach, shell, 1, 0, 0, 1); + } + + pclose(f); + } + else { + advise("popen", "could not get file from shell"); + } + } + } + + break; + + default: /* Unknown command */ advise (NULL, "say what?"); break; @@ -330,12 +637,12 @@ editfile (char **ed, char **arg, char *file, int use, struct msgs *mp, fflush (stdout); switch (pid = vfork ()) { - case NOTOK: + case NOTOK: advise ("fork", "unable to"); status = NOTOK; break; - case OK: + case OK: if (cwd) chdir (cwd); if (altmsg) { @@ -357,7 +664,7 @@ editfile (char **ed, char **arg, char *file, int use, struct msgs *mp, perror (*ed); _exit (-1); - default: + default: if ((status = pidwait (pid, NOTOK))) { #ifdef ATTVIBUG if ((cp = r1bindex (*ed, '/')) @@ -492,9 +799,9 @@ sendfile (char **arg, char *file, int pushsw) for (i = 0; (child_id = vfork()) == NOTOK && i < 5; i++) sleep (5); switch (child_id) { - case NOTOK: + case NOTOK: advise (NULL, "unable to fork, so sending directly..."); - case OK: + case OK: vecp = 0; vec[vecp++] = invo_name; if (pushsw) @@ -510,7 +817,7 @@ sendfile (char **arg, char *file, int pushsw) perror (sendproc); _exit (-1); - default: + default: if (pidwait(child_id, OK) == 0) done (0); return 1; @@ -696,6 +1003,8 @@ static struct swit sendswitches[] = { { "saslmech", SASLminc(-5) }, #define USERSW 38 { "user", SASLminc(-4) }, +#define SNDATTACHSW 39 + { "attach file", 6 }, { NULL, 0 } }; @@ -720,6 +1029,7 @@ sendit (char *sp, char **arg, char *file, int pushed) char *cp, buf[BUFSIZ], **argp; char **arguments, *vec[MAXARGS]; struct stat st; + char *attach = (char *)0; /* attachment header field name */ #ifndef lint int distsw = 0; @@ -777,14 +1087,14 @@ sendit (char *sp, char **arg, char *file, int pushed) while ((cp = *argp++)) { if (*cp == '-') { switch (smatch (++cp, sendswitches)) { - case AMBIGSW: + case AMBIGSW: ambigsw (cp, sendswitches); return; - case UNKWNSW: + case UNKWNSW: advise (NULL, "-%s unknown\n", cp); return; - case SHELPSW: + case SHELPSW: snprintf (buf, sizeof(buf), "%s [switches]", sp); print_help (buf, sendswitches, 1); return; @@ -792,34 +1102,34 @@ sendit (char *sp, char **arg, char *file, int pushed) print_version (invo_name); return; - case SPSHSW: + case SPSHSW: pushed++; continue; - case NSPSHSW: + case NSPSHSW: pushed = 0; continue; - case SPLITSW: + case SPLITSW: if (!(cp = *argp++) || sscanf (cp, "%d", &splitsw) != 1) { advise (NULL, "missing argument to %s", argp[-2]); return; } continue; - case UNIQSW: + case UNIQSW: unique++; continue; - case NUNIQSW: + case NUNIQSW: unique = 0; continue; - case FORWSW: + case FORWSW: forwsw++; continue; - case NFORWSW: + case NFORWSW: forwsw = 0; continue; - case VERBSW: + case VERBSW: verbsw++; vec[vecp++] = --cp; continue; @@ -828,33 +1138,33 @@ sendit (char *sp, char **arg, char *file, int pushed) vec[vecp++] = --cp; continue; - case DEBUGSW: + case DEBUGSW: debugsw++; /* fall */ - case NFILTSW: - case FRMTSW: - case NFRMTSW: + case NFILTSW: + case FRMTSW: + case NFRMTSW: case BITSTUFFSW: case NBITSTUFFSW: - case MIMESW: - case NMIMESW: - case MSGDSW: - case NMSGDSW: - case WATCSW: - case NWATCSW: - case MAILSW: - case SAMLSW: - case SSNDSW: - case SOMLSW: - case SNOOPSW: + case MIMESW: + case NMIMESW: + case MSGDSW: + case NMSGDSW: + case WATCSW: + case NWATCSW: + case MAILSW: + case SAMLSW: + case SSNDSW: + case SOMLSW: + case SNOOPSW: case SASLSW: vec[vecp++] = --cp; continue; - case ALIASW: - case FILTSW: - case WIDTHSW: - case CLIESW: - case SERVSW: + case ALIASW: + case FILTSW: + case WIDTHSW: + case CLIESW: + case SERVSW: case SASLMECHSW: case USERSW: vec[vecp++] = --cp; @@ -865,13 +1175,20 @@ sendit (char *sp, char **arg, char *file, int pushed) vec[vecp++] = cp; continue; - case SDRFSW: - case SDRMSW: + case SDRFSW: + case SDRMSW: if (!(cp = *argp++) || *cp == '-') { advise (NULL, "missing argument to %s", argp[-2]); return; } - case SNDRFSW: + case SNDRFSW: + continue; + + case SNDATTACHSW: + if (!(attach = *argp++) || *attach == '-') { + advise (NULL, "missing argument to %s", argp[-2]); + return; + } continue; } } @@ -938,7 +1255,7 @@ sendit (char *sp, char **arg, char *file, int pushed) vec[0] = r1bindex (postproc, '/'); closefds (3); - if (sendsbr (vec, vecp, file, &st, 1) == OK) + if (sendsbr (vec, vecp, file, &st, 1, attach) == OK) done (0); } @@ -957,11 +1274,11 @@ whomfile (char **arg, char *file) fflush (stdout); switch (pid = vfork ()) { - case NOTOK: + case NOTOK: advise ("fork", "unable to"); return 1; - case OK: + case OK: vecp = 0; vec[vecp++] = r1bindex (whomproc, '/'); vec[vecp++] = file; @@ -975,7 +1292,7 @@ whomfile (char **arg, char *file) perror (whomproc); _exit (-1); /* NOTREACHED */ - default: + default: return (pidwait (pid, NOTOK) & 0377 ? 1 : 0); } }