Add IDNA support..
[s-mailx.git] / head.c
bloba52767b0ebca17b4b9ee137082c4fb888fe058b4
1 /*
2 * S-nail - a mail user agent derived from Berkeley Mail.
4 * Copyright (c) 2000-2004 Gunnar Ritter, Freiburg i. Br., Germany.
5 * Copyright (c) 2012 Steffen "Daode" Nurpmeso.
6 */
7 /*
8 * Copyright (c) 1980, 1993
9 * The Regents of the University of California. All rights reserved.
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
13 * are met:
14 * 1. Redistributions of source code must retain the above copyright
15 * notice, this list of conditions and the following disclaimer.
16 * 2. Redistributions in binary form must reproduce the above copyright
17 * notice, this list of conditions and the following disclaimer in the
18 * documentation and/or other materials provided with the distribution.
19 * 3. All advertising materials mentioning features or use of this software
20 * must display the following acknowledgement:
21 * This product includes software developed by the University of
22 * California, Berkeley and its contributors.
23 * 4. Neither the name of the University nor the names of its contributors
24 * may be used to endorse or promote products derived from this software
25 * without specific prior written permission.
27 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
28 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
31 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
32 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
33 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
34 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
35 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
36 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
37 * SUCH DAMAGE.
40 #include "rcv.h"
41 #include "extern.h"
43 #include <time.h>
45 #ifdef USE_IDNA
46 # include <idna.h>
47 # include <stringprep.h>
48 # include <tld.h>
49 #endif
52 * Mail -- a mail program
54 * Routines for processing and detecting headlines.
57 static char * copyin(char *src, char **space);
58 static char * nextword(char *wp, char *wbuf);
59 static int gethfield(FILE *f, char **linebuf, size_t *linesize, int rem,
60 char **colon);
61 #ifdef USE_IDNA
62 static struct addrguts * idna_apply(struct addrguts *agp);
63 #endif
64 static int addrspec_check(int doskin, struct addrguts *agp);
65 static int msgidnextc(const char **cp, int *status);
66 static int charcount(char *str, int c);
69 * See if the passed line buffer is a mail header.
70 * Return true if yes. POSIX.2 leaves the content
71 * following 'From ' unspecified, so don't care about
72 * it.
74 /*ARGSUSED 2*/
75 int
76 is_head(char *linebuf, size_t linelen)
78 char *cp;
79 (void)linelen;
81 cp = linebuf;
82 if (*cp++ != 'F' || *cp++ != 'r' || *cp++ != 'o' || *cp++ != 'm' ||
83 *cp++ != ' ')
84 return (0);
85 return(1);
89 * Split a headline into its useful components.
90 * Copy the line into dynamic string space, then set
91 * pointers into the copied line in the passed headline
92 * structure. Actually, it scans.
94 void
95 parse(char *line, size_t linelen, struct headline *hl, char *pbuf)
97 char *cp;
98 char *sp;
99 char *word;
101 hl->l_from = NULL;
102 hl->l_tty = NULL;
103 hl->l_date = NULL;
104 cp = line;
105 sp = pbuf;
106 word = ac_alloc(linelen + 1);
108 * Skip over "From" first.
110 cp = nextword(cp, word);
111 cp = nextword(cp, word);
112 if (*word)
113 hl->l_from = copyin(word, &sp);
114 if (cp != NULL && cp[0] == 't' && cp[1] == 't' && cp[2] == 'y') {
115 cp = nextword(cp, word);
116 hl->l_tty = copyin(word, &sp);
118 if (cp != NULL)
119 hl->l_date = copyin(cp, &sp);
120 else
121 hl->l_date = catgets(catd, CATSET, 213, "<Unknown date>");
122 ac_free(word);
126 * Copy the string on the left into the string on the right
127 * and bump the right (reference) string pointer by the length.
128 * Thus, dynamically allocate space in the right string, copying
129 * the left string into it.
131 static char *
132 copyin(char *src, char **space)
134 char *cp;
135 char *top;
137 top = cp = *space;
138 while ((*cp++ = *src++) != '\0')
140 *space = cp;
141 return (top);
144 #ifdef notdef
145 static int cmatch(char *, char *);
147 * Test to see if the passed string is a ctime(3) generated
148 * date string as documented in the manual. The template
149 * below is used as the criterion of correctness.
150 * Also, we check for a possible trailing time zone using
151 * the tmztype template.
155 * 'A' An upper case char
156 * 'a' A lower case char
157 * ' ' A space
158 * '0' A digit
159 * 'O' An optional digit or space
160 * ':' A colon
161 * '+' A sign
162 * 'N' A new line
164 static char *tmztype[] = {
165 "Aaa Aaa O0 00:00:00 0000",
166 "Aaa Aaa O0 00:00 0000",
167 "Aaa Aaa O0 00:00:00 AAA 0000",
168 "Aaa Aaa O0 00:00 AAA 0000",
170 * Sommer time, e.g. MET DST
172 "Aaa Aaa O0 00:00:00 AAA AAA 0000",
173 "Aaa Aaa O0 00:00 AAA AAA 0000",
175 * time zone offset, e.g.
176 * +0200 or +0200 MET or +0200 MET DST
178 "Aaa Aaa O0 00:00:00 +0000 0000",
179 "Aaa Aaa O0 00:00 +0000 0000",
180 "Aaa Aaa O0 00:00:00 +0000 AAA 0000",
181 "Aaa Aaa O0 00:00 +0000 AAA 0000",
182 "Aaa Aaa O0 00:00:00 +0000 AAA AAA 0000",
183 "Aaa Aaa O0 00:00 +0000 AAA AAA 0000",
185 * time zone offset without time zone specification (pine)
187 "Aaa Aaa O0 00:00:00 0000 +0000",
188 NULL,
191 static int
192 is_date(char *date)
194 int ret = 0, form = 0;
196 while (tmztype[form]) {
197 if ( (ret = cmatch(date, tmztype[form])) == 1 )
198 break;
199 form++;
202 return ret;
206 * Match the given string (cp) against the given template (tp).
207 * Return 1 if they match, 0 if they don't
209 static int
210 cmatch(char *cp, char *tp)
212 int c;
214 while (*cp && *tp)
215 switch (*tp++) {
216 case 'a':
217 if (c = *cp++, !lowerchar(c))
218 return 0;
219 break;
220 case 'A':
221 if (c = *cp++, !upperchar(c))
222 return 0;
223 break;
224 case ' ':
225 if (*cp++ != ' ')
226 return 0;
227 break;
228 case '0':
229 if (c = *cp++, !digitchar(c))
230 return 0;
231 break;
232 case 'O':
233 if (c = *cp, c != ' ' && !digitchar(c))
234 return 0;
235 cp++;
236 break;
237 case ':':
238 if (*cp++ != ':')
239 return 0;
240 break;
241 case '+':
242 if (*cp != '+' && *cp != '-')
243 return 0;
244 cp++;
245 break;
246 case 'N':
247 if (*cp++ != '\n')
248 return 0;
249 break;
251 if (*cp || *tp)
252 return 0;
253 return (1);
255 #endif /* notdef */
258 * Collect a liberal (space, tab delimited) word into the word buffer
259 * passed. Also, return a pointer to the next word following that,
260 * or NULL if none follow.
262 static char *
263 nextword(char *wp, char *wbuf)
265 int c;
267 if (wp == NULL) {
268 *wbuf = 0;
269 return (NULL);
271 while ((c = *wp++) != '\0' && !blankchar(c)) {
272 *wbuf++ = c;
273 if (c == '"') {
274 while ((c = *wp++) != '\0' && c != '"')
275 *wbuf++ = c;
276 if (c == '"')
277 *wbuf++ = c;
278 else
279 wp--;
282 *wbuf = '\0';
283 for (; blankchar(c); c = *wp++)
285 if (c == 0)
286 return (NULL);
287 return (wp - 1);
290 void
291 extract_header(FILE *fp, struct header *hp) /* XXX no header occur-cnt check */
293 char *linebuf = NULL;
294 size_t linesize = 0;
295 int seenfields = 0;
296 char *colon, *cp, *value;
297 struct header nh;
298 struct header *hq = &nh;
299 int lc, c;
301 memset(hq, 0, sizeof *hq);
302 for (lc = 0; readline(fp, &linebuf, &linesize) > 0; lc++);
303 rewind(fp);
304 while ((lc = gethfield(fp, &linebuf, &linesize, lc, &colon)) >= 0) {
305 if ((value = thisfield(linebuf, "to")) != NULL) {
306 seenfields++;
307 hq->h_to = cat(hq->h_to, checkaddrs(
308 lextract(value, GTO|GFULL)));
309 } else if ((value = thisfield(linebuf, "cc")) != NULL) {
310 seenfields++;
311 hq->h_cc = cat(hq->h_cc, checkaddrs(
312 lextract(value, GCC|GFULL)));
313 } else if ((value = thisfield(linebuf, "bcc")) != NULL) {
314 seenfields++;
315 hq->h_bcc = cat(hq->h_bcc, checkaddrs(
316 lextract(value, GBCC|GFULL)));
317 } else if ((value = thisfield(linebuf, "from")) != NULL) {
318 seenfields++;
319 hq->h_from = cat(hq->h_from, checkaddrs(
320 sextract(value, GEXTRA|GFULL)));
321 } else if ((value = thisfield(linebuf, "reply-to")) != NULL) {
322 seenfields++;
323 hq->h_replyto = cat(hq->h_replyto, checkaddrs(
324 sextract(value, GEXTRA|GFULL)));
325 } else if ((value = thisfield(linebuf, "sender")) != NULL) {
326 seenfields++;
327 hq->h_sender = cat(hq->h_sender, checkaddrs(
328 sextract(value, GEXTRA|GFULL)));
329 } else if ((value = thisfield(linebuf,
330 "organization")) != NULL) {
331 seenfields++;
332 for (cp = value; blankchar(*cp & 0377); cp++);
333 hq->h_organization = hq->h_organization ?
334 save2str(hq->h_organization, cp) :
335 savestr(cp);
336 } else if ((value = thisfield(linebuf, "subject")) != NULL ||
337 (value = thisfield(linebuf, "subj")) != NULL) {
338 seenfields++;
339 for (cp = value; blankchar(*cp & 0377); cp++);
340 hq->h_subject = hq->h_subject ?
341 save2str(hq->h_subject, cp) :
342 savestr(cp);
343 } else
344 fprintf(stderr, catgets(catd, CATSET, 266,
345 "Ignoring header field \"%s\"\n"),
346 linebuf);
349 * In case the blank line after the header has been edited out.
350 * Otherwise, fetch the header separator.
352 if (linebuf) {
353 if (linebuf[0] != '\0') {
354 for (cp = linebuf; *(++cp) != '\0'; );
355 fseek(fp, (long)-(1 + cp - linebuf), SEEK_CUR);
356 } else {
357 if ((c = getc(fp)) != '\n' && c != EOF)
358 ungetc(c, fp);
361 if (seenfields) {
362 hp->h_to = hq->h_to;
363 hp->h_cc = hq->h_cc;
364 hp->h_bcc = hq->h_bcc;
365 hp->h_from = hq->h_from;
366 hp->h_replyto = hq->h_replyto;
367 hp->h_sender = hq->h_sender;
368 hp->h_organization = hq->h_organization;
369 hp->h_subject = hq->h_subject;
370 } else
371 fprintf(stderr, catgets(catd, CATSET, 267,
372 "Restoring deleted header lines\n"));
373 if (linebuf)
374 free(linebuf);
378 * Return the desired header line from the passed message
379 * pointer (or NULL if the desired header field is not available).
380 * If mult is zero, return the content of the first matching header
381 * field only, the content of all matching header fields else.
383 char *
384 hfield_mult(char *field, struct message *mp, int mult)
386 FILE *ibuf;
387 char *linebuf = NULL;
388 size_t linesize = 0;
389 int lc;
390 char *hfield;
391 char *colon, *oldhfield = NULL;
393 if ((ibuf = setinput(&mb, mp, NEED_HEADER)) == NULL)
394 return NULL;
395 if ((lc = mp->m_lines - 1) < 0)
396 return NULL;
397 if ((mp->m_flag & MNOFROM) == 0) {
398 if (readline(ibuf, &linebuf, &linesize) < 0) {
399 if (linebuf)
400 free(linebuf);
401 return NULL;
404 while (lc > 0) {
405 if ((lc = gethfield(ibuf, &linebuf, &linesize, lc, &colon))
406 < 0) {
407 if (linebuf)
408 free(linebuf);
409 return oldhfield;
411 if ((hfield = thisfield(linebuf, field)) != NULL) {
412 oldhfield = save2str(hfield, oldhfield);
413 if (mult == 0)
414 break;
417 if (linebuf)
418 free(linebuf);
419 return oldhfield;
423 * Return the next header field found in the given message.
424 * Return >= 0 if something found, < 0 elsewise.
425 * "colon" is set to point to the colon in the header.
426 * Must deal with \ continuations & other such fraud.
428 static int
429 gethfield(FILE *f, char **linebuf, size_t *linesize, int rem, char **colon)
431 char *line2 = NULL;
432 size_t line2size = 0;
433 char *cp, *cp2;
434 int c, isenc;
436 if (*linebuf == NULL)
437 *linebuf = srealloc(*linebuf, *linesize = 1);
438 **linebuf = '\0';
439 for (;;) {
440 if (--rem < 0)
441 return -1;
442 if ((c = readline(f, linebuf, linesize)) <= 0)
443 return -1;
444 for (cp = *linebuf; fieldnamechar(*cp & 0377); cp++);
445 if (cp > *linebuf)
446 while (blankchar(*cp & 0377))
447 cp++;
448 if (*cp != ':' || cp == *linebuf)
449 continue;
451 * I guess we got a headline.
452 * Handle wraparounding
454 *colon = cp;
455 cp = *linebuf + c;
456 for (;;) {
457 isenc = 0;
458 while (--cp >= *linebuf && blankchar(*cp & 0377));
459 cp++;
460 if (rem <= 0)
461 break;
462 if (cp-8 >= *linebuf && cp[-1] == '=' && cp[-2] == '?')
463 isenc |= 1;
464 ungetc(c = getc(f), f);
465 if (!blankchar(c))
466 break;
467 if ((c = readline(f, &line2, &line2size)) < 0)
468 break;
469 rem--;
470 for (cp2 = line2; blankchar(*cp2 & 0377); cp2++);
471 c -= cp2 - line2;
472 if (cp2[0] == '=' && cp2[1] == '?' && c > 8)
473 isenc |= 2;
474 if (cp + c >= *linebuf + *linesize - 2) {
475 size_t diff = cp - *linebuf;
476 size_t colondiff = *colon - *linebuf;
477 *linebuf = srealloc(*linebuf,
478 *linesize += c + 2);
479 cp = &(*linebuf)[diff];
480 *colon = &(*linebuf)[colondiff];
482 if (isenc != 3)
483 *cp++ = ' ';
484 memcpy(cp, cp2, c);
485 cp += c;
487 *cp = 0;
488 if (line2)
489 free(line2);
490 return rem;
492 /* NOTREACHED */
496 * Check whether the passed line is a header line of
497 * the desired breed. Return the field body, or 0.
499 char *
500 thisfield(const char *linebuf, const char *field)
502 while (lowerconv(*linebuf&0377) == lowerconv(*field&0377)) {
503 linebuf++;
504 field++;
506 if (*field != '\0')
507 return NULL;
508 while (blankchar(*linebuf&0377))
509 linebuf++;
510 if (*linebuf++ != ':')
511 return NULL;
512 while (blankchar(*linebuf&0377))
513 linebuf++;
514 return (char *)linebuf;
518 * Get sender's name from this message. If the message has
519 * a bunch of arpanet stuff in it, we may have to skin the name
520 * before returning it.
522 char *
523 nameof(struct message *mp, int reptype)
525 char *cp, *cp2;
527 cp = skin(name1(mp, reptype));
528 if (reptype != 0 || charcount(cp, '!') < 2)
529 return(cp);
530 cp2 = strrchr(cp, '!');
531 cp2--;
532 while (cp2 > cp && *cp2 != '!')
533 cp2--;
534 if (*cp2 == '!')
535 return(cp2 + 1);
536 return(cp);
540 * Start of a "comment".
541 * Ignore it.
543 char const *
544 skip_comment(char const *cp)
546 int nesting = 1;
548 for (; nesting > 0 && *cp; cp++) {
549 switch (*cp) {
550 case '\\':
551 if (cp[1])
552 cp++;
553 break;
554 case '(':
555 nesting++;
556 break;
557 case ')':
558 nesting--;
559 break;
562 return (cp);
566 * Return the start of a route-addr (address in angle brackets),
567 * if present.
569 char *
570 routeaddr(const char *name)
572 const char *np, *rp = NULL;
574 for (np = name; *np; np++) {
575 switch (*np) {
576 case '(':
577 np = skip_comment(&np[1]) - 1;
578 break;
579 case '"':
580 while (*np) {
581 if (*++np == '"')
582 break;
583 if (*np == '\\' && np[1])
584 np++;
586 break;
587 case '<':
588 rp = np;
589 break;
590 case '>':
591 return (char *)rp;
594 return NULL;
598 * Check if a name's address part contains invalid characters.
600 int
601 is_addr_invalid(struct name *np, int putmsg)
603 char cbuf[sizeof "'\\U12340'"], *name = np->n_name;
604 int f = np->n_flags, ok8bit = 1;
605 unsigned int c;
606 char const *fmt = "'\\x%02X'", *cs;
608 if ((f & NAME_ADDRSPEC_INVALID) == 0 || ! putmsg ||
609 (f & NAME_ADDRSPEC_ERR_EMPTY) != 0)
610 goto jleave;
612 if (f & NAME_ADDRSPEC_ERR_IDNA)
613 cs = tr(284, "Invalid domain name: \"%s\", character %s\n"),
614 fmt = "'\\U%04X'",
615 ok8bit = 0;
616 else if (f & NAME_ADDRSPEC_ERR_ATSEQ)
617 cs = tr(142, "\"%s\" contains invalid %s sequence\n");
618 else
619 cs = tr(143, "\"%s\" contains invalid character %s\n");
621 c = NAME_ADDRSPEC_ERR_GETWC(f);
622 if (ok8bit && c >= 040 && c <= 0177)
623 snprintf(cbuf, sizeof cbuf, "'%c'", c);
624 else
625 snprintf(cbuf, sizeof cbuf, fmt, c);
627 fprintf(stderr, cs, name, cbuf);
628 jleave:
629 return ((f & NAME_ADDRSPEC_INVALID) != 0);
633 * Returned the skinned n_name, use the cached value if available.
634 * Note well that it may *not* create a duplicate.
636 char *
637 skinned_name(struct name const*np) /* TODO !HAVE_ASSERTS legacy */
639 #ifdef HAVE_ASSERTS
640 assert(np->n_flags & NAME_SKINNED);
641 return (np->n_name);
642 #else
643 return ((np->n_flags & NAME_SKINNED) ? np->n_name : skin(np->n_name));
644 #endif
648 * Skin an arpa net address according to the RFC 822 interpretation
649 * of "host-phrase."
651 char *
652 skin(char *name)
654 struct addrguts ag;
656 if (name == NULL)
657 return (NULL);
659 (void)addrspec_with_guts(1, name, &ag);
660 name = ag.ag_skinned;
661 if ((ag.ag_n_flags & NAME_NAME_SALLOC) == 0)
662 name = savestrbuf(name, ag.ag_slen);
663 return (name);
667 * Convert the domain part of a skinned address to IDNA.
668 * If an error occurs before Unicode information is available, revert the IDNA
669 * error to a normal CHAR one so that the error message doesn't talk Unicode.
671 #ifdef USE_IDNA
672 static struct addrguts *
673 idna_apply(struct addrguts *agp)
675 char *idna_utf8, *idna_ascii, *cs;
676 uint32_t *idna_uni;
677 size_t sz, i;
678 int strict = (value("idna-strict-checks") != NULL);
680 sz = agp->ag_slen - agp->ag_sdom_start;
681 assert(sz > 0);
682 idna_utf8 = ac_alloc(sz + 1);
683 memcpy(idna_utf8, agp->ag_skinned + agp->ag_sdom_start, sz);
684 idna_utf8[sz] = '\0';
686 if (! utf8) {
687 char *tmp = stringprep_locale_to_utf8(idna_utf8);
688 ac_free(idna_utf8);
689 idna_utf8 = tmp;
690 if (idna_utf8 == NULL) {
691 agp->ag_n_flags ^= NAME_ADDRSPEC_ERR_IDNA |
692 NAME_ADDRSPEC_ERR_CHAR;
693 goto jleave;
697 if (idna_to_ascii_8z(idna_utf8, &idna_ascii,
698 strict ? IDNA_USE_STD3_ASCII_RULES : 0)
699 != IDNA_SUCCESS) {
700 agp->ag_n_flags ^= NAME_ADDRSPEC_ERR_IDNA |
701 NAME_ADDRSPEC_ERR_CHAR;
702 goto jleave1;
705 idna_uni = NULL;
706 if (! strict)
707 goto jset;
710 * Due to normalization that may have occurred we must convert back to
711 * be able to check for top level domain issues
713 if (idna_to_unicode_8z4z(idna_ascii, &idna_uni, 0) != IDNA_SUCCESS) {
714 agp->ag_n_flags ^= NAME_ADDRSPEC_ERR_IDNA |
715 NAME_ADDRSPEC_ERR_CHAR;
716 goto jleave2;
719 i = (size_t)tld_check_4z(idna_uni, &sz, NULL);
720 free(idna_uni);
721 if (i != TLD_SUCCESS) {
722 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags, NAME_ADDRSPEC_ERR_IDNA,
723 idna_uni[sz]);
724 goto jleave2;
727 jset: /* Replace the domain part of .ag_skinned with IDNA version */
728 sz = strlen(idna_ascii);
729 i = agp->ag_sdom_start;
730 cs = salloc(agp->ag_slen - i + sz + 1);
731 memcpy(cs, agp->ag_skinned, i);
732 memcpy(cs + i, idna_ascii, sz);
733 i += sz;
734 cs[i] = '\0';
736 agp->ag_skinned = cs;
737 agp->ag_slen = i;
738 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags,
739 NAME_NAME_SALLOC|NAME_SKINNED|NAME_IDNA, 0);
741 jleave2:
742 free(idna_ascii);
743 jleave1:
744 if (utf8)
745 ac_free(idna_utf8);
746 else
747 free(idna_utf8);
748 jleave:
749 return (agp);
751 #endif
754 * Classify and check a (possibly skinned) header body according to RFC
755 * *addr-spec* rules; if it (is assumed to has been) skinned it may however be
756 * also a file or a pipe command, so check that first, then.
757 * Otherwise perform content checking and isolate the domain part (for IDNA).
759 static int
760 addrspec_check(int skinned, struct addrguts *agp)
762 char *addr, *p, in_quote, in_domain, hadat;
763 union {char c; unsigned char u;} c;
764 #ifdef USE_IDNA
765 char use_idna = (value("idna-disable") == NULL);
766 #endif
768 agp->ag_n_flags |= NAME_ADDRSPEC_CHECKED;
769 addr = agp->ag_skinned;
771 if (agp->ag_iaddr_end <= agp->ag_iaddr_start) {
772 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags, NAME_ADDRSPEC_ERR_EMPTY,
774 goto jleave;
777 /* If the field is not a recipient, it cannot be a file or a pipe */
778 if (! skinned)
779 goto jaddr_check;
782 * Excerpt from nail.1:
784 * Recipient address specifications
785 * The rules are: Any name which starts with a `|' character specifies
786 * a pipe, the command string following the `|' is executed and
787 * the message is sent to its standard input; any other name which
788 * contains a `@' character is treated as a mail address; any other
789 * name which starts with a `+' character specifies a folder name; any
790 * other name which contains a `/' character but no `!' or `%'
791 * character before also specifies a folder name; what remains is
792 * treated as a mail address.
794 if (*addr == '|') {
795 agp->ag_n_flags |= NAME_ADDRSPEC_ISPIPE;
796 goto jleave;
798 if (memchr(addr, '@', agp->ag_slen) == NULL) {
799 if (*addr == '+')
800 goto jisfile;
801 for (p = addr; (c.c = *p); ++p) {
802 if (c.c == '!' || c.c == '%')
803 break;
804 if (c.c == '/') {
805 jisfile: agp->ag_n_flags |= NAME_ADDRSPEC_ISFILE;
806 goto jleave;
811 jaddr_check:
812 in_quote = in_domain = hadat = 0;
814 for (p = addr; (c.c = *p++) != '\0';) {
815 if (c.c == '"') {
816 in_quote = ! in_quote;
817 } else if (c.u < 040 || c.u >= 0177) {
818 #ifdef USE_IDNA
819 if (in_domain && use_idna) {
820 if (use_idna == 1)
821 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags,
822 NAME_ADDRSPEC_ERR_IDNA, c.u);
823 use_idna = 2;
824 } else
825 #endif
826 break;
827 } else if (in_domain == 2) {
828 if ((c.c == ']' && *p != '\0') || c.c == '\\' ||
829 whitechar(c.c))
830 break;
831 } else if (in_quote && in_domain == 0) {
832 /*EMPTY*/;
833 } else if (c.c == '\\' && *p != '\0') {
834 ++p;
835 } else if (c.c == '@') {
836 if (hadat++) {
837 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags,
838 NAME_ADDRSPEC_ERR_ATSEQ, c.u);
839 goto jleave;
841 agp->ag_sdom_start = (size_t)(p - addr);
842 in_domain = (*p == '[') ? 2 : 1;
843 continue;
844 } else if (c.c == '(' || c.c == ')' ||
845 c.c == '<' || c.c == '>' ||
846 c.c == ',' || c.c == ';' || c.c == ':' ||
847 c.c == '\\' || c.c == '[' || c.c == ']')
848 break;
849 hadat = 0;
852 if (c.c != '\0') {
853 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags, NAME_ADDRSPEC_ERR_CHAR,
854 c.u);
855 goto jleave;
858 #ifdef USE_IDNA
859 if (use_idna == 2)
860 agp = idna_apply(agp);
861 #endif
863 jleave:
864 return ((agp->ag_n_flags & NAME_ADDRSPEC_INVALID) != 0);
868 * Skin *name* and extract the *addr-spec* according to RFC 5322. TODO 822:5322
869 * Store the result in .ag_skinned and also fill in those .ag_ fields that have
870 * actually been seen.
871 * Return 0 if something good has been parsed, 1 if fun didn't exactly know how
872 * to deal with the input, or if that was plain invalid.
875 addrspec_with_guts(int doskin, char const *name, struct addrguts *agp)
877 char const *cp;
878 char *cp2, *bufend, *nbuf, c;
879 int gotlt, lastsp;
881 memset(agp, 0, sizeof *agp);
883 if ((agp->ag_input = name) == NULL || /* XXX ever? */
884 (agp->ag_ilen = strlen(name)) == 0) {
885 agp->ag_skinned = ""; /* NAME_SALLOC not set */
886 agp->ag_slen = 0;
887 agp->ag_n_flags |= NAME_ADDRSPEC_CHECKED;
888 NAME_ADDRSPEC_ERR_SET(agp->ag_n_flags, NAME_ADDRSPEC_ERR_EMPTY,
890 return (1);
893 if (! doskin || ! anyof(name, "(< ")) {
894 /*agp->ag_iaddr_start = 0;*/
895 agp->ag_iaddr_end = agp->ag_ilen - 1;
896 agp->ag_skinned = (char*)name; /* XXX (NAME_SALLOC not set) */
897 agp->ag_slen = agp->ag_ilen;
898 agp->ag_n_flags = NAME_SKINNED;
899 return (addrspec_check(doskin, agp));
902 /* Something makes us think we have to perform the skin operation */
903 nbuf = ac_alloc(agp->ag_ilen + 1);
904 /*agp->ag_iaddr_start = 0;*/
905 cp2 = bufend = nbuf;
906 gotlt = lastsp = 0;
908 for (cp = name; (c = *cp++) != '\0'; ) {
909 switch (c) {
910 case '(':
911 cp = skip_comment(cp);
912 lastsp = 0;
913 break;
915 case '"':
917 * Start of a "quoted-string".
918 * Copy it in its entirety.
919 * XXX RFC: quotes are "semantically invisible"
920 * XXX But it was explicitly added (Changelog.Heirloom,
921 * XXX [9.23] released 11/15/00, "Do not remove quotes
922 * XXX when skinning names"? No more info..
924 *cp2++ = c;
925 while ((c = *cp) != '\0') {
926 cp++;
927 if (c == '"') {
928 *cp2++ = c;
929 break;
931 if (c != '\\')
932 *cp2++ = c;
933 else if ((c = *cp) != '\0') {
934 *cp2++ = c;
935 cp++;
938 lastsp = 0;
939 break;
941 case ' ':
942 if (cp[0] == 'a' && cp[1] == 't' && cp[2] == ' ')
943 cp += 3, *cp2++ = '@';
944 else
945 if (cp[0] == '@' && cp[1] == ' ')
946 cp += 2, *cp2++ = '@';
947 else
948 lastsp = 1;
949 break;
951 case '<':
952 agp->ag_iaddr_start = (size_t)(cp - name);
953 cp2 = bufend;
954 gotlt++;
955 lastsp = 0;
956 break;
958 case '>':
959 if (gotlt) {
960 /* (addrspec_check() verifies these later!) */
961 agp->ag_iaddr_end = (size_t)(cp - 1 - name);
962 gotlt = 0;
963 while ((c = *cp) != '\0' && c != ',') {
964 cp++;
965 if (c == '(')
966 cp = skip_comment(cp);
967 else if (c == '"')
968 while ((c = *cp) != '\0') {
969 cp++;
970 if (c == '"')
971 break;
972 if (c == '\\' && *cp)
973 cp++;
976 lastsp = 0;
977 break;
979 /* FALLTRHOUGH */
981 default:
982 if (lastsp) {
983 lastsp = 0;
984 *cp2++ = ' ';
986 *cp2++ = c;
987 if (c == ',' && ! gotlt) {
988 *cp2++ = ' ';
989 for (; *cp == ' '; ++cp)
991 lastsp = 0;
992 bufend = cp2;
996 agp->ag_slen = (size_t)(cp2 - nbuf);
997 if (agp->ag_iaddr_end == 0)
998 agp->ag_iaddr_end = agp->ag_iaddr_start + agp->ag_slen;
1000 agp->ag_skinned = savestrbuf(nbuf, agp->ag_slen);
1001 ac_free(nbuf);
1002 agp->ag_n_flags = NAME_NAME_SALLOC | NAME_SKINNED;
1003 return (addrspec_check(doskin, agp));
1007 * Fetch the real name from an internet mail address field.
1009 char *
1010 realname(char *name)
1012 char const *cp, *cq, *cstart = NULL, *cend = NULL;
1013 char *rname, *rp;
1014 struct str in, out;
1015 int quoted, good, nogood;
1017 if (name == NULL)
1018 return NULL;
1019 for (cp = (char*)name; *cp; cp++) {
1020 switch (*cp) {
1021 case '(':
1022 if (cstart)
1024 * More than one comment in address, doesn't
1025 * make sense to display it without context.
1026 * Return the entire field,
1028 return mime_fromaddr(name);
1029 cstart = cp++;
1030 cp = skip_comment(cp);
1031 cend = cp--;
1032 if (cend <= cstart)
1033 cend = cstart = NULL;
1034 break;
1035 case '"':
1036 while (*cp) {
1037 if (*++cp == '"')
1038 break;
1039 if (*cp == '\\' && cp[1])
1040 cp++;
1042 break;
1043 case '<':
1044 if (cp > name) {
1045 cstart = name;
1046 cend = cp;
1048 break;
1049 case ',':
1051 * More than one address. Just use the first one.
1053 goto brk;
1056 brk: if (cstart == NULL) {
1057 if (*name == '<')
1059 * If name contains only a route-addr, the
1060 * surrounding angle brackets don't serve any
1061 * useful purpose when displaying, so they
1062 * are removed.
1064 return prstr(skin(name));
1065 return mime_fromaddr(name);
1067 rp = rname = ac_alloc(cend - cstart + 1);
1069 * Strip quotes. Note that quotes that appear within a MIME-
1070 * encoded word are not stripped. The idea is to strip only
1071 * syntactical relevant things (but this is not necessarily
1072 * the most sensible way in practice).
1074 quoted = 0;
1075 for (cp = cstart; cp < cend; cp++) {
1076 if (*cp == '(' && !quoted) {
1077 cq = skip_comment(++cp);
1078 if (--cq > cend)
1079 cq = cend;
1080 while (cp < cq) {
1081 if (*cp == '\\' && &cp[1] < cq)
1082 cp++;
1083 *rp++ = *cp++;
1085 } else if (*cp == '\\' && &cp[1] < cend)
1086 *rp++ = *++cp;
1087 else if (*cp == '"') {
1088 quoted = !quoted;
1089 continue;
1090 } else
1091 *rp++ = *cp;
1093 *rp = '\0';
1094 in.s = rname;
1095 in.l = rp - rname;
1096 mime_fromhdr(&in, &out, TD_ISPR|TD_ICONV);
1097 ac_free(rname);
1098 rname = savestr(out.s);
1099 free(out.s);
1100 while (blankchar(*rname & 0377))
1101 rname++;
1102 for (rp = rname; *rp; rp++);
1103 while (--rp >= rname && blankchar(*rp & 0377))
1104 *rp = '\0';
1105 if (rp == rname)
1106 return mime_fromaddr(name);
1108 * mime_fromhdr() has converted all nonprintable characters to
1109 * question marks now. These and blanks are considered uninteresting;
1110 * if the displayed part of the real name contains more than 25% of
1111 * them, it is probably better to display the plain email address
1112 * instead.
1114 good = 0;
1115 nogood = 0;
1116 for (rp = rname; *rp && rp < &rname[20]; rp++)
1117 if (*rp == '?' || blankchar(*rp & 0377))
1118 nogood++;
1119 else
1120 good++;
1121 if (good*3 < nogood)
1122 return prstr(skin(name));
1123 return rname;
1127 * Fetch the sender's name from the passed message.
1128 * Reptype can be
1129 * 0 -- get sender's name for display purposes
1130 * 1 -- get sender's name for reply
1131 * 2 -- get sender's name for Reply
1133 char *
1134 name1(struct message *mp, int reptype)
1136 char *namebuf;
1137 size_t namesize;
1138 char *linebuf = NULL;
1139 size_t linesize = 0;
1140 char *cp, *cp2;
1141 FILE *ibuf;
1142 int first = 1;
1144 if ((cp = hfield1("from", mp)) != NULL && *cp != '\0')
1145 return cp;
1146 if (reptype == 0 && (cp = hfield1("sender", mp)) != NULL &&
1147 *cp != '\0')
1148 return cp;
1149 namebuf = smalloc(namesize = 1);
1150 namebuf[0] = 0;
1151 if (mp->m_flag & MNOFROM)
1152 goto out;
1153 if ((ibuf = setinput(&mb, mp, NEED_HEADER)) == NULL)
1154 goto out;
1155 if (readline(ibuf, &linebuf, &linesize) < 0)
1156 goto out;
1157 newname:
1158 if (namesize <= linesize)
1159 namebuf = srealloc(namebuf, namesize = linesize + 1);
1160 for (cp = linebuf; *cp && *cp != ' '; cp++)
1162 for (; blankchar(*cp & 0377); cp++);
1163 for (cp2 = &namebuf[strlen(namebuf)];
1164 *cp && !blankchar(*cp & 0377) && cp2 < namebuf + namesize - 1;)
1165 *cp2++ = *cp++;
1166 *cp2 = '\0';
1167 if (readline(ibuf, &linebuf, &linesize) < 0)
1168 goto out;
1169 if ((cp = strchr(linebuf, 'F')) == NULL)
1170 goto out;
1171 if (strncmp(cp, "From", 4) != 0)
1172 goto out;
1173 if (namesize <= linesize)
1174 namebuf = srealloc(namebuf, namesize = linesize + 1);
1175 while ((cp = strchr(cp, 'r')) != NULL) {
1176 if (strncmp(cp, "remote", 6) == 0) {
1177 if ((cp = strchr(cp, 'f')) == NULL)
1178 break;
1179 if (strncmp(cp, "from", 4) != 0)
1180 break;
1181 if ((cp = strchr(cp, ' ')) == NULL)
1182 break;
1183 cp++;
1184 if (first) {
1185 strncpy(namebuf, cp, namesize);
1186 first = 0;
1187 } else {
1188 cp2=strrchr(namebuf, '!')+1;
1189 strncpy(cp2, cp, (namebuf+namesize)-cp2);
1191 namebuf[namesize-2]='\0';
1192 strcat(namebuf, "!");
1193 goto newname;
1195 cp++;
1197 out:
1198 if (*namebuf != '\0' || ((cp = hfield1("return-path", mp))) == NULL ||
1199 *cp == '\0')
1200 cp = savestr(namebuf);
1201 if (linebuf)
1202 free(linebuf);
1203 free(namebuf);
1204 return cp;
1207 static int
1208 msgidnextc(const char **cp, int *status)
1210 int c;
1212 for (;;) {
1213 if (*status & 01) {
1214 if (**cp == '"') {
1215 *status &= ~01;
1216 (*cp)++;
1217 continue;
1219 if (**cp == '\\') {
1220 (*cp)++;
1221 if (**cp == '\0')
1222 goto eof;
1224 goto dfl;
1226 switch (**cp) {
1227 case '(':
1228 *cp = skip_comment(&(*cp)[1]);
1229 continue;
1230 case '>':
1231 case '\0':
1232 eof:
1233 return '\0';
1234 case '"':
1235 (*cp)++;
1236 *status |= 01;
1237 continue;
1238 case '@':
1239 *status |= 02;
1240 /*FALLTHRU*/
1241 default:
1242 dfl:
1243 c = *(*cp)++ & 0377;
1244 return *status & 02 ? lowerconv(c) : c;
1249 int
1250 msgidcmp(const char *s1, const char *s2)
1252 int q1 = 0, q2 = 0;
1253 int c1, c2;
1255 do {
1256 c1 = msgidnextc(&s1, &q1);
1257 c2 = msgidnextc(&s2, &q2);
1258 if (c1 != c2)
1259 return c1 - c2;
1260 } while (c1 && c2);
1261 return c1 - c2;
1265 * Count the occurances of c in str
1267 static int
1268 charcount(char *str, int c)
1270 char *cp;
1271 int i;
1273 for (i = 0, cp = str; *cp; cp++)
1274 if (*cp == c)
1275 i++;
1276 return(i);
1280 * See if the given header field is supposed to be ignored.
1283 is_ign(char *field, size_t fieldlen, struct ignoretab ignore[2])
1285 char *realfld;
1286 int ret;
1288 if (ignore == NULL)
1289 return 0;
1290 if (ignore == allignore)
1291 return 1;
1293 * Lower-case the string, so that "Status" and "status"
1294 * will hash to the same place.
1296 realfld = ac_alloc(fieldlen + 1);
1297 i_strcpy(realfld, field, fieldlen + 1);
1298 if (ignore[1].i_count > 0)
1299 ret = !member(realfld, ignore + 1);
1300 else
1301 ret = member(realfld, ignore);
1302 ac_free(realfld);
1303 return ret;
1306 int
1307 member(char *realfield, struct ignoretab *table)
1309 struct ignore *igp;
1311 for (igp = table->i_head[hash(realfield)]; igp != 0; igp = igp->i_link)
1312 if (*igp->i_field == *realfield &&
1313 strcmp(igp->i_field, realfield) == 0)
1314 return (1);
1315 return (0);
1319 * Fake Sender for From_ lines if missing, e. g. with POP3.
1321 char *
1322 fakefrom(struct message *mp)
1324 char *name;
1326 if (((name = skin(hfield1("return-path", mp))) == NULL ||
1327 *name == '\0' ) &&
1328 ((name = skin(hfield1("from", mp))) == NULL ||
1329 *name == '\0'))
1330 name = "-";
1331 return name;
1334 char *
1335 fakedate(time_t t)
1337 char *cp, *cq;
1339 cp = ctime(&t);
1340 for (cq = cp; *cq && *cq != '\n'; cq++);
1341 *cq = '\0';
1342 return savestr(cp);
1345 char const *
1346 nexttoken(char const *cp)
1348 for (;;) {
1349 if (*cp == '\0')
1350 return NULL;
1351 if (*cp == '(') {
1352 int nesting = 0;
1354 while (*cp != '\0') {
1355 switch (*cp++) {
1356 case '(':
1357 nesting++;
1358 break;
1359 case ')':
1360 nesting--;
1361 break;
1363 if (nesting <= 0)
1364 break;
1366 } else if (blankchar(*cp & 0377) || *cp == ',')
1367 cp++;
1368 else
1369 break;
1371 return cp;
1375 * From username Fri Jan 2 20:13:51 2004
1376 * | | | | |
1377 * 0 5 10 15 20
1379 time_t
1380 unixtime(char const *from)
1382 char const *fp;
1383 char *xp;
1384 time_t t;
1385 int i, year, month, day, hour, minute, second;
1386 int tzdiff;
1387 struct tm *tmptr;
1389 for (fp = from; *fp && *fp != '\n'; fp++);
1390 fp -= 24;
1391 if (fp - from < 7)
1392 goto invalid;
1393 if (fp[3] != ' ')
1394 goto invalid;
1395 for (i = 0; month_names[i]; i++)
1396 if (strncmp(&fp[4], month_names[i], 3) == 0)
1397 break;
1398 if (month_names[i] == 0)
1399 goto invalid;
1400 month = i + 1;
1401 if (fp[7] != ' ')
1402 goto invalid;
1403 day = strtol(&fp[8], &xp, 10);
1404 if (*xp != ' ' || xp != &fp[10])
1405 goto invalid;
1406 hour = strtol(&fp[11], &xp, 10);
1407 if (*xp != ':' || xp != &fp[13])
1408 goto invalid;
1409 minute = strtol(&fp[14], &xp, 10);
1410 if (*xp != ':' || xp != &fp[16])
1411 goto invalid;
1412 second = strtol(&fp[17], &xp, 10);
1413 if (*xp != ' ' || xp != &fp[19])
1414 goto invalid;
1415 year = strtol(&fp[20], &xp, 10);
1416 if (xp != &fp[24])
1417 goto invalid;
1418 if ((t = combinetime(year, month, day, hour, minute, second)) ==
1419 (time_t)-1)
1420 goto invalid;
1421 tzdiff = t - mktime(gmtime(&t));
1422 tmptr = localtime(&t);
1423 if (tmptr->tm_isdst > 0)
1424 tzdiff += 3600;
1425 t -= tzdiff;
1426 return t;
1427 invalid:
1428 time(&t);
1429 return t;
1432 time_t
1433 rfctime(char const *date)
1435 char const *cp = date;
1436 char *x;
1437 time_t t;
1438 int i, year, month, day, hour, minute, second;
1440 if ((cp = nexttoken(cp)) == NULL)
1441 goto invalid;
1442 if (alphachar(cp[0] & 0377) && alphachar(cp[1] & 0377) &&
1443 alphachar(cp[2] & 0377) && cp[3] == ',') {
1444 if ((cp = nexttoken(&cp[4])) == NULL)
1445 goto invalid;
1447 day = strtol(cp, &x, 10);
1448 if ((cp = nexttoken(x)) == NULL)
1449 goto invalid;
1450 for (i = 0; month_names[i]; i++) {
1451 if (strncmp(cp, month_names[i], 3) == 0)
1452 break;
1454 if (month_names[i] == NULL)
1455 goto invalid;
1456 month = i + 1;
1457 if ((cp = nexttoken(&cp[3])) == NULL)
1458 goto invalid;
1459 year = strtol(cp, &x, 10);
1460 if ((cp = nexttoken(x)) == NULL)
1461 goto invalid;
1462 hour = strtol(cp, &x, 10);
1463 if (*x != ':')
1464 goto invalid;
1465 cp = &x[1];
1466 minute = strtol(cp, &x, 10);
1467 if (*x == ':') {
1468 cp = &x[1];
1469 second = strtol(cp, &x, 10);
1470 } else
1471 second = 0;
1472 if ((t = combinetime(year, month, day, hour, minute, second)) ==
1473 (time_t)-1)
1474 goto invalid;
1475 if ((cp = nexttoken(x)) != NULL) {
1476 int sign = -1;
1477 char buf[3];
1479 switch (*cp) {
1480 case '-':
1481 sign = 1;
1482 /*FALLTHRU*/
1483 case '+':
1484 cp++;
1486 if (digitchar(cp[0] & 0377) && digitchar(cp[1] & 0377) &&
1487 digitchar(cp[2] & 0377) &&
1488 digitchar(cp[3] & 0377)) {
1489 buf[2] = '\0';
1490 buf[0] = cp[0];
1491 buf[1] = cp[1];
1492 t += strtol(buf, NULL, 10) * sign * 3600;
1493 buf[0] = cp[2];
1494 buf[1] = cp[3];
1495 t += strtol(buf, NULL, 10) * sign * 60;
1498 return t;
1499 invalid:
1500 return 0;
1503 #define leapyear(year) ((year % 100 ? year : year / 100) % 4 == 0)
1505 time_t
1506 combinetime(int year, int month, int day, int hour, int minute, int second)
1508 time_t t;
1510 if (second < 0 || minute < 0 || hour < 0 || day < 1)
1511 return -1;
1512 t = second + minute * 60 + hour * 3600 + (day - 1) * 86400;
1513 if (year < 70)
1514 year += 2000;
1515 else if (year < 1900)
1516 year += 1900;
1517 if (month > 1)
1518 t += 86400 * 31;
1519 if (month > 2)
1520 t += 86400 * (leapyear(year) ? 29 : 28);
1521 if (month > 3)
1522 t += 86400 * 31;
1523 if (month > 4)
1524 t += 86400 * 30;
1525 if (month > 5)
1526 t += 86400 * 31;
1527 if (month > 6)
1528 t += 86400 * 30;
1529 if (month > 7)
1530 t += 86400 * 31;
1531 if (month > 8)
1532 t += 86400 * 31;
1533 if (month > 9)
1534 t += 86400 * 30;
1535 if (month > 10)
1536 t += 86400 * 31;
1537 if (month > 11)
1538 t += 86400 * 30;
1539 year -= 1900;
1540 t += (year - 70) * 31536000 + ((year - 69) / 4) * 86400 -
1541 ((year - 1) / 100) * 86400 + ((year + 299) / 400) * 86400;
1542 return t;
1545 void
1546 substdate(struct message *m)
1548 char const *cp;
1549 time_t now;
1552 * Determine the date to print in faked 'From ' lines. This is
1553 * traditionally the date the message was written to the mail
1554 * file. Try to determine this using RFC message header fields,
1555 * or fall back to current time.
1557 time(&now);
1558 if ((cp = hfield1("received", m)) != NULL) {
1559 while ((cp = nexttoken(cp)) != NULL && *cp != ';') {
1561 cp++;
1562 while (alnumchar(*cp & 0377));
1564 if (cp && *++cp)
1565 m->m_time = rfctime(cp);
1567 if (m->m_time == 0 || m->m_time > now)
1568 if ((cp = hfield1("date", m)) != NULL)
1569 m->m_time = rfctime(cp);
1570 if (m->m_time == 0 || m->m_time > now)
1571 m->m_time = now;
1575 check_from_and_sender(struct name *fromfield, struct name *senderfield)
1577 if (fromfield && fromfield->n_flink && senderfield == NULL) {
1578 fprintf(stderr, "A Sender: field is required with multiple "
1579 "addresses in From: field.\n");
1580 return 1;
1582 if (senderfield && senderfield->n_flink) {
1583 fprintf(stderr, "The Sender: field may contain "
1584 "only one address.\n");
1585 return 2;
1587 return 0;
1590 char *
1591 getsender(struct message *mp)
1593 char *cp;
1594 struct name *np;
1596 if ((cp = hfield1("from", mp)) == NULL ||
1597 (np = sextract(cp, GEXTRA|GSKIN)) == NULL)
1598 return NULL;
1599 return np->n_flink != NULL ? skin(hfield1("sender", mp)) : np->n_name;