rework m_strformat so that it takes the cols number to use in case of
[apps/madmutt.git] / muttlib.c
1 /*
2  * Copyright notice from original mutt:
3  * Copyright (C) 1996-2000 Michael R. Elkins <me@mutt.org>
4  * Copyright (C) 1999-2000 Thomas Roessler <roessler@does-not-exist.org>
5  *
6  * This file is part of mutt-ng, see http://www.muttng.org/.
7  * It's licensed under the GNU General Public License,
8  * please see the file GPL in the top level source directory.
9  */
10
11 #include <lib-lib/lib-lib.h>
12
13 #include <grp.h>
14 #include <pwd.h>
15
16 #include <lib-mime/mime.h>
17 #include <lib-ui/curses.h>
18 #include <lib-ui/enter.h>
19 #include <lib-sys/unix.h>
20 #include <lib-mx/mx.h>
21
22 #include "alias.h"
23 #include "mutt.h"
24 #include "attach.h"
25
26 #include "version.h"
27
28 #include <imap/imap.h>
29
30 #include <lib-crypt/crypt.h>
31
32 /* Modified by blong to accept a "suggestion" for file name.  If
33  * that file exists, then construct one with unique name but 
34  * keep any extension.  This might fail, I guess.
35  * Renamed to mutt_adv_mktemp so I only have to change where it's
36  * called, and not all possible cases.
37  */
38 void mutt_adv_mktemp (const char* dir, char *s, ssize_t l)
39 {
40     int fd;
41
42     fd = m_tempfd(s, l, m_strisempty(dir) ? NONULL(Tempdir) : dir, s);
43     if (fd < 0) {
44         *s = '\0';
45     } else {
46         close(fd);
47         unlink(s);
48     }
49 }
50
51 /* returns true if the header contained in "s" is in list "t" */
52 int mutt_matches_ignore (const char *s, string_list_t * t)
53 {
54   for (; t; t = t->next) {
55     if (!ascii_strncasecmp (s, t->data, m_strlen(t->data))
56         || *t->data == '*')
57       return 1;
58   }
59   return 0;
60 }
61
62 ssize_t _mutt_expand_path(char *s, ssize_t slen, int rx)
63 {
64     char p[_POSIX_PATH_MAX] = "";
65     char tmp[_POSIX_PATH_MAX];
66     const char *tail = "";
67
68     do {
69         const address_t *alias;
70
71         switch (*s) {
72           case '~':
73             if (s[1] == '/' || s[1] == '\0') {
74                 m_strcpy(p, sizeof(p), Homedir);
75                 tail = s + 1;
76             } else {
77                 struct passwd *pw;
78                 tail = m_strchrnul(s + 1, '/');
79
80                 m_strncpy(tmp, sizeof(tmp), s + 1, tail - s - 1);
81
82                 if ((pw = getpwnam(tmp))) {
83                     m_strcpy(p, sizeof(p), pw->pw_dir);
84                 } else {
85                     /* user not found! */
86                     tail = s;
87                 }
88             }
89             break;
90
91           case '=':
92           case '+':
93             /* if folder = imap[s]://host/: don't append slash */
94             if (imap_is_magic(NONULL(Maildir), NULL) == M_IMAP
95             &&  Maildir[m_strlen(Maildir) - 1] == '/') {
96                 m_strcpy(p, sizeof(p), Maildir);
97             } else {
98                 snprintf(p, sizeof(p), "%s/", NONULL(Maildir));
99             }
100
101             tail = s + 1;
102             break;
103
104             /* elm compatibility, @ expands alias to user name */
105
106           case '@':
107             if ((alias = alias_lookup(s + 1))) {
108                 HEADER h;
109                 header_init(&h);
110                 h.env = envelope_new();
111                 h.env->from = h.env->to = (address_t *)alias;
112                 mutt_default_save(p, sizeof (p), &h);
113                 h.env->from = h.env->to = NULL;
114                 header_wipe(&h);
115
116                 if (*p != '@') {
117                     /* recurse iff the result do not starts with '@' */
118                     m_strcpy(s, slen, p);
119                     continue;
120                 }
121             }
122             break;
123
124           case '>':
125             m_strcpy(p, sizeof(p), Inbox);
126             tail = s + 1;
127             break;
128
129           case '<':
130             m_strcpy(p, sizeof(p), Outbox);
131             tail = s + 1;
132             break;
133
134           case '!':
135             if (s[1] == '!') {
136                 m_strcpy(p, sizeof(p), LastFolder);
137                 tail = s + 2;
138             } else {
139                 m_strcpy(p, sizeof(p), Spoolfile);
140                 tail = s + 1;
141             }
142             break;
143
144           case '-':
145             m_strcpy(p, sizeof(p), NONULL(LastFolder));
146             tail = s + 1;
147             break;
148
149           case '^':
150             m_strcpy(p, sizeof(p), NONULL(CurrentFolder));
151             tail = s + 1;
152             break;
153
154           default:
155             *p = '\0';
156             tail = s;
157         }
158     } while (0);
159
160     if (rx) {
161         char q[_POSIX_PATH_MAX];
162         rx_sanitize_string(q, sizeof(q), p);
163         snprintf(tmp, sizeof(tmp), "%s%s", q, tail);
164     } else {
165         snprintf(tmp, sizeof(tmp), "%s%s", p, tail);
166     }
167
168     return m_strcpy(s, slen, tmp);
169 }
170
171 void mutt_mktemp(char *s)
172 {
173     int fd = m_tempfd(s, _POSIX_PATH_MAX, NONULL(Tempdir), NULL);
174     if (fd < 0) {
175         *s = '\0';
176     } else {
177         close(fd);
178         unlink(s);
179     }
180 }
181
182 /* collapse the pathname using ~ or = when possible */
183 void mutt_pretty_mailbox (char *s)
184 {
185   char *p = s, *q = s;
186   ssize_t len;
187   url_scheme_t scheme;
188
189   scheme = url_check_scheme (s);
190
191   if (scheme == U_IMAP || scheme == U_IMAPS) {
192     imap_pretty_mailbox (s);
193     return;
194   }
195
196   /* if s is an url, only collapse path component */
197   if (scheme != U_UNKNOWN) {
198     p = strchr (s, ':') + 1;
199     if (!m_strncmp (p, "//", 2))
200       q = strchr (p + 2, '/');
201     if (!q)
202       q = strchr (p, '\0');
203     p = q;
204   }
205
206   /* first attempt to collapse the pathname */
207   while (*p) {
208     if (*p == '/' && p[1] == '/') {
209       *q++ = '/';
210       p += 2;
211     }
212     else if (p[0] == '/' && p[1] == '.' && p[2] == '/') {
213       *q++ = '/';
214       p += 3;
215     }
216     else
217       *q++ = *p++;
218   }
219   *q = 0;
220
221   if (m_strncmp(s, Maildir, (len = m_strlen(Maildir))) == 0 &&
222       s[len] == '/') {
223     *s++ = '=';
224     memmove (s, s + len, m_strlen(s + len) + 1);
225   }
226   else if (m_strncmp(s, Homedir, (len = m_strlen(Homedir))) == 0 &&
227            s[len] == '/') {
228     *s++ = '~';
229     memmove (s, s + len - 1, m_strlen(s + len - 1) + 1);
230   }
231 }
232
233 /* return 0 on success, -1 on abort, 1 on error */
234 int mutt_check_overwrite (const char *attname, const char *path,
235                           char *fname, ssize_t flen, int *append,
236                           char **directory)
237 {
238   int rc = 0;
239   char tmp[_POSIX_PATH_MAX];
240   struct stat st;
241
242   m_strcpy(fname, flen, path);
243   if (access (fname, F_OK) != 0)
244     return 0;
245   if (stat (fname, &st) != 0)
246     return -1;
247   if (S_ISDIR (st.st_mode)) {
248     if (directory) {
249       switch (mutt_multi_choice
250               (_("File is a directory, save under it? [(y)es, (n)o, (a)ll]"),
251                _("yna"))) {
252       case 3:                  /* all */
253         m_strreplace(directory, fname);
254         break;
255       case 1:                  /* yes */
256         p_delete(directory);
257         break;
258       case -1:                 /* abort */
259         p_delete(directory);
260         return -1;
261       case 2:                  /* no */
262         p_delete(directory);
263         return 1;
264       }
265     }
266     else
267       if ((rc = mutt_yesorno(_("File is a directory, save under it?"),
268                              M_YES)) != M_YES)
269       return (rc == M_NO) ? 1 : -1;
270
271     if (!attname || !attname[0]) {
272       tmp[0] = 0;
273       if (mutt_get_field (_("File under directory: "), tmp, sizeof (tmp),
274                           M_FILE | M_CLEAR) != 0 || !tmp[0])
275         return (-1);
276       mutt_concat_path(fname, flen, path, tmp);
277     }
278     else
279       mutt_concat_path(fname, flen, path, mutt_basename(attname));
280   }
281
282   if (*append == 0 && access (fname, F_OK) == 0) {
283     switch (mutt_multi_choice
284             (_("File exists, (o)verwrite, (a)ppend, or (c)ancel?"), _("oac")))
285     {
286     case -1:                   /* abort */
287       return -1;
288     case 3:                    /* cancel */
289       return 1;
290
291     case 2:                    /* append */
292       *append = M_SAVE_APPEND;
293       break;
294     case 1:                    /* overwrite */
295       *append = M_SAVE_OVERWRITE;
296       break;
297     }
298   }
299   return 0;
300 }
301
302 void mutt_save_path(char *d, ssize_t dsize, address_t *a)
303 {
304     if (a && a->mailbox) {
305         m_strcpy(d, dsize, a->mailbox);
306
307         if (!option(OPTSAVEADDRESS)) {
308             char *p = strpbrk(d, "%@");
309             if (p)
310                 *p = '\0';
311         }
312         m_strtolower(d);
313     } else {
314         *d = '\0';
315     }
316 }
317
318 void mutt_safe_path(char *s, ssize_t l, address_t *a)
319 {
320     mutt_save_path(s, l, a);
321
322     while (*s) {
323         if (*s == '/' || ISSPACE(*s) || !isprint((unsigned char)*s))
324             *s = '_';
325         s++;
326     }
327 }
328
329 ssize_t m_strformat(char *dst, ssize_t dlen, int width, const char *fmt,
330                     format_t *callback, anytype cdata, format_flag flags)
331 {
332     ssize_t pos = flags & M_FORMAT_ARROWCURSOR ? 3 : 0;
333
334     m_strpad(dst, dlen, '\0', pos + 1);
335     if (!fmt)
336         return pos;
337
338     while (*fmt) {
339         int ch;
340
341         if (*fmt == '%') {
342             char ifstr[STRING], elstr[STRING], prefix[STRING];
343
344             *ifstr = *elstr = *prefix = '\0';
345
346             if (*++fmt == '%') {
347                 pos += m_strputc(dst + pos, dlen - pos, *fmt++);
348                 continue;
349             }
350
351             if (*fmt == '?') {
352                 flags |= M_FORMAT_OPTIONAL;
353                 fmt++;
354             } else {
355                 ssize_t pfxlen;
356                 flags &= ~M_FORMAT_OPTIONAL;
357
358                 /* eat the format string */
359                 pfxlen = strspn(fmt, "0123456789.-");
360                 m_strncpy(prefix, sizeof(prefix), fmt, pfxlen);
361                 fmt   += pfxlen;
362             }
363
364             /* save the character to switch on */
365             if (!(ch = *fmt++))
366                 break;
367
368             if (flags & M_FORMAT_OPTIONAL) {
369                 ssize_t iflen;
370                 const char *p;
371
372                 if (*fmt++ != '?')
373                     break;                /* bad format */
374
375                 /* eat the `if' part of the string */
376                 iflen = strcspn(fmt, "?&");
377                 m_strncpy(ifstr, ssizeof(ifstr), fmt, iflen);
378                 fmt  += iflen;
379
380                 /* eat the `else' part of the string (optional) */
381                 if (*fmt == '&')
382                     fmt++;                /* skip the & */
383
384                 p = m_strchrnul(fmt, '?');
385                 m_strncpy(elstr, ssizeof(elstr), fmt, p - fmt);
386                 fmt = p;
387
388                 if (!*fmt++)              /* move past the trailing `?' */
389                     break;                /* bad format */
390             }
391
392             switch (ch) {
393                 char lower, nodots, buf[LONG_STRING];
394
395               case '>':                 /* right justify to EOL */
396                 width -= mutt_strwidth(dst);
397
398                 ch = *fmt++;            /* pad char */
399
400                 if (width > 0) {
401                     m_strformat(buf, sizeof(buf), 0, fmt, callback, cdata, flags);
402                     width -= mutt_strwidth(buf);
403                     pos += m_strpad(dst + pos, dlen - pos, ch, width);
404                     pos += m_strcpy(dst + pos, dlen - pos, buf);
405                 }
406                 return pos;             /* skip rest of input */
407
408               case '|':                 /* pad to EOL */
409                 width -= mutt_strwidth(dst);
410                 return pos + m_strpad(dst + pos, dlen - pos, *fmt, width);
411
412               default:
413                 lower = nodots = 0;
414
415                 while (ch == '_' || ch == ':') {
416                     lower  |= ch == '_';
417                     nodots |= ch == ':';
418                     ch = *fmt++;
419                 }
420
421                 /* use callback function to handle this case */
422                 fmt = callback(buf, sizeof (buf), ch, fmt, prefix,
423                                ifstr, elstr, cdata, flags);
424
425                 if (lower)
426                     m_strtolower(buf);
427
428                 if (nodots) {
429                     char *p;
430
431                     for (p = buf; *p; p++) {
432                         if (*p == '.')
433                             *p = '_';
434                     }
435                 }
436
437                 pos += m_strcpy(dst + pos, dlen - pos, buf);
438                 break;
439             }
440             continue;
441         }
442
443         if (*fmt == '\\') {
444             if (!*++fmt)
445                 break;
446             switch ((ch = *fmt++)) {
447               case 'n': pos += m_strputc(dst + pos, dlen - pos, '\n'); break;
448               case 't': pos += m_strputc(dst + pos, dlen - pos, '\t'); break;
449               case 'r': pos += m_strputc(dst + pos, dlen - pos, '\r'); break;
450               case 'f': pos += m_strputc(dst + pos, dlen - pos, '\f'); break;
451               case 'v': pos += m_strputc(dst + pos, dlen - pos, '\v'); break;
452               default:  pos += m_strputc(dst + pos, dlen - pos, ch);   break;
453             }
454         } else {
455             ssize_t len = strcspn(fmt, "%\\");
456
457             pos += m_strncpy(dst + pos, dlen - pos, fmt, len);
458             fmt += len;
459         }
460     }
461
462     return pos;
463 }
464
465 /* returns 0 if OK to proceed, -1 to abort, 1 to retry */
466 int mutt_save_confirm (const char *s, struct stat *st)
467 {
468   char tmp[_POSIX_PATH_MAX];
469   int ret = 0;
470   int rc;
471   int magic = 0;
472
473   magic = mx_get_magic (s);
474
475   if (magic == M_POP) {
476     mutt_error _("Can't save message to POP mailbox.");
477
478     return 1;
479   }
480
481 #ifdef USE_NNTP
482   if (magic == M_NNTP) {
483     mutt_error _("Can't save message to newsserver.");
484
485     return 0;
486   }
487 #endif
488
489   if (magic > 0 && !mx_access (s, W_OK)) {
490     if (option (OPTCONFIRMAPPEND) &&
491         (!TrashPath || (m_strcmp(s, TrashPath) != 0))) {
492       /* if we're appending to the trash, there's no point in asking */
493       snprintf (tmp, sizeof (tmp), _("Append messages to %s?"), s);
494       if ((rc = mutt_yesorno (tmp, M_YES)) == M_NO)
495         ret = 1;
496       else if (rc == -1)
497         ret = -1;
498     }
499   }
500
501   if (stat (s, st) != -1) {
502     if (magic == -1) {
503       mutt_error (_("%s is not a mailbox!"), s);
504       return 1;
505     }
506   } else {
507     if (magic != M_IMAP)
508     {
509       st->st_mtime = 0;
510       st->st_atime = 0;
511
512       if (errno == ENOENT) {
513         if (option (OPTCONFIRMCREATE)) {
514           snprintf (tmp, sizeof (tmp), _("Create %s?"), s);
515           if ((rc = mutt_yesorno (tmp, M_YES)) == M_NO)
516             ret = 1;
517           else if (rc == -1)
518             ret = -1;
519         }
520       } else {
521         mutt_perror (s);
522         return 1;
523       }
524     }
525   }
526
527   CLEARLINE (LINES - 1);
528   return (ret);
529 }
530
531 void mutt_sleep (short s)
532 {
533     sleep(MAX(s, SleepTime));
534 }
535
536 const char *mutt_make_version (int full)
537 {
538   static char vstring[STRING];
539
540   if (full)
541     snprintf (vstring, sizeof (vstring),
542               "Madmutt/%s-r%s (based on Mutt 1.5.11)",
543               MUTT_VERSION, MUTT_REVISION);
544   else
545     snprintf (vstring, sizeof (vstring), "Madmutt/%s-%s",
546               MUTT_VERSION, MUTT_REVISION);
547   return vstring;
548 }
549
550 /* return 1 if address lists are strictly identical */
551 static int mutt_cmp_addr (const address_t * a, const address_t * b)
552 {
553   while (a && b) {
554     if (m_strcmp(a->mailbox, b->mailbox) ||
555         m_strcmp(a->personal, b->personal))
556       return (0);
557
558     a = a->next;
559     b = b->next;
560   }
561   if (a || b)
562     return (0);
563
564   return (1);
565 }
566
567 static int mutt_cmp_list (const string_list_t * a, const string_list_t * b)
568 {
569   while (a && b) {
570     if (m_strcmp(a->data, b->data))
571       return (0);
572
573     a = a->next;
574     b = b->next;
575   }
576   if (a || b)
577     return (0);
578
579   return (1);
580 }
581
582 static int mutt_cmp_env (const ENVELOPE * e1, const ENVELOPE * e2)
583 {
584   if (e1 && e2) {
585     if (m_strcmp(e1->message_id, e2->message_id) ||
586         m_strcmp(e1->subject, e2->subject) ||
587         !mutt_cmp_list (e1->references, e2->references) ||
588         !mutt_cmp_addr (e1->from, e2->from) ||
589         !mutt_cmp_addr (e1->sender, e2->sender) ||
590         !mutt_cmp_addr (e1->reply_to, e2->reply_to) ||
591         !mutt_cmp_addr (e1->to, e2->to) ||
592         !mutt_cmp_addr (e1->cc, e2->cc) ||
593         !mutt_cmp_addr (e1->return_path, e2->return_path))
594       return (0);
595     else
596       return (1);
597   }
598   else {
599     if (e1 == NULL && e2 == NULL)
600       return (1);
601     else
602       return (0);
603   }
604 }
605
606 static int mutt_cmp_body (const BODY * b1, const BODY * b2)
607 {
608   if (b1->type != b2->type ||
609       b1->encoding != b2->encoding ||
610       m_strcmp(b1->subtype, b2->subtype) ||
611       m_strcmp(b1->description, b2->description) ||
612       !parameter_equal(b1->parameter, b2->parameter) ||
613       b1->length != b2->length)
614     return (0);
615   return (1);
616 }
617 int mutt_cmp_header (const HEADER * h1, const HEADER * h2) {
618   if (h1 && h2) {
619     if (h1->received != h2->received ||
620         h1->date_sent != h2->date_sent ||
621         h1->content->length != h2->content->length ||
622         h1->lines != h2->lines ||
623         h1->zhours != h2->zhours ||
624         h1->zminutes != h2->zminutes ||
625         h1->zoccident != h2->zoccident ||
626         h1->mime != h2->mime ||
627         !mutt_cmp_env (h1->env, h2->env) ||
628         !mutt_cmp_body (h1->content, h2->content))
629       return (0);
630     else
631       return (1);
632   }
633   else {
634     if (h1 == NULL && h2 == NULL)
635       return (1);
636     else
637       return (0);
638   }
639 }
640
641
642 int mutt_extract_token(BUFFER *dest, BUFFER *tok, int flags)
643 {
644     char ch;
645     char qc = 0;                  /* quote char */
646     char *pc;
647
648     /* reset the destination pointer to the beginning of the buffer */
649     dest->dptr = dest->data;
650
651     tok->dptr = vskipspaces(tok->dptr);
652     while ((ch = *tok->dptr)) {
653         if (!qc) {
654             if ((ISSPACE(ch) && !(flags & M_TOKEN_SPACE))
655             || (ch == '#' && !(flags & M_TOKEN_COMMENT))
656             || (ch == '=' && (flags & M_TOKEN_EQUAL))
657             || (ch == ';' && !(flags & M_TOKEN_SEMICOLON))
658             || ((flags & M_TOKEN_PATTERN) && strchr("~=!|", ch)))
659             {
660                 break;
661             }
662         }
663
664         tok->dptr++;
665
666         if (ch == qc) {
667             qc = 0;                     /* end of quote */
668         } else
669         if (!qc && (ch == '\'' || ch == '"') && !(flags & M_TOKEN_QUOTE)) {
670             qc = ch;
671         } else
672         if (ch == '\\' && qc != '\'') {
673             if (!*tok->dptr)
674                 return -1;              /* premature end of token */
675
676             switch (ch = *tok->dptr++) {
677               case 'c':
678               case 'C':
679                 if (!*tok->dptr)
680                     return -1;          /* premature end of token */
681                 mutt_buffer_addch(dest,
682                                   (toupper((unsigned char)*tok->dptr) - 'A' + 1) & 0x7f);
683                 tok->dptr++;
684                 break;
685               case 'r':
686                 mutt_buffer_addch(dest, '\r');
687                 break;
688               case 'n':
689                 mutt_buffer_addch(dest, '\n');
690                 break;
691               case 't':
692                 mutt_buffer_addch(dest, '\t');
693                 break;
694               case 'f':
695                 mutt_buffer_addch(dest, '\f');
696                 break;
697               case 'e':
698                 mutt_buffer_addch(dest, '\033');
699                 break;
700               default:
701                 if (isdigit((unsigned char)ch)
702                 &&  isdigit((unsigned char)*tok->dptr)
703                 &&  isdigit((unsigned char)*(tok->dptr + 1)))
704                 {
705                     mutt_buffer_addch(dest, (ch << 6) + (*tok->dptr << 3) +
706                                             *(tok->dptr + 1) - 3504);
707                     tok->dptr += 2;
708                 } else {
709                     mutt_buffer_addch(dest, ch);
710                 }
711             }
712         } else
713         if (ch == '^' && (flags & M_TOKEN_CONDENSE)) {
714             if (!*tok->dptr)
715                 return -1;              /* premature end of token */
716             ch = *tok->dptr++;
717             if (ch == '^') {
718                 mutt_buffer_addch(dest, ch);
719             } else
720             if (ch == '[') {
721                 mutt_buffer_addch(dest, '\033');
722             } else
723             if (isalpha((unsigned char)ch)) {
724                 mutt_buffer_addch(dest, toupper((unsigned char)ch) - 'A' + 1);
725             } else {
726                 mutt_buffer_addch(dest, '^');
727                 mutt_buffer_addch(dest, ch);
728             }
729         } else
730         if (ch == '`' && (!qc || qc == '"')) {
731             FILE *fp;
732             pid_t pid;
733             char *cmd, *ptr;
734             ssize_t expnlen;
735             BUFFER expn;
736             int line = 0;
737
738             pc = tok->dptr;
739             do {
740                 if ((pc = strpbrk(pc, "\\`"))) {
741                     /* skip any quoted chars */
742                     if (*pc == '\\')
743                         pc += 2;
744                 }
745             } while (pc && *pc != '`');
746             if (!pc) {
747                 return (-1);
748             }
749
750             cmd = p_dupstr(tok->dptr, pc - tok->dptr);
751             if ((pid = mutt_create_filter(cmd, NULL, &fp, NULL)) < 0) {
752                 p_delete(&cmd);
753                 return -1;
754             }
755             p_delete(&cmd);
756
757             tok->dptr = pc + 1;
758
759             /* read line */
760             p_clear(&expn, 1);
761             expn.data = mutt_read_line(NULL, &expn.dsize, fp, &line);
762             m_fclose(&fp);
763             mutt_wait_filter(pid);
764
765             /* if we got output, make a new string consiting of the shell ouptput
766                plus whatever else was left on the original line */
767             /* BUT: If this is inside a quoted string, directly add output to 
768              * the token */
769             if (expn.data && qc) {
770                 mutt_buffer_addstr(dest, expn.data);
771                 p_delete(&expn.data);
772             } else
773             if (expn.data) {
774                 expnlen = m_strlen(expn.data);
775                 tok->dsize = expnlen + m_strlen(tok->dptr) + 1;
776                 ptr = xmalloc(tok->dsize);
777                 memcpy(ptr, expn.data, expnlen);
778                 m_strcpy(ptr + expnlen, tok->dsize - expnlen, tok->dptr);
779                 if (tok->destroy)
780                     p_delete(&tok->data);
781                 tok->data = ptr;
782                 tok->dptr = ptr;
783                 tok->destroy = 1;       /* mark that the caller should destroy this data */
784                 ptr = NULL;
785                 p_delete(&expn.data);
786             }
787         } else
788         if (ch == '$' && (!qc || qc == '"')
789         && (*tok->dptr == '{' || isalpha((unsigned char)*tok->dptr)))
790         {
791             char *env = NULL, *var = NULL;
792
793             if (*tok->dptr == '{') {
794                 tok->dptr++;
795                 if ((pc = strchr (tok->dptr, '}'))) {
796                     var = p_dupstr(tok->dptr, pc - tok->dptr);
797                     tok->dptr = pc + 1;
798                 }
799             } else {
800                 for (pc = tok->dptr; isalnum((unsigned char)*pc) || *pc == '_';
801                      pc++);
802                 var = p_dupstr(tok->dptr, pc - tok->dptr);
803                 tok->dptr = pc;
804             }
805             if (var) {
806                 char tmp[STRING];
807                 if ((env = getenv (var))
808                 || (mutt_option_value (var, tmp, sizeof (tmp)) && (env = tmp)))
809                 {
810                     mutt_buffer_addstr (dest, env);
811                 }
812             }
813             p_delete(&var);
814         } else {
815             mutt_buffer_addch(dest, ch);
816         }
817     }
818     mutt_buffer_addch(dest, 0);  /* terminate the string */
819     tok->dptr = vskipspaces(tok->dptr);
820     return 0;
821 }